Merge lp:~openerp-dev/openerp-web/7.0-searchview-invisibles-xmo into lp:openerp-web/7.0

Proposed by Xavier (Open ERP)
Status: Merged
Merged at revision: 3803
Proposed branch: lp:~openerp-dev/openerp-web/7.0-searchview-invisibles-xmo
Merge into: lp:openerp-web/7.0
Diff against target: 643 lines (+284/-83)
4 files modified
addons/web/doc/search_view.rst (+11/-0)
addons/web/static/src/js/search.js (+107/-64)
addons/web/static/src/xml/base.xml (+1/-10)
addons/web/static/test/search.js (+165/-9)
To merge this branch: bzr merge lp:~openerp-dev/openerp-web/7.0-searchview-invisibles-xmo
Reviewer Review Type Date Requested Status
Yannick Vaucher @ Camptocamp (community) test Approve
OpenERP Core Team Pending
Review via email: mp+148386@code.launchpad.net

Description of the change

Re-introduces handling of invisible fields and groups in the search view, forgotten during the reimplementation of the unified search view for 7.0

This handling is important as it's how @groups is handled (invisible fields and filters can't be removed altogether as e.g. defaults still need to work correctly).

To post a comment you must log in.
Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote :

It works

Test done:
- Install crm.
- Add a user1 with group Sales / User: Own Leads Only
- Add a user2 with groups Sales / User: Own Leads Only and Technical Features
- Edit the view search to set Customers filter under Technical Features group.
groups="base.group_no_one"

Expected:
user1 doesn't see the filter in partner search view OK
user2 see the filter in partner search view OK

