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,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.');
@@ -1,212 +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
- import { clone, getClassPrototype } from '../dist/lib/tool.js';
7
-
8
- const dom = new JSDOM('<!doctype html><html><body></body></html>');
9
- for (const name of ['window', 'document', 'Element', 'HTMLElement', 'SVGElement', 'Node']) {
10
- Object.defineProperty(globalThis, name, { configurable: true, value: dom.window[name] });
11
- }
12
- const { createApp, h, nextTick, reactive } = await import('vue');
13
- const errors = [];
14
- const editors = [];
15
- const menus = [];
16
- let popCount = 0;
17
- let moduleCount = 0;
18
- let loadModule;
19
-
20
- class AbstractControl {
21
- get refs() { return this.$refs; }
22
- get locale() { return 'en'; }
23
- watch(name, callback, options) { return this.$watch(() => this.props[name], callback, options); }
24
- emit(...args) { this.$emit(...args); }
25
- propBoolean(name) { return this.props[name] === true; }
26
- }
27
- const module = {
28
- make(element, options) {
29
- assert.ok(element instanceof HTMLElement && element.isConnected, 'Jodit requires a live host');
30
- const callbacks = {};
31
- const editor = {
32
- value: '', text: 'plain text', container: element.parentElement,
33
- options, callbacks, commands: [], images: [], destructCount: 0,
34
- events: { on(name, callback) { callbacks[name] = callback; } },
35
- setReadOnly(value) { this.readonly = value; },
36
- execCommand(value) { this.commands.push(value); },
37
- selection: { insertImage(...args) { editor.images.push(args); } },
38
- destruct() {
39
- assert.equal(element.isConnected, true, 'release the editor before Vue removes its DOM');
40
- ++this.destructCount;
41
- callbacks.change?.();
42
- },
43
- };
44
- editors.push(editor);
45
- return editor;
46
- },
47
- };
48
- const clickgo = {
49
- control: { AbstractControl },
50
- core: { getModule() { ++moduleCount; return loadModule(); } },
51
- dom: { findParentByClass() { return null; } },
52
- form: { hidePop() {}, showPop() { ++popCount; } },
53
- modules: { pointer: { menu(event, callback) { menus.push(callback); } } },
54
- };
55
- const source = await readFile(new URL('../dist/sources/control/jodit/code.ts', import.meta.url), 'utf8');
56
- const exports = {};
57
- vm.runInNewContext(ts.transpileModule(source, {
58
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
59
- }).outputText, { exports, require: () => clickgo, HTMLElement });
60
- const instance = new exports.default();
61
- const prototype = getClassPrototype(instance);
62
- const controls = [];
63
- const Control = {
64
- template: '<div><div ref="content"><textarea ref="editor"></textarea><div class="jodit-workplace"></div></div><div ref="pop"></div></div>',
65
- props: Object.fromEntries(Object.entries(instance.props).map(([name, value]) => [name, {
66
- default: () => clone(value),
67
- }])),
68
- emits: instance.emits,
69
- data: () => Object.fromEntries(Object.entries(new exports.default()).filter(([name]) =>
70
- !['props', 'emits', 'access'].includes(name))),
71
- computed: prototype.access,
72
- methods: prototype.method,
73
- created() {
74
- this.props = this.$props;
75
- this.access = clone(instance.access);
76
- controls.push(this);
77
- },
78
- // Mirror buildComponents: its mounted hook defers the control hook by one Vue tick.
79
- async mounted() { await this.$nextTick(); await this.onMounted(); },
80
- beforeUnmount: prototype.method.onBeforeUnmount,
81
- };
82
-
83
- /** --- Drain both Vue mounting and asynchronous module continuations. --- */
84
- async function flush() {
85
- for (let i = 0; i < 8; ++i) await nextTick();
86
- assert.deepEqual(errors, [], 'Jodit lifecycle must not produce runtime errors');
87
- }
88
-
89
- /** --- Mount independent instances with the framework's per-instance access storage. --- */
90
- function mount(items) {
91
- const state = reactive({ items });
92
- const events = [];
93
- const host = document.createElement('div');
94
- document.body.append(host);
95
- const app = createApp({
96
- render: () => h('div', state.items.map(item => h(Control, {
97
- key: item.id,
98
- modelValue: item.value ?? '', readonly: item.readonly ?? false,
99
- 'onUpdate:modelValue': value => events.push(['value', value]),
100
- onText: value => events.push(['text', value]),
101
- onInit: editor => events.push(['init', editor]),
102
- onImgselect: callback => events.push(['image', callback]),
103
- }))),
104
- });
105
- app.config.errorHandler = error => errors.push(error);
106
- app.mount(host);
107
- return { state, events, host, unmount() { app.unmount(); host.remove(); } };
108
- }
109
-
110
- // A parent can replace the temporary editor before the deferred control hook even starts.
111
- loadModule = () => Promise.resolve(module);
112
- let view = mount([{ id: 'temporary' }]);
113
- view.state.items = [];
114
- await flush();
115
- assert.equal(moduleCount, 0);
116
- assert.equal(editors.length, 0);
117
- view.unmount();
118
-
119
- // Module resolution after unmount must neither initialize Jodit nor change fallback state.
120
- for (const result of [module, null]) {
121
- let release;
122
- loadModule = () => new Promise(resolve => { release = resolve; });
123
- view = mount([{ id: 'pending' }]);
124
- await flush();
125
- const control = controls.at(-1);
126
- view.state.items = [];
127
- await flush();
128
- release(result);
129
- await flush();
130
- assert.equal(editors.length, 0);
131
- assert.equal(control.notInit, false);
132
- assert.deepEqual(view.events, []);
133
- view.unmount();
134
- }
135
-
136
- // Re-rendering one editor while a shared module loads must preserve its surviving sibling.
137
- let release;
138
- const pending = new Promise(resolve => { release = resolve; });
139
- loadModule = () => pending;
140
- view = mount([{ id: 'temporary' }, { id: 'live', value: 'before' }]);
141
- await flush();
142
- const removed = controls.at(-2);
143
- const live = controls.at(-1);
144
- assert.notEqual(removed.access, live.access);
145
- view.state.items.splice(0, 1);
146
- view.state.items[0].value = 'after';
147
- view.state.items[0].readonly = true;
148
- await flush();
149
- release(module);
150
- await flush();
151
- assert.equal(editors.length, 1);
152
- const editor = editors[0];
153
- assert.equal(live.access.editor, editor);
154
- assert.equal(editor.value, 'after');
155
- assert.equal(editor.readonly, true);
156
- view.state.items[0].value = 'updated';
157
- view.state.items[0].readonly = false;
158
- await flush();
159
- assert.equal(editor.value, 'updated');
160
- assert.equal(editor.readonly, false);
161
- editor.callbacks.change();
162
- assert.ok(view.events.some(event => event[0] === 'value' && event[1] === 'updated'));
163
- editor.callbacks.focus();
164
- assert.equal(live.isFocus, true);
165
- editor.callbacks.blur();
166
- assert.equal(live.isFocus, false);
167
- live.execCmd('copy');
168
- assert.deepEqual(editor.commands, ['copy']);
169
- editor.options.extraButtons[0].exec();
170
- const insertImage = view.events.find(event => event[0] === 'image')[1];
171
- const workplace = view.host.querySelector('.jodit-workplace');
172
- workplace.dispatchEvent(new dom.window.Event('pointerdown', { bubbles: true }));
173
- assert.equal(menus.length, 1);
174
- const eventCount = view.events.length;
175
- view.unmount();
176
- await flush();
177
- assert.equal(editor.destructCount, 1);
178
- assert.equal(live.access.editor, undefined);
179
- assert.equal(view.events.length, eventCount, 'destruction must not emit a model update');
180
- insertImage('image.png');
181
- menus[0]();
182
- workplace.dispatchEvent(new dom.window.Event('pointerdown', { bubbles: true }));
183
- live.execCmd('paste');
184
- live.onBeforeUnmount();
185
- assert.equal(editor.destructCount, 1, 'cleanup is idempotent');
186
- assert.deepEqual(editor.images, []);
187
- assert.equal(popCount, 0);
188
- assert.equal(menus.length, 1, 'remove the menu listener at unmount');
189
-
190
- // A cached module still resolves asynchronously on every later entry.
191
- loadModule = async () => { await Promise.resolve(); return module; };
192
- for (let i = 0; i < 20; ++i) {
193
- view = mount([{ id: 'entry', value: `entry ${i}` }]);
194
- await flush();
195
- view.state.items = [{ id: 'replacement', value: `replacement ${i}` }];
196
- await flush();
197
- view.unmount();
198
- await flush();
199
- }
200
- assert.equal(editors.length, 41);
201
- assert.ok(editors.every(item => item.destructCount === 1));
202
-
203
- // A genuine module load failure still displays the existing fallback.
204
- loadModule = () => Promise.resolve(null);
205
- view = mount([{ id: 'failure' }]);
206
- await flush();
207
- assert.equal(controls.at(-1).notInit, true);
208
- assert.equal(controls.at(-1).isLoading, false);
209
- view.unmount();
210
- await flush();
211
- dom.window.close();
212
- console.log('Jodit deferred mounting, late module resolution, instance isolation, repeated entry and editor cleanup passed.');