clickgo 6.9.0 → 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 (44) 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 +2 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lib/command/native.d.ts +18 -0
  8. package/dist/lib/command/palette.d.ts +10 -0
  9. package/dist/lib/command/schema.d.ts +31 -0
  10. package/dist/lib/command.d.ts +218 -0
  11. package/dist/lib/native.d.ts +12 -0
  12. package/dist/lib/task.d.ts +2 -0
  13. package/doc/clickgo-rag.md +2376 -537
  14. package/package.json +9 -3
  15. package/test/core-module-loading.mjs +0 -169
  16. package/test/desktop.mjs +0 -715
  17. package/test/dock-lifecycle.mjs +0 -280
  18. package/test/dom-style.mjs +0 -261
  19. package/test/form-launcher.mjs +0 -177
  20. package/test/form-modal-layer.mjs +0 -75
  21. package/test/form-native-regions.mjs +0 -115
  22. package/test/form-native-style.mjs +0 -80
  23. package/test/form-native.mjs +0 -68
  24. package/test/form-notify-animation.mjs +0 -102
  25. package/test/form-notify.mjs +0 -90
  26. package/test/form-viewport.mjs +0 -127
  27. package/test/iconview.mjs +0 -70
  28. package/test/jodit-lifecycle.mjs +0 -212
  29. package/test/locale-integrity.mjs +0 -189
  30. package/test/pop-max-size.mjs +0 -66
  31. package/test/rtl-archive.mjs +0 -440
  32. package/test/rtl-direction.mjs +0 -193
  33. package/test/scroll.mjs +0 -145
  34. package/test/select.mjs +0 -885
  35. package/test/sidebar-toggle.mjs +0 -75
  36. package/test/task-package-loading.mjs +0 -94
  37. package/test/task-start.mjs +0 -141
  38. package/test/task-tray.mjs +0 -331
  39. package/test/tool-assets.mjs +0 -64
  40. package/test/tool-clone.mjs +0 -42
  41. package/test/toolbox.mjs +0 -88
  42. package/test/tuieditor-locale.mjs +0 -175
  43. package/test/vflow.mjs +0 -122
  44. package/test/web-drag-mask.mjs +0 -96
