[FIX] don't trigger search twice when a facet is removed from the query due to its...
[odoo/odoo.git] / addons / web / static / test / search.js
1 $(document).ready(function () {
2     var instance;
3     module('query', {
4         setup: function () {
5             instance = openerp.testing.instanceFor('search');
6         }
7     });
8     test('Adding a facet to the query creates a facet and a value', function () {
9         var query = new instance.web.search.SearchQuery;
10         var field = {};
11         query.add({
12             category: 'Foo',
13             field: field,
14             values: [{label: 'Value', value: 3}]
15         });
16
17         var facet = query.at(0);
18         equal(facet.get('category'), 'Foo');
19         equal(facet.get('field'), field);
20         deepEqual(facet.get('values'), [{label: 'Value', value: 3}]);
21     });
22     test('Adding two facets', function () {
23         var query = new instance.web.search.SearchQuery;
24         query.add([
25             { category: 'Foo', field: {}, values: [{label: 'Value', value: 3}] },
26             { category: 'Bar', field: {}, values: [{label: 'Value 2', value: 4}] }
27         ]);
28
29         equal(query.length, 2);
30         equal(query.at(0).values.length, 1);
31         equal(query.at(1).values.length, 1);
32     });
33     test('If a facet already exists, add values to it', function () {
34         var query = new instance.web.search.SearchQuery;
35         var field = {};
36         query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
37         query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
38
39         equal(query.length, 1, "adding an existing facet should merge new values into old facet");
40         var facet = query.at(0);
41         deepEqual(facet.get('values'), [
42             {label: 'V1', value: 0},
43             {label: 'V2', value: 1}
44         ]);
45     });
46     test('Facet being implicitly changed should trigger change, not add', function () {
47         var query = new instance.web.search.SearchQuery;
48         var field = {}, added = false, changed = false;
49         query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
50         query.on('add', function () { added = true; })
51              .on('change', function () { changed = true });
52         query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
53
54         ok(!added, "query.add adding values to a facet should not trigger an add");
55         ok(changed, "query.add adding values to a facet should not trigger a change");
56     });
57     test('Toggling a facet, value which does not exist should add it', function () {
58         var query = new instance.web.search.SearchQuery;
59         var field = {};
60         query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
61
62         equal(query.length, 1, "Should have created a single facet");
63         var facet = query.at(0);
64         equal(facet.values.length, 1, "Facet should have a single value");
65         deepEqual(facet.get('values'), [{label: 'V1', value: 0}],
66                   "Facet's value should match input");
67     });
68     test('Toggling a facet which exists with a value which does not should add the value to the facet', function () {
69         var field = {};
70         var query = new instance.web.search.SearchQuery;
71         query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
72         query.toggle({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
73
74         equal(query.length, 1, "Should have edited the existing facet");
75         var facet = query.at(0);
76         equal(facet.values.length, 2, "Should have added the value to the existing facet");
77         deepEqual(facet.get('values'), [
78             {label: 'V1', value: 0},
79             {label: 'V2', value: 1}
80         ]);
81     });
82     test('Toggling a facet which exists with a value which does as well should remove the value from the facet', function () {
83         var field = {};
84         var query = new instance.web.search.SearchQuery;
85         query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
86         query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
87
88         query.toggle({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
89
90         equal(query.length, 1, 'Should have the same single facet');
91         var facet = query.at(0);
92         equal(facet.values.length, 1, "Should only have one value left in the facet");
93         deepEqual(facet.get('values'), [
94             {label: 'V1', value: 0}
95         ]);
96     });
97     test('Toggling off the last value of a facet should remove the facet', function () {
98         var field = {};
99         var query = new instance.web.search.SearchQuery;
100         query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
101
102         query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
103
104         equal(query.length, 0, 'Should have removed the facet');
105     });
106     test('Intermediate emptiness should not remove the facet', function () {
107         var field = {};
108         var query = new instance.web.search.SearchQuery;
109         query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
110
111         query.toggle({category: 'A', field: field, values: [
112             {label: 'V1', value: 0},
113             {label: 'V2', value: 1}
114         ]});
115
116         equal(query.length, 1, 'Should not have removed the facet');
117         var facet = query.at(0);
118         equal(facet.values.length, 1, "Should have one value");
119         deepEqual(facet.get('values'), [
120             {label: 'V2', value: 1}
121         ]);
122     });
123
124     test('Reseting with multiple facets should still work to load defaults', function () {
125         var query = new instance.web.search.SearchQuery;
126         var field = {};
127         query.reset([
128             {category: 'A', field: field, values: [{label: 'V1', value: 0}]},
129             {category: 'A', field: field, values: [{label: 'V2', value: 1}]}]);
130
131         equal(query.length, 1, 'Should have created a single facet');
132         equal(query.at(0).values.length, 2, 'the facet should have merged two values');
133         deepEqual(query.at(0).get('values'), [
134             {label: 'V1', value: 0},
135             {label: 'V2', value: 1}
136         ])
137     });
138
139     module('defaults', {
140         setup: function () {
141             instance = openerp.testing.instanceFor('search');
142
143             openerp.testing.loadTemplate(instance);
144
145             openerp.testing.mockifyRPC(instance);
146         }
147     });
148
149     /**
150      * Builds a basic search view with a single "dummy" field. The dummy
151      * extends `instance.web.search.Field`, it does not add any (class)
152      * attributes beyond what is provided through ``dummy_widget_attributes``.
153      *
154      * The view is returned un-started, it is the caller's role to start it
155      * (or use DOM-insertion methods to start it indirectly).
156      *
157      * @param [dummy_widget_attributes={}]
158      * @param [defaults={}]
159      * @return {instance.web.SearchView}
160      */
161     function makeSearchView(dummy_widget_attributes, defaults) {
162         instance.web.search.fields.add(
163             'dummy', 'instance.dummy.DummyWidget');
164         instance.dummy = {};
165         instance.dummy.DummyWidget = instance.web.search.Field.extend(
166             dummy_widget_attributes || {});
167         if (!('/web/searchview/load' in instance.session.responses)) {
168             instance.session.responses['/web/searchview/load'] = function () {
169                 return {result: {fields_view: {
170                     type: 'search',
171                     fields: {
172                         dummy: {type: 'char', string: "Dummy"}
173                     },
174                     arch: {
175                         tag: 'search',
176                         attrs: {},
177                         children: [{
178                             tag: 'field',
179                             attrs: {
180                                 name: 'dummy',
181                                 widget: 'dummy'
182                             },
183                             children: []
184                         }]
185                     }
186                 }}};
187             };
188         }
189         instance.session.responses['/web/searchview/get_filters'] = function () {
190             return {result: []};
191         };
192         instance.session.responses['/web/searchview/fields_get'] = function () {
193             return {result: {fields: {
194                 dummy: {type: 'char', string: 'Dummy'}
195             }}};
196         };
197
198         var dataset = {model: 'dummy.model', get_context: function () { return {}; }};
199         var view = new instance.web.SearchView(null, dataset, false, defaults);
200         view.on_invalid.add(function () {
201             ok(false, JSON.stringify([].slice(arguments)));
202         });
203         return view;
204     }
205     asyncTest('calling', 2, function () {
206         var defaults_called = false;
207
208         var view = makeSearchView({
209             facet_for_defaults: function (defaults) {
210                 defaults_called = true;
211                 return $.when({
212                     field: this,
213                     category: 'Dummy',
214                     values: [{label: 'dummy', value: defaults.dummy}]
215                 });
216             }
217         }, {dummy: 42});
218         view.appendTo($('#qunit-fixture'))
219             .always(start)
220             .fail(function (error) { ok(false, error.message); })
221             .done(function () {
222                 ok(defaults_called, "should have called defaults");
223                 deepEqual(
224                     view.query.toJSON(),
225                     [{category: 'Dummy', values: [{label: 'dummy', value: 42}]}],
226                     "should have generated a facet with the default value");
227             });
228     });
229     asyncTest('FilterGroup', 3, function () {
230         var view = {inputs: [], query: {on: function () {}}};
231         var filter_a = new instance.web.search.Filter(
232             {attrs: {name: 'a'}}, view);
233         var filter_b = new instance.web.search.Filter(
234             {attrs: {name: 'b'}}, view);
235         var group = new instance.web.search.FilterGroup(
236             [filter_a, filter_b], view);
237         group.facet_for_defaults({a: true, b: true})
238             .always(start)
239             .fail(function (error) { ok(false, error && error.message); })
240             .done(function (facet) {
241                 var model = facet;
242                 if (!(model instanceof instance.web.search.Facet)) {
243                     model = new instance.web.search.Facet(facet);
244                 }
245                 var values = model.values;
246                 equal(values.length, 2, 'facet should have two values');
247                 strictEqual(values.at(0).get('value'), filter_a);
248                 strictEqual(values.at(1).get('value'), filter_b);
249             });
250     });
251     asyncTest('Field', 4, function () {
252         var view = {inputs: []};
253         var f = new instance.web.search.Field(
254             {attrs: {string: 'Dummy', name: 'dummy'}}, {}, view);
255         f.facet_for_defaults({dummy: 42})
256             .always(start)
257             .fail(function (error) { ok(false, error && error.message); })
258             .done(function (facet) {
259                 var model = facet;
260                 if (!(model instanceof instance.web.search.Facet)) {
261                     model = new instance.web.search.Facet(facet);
262                 }
263                 strictEqual(
264                     model.get('category'),
265                     f.attrs.string,
266                     "facet category should be field label");
267                 strictEqual(
268                     model.get('field'), f,
269                     "facet field should be field which created default");
270                 equal(model.values.length, 1, "facet should have a single value");
271                 deepEqual(
272                     model.values.toJSON(),
273                     [{label: '42', value: 42}],
274                     "facet value should match provided default");
275                 });
276     });
277     asyncTest('Selection: valid value', 4, function () {
278         var view = {inputs: []};
279         var f = new instance.web.search.SelectionField(
280             {attrs: {name: 'dummy', string: 'Dummy'}},
281             {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Qux"]]},
282             view);
283         f.facet_for_defaults({dummy: 3})
284             .always(start)
285             .fail(function (error) { ok(false, error && error.message); })
286             .done(function (facet) {
287                 var model = facet;
288                 if (!(model instanceof instance.web.search.Facet)) {
289                     model = new instance.web.search.Facet(facet);
290                 }
291                 strictEqual(
292                     model.get('category'),
293                     f.attrs.string,
294                     "facet category should be field label");
295                 strictEqual(
296                     model.get('field'), f,
297                     "facet field should be field which created default");
298                 equal(model.values.length, 1, "facet should have a single value");
299                 deepEqual(
300                     model.values.toJSON(),
301                     [{label: 'Baz', value: 3}],
302                     "facet value should match provided default's selection");
303             });
304     });
305     asyncTest('Selection: invalid value', 1, function () {
306         var view = {inputs: []};
307         var f = new instance.web.search.SelectionField(
308             {attrs: {name: 'dummy', string: 'Dummy'}},
309             {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Qux"]]},
310             view);
311         f.facet_for_defaults({dummy: 42})
312             .always(start)
313             .fail(function (error) { ok(false, error && error.message); })
314             .done(function (facet) {
315                 ok(!facet, "an invalid value should result in a not-facet");
316             });
317     });
318     asyncTest("M2O default: value", 7, function () {
319         var view = {inputs: []}, id = 4;
320         var f = new instance.web.search.ManyToOneField(
321             {attrs: {name: 'dummy', string: 'Dummy'}},
322             {relation: 'dummy.model.name'},
323             view);
324         instance.session.responses['/web/dataset/call_kw'] = function (req) {
325             equal(req.params.method, 'name_get',
326                   "m2o should resolve default id");
327             equal(req.params.model, f.attrs.relation,
328                   "query model should match m2o relation");
329             equal(req.params.args[0], id);
330             return {result: [[id, "DumDumDum"]]};
331         };
332         f.facet_for_defaults({dummy: id})
333             .always(start)
334             .fail(function (error) { ok(false, error && error.message); })
335             .done(function (facet) {
336                 var model = facet;
337                 if (!(model instanceof instance.web.search.Facet)) {
338                     model = new instance.web.search.Facet(facet);
339                 }
340                 strictEqual(
341                     model.get('category'),
342                     f.attrs.string,
343                     "facet category should be field label");
344                 strictEqual(
345                     model.get('field'), f,
346                     "facet field should be field which created default");
347                 equal(model.values.length, 1, "facet should have a single value");
348                 deepEqual(
349                     model.values.toJSON(),
350                     [{label: 'DumDumDum', value: id}],
351                     "facet value should match provided default's selection");
352             });
353     });
354     asyncTest("M2O default: value", 1, function () {
355         var view = {inputs: []}, id = 4;
356         var f = new instance.web.search.ManyToOneField(
357             {attrs: {name: 'dummy', string: 'Dummy'}},
358             {relation: 'dummy.model.name'},
359             view);
360         instance.session.responses['/web/dataset/call_kw'] = function (req) {
361            return {result: []};
362         };
363         f.facet_for_defaults({dummy: id})
364             .always(start)
365             .fail(function (error) { ok(false, error && error.message); })
366             .done(function (facet) {
367                 ok(!facet, "an invalid m2o default should yield a non-facet");
368             });
369     });
370
371     module('completions', {
372         setup: function () {
373             instance = openerp.testing.instanceFor('search');
374
375             openerp.testing.loadTemplate(instance);
376
377             openerp.testing.mockifyRPC(instance);
378         }
379     });
380     asyncTest('calling', 4, function () {
381         var view = makeSearchView({
382             complete: function () {
383                 return $.when({
384                     label: "Dummy",
385                     facet: {
386                         field: this,
387                         category: 'Dummy',
388                         values: [{label: 'dummy', value: 42}]
389                     }
390                 });
391             }
392         });
393         view.appendTo($('#qunit-fixture'))
394             .done(function () {
395                 view.complete_global_search({term: "dum"}, function (completions) {
396                     start();
397                     equal(completions.length, 1, "should have a single completion");
398                     var completion = completions[0];
399                     equal(completion.label, "Dummy",
400                           "should have provided label");
401                     equal(completion.facet.category, "Dummy",
402                           "should have provided category");
403                     deepEqual(completion.facet.values,
404                               [{label: 'dummy', value: 42}],
405                               "should have provided values");
406                 });
407             });
408     });
409     asyncTest('facet selection', 2, function () {
410         var completion = {
411             label: "Dummy",
412             facet: {
413                 field: {
414                     get_domain: openerp.testing.noop,
415                     get_context: openerp.testing.noop,
416                     get_groupby: openerp.testing.noop
417                 },
418                 category: 'Dummy',
419                 values: [{label: 'dummy', value: 42}]
420             }
421         };
422
423         var view = makeSearchView({});
424         view.appendTo($('#qunit-fixture'))
425             .always(start)
426             .fail(function (error) { ok(false, error.message); })
427             .done(function () {
428                 view.select_completion(
429                     {preventDefault: function () {}},
430                     {item: completion});
431                 equal(view.query.length, 1, "should have one facet in the query");
432                 deepEqual(
433                     view.query.at(0).toJSON(),
434                     {category: 'Dummy', values: [{label: 'dummy', value: 42}]},
435                     "should have the right facet in the query");
436             });
437     });
438     asyncTest('facet selection: new value existing facet', 3, function () {
439         var field = {
440             get_domain: openerp.testing.noop,
441             get_context: openerp.testing.noop,
442             get_groupby: openerp.testing.noop
443         };
444         var completion = {
445             label: "Dummy",
446             facet: {
447                 field: field,
448                 category: 'Dummy',
449                 values: [{label: 'dummy', value: 42}]
450             }
451         };
452
453         var view = makeSearchView({});
454         view.appendTo($('#qunit-fixture'))
455             .always(start)
456             .fail(function (error) { ok(false, error.message); })
457             .done(function () {
458                 view.query.add({field: field, category: 'Dummy',
459                                 values: [{label: 'previous', value: 41}]});
460                 equal(view.query.length, 1, 'should have newly added facet');
461                 view.select_completion(
462                     {preventDefault: function () {}},
463                     {item: completion});
464                 equal(view.query.length, 1, "should still have only one facet");
465                 var facet = view.query.at(0);
466                 deepEqual(
467                     facet.get('values'),
468                     [{label: 'previous', value: 41}, {label: 'dummy', value: 42}],
469                     "should have added selected value to old one");
470             });
471     });
472     asyncTest('Field', 1, function () {
473         var view = {inputs: []};
474         var f = new instance.web.search.Field({attrs: {}}, {}, view);
475         f.complete('foo')
476             .always(start)
477             .fail(function (error) { ok(false, error.message); })
478             .done(function (completions) {
479                 ok(_(completions).isEmpty(), "field should not provide any completion");
480             });
481     });
482     asyncTest('CharField', 6, function () {
483         var view = {inputs: []};
484         var f = new instance.web.search.CharField(
485             {attrs: {string: "Dummy"}}, {}, view);
486         f.complete('foo<')
487             .always(start)
488             .fail(function (error) { ok(false, error.message); })
489             .done(function (completions) {
490                 equal(completions.length, 1, "should provide a single completion");
491                 var c = completions[0];
492                 equal(c.label, "Search <em>Dummy</em> for: <strong>foo&lt;</strong>",
493                       "should propose a fuzzy matching/searching, with the" +
494                       " value escaped");
495                 ok(c.facet, "completion should contain a facet proposition");
496                 var facet = new instance.web.search.Facet(c.facet);
497                 equal(facet.get('category'), f.attrs.string,
498                       "completion facet should bear the field's name");
499                 strictEqual(facet.get('field'), f,
500                             "completion facet should yield the field");
501                 deepEqual(facet.values.toJSON(), [{label: 'foo<', value: 'foo<'}],
502                           "facet should have single value using completion item");
503             });
504     });
505     asyncTest('Selection: match found', 14, function () {
506         var view = {inputs: []};
507         var f = new instance.web.search.SelectionField(
508             {attrs: {string: "Dummy"}},
509             {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Bazador"]]},
510             view);
511         f.complete("ba")
512             .always(start)
513             .fail(function (error) { ok(false, error.message); })
514             .done(function (completions) {
515                 equal(completions.length, 4,
516                     "should provide two completions and a section title");
517                 deepEqual(completions[0], {label: "Dummy"});
518
519                 var c1 = completions[1];
520                 equal(c1.label, "Bar");
521                 equal(c1.facet.category, f.attrs.string);
522                 strictEqual(c1.facet.field, f);
523                 deepEqual(c1.facet.values, [{label: "Bar", value: 2}]);
524
525                 var c2 = completions[2];
526                 equal(c2.label, "Baz");
527                 equal(c2.facet.category, f.attrs.string);
528                 strictEqual(c2.facet.field, f);
529                 deepEqual(c2.facet.values, [{label: "Baz", value: 3}]);
530
531                 var c3 = completions[3];
532                 equal(c3.label, "Bazador");
533                 equal(c3.facet.category, f.attrs.string);
534                 strictEqual(c3.facet.field, f);
535                 deepEqual(c3.facet.values, [{label: "Bazador", value: 4}]);
536             });
537     });
538     asyncTest('Selection: no match', 1, function () {
539         var view = {inputs: []};
540         var f = new instance.web.search.SelectionField(
541             {attrs: {string: "Dummy"}},
542             {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Bazador"]]},
543             view);
544         f.complete("qux")
545             .always(start)
546             .fail(function (error) { ok(false, error.message); })
547             .done(function (completions) {
548                 ok(!completions, "if no value matches the needle, no completion shall be provided");
549             });
550     });
551     asyncTest('Date', 6, function () {
552         instance.web._t.database.parameters = {
553             date_format: '%Y-%m-%d',
554             time_format: '%H:%M:%S'
555         };
556         var view = {inputs: []};
557         var f = new instance.web.search.DateField(
558             {attrs: {string: "Dummy"}}, {type: 'datetime'}, view);
559         f.complete('2012-05-21T21:21:21')
560             .always(start)
561             .fail(function (error) { ok(false, error.message); })
562             .done(function (completions) {
563                 equal(completions.length, 1, "should provide a single completion");
564                 var c = completions[0];
565                 equal(c.label, "Search <em>Dummy</em> at: <strong>2012-05-21 21:21:21</strong>");
566                 var facet = new instance.web.search.Facet(c.facet);
567                 equal(facet.get('category'), f.attrs.string);
568                 equal(facet.get('field'), f);
569                 var value = facet.values.at(0);
570                 equal(value.get('label'), "2012-05-21 21:21:21");
571                 equal(value.get('value').getTime(),
572                       new Date(2012, 4, 21, 21, 21, 21).getTime());
573             });
574     });
575     asyncTest("M2O", 15, function () {
576         instance.session.responses['/web/dataset/call_kw'] = function (req) {
577             equal(req.params.method, "name_search");
578             equal(req.params.model, "dummy.model");
579             deepEqual(req.params.args, []);
580             deepEqual(req.params.kwargs.name, 'bob');
581             return {result: [[42, "choice 1"], [43, "choice @"]]}
582         };
583
584         var view = {inputs: []};
585         var f = new instance.web.search.ManyToOneField(
586             {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view);
587         f.complete("bob")
588             .always(start)
589             .fail(function (error) { ok(false, error.message); })
590             .done(function (c) {
591                 equal(c.length, 3, "should return results + title");
592                 var title = c[0];
593                 equal(title.label, f.attrs.string, "title should match field name");
594                 ok(!title.facet, "title should not have a facet");
595
596                 var f1 = new instance.web.search.Facet(c[1].facet);
597                 equal(c[1].label, "choice 1");
598                 equal(f1.get('category'), f.attrs.string);
599                 equal(f1.get('field'), f);
600                 deepEqual(f1.values.toJSON(), [{label: 'choice 1', value: 42}]);
601
602                 var f2 = new instance.web.search.Facet(c[2].facet);
603                 equal(c[2].label, "choice @");
604                 equal(f2.get('category'), f.attrs.string);
605                 equal(f2.get('field'), f);
606                 deepEqual(f2.values.toJSON(), [{label: 'choice @', value: 43}]);
607             });
608     });
609     asyncTest("M2O no match", 5, function () {
610         instance.session.responses['/web/dataset/call_kw'] = function (req) {
611             equal(req.params.method, "name_search");
612             equal(req.params.model, "dummy.model");
613             deepEqual(req.params.args, []);
614             deepEqual(req.params.kwargs.name, 'bob');
615             return {result: []}
616         };
617         var view = {inputs: []};
618         var f = new instance.web.search.ManyToOneField(
619             {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view);
620         f.complete("bob")
621             .always(start)
622             .fail(function (error) { ok(false, error.message); })
623             .done(function (c) {
624                 ok(!c, "no match should yield no completion");
625             });
626     });
627
628     module('search-serialization', {
629         setup: function () {
630             instance = openerp.testing.instanceFor('search');
631
632             openerp.testing.loadTemplate(instance);
633
634             openerp.testing.mockifyRPC(instance);
635         }
636     });
637     asyncTest('No facet, no call', 6, function () {
638         var got_domain = false, got_context = false, got_groupby = false;
639         var $fix = $('#qunit-fixture');
640         var view = makeSearchView({
641             get_domain: function () {
642                 got_domain = true;
643                 return null;
644             },
645             get_context: function () {
646                 got_context = true;
647                 return null;
648             },
649             get_groupby: function () {
650                 got_groupby = true;
651                 return null;
652             }
653         });
654         var ds, cs, gs;
655         view.on_search.add(function (d, c, g) {
656             ds = d, cs = c, gs = g;
657         });
658         view.appendTo($fix)
659             .always(start)
660             .fail(function (error) { ok(false, error.message); })
661             .done(function () {
662                 view.do_search();
663                 ok(!got_domain, "no facet, should not have fetched domain");
664                 ok(_(ds).isEmpty(), "domains list should be empty");
665
666                 ok(!got_context, "no facet, should not have fetched context");
667                 ok(_(cs).isEmpty(), "contexts list should be empty");
668
669                 ok(!got_groupby, "no facet, should not have fetched groupby");
670                 ok(_(gs).isEmpty(), "groupby list should be empty");
671             })
672     });
673     asyncTest('London, calling', 8, function () {
674         var got_domain = false, got_context = false, got_groupby = false;
675         var $fix = $('#qunit-fixture');
676         var view = makeSearchView({
677             get_domain: function (facet) {
678                 equal(facet.get('category'), "Dummy");
679                 deepEqual(facet.values.toJSON(), [{label: "42", value: 42}]);
680                 got_domain = true;
681                 return null;
682             },
683             get_context: function () {
684                 got_context = true;
685                 return null;
686             },
687             get_groupby: function () {
688                 got_groupby = true;
689                 return null;
690             }
691         }, {dummy: 42});
692         var ds, cs, gs;
693         view.on_search.add(function (d, c, g) {
694             ds = d, cs = c, gs = g;
695         });
696         view.appendTo($fix)
697             .always(start)
698             .fail(function (error) { ok(false, error.message); })
699             .done(function () {
700                 view.do_search();
701                 ok(got_domain, "should have fetched domain");
702                 ok(_(ds).isEmpty(), "domains list should be empty");
703
704                 ok(got_context, "should have fetched context");
705                 ok(_(cs).isEmpty(), "contexts list should be empty");
706
707                 ok(got_groupby, "should have fetched groupby");
708                 ok(_(gs).isEmpty(), "groupby list should be empty");
709             })
710     });
711     asyncTest('Generate domains', 1, function () {
712         var $fix = $('#qunit-fixture');
713         var view = makeSearchView({
714             get_domain: function (facet) {
715                 return facet.values.map(function (value) {
716                     return ['win', '4', value.get('value')];
717                 });
718             }
719         }, {dummy: 42});
720         var ds;
721         view.on_search.add(function (d) { ds = d; });
722         view.appendTo($fix)
723             .always(start)
724             .fail(function (error) { ok(false, error.message); })
725             .done(function () {
726                 view.do_search();
727                 deepEqual(ds, [[['win', '4', 42]]],
728                     "search should yield an array of contexts");
729             });
730     });
731
732     test('Field single value, default domain & context', function () {
733         var f = new instance.web.search.Field({}, {name: 'foo'}, {inputs: []});
734         var facet = new instance.web.search.Facet({
735             field: f,
736             values: [{value: 42}]
737         });
738
739         deepEqual(f.get_domain(facet), [['foo', '=', 42]],
740             "default field domain is a strict equality of name to facet's value");
741         equal(f.get_context(facet), null,
742             "default field context is null");
743     });
744     test('Field multiple values, default domain & context', function () {
745         var f = new instance.web.search.Field({}, {name: 'foo'}, {inputs: []});
746         var facet = new instance.web.search.Facet({
747             field: f,
748             values: [{value: 42}, {value: 68}, {value: 999}]
749         });
750
751         var actual_domain = f.get_domain(facet);
752         equal(actual_domain.__ref, "compound_domain",
753               "multiple value should yield compound domain");
754         deepEqual(actual_domain.__domains, [
755                     ['|'],
756                     ['|'],
757                     [['foo', '=', 42]],
758                     [['foo', '=', 68]],
759                     [['foo', '=', 999]]
760             ],
761             "domain should OR a default domain for each value");
762         equal(f.get_context(facet), null,
763             "default field context is null");
764     });
765     test('Field single value, custom domain & context', function () {
766         var f = new instance.web.search.Field({attrs:{
767             context: "{'bob': self}",
768             filter_domain: "[['edmund', 'is', self]]"
769         }}, {name: 'foo'}, {inputs: []});
770         var facet = new instance.web.search.Facet({
771             field: f,
772             values: [{value: "great"}]
773         });
774
775         var actual_domain = f.get_domain(facet);
776         equal(actual_domain.__ref, "compound_domain",
777               "@filter_domain should yield compound domain");
778         deepEqual(actual_domain.__domains, [
779             "[['edmund', 'is', self]]"
780         ], 'should hold unevaluated custom domain');
781         deepEqual(actual_domain.get_eval_context(), {
782             self: "great"
783         }, "evaluation context should hold facet value as self");
784
785         var actual_context = f.get_context(facet);
786         equal(actual_context.__ref, "compound_context",
787               "@context should yield compound context");
788         deepEqual(actual_context.__contexts, [
789             "{'bob': self}"
790         ], 'should hold unevaluated custom context');
791         deepEqual(actual_context.get_eval_context(), {
792             self: "great"
793         }, "evaluation context should hold facet value as self");
794     });
795     test("M2O default", function () {
796         var f = new instance.web.search.ManyToOneField(
797             {}, {name: 'foo'}, {inputs: []});
798         var facet = new instance.web.search.Facet({
799             field: f,
800             values: [{label: "Foo", value: 42}]
801         });
802
803         deepEqual(f.get_domain(facet), [['foo', '=', 42]],
804             "m2o should use identity if default domain");
805         deepEqual(f.get_context(facet), {default_foo: 42},
806             "m2o should use value as context default");
807     });
808     test("M2O default multiple values", function () {
809         var f = new instance.web.search.ManyToOneField(
810             {}, {name: 'foo'}, {inputs: []});
811         var facet = new instance.web.search.Facet({
812             field: f,
813             values: [
814                 {label: "Foo", value: 42},
815                 {label: "Bar", value: 36}
816             ]
817         });
818
819         deepEqual(f.get_domain(facet).__domains,
820             [['|'], [['foo', '=', 42]], [['foo', '=', 36]]],
821             "m2o should or multiple values");
822         equal(f.get_context(facet), null,
823             "m2o should not have default context in case of multiple values");
824     });
825     test("M2O custom operator", function () {
826         var f = new instance.web.search.ManyToOneField(
827             {attrs: {operator: 'boos'}}, {name: 'foo'}, {inputs: []});
828         var facet = new instance.web.search.Facet({
829             field: f,
830             values: [{label: "Foo", value: 42}]
831         });
832
833         deepEqual(f.get_domain(facet), [['foo', 'boos', 'Foo']],
834             "m2o should use label with custom operators");
835         deepEqual(f.get_context(facet), {default_foo: 42},
836             "m2o should use value as context default");
837     });
838     test("M2O custom domain & context", function () {
839         var f = new instance.web.search.ManyToOneField({attrs: {
840             context: "{'whee': self}",
841             filter_domain: "[['filter', 'is', self]]"
842         }}, {name: 'foo'}, {inputs: []});
843         var facet = new instance.web.search.Facet({
844             field: f,
845             values: [{label: "Foo", value: 42}]
846         });
847
848         var domain = f.get_domain(facet);
849         deepEqual(domain.__domains, [
850             "[['filter', 'is', self]]"
851         ]);
852         deepEqual(domain.get_eval_context(), {
853             self: "Foo"
854         }, "custom domain's self should be label");
855         var context = f.get_context(facet);
856         deepEqual(context.__contexts, [
857             "{'whee': self}"
858         ]);
859         deepEqual(context.get_eval_context(), {
860             self: "Foo"
861         }, "custom context's self should be label");
862     });
863
864     asyncTest('FilterGroup', 6, function () {
865         var view = {inputs: [], query: {on: function () {}}};
866         var filter_a = new instance.web.search.Filter(
867             {attrs: {name: 'a', context: 'c1', domain: 'd1'}}, view);
868         var filter_b = new instance.web.search.Filter(
869             {attrs: {name: 'b', context: 'c2', domain: 'd2'}}, view);
870         var filter_c = new instance.web.search.Filter(
871             {attrs: {name: 'c', context: 'c3', domain: 'd3'}}, view);
872         var group = new instance.web.search.FilterGroup(
873             [filter_a, filter_b, filter_c], view);
874         group.facet_for_defaults({a: true, c: true})
875             .always(start)
876             .fail(function (error) { ok(false, error && error.message); })
877             .done(function (facet) {
878                 var model = facet;
879                 if (!(model instanceof instance.web.search.Facet)) {
880                     model = new instance.web.search.Facet(facet);
881                 }
882
883                 var domain = group.get_domain(model);
884                 equal(domain.__ref, 'compound_domain',
885                     "domain should be compound");
886                 deepEqual(domain.__domains, [
887                     ['|'], 'd1', 'd3'
888                 ], "domain should OR filter domains");
889                 ok(!domain.get_eval_context(), "domain should have no evaluation context");
890                 var context = group.get_context(model);
891                 equal(context.__ref, 'compound_context',
892                     "context should be compound");
893                 deepEqual(context.__contexts, [
894                     'c1', 'c3'
895                 ], "context should merge all filter contexts");
896                 ok(!context.get_eval_context(), "context should have no evaluation context");
897             });
898     });
899
900     module('removal', {
901         setup: function () {
902             instance = openerp.testing.instanceFor('search');
903
904             openerp.testing.loadTemplate(instance);
905
906             openerp.testing.mockifyRPC(instance);
907         }
908     });
909     asyncTest('clear button', function () {
910         var $fix = $('#qunit-fixture');
911         var view = makeSearchView({
912             facet_for_defaults: function (defaults) {
913                 return $.when({
914                     field: this,
915                     category: 'Dummy',
916                     values: [{label: 'dummy', value: defaults.dummy}]
917                 });
918             }
919         }, {dummy: 42});
920         view.appendTo($fix)
921             .always(start)
922             .fail(function (error) { ok(false, error.message); })
923             .done(function () {
924                 equal(view.query.length, 1, "view should have default facet");
925                 $fix.find('.oe_searchview_clear').click();
926                 equal(view.query.length, 0, "cleared view should not have any facet");
927             });
928     });
929
930     module('drawer', {
931         setup: function () {
932             instance = openerp.testing.instanceFor('search');
933
934             openerp.testing.loadTemplate(instance);
935
936             openerp.testing.mockifyRPC(instance);
937         }
938     });
939     asyncTest('is-drawn', 2, function () {
940         var view = makeSearchView();
941         var $fix = $('#qunit-fixture');
942         view.appendTo($fix)
943             .always(start)
944             .fail(function (error) { ok(false, error.message); })
945             .done(function () {
946                 ok($fix.find('.oe_searchview_filters').length,
947                    "filters drawer control has been drawn");
948                 ok($fix.find('.oe_searchview_advanced').length,
949                    "filters advanced search has been drawn");
950             });
951     });
952
953     module('filters', {
954         setup: function () {
955             instance = openerp.testing.instanceFor('search');
956
957             openerp.testing.loadTemplate(instance);
958
959             openerp.testing.mockifyRPC(instance, {
960                 '/web/searchview/load': function () {
961                     // view with a single group of filters
962                     return {result: {fields_view: {
963                         type: 'search',
964                         fields: {},
965                         arch: {
966                             tag: 'search',
967                             attrs: {},
968                             children: [{
969                                 tag: 'filter',
970                                 attrs: { string: "Foo1", domain: [ ['foo', '=', '1'] ] },
971                                 children: []
972                             }, {
973                                 tag: 'filter',
974                                 attrs: {
975                                     name: 'foo2',
976                                     string: "Foo2",
977                                     domain: [ ['foo', '=', '2'] ] },
978                                 children: []
979                             }, {
980                                 tag: 'filter',
981                                 attrs: { string: "Foo3", domain: [ ['foo', '=', '3'] ] },
982                                 children: []
983                             }]
984                         }
985                     }}};
986                 }
987             });
988         }
989     });
990     asyncTest('drawn', 3, function () {
991         var view = makeSearchView();
992         var $fix = $('#qunit-fixture');
993         view.appendTo($fix)
994             .always(start)
995             .fail(function (error) { ok(false, error.message); })
996             .done(function () {
997                 var $fs = $fix.find('.oe_searchview_filters ul');
998                 // 3 filters, 1 filtergroup, 1 custom filters widget,
999                 // 1 advanced and 1 Filters widget
1000                 equal(view.inputs.length, 7,
1001                       'view should have 7 inputs total');
1002                 equal($fs.children().length, 3,
1003                       "drawer should have a filter group with 3 filters");
1004                 equal(_.str.strip($fs.children().eq(0).text()), "Foo1",
1005                       "Text content of first filter option should match filter string");
1006             });
1007     });
1008     asyncTest('click adding from empty query', 4, function () {
1009         var view = makeSearchView();
1010         var $fix = $('#qunit-fixture');
1011         view.appendTo($fix)
1012             .always(start)
1013             .fail(function (error) { ok(false, error.message); })
1014             .done(function () {
1015                 var $fs = $fix.find('.oe_searchview_filters ul');
1016                 $fs.children(':eq(2)').trigger('click');
1017                 equal(view.query.length, 1, "click should have added a facet");
1018                 var facet = view.query.at(0);
1019                 equal(facet.values.length, 1, "facet should have a single value");
1020                 var value = facet.values.at(0);
1021                 ok(value.get('value') instanceof instance.web.search.Filter,
1022                    "value should be a filter");
1023                 equal(value.get('label'), "Foo3",
1024                       "value should be third filter");
1025             });
1026     });
1027     asyncTest('click adding from existing query', 4, function () {
1028         var view = makeSearchView({}, {foo2: true});
1029         var $fix = $('#qunit-fixture');
1030         view.appendTo($fix)
1031             .always(start)
1032             .fail(function (error) { ok(false, error.message); })
1033             .done(function () {
1034                 var $fs = $fix.find('.oe_searchview_filters ul');
1035                 $fs.children(':eq(2)').trigger('click');
1036                 equal(view.query.length, 1, "click should not have changed facet count");
1037                 var facet = view.query.at(0);
1038                 equal(facet.values.length, 2, "facet should have a second value");
1039                 var v1 = facet.values.at(0);
1040                 equal(v1.get('label'), "Foo2",
1041                       "first value should be default");
1042                 var v2 = facet.values.at(1);
1043                 equal(v2.get('label'), "Foo3",
1044                       "second value should be clicked filter");
1045             });
1046     });
1047     asyncTest('click removing from query', 4, function () {
1048         var calls = 0;
1049         var view = makeSearchView({}, {foo2: true});
1050         view.on_search.add(function () {
1051             ++calls;
1052         });
1053         var $fix = $('#qunit-fixture');
1054         view.appendTo($fix)
1055             .always(start)
1056             .fail(function (error) { ok(false, error.message); })
1057             .done(function () {
1058                 var $fs = $fix.find('.oe_searchview_filters ul');
1059                 // sanity check
1060                 equal(view.query.length, 1, "query should have default facet");
1061                 strictEqual(calls, 0);
1062                 $fs.children(':eq(1)').trigger('click');
1063                 equal(view.query.length, 0, "click should have removed facet");
1064                 strictEqual(calls, 1, "one search should have been triggered");
1065             });
1066     });
1067
1068     module('saved_filters', {
1069         setup: function () {
1070             instance = openerp.testing.instanceFor('search');
1071
1072             openerp.testing.loadTemplate(instance);
1073
1074             openerp.testing.mockifyRPC(instance);
1075         }
1076     });
1077     asyncTest('checkboxing', 6, function () {
1078         var view = makeSearchView();
1079         instance.session.responses['/web/searchview/get_filters'] = function () {
1080             return {result: [{
1081                 name: "filter name",
1082                 user_id: 42
1083             }]};
1084         };
1085         var $fix = $('#qunit-fixture');
1086
1087         view.appendTo($fix)
1088             .always(start)
1089             .fail(function (error) { ok(false, error.message); })
1090             .done(function () {
1091                 var $row = $fix.find('.oe_searchview_custom li:first').click();
1092
1093                 ok($row.hasClass('oe_selected'), "should check/select the filter's row");
1094                 ok($row.hasClass("oe_searchview_custom_private"),
1095                     "should have private filter note/class");
1096                 equal(view.query.length, 1, "should have only one facet");
1097                 var values = view.query.at(0).values;
1098                 equal(values.length, 1,
1099                     "should have only one value in the facet");
1100                 equal(values.at(0).get('label'), 'filter name',
1101                     "displayed label should be the name of the filter");
1102                 equal(values.at(0).get('value'), null,
1103                     "should have no value set");
1104             })
1105     });
1106     asyncTest('removal', 1, function () {
1107         var view = makeSearchView();
1108         instance.session.responses['/web/searchview/get_filters'] = function () {
1109             return {result: [{
1110                 name: "filter name",
1111                 user_id: 42
1112             }]};
1113         };
1114         var $fix = $('#qunit-fixture');
1115
1116         view.appendTo($fix)
1117             .always(start)
1118             .fail(function (error) { ok(false, error.message); })
1119             .done(function () {
1120                 var $row = $fix.find('.oe_searchview_custom li:first').click();
1121
1122                 view.query.remove(view.query.at(0));
1123                 ok(!$row.hasClass('oe_selected'),
1124                     "should not be checked anymore");
1125             })
1126     });
1127
1128     module('advanced', {
1129         setup: function () {
1130             instance = openerp.testing.instanceFor('search');
1131
1132             openerp.testing.loadTemplate(instance);
1133
1134             openerp.testing.mockifyRPC(instance);
1135         }
1136     });
1137     asyncTest('single-advanced', 6, function () {
1138         var view = makeSearchView();
1139         var $fix = $('#qunit-fixture');
1140
1141         view.appendTo($fix)
1142             .always(start)
1143             .fail(function (error) { ok(false, error.message); })
1144             .done(function () {
1145                 var $advanced = $fix.find('.oe_searchview_advanced');
1146                 // open advanced search (not actually useful)
1147                 $advanced.find('> h4').click();
1148                 // select proposition (only one)
1149                 var $prop = $advanced.find('> form li:first');
1150                 // field select should have two possible values, dummy and id
1151                 equal($prop.find('.searchview_extended_prop_field option').length,
1152                       2, "advanced search should provide choice between two fields");
1153                 // field should be dummy
1154                 equal($prop.find('.searchview_extended_prop_field').val(),
1155                       'dummy',
1156                       "only field should be dummy");
1157                 // operator should be "contains"/'ilike'
1158                 equal($prop.find('.searchview_extended_prop_op').val(),
1159                       'ilike', "default char operator should be ilike");
1160                 // put value in
1161                 $prop.find('.searchview_extended_prop_value input')
1162                      .val("stupid value");
1163                 // validate advanced search
1164                 $advanced.find('button.oe_apply').click();
1165
1166                 // resulting search
1167                 equal(view.query.length, 1, "search query should have a single facet");
1168                 var facet = view.query.at(0);
1169                 ok(!facet.get('field').get_context(facet),
1170                    "advanced search facets should yield no context");
1171                 deepEqual(facet.get('field').get_domain(facet),
1172                           [['dummy', 'ilike', "stupid value"]],
1173                           "advanced search facet should return proposed domain");
1174             });
1175     });
1176     asyncTest('multiple-advanced', 3, function () {
1177         var view = makeSearchView();
1178         var $fix = $('#qunit-fixture');
1179
1180         view.appendTo($fix)
1181             .always(start)
1182             .fail(function (error) { ok(false, error.message); })
1183             .done(function () {
1184                 var $advanced = $fix.find('.oe_searchview_advanced');
1185                 // open advanced search (not actually useful)
1186                 $advanced.find('> h4').click();
1187                 // open second condition
1188                 $advanced.find('button.oe_add_condition').click();
1189                 // select first proposition
1190                 var $prop1 = $advanced.find('> form li:first');
1191                 $prop1.find('.searchview_extended_prop_field').val('dummy').change();
1192                 $prop1.find('.searchview_extended_prop_op').val('ilike');
1193                 $prop1.find('.searchview_extended_prop_value input')
1194                      .val("stupid value");
1195
1196                 // select first proposition
1197                 var $prop2 = $advanced.find('> form li:last');
1198                 // need to trigger event manually or op not changed
1199                 $prop2.find('.searchview_extended_prop_field').val('id').change();
1200                 $prop2.find('.searchview_extended_prop_op').val('=');
1201                 $prop2.find('.searchview_extended_prop_value input')
1202                      .val(42);
1203                 // validate advanced search
1204                 $advanced.find('button.oe_apply').click();
1205
1206                 // resulting search
1207                 equal(view.query.length, 1, "search query should have a single facet");
1208                 var facet = view.query.at(0);
1209                 ok(!facet.get('field').get_context(facet),
1210                    "advanced search facets should yield no context");
1211                 deepEqual(facet.get('field').get_domain(facet),
1212                           ['|', ['dummy', 'ilike', "stupid value"],
1213                                 ['id', '=', 42]],
1214                           "advanced search facet should return proposed domain");
1215             });
1216     });
1217     // TODO: UI tests?
1218 });