review: Approve (test)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'addons/web/doc/search_view.rst'
2--- addons/web/doc/search_view.rst 2012-11-14 23:00:42 +0000
3+++ addons/web/doc/search_view.rst 2013-02-14 08:53:20 +0000
4@@ -107,6 +107,12 @@
5 items, it *should* prefix those with a section title using its own
6 name. This has no technical consequence but is clearer for users.
7
8+.. note::
9+
10+ If a field is :js:func:`invisible
11+ <openerp.web.search.Input.visible>`, its completion function will
12+ *not* be called.
13+
14 Providing drawer/supplementary UI
15 +++++++++++++++++++++++++++++++++
16
17@@ -145,6 +151,11 @@
18 dynamically collects, lays out and renders filters? =>
19 exercises drawer thingies
20
21+.. note::
22+
23+ An :js:func:`invisible <openerp.web.search.Input.visible>` input
24+ will not be inserted into the drawer.
25+
26 Converting from facet objects
27 +++++++++++++++++++++++++++++
28
29
30=== modified file 'addons/web/static/src/js/search.js'
31--- addons/web/static/src/js/search.js 2013-01-31 13:51:25 +0000
32+++ addons/web/static/src/js/search.js 2013-02-14 08:53:20 +0000
33@@ -359,7 +359,7 @@
34 this.has_defaults = !_.isEmpty(this.defaults);
35
36 this.inputs = [];
37- this.controls = {};
38+ this.controls = [];
39
40 this.headless = this.options.hidden && !this.has_defaults;
41
42@@ -499,6 +499,7 @@
43 */
44 complete_global_search: function (req, resp) {
45 $.when.apply(null, _(this.inputs).chain()
46+ .filter(function (input) { return input.visible(); })
47 .invoke('complete', req.term)
48 .value()).then(function () {
49 resp(_(_(arguments).compact()).flatten(true));
50@@ -587,18 +588,18 @@
51 *
52 * @param {Array} items a list of nodes to convert to widgets
53 * @param {Object} fields a mapping of field names to (ORM) field attributes
54- * @param {String} [group_name] name of the group to put the new controls in
55+ * @param {Object} [group] group to put the new controls in
56 */
57- make_widgets: function (items, fields, group_name) {
58- group_name = group_name || null;
59- if (!(group_name in this.controls)) {
60- this.controls[group_name] = [];
61+ make_widgets: function (items, fields, group) {
62+ if (!group) {
63+ group = new instance.web.search.Group(
64+ this, 'q', {attrs: {string: _t("Filters")}});
65 }
66- var self = this, group = this.controls[group_name];
67+ var self = this;
68 var filters = [];
69 _.each(items, function (item) {
70 if (filters.length && item.tag !== 'filter') {
71- group.push(new instance.web.search.FilterGroup(filters, this));
72+ group.push(new instance.web.search.FilterGroup(filters, group));
73 filters = [];
74 }
75
76@@ -606,15 +607,18 @@
77 case 'separator': case 'newline':
78 break;
79 case 'filter':
80- filters.push(new instance.web.search.Filter(item, this));
81+ filters.push(new instance.web.search.Filter(item, group));
82 break;
83 case 'group':
84- self.make_widgets(item.children, fields, item.attrs.string);
85+ self.make_widgets(item.children, fields,
86+ new instance.web.search.Group(group, 'w', item));
87 break;
88 case 'field':
89- group.push(this.make_field(item, fields[item['attrs'].name]));
90+ var field = this.make_field(
91+ item, fields[item['attrs'].name], group);
92+ group.push(field);
93 // filters
94- self.make_widgets(item.children, fields, group_name);
95+ self.make_widgets(item.children, fields, group);
96 break;
97 }
98 }, this);
99@@ -629,12 +633,13 @@
100 *
101 * @param {Object} item fields_view_get node for the field
102 * @param {Object} field fields_get result for the field
103+ * @param {Object} [parent]
104 * @returns instance.web.search.Field
105 */
106- make_field: function (item, field) {
107+ make_field: function (item, field, parent) {
108 var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]);
109 if(obj) {
110- return new (obj) (item, field, this);
111+ return new (obj) (item, field, parent || this);
112 } else {
113 console.group('Unknown field type ' + field.type);
114 console.error('View node', item);
115@@ -869,13 +874,18 @@
116 * @constructs instance.web.search.Widget
117 * @extends instance.web.Widget
118 *
119- * @param view the ancestor view of this widget
120+ * @param parent parent of this widget
121 */
122- init: function (view) {
123- this._super(view);
124- this.view = view;
125+ init: function (parent) {
126+ this._super(parent);
127+ var ancestor = parent;
128+ do {
129+ this.view = ancestor;
130+ } while (!(ancestor instanceof instance.web.SearchView)
131+ && (ancestor = (ancestor.getParent && ancestor.getParent())));
132 }
133 });
134+
135 instance.web.search.add_expand_listener = function($root) {
136 $root.find('a.searchview_group_string').click(function (e) {
137 $root.toggleClass('folded expanded');
138@@ -884,13 +894,24 @@
139 });
140 };
141 instance.web.search.Group = instance.web.search.Widget.extend({
142- template: 'SearchView.group',
143- init: function (view_section, view, fields) {
144- this._super(view);
145- this.attrs = view_section.attrs;
146- this.lines = view.make_widgets(
147- view_section.children, fields);
148- }
149+ init: function (parent, icon, node) {
150+ this._super(parent);
151+ var attrs = node.attrs;
152+ this.modifiers = attrs.modifiers =
153+ attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
154+ this.attrs = attrs;
155+ this.icon = icon;
156+ this.name = attrs.string;
157+ this.children = [];
158+
159+ this.view.controls.push(this);
160+ },
161+ push: function (input) {
162+ this.children.push(input);
163+ },
164+ visible: function () {
165+ return !this.modifiers.invisible;
166+ },
167 });
168
169 instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{
170@@ -899,12 +920,12 @@
171 * @constructs instance.web.search.Input
172 * @extends instance.web.search.Widget
173 *
174- * @param view
175+ * @param parent
176 */
177- init: function (view) {
178- this._super(view);
179+ init: function (parent) {
180+ this._super(parent);
181+ this.load_attrs({});
182 this.view.inputs.push(this);
183- this.style = undefined;
184 },
185 /**
186 * Fetch auto-completion values for the widget.
187@@ -952,15 +973,30 @@
188 "get_domain not implemented for widget " + this.attrs.type);
189 },
190 load_attrs: function (attrs) {
191- if (attrs.modifiers) {
192- attrs.modifiers = JSON.parse(attrs.modifiers);
193- attrs.invisible = attrs.modifiers.invisible || false;
194- if (attrs.invisible) {
195- this.style = 'display: none;'
196+ attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
197+ this.attrs = attrs;
198+ },
199+ /**
200+ * Returns whether the input is "visible". The default behavior is to
201+ * query the ``modifiers.invisible`` flag on the input's description or
202+ * view node.
203+ *
204+ * @returns {Boolean}
205+ */
206+ visible: function () {
207+ if (this.attrs.modifiers.invisible) {
208+ return false;
209+ }
210+ var parent = this;
211+ while ((parent = parent.getParent()) &&
212+ ( (parent instanceof instance.web.search.Group)
213+ || (parent instanceof instance.web.search.Input))) {
214+ if (!parent.visible()) {
215+ return false;
216 }
217 }
218- this.attrs = attrs;
219- }
220+ return true;
221+ },
222 });
223 instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
224 template: 'SearchView.filters',
225@@ -974,17 +1010,17 @@
226 * @extends instance.web.search.Input
227 *
228 * @param {Array<instance.web.search.Filter>} filters elements of the group
229- * @param {instance.web.SearchView} view view in which the filters are contained
230+ * @param {instance.web.SearchView} parent parent in which the filters are contained
231 */
232- init: function (filters, view) {
233+ init: function (filters, parent) {
234 // If all filters are group_by and we're not initializing a GroupbyGroup,
235 // create a GroupbyGroup instead of the current FilterGroup
236 if (!(this instanceof instance.web.search.GroupbyGroup) &&
237 _(filters).all(function (f) {
238 return f.attrs.context && f.attrs.context.group_by; })) {
239- return new instance.web.search.GroupbyGroup(filters, view);
240+ return new instance.web.search.GroupbyGroup(filters, parent);
241 }
242- this._super(view);
243+ this._super(parent);
244 this.filters = filters;
245 this.view.query.on('add remove change reset', this.proxy('search_change'));
246 },
247@@ -1103,6 +1139,7 @@
248 var self = this;
249 item = item.toLowerCase();
250 var facet_values = _(this.filters).chain()
251+ .filter(function (filter) { return filter.visible(); })
252 .filter(function (filter) {
253 var at = {
254 string: filter.attrs.string || '',
255@@ -1129,8 +1166,8 @@
256 instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
257 icon: 'w',
258 completion_label: _lt("Group by: %s"),
259- init: function (filters, view) {
260- this._super(filters, view);
261+ init: function (filters, parent) {
262+ this._super(filters, parent);
263 // Not flanders: facet unicity is handled through the
264 // (category, field) pair of facet attributes. This is all well and
265 // good for regular filter groups where a group matches a facet, but for
266@@ -1138,8 +1175,8 @@
267 // view which proxies to the first GroupbyGroup, so it can be used
268 // for every GroupbyGroup and still provides the various methods needed
269 // by the search view. Use weirdo name to avoid risks of conflicts
270- if (!this.getParent()._s_groupby) {
271- this.getParent()._s_groupby = {
272+ if (!this.view._s_groupby) {
273+ this.view._s_groupby = {
274 help: "See GroupbyGroup#init",
275 get_context: this.proxy('get_context'),
276 get_domain: this.proxy('get_domain'),
277@@ -1148,7 +1185,7 @@
278 }
279 },
280 match_facet: function (facet) {
281- return facet.get('field') === this.getParent()._s_groupby;
282+ return facet.get('field') === this.view._s_groupby;
283 },
284 make_facet: function (values) {
285 return {
286@@ -1173,10 +1210,10 @@
287 * @extends instance.web.search.Input
288 *
289 * @param node
290- * @param view
291+ * @param parent
292 */
293- init: function (node, view) {
294- this._super(view);
295+ init: function (node, parent) {
296+ this._super(parent);
297 this.load_attrs(node.attrs);
298 },
299 facet_for: function () { return $.when(null); },
300@@ -1192,10 +1229,10 @@
301 *
302 * @param view_section
303 * @param field
304- * @param view
305+ * @param parent
306 */
307- init: function (view_section, field, view) {
308- this._super(view);
309+ init: function (view_section, field, parent) {
310+ this._super(parent);
311 this.load_attrs(_.extend({}, field, view_section.attrs));
312 },
313 facet_for: function (value) {
314@@ -1235,7 +1272,7 @@
315 *
316 * @param {String} name the field's name
317 * @param {String} operator the field's operator (either attribute-specified or default operator for the field
318- * @param {Number|String} value parsed value for the field
319+ * @param {Number|String} facet parsed value for the field
320 * @returns {Array<Array>} domain to include in the resulting search
321 */
322 make_domain: function (name, operator, facet) {
323@@ -1467,8 +1504,8 @@
324 });
325 instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
326 default_operator: {},
327- init: function (view_section, field, view) {
328- this._super(view_section, field, view);
329+ init: function (view_section, field, parent) {
330+ this._super(view_section, field, parent);
331 this.model = new instance.web.Model(this.attrs.relation);
332 },
333 complete: function (needle) {
334@@ -1703,22 +1740,28 @@
335 var running_count = 0;
336 // get total filters count
337 var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
338- var filters_count = _(this.view.controls).chain()
339+ var visible_filters = _(this.view.controls).chain().reject(function (group) {
340+ return _(_(group.children).filter(is_group)).isEmpty()
341+ || group.modifiers.invisible;
342+ });
343+ var filters_count = visible_filters
344+ .pluck('children')
345 .flatten()
346 .filter(is_group)
347 .map(function (i) { return i.filters.length; })
348 .sum()
349 .value();
350
351- var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) {
352- var filters = _(inputs).filter(is_group);
353- return {
354- name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group,
355- filters: filters,
356- length: _(filters).chain().map(function (i) {
357- return i.filters.length; }).sum().value()
358- };
359- });
360+ var col1 = [], col2 = visible_filters.map(function (group) {
361+ var filters = _(group.children).filter(is_group);
362+ return {
363+ name: _.str.sprintf("<span class='oe_i'>%s</span> %s",
364+ group.icon, group.name),
365+ filters: filters,
366+ length: _(filters).chain().map(function (i) {
367+ return i.filters.length; }).sum().value()
368+ };
369+ }).value();
370
371 while (col2.length) {
372 // col1 + group should be smaller than col2 + group
373
374=== modified file 'addons/web/static/src/xml/base.xml'
375--- addons/web/static/src/xml/base.xml 2013-02-13 15:21:58 +0000
376+++ addons/web/static/src/xml/base.xml 2013-02-14 08:53:20 +0000
377@@ -1496,7 +1496,7 @@
378 <t t-esc="attrs.string"/>
379 </button>
380 <ul t-name="SearchView.filters">
381- <li t-foreach="widget.filters" t-as="filter"
382+ <li t-foreach="widget.filters" t-as="filter" t-if="filter.visible()"
383 t-att-title="filter.attrs.string ? filter.attrs.help : undefined">
384 <t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
385 </li>
386@@ -1584,15 +1584,6 @@
387 </div>
388 </div>
389 </t>
390-<t t-name="SearchView.group">
391- <t t-call="SearchView.util.expand">
392- <t t-set="expand" t-value="attrs.expand"/>
393- <t t-set="label" t-value="attrs.string"/>
394- <t t-set="content">
395- <t t-call="SearchView.render_lines"/>
396- </t>
397- </t>
398-</t>
399 <div t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section">
400
401 </div>
402
403=== modified file 'addons/web/static/test/search.js'
404--- addons/web/static/test/search.js 2013-01-31 11:26:17 +0000
405+++ addons/web/static/test/search.js 2013-02-14 08:53:20 +0000
406@@ -1,4 +1,4 @@
407-openerp.testing.section('query', {
408+openerp.testing.section('search.query', {
409 dependencies: ['web.search']
410 }, function (test) {
411 test('Adding a facet to the query creates a facet and a value', function (instance) {
412@@ -180,7 +180,7 @@
413 });
414 return view;
415 };
416-openerp.testing.section('defaults', {
417+openerp.testing.section('search.defaults', {
418 dependencies: ['web.search'],
419 rpc: 'mock',
420 templates: true,
421@@ -331,7 +331,7 @@
422 });
423 });
424 });
425-openerp.testing.section('completions', {
426+openerp.testing.section('search.completions', {
427 dependencies: ['web.search'],
428 rpc: 'mock',
429 templates: true
430@@ -564,7 +564,7 @@
431 });
432 });
433 });
434-openerp.testing.section('search-serialization', {
435+openerp.testing.section('search.serialization', {
436 dependencies: ['web.search'],
437 rpc: 'mock',
438 templates: true
439@@ -871,7 +871,7 @@
440 return $.when(t1, t2);
441 });
442 });
443-openerp.testing.section('removal', {
444+openerp.testing.section('search.removal', {
445 dependencies: ['web.search'],
446 rpc: 'mock',
447 templates: true
448@@ -894,7 +894,7 @@
449 });
450 });
451 });
452-openerp.testing.section('drawer', {
453+openerp.testing.section('search.drawer', {
454 dependencies: ['web.search'],
455 rpc: 'mock',
456 templates: true
457@@ -910,7 +910,7 @@
458 });
459 });
460 });
461-openerp.testing.section('filters', {
462+openerp.testing.section('search.filters', {
463 dependencies: ['web.search'],
464 rpc: 'mock',
465 templates: true,
466@@ -995,7 +995,7 @@
467 });
468 });
469 });
470-openerp.testing.section('saved_filters', {
471+openerp.testing.section('search.filters.saved', {
472 dependencies: ['web.search'],
473 rpc: 'mock',
474 templates: true
475@@ -1077,7 +1077,7 @@
476 });
477 });
478 });
479-openerp.testing.section('advanced', {
480+openerp.testing.section('search.advanced', {
481 dependencies: ['web.search'],
482 rpc: 'mock',
483 templates: true
484@@ -1158,3 +1158,159 @@
485 });
486 // TODO: UI tests?
487 });
488+openerp.testing.section('search.invisible', {
489+ dependencies: ['web.search'],
490+ rpc: 'mock',
491+ templates: true,
492+}, function (test) {
493+ var registerTestField = function (instance, methods) {
494+ instance.web.search.fields.add('test', 'instance.testing.TestWidget');
495+ instance.testing = {
496+ TestWidget: instance.web.search.Field.extend(methods),
497+ };
498+ };
499+ var makeView = function (instance, mock, fields, arch, defaults) {
500+ mock('ir.filters:get_filters', function () { return []; });
501+ mock('test.model:fields_get', function () { return fields; });
502+ mock('test.model:fields_view_get', function () {
503+ return { type: 'search', fields: fields, arch: arch };
504+ });
505+ var ds = new instance.web.DataSet(null, 'test.model');
506+ return new instance.web.SearchView(null, ds, false, defaults);
507+ };
508+ // Invisible fields should not auto-complete
509+ test('invisible-field-no-autocomplete', {asserts: 1}, function (instance, $fix, mock) {
510+ registerTestField(instance, {
511+ complete: function () {
512+ return $.when([{label: this.attrs.string}]);
513+ },
514+ });
515+ var view = makeView(instance, mock, {
516+ field0: {type: 'test', string: 'Field 0'},
517+ field1: {type: 'test', string: 'Field 1'},
518+ }, ['<search>',
519+ '<field name="field0"/>',
520+ '<field name="field1" modifiers="{&quot;invisible&quot;: true}"/>',
521+ '</search>'].join());
522+ return view.appendTo($fix)
523+ .then(function () {
524+ var done = $.Deferred();
525+ view.complete_global_search({term: 'test'}, function (comps) {
526+ done.resolve(comps);
527+ });
528+ return done;
529+ }).then(function (completions) {
530+ deepEqual(completions, [{label: 'Field 0'}],
531+ "should only complete the visible field");
532+ });
533+ });
534+ // Invisible filters should not appear in the drawer
535+ test('invisible-filter-no-drawer', {asserts: 4}, function (instance, $fix, mock) {
536+ var view = makeView(instance, mock, {}, [
537+ '<search>',
538+ '<filter string="filter 0"/>',
539+ '<filter string="filter 1" modifiers="{&quot;invisible&quot;: true}"/>',
540+ '</search>'].join());
541+ return view.appendTo($fix)
542+ .then(function () {
543+ var $fs = $fix.find('.oe_searchview_filters ul');
544+ strictEqual($fs.children().length,
545+ 1,
546+ "should only display one filter");
547+ strictEqual(_.str.trim($fs.children().text()),
548+ "filter 0",
549+ "should only display filter 0");
550+ var done = $.Deferred();
551+ view.complete_global_search({term: 'filter'}, function (comps) {
552+ done.resolve();
553+ strictEqual(comps.length, 1, "should only complete visible filter");
554+ strictEqual(comps[0].label, "Filter on: filter 0",
555+ "should complete filter 0");
556+ });
557+ return done;
558+ });
559+ });
560+ // Invisible filter groups should not appear in the drawer
561+ // Group invisibility should be inherited by children
562+ test('group-invisibility', {asserts: 6}, function (instance, $fix, mock) {
563+ registerTestField(instance, {
564+ complete: function () {
565+ return $.when([{label: this.attrs.string}]);
566+ },
567+ });
568+ var view = makeView(instance, mock, {
569+ field0: {type: 'test', string: 'Field 0'},
570+ field1: {type: 'test', string: 'Field 1'},
571+ }, [
572+ '<search>',
573+ '<group string="Visibles">',
574+ '<field name="field0"/>',
575+ '<filter string="Filter 0"/>',
576+ '</group>',
577+ '<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
578+ '<field name="field1"/>',
579+ '<filter string="Filter 1"/>',
580+ '</group>',
581+ '</search>'
582+ ].join(''));
583+ return view.appendTo($fix)
584+ .then(function () {
585+ strictEqual($fix.find('.oe_searchview_filters h3').length,
586+ 1,
587+ "should only display one group");
588+ strictEqual($fix.find('.oe_searchview_filters h3').text(),
589+ 'w Visibles',
590+ "should only display the Visibles group (and its icon char)");
591+
592+ var $fs = $fix.find('.oe_searchview_filters ul');
593+ strictEqual($fs.children().length, 1,
594+ "should only have one filter in the drawer");
595+ strictEqual(_.str.trim($fs.text()), "Filter 0",
596+ "should have filter 0 as sole filter");
597+
598+ var done = $.Deferred();
599+ view.complete_global_search({term: 'filter'}, function (compls) {
600+ done.resolve();
601+ strictEqual(compls.length, 2,
602+ "should have 2 completions");
603+ deepEqual(_.pluck(compls, 'label'),
604+ ['Field 0', 'Filter on: Filter 0'],
605+ "should complete on field 0 and filter 0");
606+ });
607+ return done;
608+ });
609+ });
610+ // Default on invisible fields should still work, for fields and filters both
611+ test('invisible-defaults', {asserts: 1}, function (instance, $fix, mock) {
612+ var view = makeView(instance, mock, {
613+ field: {type: 'char', string: "Field"},
614+ field2: {type: 'char', string: "Field 2"},
615+ }, [
616+ '<search>',
617+ '<field name="field2"/>',
618+ '<filter name="filter2" string="Filter"',
619+ ' domain="[[\'qwa\', \'=\', 42]]"/>',
620+ '<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
621+ '<field name="field"/>',
622+ '<filter name="filter" string="Filter"',
623+ ' domain="[[\'whee\', \'=\', \'42\']]"/>',
624+ '</group>',
625+ '</search>'
626+ ].join(''), {field: "foo", filter: true});
627+
628+ return view.appendTo($fix)
629+ .then(function () {
630+ deepEqual(view.build_search_data(), {
631+ errors: [],
632+ groupbys: [],
633+ contexts: [],
634+ domains: [
635+ // Generated from field
636+ [['field', 'ilike', 'foo']],
637+ // generated from filter
638+ "[['whee', '=', '42']]"
639+ ],
640+ }, "should yield invisible fields selected by defaults");
641+ });
642+ });
643+});