clickgo 6.8.2 → 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.
- package/dist/app/demo.cga +0 -0
- package/dist/app/task.cga +0 -0
- package/dist/clickgo.d.ts +6 -0
- package/dist/control/common.cgc +0 -0
- package/dist/control/form.cgc +0 -0
- package/dist/control/novnc.cgc +0 -0
- package/dist/control/task.cgc +0 -0
- package/dist/global.css +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/core.d.ts +7 -1
- package/dist/lib/form.d.ts +15 -2
- package/dist/theme/admin.cgt +0 -0
- package/dist/theme/classic.cgt +0 -0
- package/dist/theme/cyber.cgt +0 -0
- package/dist/theme/dark.cgt +0 -0
- package/dist/theme/light.cgt +0 -0
- package/dist/theme/modern-light.cgt +0 -0
- package/dist/theme/modern.cgt +0 -0
- package/dist/theme/software-dark.cgt +0 -0
- package/dist/theme/software-light.cgt +0 -0
- package/doc/clickgo-rag.md +467 -282
- package/package.json +3 -2
- package/test/form-launcher.mjs +75 -0
- package/test/form-notify-animation.mjs +102 -0
- package/test/rtl-archive.mjs +13 -1
- package/test/task-start.mjs +141 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clickgo",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.0",
|
|
4
4
|
"description": "Background interface, software interface, mobile phone APP interface operation library.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
"test:jodit": "node test/jodit-lifecycle.mjs",
|
|
21
21
|
"test:scroll": "node test/scroll.mjs",
|
|
22
22
|
"test:vflow": "node test/vflow.mjs",
|
|
23
|
-
"test:task": "node test/task-tray.mjs && node test/task-package-loading.mjs",
|
|
23
|
+
"test:task": "node test/task-tray.mjs && node test/task-start.mjs && node test/task-package-loading.mjs",
|
|
24
24
|
"test:form": "node test/form-viewport.mjs && node --experimental-vm-modules test/form-native.mjs && node test/form-launcher.mjs && node test/form-notify.mjs",
|
|
25
|
+
"test:notify-animation": "node test/form-notify-animation.mjs",
|
|
25
26
|
"test:native-regions": "node test/form-native-regions.mjs",
|
|
26
27
|
"test:desktop": "node --experimental-vm-modules test/desktop.mjs && node --experimental-vm-modules test/iconview.mjs",
|
|
27
28
|
"test:style": "node --experimental-vm-modules test/dom-style.mjs",
|
package/test/form-launcher.mjs
CHANGED
|
@@ -100,3 +100,78 @@ assert.deepEqual(errors, [], 'event handlers do not throw');
|
|
|
100
100
|
app.unmount();
|
|
101
101
|
dom.window.close();
|
|
102
102
|
console.log('Launcher blank areas, pointer tolerance, drag/cancel, input, folders and single app launch passed.');
|
|
103
|
+
|
|
104
|
+
// --- 验证实际显示接口及动画时序,重复调用只通知真实状态变更。 ---
|
|
105
|
+
const stateNames = new Set(['launcherShown', 'launcherHideTimer', 'getLauncherShow', 'showLauncher', 'hideLauncher']);
|
|
106
|
+
const stateSource = tree.statements.filter(node => {
|
|
107
|
+
if (ts.isFunctionDeclaration(node)) return stateNames.has(node.name?.text);
|
|
108
|
+
return ts.isVariableStatement(node) && node.declarationList.declarations.some(d => stateNames.has(d.name.getText(tree)));
|
|
109
|
+
}).map(node => node.getText(tree)).join('\n');
|
|
110
|
+
const notifications = [];
|
|
111
|
+
const frames = [];
|
|
112
|
+
const timers = new Map();
|
|
113
|
+
let timerId = 0;
|
|
114
|
+
const surface = { style: {}, classList: new Set() };
|
|
115
|
+
surface.classList.remove = surface.classList.delete;
|
|
116
|
+
const launcher = { folderName: 'Folder', name: 'search', closeFolder() { this.folderName = ''; } };
|
|
117
|
+
const stateScope = {
|
|
118
|
+
exports: {}, elements: { launcher: surface }, launcherRoot: launcher,
|
|
119
|
+
lCore: { trigger: async (...args) => notifications.push(args) },
|
|
120
|
+
requestAnimationFrame: callback => frames.push(callback),
|
|
121
|
+
setTimeout: callback => { timers.set(++timerId, callback); return timerId; },
|
|
122
|
+
clearTimeout: id => timers.delete(id),
|
|
123
|
+
};
|
|
124
|
+
vm.runInNewContext(ts.transpileModule(stateSource, {
|
|
125
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
126
|
+
}).outputText, stateScope);
|
|
127
|
+
const display = stateScope.exports;
|
|
128
|
+
assert.equal(display.getLauncherShow(), false);
|
|
129
|
+
display.hideLauncher();
|
|
130
|
+
display.showLauncher();
|
|
131
|
+
display.showLauncher();
|
|
132
|
+
assert.equal(display.getLauncherShow(), true);
|
|
133
|
+
assert.equal(surface.style.display, 'flex');
|
|
134
|
+
display.hideLauncher();
|
|
135
|
+
frames.splice(0).forEach(callback => callback());
|
|
136
|
+
assert.equal(surface.classList.has('cg-show'), false, 'hide before first paint cannot add a stale visible class');
|
|
137
|
+
display.hideLauncher();
|
|
138
|
+
assert.equal(timers.size, 1, 'duplicate hide does not schedule extra cleanup');
|
|
139
|
+
display.showLauncher();
|
|
140
|
+
assert.equal(timers.size, 0, 'reopening cancels the old closing animation');
|
|
141
|
+
frames.splice(0).forEach(callback => callback());
|
|
142
|
+
assert.equal(surface.classList.has('cg-show'), true);
|
|
143
|
+
assert.equal(launcher.name, 'search', 'cancelled cleanup does not reset the reopened search');
|
|
144
|
+
display.hideLauncher();
|
|
145
|
+
timers.forEach(callback => callback());
|
|
146
|
+
timers.clear();
|
|
147
|
+
assert.equal(surface.style.display, 'none');
|
|
148
|
+
assert.equal(launcher.folderName, '');
|
|
149
|
+
assert.equal(launcher.name, '');
|
|
150
|
+
assert.deepEqual(notifications, [
|
|
151
|
+
['launcherShowChanged', true], ['launcherShowChanged', false],
|
|
152
|
+
['launcherShowChanged', true], ['launcherShowChanged', false],
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// --- 非 root 应用、自定义任务栏与宿主均接收通知,异步应用不阻塞窗体。 ---
|
|
156
|
+
const coreSource = await readFile(new URL('../dist/lib/core.ts', import.meta.url), 'utf8');
|
|
157
|
+
const coreTree = ts.createSourceFile('core.ts', coreSource, ts.ScriptTarget.ES2022, true);
|
|
158
|
+
const triggerSource = coreTree.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === 'trigger').getText(coreTree);
|
|
159
|
+
const delivered = [];
|
|
160
|
+
const routeScope = {
|
|
161
|
+
exports: {}, sysId: 'system',
|
|
162
|
+
boot: { onLauncherShowChanged: state => delivered.push(['boot', state]) },
|
|
163
|
+
lTask: { getOriginList: async () => ({
|
|
164
|
+
ordinary: { class: { onLauncherShowChanged: state => { delivered.push(['app', state]); return new Promise(() => {}); } },
|
|
165
|
+
forms: { bar: { vroot: { onLauncherShowChanged: state => delivered.push(['form', state]) } } } },
|
|
166
|
+
}) },
|
|
167
|
+
};
|
|
168
|
+
vm.runInNewContext(ts.transpileModule(triggerSource, {
|
|
169
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
170
|
+
}).outputText, routeScope);
|
|
171
|
+
await routeScope.exports.trigger('launcherShowChanged', true);
|
|
172
|
+
await routeScope.exports.trigger('launcherShowChanged', false);
|
|
173
|
+
assert.deepEqual(delivered, [
|
|
174
|
+
['boot', true], ['app', true], ['form', true],
|
|
175
|
+
['boot', false], ['app', false], ['form', false],
|
|
176
|
+
]);
|
|
177
|
+
console.log('Launcher state query, broadcast, duplicate calls and close/reopen animation ordering passed.');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Requires a display; run with npm run test:notify-animation.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import electron from 'electron';
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
|
|
9
|
+
if (!process.versions.electron) {
|
|
10
|
+
const child = spawn(electron, [fileURLToPath(import.meta.url), '--ozone-platform=x11'], {
|
|
11
|
+
'stdio': ['ignore', 'pipe', 'pipe'],
|
|
12
|
+
});
|
|
13
|
+
let output = '';
|
|
14
|
+
child.stdout.on('data', chunk => { output += chunk; });
|
|
15
|
+
child.stderr.on('data', chunk => { output += chunk; });
|
|
16
|
+
const timeout = setTimeout(() => child.kill(), 20000);
|
|
17
|
+
const code = await new Promise(resolve => {
|
|
18
|
+
child.on('error', error => { output += error.message; resolve(1); });
|
|
19
|
+
child.on('exit', resolve);
|
|
20
|
+
});
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
assert.equal(code, 0, output);
|
|
23
|
+
console.log('Notify animation passed: consecutive LTR/RTL entries keep their stacked height and slide horizontally.');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const { app, BrowserWindow } = electron;
|
|
27
|
+
app.whenReady().then(async () => {
|
|
28
|
+
const win = new BrowserWindow({
|
|
29
|
+
'show': false, 'width': 800, 'height': 600,
|
|
30
|
+
'webPreferences': { 'backgroundThrottling': false, 'offscreen': true },
|
|
31
|
+
});
|
|
32
|
+
let exitCode = 0;
|
|
33
|
+
try {
|
|
34
|
+
const source = await readFile(new URL('../dist/lib/form.ts', import.meta.url), 'utf8');
|
|
35
|
+
const start = source.indexOf('let notifyId:');
|
|
36
|
+
const end = source.indexOf('export function appendToPop', start);
|
|
37
|
+
const script = ts.transpileModule(source.slice(start, end), {
|
|
38
|
+
'compilerOptions': { 'module': ts.ModuleKind.CommonJS, 'target': ts.ScriptTarget.ES2022 },
|
|
39
|
+
}).outputText;
|
|
40
|
+
const style = await readFile(new URL('../dist/global.css', import.meta.url), 'utf8');
|
|
41
|
+
await win.loadURL(`data:text/html,${encodeURIComponent(`<style>${style}</style><div id="cg-wrap"><div id="cg-notify"></div></div>`)}`);
|
|
42
|
+
await win.webContents.executeJavaScript(`
|
|
43
|
+
window.notifyCore = {
|
|
44
|
+
config: { locale: 'en' },
|
|
45
|
+
getAvailArea: () => ({ left: 0, top: 0, width: innerWidth, height: innerHeight - 48 }),
|
|
46
|
+
};
|
|
47
|
+
window.notifyApi = {};
|
|
48
|
+
new Function('exports', 'elements', 'lCore', 'lTool', ${JSON.stringify(script)})(
|
|
49
|
+
window.notifyApi, { notify: document.getElementById('cg-notify') }, window.notifyCore,
|
|
50
|
+
{ lang: { getDirection: locale => locale === 'ar' ? 'rtl' : 'ltr' }, escapeHTML: value => value },
|
|
51
|
+
);
|
|
52
|
+
`);
|
|
53
|
+
const traces = await win.webContents.executeJavaScript(`(${recordEntries.toString()})()`);
|
|
54
|
+
for (const trace of traces) {
|
|
55
|
+
const label = `${trace.direction} notification ${trace.index + 1}`;
|
|
56
|
+
assert.ok(trace.samples.every(point => Math.abs(point.y - trace.targetY) < 0.5),
|
|
57
|
+
`${label} must enter at its stacked height: ${JSON.stringify(trace)}`);
|
|
58
|
+
const movement = trace.samples.at(-1).x - trace.samples[0].x;
|
|
59
|
+
assert.ok(trace.direction === 'ltr' ? movement < -20 : movement > 20,
|
|
60
|
+
`${label} must slide in from the outer edge`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(error);
|
|
65
|
+
exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
win.destroy();
|
|
69
|
+
app.exit(exitCode);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* --- 在实际渲染帧中记录连续通知的轨迹,避免最终坐标测试漏掉错误的入场方向 ---
|
|
76
|
+
* @returns LTR/RTL 通知入场的逐帧坐标与目标高度
|
|
77
|
+
*/
|
|
78
|
+
async function recordEntries() {
|
|
79
|
+
const traces = [];
|
|
80
|
+
for (const direction of ['ltr', 'rtl']) {
|
|
81
|
+
document.getElementById('cg-wrap').dir = direction;
|
|
82
|
+
window.notifyCore.config.locale = direction === 'rtl' ? 'ar' : 'en';
|
|
83
|
+
for (let index = 0; index < 3; ++index) {
|
|
84
|
+
const id = window.notifyApi.notify({ 'title': 'Notify', 'content': `Notification ${index}`, 'timeout': 0 });
|
|
85
|
+
const el = document.querySelector(`[data-notifyid="${id}"]`);
|
|
86
|
+
const samples = [];
|
|
87
|
+
for (let frame = 0; frame < 7; ++frame) {
|
|
88
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
89
|
+
const rect = el.getBoundingClientRect();
|
|
90
|
+
samples.push({ 'x': rect.x, 'y': rect.y });
|
|
91
|
+
}
|
|
92
|
+
const target = new DOMMatrix(el.style.transform);
|
|
93
|
+
traces.push({ direction, index, samples, 'targetY': innerHeight - el.offsetHeight + target.m42 });
|
|
94
|
+
await Promise.all(el.getAnimations().map(animation => animation.finished));
|
|
95
|
+
}
|
|
96
|
+
for (const el of document.querySelectorAll('[data-notifyid]')) {
|
|
97
|
+
window.notifyApi.hideNotify(Number(el.dataset.notifyid));
|
|
98
|
+
}
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
100
|
+
}
|
|
101
|
+
return traces;
|
|
102
|
+
}
|
package/test/rtl-archive.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const commonControls = new Set([
|
|
|
22
22
|
'select', 'sidebar-toggle', 'text', 'toolbox',
|
|
23
23
|
]);
|
|
24
24
|
/** --- 全部内置主题 --- */
|
|
25
|
-
const themes = ['admin', 'cyber', 'dark', 'light', 'modern', 'modern-light', 'software-dark', 'software-light'];
|
|
25
|
+
const themes = ['admin', 'classic', 'cyber', 'dark', 'light', 'modern', 'modern-light', 'software-dark', 'software-light'];
|
|
26
26
|
/** --- CGA 格式常量,与编译器的公开包格式保持一致 --- */
|
|
27
27
|
const cgaHeaderLength = 106;
|
|
28
28
|
const cgaKeyContext = Buffer.from('ClickGo/Application/Package');
|
|
@@ -423,6 +423,18 @@ for (const theme of themes) {
|
|
|
423
423
|
/(?:border|inset|margin|padding)-inline/,
|
|
424
424
|
archiveError(archive, 'style.css has no logical inline-direction properties'),
|
|
425
425
|
);
|
|
426
|
+
// --- 主题自带图标必须随归档发布,不能只在源码目录中存在。 ---
|
|
427
|
+
const zip = await loadZipArchive(archive);
|
|
428
|
+
for (const match of archivedCss.matchAll(/url\(['"]?([^'"\)]+)['"]?\)/g)) {
|
|
429
|
+
const resource = match[1].replace(/^\.\//, '');
|
|
430
|
+
if (/^(?:data:|https?:|\/\/|#)/.test(resource)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const entry = zip.file(resource);
|
|
434
|
+
assert.ok(entry, archiveError(archive, `referenced resource ${resource} is absent`));
|
|
435
|
+
const source = await readProjectFile(`dist/sources/theme/${theme}/${resource}`);
|
|
436
|
+
assert.deepEqual(await entry.async('nodebuffer'), source, archiveError(archive, `${resource} differs from its source`));
|
|
437
|
+
}
|
|
426
438
|
}
|
|
427
439
|
|
|
428
440
|
console.log(`Archive check passed: task.cga has 16 locales, ${localeControls.length} control locale tables are current, and RTL output is present in all ${themes.length} themes.`);
|
|
@@ -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.');
|