@cocreate/element-prototype 1.34.0 → 1.38.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,275 @@
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 setter = load('setValue.js');
41
+ await setter.link(specifier => specifier === '@cocreate/utils' ? utils : load(specifier));
42
+ await setter.evaluate();
43
+ return Element;
44
+ }
45
+
46
+ test('getter transformations remain read-only and match in sync/async paths', async () => {
47
+ const Element = await setup();
48
+ const el = new Element({ 'get-value-uppercase': '', 'get-value-replace': 'HELLO', 'get-value-replacement': 'HI' });
49
+ el.setValue('hello');
50
+ assert.equal(el.value, 'hello');
51
+ assert.equal(el.getValue(), 'HI');
52
+ assert.equal(await el.getValueAsync(), 'HI');
53
+ });
54
+
55
+ test('setter transformations run before rendering and are not repeated by getters', async () => {
56
+ const Element = await setup();
57
+ const el = new Element({ 'set-value-replace': 'a', 'set-value-replacement': 'aa' });
58
+ el.setValue('a');
59
+ assert.equal(el.value, 'aa');
60
+ assert.equal(el.getValue(), 'aa');
61
+ assert.deepEqual(el.events, ['input', 'change']);
62
+ });
63
+
64
+ test('setter supports case, regex, and Unicode encoding/decoding', async () => {
65
+ const Element = await setup();
66
+ const encoded = new Element({ 'set-value-uppercase': '', 'set-value-encode': 'base64' });
67
+ encoded.setValue('hello 密碼');
68
+ const decoded = new Element({ 'set-value-decode': 'base64' });
69
+ decoded.setValue(encoded.value);
70
+ assert.equal(decoded.value, 'HELLO 密碼');
71
+ });
72
+
73
+ test('component storage retains parsed objects and arrays', async () => {
74
+ const Element = await setup();
75
+ for (const [type, input, expected] of [
76
+ ['object', '{"a":1}', { a: 1 }],
77
+ ['array.$keys', '{"a":1}', ['a']],
78
+ ['boolean', 'false', false]
79
+ ]) {
80
+ const el = new Element({ component: '', 'set-value-type': type });
81
+ el.setValue(input);
82
+ assert.equal(JSON.stringify(el.getValue()), JSON.stringify(expected));
83
+ assert.deepEqual(el.events, ['input', 'change']);
84
+ }
85
+ });
86
+
87
+ test('existing objects keep their identity in component storage', async () => {
88
+ const Element = await setup();
89
+ const value = { a: 1 };
90
+ const el = new Element({ component: '', 'set-value-type': 'object', 'set-value-uppercase': '' });
91
+ el.setValue(value);
92
+ assert.equal(el.getValue(), value);
93
+ });
94
+
95
+ test('textarea serializes objects after setter transformations', async () => {
96
+ const Element = await setup();
97
+ const el = new Element({ 'set-value-decode': 'json', 'set-value-type': 'object' });
98
+ el.setValue('{"a":1}');
99
+ assert.equal(el.value, JSON.stringify({ a: 1 }, null, 2));
100
+ });
101
+
102
+ test('regex may return a non-string without subsequent string operations failing', async () => {
103
+ const Element = await setup();
104
+ const el = new Element({ component: '', 'set-value-split': ',', 'set-value-replace': 'a' });
105
+ el.setValue('a,b');
106
+ assert.equal(JSON.stringify(el.getValue()), '["a","b"]');
107
+ });
108
+
109
+ test('getter JSON decoding can precede object conversion', async () => {
110
+ const Element = await setup();
111
+ const el = new Element({ 'get-value-decode': 'json', 'get-value-type': 'object' });
112
+ el.value = '{"a":1}';
113
+ assert.equal(JSON.stringify(el.getValue()), '{"a":1}');
114
+ assert.equal(JSON.stringify(await el.getValueAsync()), '{"a":1}');
115
+ });
116
+
117
+ test('unscoped attributes transform both reads and writes', async () => {
118
+ const Element = await setup();
119
+ const el = new Element({ 'value-replace': 'a', 'value-replacement': 'aa' });
120
+ el.setValue('a');
121
+ assert.equal(el.value, 'aa');
122
+ assert.equal(el.getValue(), 'aaa');
123
+ assert.equal(await el.getValueAsync(), 'aaa');
124
+ });
125
+
126
+ test('scoped replacements override shared operations in either attribute order', async () => {
127
+ const Element = await setup();
128
+ const entries = [
129
+ ['value-replace', 'a'], ['value-replacement', 'shared'],
130
+ ['get-value-replace', 'set'], ['get-value-replacement', 'get'],
131
+ ['set-value-replace', 'a'], ['set-value-replacement', 'set']
132
+ ];
133
+ for (const attributes of [entries, [...entries].reverse()]) {
134
+ const el = new Element(Object.fromEntries(attributes));
135
+ el.setValue('a');
136
+ assert.equal(el.value, 'set');
137
+ assert.equal(el.getValue(), 'get');
138
+ assert.equal(await el.getValueAsync(), 'get');
139
+ }
140
+ });
141
+
142
+ test('scoped false and empty values disable shared transformations', async () => {
143
+ const Element = await setup();
144
+ const el = new Element({
145
+ 'value-uppercase': '', 'set-value-uppercase': 'false',
146
+ 'value-replace': 'a', 'value-replacement': 'b',
147
+ 'set-value-replace': '', 'get-value-replace': 'false',
148
+ 'value-encode': 'base64', 'set-value-encode': 'false', 'get-value-encode': ''
149
+ });
150
+ el.setValue('a');
151
+ assert.equal(el.value, 'a');
152
+ assert.equal(el.getValue(), 'A');
153
+ });
154
+
155
+ test('each option falls back independently to shared attributes', async () => {
156
+ const Element = await setup();
157
+ const el = new Element({ 'set-value-uppercase': '', 'value-replace': 'A', 'value-replacement': 'B' });
158
+ el.setValue('a');
159
+ assert.equal(el.value, 'B');
160
+ });
161
+
162
+ test('shared and scoped type conversion applies before component storage', async () => {
163
+ const Element = await setup();
164
+ const shared = new Element({ component: '', 'value-type': 'object' });
165
+ shared.setValue('{"a":1}');
166
+ assert.equal(JSON.stringify(shared.getValue()), '{"a":1}');
167
+ const scoped = new Element({ component: '', 'value-type': 'object', 'set-value-type': 'array.$keys' });
168
+ scoped.setValue('{"a":1}');
169
+ assert.equal(JSON.stringify(scoped.getValue()), '["a"]');
170
+ });
171
+
172
+ test('scoped getter type and prefixes override shared options', async () => {
173
+ const Element = await setup();
174
+ const el = new Element({ 'value-type': 'object', 'get-value-type': 'text', 'set-value-type': 'text',
175
+ 'value-prefix': 'shared:', 'get-value-prefix': 'get:', 'set-value-prefix': 'set:' });
176
+ el.setValue('hello');
177
+ assert.equal(el.value, 'set:hello');
178
+ assert.equal(el.getValue(), 'get:set:hello');
179
+ assert.equal(await el.getValueAsync(), 'get:set:hello');
180
+ });
181
+
182
+ function option(Element, value, attributes = {}) {
183
+ const el = new Element(attributes, 'OPTION');
184
+ el.value = value;
185
+ return el;
186
+ }
187
+
188
+ test('single select preserves unset and reads boolean types from selected options', async () => {
189
+ const Element = await setup();
190
+ const select = new Element({}, 'SELECT');
191
+ for (const [value, attributes, expected] of [
192
+ ['', {}, ''], ['true', { 'value-type': 'boolean' }, true],
193
+ ['false', { 'value-type': 'boolean' }, false], ['false', {}, 'false']
194
+ ]) {
195
+ select.selectedOptions = [option(Element, value, attributes)];
196
+ assert.equal(select.getValue(), expected);
197
+ assert.equal(await select.getValueAsync(), expected);
198
+ }
199
+ });
200
+
201
+ test('multiple select preserves individually processed types and empty options', async () => {
202
+ const Element = await setup();
203
+ const select = new Element({ multiple: '' }, 'SELECT');
204
+ select.selectedOptions = [
205
+ option(Element, '', {}),
206
+ option(Element, 'false', { 'value-type': 'boolean' }),
207
+ option(Element, '{"id":1}', { 'value-type': 'json' }),
208
+ option(Element, 'hello', { 'get-value-uppercase': '' })
209
+ ];
210
+ for (const value of [select.getValue(), await select.getValueAsync()]) {
211
+ assert.equal(JSON.stringify(value), JSON.stringify(['', false, { id: 1 }, 'HELLO']));
212
+ }
213
+ });
214
+
215
+ test('select transformations still run after option transformations', async () => {
216
+ const Element = await setup();
217
+ const select = new Element({ 'get-value-prefix': '[', 'get-value-suffix': ']' }, 'SELECT');
218
+ select.selectedOptions = [option(Element, 'hello', { 'get-value-uppercase': '' })];
219
+ assert.equal(select.getValue(), '[HELLO]');
220
+ assert.equal(await select.getValueAsync(), '[HELLO]');
221
+ const multiple = new Element({ multiple: '', 'value-prefix': '[', 'value-suffix': ']' }, 'SELECT');
222
+ multiple.selectedOptions = select.selectedOptions;
223
+ assert.deepEqual(Array.from(multiple.getValue()), ['[HELLO]']);
224
+ assert.deepEqual(Array.from(await multiple.getValueAsync()), ['[HELLO]']);
225
+ const booleanSelect = new Element({ 'value-type': 'boolean' }, 'SELECT');
226
+ booleanSelect.selectedOptions = [option(Element, 'false')];
227
+ assert.equal(booleanSelect.getValue(), false);
228
+ assert.equal(await booleanSelect.getValueAsync(), false);
229
+ });
230
+
231
+ test('selects with no selected options return an empty string or array', async () => {
232
+ const Element = await setup();
233
+ for (const attributes of [{}, { multiple: '' }]) {
234
+ const select = new Element(attributes, 'SELECT');
235
+ select.selectedOptions = [];
236
+ const expected = 'multiple' in attributes ? '[]' : '""';
237
+ assert.equal(JSON.stringify(select.getValue()), expected);
238
+ assert.equal(JSON.stringify(await select.getValueAsync()), expected);
239
+ }
240
+ });
241
+
242
+ test('explicit setter prefix and suffix build the rendered value', async () => {
243
+ const Element = await setup();
244
+ const el = new Element({'set-value-prefix':'/assets/images/providers/', 'set-value-suffix':'.svg'});
245
+ el.setValue('aws');
246
+ assert.equal(el.value, '/assets/images/providers/aws.svg');
247
+ });
248
+
249
+ test('iframes use src when the src attribute is present and srcdoc otherwise', async () => {
250
+ const Element = await setup();
251
+ const srcFrame = new Element({ src: '' }, 'IFRAME');
252
+ const srcdocFrame = new Element({}, 'IFRAME');
253
+
254
+ srcFrame.setValue('/assets/files/preview.html');
255
+ srcdocFrame.setValue('<h1>Preview</h1>');
256
+
257
+ assert.equal(srcFrame.src, '/assets/files/preview.html');
258
+ assert.equal(srcFrame.srcdoc, undefined);
259
+ assert.equal(srcdocFrame.src, undefined);
260
+ assert.equal(srcdocFrame.srcdoc, '<h1>Preview</h1>');
261
+ });
262
+
263
+ test('shared affixes retain the existing get/set round trip', async () => {
264
+ const Element = await setup();
265
+ const el = new Element({'value-prefix':'[', 'value-suffix':']'});
266
+ el.setValue('[aws]');
267
+ assert.equal(el.value, 'aws');
268
+ assert.equal(el.getValue(), '[aws]');
269
+ });
270
+ test('empty setter affixes override shared affixes', async () => {
271
+ const Element = await setup();
272
+ const el = new Element({'value-prefix':'[', 'value-suffix':']', 'set-value-prefix':'', 'set-value-suffix':''});
273
+ el.setValue('[aws]');
274
+ assert.equal(el.value, '[aws]');
275
+ });