clickgo 6.8.2 → 6.10.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 (58) hide show
  1. package/AGENTS.md +16 -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/control/common.cgc +0 -0
  7. package/dist/control/form.cgc +0 -0
  8. package/dist/control/novnc.cgc +0 -0
  9. package/dist/control/task.cgc +0 -0
  10. package/dist/global.css +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/lib/command/native.d.ts +18 -0
  13. package/dist/lib/command/palette.d.ts +10 -0
  14. package/dist/lib/command/schema.d.ts +31 -0
  15. package/dist/lib/command.d.ts +218 -0
  16. package/dist/lib/core.d.ts +7 -1
  17. package/dist/lib/form.d.ts +15 -2
  18. package/dist/lib/native.d.ts +12 -0
  19. package/dist/lib/task.d.ts +2 -0
  20. package/dist/theme/admin.cgt +0 -0
  21. package/dist/theme/classic.cgt +0 -0
  22. package/dist/theme/cyber.cgt +0 -0
  23. package/dist/theme/dark.cgt +0 -0
  24. package/dist/theme/light.cgt +0 -0
  25. package/dist/theme/modern-light.cgt +0 -0
  26. package/dist/theme/modern.cgt +0 -0
  27. package/dist/theme/software-dark.cgt +0 -0
  28. package/dist/theme/software-light.cgt +0 -0
  29. package/doc/clickgo-rag.md +2735 -711
  30. package/package.json +11 -4
  31. package/test/core-module-loading.mjs +0 -169
  32. package/test/desktop.mjs +0 -715
  33. package/test/dock-lifecycle.mjs +0 -280
  34. package/test/dom-style.mjs +0 -261
  35. package/test/form-launcher.mjs +0 -102
  36. package/test/form-modal-layer.mjs +0 -75
  37. package/test/form-native-regions.mjs +0 -115
  38. package/test/form-native-style.mjs +0 -80
  39. package/test/form-native.mjs +0 -68
  40. package/test/form-notify.mjs +0 -90
  41. package/test/form-viewport.mjs +0 -127
  42. package/test/iconview.mjs +0 -70
  43. package/test/jodit-lifecycle.mjs +0 -212
  44. package/test/locale-integrity.mjs +0 -189
  45. package/test/pop-max-size.mjs +0 -66
  46. package/test/rtl-archive.mjs +0 -428
  47. package/test/rtl-direction.mjs +0 -193
  48. package/test/scroll.mjs +0 -145
  49. package/test/select.mjs +0 -885
  50. package/test/sidebar-toggle.mjs +0 -75
  51. package/test/task-package-loading.mjs +0 -94
  52. package/test/task-tray.mjs +0 -331
  53. package/test/tool-assets.mjs +0 -64
  54. package/test/tool-clone.mjs +0 -42
  55. package/test/toolbox.mjs +0 -88
  56. package/test/tuieditor-locale.mjs +0 -175
  57. package/test/vflow.mjs +0 -122
  58. package/test/web-drag-mask.mjs +0 -96