@@ -1,440 +0,0 @@
1
- // Run after TypeScript/Sass compilation and `node dist/pack.js`: node test/rtl-archive.mjs
2
- import assert from 'node:assert/strict';
3
- import { createDecipheriv, createHash } from 'node:crypto';
4
- import { readFile } from 'node:fs/promises';
5
- import CleanCSS from 'clean-css';
6
- import JSZip from 'jszip';
7
- import ts from 'typescript';
8
-
9
- /** --- ClickGo 的固定内置语言顺序 --- */
10
- const codes = [
11
- 'sc', 'tc', 'ja', 'ko', 'th', 'vi', 'ar', 'id',
12
- 'en', 'es', 'de', 'fr', 'pt', 'ru', 'it', 'tr',
13
- ];
14
- /** --- 包含 localeData 的全部内置控件 --- */
15
- const localeControls = [
16
- 'arteditor', 'calendar', 'captcha', 'date', 'datepanel', 'daterange', 'empty', 'jodit', 'label',
17
- 'levelselect', 'map', 'monaco', 'number', 'page', 'palette', 'property', 'select', 'sidebar-toggle', 'text', 'toolbox', 'tuieditor',
18
- ];
19
- /** --- 合并到 common.cgc 的多语言控件 --- */
20
- const commonControls = new Set([
21
- 'calendar', 'date', 'datepanel', 'daterange', 'empty', 'label', 'levelselect', 'number', 'palette',
22
- 'select', 'sidebar-toggle', 'text', 'toolbox',
23
- ]);
24
- /** --- 全部内置主题 --- */
25
- const themes = ['admin', 'classic', 'cyber', 'dark', 'light', 'modern', 'modern-light', 'software-dark', 'software-light'];
26
- /** --- CGA 格式常量,与编译器的公开包格式保持一致 --- */
27
- const cgaHeaderLength = 106;
28
- const cgaKeyContext = Buffer.from('ClickGo/Application/Package');
29
- /** --- 已解压的 ZIP 归档缓存 --- */
30
- const zipCache = new Map();
31
-
32
- /**
33
- * --- 生成归档不存在或过期时的统一错误信息 ---
34
- * @param archive 归档路径
35
- * @param detail 失败原因
36
- * @returns 可操作的错误信息
37
- */
38
- function archiveError(archive, detail) {
39
- return `${archive} is missing or stale: ${detail}. Build TypeScript and Sass, run node dist/pack.js, then rerun this test.`;
40
- }
41
-
42
- /**
43
- * --- 读取项目文件 ---
44
- * @param path 相对项目根目录的路径
45
- * @returns 文件内容
46
- */
47
- async function readProjectFile(path) {
48
- return await readFile(new URL(`../${path}`, import.meta.url));
49
- }
50
-
51
- /**
52
- * --- 读取必要归档,并把不存在转换为清晰诊断 ---
53
- * @param path 归档路径
54
- * @returns 归档内容
55
- */
56
- async function readArchive(path) {
57
- try {
58
- return await readProjectFile(path);
59
- }
60
- catch (error) {
61
- if (error?.code === 'ENOENT') {
62
- throw new Error(archiveError(path, 'the archive file does not exist'));
63
- }
64
- throw error;
65
- }
66
- }
67
-
68
- /**
69
- * --- 加载 cgc/cgt ZIP 归档 ---
70
- * @param path 归档路径
71
- * @returns JSZip 实例
72
- */
73
- async function loadZipArchive(path) {
74
- if (zipCache.has(path)) {
75
- return zipCache.get(path);
76
- }
77
- let zip;
78
- try {
79
- zip = await JSZip.loadAsync(await readArchive(path));
80
- }
81
- catch (error) {
82
- throw new Error(archiveError(path, `the archive cannot be decoded (${error.message})`));
83
- }
84
- zipCache.set(path, zip);
85
- return zip;
86
- }
87
-
88
- /**
89
- * --- 读取 cgc/cgt 内文件 ---
90
- * @param archive 归档路径
91
- * @param entry 包内路径
92
- * @returns 文件内容
93
- */
94
- async function readZipText(archive, entry) {
95
- const zip = await loadZipArchive(archive);
96
- const file = zip.file(entry);
97
- assert.ok(file, archiveError(archive, `required entry ${entry} is absent`));
98
- return await file.async('string');
99
- }
100
-
101
- /**
102
- * --- 恢复 CGA 文件头中打散的密钥种子 ---
103
- * @param maskedSeed 打散后的种子
104
- * @param salt 随机盐
105
- * @param packageId 包 ID
106
- * @returns 原始密钥种子
107
- */
108
- function unmaskCgaSeed(maskedSeed, salt, packageId) {
109
- const seed = Buffer.alloc(maskedSeed.length);
110
- const shift = packageId[0] % seed.length;
111
- for (let i = 0; i < seed.length; ++i) {
112
- const sourceIndex = (i + shift) % seed.length;
113
- seed[sourceIndex] = maskedSeed[i] ^ salt[i]
114
- ^ packageId[i % packageId.length] ^ ((i * 29 + 17) & 0xff);
115
- }
116
- return seed;
117
- }
118
-
119
- /**
120
- * --- 解密带认证标签的 CGA AES-GCM 数据块 ---
121
- * @param data 密文和末尾认证标签
122
- * @param key 解密密钥
123
- * @param nonce 随机数
124
- * @param aad 附加认证数据
125
- * @returns 明文
126
- */
127
- function decryptCga(data, key, nonce, aad) {
128
- assert.ok(data.length >= 16, 'encrypted CGA data must include an authentication tag');
129
- const decipher = createDecipheriv('aes-256-gcm', key, nonce);
130
- decipher.setAAD(aad);
131
- decipher.setAuthTag(data.subarray(-16));
132
- return Buffer.concat([decipher.update(data.subarray(0, -16)), decipher.final()]);
133
- }
134
-
135
- /**
136
- * --- 加载 CGA 清单并提供按路径读取能力 ---
137
- * @param path CGA 归档路径
138
- * @returns CGA 读取器
139
- */
140
- async function loadCgaArchive(path) {
141
- const data = await readArchive(path);
142
- try {
143
- assert.ok(data.length >= cgaHeaderLength, 'file is shorter than the CGA header');
144
- assert.equal(data.subarray(0, 5).toString(), '-CGA-', 'CGA magic is invalid');
145
- assert.equal(data.readUInt8(5), 1, 'CGA version is unsupported');
146
-
147
- const iconLength = data.readUInt32BE(6);
148
- const manifestLength = data.readUInt32BE(10);
149
- const packageId = data.subarray(14, 30);
150
- const salt = data.subarray(30, 62);
151
- const maskedSeed = data.subarray(62, 94);
152
- const manifestNonce = data.subarray(94, cgaHeaderLength);
153
- const manifestOffset = cgaHeaderLength + iconLength;
154
- const blockOffset = manifestOffset + manifestLength;
155
- assert.ok(blockOffset <= data.length, 'CGA manifest exceeds the archive size');
156
-
157
- const seed = unmaskCgaSeed(maskedSeed, salt, packageId);
158
- const key = createHash('sha256').update(Buffer.concat([
159
- seed, salt, packageId, cgaKeyContext,
160
- ])).digest();
161
- const manifestData = decryptCga(
162
- data.subarray(manifestOffset, blockOffset),
163
- key,
164
- manifestNonce,
165
- Buffer.concat([packageId, Buffer.from('manifest')]),
166
- );
167
- const manifest = JSON.parse(manifestData.toString());
168
- const blocks = new Map();
169
-
170
- return {
171
- /**
172
- * --- 从加密应用包读取单个文件 ---
173
- * @param entryPath 包内绝对路径
174
- * @returns 解密后的文件内容
175
- */
176
- async read(entryPath) {
177
- const entry = manifest.f?.[entryPath];
178
- assert.ok(entry, archiveError(path, `required entry ${entryPath} is absent`));
179
- let blockZip = blocks.get(entry.b);
180
- if (!blockZip) {
181
- const block = manifest.b?.[entry.b];
182
- assert.ok(block, `manifest block ${entry.b} is absent`);
183
- const start = blockOffset + block.o;
184
- const end = start + block.l;
185
- assert.ok((start >= blockOffset) && (end <= data.length), `manifest block ${entry.b} exceeds the archive size`);
186
- const blockData = decryptCga(
187
- data.subarray(start, end),
188
- key,
189
- Buffer.from(block.n, 'base64'),
190
- Buffer.concat([packageId, Buffer.from(entry.b)]),
191
- );
192
- blockZip = await JSZip.loadAsync(blockData);
193
- blocks.set(entry.b, blockZip);
194
- }
195
- const file = blockZip.file(entry.e);
196
- assert.ok(file, `manifest entry ${entryPath} points to a missing file`);
197
- return await file.async('nodebuffer');
198
- },
199
- };
200
- }
201
- catch (error) {
202
- if (error.message.includes('is missing or stale')) {
203
- throw error;
204
- }
205
- throw new Error(archiveError(path, `the encrypted package cannot be decoded (${error.message})`));
206
- }
207
- }
208
-
209
- /**
210
- * --- 取得 AST 静态属性名 ---
211
- * @param name 属性名节点
212
- * @returns 静态名称;动态属性返回空字符串
213
- */
214
- function getPropertyName(name) {
215
- if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
216
- return name.text;
217
- }
218
- return '';
219
- }
220
-
221
- /**
222
- * --- 将只含静态字符串的对象字面量转换为普通对象 ---
223
- * @param node 对象字面量节点
224
- * @param label 诊断标签
225
- * @returns 普通对象
226
- */
227
- function readStaticObject(node, label) {
228
- const result = {};
229
- for (const property of node.properties) {
230
- assert.equal(ts.isPropertyAssignment(property), true, `${label} must contain only static properties`);
231
- const name = getPropertyName(property.name);
232
- assert.notEqual(name, '', `${label} contains a dynamic property name`);
233
- assert.equal(Object.hasOwn(result, name), false, `${label} repeats property ${name}`);
234
- const value = property.initializer;
235
- if (ts.isObjectLiteralExpression(value)) {
236
- result[name] = readStaticObject(value, `${label}.${name}`);
237
- continue;
238
- }
239
- const isText = ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value);
240
- assert.equal(isText, true, `${label}.${name} must be a static string`);
241
- result[name] = value.text;
242
- }
243
- return result;
244
- }
245
-
246
- /**
247
- * --- 从控件源码或打包 JS 中读取 localeData ---
248
- * @param code TypeScript 或 JavaScript 源码
249
- * @param label 诊断标签
250
- * @param scriptKind 脚本类型
251
- * @returns localeData 普通对象
252
- */
253
- function readLocaleData(code, label, scriptKind) {
254
- const source = ts.createSourceFile(label, code, ts.ScriptTarget.Latest, true, scriptKind);
255
- const candidates = [];
256
- const visit = (node) => {
257
- if (
258
- ts.isPropertyDeclaration(node)
259
- && (getPropertyName(node.name) === 'localeData')
260
- && ts.isObjectLiteralExpression(node.initializer)
261
- ) {
262
- candidates.push(node.initializer);
263
- }
264
- ts.forEachChild(node, visit);
265
- };
266
- visit(source);
267
- assert.equal(candidates.length, 1, `${label} must contain exactly one static localeData table`);
268
- return readStaticObject(candidates[0], `${label} localeData`);
269
- }
270
-
271
- /**
272
- * --- 断言归档代码与本地 pack 产物完全一致 ---
273
- * @param archive cgc 路径
274
- * @param control 控件名
275
- * @returns 归档代码
276
- */
277
- async function assertControlCodeCurrent(archive, control) {
278
- const archived = await readZipText(archive, `${control}/code.js`);
279
- let generated;
280
- try {
281
- generated = (await readProjectFile(`dist/sources/control/${control}/code.pack.js`)).toString();
282
- }
283
- catch (error) {
284
- if (error?.code === 'ENOENT') {
285
- throw new Error(archiveError(archive, `generated prerequisite ${control}/code.pack.js is absent`));
286
- }
287
- throw error;
288
- }
289
- assert.equal(archived, generated, archiveError(archive, `${control}/code.js differs from code.pack.js`));
290
- return archived;
291
- }
292
-
293
- /**
294
- * --- 断言归档 CSS 与本地编译 CSS 完全一致 ---
295
- * @param archive cgc/cgt 路径
296
- * @param entry 归档内 CSS 路径
297
- * @param sourcePath 本地生成 CSS 路径
298
- * @returns 归档 CSS
299
- */
300
- async function assertCssCurrent(archive, entry, sourcePath) {
301
- const archived = await readZipText(archive, entry);
302
- const source = (await readProjectFile(sourcePath)).toString();
303
- const result = new CleanCSS().minify(source);
304
- assert.deepEqual(result.errors, [], `${sourcePath} must be valid CSS`);
305
- assert.equal(archived, result.styles, archiveError(archive, `${entry} differs from ${sourcePath}`));
306
- return archived;
307
- }
308
-
309
- // --- task.cga 必须包含与源码一致的 16 个语言包 ---
310
- const taskArchivePath = 'dist/app/task.cga';
311
- const taskArchive = await loadCgaArchive(taskArchivePath);
312
- const archivedTaskConfigText = (await taskArchive.read('/config.json')).toString();
313
- const sourceTaskConfigText = (await readProjectFile('dist/app/task/config.json')).toString();
314
- const archivedTaskConfig = JSON.parse(archivedTaskConfigText);
315
- const sourceTaskConfig = JSON.parse(sourceTaskConfigText);
316
- assert.deepEqual(archivedTaskConfig, sourceTaskConfig, archiveError(taskArchivePath, '/config.json differs from its source'));
317
- assert.deepEqual(Object.values(archivedTaskConfig.locales), codes, archiveError(taskArchivePath, 'its locale registry is not the agreed 16 locales'));
318
-
319
- const archivedTaskApp = (await taskArchive.read('/app.js')).toString();
320
- const generatedTaskApp = (await readProjectFile('dist/app/task/app.pack.js')).toString();
321
- assert.equal(archivedTaskApp, generatedTaskApp, archiveError(taskArchivePath, '/app.js differs from app.pack.js'));
322
-
323
- let taskLocaleKeys;
324
- for (const code of codes) {
325
- const entry = `/locale/${code}.json`;
326
- const archivedText = (await taskArchive.read(entry)).toString();
327
- const sourceText = (await readProjectFile(`dist/app/task/locale/${code}.json`)).toString();
328
- const archived = JSON.parse(archivedText);
329
- const source = JSON.parse(sourceText);
330
- assert.deepEqual(archived, source, archiveError(taskArchivePath, `${entry} differs from its source`));
331
- taskLocaleKeys ??= Object.keys(archived).sort();
332
- assert.deepEqual(Object.keys(archived).sort(), taskLocaleKeys, archiveError(taskArchivePath, `${entry} has incomplete translation keys`));
333
- if (code === 'ar') {
334
- assert.match(JSON.stringify(archived), /[\u0600-\u06ff]/, archiveError(taskArchivePath, `${entry} has no Arabic text`));
335
- }
336
- }
337
-
338
- // --- 所有多语言控件的归档语言表必须与 TypeScript 源码一致 ---
339
- const archivedControlCode = new Map();
340
- for (const control of localeControls) {
341
- const packageName = commonControls.has(control) ? 'common' : control;
342
- const archive = `dist/control/${packageName}.cgc`;
343
- const archivedCode = await assertControlCodeCurrent(archive, control);
344
- archivedControlCode.set(control, archivedCode);
345
- const sourceCode = (await readProjectFile(`dist/sources/control/${control}/code.ts`)).toString();
346
- const archivedLocale = readLocaleData(archivedCode, `${archive}:${control}/code.js`, ts.ScriptKind.JS);
347
- const sourceLocale = readLocaleData(sourceCode, `dist/sources/control/${control}/code.ts`, ts.ScriptKind.TS);
348
- assert.deepEqual(
349
- Object.keys(archivedLocale).sort(),
350
- [...codes].sort(),
351
- archiveError(archive, `${control}/code.js does not contain exactly 16 locales`),
352
- );
353
- assert.deepEqual(archivedLocale, sourceLocale, archiveError(archive, `${control}/code.js localeData differs from TypeScript source`));
354
- }
355
-
356
- // --- 关键滚动与编辑器方向边界必须进入 cgc ---
357
- const rtlCodeChecks = [
358
- ['dist/control/common.cgc', 'flow', ['getScrollInlineOffset', 'setScrollInlineOffset']],
359
- ['dist/control/common.cgc', 'vflow', ['isRtl']],
360
- ['dist/control/common.cgc', 'scroll', ['isRtl', 'logicalOffsetPx']],
361
- ['dist/control/common.cgc', 'text', ['getScrollInlineOffset', 'setScrollInlineOffset']],
362
- ['dist/control/common.cgc', 'select', ['getScrollLeft', 'setScrollLeft']],
363
- ['dist/control/common.cgc', 'tab', ['getScrollInlineOffset', 'setScrollInlineOffset']],
364
- ['dist/control/common.cgc', 'toolbar', ['getScrollInlineOffset', 'setScrollLeft']],
365
- ['dist/control/common.cgc', 'date', ['autoPosition', 'autoScroll']],
366
- ['dist/control/common.cgc', 'daterange', ['autoPosition', 'autoScroll']],
367
- ['dist/control/common.cgc', 'dock-group', ['isRtl', 'floatMaxWidth']],
368
- ['dist/control/common.cgc', 'dock', ['updateColumns', 'getFloatBounds']],
369
- ['dist/control/common.cgc', 'dock-column', ['registerGroup', 'expandedPreference']],
370
- ['dist/control/common.cgc', 'toolbox', ['getBoundingClientRect', 'toggleColumns']],
371
- ['dist/control/common.cgc', 'sidebar-toggle', ['chevronRight', 'tipLabelComp', 'update:expanded']],
372
- ['dist/control/table.cgc', 'table', ['localeDirection', 'getScrollLeft', 'setScrollInlineOffset']],
373
- ['dist/control/desc.cgc', 'desc', ['getScrollInlineOffset', 'setScrollInlineOffset']],
374
- ['dist/control/iconview.cgc', 'iconview', ['isRtl']],
375
- ['dist/control/swiper.cgc', 'swiper', ['isRtl', 'directionVersion']],
376
- ['dist/control/jodit.cgc', 'jodit', ['_refreshDirection', 'direction', 'rtl']],
377
- ['dist/control/tuieditor.cgc', 'tuieditor', ['_refreshDirection', 'ProseMirror', 'rtl']],
378
- ['dist/control/monaco.cgc', 'monaco', ['documentElement.dir', 'ltr']],
379
- ];
380
- for (const [archive, control, tokens] of rtlCodeChecks) {
381
- const archivedCode = archivedControlCode.get(control) ?? await assertControlCodeCurrent(archive, control);
382
- for (const token of tokens) {
383
- assert.ok(archivedCode.includes(token), archiveError(archive, `${control}/code.js lacks RTL marker ${token}`));
384
- }
385
- }
386
-
387
- // --- 控件样式必须进入 cgc,并与 Sass 生成 CSS 一致 ---
388
- const rtlStyleChecks = [
389
- ['dist/control/form.cgc', 'form/style.css', 'dist/sources/control/form/style.css', ['isolation:isolate']],
390
- ['dist/control/common.cgc', 'bgroup/style.css', 'dist/sources/control/bgroup/style.css', ['display:inline-flex']],
391
- ['dist/control/common.cgc', 'button/style.css', 'dist/sources/control/button/style.css', [':dir(rtl)', 'border-inline-end']],
392
- ['dist/control/common.cgc', 'list/style.css', 'dist/sources/control/list/style.css', [':dir(rtl)', 'margin-inline-end']],
393
- ['dist/control/common.cgc', 'tab/style.css', 'dist/sources/control/tab/style.css', ['border-inline-start-width', 'border-inline-end-width']],
394
- ['dist/control/common.cgc', 'timeline/style.css', 'dist/sources/control/timeline/style.css', ['border-inline-start']],
395
- ['dist/control/common.cgc', 'dock/style.css', 'dist/sources/control/dock/style.css', ['border-inline-end']],
396
- ['dist/control/common.cgc', 'dock-group/style.css', 'dist/sources/control/dock-group/style.css', ['inset-inline-start', 'max-width:calc(100vw - 40px)']],
397
- ['dist/control/common.cgc', 'dock-column/style.css', 'dist/sources/control/dock-column/style.css', ['border-inline-start']],
398
- ['dist/control/common.cgc', 'toolbox/style.css', 'dist/sources/control/toolbox/style.css', ['border-inline-end']],
399
- ['dist/control/common.cgc', 'sidebar-toggle/style.css', 'dist/sources/control/sidebar-toggle/style.css', [':dir(rtl)', 'border-bottom', '--sidebar-toggle-background']],
400
- ['dist/control/table.cgc', 'table/style.css', 'dist/sources/control/table/style.css', ['border-inline-end', 'inset-inline-end']],
401
- ['dist/control/xterm.cgc', 'xterm/style.css', 'dist/sources/control/xterm/style.css', ['direction:ltr']],
402
- ];
403
- for (const [archive, entry, sourcePath, tokens] of rtlStyleChecks) {
404
- const archivedCss = await assertCssCurrent(archive, entry, sourcePath);
405
- for (const token of tokens) {
406
- assert.ok(archivedCss.includes(token), archiveError(archive, `${entry} lacks RTL/LTR boundary marker ${token}`));
407
- }
408
- }
409
-
410
- // --- 全部主题必须全部包含最新的逻辑方向样式 ---
411
- for (const theme of themes) {
412
- const archive = `dist/theme/${theme}.cgt`;
413
- const archivedConfig = JSON.parse(await readZipText(archive, 'config.json'));
414
- const sourceConfig = JSON.parse((await readProjectFile(`dist/sources/theme/${theme}/config.json`)).toString());
415
- assert.deepEqual(archivedConfig, sourceConfig, archiveError(archive, 'config.json differs from its source'));
416
- const archivedCss = await assertCssCurrent(
417
- archive,
418
- 'style.css',
419
- `dist/sources/theme/${theme}/style.css`,
420
- );
421
- assert.match(
422
- archivedCss,
423
- /(?:border|inset|margin|padding)-inline/,
424
- archiveError(archive, 'style.css has no logical inline-direction properties'),
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
- }
438
- }
439
-
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.`);
@@ -1,193 +0,0 @@
1
- // Run: node --experimental-vm-modules test/rtl-direction.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 dom = new JSDOM('<!doctype html><html><head></head><body></body></html>');
9
- for (const name of ['window', 'document', 'navigator', 'Element', 'SVGElement', 'Node', 'HTMLElement', 'PointerEvent']) {
10
- Object.defineProperty(globalThis, name, {
11
- 'configurable': true,
12
- 'value': dom.window[name],
13
- });
14
- }
15
- globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
16
- globalThis.ResizeObserver = class {
17
- observe() {}
18
- unobserve() {}
19
- disconnect() {}
20
- };
21
-
22
- /** --- 当前模拟任务的 locale 状态 --- */
23
- const tasks = new Map([
24
- ['arabic', { 'locale': { 'lang': 'ar' } }],
25
- ['english', { 'locale': { 'lang': 'en' } }],
26
- ['inherit', { 'locale': { 'lang': '' } }],
27
- ]);
28
- /** --- 系统 locale,可在测试中模拟运行时切换 --- */
29
- const config = { 'locale': 'en' };
30
-
31
- const toolSource = await readFile(new URL('../dist/lib/tool.ts', import.meta.url), 'utf8');
32
- const toolModule = new SourceTextModule(ts.transpileModule(toolSource, {
33
- 'compilerOptions': {
34
- 'target': ts.ScriptTarget.ES2022,
35
- 'module': ts.ModuleKind.ESNext,
36
- },
37
- }).outputText);
38
- await toolModule.link(() => {
39
- throw new Error('tool.ts must not import runtime modules');
40
- });
41
- await toolModule.evaluate();
42
-
43
- const clickgo = new SyntheticModule(['modules'], function() {
44
- this.setExport('modules', {
45
- 'pointer': {
46
- 'move': () => {},
47
- },
48
- 'vue': {
49
- 'createApp': () => ({
50
- 'mount': () => {},
51
- }),
52
- },
53
- });
54
- });
55
- const core = new SyntheticModule(['config'], function() {
56
- this.setExport('config', config);
57
- });
58
- const task = new SyntheticModule(['getOrigin'], function() {
59
- this.setExport('getOrigin', (taskId) => tasks.get(taskId) ?? null);
60
- });
61
- const tool = new SyntheticModule(['lang', 'logicalOr'], function() {
62
- this.setExport('lang', toolModule.namespace.lang);
63
- this.setExport('logicalOr', toolModule.namespace.logicalOr);
64
- });
65
- const empty = new SyntheticModule([], function() {});
66
-
67
- const formSource = await readFile(new URL('../dist/lib/form.ts', import.meta.url), 'utf8');
68
- const formModule = new SourceTextModule(ts.transpileModule(formSource, {
69
- 'compilerOptions': {
70
- 'target': ts.ScriptTarget.ES2022,
71
- 'module': ts.ModuleKind.ESNext,
72
- },
73
- }).outputText);
74
- await formModule.link((specifier) => {
75
- if (specifier === '../clickgo') {
76
- return clickgo;
77
- }
78
- if (specifier === './core') {
79
- return core;
80
- }
81
- if (specifier === './task') {
82
- return task;
83
- }
84
- if (specifier === './tool') {
85
- return tool;
86
- }
87
- return empty;
88
- });
89
- await formModule.evaluate();
90
-
91
- const domSource = await readFile(new URL('../dist/lib/dom.ts', import.meta.url), 'utf8');
92
- const domModule = new SourceTextModule(ts.transpileModule(domSource, {
93
- 'compilerOptions': {
94
- 'target': ts.ScriptTarget.ES2022,
95
- 'module': ts.ModuleKind.ESNext,
96
- },
97
- }).outputText);
98
- await domModule.link((specifier) => {
99
- if (specifier === '../clickgo') {
100
- return clickgo;
101
- }
102
- if (specifier === './form') {
103
- return formModule;
104
- }
105
- if (specifier === './core') {
106
- return core;
107
- }
108
- if (specifier === './task') {
109
- return task;
110
- }
111
- return empty;
112
- });
113
- await domModule.evaluate();
114
-
115
- const { isRtl } = domModule.namespace;
116
- const rtlElement = document.createElement('div');
117
- rtlElement.style.direction = 'rtl';
118
- document.body.append(rtlElement);
119
- assert.equal(isRtl(rtlElement), true, 'an RTL element must resolve to RTL');
120
- assert.equal(isRtl(document.createComment('pending render')), false, 'a Vue placeholder node must safely resolve to LTR');
121
- assert.equal(isRtl(null), false, 'a missing element must safely resolve to LTR');
122
-
123
- const { elements, refreshLocaleDirection } = formModule.namespace;
124
- elements.init();
125
- assert.equal(document.querySelector('#cg-wrap'), elements.wrap, 'system root must initialize as #cg-wrap');
126
- assert.equal(elements.wrap.lang, 'en', 'system root must receive the global language tag');
127
- assert.equal(elements.wrap.dir, 'ltr', 'system root must receive the global direction');
128
-
129
- /**
130
- * --- 创建同一任务的 Form 与 pop 容器 ---
131
- * @param taskId ClickGo task id
132
- * @param formId ClickGo form id
133
- * @returns 两个语言容器
134
- */
135
- function appendTaskWrap(taskId, formId) {
136
- const formWrap = document.createElement('div');
137
- formWrap.className = 'cg-form-wrap';
138
- formWrap.dataset.taskId = taskId;
139
- formWrap.dataset.formId = formId;
140
- const popWrap = document.createElement('div');
141
- popWrap.dataset.taskId = taskId;
142
- popWrap.dataset.formId = formId;
143
- elements.list.append(formWrap);
144
- elements.popList.append(popWrap);
145
- return { formWrap, popWrap };
146
- }
147
-
148
- const arabic = appendTaskWrap('arabic', 'form-ar');
149
- const english = appendTaskWrap('english', 'form-en');
150
- const inherited = appendTaskWrap('inherit', 'form-inherit');
151
- refreshLocaleDirection();
152
- for (const wrap of [arabic.formWrap, arabic.popWrap]) {
153
- assert.equal(wrap.lang, 'ar', 'Arabic Form and pop wrappers must receive lang=ar');
154
- assert.equal(wrap.dir, 'rtl', 'Arabic Form and pop wrappers must receive RTL');
155
- }
156
- for (const wrap of [english.formWrap, english.popWrap, inherited.formWrap, inherited.popWrap]) {
157
- assert.equal(wrap.lang, 'en', 'English and inherited Form/pop wrappers must receive lang=en');
158
- assert.equal(wrap.dir, 'ltr', 'English and inherited Form/pop wrappers must receive LTR');
159
- }
160
-
161
- // A task-only refresh must update that task's Form and pop wrappers without disturbing a concurrent task.
162
- tasks.get('arabic').locale.lang = 'en';
163
- refreshLocaleDirection('arabic');
164
- for (const wrap of [arabic.formWrap, arabic.popWrap]) {
165
- assert.equal(wrap.lang, 'en', 'task locale switch ar -> en must update both wrapper types');
166
- assert.equal(wrap.dir, 'ltr', 'task locale switch ar -> en must update both wrapper types');
167
- }
168
- assert.equal(english.formWrap.dir, 'ltr', 'task-only refresh must preserve another task direction');
169
-
170
- tasks.get('arabic').locale.lang = 'ar';
171
- refreshLocaleDirection('arabic');
172
- assert.equal(arabic.formWrap.dir, 'rtl', 'task locale switch en -> ar must restore RTL');
173
- assert.equal(arabic.popWrap.dir, 'rtl', 'task locale switch en -> ar must restore pop RTL');
174
-
175
- // Global direction refresh changes the system root and inherited tasks, while explicit task locales remain isolated.
176
- config.locale = 'ar';
177
- refreshLocaleDirection();
178
- assert.equal(elements.wrap.lang, 'ar', 'global locale switch must update #cg-wrap language');
179
- assert.equal(elements.wrap.dir, 'rtl', 'global locale switch must update #cg-wrap direction');
180
- assert.equal(inherited.formWrap.dir, 'rtl', 'task without an override must inherit global RTL');
181
- assert.equal(inherited.popWrap.dir, 'rtl', 'inherited pop wrapper must inherit global RTL');
182
- assert.equal(english.formWrap.dir, 'ltr', 'explicit English task must coexist with global Arabic RTL');
183
- assert.equal(arabic.formWrap.dir, 'rtl', 'explicit Arabic task must coexist with global Arabic RTL');
184
-
185
- config.locale = 'en';
186
- refreshLocaleDirection();
187
- assert.equal(elements.wrap.dir, 'ltr', 'global locale switch ar -> en must restore root LTR');
188
- assert.equal(inherited.formWrap.dir, 'ltr', 'global locale switch ar -> en must restore inherited Form LTR');
189
- assert.equal(inherited.popWrap.dir, 'ltr', 'global locale switch ar -> en must restore inherited pop LTR');
190
- assert.equal(arabic.formWrap.dir, 'rtl', 'explicit Arabic task must remain RTL after global refresh');
191
-
192
- dom.window.close();
193
- console.log('RTL root, Form/pop isolation, task switching and global refresh checks passed from TypeScript source.');