diff --git a/addons/web/Gruntfile.js b/addons/web/Gruntfile.js index 372feb2d9c0..d1e897d8e20 100644 --- a/addons/web/Gruntfile.js +++ b/addons/web/Gruntfile.js @@ -2,7 +2,7 @@ module.exports = function(grunt) { grunt.initConfig({ jshint: { - files: ['static/src/**/*.js'], + files: ['static/src/**/*.js', 'static/test/**/*.js'], options: { sub: true, //[] instead of . evil: true, //eval diff --git a/addons/web/static/test/Widget.js b/addons/web/static/test/Widget.js index 44b7cfe0dfa..858044f8cb0 100644 --- a/addons/web/static/test/Widget.js +++ b/addons/web/static/test/Widget.js @@ -7,7 +7,7 @@ openerp.testing.section('Widget.proxy', { this.executed = true; } }); - var w = new W; + var w = new W(); var fn = w.proxy('exec'); fn(); ok(w.executed, 'should execute the named method in the right context'); @@ -18,7 +18,7 @@ openerp.testing.section('Widget.proxy', { this.executed = arg; } }); - var w = new W; + var w = new W(); var fn = w.proxy('exec'); fn(42); ok(w.executed, "should execute the named method in the right context"); @@ -32,7 +32,7 @@ openerp.testing.section('Widget.proxy', { this.executed = 1; } }); - var w = new W; + var w = new W(); var fn = w.proxy('exec'); W.include({ exec: function () { this.executed = 2; } @@ -43,14 +43,14 @@ openerp.testing.section('Widget.proxy', { }); test('(Function)', function (instance) { - var w = new (instance.web.Widget.extend({ })); + var w = new (instance.web.Widget.extend({ }))(); var fn = w.proxy(function () { this.executed = true; }); fn(); ok(w.executed, "should set the function's context (like Function#bind)"); }); test('(Function)(*args)', function (instance) { - var w = new (instance.web.Widget.extend({ })); + var w = new (instance.web.Widget.extend({ }))(); var fn = w.proxy(function (arg) { this.executed = arg; }); fn(42); @@ -79,7 +79,7 @@ openerp.testing.section('Widget.renderElement', { } }, function (test) { test('no template, default', function (instance) { - var w = new (instance.web.Widget.extend({ })); + var w = new (instance.web.Widget.extend({ }))(); var $original = w.$el; ok($original, "should initially have a root element"); @@ -96,7 +96,7 @@ openerp.testing.section('Widget.renderElement', { test('no template, custom tag', function (instance) { var w = new (instance.web.Widget.extend({ tagName: 'ul' - })); + }))(); w.renderElement(); equal(w.el.nodeName, 'UL', "should have generated the custom element tag"); @@ -104,7 +104,7 @@ openerp.testing.section('Widget.renderElement', { test('no template, @id', function (instance) { var w = new (instance.web.Widget.extend({ id: 'foo' - })); + }))(); w.renderElement(); equal(w.el.attributes.length, 1, "should have one attribute"); @@ -114,7 +114,7 @@ openerp.testing.section('Widget.renderElement', { test('no template, @className', function (instance) { var w = new (instance.web.Widget.extend({ className: 'oe_some_class' - })); + }))(); w.renderElement(); equal(w.el.className, 'oe_some_class', "should have the right property"); @@ -129,7 +129,7 @@ openerp.testing.section('Widget.renderElement', { 'clark': 'gable', 'spoiler': 'snape kills dumbledore' } - })); + }))(); w.renderElement(); equal(w.el.attributes.length, 5, "should have all the specified attributes"); @@ -150,7 +150,7 @@ openerp.testing.section('Widget.renderElement', { test('template', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' - })); + }))(); w.renderElement(); equal(w.el.nodeName, 'OL'); @@ -160,7 +160,7 @@ openerp.testing.section('Widget.renderElement', { test('repeated', { asserts: 4 }, function (instance, $fix) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template-value' - })); + }))(); w.value = 42; return w.appendTo($fix) .done(function () { @@ -194,7 +194,7 @@ openerp.testing.section('Widget.$', { test('basic-alias', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' - })); + }))(); w.renderElement(); ok(w.$('li:eq(3)').is(w.$el.find('li:eq(3)')), @@ -226,13 +226,13 @@ openerp.testing.section('Widget.events', { events: { 'click': function () { a[0] = true; - strictEqual(this, w, "should trigger events in widget") + strictEqual(this, w, "should trigger events in widget"); }, 'click li.class-3': 'class3', 'change input': function () { a[2] = true; } }, class3: function () { a[1] = true; } - })); + }))(); w.renderElement(); w.$el.click(); @@ -248,9 +248,9 @@ openerp.testing.section('Widget.events', { var w = new (instance.web.Widget.extend({ template: 'test.widget.template', events: { 'click li': function () { clicked = true; } } - })); + }))(); w.renderElement(); - w.$el.on('click', 'li', function () { newclicked = true }); + w.$el.on('click', 'li', function () { newclicked = true; }); w.$('li').click(); ok(clicked, "should trigger bound events"); diff --git a/addons/web/static/test/data.js b/addons/web/static/test/data.js new file mode 100644 index 00000000000..d7a957ad920 --- /dev/null +++ b/addons/web/static/test/data.js @@ -0,0 +1,76 @@ +openerp.testing.section('data.model.group_by', { + rpc: 'mock', + dependencies: ['web.data'], +}, function (test) { + var group_result = [{ + bar: 3, bar_count: 5, __context: {}, __domain: [['bar', '=', 3]], + }, { + bar: 5, bar_count: 3, __context: {}, __domain: [['bar', '=', 5]], + }, { + bar: 8, bar_count: 0, __context: {}, __domain: [['bar', '=', 8]], + }]; + test('basic', {asserts: 7}, function (instance, $fix, mock) { + var m = new instance.web.Model('foo'); + mock('foo:read_group', function (args, kwargs) { + deepEqual(kwargs.fields, ['bar'], + "should read grouping field"); + deepEqual(kwargs.groupby, ['bar'], + "should have single grouping field"); + return group_result; + }); + mock('/web/dataset/search_read', function (args) { + deepEqual(args.params.domain, [['bar', '=', 3]], + "should have domain matching that of group_by result"); + return {records: [ + {bar: 3, id: 1}, + {bar: 3, id: 2}, + {bar: 3, id: 4}, + {bar: 3, id: 8}, + {bar: 3, id: 16} + ], length: 5}; + }); + + return m.query().group_by('bar') + .then(function (groups) { + ok(groups, "should have data"); + equal(groups.length, 3, "should have three results"); + var first = groups[0]; + ok(first.attributes.has_children, "should have children"); + return first.query().all(); + }).done(function (first) { + equal(first.length, 5, "should have 5 records"); + }); + }); + test('noleaf', {asserts: 5}, function (instance, $fix, mock) { + var m = new instance.web.Model('foo', {group_by_no_leaf: true}); + mock('foo:read_group', function (args, kwargs) { + deepEqual(kwargs.fields, ['bar'], + "should read grouping field"); + deepEqual(kwargs.groupby, ['bar'], + "should have single grouping field"); + + return group_result; + }); + return m.query().group_by('bar') + .then(function (groups) { + ok(groups, "should have data"); + equal(groups.length, 3, "should have three results"); + ok(!groups[0].attributes.has_children, + "should not have children because no_leaf"); + }); + }); + test('nogroup', {rpc: false}, function (instance, $f, mock) { + var m = new instance.web.Model('foo'); + strictEqual(m.query().group_by(), null, "should not group"); + }); + test('empty.noleaf', {asserts: 1}, function (instance, $f, mock) { + var m = new instance.web.Model('foo', {group_by_no_leaf: true}); + mock('foo:read_group', function (args, kwargs) { + return [{__context: [], __domain: []}]; + }); + return m.query().group_by().done(function (groups) { + strictEqual(groups.length, 1, + "should generate a single fake-ish group"); + }); + }); +}); diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 8e9fb682e0c..405589e1be8 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -9,9 +9,9 @@ openerp.testing.section('eval.types', { return function (expr, func, message) { // evaluate expr between two calls to new Date(), and check that // the result is between the transformed dates - var d0 = new Date; + var d0 = new Date(); var result = py.eval(expr, context); - var d1 = new Date; + var d1 = new Date(); ok(func(d0) <= result && result <= func(d1), message); }; }; @@ -118,7 +118,7 @@ openerp.testing.section('eval.types', { // Issue #11576 eq('td(999999999, 86399, 999999) - td(999999999, 86399, 999998)', 'td(0, 0, 1)'); eq('td(999999999, 1, 1) - td(999999999, 1, 0)', - 'td(0, 0, 1)') + 'td(0, 0, 1)'); }); test('timedelta.test_basic_attributes', function (instance) { var ctx = instance.web.pyeval.context(); @@ -264,7 +264,7 @@ openerp.testing.section('eval.types', { py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')", instance.web.pyeval.context()), "2012-02-14 23:59:59"); - }) + }); }); openerp.testing.section('eval.edc', { dependencies: ['web.data'], diff --git a/addons/web/static/test/formats.js b/addons/web/static/test/formats.js index ac602e4b693..fe8388c8929 100644 --- a/addons/web/static/test/formats.js +++ b/addons/web/static/test/formats.js @@ -126,8 +126,8 @@ openerp.testing.section('web-formats', { var str = "134,112.1234"; var val = instance.web.parse_value(str, {type:"float"}); equal(val, 134112.1234); - var str = "-134,112.1234"; - var val = instance.web.parse_value(str, {type:"float"}); + str = "-134,112.1234"; + val = instance.web.parse_value(str, {type:"float"}); equal(val, -134112.1234); _.extend(instance.web._t.database.parameters, { decimal_point: ',', diff --git a/addons/web/static/test/list-editable.js b/addons/web/static/test/list-editable.js index 89907aa16ee..20a14cb24d5 100644 --- a/addons/web/static/test/list-editable.js +++ b/addons/web/static/test/list-editable.js @@ -49,7 +49,7 @@ openerp.testing.section('editor', { readonly: field.readonly }) } - } + }; }); return { arch: { @@ -108,7 +108,7 @@ openerp.testing.section('editor', { .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); equal(record.id, 42, "should have newly created id"); - }) + }); }); test('toggle-edition-cancel', { asserts: 2 }, function (instance, $fix) { var e = new instance.web.list.Editor({ @@ -131,7 +131,7 @@ openerp.testing.section('editor', { .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); ok(!record.id, "should have no id"); - }) + }); }); test('toggle-save-required', { asserts: 2, diff --git a/addons/web/static/test/list-utils.js b/addons/web/static/test/list-utils.js index 84661138315..01777cd5a18 100644 --- a/addons/web/static/test/list-utils.js +++ b/addons/web/static/test/list-utils.js @@ -7,7 +7,7 @@ openerp.testing.section('list.events', { } function Cls() {} Cls.prototype = o; - return new Cls; + return new Cls(); }; test('Simple event triggering', function (instance) { var e = create(instance.web.list.Events), passed = false; @@ -23,9 +23,9 @@ openerp.testing.section('list.events', { }); test('Propagate trigger params', function (instance) { var e = create(instance.web.list.Events), p = false; - e.bind(null, function (_, param) { p = param }); + e.bind(null, function (_, param) { p = param; }); e.trigger('foo', true); - strictEqual(p, true) + strictEqual(p, true); }); test('Bind multiple callbacks', function (instance) { var e = create(instance.web.list.Events), count; @@ -53,7 +53,7 @@ openerp.testing.section('list.events', { method: function () { this.trigger('e'); } }); cls.include(instance.web.list.Events); - var i = new cls, triggered = false; + var i = new cls(), triggered = false; i.bind('e', function () { triggered = true; }); i.method(); @@ -97,7 +97,7 @@ openerp.testing.section('list.records', { test('Change all the things', function (instance) { var r = new instance.web.list.Record(), changed = false, field; r.bind('change', function () { changed = true; }); - r.bind(null, function (e) { field = field || e.split(':')[1]}); + r.bind(null, function (e) { field = field || e.split(':')[1]; }); r.set('foo', 1); strictEqual(r.get('foo'), 1); ok(changed); diff --git a/addons/web/static/test/rpc.js b/addons/web/static/test/rpc.js index 85f562ef225..410c041633d 100644 --- a/addons/web/static/test/rpc.js +++ b/addons/web/static/test/rpc.js @@ -122,6 +122,6 @@ openerp.testing.section('misordered resolution managemeng', { ok(!fail2); done.resolve(); }, 400); - return $.when(d1, d2, done) + return $.when(d1, d2, done); }); }); diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index 5c02d1f16db..268fe4d1c0c 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -2,7 +2,7 @@ openerp.testing.section('search.query', { dependencies: ['web.search'] }, function (test) { test('Adding a facet to the query creates a facet and a value', function (instance) { - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); var field = {}; query.add({ category: 'Foo', @@ -16,7 +16,7 @@ openerp.testing.section('search.query', { deepEqual(facet.get('values'), [{label: 'Value', value: 3}]); }); test('Adding two facets', function (instance) { - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); query.add([ { category: 'Foo', field: {}, values: [{label: 'Value', value: 3}] }, { category: 'Bar', field: {}, values: [{label: 'Value 2', value: 4}] } @@ -27,7 +27,7 @@ openerp.testing.section('search.query', { equal(query.at(1).values.length, 1); }); test('If a facet already exists, add values to it', function (instance) { - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); var field = {}; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]}); @@ -40,18 +40,18 @@ openerp.testing.section('search.query', { ]); }); test('Facet being implicitly changed should trigger change, not add', function (instance) { - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); var field = {}, added = false, changed = false; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); query.on('add', function () { added = true; }) - .on('change', function () { changed = true }); + .on('change', function () { changed = true; }); query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]}); ok(!added, "query.add adding values to a facet should not trigger an add"); ok(changed, "query.add adding values to a facet should not trigger a change"); }); test('Toggling a facet, value which does not exist should add it', function (instance) { - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); var field = {}; query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -63,7 +63,7 @@ openerp.testing.section('search.query', { }); test('Toggling a facet which exists with a value which does not should add the value to the facet', function (instance) { var field = {}; - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); query.toggle({category: 'A', field: field, values: [{label: 'V2', value: 1}]}); @@ -77,7 +77,7 @@ openerp.testing.section('search.query', { }); test('Toggling a facet which exists with a value which does as well should remove the value from the facet', function (instance) { var field = {}; - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]}); @@ -92,7 +92,7 @@ openerp.testing.section('search.query', { }); test('Toggling off the last value of a facet should remove the facet', function (instance) { var field = {}; - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -101,7 +101,7 @@ openerp.testing.section('search.query', { }); test('Intermediate emptiness should not remove the facet', function (instance) { var field = {}; - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); query.toggle({category: 'A', field: field, values: [ @@ -118,7 +118,7 @@ openerp.testing.section('search.query', { }); test('Reseting with multiple facets should still work to load defaults', function (instance) { - var query = new instance.web.search.SearchQuery; + var query = new instance.web.search.SearchQuery(); var field = {}; query.reset([ {category: 'A', field: field, values: [{label: 'V1', value: 0}]}, @@ -129,7 +129,7 @@ openerp.testing.section('search.query', { deepEqual(query.at(0).get('values'), [ {label: 'V1', value: 0}, {label: 'V2', value: 1} - ]) + ]); }); }); @@ -346,7 +346,7 @@ openerp.testing.section('search.defaults', { {attrs: {name: 'dummy', string: 'Dummy'}}, {relation: 'dummy.model.name'}, view); - mock('dummy.model.name:name_get', function () { return [] }); + mock('dummy.model.name:name_get', function () { return []; }); return f.facet_for_defaults({dummy: id}) .done(function (facet) { ok(!facet, "an invalid m2o default should yield a non-facet"); @@ -358,9 +358,9 @@ openerp.testing.section('search.defaults', { {attrs: {name: 'dummy', string: 'Dummy'}}, {relation: 'dummy.model.name'}, view); - raises(function () { f.facet_for_defaults({dummy: [6, 7]}) }, + raises(function () { f.facet_for_defaults({dummy: [6, 7]}); }, "should not accept multiple default values"); - }) + }); }); openerp.testing.section('search.completions', { dependencies: ['web.search'], @@ -614,6 +614,59 @@ openerp.testing.section('search.completions', { {relation: 'dummy.model'}, view); return f.complete("bob"); }); + test('Integer: invalid', {asserts: 1}, function (instance) { + var view = {inputs: []}; + var f = new instance.web.search.IntegerField( + {attrs: {string: "Dummy"}}, {}, view); + return f.complete("qux") + .done(function (completions) { + ok(!completions, "non-number => no completion"); + }); + }); + test('Integer: non-zero', {asserts: 5}, function (instance) { + var view = {inputs: []}; + var f = new instance.web.search.IntegerField( + {attrs: {string: "Dummy"}}, {}, view); + return f.complete("-2") + .done(function (completions) { + equal(completions.length, 1, "number fields provide 1 completion only"); + var facet = new instance.web.search.Facet(completions[0].facet); + equal(facet.get('category'), f.attrs.string); + equal(facet.get('field'), f); + var value = facet.values.at(0); + equal(value.get('label'), "-2"); + equal(value.get('value'), -2); + }); + }); + test('Integer: zero', {asserts: 3}, function (instance) { + var view = {inputs: []}; + var f = new instance.web.search.IntegerField( + {attrs: {string: "Dummy"}}, {}, view); + return f.complete("0") + .done(function (completions) { + equal(completions.length, 1, "number fields provide 1 completion only"); + var facet = new instance.web.search.Facet(completions[0].facet); + var value = facet.values.at(0); + equal(value.get('label'), "0"); + equal(value.get('value'), 0); + }); + }); + test('Float: non-zero', {asserts: 5}, function (instance) { + var view = {inputs: []}; + var f = new instance.web.search.FloatField( + {attrs: {string: "Dummy"}}, {}, view); + return f.complete("42.37") + .done(function (completions) { + equal(completions.length, 1, "float fields provide 1 completion only"); + var facet = new instance.web.search.Facet(completions[0].facet); + equal(facet.get('category'), f.attrs.string); + equal(facet.get('field'), f); + var value = facet.values.at(0); + equal(value.get('label'), "42.37"); + equal(value.get('value'), 42.37); + }); + }); + }); openerp.testing.section('search.serialization', { dependencies: ['web.search'], @@ -651,7 +704,7 @@ openerp.testing.section('search.serialization', { ok(!got_groupby, "no facet, should not have fetched groupby"); ok(_(gs).isEmpty(), "groupby list should be empty"); - }) + }); }); test('London, calling', {asserts: 8}, function (instance, $fix) { var got_domain = false, got_context = false, got_groupby = false; @@ -686,7 +739,7 @@ openerp.testing.section('search.serialization', { ok(got_groupby, "should have fetched groupby"); ok(_(gs).isEmpty(), "groupby list should be empty"); - }) + }); }); test('Generate domains', {asserts: 1}, function (instance, $fix) { var view = makeSearchView(instance, { @@ -1065,7 +1118,7 @@ openerp.testing.section('search.groupby', { '', '' ].join(''), - } + }; }); } }, function (instance, $fix) { @@ -1076,7 +1129,7 @@ openerp.testing.section('search.groupby', { equal(view.inputs.length, 7, 'should have 7 inputs total'); var group = _.find(view.inputs, function (f) { - return f instanceof instance.web.search.GroupbyGroup + return f instanceof instance.web.search.GroupbyGroup; }); ok(group, "should have a GroupbyGroup input"); strictEqual(group.getParent(), view, @@ -1095,7 +1148,7 @@ openerp.testing.section('search.groupby', { deepEqual(results.groupbys, [ "{'group_by': 'foo'}", "{'group_by': 'baz'}" - ], "should have sequence of contexts") + ], "should have sequence of contexts"); }); }); test('unified multiple groupby groups', { @@ -1114,7 +1167,7 @@ openerp.testing.section('search.groupby', { '', '' ].join(''), - } + }; }); } }, function (instance, $fix) { @@ -1125,7 +1178,7 @@ openerp.testing.section('search.groupby', { equal(view.inputs.length, 9, "should have 9 inputs total"); var groups = _.filter(view.inputs, function (f) { - return f instanceof instance.web.search.GroupbyGroup + return f instanceof instance.web.search.GroupbyGroup; }); equal(groups.length, 3, "should have 3 GroupbyGroups"); @@ -1169,7 +1222,7 @@ openerp.testing.section('search.filters.saved', { "displayed label should be the name of the filter"); equal(values.at(0).get('value'), null, "should have no value set"); - }) + }); }); test('removal', {asserts: 1}, function (instance, $fix, mock) { var view = makeSearchView(instance); @@ -1361,7 +1414,7 @@ openerp.testing.section('search.invisible', { }, ['', '', '', - ''].join()); + ''].join('')); return view.appendTo($fix) .then(function () { var done = $.Deferred(); @@ -1380,7 +1433,7 @@ openerp.testing.section('search.invisible', { '', '', '', - ''].join()); + ''].join('')); return view.appendTo($fix) .then(function () { var $fs = $fix.find('.oe_searchview_filters ul'); @@ -1400,6 +1453,26 @@ openerp.testing.section('search.invisible', { return done; }); }); + test('invisible-previous-sibling', {asserts: 3}, function (instance, $fix, mock) { + var view = makeView(instance, mock, {}, [ + '', + '', + '', + '', + '', + ''].join('')); + return view.appendTo($fix) + .done(function () { + // Select filter 3 + $fix.find('.oe_searchview_filters ul li:contains("filter 3")').click(); + equal(view.query.length, 1, "should have selected a filter"); + var facet = view.query.at(0); + strictEqual(facet.values.at(0).get('label'), "filter 3", + "should have correctly labelled the facet"); + deepEqual(view.build_search_data().contexts, [{test: 3}], + "should have built correct context"); + }); + }); // Invisible filter groups should not appear in the drawer // Group invisibility should be inherited by children test('group-invisibility', {asserts: 6}, function (instance, $fix, mock) { diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js index 5bd008ad7ec..ff1aae22663 100644 --- a/addons/web/static/test/testing.js +++ b/addons/web/static/test/testing.js @@ -21,7 +21,7 @@ openerp.testing.section('testing.stack', function (test) { return s.execute(function () { return $.when(42); }).then(function (val) { - strictEqual(val, 42, "should return the handler value") + strictEqual(val, 42, "should return the handler value"); }); }); test('direct, deferred, failure', {asserts: 1}, function () {