@@ -1,75 +0,0 @@
1
- // Run after TypeScript compilation: node --experimental-vm-modules test/sidebar-toggle.mjs
2
- import assert from 'node:assert/strict';
3
- import { readFile } from 'node:fs/promises';
4
- import { SourceTextModule, SyntheticModule } from 'node:vm';
5
-
6
- class AbstractControl {
7
- constructor() {
8
- this.emitted = [];
9
- this.locale = 'sc';
10
- }
11
- propBoolean(name) {
12
- return [true, 'true', ''].includes(this.props[name]);
13
- }
14
- l(key) {
15
- return this.localeData[this.locale][key];
16
- }
17
- emit(name, value) {
18
- this.emitted.push([name, value]);
19
- }
20
- }
21
-
22
- const clickgo = new SyntheticModule(['control'], function() {
23
- this.setExport('control', { AbstractControl });
24
- });
25
- const mod = new SourceTextModule(await readFile(new URL('../dist/sources/control/sidebar-toggle/code.js', import.meta.url), 'utf8'));
26
- await mod.link(() => clickgo);
27
- await mod.evaluate();
28
- const SidebarToggle = mod.namespace.default;
29
- const toggle = new SidebarToggle();
30
-
31
- for (const [position, expanded, pointsRight] of [
32
- ['left', false, true], ['left', true, false], ['right', false, false], ['right', true, true],
33
- ]) {
34
- toggle.props.position = position;
35
- toggle.props.expanded = expanded;
36
- assert.equal(toggle.chevronRight, pointsRight, `${position}, expanded=${expanded}`);
37
- }
38
-
39
- toggle.props.expanded = 'false';
40
- toggle.props.label = 'Project';
41
- assert.equal(toggle.tipLabelComp, '展开侧栏 · Project');
42
- toggle.props.expanded = 'true';
43
- assert.equal(toggle.tipLabelComp, '收起侧栏 · Project');
44
- toggle.locale = 'ar';
45
- assert.equal(toggle.tipLabelComp, 'طي الشريط الجانبي · Project');
46
- toggle.props.tipLabel = 'Use one tool column';
47
- assert.equal(toggle.tipLabelComp, 'Use one tool column', 'Toolbox keeps its column-specific operation');
48
-
49
- for (const key of ['Enter', ' ']) {
50
- toggle.props.expanded = false;
51
- const count = toggle.emitted.length;
52
- let prevented = false;
53
- let stopped = false;
54
- const event = {
55
- key, 'repeat': false,
56
- 'preventDefault': () => { prevented = true; },
57
- 'stopPropagation': () => { stopped = true; },
58
- };
59
- toggle.keydown(event);
60
- assert.equal(prevented && stopped, true);
61
- assert.deepEqual(toggle.emitted[count], ['update:expanded', true]);
62
- assert.equal(toggle.props.expanded, false, 'the owner applies the requested state');
63
- toggle.keydown({ ...event, 'repeat': true });
64
- assert.equal(toggle.emitted.length, count + 1, 'holding a key must not repeatedly flip the sidebar');
65
- }
66
- toggle.keydown({
67
- 'key': 'Tab',
68
- 'preventDefault': () => assert.fail('Tab keeps native focus navigation'),
69
- 'stopPropagation': () => assert.fail('Other keys are not consumed'),
70
- });
71
- toggle.props.expanded = true;
72
- toggle.toggle();
73
- assert.deepEqual(toggle.emitted.at(-1), ['update:expanded', false]);
74
-
75
- console.log('Shared sidebar toggle direction, localized tips, controlled state and keyboard activation passed.');
@@ -1,94 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { readFile } from 'node:fs/promises';
3
- import vm from 'node:vm';
4
- import ts from 'typescript';
5
-
6
- const requests = new Map();
7
- const styles = [];
8
- const progress = [];
9
- const events = [];
10
- let id = 0;
11
- let taskApi;
12
- function getContent(taskId, path) {
13
- return new Promise((resolve, reject) => requests.set(path, { resolve, reject }));
14
- }
15
- const tool = {
16
- urlResolve: (base, path) => path,
17
- random: () => `id${++id}`,
18
- stateMachine() {},
19
- runIife: () => class { async main() { events.push('main'); } },
20
- stylePrepend: style => ({ prep: '', style }),
21
- styleUrl2DataUrl: async (base, style) => style,
22
- layoutAddTagClassAndReTagName: layout => layout,
23
- layoutClassPrepend: layout => layout,
24
- eventsAttrWrap: layout => layout,
25
- getClassPrototype: () => ({ method: {}, access: {} }),
26
- };
27
- const core = { config: { locale: 'en' }, trigger: async (...args) => { events.push(args); } };
28
- const form = { notify() {}, notifyContent() {} };
29
- const dom = { createToStyleList() {}, removeFromStyleList() {}, pushStyle: (taskId, style) => styles.push(style) };
30
- const zip = { get: async blob => {
31
- const label = await blob.text();
32
- return {
33
- readDir: path => path === '/' ? [{ name: 'same', isFile: false }] : [
34
- { path: 'same/', name: 'layout.html' }, { path: 'same/', name: 'style.css' },
35
- ],
36
- getContent: async path => path.endsWith('config.json') ?
37
- JSON.stringify({ name: 'same', layout: 'layout', style: 'style' }) :
38
- path.endsWith('html') ? `<div>${label}</div>` : label,
39
- };
40
- } };
41
- tool.getMimeByPath = name => ({ ext: name.split('.').pop() });
42
- async function load(file, modules) {
43
- const source = await readFile(new URL(`../dist/lib/${file}.ts`, import.meta.url), 'utf8');
44
- const exports = {};
45
- vm.runInNewContext(ts.transpileModule(source, {
46
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
47
- }).outputText, { exports, require: name => modules[name] ?? {}, Blob, setTimeout, clearTimeout,
48
- window: { innerWidth: 800, innerHeight: 600 } });
49
- return exports;
50
- }
51
- const clickgo = { getDirname: () => '/clickgo/', modules: { vue: { reactive: value => value, watch() {} } } };
52
- const common = { '../clickgo': clickgo, './tool': tool, './core': core, './fs': { getContent }, './dom': dom, './form': form };
53
- const controls = await load('control', { ...common, './zip': zip, './task': { getOrigin: taskId => taskApi.getOrigin(taskId) } });
54
- const themeOrder = [];
55
- taskApi = await load('task', { ...common, './control': controls, './theme': {
56
- read: async blob => ({ name: await blob.text() }),
57
- load: async (taskId, theme) => { themeOrder.push(theme?.name ?? 'global'); },
58
- } });
59
- taskApi.initSysId('sys');
60
- taskApi.init();
61
- const app = {
62
- type: 'app', config: { name: 'Concurrent packages', controls: ['/a', '/b.cgc'], themes: ['/light', '/dark'] },
63
- package: { getContent: async path => path === '/app.js' ? 'app fixture' : null },
64
- };
65
- const running = taskApi.run('sys', app, { notify: false, progress: (loaded, total, type, path) => progress.push(path) });
66
- await new Promise(resolve => setImmediate(resolve));
67
- assert.deepEqual([...requests.keys()], ['/light.cgt', '/dark.cgt', '/a.cgc', '/b.cgc'], 'all themes and controls request before the first package completes');
68
- requests.get('/b.cgc').resolve(new Blob(['second']));
69
- requests.get('/dark.cgt').resolve(new Blob(['dark']));
70
- await new Promise(resolve => setImmediate(resolve));
71
- assert.deepEqual(styles, [], 'later downloads cannot change registration order');
72
- assert.deepEqual(themeOrder, [], 'themes wait for controls and preceding themes');
73
- requests.get('/a.cgc').resolve(new Blob(['first']));
74
- await new Promise(resolve => setImmediate(resolve));
75
- assert.deepEqual(styles, ['first', 'second'], 'CSS registers in configured order');
76
- assert.deepEqual(progress, ['/a.cgc', '/b.cgc']);
77
- requests.get('/light.cgt').resolve(new Blob(['light']));
78
- const taskId = await running;
79
- assert.equal(typeof taskId, 'string');
80
- assert.ok(taskApi.getOrigin(taskId).controls.same.layout.includes('second'), 'last configured duplicate control wins');
81
- assert.deepEqual(themeOrder, ['light', 'dark'], 'theme application preserves configured order');
82
- assert.equal(events.at(-1), 'main');
83
-
84
- requests.clear();
85
- const failed = taskApi.run('sys', { ...app, config: { ...app.config, controls: ['/missing', '/later'], themes: ['/unused'] } }, { notify: false });
86
- await new Promise(resolve => setImmediate(resolve));
87
- requests.get('/later.cgc').reject(new Error('later request failure'));
88
- requests.get('/unused.cgt').reject(new Error('unused theme failure'));
89
- await new Promise(resolve => setImmediate(resolve));
90
- requests.get('/missing.cgc').resolve(null);
91
- assert.equal(await failed, -905, 'missing control retains the task failure code');
92
- await new Promise(resolve => setImmediate(resolve));
93
- assert.deepEqual(themeOrder, ['light', 'dark'], 'failure does not apply prefetched themes');
94
- console.log('Concurrent CGC/CGT downloads, ordered registration/themes, duplicate controls and failure handling passed.');
@@ -1,331 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import vm from 'node:vm';
3
- import { readFile } from 'node:fs/promises';
4
- import ts from 'typescript';
5
-
6
- const events = [];
7
- const sizeWatch = new Map();
8
- const pendingIcons = new Map();
9
- const core = {
10
- config: { 'task.position': 'bottom', 'task.mode': 'bar', 'task.margin': 8 },
11
- trigger: async (...args) => { events.push(args); },
12
- };
13
- const form = { simpleSystemTaskRoot: { forms: {} }, notify() {}, getList: () => ({}),
14
- getMaxZIndexID: async () => null, changeFocus: async () => {},
15
- elements: { list: { querySelectorAll: () => [] } } };
16
- const tick = [];
17
- const clickgo = {
18
- modules: { vue: { reactive: value => value, watch() {}, nextTick: callback => {
19
- const promise = Promise.resolve().then(callback); tick.push(promise); return promise;
20
- } } },
21
- isNative: () => false,
22
- };
23
- const modules = {
24
- '../clickgo': clickgo, './core': core, './form': form,
25
- './tool': { clone: structuredClone, blob2DataUrl: async () => 'data:image/png;base64,icon' },
26
- './fs': { getContent: async (taskId, path) => {
27
- if (path === '/slow') return new Promise(resolve => pendingIcons.set(taskId, resolve));
28
- assert.ok(path.startsWith('/package/') || path.startsWith('/clickgo/'));
29
- return new Blob(['icon']);
30
- } },
31
- './control': { clearComponents() {} },
32
- './native': { clear() {} },
33
- './dom': {
34
- removeFromStyleList() {}, clearWatchSize() {}, clearWatch() {},
35
- watchSizeMulti: (taskId, el, callback) => { sizeWatch.set(el, { taskId, callback }); },
36
- unwatchSizeMulti: (taskId, el) => { sizeWatch.delete(el); },
37
- },
38
- };
39
- const source = await readFile(new URL('../dist/lib/task.ts', import.meta.url), 'utf8');
40
- const compiled = ts.transpileModule(source + '\nexport const testState = { list, runtime };', {
41
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
42
- }).outputText;
43
- const exports = {};
44
- vm.runInNewContext(compiled, { exports, require: name => modules[name] ?? {}, Blob, setTimeout, clearTimeout,
45
- window: { innerWidth: 800, innerHeight: 600 } });
46
- const api = exports;
47
- api.initSysId('sys');
48
- api.init();
49
- const calls = [];
50
- const owner = {
51
- id: 'owner', class: { onTrayClick: id => calls.push(['app', id]), onTrayMenuClick: (id, cmd) => calls.push(['app-menu', id, cmd]) },
52
- forms: { main: { vroot: { onTrayClick: id => calls.push(['form', id]), onTrayMenuClick: (id, cmd) => calls.push(['form-menu', id, cmd]) } } },
53
- };
54
- api.testState.list.owner = owner;
55
- api.testState.list.other = { id: 'other', forms: {} };
56
- api.testState.runtime.other = { permissions: [] };
57
- const input = { icon: '/package/res/icon.png', tip: 'Tray', menu: [
58
- { id: 'show', label: 'Show' }, { id: 'disabled', label: 'Disabled', disabled: true },
59
- { id: 'split', label: '', separator: true },
60
- ] };
61
- const id = await api.createTray('owner', input);
62
- assert.equal(typeof id, 'string');
63
- input.menu[0].label = 'Changed externally';
64
- assert.equal(api.getTrayList('owner')[id].menu[0].label, 'Show');
65
- assert.equal(Object.keys(api.getTrayList('other')).length, 0);
66
- assert.equal(await api.updateTray('other', id, { tip: 'Hijacked' }), false);
67
- assert.equal(api.removeTray('other', id), false);
68
- assert.equal(await api.activateTray('other', id), false);
69
- assert.equal(await api.activateTray('owner', id), false);
70
-
71
- function bar(taskId, formId, width, height) {
72
- const props = {};
73
- const el = { offsetWidth: width, offsetHeight: height };
74
- const vroot = { position: 'bottom', $el: el, $refs: { form: { setPropData: (key, value) => { props[key] = value; } } } };
75
- api.testState.list[taskId] = { id: taskId, forms: { [formId]: { vroot } } };
76
- return { props, el };
77
- }
78
- const first = bar('bar1', 'f1', 220, 44);
79
- assert.equal(api.setSystem('bar1', 'f1'), true);
80
- await Promise.all(tick);
81
- assert.equal(first.props.width, 800);
82
- assert.equal(first.props.top, 556);
83
- assert.equal(api.systemTaskInfo.length, 44);
84
- assert.equal(Object.keys(api.getTrayList('bar1')).length, 1);
85
- const snapshot = api.getTrayList('bar1'); snapshot[id].menu[0].label = 'Hijacked';
86
- assert.equal(api.getTrayList('owner')[id].menu[0].label, 'Show');
87
- assert.equal(await api.activateTray('bar1', id, 'disabled'), false);
88
- assert.equal(await api.activateTray('bar1', id, 'split'), false);
89
- assert.equal(await api.activateTray('bar1', id, 'stale'), false);
90
- assert.equal(await api.activateTray('bar1', id), true);
91
- assert.equal(await api.activateTray('bar1', id, 'show'), true);
92
- assert.equal(calls.length, 4);
93
- core.config['task.mode'] = 'dock';
94
- api.refreshSystemPosition();
95
- await Promise.all(tick);
96
- assert.deepEqual(first.props, { width: 0, height: 0, left: 290, top: 548 });
97
- assert.equal(api.systemTaskInfo.length, 52);
98
- first.el.offsetWidth = 300;
99
- sizeWatch.get(first.el).callback();
100
- await Promise.all(tick);
101
- assert.equal(first.props.left, 250);
102
- for (const [position, expected] of Object.entries({
103
- top: [250, 8, 52], left: [8, 278, 308], right: [492, 278, 308], bottom: [250, 548, 52],
104
- })) {
105
- core.config['task.position'] = position;
106
- api.refreshSystemPosition(); await Promise.all(tick);
107
- assert.deepEqual([first.props.left, first.props.top, api.systemTaskInfo.length], expected);
108
- }
109
- // An old pending position update cannot move a replacement bar.
110
- api.refreshSystemPosition();
111
- const second = bar('bar2', 'f2', 100, 40);
112
- api.setSystem('bar2', 'f2'); await Promise.all(tick);
113
- assert.equal(sizeWatch.has(first.el), false);
114
- assert.equal(sizeWatch.has(second.el), true);
115
- assert.equal(second.props.left, 350);
116
- assert.equal(await api.activateTray('bar1', id), false);
117
- assert.equal(await api.activateTray('bar2', id), true);
118
- const old = api.updateTray('owner', id, { icon: '/slow' });
119
- await Promise.resolve();
120
- await api.updateTray('owner', id, { icon: 'data:image/png;base64,new', tip: 'New' });
121
- pendingIcons.get('owner')(new Blob(['old']));
122
- assert.equal(await old, false);
123
- assert.equal(api.getTrayList('owner')[id].icon, 'data:image/png;base64,new');
124
- const deletedUpdate = api.updateTray('owner', id, { icon: '/slow' });
125
- await Promise.resolve(); api.removeTray('owner', id);
126
- pendingIcons.get('owner')(new Blob(['old']));
127
- assert.equal(await deletedUpdate, false);
128
- assert.equal(await api.activateTray('bar2', id), false);
129
- const creation = api.createTray('owner', { icon: '/slow' });
130
- await Promise.resolve(); delete api.testState.list.owner;
131
- pendingIcons.get('owner')(new Blob(['old']));
132
- assert.equal(await creation, false, 'late registration cannot resurrect an ended task');
133
- assert.equal(Object.keys(api.getTrayList('sys')).length, 0);
134
- await api.clearSystem('bar2');
135
- assert.equal(sizeWatch.size, 0);
136
- assert.equal(api.systemTaskInfo.length, 0);
137
- assert.ok(events.some(event => event[0] === 'trayCreated'));
138
- assert.ok(events.some(event => event[0] === 'trayChanged'));
139
- assert.ok(events.some(event => event[0] === 'trayRemoved'));
140
- console.log('Tray ownership, snapshot isolation, app/form delivery, disabled/stale commands, replacement, async races and four-edge Dock geometry passed.');
141
-
142
- // Exercise real public event routing, including a taskbar without root permission.
143
- const coreSource = await readFile(new URL('../dist/lib/core.ts', import.meta.url), 'utf8');
144
- const coreExports = {};
145
- vm.runInNewContext(ts.transpileModule(coreSource, {
146
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
147
- }).outputText, { exports: coreExports, require: name => name === './task' ? api : (modules[name] ?? {}), window: { addEventListener() {} }, TextEncoder });
148
- coreExports.initSysId('sys');
149
- const delivered = [];
150
- const observer = name => ({ onTrayCreated: (ownerId, trayId) => delivered.push([name, ownerId, trayId]) });
151
- api.testState.list.owner = { class: observer('owner-app'), forms: { ownerForm: { vroot: observer('owner-form') } } };
152
- api.testState.list.other = { class: observer('other'), forms: {} };
153
- api.testState.list.root = { class: observer('root'), forms: {} };
154
- api.testState.list.bar2.class = observer('system-app');
155
- api.testState.list.bar2.forms.f2.vroot.onTrayCreated = observer('system-form').onTrayCreated;
156
- api.testState.runtime.root = { permissions: ['root'] };
157
- api.systemTaskInfo.taskId = 'bar2';
158
- await coreExports.trigger('trayCreated', 'owner', 'tray100');
159
- assert.deepEqual(delivered.map(item => item[0]).sort(), ['owner-app', 'owner-form', 'root', 'system-app', 'system-form'].sort());
160
- assert.ok(delivered.every(item => item[1] === 'owner' && item[2] === 'tray100'));
161
- api.systemTaskInfo.taskId = '';
162
- delivered.length = 0;
163
- await coreExports.trigger('trayCreated', 'owner', 'tray101');
164
- assert.ok(!delivered.some(item => item[0].startsWith('system')));
165
- console.log('Public tray events reach owner App/Form, root and the designated system taskbar; replacement removes event access.');
166
-
167
- // Prompt/menus can change while an icon is loading without losing either update.
168
- const mergedId = await api.createTray('owner', { icon: 'data:image/png;base64,initial' });
169
- const pending = api.updateTray('owner', mergedId, { icon: '/slow' });
170
- await Promise.resolve();
171
- await api.updateTray('owner', mergedId, { tip: 'latest tip', menu: [
172
- { id: 'command', label: 'First', disabled: true, callback: () => {} },
173
- { id: 'command', label: 'Duplicate' },
174
- ] });
175
- pendingIcons.get('owner')(new Blob(['merged']));
176
- assert.equal(await pending, true);
177
- const merged = api.getTrayList('owner')[mergedId];
178
- assert.equal(merged.tip, 'latest tip');
179
- assert.equal(merged.icon, 'data:image/png;base64,icon');
180
- assert.equal(merged.menu.length, 1);
181
- assert.equal('callback' in merged.menu[0], false);
182
- api.removeTray('owner', mergedId);
183
- console.log('Partial updates merge across pending icons and menus retain only public fields with unique command IDs.');
184
-
185
- // Real task.end releases trays even when there are no application Forms left.
186
- Object.assign(form, { getMaxZIndexID: async () => null, changeFocus: async () => {},
187
- elements: { list: { querySelectorAll: () => [] } } });
188
- Object.assign(modules['./dom'], { removeFromStyleList() {}, clearWatchSize() {}, clearWatch() {} });
189
- modules['./control'] = { clearComponents() {} };
190
- modules['./native'] = { clear() {} };
191
- api.testState.list.exiting = { id: 'exiting', class: {}, forms: {}, threads: {}, timers: {}, app: { package: { clear() {} } } };
192
- const exitingId = await api.createTray('exiting', { icon: 'data:image/png;base64,exiting' });
193
- const lateCreate = api.createTray('exiting', { icon: '/slow' });
194
- await Promise.resolve();
195
- await api.end('exiting');
196
- pendingIcons.get('exiting')(new Blob(['late']));
197
- assert.equal(await lateCreate, false);
198
- assert.equal(api.getTrayList('sys')[exitingId], undefined);
199
- assert.equal(api.testState.list.exiting, undefined);
200
- console.log('Actual task termination removes registered trays and rejects in-flight registrations.');
201
-
202
- // Escape is handled once by the shared popup service, preserving IME and focus.
203
- const formSource = await readFile(new URL('../dist/lib/form.ts', import.meta.url), 'utf8');
204
- const formAst = ts.createSourceFile('form.ts', formSource, ts.ScriptTarget.ES2022, true);
205
- const popupHandler = formAst.statements.find(statement => ts.isFunctionDeclaration(statement) && statement.name?.text === 'keydownPop');
206
- assert.ok(popupHandler);
207
- const popInfo = { list: [], elList: [] };
208
- let popHidden = 0, focusRestored = 0, popupFocused = 0, childFocused = 0, prevented = 0;
209
- const popupContext = vm.createContext({ popInfo, hidePop: () => {
210
- ++popHidden;
211
- popInfo.list.length = 0;
212
- popInfo.elList.length = 0;
213
- } });
214
- vm.runInContext(ts.transpileModule(popupHandler.getText(formAst), {
215
- compilerOptions: { target: ts.ScriptTarget.ES2022 },
216
- }).outputText, popupContext);
217
- const key = (name, isComposing = false) => ({ key: name, isComposing, preventDefault: () => { ++prevented; } });
218
- popupContext.keydownPop(key('Escape'));
219
- assert.equal(popHidden, 0);
220
- popInfo.list = [
221
- { focus: () => { ++popupFocused; } },
222
- { focus: () => { ++popupFocused; } },
223
- ];
224
- popInfo.elList = [
225
- { focus: options => {
226
- assert.equal(popHidden, 1);
227
- assert.equal(popInfo.list.length, 0);
228
- assert.equal(popInfo.elList.length, 0);
229
- assert.equal(options.preventScroll, true);
230
- ++focusRestored;
231
- } },
232
- { focus: () => { ++childFocused; } },
233
- ];
234
- popupContext.keydownPop(key('Escape', true));
235
- popupContext.keydownPop(key('Enter'));
236
- assert.equal(popHidden, 0);
237
- assert.equal(focusRestored, 0);
238
- assert.equal(prevented, 0);
239
- popupContext.keydownPop(key('Escape'));
240
- assert.equal(popHidden, 1);
241
- assert.equal(focusRestored, 1);
242
- assert.equal(popupFocused, 0);
243
- assert.equal(childFocused, 0);
244
- assert.equal(prevented, 1);
245
- popupContext.keydownPop(key('Escape'));
246
- assert.equal(popHidden, 1);
247
- assert.equal(focusRestored, 1);
248
- assert.equal(prevented, 1);
249
- console.log('Shared popup Escape handling ignores IME, closes once and restores the trigger focus.');
250
-
251
- // Tray keyboard and lifecycle use the same public popup path as pointer menus.
252
- const trayEvents = [], trayPops = [];
253
- clickgo.control = { AbstractControl: class {
254
- constructor() { this.slots = {}; this.refs = { pop: {} }; this.element = { dataset: {} }; }
255
- emit(...args) { trayEvents.push(args); }
256
- watch(_name, callback) { this.menuWatcher = callback; }
257
- } };
258
- clickgo.form = { hidePop: () => trayPops.push('hide'), showPop: (...args) => trayPops.push(args) };
259
- const traySource = await readFile(new URL('../dist/sources/control/task-tray/code.ts', import.meta.url), 'utf8');
260
- const trayExports = {};
261
- vm.runInNewContext(ts.transpileModule(traySource, {
262
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
263
- }).outputText, { exports: trayExports, require: () => clickgo });
264
- const trayControl = new trayExports.default();
265
- trayControl.keydown({ ...key('Enter'), repeat: true });
266
- assert.equal(trayEvents.length, 0);
267
- trayControl.keydown(key('Enter'));
268
- trayControl.keydown(key(' '));
269
- assert.deepEqual(trayEvents.map(event => event[0]), ['activate', 'activate']);
270
- trayControl.keydown(key('ContextMenu'));
271
- assert.equal(trayPops.filter(Array.isArray).length, 0);
272
- trayControl.props.menu = [{ id: 'show', label: 'Show' }];
273
- trayControl.keydown({ ...key('F10'), shiftKey: true });
274
- assert.equal(trayPops.filter(Array.isArray).length, 1);
275
- trayControl.select({ id: 'disabled', disabled: true });
276
- assert.equal(trayEvents.length, 2);
277
- trayControl.select(trayControl.props.menu[0]);
278
- assert.equal(trayEvents[2][0], 'menu');
279
- assert.equal(trayEvents[2][1].detail.id, 'show');
280
- trayControl.onMounted();
281
- trayControl.element.dataset.cgPopOpen = '';
282
- trayControl.menuWatcher();
283
- trayControl.onBeforeUnmount();
284
- assert.equal(trayPops.slice(-2).join(','), 'hide,hide');
285
- console.log('Tray keyboard activation/context menu, disabled commands, menu events and popup cleanup passed.');
286
-
287
- // Date formatting uses the framework's complete language registry and the local calendar day.
288
- const toolExports = {};
289
- vm.runInNewContext(ts.transpileModule(await readFile(new URL('../dist/lib/tool.ts', import.meta.url), 'utf8'), {
290
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
291
- }).outputText, { exports: toolExports });
292
- let clockNow = new Date(2026, 9, 1, 23, 59).getTime();
293
- class ClockDate extends Date {
294
- constructor(value) { super(value === undefined ? clockNow : value); }
295
- static now() { return clockNow; }
296
- }
297
- let clockTick;
298
- clickgo.task = { createTimer: (_control, callback, _delay, options) => {
299
- clockTick = callback;
300
- if (options.immediate) callback();
301
- } };
302
- const clockExports = {};
303
- vm.runInNewContext(ts.transpileModule(await readFile(new URL('../dist/sources/control/task/code.ts', import.meta.url), 'utf8'), {
304
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
305
- }).outputText, { exports: clockExports, require: () => clickgo, Date: ClockDate, Intl });
306
- const clockControl = new clockExports.default();
307
- clockControl.onMounted();
308
- assert.equal(clockControl.date, '23:59');
309
- assert.equal(toolExports.lang.codes.length, 16);
310
- for (const code of toolExports.lang.codes) {
311
- clockControl.localeTag = toolExports.lang.getTag(code);
312
- const text = clockControl.calendarDate;
313
- const parts = new Intl.DateTimeFormat(clockControl.localeTag, {
314
- calendar: 'gregory', month: 'short', day: 'numeric',
315
- }).formatToParts(new Date(clockNow));
316
- for (const type of ['month', 'day']) {
317
- assert.ok(text.includes(parts.find(part => part.type === type).value), `${code} must show the localized ${type}`);
318
- }
319
- assert.equal(clockControl.timeFirst, code === 'vi', `${code} must use its localized date/time order`);
320
- }
321
- clockControl.localeTag = 'zh-CN';
322
- assert.equal(clockControl.calendarDate, '10月1日');
323
- clockControl.localeTag = 'en';
324
- assert.equal(clockControl.calendarDate, 'Oct 1');
325
- clockNow = new Date(2026, 9, 2, 0, 0).getTime();
326
- clockTick();
327
- assert.equal(clockControl.date, '00:00');
328
- assert.equal(clockControl.calendarDate, 'Oct 2');
329
- clockControl.localeTag = 'zh-TW';
330
- assert.equal(clockControl.calendarDate, '10月2日');
331
- console.log('Task clock shows localized month/day and date/time order for all 16 languages, switches language, and updates at local midnight.');
@@ -1,64 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { readFile } from 'node:fs/promises';
3
- import vm from 'node:vm';
4
- import { JSDOM } from 'jsdom';
5
- import ts from 'typescript';
6
-
7
- const dom = new JSDOM('<!doctype html><head></head><body></body>', { url: 'https://example.test/' });
8
- const source = await readFile(new URL('../dist/lib/tool.ts', import.meta.url), 'utf8');
9
- const tree = ts.createSourceFile('tool.ts', source, ts.ScriptTarget.ES2022, true);
10
- const names = new Set(['getHeadElement', 'loadScript', 'loadScripts', 'loadLink', 'loadLinks', 'loadAssets']);
11
- const nodes = tree.statements.filter(node =>
12
- (ts.isVariableStatement(node) && node.declarationList.declarations.some(item => item.name.getText(tree) === 'headElement')) ||
13
- (ts.isFunctionDeclaration(node) && node.body && names.has(node.name?.text)));
14
- const api = {};
15
- vm.runInNewContext(ts.transpileModule(nodes.map(node => node.getText(tree)).join('\n'), {
16
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
17
- }).outputText, { exports: api, document: dom.window.document });
18
- const head = dom.window.document.head;
19
- function finish(el, success = true) { el.dispatchEvent(new dom.window.Event(success ? 'load' : 'error')); }
20
- async function tick() { await new Promise(resolve => setImmediate(resolve)); }
21
-
22
- assert.equal(await api.loadAssets([]), true);
23
- assert.equal(head.children.length, 0);
24
- const urls = ['/base.js?file=theme.css', '/theme.CSS?version=1#dark', '/addon.js#theme.css', '/override.css#last'];
25
- const events = [];
26
- let complete = false;
27
- const loading = api.loadAssets(urls, { loaded: (url, state) => events.push([url, state]) }).then(value => { complete = true; return value; });
28
- const scripts = [...head.querySelectorAll('script')];
29
- const links = [...head.querySelectorAll('link')];
30
- assert.equal(scripts.length, 2);
31
- assert.equal(links.length, 2);
32
- assert.equal(head.children.length, 4, 'all JS and CSS requests start before awaiting a resource');
33
- assert.deepEqual(scripts.map(el => el.getAttribute('src')), [urls[0], urls[2]], 'query/hash values do not change the resource type');
34
- assert.deepEqual(links.map(el => el.getAttribute('href')), [urls[1], urls[3]], 'CSS insertion order and original URLs are preserved');
35
- assert.ok(scripts.every(el => el.async === false), 'unified loader uses browser ordered script execution');
36
- finish(scripts[1]); finish(links[1]); finish(links[0]);
37
- await tick();
38
- assert.equal(complete, false, 'the aggregate waits for every file, regardless of completion order');
39
- finish(scripts[0]);
40
- assert.equal(await loading, true);
41
- assert.deepEqual(events.map(([url, state]) => `${url}:${state}`).sort(), urls.map(url => `${url}:1`).sort(), 'each resource reports its own result once');
42
-
43
- head.replaceChildren();
44
- const cssFailed = api.loadAssets(['/ok.js', '/missing.css']);
45
- finish(head.querySelector('link'), false);
46
- finish(head.querySelector('script'));
47
- assert.equal(await cssFailed, false, 'CSS errors affect the aggregate result');
48
- head.replaceChildren();
49
- const jsFailed = api.loadAssets(['/missing.js', '/ok.css']);
50
- finish(head.querySelector('script'), false);
51
- finish(head.querySelector('link'));
52
- assert.equal(await jsFailed, false, 'JS errors affect the aggregate result');
53
- head.replaceChildren();
54
- const legacy = api.loadScript('/legacy.js');
55
- assert.equal(head.querySelector('script').async, true, 'existing single-script loading retains async behavior');
56
- finish(head.querySelector('script'));
57
- assert.equal(await legacy, true);
58
- head.replaceChildren();
59
- const batch = api.loadScripts(['/legacy-a.js', '/legacy-b.js']);
60
- assert.ok([...head.querySelectorAll('script')].every(el => el.async === true), 'existing script batches retain async behavior by default');
61
- for (const el of head.querySelectorAll('script')) finish(el);
62
- await batch;
63
- dom.window.close();
64
- console.log('Unified asset loading, URL classification, order, concurrency, callbacks, errors and legacy behavior passed.');
@@ -1,42 +0,0 @@
1
- // Run after TypeScript compilation: node test/tool-clone.mjs
2
- import assert from 'node:assert/strict';
3
- import { clone } from '../dist/lib/tool.js';
4
-
5
- const shared = { 'value': 1 };
6
- const source = {
7
- 'map': new Map(),
8
- 'set': new Set(),
9
- shared,
10
- 'alias': shared,
11
- 'date': new Date('2026-09-20T00:00:00.000Z'),
12
- };
13
- source.self = source;
14
- source.map.set(shared, source);
15
- source.set.add(shared);
16
- source.set.add(source);
17
-
18
- const first = clone(source);
19
- const second = clone(source);
20
-
21
- assert.notEqual(first, source);
22
- assert.notEqual(first, second);
23
- assert(first.map instanceof Map);
24
- assert(first.set instanceof Set);
25
- assert.notEqual(first.map, source.map);
26
- assert.notEqual(first.map, second.map);
27
- assert.notEqual(first.set, source.set);
28
- assert.notEqual(first.set, second.set);
29
- assert.equal(first.self, first);
30
- assert.equal(first.shared, first.alias);
31
- assert.notEqual(first.shared, shared);
32
- assert.equal(first.map.get(first.shared), first);
33
- assert(first.set.has(first.shared));
34
- assert(first.set.has(first));
35
- assert(first.date instanceof Date);
36
- assert.notEqual(first.date, source.date);
37
- assert.equal(first.date.getTime(), source.date.getTime());
38
-
39
- first.map.set('only-first', true);
40
- assert.equal(second.map.has('only-first'), false);
41
-
42
- console.log('Tool clone Map/Set, circular-reference and instance-isolation checks passed.');