clickgo 6.9.0 → 6.11.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.
Files changed (47) hide show
  1. package/AGENTS.md +21 -0
  2. package/README.md +4 -0
  3. package/dist/app/demo.cga +0 -0
  4. package/dist/app/task.cga +0 -0
  5. package/dist/clickgo.d.ts +8 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lib/command/guide.d.ts +7 -0
  8. package/dist/lib/command/native.d.ts +18 -0
  9. package/dist/lib/command/palette.d.ts +10 -0
  10. package/dist/lib/command/schema.d.ts +31 -0
  11. package/dist/lib/command.d.ts +271 -0
  12. package/dist/lib/core.d.ts +8 -1
  13. package/dist/lib/form.d.ts +7 -0
  14. package/dist/lib/native.d.ts +29 -0
  15. package/dist/lib/task.d.ts +2 -0
  16. package/doc/clickgo-rag.md +4637 -2263
  17. package/package.json +8 -3
  18. package/test/core-module-loading.mjs +0 -169
  19. package/test/desktop.mjs +0 -715
  20. package/test/dock-lifecycle.mjs +0 -280
  21. package/test/dom-style.mjs +0 -261
  22. package/test/form-launcher.mjs +0 -177
  23. package/test/form-modal-layer.mjs +0 -75
  24. package/test/form-native-regions.mjs +0 -115
  25. package/test/form-native-style.mjs +0 -80
  26. package/test/form-native.mjs +0 -68
  27. package/test/form-notify-animation.mjs +0 -102
  28. package/test/form-notify.mjs +0 -90
  29. package/test/form-viewport.mjs +0 -127
  30. package/test/iconview.mjs +0 -70
  31. package/test/jodit-lifecycle.mjs +0 -212
  32. package/test/locale-integrity.mjs +0 -189
  33. package/test/pop-max-size.mjs +0 -66
  34. package/test/rtl-archive.mjs +0 -440
  35. package/test/rtl-direction.mjs +0 -193
  36. package/test/scroll.mjs +0 -145
  37. package/test/select.mjs +0 -885
  38. package/test/sidebar-toggle.mjs +0 -75
  39. package/test/task-package-loading.mjs +0 -94
  40. package/test/task-start.mjs +0 -141
  41. package/test/task-tray.mjs +0 -331
  42. package/test/tool-assets.mjs +0 -64
  43. package/test/tool-clone.mjs +0 -42
  44. package/test/toolbox.mjs +0 -88
  45. package/test/tuieditor-locale.mjs +0 -175
  46. package/test/vflow.mjs +0 -122
  47. package/test/web-drag-mask.mjs +0 -96
