clickgo 6.8.1 → 6.9.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,141 @@
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
+ import JSZip from 'jszip';
6
+ import { JSDOM } from 'jsdom';
7
+
8
+ const source = await readFile(new URL('../dist/sources/control/task-start/code.ts', import.meta.url), 'utf8');
9
+ const scope = {
10
+ exports: {},
11
+ require: () => ({ control: { AbstractControl: class {
12
+ propBoolean(name) { return this.props[name] === true || this.props[name] === 'true' || this.props[name] === ''; }
13
+ parentByName() { return this.taskParent; }
14
+ watch(name, callback) { this.watched = callback; }
15
+ emit(name) { this.events.push(name); }
16
+ } } }),
17
+ };
18
+ vm.runInNewContext(ts.transpileModule(source, {
19
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
20
+ }).outputText, scope);
21
+ const start = new scope.exports.default();
22
+ start.events = [];
23
+ let nativeClicks = 0;
24
+ // --- 原生 click 不会触发框架转换后的 tap 监听,键盘必须直接发送控件事件。 ---
25
+ start.element = { click: () => ++nativeClicks };
26
+ const key = (value, extra = {}) => ({ key: value, preventDefault() {}, stopPropagation() {}, ...extra });
27
+ assert.equal(start.labelMode, 'auto');
28
+ start.props.showLabel = 'false';
29
+ assert.equal(start.labelMode, 'hide');
30
+ start.props.showLabel = true;
31
+ assert.equal(start.labelMode, 'show');
32
+ start.taskParent = { position: 'right', mode: 'dock' };
33
+ assert.equal(start.position, 'right');
34
+ assert.equal(start.mode, 'dock');
35
+ start.keydown(key(' '));
36
+ assert.equal(start.isSpaceDown, true);
37
+ assert.equal(start.events.length, 0, 'Space activates on release');
38
+ start.keydown(key(' ', { repeat: true }));
39
+ start.keyup(key(' '));
40
+ start.keyup(key(' '));
41
+ assert.deepEqual(start.events, ['tap']);
42
+ start.props.opened = true;
43
+ start.keydown(key('Enter'));
44
+ start.keydown(key('Enter', { repeat: true }));
45
+ assert.equal(start.events.length, 2, 'held Enter does not repeatedly activate');
46
+ assert.equal(start.props.opened, true, 'activation does not overwrite the caller-owned menu state');
47
+ start.keydown(key('Enter', { isComposing: true }));
48
+ start.onMounted();
49
+ start.keydown(key(' '));
50
+ start.props.disabled = true;
51
+ start.watched();
52
+ assert.equal(start.isSpaceDown, false);
53
+ start.keyup(key(' '));
54
+ start.keydown(key('Enter'));
55
+ start.click({ stopPropagation() {} });
56
+ assert.equal(start.events.length, 2, 'disabled controls never activate');
57
+ assert.equal(nativeClicks, 0, 'keyboard activation does not depend on a native click reaching a tap listener');
58
+
59
+ // --- 使用真实 Vue、Pointer.js 与框架事件转换,验证 @click 不丢失键盘或重复指针激活。 ---
60
+ const dom = new JSDOM('<!doctype html><html><body><div id="app"></div></body></html>');
61
+ for (const name of ['window', 'document', 'Element', 'HTMLElement', 'SVGElement', 'Node', 'Event', 'CustomEvent']) {
62
+ Object.defineProperty(globalThis, name, { configurable: true, value: dom.window[name] });
63
+ }
64
+ const vue = await import('vue');
65
+ const pointer = await import('../node_modules/@litert/pointer/dist/index.esm.js');
66
+ const toolSource = await readFile(new URL('../dist/lib/tool.ts', import.meta.url), 'utf8');
67
+ const toolTree = ts.createSourceFile('tool.ts', toolSource, ts.ScriptTarget.ES2022, true);
68
+ const wrapSource = toolTree.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === 'eventsAttrWrap').getText(toolTree);
69
+ const toolScope = { exports: {} };
70
+ vm.runInNewContext(ts.transpileModule(wrapSource, {
71
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
72
+ }).outputText, toolScope);
73
+ const prepare = layout => toolScope.exports.eventsAttrWrap(layout).replaceAll('@click="', '@tap="');
74
+ const methods = {};
75
+ const computed = {};
76
+ for (const name of Object.getOwnPropertyNames(Object.getPrototypeOf(start))) {
77
+ if (name === 'constructor') continue;
78
+ const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(start), name);
79
+ if (descriptor.value) methods[name] = descriptor.value;
80
+ else if (descriptor.get) computed[name] = descriptor.get;
81
+ }
82
+ methods.propBoolean = function(name) { return pointerBoolean(this.props[name]); };
83
+ function pointerBoolean(value) { return value === true || value === 'true' || value === ''; }
84
+ methods.parentByName = () => null;
85
+ methods.emit = function(name, event) { this.$emit(name, event); };
86
+ methods.allowEvent = pointer.allowEvent;
87
+ const layout = await readFile(new URL('../dist/sources/control/task-start/layout.html', import.meta.url), 'utf8');
88
+ const component = {
89
+ template: prepare(layout), props: Object.fromEntries(Object.entries(start.props).map(([name, value]) => [name, { default: value }])),
90
+ components: { 'cg-img': { template: '<img>' } },
91
+ emits: start.emits, methods, computed,
92
+ data: () => ({ isSpaceDown: false, props: {}, element: null }),
93
+ created() { this.props = this.$props; }, mounted() { this.element = this.$el; },
94
+ };
95
+ const app = vue.createApp({
96
+ template: prepare('<task-start label="Start" :opened="opened" :disabled="disabled" @click="count++; opened = !opened"></task-start>'),
97
+ components: { 'task-start': component }, methods: { allowEvent: pointer.allowEvent },
98
+ data: () => ({ count: 0, opened: false, disabled: false }),
99
+ });
100
+ const root = app.mount('#app');
101
+ const button = document.querySelector('[role="button"]');
102
+ const keyboard = (type, key) => button.dispatchEvent(new window.KeyboardEvent(type, { key, bubbles: true, cancelable: true }));
103
+ keyboard('keydown', 'Enter');
104
+ await vue.nextTick();
105
+ assert.equal(root.count, 1);
106
+ assert.equal(button.getAttribute('aria-expanded'), 'true');
107
+ keyboard('keydown', ' ');
108
+ await vue.nextTick();
109
+ assert.equal(root.count, 1);
110
+ assert.ok(button.classList.contains('active'));
111
+ keyboard('keyup', ' ');
112
+ await vue.nextTick();
113
+ assert.equal(root.count, 2);
114
+ assert.equal(button.getAttribute('aria-expanded'), 'false');
115
+ function pointerEvent(type) {
116
+ const event = new window.MouseEvent(type, { bubbles: true, cancelable: true, clientX: 10, clientY: 10, button: 0, view: window });
117
+ Object.defineProperties(event, { pointerId: { value: 1 }, pointerType: { value: 'mouse' } });
118
+ button.dispatchEvent(event);
119
+ }
120
+ pointerEvent('pointerdown');
121
+ pointerEvent('pointerup');
122
+ await vue.nextTick();
123
+ assert.equal(root.count, 3, 'pointer tap invokes the caller exactly once');
124
+ root.disabled = true;
125
+ await vue.nextTick();
126
+ keyboard('keydown', 'Enter');
127
+ pointerEvent('pointerdown');
128
+ pointerEvent('pointerup');
129
+ assert.equal(root.count, 3);
130
+ assert.equal(button.getAttribute('tabindex'), null);
131
+ app.unmount();
132
+ dom.window.close();
133
+
134
+ // --- 检查真实 task 包包含新控件,避免源码存在但用户无法加载。 ---
135
+ const archive = await JSZip.loadAsync(await readFile(new URL('../dist/control/task.cgc', import.meta.url)));
136
+ for (const file of ['config.json', 'code.js', 'layout.html', 'style.css']) {
137
+ assert.ok(archive.file(`task-start/${file}`), `task.cgc contains task-start/${file}`);
138
+ }
139
+ assert.equal(await archive.file('task-start/layout.html').async('string'),
140
+ (await readFile(new URL('../dist/sources/control/task-start/layout.html', import.meta.url), 'utf8')).replace(/>\s+</g, '><').trim());
141
+ console.log('Task start caller-owned state, label override, bar/dock, keyboard, disabled and package loading passed.');