clickgo 6.8.2 → 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 (58) 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 +8 -1
  6. package/dist/control/common.cgc +0 -0
  7. package/dist/control/form.cgc +0 -0
  8. package/dist/control/novnc.cgc +0 -0
  9. package/dist/control/task.cgc +0 -0
  10. package/dist/global.css +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/lib/command/native.d.ts +18 -0
  13. package/dist/lib/command/palette.d.ts +10 -0
  14. package/dist/lib/command/schema.d.ts +31 -0
  15. package/dist/lib/command.d.ts +218 -0
  16. package/dist/lib/core.d.ts +7 -1
  17. package/dist/lib/form.d.ts +15 -2
  18. package/dist/lib/native.d.ts +12 -0
  19. package/dist/lib/task.d.ts +2 -0
  20. package/dist/theme/admin.cgt +0 -0
  21. package/dist/theme/classic.cgt +0 -0
  22. package/dist/theme/cyber.cgt +0 -0
  23. package/dist/theme/dark.cgt +0 -0
  24. package/dist/theme/light.cgt +0 -0
  25. package/dist/theme/modern-light.cgt +0 -0
  26. package/dist/theme/modern.cgt +0 -0
  27. package/dist/theme/software-dark.cgt +0 -0
  28. package/dist/theme/software-light.cgt +0 -0
  29. package/doc/clickgo-rag.md +2735 -711
  30. package/package.json +11 -4
  31. package/test/core-module-loading.mjs +0 -169
  32. package/test/desktop.mjs +0 -715
  33. package/test/dock-lifecycle.mjs +0 -280
  34. package/test/dom-style.mjs +0 -261
  35. package/test/form-launcher.mjs +0 -102
  36. package/test/form-modal-layer.mjs +0 -75
  37. package/test/form-native-regions.mjs +0 -115
  38. package/test/form-native-style.mjs +0 -80
  39. package/test/form-native.mjs +0 -68
  40. package/test/form-notify.mjs +0 -90
  41. package/test/form-viewport.mjs +0 -127
  42. package/test/iconview.mjs +0 -70
  43. package/test/jodit-lifecycle.mjs +0 -212
  44. package/test/locale-integrity.mjs +0 -189
  45. package/test/pop-max-size.mjs +0 -66
  46. package/test/rtl-archive.mjs +0 -428
  47. package/test/rtl-direction.mjs +0 -193
  48. package/test/scroll.mjs +0 -145
  49. package/test/select.mjs +0 -885
  50. package/test/sidebar-toggle.mjs +0 -75
  51. package/test/task-package-loading.mjs +0 -94
  52. package/test/task-tray.mjs +0 -331
  53. package/test/tool-assets.mjs +0 -64
  54. package/test/tool-clone.mjs +0 -42
  55. package/test/toolbox.mjs +0 -88
  56. package/test/tuieditor-locale.mjs +0 -175
  57. package/test/vflow.mjs +0 -122
  58. package/test/web-drag-mask.mjs +0 -96
