@cocreate/element-prototype 1.35.0 → 2.0.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,104 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ globalThis.window = {
5
+ innerWidth: 1024,
6
+ innerHeight: 768,
7
+ location: {
8
+ href: "https://example.test/",
9
+ origin: "https://example.test",
10
+ protocol: "https:",
11
+ hostname: "example.test",
12
+ host: "example.test",
13
+ port: "",
14
+ pathname: "/",
15
+ hash: ""
16
+ },
17
+ addEventListener() {}
18
+ };
19
+ globalThis.localStorage = { getItem: () => null };
20
+
21
+ class TestElement {
22
+ constructor(attributes, value) {
23
+ this.attributes = new Map(Object.entries(attributes));
24
+ this.value = value;
25
+ }
26
+
27
+ getAttribute(name) {
28
+ return this.attributes.has(name) ? this.attributes.get(name) : null;
29
+ }
30
+
31
+ getValue() {
32
+ return this.value;
33
+ }
34
+ }
35
+
36
+ globalThis.Element = TestElement;
37
+
38
+ const { getAttribute, getAttributeAsync } = await import("../src/getAttribute.js");
39
+
40
+ const labelExpression = JSON.stringify({
41
+ parsed: { $parse: "$value" },
42
+ $return: {
43
+ $if: {
44
+ $parsed: {
45
+ $eq: {
46
+ $true: "Enabled",
47
+ $false: "Disabled",
48
+ "true": "Enabled",
49
+ "false": "Disabled",
50
+ Enabled: "Enabled",
51
+ Disabled: "Disabled",
52
+ $else: ""
53
+ }
54
+ }
55
+ }
56
+ }
57
+ });
58
+
59
+ const directStringLabelExpression = JSON.stringify({
60
+ $if: {
61
+ $value: {
62
+ $eq: {
63
+ "true": "Enabled",
64
+ "false": "Disabled",
65
+ $else: ""
66
+ }
67
+ }
68
+ }
69
+ });
70
+
71
+ const quotedOperatorLabelExpression =
72
+ "$(`\u0024value` === 'true' ? 'Enabled' : 'Disabled')";
73
+
74
+ test("input.value evaluates direct string equality branches", async () => {
75
+ for (const [value, expected] of [["true", "Enabled"], ["false", "Disabled"]]) {
76
+ const element = new TestElement({ "input.value": directStringLabelExpression }, value);
77
+
78
+ assert.equal(getAttribute(element, "input.value"), expected);
79
+ assert.equal(await getAttributeAsync(element, "input.value"), expected);
80
+ }
81
+ });
82
+
83
+ test("input.value evaluates backtick OperatorJS values as strings", () => {
84
+ for (const [value, expected] of [["true", "Enabled"], ["false", "Disabled"]]) {
85
+ const element = new TestElement({ "input.value": quotedOperatorLabelExpression }, value);
86
+ assert.equal(getAttribute(element, "input.value"), expected);
87
+ }
88
+ });
89
+
90
+ test("client getAttribute resolves $value from the element and maps it through AST", async () => {
91
+ for (const [value, expected] of [
92
+ [true, "Enabled"],
93
+ [false, "Disabled"],
94
+ ["true", "Enabled"],
95
+ ["false", "Disabled"],
96
+ ["Enabled", "Enabled"],
97
+ ["Disabled", "Disabled"]
98
+ ]) {
99
+ const element = new TestElement({ "input.value": labelExpression }, value);
100
+
101
+ assert.equal(getAttribute(element, "input.value"), expected);
102
+ assert.equal(await getAttributeAsync(element, "input.value"), expected);
103
+ }
104
+ });
@@ -0,0 +1,48 @@
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
+ class Element {
9
+ constructor() { this.attributes = []; this.tagName = 'INPUT'; this.type = 'password'; this.value = ''; this.events = []; }
10
+ hasAttribute() { return false; }
11
+ getAttribute(name) { return name === 'type' ? this.type : null; }
12
+ dispatchEvent(event) { this.events.push(event.type); }
13
+ }
14
+ const context = vm.createContext({
15
+ HTMLElement: Element, SVGElement: class {}, atob,
16
+ CustomEvent: class { constructor(type) { this.type = type; } }
17
+ });
18
+ const module = new vm.SourceTextModule(fs.readFileSync(path.join(__dirname, '../src/setValue.js'), 'utf8'), { context });
19
+ const handlers = await import('../../CoCreate-utils/src/modules/valueHandler.js');
20
+ await module.link((specifier) => {
21
+ if (specifier === '@cocreate/utils') {
22
+ return new vm.SyntheticModule(Object.keys(handlers), function () {
23
+ for (const [name, value] of Object.entries(handlers)) this.setExport(name, value);
24
+ }, { context });
25
+ }
26
+ return new vm.SyntheticModule(['storage'], function () {
27
+ this.setExport('storage', new WeakMap());
28
+ }, { context });
29
+ });
30
+ await module.evaluate();
31
+ return new Element();
32
+ }
33
+
34
+ for (const [name, incoming, expected] of [
35
+ ['legacy Base64', btoa('sample-password!'), 'sample-password!'],
36
+ ['plain-text credential', 'sample-password!', 'sample-password!'],
37
+ ['malformed padding', 'YWJj===', 'YWJj==='],
38
+ ['plain Unicode', 'sample-密碼', 'sample-密碼'],
39
+ ['empty value', '', '']
40
+ ]) {
41
+ test(`password setValue handles ${name} and completes event dispatch`, async () => {
42
+ const element = await setup();
43
+ element.setValue(incoming);
44
+ assert.equal(element.value, expected);
45
+ assert.ok(element.events.includes('input'));
46
+ assert.ok(element.events.includes('change'));
47
+ });
48
+ }
@@ -0,0 +1,320 @@
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
+ class Element {
9
+ constructor(attributes = {}, tagName = 'TEXTAREA') {
10
+ this.attributes = Object.entries(attributes).map(([name, value]) => ({ name, value }));
11
+ this.attributes.getNamedItem = name => this.attributes.find(attr => attr.name === name);
12
+ this.tagName = tagName;
13
+ this.value = '';
14
+ this.events = [];
15
+ }
16
+ getAttribute(name) { return this.attributes.getNamedItem(name)?.value ?? null; }
17
+ hasAttribute(name) { return this.getAttribute(name) !== null; }
18
+ dispatchEvent(event) { this.events.push(event.type); }
19
+ }
20
+ const context = vm.createContext({
21
+ HTMLElement: Element, SVGElement: class {}, console,
22
+ atob, btoa, TextEncoder, TextDecoder,
23
+ CustomEvent: class { constructor(type) { this.type = type; } }
24
+ });
25
+ const modules = new Map();
26
+ const load = name => {
27
+ if (!modules.has(name)) {
28
+ modules.set(name, new vm.SourceTextModule(
29
+ fs.readFileSync(path.join(__dirname, '../src', name), 'utf8'), { context }
30
+ ));
31
+ }
32
+ return modules.get(name);
33
+ };
34
+ const handlers = await import('../../CoCreate-utils/src/modules/valueHandler.js');
35
+ const utils = new vm.SyntheticModule(['astSync', 'astAsync', ...Object.keys(handlers)], function () {
36
+ for (const [name, value] of Object.entries(handlers)) this.setExport(name, value);
37
+ this.setExport('astSync', ({ executeAST }) => executeAST);
38
+ this.setExport('astAsync', async ({ executeAST }) => executeAST);
39
+ }, { context });
40
+ const getter = load('getValue.js');
41
+ await getter.link(() => utils);
42
+ await getter.evaluate();
43
+ const setter = load('setValue.js');
44
+ await setter.link(specifier => specifier === '@cocreate/utils' ? utils : load(specifier));
45
+ await setter.evaluate();
46
+ return Element;
47
+ }
48
+
49
+ test('getter transformations remain read-only and match in sync/async paths', async () => {
50
+ const Element = await setup();
51
+ const el = new Element({ 'get-value-uppercase': '', 'get-value-replace': 'HELLO', 'get-value-replacement': 'HI' });
52
+ el.setValue('hello');
53
+ assert.equal(el.value, 'hello');
54
+ assert.equal(el.getValue(), 'HI');
55
+ assert.equal(await el.getValueAsync(), 'HI');
56
+ });
57
+
58
+ test('setter transformations run before rendering and are not repeated by getters', async () => {
59
+ const Element = await setup();
60
+ const el = new Element({ 'set-value-replace': 'a', 'set-value-replacement': 'aa' });
61
+ el.setValue('a');
62
+ assert.equal(el.value, 'aa');
63
+ assert.equal(el.getValue(), 'aa');
64
+ assert.deepEqual(el.events, ['input', 'change']);
65
+ });
66
+
67
+ test('setter supports case, regex, and Unicode encoding/decoding', async () => {
68
+ const Element = await setup();
69
+ const encoded = new Element({ 'set-value-uppercase': '', 'set-value-encode': 'base64' });
70
+ encoded.setValue('hello 密碼');
71
+ const decoded = new Element({ 'set-value-decode': 'base64' });
72
+ decoded.setValue(encoded.value);
73
+ assert.equal(decoded.value, 'HELLO 密碼');
74
+ });
75
+
76
+ test('component storage retains parsed objects and arrays', async () => {
77
+ const Element = await setup();
78
+ for (const [type, input, expected] of [
79
+ ['object', '{"a":1}', { a: 1 }],
80
+ ['array.$keys', '{"a":1}', ['a']],
81
+ ['boolean', 'false', false]
82
+ ]) {
83
+ const el = new Element({ component: '', 'set-value-type': type }, 'DIV');
84
+ el.setValue(input);
85
+ assert.equal(JSON.stringify(el.getValue()), JSON.stringify(expected));
86
+ assert.deepEqual(el.events, ['input', 'change']);
87
+ }
88
+ });
89
+
90
+ test('existing objects keep their identity in component storage', async () => {
91
+ const Element = await setup();
92
+ const value = { a: 1 };
93
+ const el = new Element({ component: '', 'set-value-type': 'object', 'set-value-uppercase': '' }, 'DIV');
94
+ el.setValue(value);
95
+ assert.equal(el.getValue(), value);
96
+ });
97
+
98
+ test('textarea serializes objects after setter transformations', async () => {
99
+ const Element = await setup();
100
+ const el = new Element({ 'set-value-decode': 'json', 'set-value-type': 'object' });
101
+ el.setValue('{"a":1}');
102
+ assert.equal(el.value, JSON.stringify({ a: 1 }, null, 2));
103
+ });
104
+
105
+ test('regex may return a non-string without subsequent string operations failing', async () => {
106
+ const Element = await setup();
107
+ const el = new Element({ component: '', 'set-value-split': ',', 'set-value-replace': 'a' }, 'DIV');
108
+ el.setValue('a,b');
109
+ assert.equal(JSON.stringify(el.getValue()), '["a","b"]');
110
+ });
111
+
112
+ test('getter JSON decoding can precede object conversion', async () => {
113
+ const Element = await setup();
114
+ const el = new Element({ 'get-value-decode': 'json', 'get-value-type': 'object' });
115
+ el.value = '{"a":1}';
116
+ assert.equal(JSON.stringify(el.getValue()), '{"a":1}');
117
+ assert.equal(JSON.stringify(await el.getValueAsync()), '{"a":1}');
118
+ });
119
+
120
+ test('unscoped attributes transform both reads and writes', async () => {
121
+ const Element = await setup();
122
+ const el = new Element({ 'value-replace': 'a', 'value-replacement': 'aa' });
123
+ el.setValue('a');
124
+ assert.equal(el.value, 'aa');
125
+ assert.equal(el.getValue(), 'aaa');
126
+ assert.equal(await el.getValueAsync(), 'aaa');
127
+ });
128
+
129
+ test('scoped replacements override shared operations in either attribute order', async () => {
130
+ const Element = await setup();
131
+ const entries = [
132
+ ['value-replace', 'a'], ['value-replacement', 'shared'],
133
+ ['get-value-replace', 'set'], ['get-value-replacement', 'get'],
134
+ ['set-value-replace', 'a'], ['set-value-replacement', 'set']
135
+ ];
136
+ for (const attributes of [entries, [...entries].reverse()]) {
137
+ const el = new Element(Object.fromEntries(attributes));
138
+ el.setValue('a');
139
+ assert.equal(el.value, 'set');
140
+ assert.equal(el.getValue(), 'get');
141
+ assert.equal(await el.getValueAsync(), 'get');
142
+ }
143
+ });
144
+
145
+ test('scoped false and empty values disable shared transformations', async () => {
146
+ const Element = await setup();
147
+ const el = new Element({
148
+ 'value-uppercase': '', 'set-value-uppercase': 'false',
149
+ 'value-replace': 'a', 'value-replacement': 'b',
150
+ 'set-value-replace': '', 'get-value-replace': 'false',
151
+ 'value-encode': 'base64', 'set-value-encode': 'false', 'get-value-encode': ''
152
+ });
153
+ el.setValue('a');
154
+ assert.equal(el.value, 'a');
155
+ assert.equal(el.getValue(), 'A');
156
+ });
157
+
158
+ test('each option falls back independently to shared attributes', async () => {
159
+ const Element = await setup();
160
+ const el = new Element({ 'set-value-uppercase': '', 'value-replace': 'A', 'value-replacement': 'B' });
161
+ el.setValue('a');
162
+ assert.equal(el.value, 'B');
163
+ });
164
+
165
+ test('shared and scoped type conversion applies before component storage', async () => {
166
+ const Element = await setup();
167
+ const shared = new Element({ component: '', 'value-type': 'object' }, 'DIV');
168
+ shared.setValue('{"a":1}');
169
+ assert.equal(JSON.stringify(shared.getValue()), '{"a":1}');
170
+ const scoped = new Element({ component: '', 'value-type': 'object', 'set-value-type': 'array.$keys' }, 'DIV');
171
+ scoped.setValue('{"a":1}');
172
+ assert.equal(JSON.stringify(scoped.getValue()), '["a"]');
173
+ });
174
+
175
+ test('scoped getter type and prefixes override shared options', async () => {
176
+ const Element = await setup();
177
+ const el = new Element({ 'value-type': 'object', 'get-value-type': 'text', 'set-value-type': 'text',
178
+ 'value-prefix': 'shared:', 'get-value-prefix': 'get:', 'set-value-prefix': 'set:' });
179
+ el.setValue('hello');
180
+ assert.equal(el.value, 'set:hello');
181
+ assert.equal(el.getValue(), 'get:set:hello');
182
+ assert.equal(await el.getValueAsync(), 'get:set:hello');
183
+ });
184
+
185
+ function option(Element, value, attributes = {}) {
186
+ const el = new Element(attributes, 'OPTION');
187
+ el.value = value;
188
+ return el;
189
+ }
190
+
191
+ test('single select preserves unset and reads boolean types from selected options', async () => {
192
+ const Element = await setup();
193
+ const select = new Element({}, 'SELECT');
194
+ for (const [value, attributes, expected] of [
195
+ ['', {}, ''], ['true', { 'value-type': 'boolean' }, true],
196
+ ['false', { 'value-type': 'boolean' }, false], ['false', {}, 'false']
197
+ ]) {
198
+ select.selectedOptions = [option(Element, value, attributes)];
199
+ assert.equal(select.getValue(), expected);
200
+ assert.equal(await select.getValueAsync(), expected);
201
+ }
202
+ });
203
+
204
+ test('multiple select preserves individually processed types and empty options', async () => {
205
+ const Element = await setup();
206
+ const select = new Element({ multiple: '' }, 'SELECT');
207
+ select.selectedOptions = [
208
+ option(Element, '', {}),
209
+ option(Element, 'false', { 'value-type': 'boolean' }),
210
+ option(Element, '{"id":1}', { 'value-type': 'json' }),
211
+ option(Element, 'hello', { 'get-value-uppercase': '' })
212
+ ];
213
+ for (const value of [select.getValue(), await select.getValueAsync()]) {
214
+ assert.equal(JSON.stringify(value), JSON.stringify(['', false, { id: 1 }, 'HELLO']));
215
+ }
216
+ });
217
+
218
+ test('select transformations still run after option transformations', async () => {
219
+ const Element = await setup();
220
+ const select = new Element({ 'get-value-prefix': '[', 'get-value-suffix': ']' }, 'SELECT');
221
+ select.selectedOptions = [option(Element, 'hello', { 'get-value-uppercase': '' })];
222
+ assert.equal(select.getValue(), '[HELLO]');
223
+ assert.equal(await select.getValueAsync(), '[HELLO]');
224
+ const multiple = new Element({ multiple: '', 'value-prefix': '[', 'value-suffix': ']' }, 'SELECT');
225
+ multiple.selectedOptions = select.selectedOptions;
226
+ assert.deepEqual(Array.from(multiple.getValue()), ['[HELLO]']);
227
+ assert.deepEqual(Array.from(await multiple.getValueAsync()), ['[HELLO]']);
228
+ const booleanSelect = new Element({ 'value-type': 'boolean' }, 'SELECT');
229
+ booleanSelect.selectedOptions = [option(Element, 'false')];
230
+ assert.equal(booleanSelect.getValue(), false);
231
+ assert.equal(await booleanSelect.getValueAsync(), false);
232
+ });
233
+
234
+ test('selects with no selected options return an empty string or array', async () => {
235
+ const Element = await setup();
236
+ for (const attributes of [{}, { multiple: '' }]) {
237
+ const select = new Element(attributes, 'SELECT');
238
+ select.selectedOptions = [];
239
+ const expected = 'multiple' in attributes ? '[]' : '""';
240
+ assert.equal(JSON.stringify(select.getValue()), expected);
241
+ assert.equal(JSON.stringify(await select.getValueAsync()), expected);
242
+ }
243
+ });
244
+
245
+ test('explicit setter prefix and suffix build the rendered value', async () => {
246
+ const Element = await setup();
247
+ const el = new Element({'set-value-prefix':'/assets/images/providers/', 'set-value-suffix':'.svg'});
248
+ el.setValue('aws');
249
+ assert.equal(el.value, '/assets/images/providers/aws.svg');
250
+ });
251
+
252
+ test('iframes honor explicit src and srcdoc setter paths', async () => {
253
+ const Element = await setup();
254
+ const srcFrame = new Element({ src: '' }, 'IFRAME');
255
+ const srcdocFrame = new Element({ srcdoc: '' }, 'IFRAME');
256
+
257
+ srcFrame.setValue('/assets/files/preview.html');
258
+ srcdocFrame.setValue('<h1>Preview</h1>');
259
+
260
+ assert.equal(srcFrame.src, '/assets/files/preview.html');
261
+ assert.equal(srcFrame.srcdoc, undefined);
262
+ assert.equal(srcdocFrame.src, undefined);
263
+ assert.equal(srcdocFrame.srcdoc, '<h1>Preview</h1>');
264
+ });
265
+
266
+ test('shared affixes retain the existing get/set round trip', async () => {
267
+ const Element = await setup();
268
+ const el = new Element({'value-prefix':'[', 'value-suffix':']'});
269
+ el.setValue('[aws]');
270
+ assert.equal(el.value, 'aws');
271
+ assert.equal(el.getValue(), '[aws]');
272
+ });
273
+ test('empty setter affixes override shared affixes', async () => {
274
+ const Element = await setup();
275
+ const el = new Element({'value-prefix':'[', 'value-suffix':']', 'set-value-prefix':'', 'set-value-suffix':''});
276
+ el.setValue('[aws]');
277
+ assert.equal(el.value, '[aws]');
278
+ });
279
+
280
+ test('generic values prefer attributes then properties while explicit reads select the DOM', async () => {
281
+ const Element = await setup();
282
+ const el = new Element({ value: '' }, 'DIV');
283
+ el.value = 'property';
284
+ el.innerHTML = '<b>markup</b>';
285
+ el.textContent = 'markup';
286
+ assert.equal(el.getValue(), '');
287
+ assert.equal(await el.getValueAsync(), '');
288
+ assert.equal(el.getValue('textContent'), 'markup');
289
+ assert.equal(await el.getValueAsync('innerHTML'), '<b>markup</b>');
290
+ el.attributes.splice(0);
291
+ el.value = '';
292
+ assert.equal(el.getValue(), '');
293
+ delete el.value;
294
+ assert.equal(el.getValue(), '<b>markup</b>');
295
+ });
296
+
297
+ test('component values stay on the element across repeated asynchronous reads', async () => {
298
+ const Element = await setup();
299
+ const el = new Element({ component: '' }, 'DIV'), value = { source: 'stored' };
300
+ el.setValue(value);
301
+ assert.equal(el.value, value);
302
+ assert.equal(await el.getValueAsync(), value);
303
+ assert.equal(await el.getValueAsync(), value);
304
+ assert.equal(el.getValue(), value);
305
+ });
306
+
307
+ test('component and editor markers do not bypass native checkbox handling', async () => {
308
+ const Element = await setup();
309
+ for (const marker of ['component', 'editor', 'language']) {
310
+ const el = new Element({ [marker]: '', value: 'yes' }, 'INPUT');
311
+ el.type = 'checkbox';
312
+ el.value = 'yes';
313
+ el.setValue(true);
314
+ assert.equal(el.checked, true);
315
+ assert.equal(el.value, 'yes');
316
+ el.setValue(false);
317
+ assert.equal(el.checked, false);
318
+ assert.equal(el.getValue(), false);
319
+ }
320
+ });