@cocreate/elements 1.45.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ const {test}=require('node:test');
2
+ const assert=require('node:assert/strict');
3
+ const fs=require('node:fs');
4
+ const vm=require('node:vm');
5
+ const source=fs.readFileSync(require.resolve('../src/srcHandler.js'),'utf8');
6
+ test('deferred SSR source fetches, renders and clears its 304 marker',async()=>{
7
+ const attrs=new Map([['src','/component.html'],['error','304']]);let content,requests=0;
8
+ const el={tagName:'DIV',getAttribute:key=>attrs.get(key)??null,hasAttribute:key=>attrs.has(key),removeAttribute:key=>attrs.delete(key),setValue:value=>{content=value;}};
9
+ const init=vm.runInNewContext(source.slice(source.indexOf('async function initElements('),source.indexOf('async function initMediaSource('))+'\ninitElements',{initializing:new WeakMap(),isSameOrigin:()=>true,sessionStorage:{getItem:()=>null},fetch:async()=>{requests++;return {status:200,ok:true,text:async()=>'<p>Cached or fetched content</p>'};},console,ObjectId:()=>''});
10
+ await init([el]);assert.equal(requests,1);assert.equal(content,'<p>Cached or fetched content</p>');assert.equal(attrs.has('error'),false);
11
+ });
@@ -0,0 +1,48 @@
1
+ const assert = require('node:assert/strict');
2
+ const { readFileSync } = require('node:fs');
3
+ const path = require('node:path');
4
+ const vm = require('node:vm');
5
+ const test = require('node:test');
6
+ const source = readFileSync(path.join(__dirname, '../src/formHandler.js'), 'utf8');
7
+ const handler = source.slice(source.indexOf('function handleEnter('), source.indexOf('\n/**\n * @param form'));
8
+ function run(options = {}) {
9
+ let clicked = 0, prevented = 0, queried = 0;
10
+ const form = { contains: () => true };
11
+ const scope = { getAttribute: () => '#next' };
12
+ const target = {
13
+ form, hasAttribute: () => true,
14
+ matches: () => options.disabled || false,
15
+ closest: () => options.hidden ? {} : null,
16
+ getClientRects: () => options.invisible ? [] : [{}],
17
+ click: () => clicked++
18
+ };
19
+ const input = {
20
+ tagName: options.tagName || 'INPUT', type: options.type || 'text', form,
21
+ closest: selector => selector === '[submit-query]' ? (options.noQuery ? null : scope) : (options.widget ? {} : null)
22
+ };
23
+ const event = { key: 'Enter', target: input, currentTarget: form, preventDefault: () => prevented++, ...options.event };
24
+ vm.runInNewContext(handler + '\nhandleEnter(event);', {
25
+ event, getComputedStyle: () => ({ visibility: 'visible' }),
26
+ queryElements: ({ element }) => { assert.equal(element, scope); queried++; return options.multiple ? [target, target] : [target]; }
27
+ });
28
+ return { clicked, prevented, queried };
29
+ }
30
+ test('an explicit query clicks its target once', () => {
31
+ assert.deepEqual(run(), { clicked: 1, prevented: 1, queried: 1 });
32
+ });
33
+ test('without a query, native behavior is left alone', () => {
34
+ assert.deepEqual(run({noQuery:true}), {clicked:0,prevented:0,queried:0});
35
+ });
36
+ for (const option of ['disabled','hidden','invisible','multiple']) {
37
+ test(`${option} targets are not clicked and cannot fall through to submission`, () => {
38
+ const result=run({[option]:true}); assert.equal(result.clicked,0);assert.equal(result.prevented,1);
39
+ });
40
+ }
41
+ for (const options of [{tagName:'TEXTAREA'},{tagName:'SELECT'},{type:'checkbox'},{type:'submit'},{widget:true},{event:{isComposing:true}},{event:{ctrlKey:true}},{event:{defaultPrevented:true}}]) {
42
+ test(`preserves control behavior: ${JSON.stringify(options)}`,()=>{
43
+ assert.deepEqual(run(options),{clicked:0,prevented:0,queried:0});
44
+ });
45
+ }
46
+ test('held Enter does not repeat or fall through to native submission',()=>{
47
+ assert.deepEqual(run({event:{repeat:true}}),{clicked:0,prevented:1,queried:0});
48
+ });
@@ -0,0 +1,100 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const test = require('node:test');
5
+ const vm = require('node:vm');
6
+
7
+ async function setup() {
8
+ const context = vm.createContext({
9
+ localStorage: { getItem: () => null }
10
+ });
11
+ const registries = {
12
+ elements: new Map(),
13
+ forms: new Map(),
14
+ keys: new Map(),
15
+ elementSubscriptions: new Map(),
16
+ subscriptions: new Map()
17
+ };
18
+ const stubs = {
19
+ '@cocreate/utils': {
20
+ getAttributes: element => ({ ...element.data }),
21
+ checkValue: () => true
22
+ },
23
+ 'mapRegistry.js': registries,
24
+ 'subscribeHandler.js': { getSubscription: () => null },
25
+ 'crudHandler.js': { read() {}, save() {} }
26
+ };
27
+ const modules = new Map();
28
+
29
+ function load(name) {
30
+ name = name.startsWith('./') ? name.slice(2) : name;
31
+ if (modules.has(name)) return modules.get(name);
32
+
33
+ const stub = stubs[name];
34
+ const module = stub
35
+ ? new vm.SyntheticModule(Object.keys(stub), function () {
36
+ for (const [key, value] of Object.entries(stub)) {
37
+ this.setExport(key, value);
38
+ }
39
+ }, { context })
40
+ : new vm.SourceTextModule(
41
+ fs.readFileSync(path.join(__dirname, '../src', name), 'utf8'),
42
+ { context, identifier: name }
43
+ );
44
+ modules.set(name, module);
45
+ return module;
46
+ }
47
+
48
+ const module = load('getDataHandler.js');
49
+ await module.link(load);
50
+ await module.evaluate();
51
+ return module.namespace;
52
+ }
53
+
54
+ function field(form, key, value) {
55
+ return {
56
+ data: { array: 'organizations', object: '', key },
57
+ form,
58
+ attributes: [],
59
+ closest: () => form,
60
+ getAttribute: name => name === 'key' ? key : null,
61
+ hasAttribute: name => name === 'key',
62
+ getValue: async () => value,
63
+ matches: selector => selector !== '[render-query]' && selector !== 'input[type="file"]'
64
+ };
65
+ }
66
+
67
+ test('getData preserves each field key when controls share an object scope', async () => {
68
+ const { getData, getDataObject, initDataKey } = await setup();
69
+ const form = { elements: [] };
70
+ const fields = [
71
+ field(form, 'name', 'Acme'),
72
+ field(form, '_id', 'org-1'),
73
+ field(form, 'key', 'secret')
74
+ ];
75
+ form.elements = fields;
76
+ for (const element of fields) initDataKey(element, getDataObject(element));
77
+
78
+ const [payload] = await getData(form);
79
+ assert.deepEqual(
80
+ JSON.parse(JSON.stringify(payload.object)),
81
+ { name: 'Acme', _id: 'org-1', key: 'secret' }
82
+ );
83
+ });
84
+
85
+ test('getData does not apply a whole-object key to neighboring fields', async () => {
86
+ const { getData, getDataObject, initDataKey } = await setup();
87
+ const form = { elements: [] };
88
+ const fields = [
89
+ field(form, '{}', { name: 'Acme' }),
90
+ field(form, '_id', 'org-1')
91
+ ];
92
+ form.elements = fields;
93
+ for (const element of fields) initDataKey(element, getDataObject(element));
94
+
95
+ const [payload] = await getData(form);
96
+ assert.deepEqual(
97
+ JSON.parse(JSON.stringify(payload.object)),
98
+ { name: 'Acme', _id: 'org-1' }
99
+ );
100
+ });
@@ -0,0 +1,145 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const vm = require('node:vm');
5
+ const test = require('node:test');
6
+
7
+ async function setup() {
8
+ const requests = [], jobs = new Map(), errors = [], observers = [];
9
+ let timerId = 0;
10
+ const context = vm.createContext({
11
+ console: { error: (...args) => errors.push(args), log() {} },
12
+ document: { querySelectorAll: () => [] },
13
+ localStorage: { getItem: () => null },
14
+ HTMLCollection: class {}, NodeList: class {},
15
+ setTimeout: (callback) => { jobs.set(++timerId, callback); return timerId; },
16
+ clearTimeout: (id) => jobs.delete(id)
17
+ });
18
+ const stubs = {
19
+ '@cocreate/observer': { default: { init(options) { observers.push(options); } } },
20
+ '@cocreate/actions': { default: { init() {} } },
21
+ '@cocreate/socket-client': { default: { send() {} } },
22
+ '@cocreate/crud-client': { default: { listen() {}, send: async (data) => { requests.push(data); } } },
23
+ '@cocreate/filter': { default: { init: async (element) => { if (element.ready) await element.ready; } } },
24
+ '@cocreate/utils': {
25
+ getAttributes: (element) => ({ ...element.data }),
26
+ getAttributeNames: () => [], queryElements: () => [],
27
+ checkValue: (value) => !/{{.*}}/.test(value)
28
+ },
29
+ 'setDataHandler.js': { setData() {}, setDataType() {} }
30
+ };
31
+ const modules = new Map();
32
+ function load(name) {
33
+ name = name.startsWith('./') ? name.slice(2) : name;
34
+ if (modules.has(name)) return modules.get(name);
35
+ const stub = stubs[name];
36
+ const module = stub
37
+ ? new vm.SyntheticModule(Object.keys(stub), function () {
38
+ for (const [key, value] of Object.entries(stub)) this.setExport(key, value);
39
+ }, { context })
40
+ : new vm.SourceTextModule(fs.readFileSync(path.join(__dirname, '../src', name), 'utf8'), { context, identifier: name });
41
+ modules.set(name, module);
42
+ return module;
43
+ }
44
+ const module = load('initHandler.js');
45
+ await module.link(load);
46
+ await module.evaluate();
47
+ return {
48
+ init: module.namespace.init, requests, errors, observers,
49
+ flush() { const batch = [...jobs.values()]; jobs.clear(); for (const job of batch) job(); }
50
+ };
51
+ }
52
+ function element(id, attributes = {}) {
53
+ return {
54
+ tagName: 'DIV', data: { array: 'apis', object: id, key: 'name' },
55
+ getAttribute: (key) => attributes[key] ?? null,
56
+ hasAttribute: (key) => Object.hasOwn(attributes, key),
57
+ addEventListener() {}
58
+ };
59
+ }
60
+ const tick = () => new Promise(resolve => setImmediate(resolve));
61
+
62
+ test('filtered counts for different clusters are read in separate batches', async () => {
63
+ const f = await setup();
64
+ const counts = ['cluster-a', 'cluster-b'].map(cluster => {
65
+ const el = element('');
66
+ el.data = { array: 'servers', object: '', key: '$length' };
67
+ el.getFilter = async () => ({ query: { cluster_id: cluster }, index: 0 });
68
+ return el;
69
+ });
70
+ await f.init(counts);
71
+ f.flush();
72
+ assert.equal(f.requests.length, 2);
73
+ assert.deepEqual(f.requests.map(data => data.filter.query.cluster_id), ['cluster-a', 'cluster-b']);
74
+ });
75
+
76
+ test('a pending field does not block reads for ready elements in the same SSR page', async () => {
77
+ const f = await setup();
78
+ const slow = element('111111111111111111111111');
79
+ const ready = element('222222222222222222222222', { rendered: 'true' });
80
+ let release;
81
+ slow.ready = new Promise(resolve => { release = resolve; });
82
+ const pending = f.init([slow, ready]);
83
+ await tick();
84
+ f.flush();
85
+ try {
86
+ assert.equal(f.requests.length, 1);
87
+ assert.equal(f.requests[0].object._id, ready.data.object);
88
+ } finally {
89
+ release();
90
+ await pending;
91
+ }
92
+ f.flush();
93
+ assert.equal(f.requests.length, 2);
94
+ });
95
+
96
+ test('one failed initialization does not prevent other reads and can be retried', async () => {
97
+ const f = await setup();
98
+ const broken = element('111111111111111111111111');
99
+ const ready = element('222222222222222222222222');
100
+ broken.ready = Promise.reject(new Error('value lookup failed'));
101
+ await f.init([broken, ready]);
102
+ f.flush();
103
+ assert.equal(f.requests.length, 1);
104
+ assert.equal(f.errors.length, 1);
105
+ delete broken.ready;
106
+ await f.init(broken);
107
+ f.flush();
108
+ assert.equal(f.requests.length, 2);
109
+ });
110
+
111
+ test('ready fields for the same document still share one debounced read', async () => {
112
+ const f = await setup();
113
+ await f.init([element('111111111111111111111111'), element('111111111111111111111111')]);
114
+ f.flush();
115
+ assert.equal(f.requests.length, 1);
116
+ });
117
+
118
+ test('read=false and already read elements remain excluded', async () => {
119
+ const f = await setup();
120
+ const read = element('222222222222222222222222');
121
+ read.hasRead = true;
122
+ await f.init([element('111111111111111111111111', { read: 'false' }), read]);
123
+ f.flush();
124
+ assert.equal(f.requests.length, 0);
125
+ });
126
+
127
+ test('changing read from false to true reads the current composed filter', async () => {
128
+ const f = await setup();
129
+ const attributes = { read: 'false' };
130
+ const el = element('', attributes);
131
+ el.data = { array: 'files', object: '', key: '' };
132
+ el.getFilter = async () => ({ query: { path: '/' }, index: 0 });
133
+
134
+ await f.init(el);
135
+ f.flush();
136
+ assert.equal(f.requests.length, 0);
137
+
138
+ attributes.read = 'true';
139
+ const observer = f.observers.find(item => item.name === 'CoCreateElementsReadAttribute');
140
+ await observer.callback({ target: el, oldValue: 'false' });
141
+ f.flush();
142
+
143
+ assert.equal(f.requests.length, 1);
144
+ assert.equal(f.requests[0].filter.query.path, '/');
145
+ });
@@ -0,0 +1,19 @@
1
+ const {test}=require('node:test');
2
+ const assert=require('node:assert/strict');
3
+ const fs=require('node:fs');
4
+ const vm=require('node:vm');
5
+ const source=fs.readFileSync(require.resolve('../src/setDataHandler.js'),'utf8');
6
+ const loadMore=vm.runInNewContext('('+source.slice(source.indexOf('async function loadMore('),source.indexOf('\nasync function checkFilters'))+')');
7
+ test('append deduplicates without mutating shared cache/server responses',async()=>{
8
+ const response={object:[{_id:'a'},{_id:'b'},{_id:'b'}],filter:{index:3,startingIndex:1}};
9
+ const original=JSON.stringify(response);let rendered;
10
+ await loadMore({getData:async()=>({object:[{_id:'a'}]}),renderValue:async data=>{rendered=data;}},response,'object');
11
+ assert.equal(JSON.stringify(response),original);
12
+ assert.equal(rendered.object.length,1);assert.equal(rendered.object[0]._id,'b');
13
+ assert.equal(rendered.filter.loadMore,true);assert.equal(rendered.filter.overwrite,false);
14
+ });
15
+ test('duplicate-only response leaves existing render untouched',async()=>{
16
+ let called=false;
17
+ await loadMore({getData:async()=>({object:[{_id:'a'}]}),renderValue:async()=>{called=true;}},{object:[{_id:'a'}]},'object');
18
+ assert.equal(called,false);
19
+ });
@@ -0,0 +1,31 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const vm = require('node:vm');
5
+
6
+ const source = fs.readFileSync(require.resolve('../src/crudHandler.js'), 'utf8');
7
+ const saveSource = source.slice(source.indexOf('export async function save('), source.indexOf('\nActions.init(')).replace('export ', '');
8
+
9
+ for (const value of [[{ name: 'image.svg' }], [{ name: 'a.svg' }, { name: 'b.svg' }], { name: 'image.svg' }]) {
10
+ test(`saving an entire document preserves its shape: ${JSON.stringify(value)}`, async () => {
11
+ let sent;
12
+ const element = {
13
+ tagName: 'INPUT',
14
+ getAttribute: name => name === 'key' ? '{}' : null,
15
+ getValue: async () => value,
16
+ setAttribute() {}
17
+ };
18
+ const save = vm.runInNewContext(`(${saveSource})`, {
19
+ elements: new Map([[element, 'files']]),
20
+ keys: new Map([['files', { dataKey: { object: { type: 'object', array: 'files', object: '', key: '{}' } } }]]),
21
+ getSubscription: () => null,
22
+ deriveSubscriptionKeys: () => [],
23
+ CRUD: { send: async data => { sent = data; } }
24
+ });
25
+ await save(element);
26
+ assert.equal(Array.isArray(sent.object), Array.isArray(value));
27
+ assert.equal(JSON.stringify(sent.object), JSON.stringify(value));
28
+ assert.equal(sent.method, 'object.create');
29
+ if (Array.isArray(value)) assert.notEqual(sent.object, value);
30
+ });
31
+ }
@@ -0,0 +1,36 @@
1
+ const assert = require('node:assert/strict');
2
+ const { readFileSync } = require('node:fs');
3
+ const { join } = require('node:path');
4
+ const vm = require('node:vm');
5
+ const test = require('node:test');
6
+
7
+ test('save waits for the response to be applied before its caller continues', async () => {
8
+ const source = readFileSync(join(__dirname, '../src/crudHandler.js'), 'utf8');
9
+ const saveCode = source.slice(source.indexOf('export async function save('), source.indexOf('\nActions.init(')).replace('export ', '');
10
+ let finishApplying;
11
+ let applied = false;
12
+ let completed = false;
13
+ const gate = new Promise(resolve => { finishApplying = resolve; });
14
+ const form = {
15
+ tagName: 'FORM',
16
+ getAttribute: () => null,
17
+ setAttribute() {},
18
+ getData: async () => [{ type: 'object', array: 'clusters', object: { name: 'Cluster' } }]
19
+ };
20
+ const save = vm.runInNewContext(`(${saveCode})`, {
21
+ getSubscription: () => null,
22
+ deriveSubscriptionKeys: () => [],
23
+ CRUD: { send: async () => ({
24
+ type: 'object', method: 'object.create', array: 'clusters',
25
+ object: [{ _id: '65f8cb447b39a3c1a8904e22' }]
26
+ }) },
27
+ setDataType: async () => { await gate; applied = true; }
28
+ });
29
+ const completion = save(form).then(() => { completed = true; });
30
+ await new Promise(resolve => setImmediate(resolve));
31
+ assert.equal(completed, false);
32
+ finishApplying();
33
+ await completion;
34
+ assert.equal(applied, true);
35
+ assert.equal(completed, true);
36
+ });
@@ -0,0 +1,119 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const vm = require("node:vm");
5
+ const test = require("node:test");
6
+
7
+ async function loadSetDataType(watchedElements = []) {
8
+ const context = vm.createContext({
9
+ console,
10
+ HTMLCollection: class {},
11
+ CustomEvent: class {
12
+ constructor(name, options) {
13
+ this.name = name;
14
+ this.detail = options?.detail;
15
+ }
16
+ }
17
+ });
18
+ const source = fs.readFileSync(
19
+ path.join(__dirname, "../src/setDataHandler.js"),
20
+ "utf8"
21
+ );
22
+ const module = new vm.SourceTextModule(source, { context });
23
+ const modules = {
24
+ "@cocreate/utils": {
25
+ dotNotationToObject: value => value,
26
+ queryData: () => true,
27
+ searchData: () => true,
28
+ sortData: value => value,
29
+ getAttributes: element => ({ key: element.getAttribute('key') }),
30
+ checkValue: () => true,
31
+ getValueFromObject: () => undefined
32
+ },
33
+ "@cocreate/crud-client": { default: { getValueFromObject: (object, key) => object[key] } },
34
+ "@cocreate/filter": { default: { filters: new Map() } },
35
+ "./mapRegistry.js": {
36
+ elements: new WeakMap(),
37
+ forms: new WeakMap(),
38
+ keys: new Map()
39
+ },
40
+ "./getDataHandler.js": {
41
+ getDataElements: () => watchedElements,
42
+ getDataObject: () => undefined
43
+ }
44
+ };
45
+
46
+ await module.link(name => new vm.SyntheticModule(
47
+ Object.keys(modules[name]),
48
+ function () {
49
+ for (const [key, value] of Object.entries(modules[name])) {
50
+ this.setExport(key, value);
51
+ }
52
+ },
53
+ { context }
54
+ ));
55
+ await module.evaluate();
56
+ return module.namespace.setDataType;
57
+ }
58
+
59
+ test("setDataType tolerates a form that has not entered the registry yet", async () => {
60
+ const setDataType = await loadSetDataType();
61
+ const attributes = { array: "apis", object: "pending" };
62
+ const form = {
63
+ tagName: "FORM",
64
+ querySelectorAll: () => [],
65
+ getAttribute: name => attributes[name] ?? null,
66
+ hasAttribute: name => Object.hasOwn(attributes, name),
67
+ setAttribute: (name, value) => {
68
+ attributes[name] = value;
69
+ }
70
+ };
71
+
72
+ await assert.doesNotReject(() => setDataType(form, {
73
+ type: "object",
74
+ object: [{
75
+ _id: "6abb2ce702c3c7e6fbec0215",
76
+ $array: "apis"
77
+ }]
78
+ }, { element: { dispatchEvent() {} } }));
79
+ assert.equal(attributes.object, "6abb2ce702c3c7e6fbec0215");
80
+ });
81
+
82
+ test('saved is dispatched only after response fields following async work are updated', async () => {
83
+ let release;
84
+ let idValue = '';
85
+ const gate = new Promise(resolve => { release = resolve; });
86
+ const watcher = attributes => ({
87
+ hasAttribute: name => Object.hasOwn(attributes, name),
88
+ getAttribute: name => attributes[name] ?? null,
89
+ querySelector: () => null,
90
+ dispatchEvent() {}
91
+ });
92
+ const renderer = watcher({ 'render-query': 'template' });
93
+ renderer.renderValue = async () => {};
94
+ renderer.getData = () => gate;
95
+ const idField = watcher({ key: '_id' });
96
+ idField.setValue = value => { idValue = value; };
97
+ const setDataType = await loadSetDataType([renderer, idField]);
98
+ const attributes = { array: 'clusters', object: 'pending' };
99
+ const form = {
100
+ tagName: 'FORM',
101
+ querySelectorAll: () => [],
102
+ hasAttribute: name => Object.hasOwn(attributes, name),
103
+ getAttribute: name => attributes[name] ?? null,
104
+ setAttribute: (name, value) => { attributes[name] = value; }
105
+ };
106
+ const id = '65f8cb447b39a3c1a8904e22';
107
+ const savedIds = [];
108
+ const completion = setDataType(form, {
109
+ type: 'object', method: 'object.create', array: 'clusters',
110
+ object: [{ _id: id, $array: 'clusters' }]
111
+ }, { element: { dispatchEvent: () => savedIds.push(idValue) } });
112
+ await new Promise(resolve => setImmediate(resolve));
113
+ assert.equal(idValue, '');
114
+ assert.deepEqual(savedIds, []);
115
+ release();
116
+ await completion;
117
+ assert.equal(idValue, id);
118
+ assert.deepEqual(savedIds, [id]);
119
+ });
@@ -0,0 +1,108 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const vm = require('node:vm');
5
+ const test = require('node:test');
6
+
7
+ async function loadSetData() {
8
+ const context = vm.createContext({ console, HTMLCollection: class {}, CustomEvent: class {} });
9
+ const source = fs.readFileSync(path.join(__dirname, '../src/setDataHandler.js'), 'utf8');
10
+ const module = new vm.SourceTextModule(source, { context });
11
+ const utils = {
12
+ dotNotationToObject: value => value, queryData: () => true,
13
+ searchData: () => true, sortData: value => value,
14
+ getAttributes: element => ({ key: element.getAttribute('key'), isRead: element.getAttribute('read') }),
15
+ checkValue: () => true,
16
+ getValueFromObject: (object, key) => object[key]
17
+ };
18
+ const modules = {
19
+ '@cocreate/utils': utils,
20
+ '@cocreate/crud-client': { default: { getValueFromObject: (object, key) => object[key] } },
21
+ '@cocreate/filter': { default: { filters: new Map() } },
22
+ './mapRegistry.js': { elements: new WeakMap(), forms: new WeakMap(), keys: new Map() },
23
+ './getDataHandler.js': { getDataElements: () => [], getDataObject: element => ({ object: element.getAttribute('object') }) }
24
+ };
25
+ await module.link(name => new vm.SyntheticModule(Object.keys(modules[name]), function () {
26
+ for (const [key, value] of Object.entries(modules[name])) this.setExport(key, value);
27
+ }, { context }));
28
+ await module.evaluate();
29
+ return module.namespace.setData;
30
+ }
31
+
32
+ function element(attributes) {
33
+ return {
34
+ hasAttribute: name => Object.hasOwn(attributes, name),
35
+ getAttribute: name => attributes[name] ?? null,
36
+ removeAttribute: name => delete attributes[name],
37
+ querySelector: () => null,
38
+ dispatchEvent() {},
39
+ values: [],
40
+ setValue(value) { this.values.push(value); }
41
+ };
42
+ }
43
+
44
+ test('SSR app labels do not prevent later table columns from receiving shared data', async () => {
45
+ const setData = await loadSetData();
46
+ const id = '28b6ddbe8b35de252ecd79e1';
47
+ const label = element({ array: 'apps', object: id, key: 'name', rendered: 'true' });
48
+ const table = element({ array: 'apps', object: id, key: 'fields', 'render-query': "[template='column'][]" });
49
+ const renders = [];
50
+ table.renderValue = async data => renders.push(data);
51
+ const fields = [{ key: 'name', label: 'Name' }, { key: 'type', label: 'Type' }];
52
+ await setData([label, table], {
53
+ method: 'object.read', array: 'apps',
54
+ object: [{ _id: id, $storage: ['mongodb'], name: 'Cluster', fields }]
55
+ });
56
+ assert.equal(label.hasAttribute('rendered'), false);
57
+ assert.deepEqual(label.values, []);
58
+ assert.equal(renders.length, 1);
59
+ assert.deepEqual(Array.from(renders[0].fields), fields);
60
+ });
61
+
62
+ test('filtered counts retain their value during writes and refresh each cluster independently', async () => {
63
+ const setData = await loadSetData();
64
+ const counts = ['cluster-a', 'cluster-b'].map(cluster => {
65
+ const el = element({ array: 'servers', object: '', key: '$length' });
66
+ el.getFilter = async () => ({ query: { cluster_id: cluster }, index: 0 });
67
+ el.refreshes = [];
68
+ el.setFilter = filter => el.refreshes.push(filter);
69
+ return el;
70
+ });
71
+ for (const el of counts) {
72
+ await setData(el, { method: 'object.read', array: 'servers', object: [{ _id: 'server' }] });
73
+ }
74
+ for (const method of ['object.create', 'object.update', 'object.delete']) {
75
+ await setData(counts, { method, array: 'servers', object: [{ _id: 'server', $update: { cluster_id: 'cluster-b' } }] });
76
+ }
77
+ for (let i = 0; i < counts.length; i++) {
78
+ assert.deepEqual(counts[i].values, [1]);
79
+ assert.equal(counts[i].refreshes.length, 3);
80
+ assert.equal(counts[i].refreshes[0].query.cluster_id, ['cluster-a', 'cluster-b'][i]);
81
+ }
82
+ await setData(counts[0], { method: 'object.read', array: 'servers', object: [], error: [{ message: 'failed' }] });
83
+ assert.deepEqual(counts[0].values, [1]);
84
+ await setData(counts[0], { method: 'object.read', array: 'servers', object: [] });
85
+ assert.deepEqual(counts[0].values, [1, 0]);
86
+ });
87
+
88
+ test('unmatched document updates do not write the empty object attribute into a value', async () => {
89
+ const setData = await loadSetData();
90
+ const el = element({ array: 'servers', object: '', key: '$length' });
91
+ el.getFilter = async () => ({});
92
+ await setData(el, { method: 'object.update', array: 'servers', object: [{ _id: 'other' }] });
93
+ assert.deepEqual(el.values, []);
94
+ });
95
+
96
+ test('read=false keeps a deferred value expression intact when a save response arrives', async () => {
97
+ const setData = await loadSetData();
98
+ const expression = '$query($closest #provision-workflow #cluster-object-id).getValue()';
99
+ const el = element({ key: 'cluster_id', read: 'false', value: expression });
100
+ el.setValue = value => { el.values.push(value); el.value = value; };
101
+ el.value = expression;
102
+ await setData(el, {
103
+ method: 'object.create', array: 'servers',
104
+ object: [{ _id: 'server', cluster_id: '65f8cb447b39a3c1a8904e22' }]
105
+ });
106
+ assert.equal(el.value, expression);
107
+ assert.deepEqual(el.values, []);
108
+ });
package/webpack.config.js CHANGED
@@ -48,6 +48,7 @@ module.exports = async (env, argv) => {
48
48
  minimizer: [
49
49
  new EsbuildPlugin({
50
50
  target: "es2017",
51
+ minify: isProduction,
51
52
  css: true
52
53
  })
53
54
  ],