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,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.');
@@ -1,189 +0,0 @@
1
- // Run: node --experimental-vm-modules test/locale-integrity.mjs
2
- import assert from 'node:assert/strict';
3
- import { access, readdir, readFile } from 'node:fs/promises';
4
- import { SourceTextModule } from 'node:vm';
5
- import { JSDOM } from 'jsdom';
6
- import ts from 'typescript';
7
-
8
- /** --- ClickGo 的固定内置语言顺序 --- */
9
- const codes = [
10
- 'sc', 'tc', 'ja', 'ko', 'th', 'vi', 'ar', 'id',
11
- 'en', 'es', 'de', 'fr', 'pt', 'ru', 'it', 'tr',
12
- ];
13
-
14
- /** --- 仍使用问号占位符、会在运行时被替换的语言键 --- */
15
- const placeholderKeys = new Set([
16
- 'dist/lib/fs.ts:apply-unmount',
17
- 'dist/lib/storage.ts:sure-clear',
18
- 'dist/sources/control/page/code.ts:total-of',
19
- ]);
20
-
21
- /**
22
- * --- 获取对象字面量的静态属性名 ---
23
- * @param property TypeScript 属性节点
24
- * @returns 属性名;动态属性返回空字符串
25
- */
26
- function getPropertyName(property) {
27
- if (!ts.isPropertyAssignment(property) || !property.name) {
28
- return '';
29
- }
30
- if (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) || ts.isNumericLiteral(property.name)) {
31
- return property.name.text;
32
- }
33
- return '';
34
- }
35
-
36
- /**
37
- * --- 从对象字面量取静态属性 ---
38
- * @param object 对象字面量
39
- * @param key 属性名
40
- * @returns 属性初始化节点
41
- */
42
- function getProperty(object, key) {
43
- return object.properties.find((property) => getPropertyName(property) === key)?.initializer;
44
- }
45
-
46
- /**
47
- * --- 断言翻译值是非空静态字符串 ---
48
- * @param value 翻译的 AST 节点
49
- * @param message 失败信息
50
- */
51
- function assertText(value, message) {
52
- const isText = ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value);
53
- assert.equal(isText, true, `${message} must be a static string literal`);
54
- assert.notEqual(value.text.trim(), '', `${message} must not be empty`);
55
- }
56
-
57
- /**
58
- * --- 从 TypeScript 源码收集含英文条目的语言表 ---
59
- * @param path TypeScript 文件路径
60
- * @returns 语言表及其行号
61
- */
62
- async function collectLocaleTables(path) {
63
- const source = ts.createSourceFile(path, await readFile(path, 'utf8'), ts.ScriptTarget.Latest, true);
64
- const tables = [];
65
- const visit = (node) => {
66
- if (ts.isObjectLiteralExpression(node) && getProperty(node, 'en')) {
67
- const keys = node.properties.map(getPropertyName);
68
- if (codes.every((code) => keys.includes(code))) {
69
- tables.push({
70
- 'node': node,
71
- 'line': source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
72
- });
73
- }
74
- }
75
- ts.forEachChild(node, visit);
76
- };
77
- visit(source);
78
- return tables;
79
- }
80
-
81
- const dom = new JSDOM('<!doctype html><html><head></head><body></body></html>');
82
- for (const name of ['window', 'document', 'navigator']) {
83
- Object.defineProperty(globalThis, name, {
84
- 'configurable': true,
85
- 'value': dom.window[name],
86
- });
87
- }
88
-
89
- // The language registry is executed from TypeScript source, not generated JavaScript.
90
- const toolSource = await readFile(new URL('../dist/lib/tool.ts', import.meta.url), 'utf8');
91
- const toolModule = new SourceTextModule(ts.transpileModule(toolSource, {
92
- 'compilerOptions': {
93
- 'target': ts.ScriptTarget.ES2022,
94
- 'module': ts.ModuleKind.ESNext,
95
- },
96
- }).outputText);
97
- await toolModule.link(() => {
98
- throw new Error('tool.ts must not import runtime modules');
99
- });
100
- await toolModule.evaluate();
101
-
102
- const { lang } = toolModule.namespace;
103
- assert.deepEqual(lang.codes, codes, 'language codes must retain all 16 supported locales');
104
- assert.equal(lang.names.length, codes.length, 'every locale must have a display name');
105
- assert.equal(new Set(lang.names).size, codes.length, 'language display names must be unique');
106
- assert.deepEqual(Object.keys(lang.tags).sort(), [...codes].sort(), 'every locale must have an HTML lang tag');
107
- for (const code of codes) {
108
- assert.notEqual(lang.names[lang.codes.indexOf(code)].trim(), '', `${code} must have a non-empty display name`);
109
- assert.notEqual(lang.getTag(code).trim(), '', `${code} must resolve to a non-empty HTML lang tag`);
110
- assert.equal(lang.getDirection(code), code === 'ar' ? 'rtl' : 'ltr', `${code} must resolve to its writing direction`);
111
- }
112
- assert.equal(lang.getDirection('ar-EG'), 'rtl', 'Arabic HTML tags must resolve to RTL');
113
- assert.equal(lang.getDirection('en-US'), 'ltr', 'non-Arabic HTML tags must resolve to LTR');
114
-
115
- const controlRoot = new URL('../dist/sources/control/', import.meta.url);
116
- const controlNames = (await readdir(controlRoot, { 'withFileTypes': true }))
117
- .filter((item) => item.isDirectory())
118
- .map((item) => item.name)
119
- .sort();
120
- const controlSources = [];
121
- for (const name of controlNames) {
122
- const path = `dist/sources/control/${name}/code.ts`;
123
- try {
124
- await access(path);
125
- controlSources.push(path);
126
- }
127
- catch {}
128
- }
129
- const localeSources = [
130
- 'dist/lib/form.ts',
131
- 'dist/lib/fs.ts',
132
- 'dist/lib/storage.ts',
133
- 'dist/lib/task.ts',
134
- ...controlSources,
135
- ];
136
-
137
- const tables = [];
138
- for (const path of localeSources) {
139
- for (const table of await collectLocaleTables(path)) {
140
- tables.push({ ...table, path });
141
- }
142
- }
143
- assert.equal(tables.length, 26, 'all 26 built-in TypeScript locale tables must be present');
144
- for (const { node, path, line } of tables) {
145
- const tableCodes = node.properties.map(getPropertyName);
146
- assert.deepEqual([...tableCodes].sort(), [...codes].sort(), `${path}:${line} must contain exactly 16 locale entries`);
147
- const english = getProperty(node, 'en');
148
- assert.equal(ts.isObjectLiteralExpression(english), true, `${path}:${line} English locale must be an object`);
149
- const keys = english.properties.map(getPropertyName);
150
- assert.equal(new Set(keys).size, keys.length, `${path}:${line} English locale must not repeat keys`);
151
- for (const code of codes) {
152
- const localized = getProperty(node, code);
153
- assert.equal(ts.isObjectLiteralExpression(localized), true, `${path}:${line} ${code} locale must be an object`);
154
- const localizedKeys = localized.properties.map(getPropertyName);
155
- assert.deepEqual([...localizedKeys].sort(), [...keys].sort(), `${path}:${line} ${code} locale keys must match English`);
156
- for (const key of keys) {
157
- const localizedText = getProperty(localized, key);
158
- assertText(localizedText, `${path}:${line} ${code}.${key}`);
159
- if (placeholderKeys.has(`${path}:${key}`)) {
160
- assert.equal(localizedText.text.includes('?'), true, `${path}:${line} ${code}.${key} must retain its ? placeholder`);
161
- }
162
- }
163
- }
164
- }
165
-
166
- const taskConfig = JSON.parse(await readFile(new URL('../dist/app/task/config.json', import.meta.url), 'utf8'));
167
- assert.deepEqual(Object.values(taskConfig.locales), codes, 'task app config must load all 16 locales in registry order');
168
- for (const code of codes) {
169
- assert.equal(taskConfig.locales[`/package/locale/${code}`], code, `task app must map /package/locale/${code}`);
170
- }
171
- const englishTaskLocale = JSON.parse(await readFile(new URL('../dist/app/task/locale/en.json', import.meta.url), 'utf8'));
172
- const taskKeys = Object.keys(englishTaskLocale).sort();
173
- for (const code of codes) {
174
- const locale = JSON.parse(await readFile(new URL(`../dist/app/task/locale/${code}.json`, import.meta.url), 'utf8'));
175
- assert.deepEqual(Object.keys(locale).sort(), taskKeys, `task app ${code}.json keys must match en.json`);
176
- for (const [key, value] of Object.entries(locale)) {
177
- assert.equal(typeof value, 'string', `task app ${code}.json ${key} must be a string`);
178
- assert.notEqual(value.trim(), '', `task app ${code}.json ${key} must not be empty`);
179
- }
180
- }
181
-
182
- const demoTaskLayout = await readFile(new URL('../dist/app/demo/form/method/task/task.xml', import.meta.url), 'utf8');
183
- const demoLocaleData = demoTaskLayout.match(/<label>Change global locale lang:<\/label>\s*<layout[^>]*>\s*<select[^>]*:data="\[([^"]+)\]"/)?.[1];
184
- assert.ok(demoLocaleData, 'demo Library task form must retain the global locale selector');
185
- const demoLocaleCodes = [...demoLocaleData.matchAll(/'([^']+)'/g)].map((match) => match[1]);
186
- assert.deepEqual(demoLocaleCodes, codes, 'demo global locale selector must expose all 16 locales in registry order');
187
-
188
- dom.window.close();
189
- console.log(`Locale registry, ${tables.length} TypeScript tables, 16 task app locale JSON files and the Demo selector are complete.`);
@@ -1,66 +0,0 @@
1
- // Run: node --experimental-vm-modules test/pop-max-size.mjs
2
- import assert from 'node:assert/strict';
3
- import { readFile } from 'node:fs/promises';
4
- import { SourceTextModule, SyntheticModule } from 'node:vm';
5
- import { JSDOM } from 'jsdom';
6
- import ts from 'typescript';
7
-
8
- const dom = new JSDOM('<!doctype html><style>.pop { max-width: 480px; max-height: 300px; }</style><div id="trigger"></div><div class="pop"></div>');
9
- for (const name of ['window', 'document', 'Element', 'HTMLElement', 'PointerEvent']) {
10
- Object.defineProperty(globalThis, name, {
11
- 'configurable': true,
12
- 'value': dom.window[name],
13
- });
14
- }
15
- globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
16
- Object.defineProperty(window, 'innerWidth', { 'configurable': true, 'value': 1024 });
17
- Object.defineProperty(window, 'innerHeight', { 'configurable': true, 'writable': true, 'value': 768 });
18
-
19
- let refreshSize;
20
- const domModule = new SyntheticModule(['findParentByData', 'watchSize'], function() {
21
- this.setExport('findParentByData', () => null);
22
- this.setExport('watchSize', (_id, _pop, callback) => { refreshSize = callback; });
23
- });
24
- const toolModule = new SyntheticModule(['sleep'], function() {
25
- this.setExport('sleep', () => Promise.resolve());
26
- });
27
- const empty = new SyntheticModule([], function() {});
28
- const source = await readFile(new URL('../dist/lib/form.ts', import.meta.url), 'utf8');
29
- const formModule = new SourceTextModule(ts.transpileModule(source, {
30
- 'compilerOptions': {
31
- 'target': ts.ScriptTarget.ES2022,
32
- 'module': ts.ModuleKind.ESNext,
33
- },
34
- }).outputText);
35
- await formModule.link((specifier) => {
36
- if (specifier === './dom') {
37
- return domModule;
38
- }
39
- if (specifier === './tool') {
40
- return toolModule;
41
- }
42
- return empty;
43
- });
44
- await formModule.evaluate();
45
-
46
- const trigger = document.querySelector('#trigger');
47
- const pop = document.querySelector('.pop');
48
- formModule.namespace.showPop(trigger, pop, 'v', { 'autoPosition': true });
49
- await Promise.resolve();
50
- assert.equal(pop.style.maxHeight, 'min(300px, 760px)', 'CSS max-height must survive the viewport limit');
51
- assert.equal(pop.style.maxWidth, 'min(480px, 1016px)', 'CSS max-width must survive the viewport limit');
52
-
53
- window.innerHeight = 240;
54
- refreshSize();
55
- assert.equal(pop.style.maxHeight, 'min(300px, 232px)', 'a smaller viewport must still constrain the popup');
56
-
57
- pop.style.maxHeight = '180px';
58
- refreshSize();
59
- assert.equal(pop.style.maxHeight, 'min(180px, 232px)', 'an explicit inline limit must take effect');
60
-
61
- pop.style.maxHeight = '';
62
- refreshSize();
63
- assert.equal(pop.style.maxHeight, 'min(300px, 232px)', 'removing an inline limit must restore the CSS limit');
64
-
65
- dom.window.close();
66
- console.log('Popup CSS limits, viewport limits, and inline overrides passed.');