@@ -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.');
@@ -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,90 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { readFile } from 'node:fs/promises';
3
- import vm from 'node:vm';
4
- import { JSDOM } from 'jsdom';
5
- import ts from 'typescript';
6
-
7
- const dom = new JSDOM('<!doctype html><div id="notify"></div>');
8
- const { document } = dom.window;
9
- const notify = document.getElementById('notify');
10
- Object.defineProperty(dom.window.HTMLElement.prototype, 'offsetHeight', {
11
- get() { return Number(this.dataset.height ?? 96); },
12
- });
13
- const frames = [];
14
- const timers = new Map();
15
- const observed = new Set();
16
- let timerId = 0;
17
- let observerCallback;
18
- let area = { left: 0, top: 0, width: 800, height: 600 };
19
- const core = { config: { locale: 'en' }, getAvailArea: () => area };
20
- const source = await readFile(new URL('../dist/lib/form.ts', import.meta.url), 'utf8');
21
- const start = source.indexOf('let notifyId:');
22
- const end = source.indexOf('export function appendToPop', start);
23
- const api = {};
24
- vm.runInNewContext(ts.transpileModule(source.slice(start, end), {
25
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
26
- }).outputText, {
27
- exports: api, document, elements: { notify }, lCore: core,
28
- lTool: { lang: { getDirection: locale => locale === 'ar' ? 'rtl' : 'ltr' }, escapeHTML: value => value },
29
- window: { innerWidth: 800, innerHeight: 600, setTimeout: schedule },
30
- setTimeout: schedule, clearTimeout: id => timers.delete(id),
31
- requestAnimationFrame: cb => frames.push(cb),
32
- ResizeObserver: class {
33
- constructor(callback) { observerCallback = callback; }
34
- observe(el) { observed.add(el); }
35
- unobserve(el) { observed.delete(el); }
36
- },
37
- });
38
- function schedule(callback, delay) { const id = ++timerId; timers.set(id, { callback, delay }); return id; }
39
- function frame() { frames.splice(0).forEach(callback => callback()); }
40
- function remove() {
41
- for (const [id, timer] of [...timers]) {
42
- if (timer.delay === 100) { timers.delete(id); timer.callback(); }
43
- }
44
- }
45
- function position(el) { return [...el.style.transform.matchAll(/\((-?[\d.]+)px\)/g)].map(match => Number(match[1])); }
46
- const firstId = api.notify({ title: 'Loading', content: 'Task', note: 'Start', timeout: 0 });
47
- const first = notify.children[0];
48
- area.height = 552; // Task bar appears before the enter frame.
49
- api.refreshNotifyPosition();
50
- frame();
51
- assert.deepEqual(position(first), [-58, -10]);
52
- const secondId = api.notify({ title: 'Loading', content: 'Demo', note: 'Start', timeout: 0 });
53
- const second = notify.children[1];
54
- frame();
55
- assert.deepEqual(position(second), [-164, -10], 'notifications share the current task bar offset');
56
- first.dataset.height = '128.5';
57
- api.notifyContent(firstId, { note: 'Wrapped content' });
58
- assert.deepEqual(position(second), [-196.5, -10], 'content growth uses actual fractional height');
59
- first.dataset.height = '146';
60
- observerCallback();
61
- assert.deepEqual(position(second), [-214, -10], 'size observer also restacks notifications');
62
- area = { left: 0, top: 0, width: 750, height: 600 };
63
- api.refreshNotifyPosition();
64
- assert.deepEqual(position(first), [-10, -60], 'right bar moves the existing stack left');
65
- core.config.locale = 'ar';
66
- area = { left: 50, top: 0, width: 750, height: 600 };
67
- api.refreshNotifyPosition();
68
- assert.deepEqual(position(first), [-10, 60], 'RTL avoids the left task bar');
69
- area = { left: 0, top: 50, width: 800, height: 550 };
70
- api.refreshNotifyPosition();
71
- assert.deepEqual(position(first), [-10, 10], 'top bar does not push the bottom anchor');
72
- api.hideNotify(firstId);
73
- api.hideNotify(firstId);
74
- assert.equal([...timers.values()].filter(timer => timer.delay === 100).length, 1, 'duplicate hides are idempotent');
75
- remove();
76
- assert.deepEqual(position(second), [-10, 10], 'removing a notification closes its space');
77
- assert.equal(observed.has(first), false, 'removed notifications release size subscriptions');
78
- api.hideNotify(secondId); remove();
79
- const earlyId = api.notify({ content: 'Close before entering' });
80
- api.hideNotify(earlyId); frame();
81
- assert.equal(notify.children[0].style.opacity, '0', 'pending enter cannot resurrect a closing notification');
82
- remove();
83
- assert.equal(notify.children.length, 0);
84
- assert.equal(observed.size, 0);
85
- assert.equal(timers.size, 0, 'no auto-hide timer survives an early hide');
86
- const next = api.notify({ content: 'Fresh stack' }); frame();
87
- assert.deepEqual(position(notify.children[0]), [-10, 10], 'empty stack leaves no stale offsets');
88
- api.hideNotify(next); remove();
89
- dom.window.close();
90
- console.log('Notify task bar appearance/movement, RTL, height changes, stacking and hide races passed.');
@@ -1,127 +0,0 @@
1
- // Run: node test/form-viewport.mjs
2
- import assert from 'node:assert/strict';
3
- import { readFile } from 'node:fs/promises';
4
- import vm from 'node:vm';
5
- import ts from 'typescript';
6
-
7
- let nativeMode = false;
8
- let area = { left: 0, top: 0, width: 640, height: 400, owidth: 640, oheight: 440 };
9
- const bool = value => value === true || value === '' || value === 'true';
10
- const clickgo = {
11
- isNative: () => nativeMode,
12
- control: {
13
- AbstractControl: class {
14
- constructor() { this.rootForm = { bottomMost: false }; this.parent = { controlName: 'root' }; this.watches = {}; this.events = []; }
15
- emit(...event) { this.events.push(event); }
16
- propBoolean(name) { return bool(this.props[name]); }
17
- watch(name, callback, options) { this.watches[name] = callback; if(options?.immediate) callback(); }
18
- propInt(name) { return Number(this.props[name]); }
19
- trigger() { return Promise.resolve(); }
20
- },
21
- },
22
- core: { getAvailArea: () => area },
23
- dom: { watchSize() {} },
24
- native: {
25
- restore: async () => {},
26
- size: async () => {},
27
- },
28
- tool: { sleep: () => Promise.resolve(), getBoolean: bool },
29
- };
30
- const source = await readFile(new URL('../dist/sources/control/form/code.ts', import.meta.url), 'utf8');
31
- const compiled = ts.transpileModule(source, {
32
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
33
- }).outputText;
34
- const exports = {};
35
- vm.runInNewContext(compiled, {
36
- exports, require: () => clickgo,
37
- window: { innerWidth: 640, innerHeight: 440 },
38
- });
39
- const Form = exports.default;
40
-
41
- function restore(history, mode = 'web') {
42
- nativeMode = mode !== 'web';
43
- const form = new Form();
44
- form.isNativeNoFrameFirst = mode === 'native-frameless';
45
- form.stateMaxData = true;
46
- form.historyLocation = history;
47
- form.element = { style: {}, removeAttribute() {} };
48
- form.maxMethod();
49
- return [form.widthData, form.heightData, form.leftData, form.topData];
50
- }
51
-
52
- assert.deepEqual(restore({ width: 900, height: 700, left: 500, top: 300 }), [900, 700, 0, 0]);
53
- assert.deepEqual(restore({ width: 400, height: 300, left: 500, top: 300 }), [400, 300, 240, 140]);
54
- assert.deepEqual(restore({ width: 600, height: 40, left: 40, top: 400 }), [600, 40, 40, 400]);
55
- assert.deepEqual(restore({ width: 900, height: 700, left: 500, top: 300 }, 'native-framed'), [900, 700, 500, 300]);
56
- assert.deepEqual(restore({ width: 900, height: 700, left: 500, top: 300 }, 'native-frameless'), [900, 700, 500, 300]);
57
- console.log('Form restore stays visible, edge-positioned Forms stay anchored, and Native geometry is unchanged.');
58
-
59
-
60
- nativeMode = false;
61
- function create(viewport = false, initialMax = false) {
62
- const form = new Form();
63
- form.props.viewport = viewport;
64
- form.props.stateMax = initialMax;
65
- form.props.padding = 'safe';
66
- form.element = { style: {}, dataset: {}, offsetWidth: 300, offsetHeight: 200, removeAttribute() {} };
67
- form.onMounted();
68
- if (!initialMax) form.maxMethod();
69
- return form;
70
- }
71
- function rect(form) { return [form.leftData, form.topData, form.widthData, form.heightData]; }
72
- const full = create(true, true);
73
- assert.deepEqual(rect(full), [0, 0, 640, 440], 'initial state-max uses the full viewport');
74
- const history = JSON.stringify(full.historyLocation);
75
- for (const a of [
76
- { left: 0, top: 0, width: 640, height: 400, owidth: 640, oheight: 440, padding: '0px 0px 40px 0px' },
77
- { left: 60, top: 0, width: 580, height: 440, owidth: 640, oheight: 440, padding: '0px 0px 0px 60px' },
78
- { left: 0, top: 40, width: 640, height: 400, owidth: 640, oheight: 440, padding: '40px 0px 0px 0px' },
79
- { left: 0, top: 0, width: 580, height: 440, owidth: 640, oheight: 440, padding: '0px 60px 0px 0px' },
80
- { left: 0, top: 0, width: 800, height: 520, owidth: 800, oheight: 560, padding: '0px 0px 40px 0px' },
81
- { left: 0, top: 0, width: 800, height: 560, owidth: 800, oheight: 560, padding: '0px 0px 0px 0px' }
82
- ]) {
83
- area = a;
84
- full.refreshMaxPosition();
85
- assert.deepEqual(rect(full), [0, 0, a.owidth, a.oheight]);
86
- assert.equal(full.contentPadding, a.padding);
87
- assert.equal(JSON.stringify(full.historyLocation), history, 'refresh never overwrites restore geometry');
88
- }
89
- area = { left: 60, top: 0, width: 580, height: 440, owidth: 640, oheight: 440 };
90
- full.props.viewport = false;
91
- full.watches.viewport();
92
- assert.deepEqual(rect(full), [60, 0, 580, 440]);
93
- assert.equal(full.contentPadding, '0px 0px 0px 0px', 'available-area maximization does not reserve the taskbar twice');
94
- full.props.viewport = 'true';
95
- full.watches.viewport();
96
- assert.deepEqual(rect(full), [0, 0, 640, 440]);
97
- full.maxMethod();
98
- assert.equal(full.contentPadding, undefined, 'safe padding is removed when restoring');
99
- full.props.padding = '4 8';
100
- assert.equal(full.contentPadding, '4px 8px', 'numeric padding is unchanged');
101
- const normal = create();
102
- assert.deepEqual(rect(normal), [60, 0, 580, 440], 'default maximization still avoids the taskbar');
103
- const bottom = create();
104
- bottom.rootForm.bottomMost = true;
105
- bottom.refreshMaxPosition();
106
- assert.deepEqual(rect(bottom), [60, 0, 580, 440], 'bottomMost alone does not change the layout boundary');
107
- assert.equal(bottom.contentPadding, '0px 0px 0px 0px');
108
- bottom.props.viewport = true;
109
- bottom.watches.viewport();
110
- assert.deepEqual(rect(bottom), [0, 0, 640, 440], 'bottomMost uses the full viewport only when explicitly enabled');
111
- assert.equal(bottom.contentPadding, '0px 0px 0px 60px');
112
- bottom.props.viewport = false;
113
- bottom.watches.viewport();
114
- assert.deepEqual(rect(bottom), [60, 0, 580, 440], 'disabling viewport also restores taskbar exclusion for bottomMost');
115
- const inline = create();
116
- inline.isInside = true;
117
- const previous = rect(inline);
118
- inline.props.viewport = true;
119
- inline.refreshMaxPosition();
120
- assert.deepEqual(rect(inline), previous, 'inline Forms do not resize to the viewport');
121
- const automatic = create();
122
- automatic.props.width = 0;
123
- automatic.props.height = 0;
124
- automatic.events = [];
125
- automatic.refreshMaxPosition();
126
- assert.ok(automatic.events.every(([name]) => !['update:width', 'update:height'].includes(name)));
127
- console.log('Form viewport bounds, four taskbar edges, safe padding, live switching, restore and bottomMost layout independence passed.');
package/test/iconview.mjs DELETED
@@ -1,70 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { readFile } from 'node:fs/promises';
3
- import { SourceTextModule, SyntheticModule } from 'node:vm';
4
-
5
- class AbstractControl {
6
- element = { dir: 'ltr' };
7
- emitted = [];
8
- propInt(name) { return Number(this.props[name]); }
9
- propBoolean(name) { return Boolean(this.props[name]); }
10
- emit(name, value) { this.emitted.push([name, value]); }
11
- }
12
- const clickgo = new SyntheticModule(['control', 'dom', 'tool'], function() {
13
- this.setExport('control', { AbstractControl });
14
- this.setExport('dom', { isRtl: element => element.dir === 'rtl' });
15
- this.setExport('tool', { clone: structuredClone });
16
- });
17
- const module = new SourceTextModule(await readFile('dist/sources/control/iconview/code.js', 'utf8'));
18
- await module.link(() => clickgo);
19
- await module.evaluate();
20
- const view = new module.namespace.default();
21
- view.props.data = Array.from({ length: 8 }, (_, i) => ({ name: 'item-' + i }));
22
-
23
- // Resizing crosses column thresholds without stretching existing cells.
24
- for (const [width, columns] of [[360, 2], [420, 2], [539, 2], [540, 3], [620, 3]]) {
25
- view.clientwidth(width);
26
- assert.equal(view.rowCount, columns);
27
- assert.equal(view.cellWidth, 180);
28
- assert.equal(view.dataComp[0].length, columns);
29
- }
30
- view.props.size = 64;
31
- assert.equal(view.cellWidth, 144);
32
- assert.equal(view.rowCount, 4);
33
- view.clientwidth(100);
34
- assert.equal(view.cellWidth, 100);
35
- assert.equal(view.rowCount, 1);
36
- view.clientwidth(0);
37
- assert.ok(Number.isFinite(view.rowCount));
38
- assert.equal(view.rowCount, 1);
39
- view.props.size = 100;
40
- view.clientwidth(500);
41
-
42
- function box(x, width, modifiers = {}) {
43
- view.onBeforeSelect();
44
- view.onSelect({ x, width, y: 0, height: 100, start: 0, end: 0, empty: false, ...modifiers });
45
- return [...view.valueData];
46
- }
47
-
48
- // Column hit testing uses actual fixed cells; trailing whitespace selects nothing.
49
- assert.deepEqual(box(190, 10), [1]);
50
- assert.deepEqual(box(370, 80), []);
51
- assert.equal(view.emitted.at(-1)[1].detail.area.empty, true);
52
- assert.deepEqual(box(170, 200), [0, 1]);
53
- assert.deepEqual(box(0, 20), [0]);
54
- assert.deepEqual(box(370, 50, { ctrl: true }), [0]);
55
- assert.deepEqual(box(190, 20, { shift: true }), [0, 1]);
56
- view.arrowDown();
57
- assert.ok(view.valueData.every(index => index >= 0 && index < 8));
58
-
59
- // RTL starts at the right edge, leaving unused space at the left edge.
60
- view.element.dir = 'rtl';
61
- assert.deepEqual(box(10, 50), []);
62
- assert.deepEqual(box(450, 20), [0]);
63
- assert.deepEqual(box(280, 20), [1]);
64
- assert.deepEqual(box(280, 200), [0, 1]);
65
- view.props.multi = false;
66
- assert.deepEqual(box(280, 20), [1]);
67
- assert.deepEqual(box(10, 50), [1]);
68
- view.props.data = [];
69
- assert.deepEqual(view.dataComp, []);
70
- console.log('Iconview fixed cells, column thresholds, size changes, narrow widths, LTR/RTL selection and modifiers passed.');