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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clickgo",
3
- "version": "6.8.1",
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": [
@@ -14,13 +14,15 @@
14
14
  "types": "./dist/clickgo.d.ts",
15
15
  "scripts": {
16
16
  "sass": "sass --watch dist/:dist/ --style compressed --no-source-map",
17
- "check": "tsc --noEmit --pretty false && eslint \"dist/**/*.ts\" --format stylish && npm run test:locale && npm run test:style && npm run test:desktop && npm run test:form && npm run test:task && npm run test:module && npm run test:vflow && npm run test:scroll && npm run test:jodit",
17
+ "check": "tsc --noEmit --pretty false && eslint \"dist/**/*.ts\" --format stylish && npm run test:locale && npm run test:style && npm run test:desktop && npm run test:form && npm run test:task && npm run test:module && npm run test:vflow && npm run test:scroll && npm run test:jodit && npm run test:select",
18
+ "test:select": "node test/select.mjs",
18
19
  "test:module": "node test/tool-assets.mjs && node test/core-module-loading.mjs && node test/tuieditor-locale.mjs",
19
20
  "test:jodit": "node test/jodit-lifecycle.mjs",
20
21
  "test:scroll": "node test/scroll.mjs",
21
22
  "test:vflow": "node test/vflow.mjs",
22
- "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",
23
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",
24
26
  "test:native-regions": "node test/form-native-regions.mjs",
25
27
  "test:desktop": "node --experimental-vm-modules test/desktop.mjs && node --experimental-vm-modules test/iconview.mjs",
26
28
  "test:style": "node --experimental-vm-modules test/dom-style.mjs",
@@ -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
+ }
@@ -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.`);