@@ -1,280 +0,0 @@
1
- // Run after TypeScript compilation: node --experimental-vm-modules test/dock-lifecycle.mjs
2
- import assert from 'node:assert/strict';
3
- import { readFile } from 'node:fs/promises';
4
- import { SourceTextModule, SyntheticModule } from 'node:vm';
5
-
6
- const sizeWatches = [];
7
- const sizeUnwatches = [];
8
-
9
- globalThis.document = {
10
- 'documentElement': {
11
- 'clientWidth': 360,
12
- 'clientHeight': 800
13
- }
14
- };
15
-
16
- class AbstractControl {
17
- constructor(rootForm, height) {
18
- this.rootForm = rootForm;
19
- this.refs = {
20
- 'body': { 'clientHeight': height }
21
- };
22
- }
23
-
24
- watch(name, callback, options) {
25
- if ((name === 'expanded') && options?.immediate) {
26
- callback();
27
- }
28
- }
29
-
30
- async nextTick() {}
31
-
32
- propBoolean(name) {
33
- return Boolean(this.props[name]);
34
- }
35
-
36
- propNumber(name) {
37
- return Number(this.props[name]);
38
- }
39
-
40
- parentByName(name) {
41
- return this.parents?.[name] ?? null;
42
- }
43
-
44
- emit() {}
45
- }
46
-
47
- const clickgo = new SyntheticModule(['control', 'dom'], function() {
48
- this.setExport('control', { AbstractControl });
49
- this.setExport('dom', {
50
- watchSizeMulti(current, element, handler, immediate) {
51
- sizeWatches.push({ current, element, handler });
52
- if (immediate) {
53
- handler();
54
- }
55
- return true;
56
- },
57
- unwatchSizeMulti(current, element, handler) {
58
- sizeUnwatches.push({ current, element, handler });
59
- },
60
- watchSize(current, element, handler, immediate) {
61
- if (immediate) {
62
- handler();
63
- }
64
- return true;
65
- },
66
- isRtl(element) {
67
- return element.rtl;
68
- },
69
- index(element) {
70
- return element.order;
71
- }
72
- });
73
- });
74
- const mod = new SourceTextModule(await readFile(new URL('../dist/sources/control/dock/code.js', import.meta.url), 'utf8'));
75
- await mod.link(() => clickgo);
76
- await mod.evaluate();
77
- const Dock = mod.namespace.default;
78
-
79
- const formA = { 'element': { 'isConnected': true, 'offsetWidth': 500 } };
80
- const formB = { 'element': { 'isConnected': true, 'offsetWidth': 800 } };
81
- const dockA1 = new Dock(formA, 300);
82
- const dockA2 = new Dock(formA, 400);
83
- const dockB = new Dock(formB, 500);
84
-
85
- await dockA1.onMounted();
86
- await dockA2.onMounted();
87
- await dockB.onMounted();
88
-
89
- assert.notEqual(dockA1.access, dockA2.access);
90
- assert.notEqual(dockA1.access.formSizeWatch, dockA2.access.formSizeWatch);
91
- assert.equal(dockA1.narrow, true);
92
- assert.equal(dockA2.narrow, true);
93
- assert.equal(dockB.narrow, false);
94
- assert.equal(dockA1.floatAreaHeight, 300);
95
- assert.equal(dockA2.floatAreaHeight, 400);
96
- assert.equal(dockB.floatAreaHeight, 500);
97
- assert.equal(dockA1.viewportWidth, 360);
98
- assert.equal(dockA2.viewportWidth, 360);
99
- assert.equal(dockB.viewportWidth, 360);
100
-
101
- dockB.toggleFloat(1);
102
- dockA1.toggleFloat(2);
103
- dockA2.toggleFloat(3);
104
- assert.equal(dockA1.floatGroup, -1);
105
- assert.equal(dockA2.floatGroup, 3);
106
- assert.equal(dockB.floatGroup, 1);
107
-
108
- const dockA1Watch = dockA1.access.formSizeWatch;
109
- const dockA1ViewportWatch = dockA1.access.viewportSizeWatch;
110
- dockA1.onUnmounted();
111
- assert.equal(dockA1.access.formSizeWatch, null);
112
- assert.equal(dockA1.access.viewportSizeWatch, null);
113
- assert.equal(sizeUnwatches.length, 2);
114
- assert.equal(sizeUnwatches[0].current, dockA1);
115
- assert.equal(sizeUnwatches[0].element, formA.element);
116
- assert.equal(sizeUnwatches[0].handler, dockA1Watch.handler);
117
- assert.equal(sizeUnwatches[1].current, dockA1);
118
- assert.equal(sizeUnwatches[1].element, document.documentElement);
119
- assert.equal(sizeUnwatches[1].handler, dockA1ViewportWatch.handler);
120
- assert.equal(sizeWatches.length, 6);
121
-
122
- const groupMod = new SourceTextModule(await readFile(new URL('../dist/sources/control/dock-group/code.js', import.meta.url), 'utf8'));
123
- await groupMod.link(() => clickgo);
124
- await groupMod.evaluate();
125
- const DockGroup = groupMod.namespace.default;
126
-
127
- const areaRect = {
128
- 'left': 40,
129
- 'right': 80,
130
- 'top': 0,
131
- 'bottom': 300
132
- };
133
- const group = new DockGroup();
134
- group.index = 0;
135
- group.element = {
136
- 'rtl': false,
137
- 'getBoundingClientRect': () => ({
138
- 'top': 0
139
- })
140
- };
141
- group.refs = {
142
- 'content': {
143
- 'offsetHeight': 200
144
- }
145
- };
146
- group.dock = {
147
- 'expandedData': false,
148
- 'floatGroup': 0,
149
- 'floatAreaHeight': 300,
150
- 'viewportWidth': 360,
151
- 'positionData': 'left',
152
- 'widthComp': '400px',
153
- 'getFloatArea': () => ({
154
- 'getBoundingClientRect': () => areaRect
155
- })
156
- };
157
-
158
- group.updateFloatLayout();
159
- assert.equal(group.floatMaxWidth, 280, 'LTR left Dock must use only the viewport space on its right');
160
- assert.equal(group.floatStyle['max-width'], '280px');
161
- assert.equal(group.floatStyle.height, undefined, 'short floating panels must size to their content');
162
- group.props.grow = true;
163
- assert.equal(group.floatStyle.height, '300px', 'grow floating panels fill the available height');
164
- group.props.grow = false;
165
-
166
- group.element.rtl = true;
167
- areaRect.left = 320;
168
- areaRect.right = 360;
169
- group.updateFloatLayout();
170
- assert.equal(group.floatMaxWidth, 320, 'RTL left Dock must open toward the viewport space on its left');
171
-
172
- group.dock.positionData = 'right';
173
- areaRect.left = 0;
174
- areaRect.right = 40;
175
- group.updateFloatLayout();
176
- assert.equal(group.floatMaxWidth, 320, 'RTL right Dock must open toward the viewport space on its right');
177
-
178
- group.element.rtl = false;
179
- group.dock.getFloatBounds = () => ({ 'left': 30, 'right': 330, 'top': 60, 'bottom': 220 });
180
- areaRect.left = 300;
181
- areaRect.right = 340;
182
- group.updateFloatLayout();
183
- assert.equal(group.floatMaxWidth, 270, 'floating panels must stay inside their Form, not only the viewport');
184
- assert.equal(group.floatMaxHeight, '160px');
185
- assert.equal(group.floatTop, 60);
186
- group.dock.positionData = 'left';
187
- group.updateFloatLayout();
188
- assert.equal(group.floatMaxWidth, 0, 'zero available width must remain zero rather than use a fallback');
189
- assert.equal(group.floatStyle['max-width'], '0px');
190
-
191
- const columnMod = new SourceTextModule(await readFile(new URL('../dist/sources/control/dock-column/code.js', import.meta.url), 'utf8'));
192
- await columnMod.link(() => clickgo);
193
- await columnMod.evaluate();
194
- const DockColumn = columnMod.namespace.default;
195
-
196
- document.documentElement.clientWidth = 1200;
197
- document.documentElement.clientHeight = 900;
198
- const multiForm = {
199
- 'element': {
200
- 'isConnected': true,
201
- 'offsetWidth': 1100,
202
- 'getBoundingClientRect': () => ({ 'left': 60, 'right': 1160, 'top': 30, 'bottom': 930 })
203
- }
204
- };
205
- const multiDock = new Dock(multiForm, 600);
206
- await multiDock.onMounted();
207
- assert.deepEqual(multiDock.getFloatBounds(), { 'left': 60, 'right': 1160, 'top': 30, 'bottom': 900 });
208
-
209
- const projectColumn = new DockColumn(multiForm, 600);
210
- const mainColumn = new DockColumn(multiForm, 600);
211
- projectColumn.parents = mainColumn.parents = { 'dock': multiDock };
212
- projectColumn.element = { 'order': 0 };
213
- mainColumn.element = { 'order': 1 };
214
- projectColumn.props.width = 220;
215
- mainColumn.props.width = 340;
216
- // Child groups mount before their column: both columns must allocate through the same root.
217
- const projectIndex = projectColumn.registerGroup();
218
- const mainIndex = mainColumn.registerGroup();
219
- assert.notEqual(projectIndex, mainIndex);
220
- await projectColumn.onMounted();
221
- await mainColumn.onMounted();
222
- assert.equal(projectColumn.expandedData, true);
223
- assert.equal(mainColumn.expandedData, true);
224
-
225
- function resizeMultiForm(width) {
226
- multiForm.element.offsetWidth = width;
227
- multiDock.access.formSizeWatch.handler();
228
- }
229
- resizeMultiForm(850);
230
- assert.equal(projectColumn.autoCollapsed, true, 'the auxiliary column collapses first');
231
- assert.equal(mainColumn.autoCollapsed, false, 'keep the main column open when it still fits');
232
- assert.equal(projectColumn.expandedPreference, true, 'automatic collapse must preserve user preference');
233
- assert.equal(projectColumn.props.expanded, true);
234
- assert.match(multiDock.widthComp, /var\(--dock-collapsed-width\).*340px/);
235
-
236
- resizeMultiForm(650);
237
- assert.equal(projectColumn.expandedData, false);
238
- assert.equal(mainColumn.expandedData, false, 'collapse the main column too when workspace space is insufficient');
239
- resizeMultiForm(1100);
240
- assert.equal(projectColumn.expandedData, true);
241
- assert.equal(mainColumn.expandedData, true);
242
-
243
- document.documentElement.clientWidth = 600;
244
- multiDock.access.viewportSizeWatch.handler();
245
- assert.equal(projectColumn.expandedData, false);
246
- assert.equal(mainColumn.expandedData, false, 'viewport clipping must also limit multiple expanded columns');
247
- document.documentElement.clientWidth = 1200;
248
- multiDock.access.viewportSizeWatch.handler();
249
- assert.equal(mainColumn.expandedData, true);
250
- projectColumn.toggle();
251
- assert.equal(projectColumn.expandedPreference, false);
252
- resizeMultiForm(650);
253
- resizeMultiForm(1100);
254
- assert.equal(projectColumn.expandedData, false, 'a column the user closed must remain closed after resizing');
255
- assert.equal(mainColumn.expandedData, true);
256
-
257
- multiDock.toggleFloat(projectIndex);
258
- multiDock.toggleFloat(mainIndex);
259
- assert.equal(multiDock.floatGroup, mainIndex, 'only one floating group may be open across columns');
260
- mainColumn.unregisterGroup(mainIndex);
261
- assert.equal(multiDock.floatGroup, -1);
262
- multiDock.toggleFloat(projectIndex);
263
- projectColumn.onUnmounted();
264
- assert.equal(multiDock.columns.length, 1);
265
- assert.equal(multiDock.floatGroup, -1, 'column removal closes its floating panel');
266
- mainColumn.onUnmounted();
267
- multiDock.onUnmounted();
268
-
269
- const dockStyle = await readFile(new URL('../dist/sources/control/dock/style.scss', import.meta.url), 'utf8');
270
- assert.match(dockStyle, /border-inline-end/, 'left Dock border must follow the inline end edge');
271
- assert.match(dockStyle, /border-inline-start/, 'right Dock border must follow the inline start edge');
272
- const toggleStyle = await readFile(new URL('../dist/sources/control/sidebar-toggle/style.scss', import.meta.url), 'utf8');
273
- assert.match(toggleStyle, /:dir\(rtl\)[\s\S]*scaleX\(-1\)/, 'shared sidebar chevron must mirror in RTL');
274
-
275
- const groupStyle = await readFile(new URL('../dist/sources/control/dock-group/style.scss', import.meta.url), 'utf8');
276
- assert.match(groupStyle, /inset-inline-start:\s*100%/, 'left Dock floating panel must open toward inline end');
277
- assert.match(groupStyle, /inset-inline-end:\s*100%/, 'right Dock floating panel must open toward inline start');
278
- assert.match(groupStyle, /max-width:\s*calc\(100vw - 40px\)/, 'floating panel must reserve the collapsed Dock width');
279
-
280
- console.log('Dock lifecycle, multiple columns, preference restoration, RTL and Form/viewport bounds passed.');
@@ -1,261 +0,0 @@
1
- // Run: node --experimental-vm-modules test/dom-style.mjs
2
- import assert from 'node:assert/strict';
3
- import { readFile } from 'node:fs/promises';
4
- import { SourceTextModule, SyntheticModule } from 'node:vm';
5
- import { JSDOM } from 'jsdom';
6
- import ts from 'typescript';
7
-
8
- const documentDom = new JSDOM('<!doctype html><html><body></body></html>');
9
- for (const name of ['window', 'document', 'HTMLElement', 'Element', 'Node']) {
10
- Object.defineProperty(globalThis, name, { 'configurable': true, 'value': documentDom.window[name] });
11
- }
12
- window.setTimeout = () => 0;
13
- globalThis.ResizeObserver = class { observe() {} unobserve() {} };
14
- globalThis.requestAnimationFrame = () => 0;
15
- const values = new WeakMap();
16
- const reads = [];
17
- let computedCalls = 0;
18
- globalThis.getComputedStyle = (el) => {
19
- ++computedCalls;
20
- return new Proxy({}, {
21
- get(_, name) {
22
- reads.push([el, name]);
23
- return values.get(el)?.[name] ?? '';
24
- },
25
- });
26
- };
27
- let focus = 'form-a';
28
- let activePanels = [];
29
- const formModule = new SyntheticModule(['getFocus', 'getActivePanel'], function() {
30
- this.setExport('getFocus', () => focus);
31
- this.setExport('getActivePanel', () => activePanels);
32
- });
33
- const emptyModule = new SyntheticModule([], function() {});
34
- const source = await readFile(new URL('../dist/lib/dom.ts', import.meta.url), 'utf8');
35
- const domModule = new SourceTextModule(ts.transpileModule(`${source}\nexport { watchTimerHandler };`, {
36
- 'compilerOptions': { 'target': ts.ScriptTarget.ES2022, 'module': ts.ModuleKind.ESNext },
37
- }).outputText);
38
- await domModule.link((specifier) => specifier === './form' ? formModule : emptyModule);
39
- await domModule.evaluate();
40
- const api = domModule.namespace;
41
- const tick = api.watchTimerHandler;
42
- const formA = document.createElement('div');
43
- formA.dataset.formId = 'form-a';
44
- document.body.append(formA);
45
- const formB = document.createElement('div');
46
- formB.dataset.formId = 'form-b';
47
- document.body.append(formB);
48
-
49
- /** --- 创建有独立计算样式值的测试元素 --- */
50
- function element(parent = formA, style = { 'font-size': '13px' }) {
51
- const el = document.createElement('div');
52
- parent.append(el);
53
- values.set(el, style);
54
- return el;
55
- }
56
- const events = [];
57
- const first = element();
58
- const second = element();
59
- const firstHandler = (name, value, old) => {
60
- events.push(['first', name, value, old, reads.length]);
61
- values.get(second)['font-size'] = '30px';
62
- };
63
- api.watchStyle(first, 'font-size', firstHandler, true);
64
- assert.deepEqual(events[0].slice(0, 4), ['first', 'font-size', '13px', '']);
65
- values.get(second)['font-size'] = '13px';
66
- api.watchStyle(second, 'font-size', (name, value, old) => {
67
- events.push(['second', name, value, old, reads.length]);
68
- });
69
- const extraEvents = [];
70
- const extraHandler = (...args) => extraEvents.push(args);
71
- const beforeCalls = computedCalls;
72
- api.watchStyle(first, 'font-size', extraHandler);
73
- assert.equal(computedCalls, beforeCalls, 'repeat registration reuses the live style object');
74
- values.get(first)['font-size'] = '14px';
75
- values.get(second)['font-size'] = '15px';
76
- reads.length = 0;
77
- events.length = 0;
78
- tick();
79
- assert.equal(reads.length, 2, 'each changed property is read once, including multiple subscribers');
80
- assert.deepEqual(events.map(event => event.slice(0, 4)), [
81
- ['first', 'font-size', '14px', '13px'],
82
- ['second', 'font-size', '15px', '13px'],
83
- ]);
84
- assert.ok(events.every(event => event[4] === 2), 'all style reads finish before any callback');
85
- assert.deepEqual(extraEvents, [['font-size', '14px', '13px']]);
86
- events.length = 0;
87
- tick();
88
- assert.deepEqual(events[0].slice(0, 4), ['second', 'font-size', '30px', '15px'], 'callback writes are detected in the following poll');
89
- events.length = 0;
90
- tick();
91
- assert.equal(events.length, 0, 'unchanged values do not notify');
92
- api.unwatchStyle(first, undefined, firstHandler);
93
- values.get(first)['font-size'] = '16px';
94
- tick();
95
- assert.equal(events.length, 0, 'removing one callback preserves the other subscriber');
96
- assert.deepEqual(extraEvents.at(-1), ['font-size', '16px', '14px']);
97
- api.unwatchStyle(first);
98
- assert.equal(api.isWatchStyle(first), false);
99
- assert.equal(first.dataset.cgStyleindex, undefined);
100
- api.unwatchStyle(second);
101
-
102
- // Copied DOM attributes must never reuse another element's subscription.
103
- const original = element();
104
- api.watchStyle(original, 'font-size', () => {});
105
- const copy = original.cloneNode();
106
- formA.append(copy);
107
- values.set(copy, { 'font-size': '20px' });
108
- assert.equal(api.isWatchStyle(copy), false);
109
- api.watchStyle(copy, 'font-size', () => {});
110
- assert.notEqual(copy.dataset.cgStyleindex, original.dataset.cgStyleindex);
111
- original.remove();
112
- activePanels = ['nonexistent'];
113
- tick();
114
- assert.equal(api.isWatchStyle(original), false);
115
- assert.equal(original.dataset.cgStyleindex, undefined);
116
- formA.append(original);
117
- api.watchStyle(original, 'font-size', () => {});
118
- assert.equal(api.isWatchStyle(original), true, 'a detached and reinserted element can be registered again');
119
- api.clearWatchStyle('form-a');
120
- assert.equal(api.isWatchStyle(copy), false);
121
- assert.equal(api.isWatchStyle(original), false);
122
-
123
- // Keep focused Form and active Panel filtering, including delayed changes on activation.
124
- const panel = document.createElement('div');
125
- panel.dataset.panelId = 'panel-a';
126
- formA.append(panel);
127
- const main = element();
128
- const panelItem = element(panel);
129
- const otherFormItem = element(formB);
130
- const scopedEvents = [];
131
- for (const [label, el] of [['main', main], ['panel', panelItem], ['other', otherFormItem]]) {
132
- api.watchStyle(el, 'font-size', () => scopedEvents.push(label));
133
- values.get(el)['font-size'] = '18px';
134
- }
135
- activePanels = [];
136
- tick();
137
- assert.deepEqual(scopedEvents, ['main']);
138
- activePanels = ['panel-a'];
139
- tick();
140
- assert.deepEqual(scopedEvents, ['main', 'panel']);
141
- focus = 'form-b';
142
- tick();
143
- assert.deepEqual(scopedEvents, ['main', 'panel', 'other']);
144
- api.clearWatchStyle('form-a', 'panel-a');
145
- assert.equal(api.isWatchStyle(main), true);
146
- assert.equal(api.isWatchStyle(panelItem), false);
147
- api.clearWatchStyle('form-a');
148
- api.clearWatchStyle('form-b');
149
- focus = 'form-a';
150
- activePanels = ['panel-a'];
151
-
152
- // An earlier notification may cancel records already collected for this poll.
153
- const cancelFirst = element();
154
- const cancelSecond = element();
155
- let cancelledCalls = 0;
156
- api.watchStyle(cancelFirst, 'font-size', () => api.clearWatchStyle('form-a'));
157
- api.watchStyle(cancelSecond, 'font-size', () => ++cancelledCalls);
158
- values.get(cancelFirst)['font-size'] = '22px';
159
- values.get(cancelSecond)['font-size'] = '22px';
160
- tick();
161
- assert.equal(cancelledCalls, 0, 'clearing a Form cancels queued notifications');
162
- assert.equal(api.isWatchStyle(cancelSecond), false);
163
-
164
- const named = element(formA, { 'font-size': '13px', 'font-family': 'sans-serif' });
165
- api.watchStyle(named, ['font-size', 'font-family'], () => {});
166
- api.unwatchStyle(named, 'font-size');
167
- assert.equal(api.isWatchStyle(named), true);
168
- assert.deepEqual(api.getWatchInfo().default.unknown.style.list, ['font-family']);
169
- api.unwatchStyle(named, 'font-family');
170
- assert.equal(api.isWatchStyle(named), false);
171
-
172
- // Removing a later queued element also suppresses its notification and releases its index.
173
- const removeFirst = element();
174
- const removeSecond = element();
175
- api.watchStyle(removeFirst, 'font-size', () => removeSecond.remove());
176
- api.watchStyle(removeSecond, 'font-size', () => ++cancelledCalls);
177
- values.get(removeFirst)['font-size'] = '19px';
178
- values.get(removeSecond)['font-size'] = '19px';
179
- tick();
180
- assert.equal(cancelledCalls, 0);
181
- assert.equal(api.isWatchStyle(removeSecond), false);
182
- api.unwatchStyle(removeFirst);
183
- const immediateCancel = element(formA, { 'font-size': '13px', 'font-family': 'sans-serif' });
184
- const immediateNames = [];
185
- api.watchStyle(immediateCancel, ['font-size', 'font-family'], name => {
186
- immediateNames.push(name);
187
- api.unwatchStyle(immediateCancel);
188
- }, true);
189
- assert.deepEqual(immediateNames, ['font-size']);
190
- assert.equal(api.isWatchStyle(immediateCancel), false);
191
-
192
- // Reentrant registration retains per-property subscription order and cancellation.
193
- const reentrant = element();
194
- let registeredCalls = 0;
195
- const registered = () => ++registeredCalls;
196
- api.watchStyle(reentrant, 'font-size', () => api.watchStyle(reentrant, 'font-size', registered));
197
- values.get(reentrant)['font-size'] = '14px';
198
- tick();
199
- assert.equal(registeredCalls, 0, 'a callback registered during dispatch does not receive an old queued change');
200
- values.get(reentrant)['font-size'] = '15px';
201
- tick();
202
- assert.equal(registeredCalls, 1);
203
- api.unwatchStyle(reentrant);
204
-
205
- // A failed subscriber must not discard collected changes or stop subsequent polls.
206
- const failing = element();
207
- const following = element();
208
- const followingValues = [];
209
- api.watchStyle(failing, 'font-size', () => { throw new Error('test subscriber failure'); });
210
- api.watchStyle(following, 'font-size', (name, value) => followingValues.push(value));
211
- values.get(failing)['font-size'] = '17px';
212
- values.get(following)['font-size'] = '17px';
213
- tick();
214
- values.get(following)['font-size'] = '18px';
215
- tick();
216
- assert.deepEqual(followingValues, ['17px', '18px']);
217
- api.clearWatchStyle('form-a');
218
-
219
- // Stab coalesces repeated requests and discards pending measurements on unmount.
220
- class AbstractControl {
221
- constructor() {
222
- this.element = element();
223
- }
224
- nextTick() { return Promise.resolve(); }
225
- watch(name, callback, options) { if (options?.immediate) callback(); }
226
- }
227
- const stopped = [];
228
- const clickgoModule = new SyntheticModule(['control', 'dom'], function() {
229
- this.setExport('control', { AbstractControl });
230
- this.setExport('dom', {
231
- 'watchSizeMulti': () => true,
232
- 'unwatchSizeMulti': (...args) => stopped.push(args),
233
- 'watchStyle': api.watchStyle,
234
- 'unwatchStyle': api.unwatchStyle,
235
- });
236
- });
237
- const stabSource = await readFile(new URL('../dist/sources/control/stab/code.ts', import.meta.url), 'utf8');
238
- const stabModule = new SourceTextModule(ts.transpileModule(stabSource, {
239
- 'compilerOptions': { 'target': ts.ScriptTarget.ES2022, 'module': ts.ModuleKind.ESNext },
240
- }).outputText);
241
- await stabModule.link(() => clickgoModule);
242
- await stabModule.evaluate();
243
- const stab = new stabModule.namespace.default();
244
- stab.props.type = 'rect';
245
- let measurements = 0;
246
- stab.resize = () => ++measurements;
247
- stab.onMounted();
248
- for (let i = 0; i < 9; ++i) stab.requestResize();
249
- await Promise.resolve();
250
- assert.equal(measurements, 1);
251
- const unrelated = () => {};
252
- api.watchStyle(stab.element, 'font-size', unrelated);
253
- stab.requestResize();
254
- stab.onBeforeUnmount();
255
- await Promise.resolve();
256
- assert.equal(measurements, 1, 'unmount cancels pending geometry measurement');
257
- assert.equal(stopped.length, 1);
258
- assert.equal(api.isWatchStyle(stab.element), true, 'unmount preserves unrelated style subscribers');
259
- assert.deepEqual(api.getWatchInfo().default.unknown.style.list, ['font-size']);
260
- api.unwatchStyle(stab.element);
261
- console.log('Style read batching, callback order/cancellation, DOM identity, Form/Panel isolation and stab resize coalescing passed.');