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.
- package/AGENTS.md +16 -0
- package/README.md +4 -0
- package/dist/app/demo.cga +0 -0
- package/dist/app/task.cga +0 -0
- package/dist/clickgo.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/lib/command/native.d.ts +18 -0
- package/dist/lib/command/palette.d.ts +10 -0
- package/dist/lib/command/schema.d.ts +31 -0
- package/dist/lib/command.d.ts +218 -0
- package/dist/lib/native.d.ts +12 -0
- package/dist/lib/task.d.ts +2 -0
- package/doc/clickgo-rag.md +2376 -537
- package/package.json +9 -3
- package/test/core-module-loading.mjs +0 -169
- package/test/desktop.mjs +0 -715
- package/test/dock-lifecycle.mjs +0 -280
- package/test/dom-style.mjs +0 -261
- package/test/form-launcher.mjs +0 -177
- package/test/form-modal-layer.mjs +0 -75
- package/test/form-native-regions.mjs +0 -115
- package/test/form-native-style.mjs +0 -80
- package/test/form-native.mjs +0 -68
- package/test/form-notify-animation.mjs +0 -102
- package/test/form-notify.mjs +0 -90
- package/test/form-viewport.mjs +0 -127
- package/test/iconview.mjs +0 -70
- package/test/jodit-lifecycle.mjs +0 -212
- package/test/locale-integrity.mjs +0 -189
- package/test/pop-max-size.mjs +0 -66
- package/test/rtl-archive.mjs +0 -440
- package/test/rtl-direction.mjs +0 -193
- package/test/scroll.mjs +0 -145
- package/test/select.mjs +0 -885
- package/test/sidebar-toggle.mjs +0 -75
- package/test/task-package-loading.mjs +0 -94
- package/test/task-start.mjs +0 -141
- package/test/task-tray.mjs +0 -331
- package/test/tool-assets.mjs +0 -64
- package/test/tool-clone.mjs +0 -42
- package/test/toolbox.mjs +0 -88
- package/test/tuieditor-locale.mjs +0 -175
- package/test/vflow.mjs +0 -122
- package/test/web-drag-mask.mjs +0 -96
package/test/form-launcher.mjs
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// Run: node test/form-launcher.mjs
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import vm from 'node:vm';
|
|
5
|
-
import { JSDOM } from 'jsdom';
|
|
6
|
-
import ts from 'typescript';
|
|
7
|
-
|
|
8
|
-
const dom = new JSDOM('<!doctype html><html><body><div id="cg-launcher"></div></body></html>');
|
|
9
|
-
for (const name of ['window', 'document', 'Element', 'HTMLElement', 'SVGElement', 'Node', 'Event', 'CustomEvent']) {
|
|
10
|
-
Object.defineProperty(globalThis, name, { configurable: true, value: dom.window[name] });
|
|
11
|
-
}
|
|
12
|
-
const errors = [];
|
|
13
|
-
window.addEventListener('error', e => errors.push(e.error));
|
|
14
|
-
const vue = await import('vue');
|
|
15
|
-
const pointer = await import('../node_modules/@litert/pointer/dist/index.esm.js');
|
|
16
|
-
const source = await readFile(process.argv[2] ?? new URL('../dist/lib/form.ts', import.meta.url), 'utf8');
|
|
17
|
-
const tree = ts.createSourceFile('form.ts', source, ts.ScriptTarget.ES2022, true);
|
|
18
|
-
let config;
|
|
19
|
-
function find(node) {
|
|
20
|
-
if (ts.isVariableDeclaration(node) && node.name.getText(tree) === 'launcherApp') {
|
|
21
|
-
config = node.initializer.arguments[0].getText(tree);
|
|
22
|
-
}
|
|
23
|
-
ts.forEachChild(node, find);
|
|
24
|
-
}
|
|
25
|
-
find(tree);
|
|
26
|
-
assert.ok(config, 'load the actual launcher template and handlers');
|
|
27
|
-
let hidden = 0;
|
|
28
|
-
const runs = [];
|
|
29
|
-
const exports = {};
|
|
30
|
-
const scope = {
|
|
31
|
-
exports, document,
|
|
32
|
-
clickgo: { modules: { pointer } },
|
|
33
|
-
info: { locale: { en: { search: 'Search' } } },
|
|
34
|
-
lCore: { config: { locale: 'en', 'launcher.list': [
|
|
35
|
-
{ name: 'App', path: '/app', icon: '' },
|
|
36
|
-
{ name: 'Folder', list: [{ name: 'Child', path: '/child', icon: '' }] },
|
|
37
|
-
] } },
|
|
38
|
-
lTask: { async run(...args) { runs.push(args); } },
|
|
39
|
-
sysId: 'system',
|
|
40
|
-
hideLauncher() { ++hidden; },
|
|
41
|
-
requestAnimationFrame(callback) { callback(); },
|
|
42
|
-
setTimeout() { return 0; },
|
|
43
|
-
launcherRoot: null,
|
|
44
|
-
};
|
|
45
|
-
vm.runInNewContext(ts.transpileModule(`export default ${config};`, {
|
|
46
|
-
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
47
|
-
}).outputText, scope);
|
|
48
|
-
const app = vue.createApp(exports.default);
|
|
49
|
-
const root = app.mount('#cg-launcher');
|
|
50
|
-
const list = document.querySelector('.cg-launcher-list');
|
|
51
|
-
const search = document.querySelector('.cg-launcher-search');
|
|
52
|
-
|
|
53
|
-
function event(target, type, x, y, button = 0) {
|
|
54
|
-
const e = new window.MouseEvent(type, { bubbles: true, clientX: x, clientY: y, button, view: window });
|
|
55
|
-
Object.defineProperties(e, { pointerId: { value: 1 }, pointerType: { value: 'mouse' } });
|
|
56
|
-
target.dispatchEvent(e);
|
|
57
|
-
}
|
|
58
|
-
async function click(target, { start = [100, 200], end = start, cancel = false, button = 0 } = {}) {
|
|
59
|
-
event(target, 'pointerdown', ...start, button);
|
|
60
|
-
event(target, cancel ? 'pointercancel' : 'pointerup', ...end, button);
|
|
61
|
-
if (!cancel) { event(target, 'click', ...end, button); }
|
|
62
|
-
await vue.nextTick();
|
|
63
|
-
}
|
|
64
|
-
await click(list);
|
|
65
|
-
assert.equal(hidden, 1, 'stationary list blank click closes');
|
|
66
|
-
await click(list, { end: [101, 201] });
|
|
67
|
-
assert.equal(hidden, 2, 'minor pointer movement still counts as a click');
|
|
68
|
-
await click(list, { start: [100.4, 200.4], end: [100, 200] });
|
|
69
|
-
assert.equal(hidden, 3, 'fractional pointer coordinates do not reject a click');
|
|
70
|
-
await click(list, { end: [120, 180] });
|
|
71
|
-
await click(list, { cancel: true });
|
|
72
|
-
await click(list, { button: 2 });
|
|
73
|
-
assert.equal(hidden, 3, 'diagonal drag, cancellation and right click do not close');
|
|
74
|
-
await click(search);
|
|
75
|
-
await click(document.querySelector('.cg-launcher-space'));
|
|
76
|
-
assert.equal(hidden, 5, 'search padding and item spacers close');
|
|
77
|
-
await click(document.querySelector('input'));
|
|
78
|
-
assert.equal(hidden, 5, 'search input stays interactive');
|
|
79
|
-
await click(document.querySelector('.cg-launcher-icon'), { end: [101, 201] });
|
|
80
|
-
assert.equal(hidden, 6);
|
|
81
|
-
assert.deepEqual(runs.map(args => args[1]), ['/app'], 'app runs once without a bubbled blank click');
|
|
82
|
-
const folder = document.querySelector('.cg-launcher-folder > div');
|
|
83
|
-
for (const target of folder.querySelectorAll('.cg-launcher-icon, .cg-launcher-inner, .cg-launcher-item, .cg-launcher-space')) {
|
|
84
|
-
await click(target);
|
|
85
|
-
assert.equal(root.folderName, 'Folder', 'every collapsed preview target opens its folder');
|
|
86
|
-
assert.equal(hidden, 6, 'collapsed previews do not dismiss the launcher');
|
|
87
|
-
assert.deepEqual(runs.map(args => args[1]), ['/app'], 'collapsed previews never start a child app');
|
|
88
|
-
await click(search);
|
|
89
|
-
}
|
|
90
|
-
await click(folder, { end: [101, 201] });
|
|
91
|
-
assert.equal(root.folderName, 'Folder');
|
|
92
|
-
await click(search);
|
|
93
|
-
assert.equal(root.folderName, '', 'blank click closes the folder before the launcher');
|
|
94
|
-
assert.equal(hidden, 6);
|
|
95
|
-
await click(folder);
|
|
96
|
-
await click(folder.querySelector('.cg-launcher-icon'), { end: [101, 201] });
|
|
97
|
-
assert.equal(hidden, 7);
|
|
98
|
-
assert.deepEqual(runs.map(args => args[1]), ['/app', '/child'], 'folder child runs once');
|
|
99
|
-
assert.deepEqual(errors, [], 'event handlers do not throw');
|
|
100
|
-
app.unmount();
|
|
101
|
-
dom.window.close();
|
|
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.');
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Run after Sass compilation: node test/form-modal-layer.mjs
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { JSDOM } from 'jsdom';
|
|
5
|
-
import * as tool from '../dist/lib/tool.js';
|
|
6
|
-
|
|
7
|
-
const dom = new JSDOM('<!doctype html><html><head></head><body></body></html>');
|
|
8
|
-
for (const name of ['window', 'document', 'Element', 'SVGElement', 'Node', 'HTMLElement']) {
|
|
9
|
-
globalThis[name] = dom.window[name];
|
|
10
|
-
}
|
|
11
|
-
const { createApp, compile, nextTick } = await import('vue');
|
|
12
|
-
const template = await readFile(new URL('../dist/sources/control/form/layout.html', import.meta.url), 'utf8');
|
|
13
|
-
const formStyle = document.createElement('style');
|
|
14
|
-
formStyle.textContent = await readFile(new URL('../dist/sources/control/form/style.css', import.meta.url), 'utf8');
|
|
15
|
-
document.head.append(formStyle);
|
|
16
|
-
const dockStyle = document.createElement('style');
|
|
17
|
-
dockStyle.textContent = tool.stylePrepend(await readFile(
|
|
18
|
-
new URL('../dist/sources/control/dock-group/style.css', import.meta.url), 'utf8',
|
|
19
|
-
), 'dock-group_').style;
|
|
20
|
-
document.head.append(dockStyle);
|
|
21
|
-
|
|
22
|
-
for (const border of ['normal', 'thin', 'plain', 'none']) {
|
|
23
|
-
const host = document.createElement('div');
|
|
24
|
-
document.body.append(host);
|
|
25
|
-
const app = createApp({
|
|
26
|
-
render: compile(template),
|
|
27
|
-
data: () => ({
|
|
28
|
-
border, isNative: false, isNativeNoFrameFirst: false, flashTimer: undefined,
|
|
29
|
-
stateMinData: false, stateMaxData: false, taskPosition: 'bottom',
|
|
30
|
-
isShow: true, isInside: false, formFocus: true, widthData: 500,
|
|
31
|
-
heightData: 400, leftData: 0, topData: 0, zIndex: 1,
|
|
32
|
-
isMask: false, isResize: false, iconDataUrl: '', title: 'Dock',
|
|
33
|
-
isMin: false, isMax: false, isClose: false, direction: 'v',
|
|
34
|
-
isLoading: false, loading: false, loadingComp: false,
|
|
35
|
-
background: '', padding: '', stepShowData: false, stepData: [],
|
|
36
|
-
stepDown: () => {}, stepValue: '', stepValues: [],
|
|
37
|
-
}),
|
|
38
|
-
});
|
|
39
|
-
app.component('cg-loading', { render: () => null });
|
|
40
|
-
app.component('cg-step', { render: () => null });
|
|
41
|
-
const form = app.mount(host);
|
|
42
|
-
const wrap = host.querySelector('.wrap');
|
|
43
|
-
const inner = wrap.querySelector(':scope > .inner');
|
|
44
|
-
const content = inner.querySelector(':scope > .content');
|
|
45
|
-
const dock = document.createElement('div');
|
|
46
|
-
dock.className = 'dock-group_wrap dock-group_collapsed dock-group_floating';
|
|
47
|
-
dock.innerHTML = '<div class="dock-group_content"><button>Images</button></div>';
|
|
48
|
-
content.append(dock);
|
|
49
|
-
const floating = dock.firstElementChild;
|
|
50
|
-
const button = floating.firstElementChild;
|
|
51
|
-
assert.equal(dom.window.getComputedStyle(floating).zIndex, '10');
|
|
52
|
-
|
|
53
|
-
// --- 模态打开、嵌套模态仍遮罩、关闭后的恢复均不得重建 Dock 内容 ---
|
|
54
|
-
for (const masked of [true, true, false, true, false]) {
|
|
55
|
-
form.isMask = masked;
|
|
56
|
-
await nextTick();
|
|
57
|
-
const mask = wrap.querySelector(':scope > .mask');
|
|
58
|
-
assert.equal(Boolean(mask), masked);
|
|
59
|
-
assert.equal(dock.firstElementChild, floating);
|
|
60
|
-
assert.equal(floating.firstElementChild, button);
|
|
61
|
-
assert.equal(dom.window.getComputedStyle(floating).display, 'flex');
|
|
62
|
-
if (mask) {
|
|
63
|
-
assert.equal(mask.parentElement, inner.parentElement);
|
|
64
|
-
assert.equal(dom.window.getComputedStyle(mask).zIndex, '10');
|
|
65
|
-
// --- 子浮层不能参与遮罩所在层的排序,即使两者 z-index 相同且浮层 DOM 在后 ---
|
|
66
|
-
assert.equal(dom.window.getComputedStyle(inner).isolation, 'isolate');
|
|
67
|
-
assert.equal(inner.contains(floating), true);
|
|
68
|
-
assert.equal(inner.contains(mask), false);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
app.unmount();
|
|
72
|
-
host.remove();
|
|
73
|
-
}
|
|
74
|
-
dom.window.close();
|
|
75
|
-
console.log('Form modal layer checks passed: Dock floats stay inside the content stacking context for all borders; modal toggles preserve panel content.');
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// Run after Sass compilation: node test/form-native-regions.mjs (requires an Electron display).
|
|
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 { createSSRApp } from 'vue';
|
|
8
|
-
import { renderToString } from '@vue/server-renderer';
|
|
9
|
-
|
|
10
|
-
if (!process.versions.electron) {
|
|
11
|
-
const child = spawn(electron, [fileURLToPath(import.meta.url), '--ozone-platform=x11'], {
|
|
12
|
-
'env': { ...process.env, 'ELECTRON_DEBUG_DRAGGABLE_REGIONS': '1' },
|
|
13
|
-
'stdio': ['ignore', 'ignore', 'pipe'],
|
|
14
|
-
});
|
|
15
|
-
let output = '';
|
|
16
|
-
child.stderr.setEncoding('utf8');
|
|
17
|
-
child.stderr.on('data', chunk => { output += chunk; });
|
|
18
|
-
const timeout = setTimeout(() => child.kill(), 20000);
|
|
19
|
-
const code = await new Promise((resolve, reject) => {
|
|
20
|
-
child.on('error', reject);
|
|
21
|
-
child.on('exit', resolve);
|
|
22
|
-
});
|
|
23
|
-
clearTimeout(timeout);
|
|
24
|
-
assert.equal(code, 0, output);
|
|
25
|
-
let region;
|
|
26
|
-
const cases = new Map();
|
|
27
|
-
for (const line of output.split('\n')) {
|
|
28
|
-
const update = /hit-test region computed in .*?: (\d+) rect\(s\), bounds (\S+ \S+)/.exec(line);
|
|
29
|
-
if (update) {
|
|
30
|
-
region = { 'rects': Number(update[1]), 'bounds': update[2] };
|
|
31
|
-
}
|
|
32
|
-
const completed = /CASE-END (\S+)/.exec(line);
|
|
33
|
-
if (completed) {
|
|
34
|
-
assert.ok(region, 'Electron must report an actual native drag region');
|
|
35
|
-
cases.set(completed[1], region);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
assert.equal(cases.size, 8, output);
|
|
39
|
-
assert.ok(cases.get('baseline').rects > 0);
|
|
40
|
-
for (const name of ['bottom', 'hidden', 'minimized', 'bottom-restored', 'nested', 'closed']) {
|
|
41
|
-
assert.deepEqual(cases.get(name), cases.get('baseline'), `${name} must preserve the native title drag region`);
|
|
42
|
-
}
|
|
43
|
-
assert.equal(cases.get('foreground').rects, 0, `an overlapping foreground Form must still block native dragging\n${output}`);
|
|
44
|
-
console.log('Electron native drag regions passed: bottomMost, hidden, minimized, nested, foreground and close.');
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
const { app, BrowserWindow } = electron;
|
|
48
|
-
app.commandLine.appendSwitch('enable-logging');
|
|
49
|
-
/** --- 等待 Chromium 将布局更新发送给 Native 命中区域计算器 --- */
|
|
50
|
-
const settle = () => new Promise(resolve => setTimeout(resolve, 350));
|
|
51
|
-
app.whenReady().then(async () => {
|
|
52
|
-
const win = new BrowserWindow({
|
|
53
|
-
'show': false, 'frame': false, 'width': 400, 'height': 300,
|
|
54
|
-
'webPreferences': { 'backgroundThrottling': false },
|
|
55
|
-
});
|
|
56
|
-
try {
|
|
57
|
-
const template = await readFile(new URL('../dist/sources/control/form/layout.html', import.meta.url), 'utf8');
|
|
58
|
-
const style = await readFile(new URL('../dist/sources/control/form/style.css', import.meta.url), 'utf8');
|
|
59
|
-
/**
|
|
60
|
-
* --- 用实际 Form 模板生成 Native 主窗体与后创建的背景窗体 ---
|
|
61
|
-
* @param nativeFirst 是否为 Native 主窗体
|
|
62
|
-
* @returns 已渲染的 Form HTML
|
|
63
|
-
*/
|
|
64
|
-
async function render(nativeFirst) {
|
|
65
|
-
const form = createSSRApp({
|
|
66
|
-
template,
|
|
67
|
-
data: () => ({
|
|
68
|
-
'border': nativeFirst ? 'normal' : 'none',
|
|
69
|
-
'isNative': true, 'isNativeNoFrameFirst': nativeFirst,
|
|
70
|
-
'flashTimer': undefined, 'stateMinData': false, 'stateMaxData': false,
|
|
71
|
-
'taskPosition': 'bottom', 'isShow': true, 'isInside': false,
|
|
72
|
-
'formFocus': nativeFirst, 'widthData': 400, 'heightData': 300,
|
|
73
|
-
'leftData': 0, 'topData': 0, 'zIndex': nativeFirst ? 10 : 1,
|
|
74
|
-
'isMask': false, 'isResize': false, 'iconDataUrl': '', 'title': 'Native',
|
|
75
|
-
'isMin': true, 'isMax': true, 'isClose': true, 'direction': 'v',
|
|
76
|
-
'background': '', 'contentPadding': '', 'stepShowData': false,
|
|
77
|
-
'stepData': [], 'stepValue': '', 'isLoading': false,
|
|
78
|
-
}),
|
|
79
|
-
});
|
|
80
|
-
form.component('cg-step', { 'render': () => null });
|
|
81
|
-
form.component('cg-loading', { 'render': () => null });
|
|
82
|
-
return await renderToString(form);
|
|
83
|
-
}
|
|
84
|
-
const native = await render(true);
|
|
85
|
-
const background = await render(false);
|
|
86
|
-
await win.loadURL(`data:text/html,${encodeURIComponent(`<style>body{margin:0}${style}</style><div id="native">${native}</div><div id="background"></div>`)}`);
|
|
87
|
-
await settle();
|
|
88
|
-
console.error('CASE-END baseline');
|
|
89
|
-
const scripts = [
|
|
90
|
-
['bottom', `document.querySelector('#background').innerHTML=${JSON.stringify(background)};const bg=document.querySelector('#background>.wrap');bg.dataset.cgBottomMost='';`],
|
|
91
|
-
['hidden', `const bg=document.querySelector('#background>.wrap');delete bg.dataset.cgBottomMost;bg.classList.remove('show');`],
|
|
92
|
-
['minimized', `const bg=document.querySelector('#background>.wrap');bg.classList.add('show');bg.dataset.cgMin='';`],
|
|
93
|
-
['foreground', `const bg=document.querySelector('#background>.wrap');delete bg.dataset.cgMin;bg.style.zIndex='20';`],
|
|
94
|
-
['bottom-restored', `const bg=document.querySelector('#background>.wrap');bg.style.zIndex='1';bg.dataset.cgBottomMost='';`],
|
|
95
|
-
['nested', `document.querySelector('#background .content').innerHTML=${JSON.stringify(background)};`],
|
|
96
|
-
['closed', `document.querySelector('#background').replaceChildren();`],
|
|
97
|
-
];
|
|
98
|
-
for (const [name, script] of scripts) {
|
|
99
|
-
await win.webContents.executeJavaScript(`{${script}}`);
|
|
100
|
-
// --- 隐藏测试窗口也必须完成布局和绘制,不能读取上一帧的命中区域 ---
|
|
101
|
-
await win.webContents.capturePage();
|
|
102
|
-
await settle();
|
|
103
|
-
console.error(`CASE-END ${name}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
console.error(error);
|
|
108
|
-
app.exit(1);
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
win.destroy();
|
|
112
|
-
app.quit();
|
|
113
|
-
}
|
|
114
|
-
}).catch(error => { console.error(error); app.exit(1); });
|
|
115
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// Run: node test/form-native-style.mjs
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { JSDOM } from 'jsdom';
|
|
5
|
-
|
|
6
|
-
const dom = new JSDOM('<!doctype html><html><head></head><body></body></html>');
|
|
7
|
-
for (const name of ['window', 'document', 'Element', 'SVGElement', 'Node', 'HTMLElement']) {
|
|
8
|
-
globalThis[name] = dom.window[name];
|
|
9
|
-
}
|
|
10
|
-
const { createApp, compile, nextTick } = await import('vue');
|
|
11
|
-
const template = await readFile(new URL('../dist/sources/control/form/layout.html', import.meta.url), 'utf8');
|
|
12
|
-
const render = compile(template);
|
|
13
|
-
const theme = document.createElement('style');
|
|
14
|
-
theme.textContent = `
|
|
15
|
-
.wrap { border: 3px solid black !important; border-radius: 24px !important; box-shadow: 0 0 8px black !important; }
|
|
16
|
-
.wrap > .inner, .wrap > .inner > .header, .wrap > .inner > .content { border-radius: 24px !important; }
|
|
17
|
-
.child-control { border: 1px solid blue; border-radius: 12px; }
|
|
18
|
-
`;
|
|
19
|
-
document.head.append(theme);
|
|
20
|
-
|
|
21
|
-
for (const mode of ['web', 'native-framed', 'native-frameless']) {
|
|
22
|
-
const nativeFirst = mode === 'native-frameless';
|
|
23
|
-
const nativeMode = mode !== 'web';
|
|
24
|
-
for (const border of ['normal', 'thin', 'plain', 'none']) {
|
|
25
|
-
const host = document.createElement('div');
|
|
26
|
-
document.body.append(host);
|
|
27
|
-
const app = createApp({
|
|
28
|
-
render,
|
|
29
|
-
data: () => ({
|
|
30
|
-
border, isNative: nativeMode, isNativeNoFrameFirst: nativeFirst, flashTimer: undefined,
|
|
31
|
-
stateMinData: false, stateMaxData: false, taskPosition: 'bottom',
|
|
32
|
-
isShow: true, isInside: false, formFocus: false, widthData: 500,
|
|
33
|
-
heightData: 400, leftData: 0, topData: 0, zIndex: 1,
|
|
34
|
-
isMask: false, isResize: false, iconDataUrl: '', title: 'Notepad',
|
|
35
|
-
isMin: true, isMax: true, isClose: true, direction: 'v',
|
|
36
|
-
background: '', padding: '', stepShowData: false, stepData: [],
|
|
37
|
-
stepValue: '', isLoading: false,
|
|
38
|
-
}),
|
|
39
|
-
methods: { moveMethod() {}, minMethod() {}, maxMethod() {}, closeMethod() {}, stepDown() {} },
|
|
40
|
-
});
|
|
41
|
-
app.component('cg-step', { render: () => null });
|
|
42
|
-
app.component('cg-loading', { render: () => null });
|
|
43
|
-
const form = app.mount(host);
|
|
44
|
-
const wrap = host.querySelector('.wrap');
|
|
45
|
-
assert.equal(wrap.style.maxWidth, nativeMode ? '' : '100vw');
|
|
46
|
-
assert.equal(wrap.style.maxHeight, nativeMode ? '' : '100vh');
|
|
47
|
-
const inner = host.querySelector('.inner');
|
|
48
|
-
const content = host.querySelector('.content');
|
|
49
|
-
const header = host.querySelector('.header');
|
|
50
|
-
assert.equal(Boolean(header), border === 'normal' || border === 'thin');
|
|
51
|
-
const child = document.createElement('div');
|
|
52
|
-
child.className = 'child-control';
|
|
53
|
-
content.append(child);
|
|
54
|
-
if (!nativeMode && border === 'none') {
|
|
55
|
-
form.leftData = 700;
|
|
56
|
-
form.topData = 600;
|
|
57
|
-
await nextTick();
|
|
58
|
-
assert.deepEqual([wrap.style.left, wrap.style.top], ['700px', '600px']);
|
|
59
|
-
}
|
|
60
|
-
for (const focused of [false, true]) {
|
|
61
|
-
form.formFocus = focused;
|
|
62
|
-
form.stateMaxData = focused;
|
|
63
|
-
await nextTick();
|
|
64
|
-
for (const el of [wrap, inner, content, header].filter(Boolean)) {
|
|
65
|
-
assert.equal(el.style.getPropertyPriority('border-radius'), nativeFirst ? 'important' : '');
|
|
66
|
-
assert.equal(dom.window.getComputedStyle(el).borderRadius, nativeFirst ? '0' : '24px');
|
|
67
|
-
}
|
|
68
|
-
for (const property of ['border', 'box-shadow']) {
|
|
69
|
-
assert.equal(wrap.style.getPropertyPriority(property), nativeFirst ? 'important' : '');
|
|
70
|
-
}
|
|
71
|
-
assert.equal(dom.window.getComputedStyle(wrap).boxShadow, nativeFirst ? 'none' : '0 0 8px black');
|
|
72
|
-
assert.equal(dom.window.getComputedStyle(wrap).borderTopWidth, nativeFirst ? '0px' : '3px');
|
|
73
|
-
assert.equal(dom.window.getComputedStyle(child).borderRadius, '12px');
|
|
74
|
-
assert.equal(dom.window.getComputedStyle(child).borderTopWidth, '1px');
|
|
75
|
-
}
|
|
76
|
-
app.unmount();
|
|
77
|
-
host.remove();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
console.log('Native first-Form styling, theme overrides and ordinary Form isolation checks passed.');
|
package/test/form-native.mjs
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// Run after TypeScript compilation: node --experimental-vm-modules test/form-native.mjs
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { SourceTextModule, SyntheticModule } from 'node:vm';
|
|
5
|
-
|
|
6
|
-
const listeners = {};
|
|
7
|
-
const minimums = [];
|
|
8
|
-
let ended = 0;
|
|
9
|
-
let cancelled = true;
|
|
10
|
-
class AbstractControl {
|
|
11
|
-
constructor() {
|
|
12
|
-
this.parent = { controlName: 'root', isNativeNoFrameFirst: true };
|
|
13
|
-
this.formId = 'form';
|
|
14
|
-
this.element = {};
|
|
15
|
-
this.watches = {};
|
|
16
|
-
}
|
|
17
|
-
watch(name, callback, options) {
|
|
18
|
-
this.watches[name] = callback;
|
|
19
|
-
if (options?.immediate) { callback(); }
|
|
20
|
-
}
|
|
21
|
-
propInt(name) { return parseInt(this.props[name]); }
|
|
22
|
-
propBoolean(name) { return Boolean(this.props[name]); }
|
|
23
|
-
emit(name, event) {
|
|
24
|
-
if (name === 'close') {
|
|
25
|
-
assert.equal(event.detail.event, null);
|
|
26
|
-
if (cancelled) { event.preventDefault(); }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const clickgo = new SyntheticModule(['control', 'native', 'dom', 'tool', 'task'], function() {
|
|
31
|
-
this.setExport('control', { AbstractControl });
|
|
32
|
-
this.setExport('native', {
|
|
33
|
-
on(current, name, handler) { listeners[name] = handler; },
|
|
34
|
-
async minSize(current, w, h) { minimums.push([w, h]); },
|
|
35
|
-
async maximizable() {},
|
|
36
|
-
});
|
|
37
|
-
this.setExport('dom', { watchSize() {} });
|
|
38
|
-
this.setExport('tool', { getBoolean: Boolean });
|
|
39
|
-
this.setExport('task', { async end() { ++ended; } });
|
|
40
|
-
});
|
|
41
|
-
const mod = new SourceTextModule(await readFile(new URL('../dist/sources/control/form/code.js', import.meta.url), 'utf8'));
|
|
42
|
-
await mod.link(() => clickgo);
|
|
43
|
-
await mod.evaluate();
|
|
44
|
-
const Form = mod.namespace.default;
|
|
45
|
-
const form = new Form();
|
|
46
|
-
form.onMounted();
|
|
47
|
-
assert.deepEqual(minimums.at(-1), [200, 100]);
|
|
48
|
-
form.props.minWidth = '360';
|
|
49
|
-
form.watches.minWidth();
|
|
50
|
-
assert.deepEqual(minimums.at(-1), [360, 100]);
|
|
51
|
-
form.props.minHeight = 240;
|
|
52
|
-
form.watches.minHeight();
|
|
53
|
-
assert.deepEqual(minimums.at(-1), [360, 240]);
|
|
54
|
-
listeners['close-request']();
|
|
55
|
-
assert.equal(ended, 0);
|
|
56
|
-
cancelled = false;
|
|
57
|
-
listeners['close-request']();
|
|
58
|
-
assert.equal(ended, 1);
|
|
59
|
-
form.props.close = false;
|
|
60
|
-
listeners['close-request']();
|
|
61
|
-
assert.equal(ended, 1);
|
|
62
|
-
|
|
63
|
-
const web = new Form();
|
|
64
|
-
web.parent.isNativeNoFrameFirst = false;
|
|
65
|
-
const before = minimums.length;
|
|
66
|
-
web.onMounted();
|
|
67
|
-
assert.equal(minimums.length, before);
|
|
68
|
-
console.log('Form native minimum-size and cancellable close-event checks passed.');
|
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|