jsgui3-server 0.0.151 → 0.0.155
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/README.md +21 -0
- package/admin-ui/v1/controls/admin_shell.js +33 -0
- package/admin-ui/v1/server.js +14 -1
- package/docs/agi/skills/README.md +23 -0
- package/docs/agi/skills/agent-output-control/SKILL.md +56 -0
- package/docs/agi/skills/ai-deep-research/SKILL.md +52 -0
- package/docs/agi/skills/autonomous-ui-inspection/SKILL.md +102 -0
- package/docs/agi/skills/deep-research/SKILL.md +156 -0
- package/docs/agi/skills/endurance/SKILL.md +53 -0
- package/docs/agi/skills/exploring-other-codebases/SKILL.md +56 -0
- package/docs/agi/skills/instruction-adherence/SKILL.md +73 -0
- package/docs/agi/skills/jsgui3-activation-debug/SKILL.md +94 -0
- package/docs/agi/skills/jsgui3-context-menu-patterns/SKILL.md +94 -0
- package/docs/agi/skills/puppeteer-efficient-ui-verification/SKILL.md +65 -0
- package/docs/agi/skills/runaway-process-guard/SKILL.md +49 -0
- package/docs/agi/skills/session-discipline/SKILL.md +40 -0
- package/docs/agi/skills/skill-writing/SKILL.md +211 -0
- package/docs/agi/skills/static-analysis/SKILL.md +58 -0
- package/docs/agi/skills/targeted-testing/SKILL.md +63 -0
- package/docs/agi/skills/understanding-jsgui3/SKILL.md +85 -0
- package/docs/api-reference.md +120 -2
- package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +1 -0
- package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +33 -0
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/bundling-system-deep-dive.md +112 -3
- package/docs/configuration-reference.md +84 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/json-body.js +126 -0
- package/openapi.js +474 -0
- package/package.json +13 -7
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +90 -22
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +50 -14
- package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +48 -14
- package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +396 -44
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +677 -18
- package/server.js +585 -167
- package/tests/README.md +86 -2
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/bundling-default-control-elimination.puppeteer.test.js +32 -1
- package/tests/control-elimination-root-feature-pruning.test.js +440 -0
- package/tests/control-elimination-static-bracket-access.test.js +245 -0
- package/tests/control-scan-manifest-regression.test.js +2 -0
- package/tests/end-to-end.test.js +22 -21
- package/tests/fixtures/control_scan_manifest_expectations.json +4 -2
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/helpers/puppeteer-e2e-harness.js +62 -1
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/project-local-controls-bundling.puppeteer.test.js +462 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- package/tests/test-runner.js +4 -0
|
@@ -16,8 +16,10 @@ const normalize_manifest = (manifest) => {
|
|
|
16
16
|
entry_file: manifest && manifest.entry_file_path ? path.basename(manifest.entry_file_path) : null,
|
|
17
17
|
uses_jsgui3_html: Boolean(manifest && manifest.uses_jsgui3_html),
|
|
18
18
|
dynamic_control_access_detected: Boolean(manifest && manifest.dynamic_control_access_detected),
|
|
19
|
+
dynamic_resource_access_detected: Boolean(manifest && manifest.dynamic_resource_access_detected),
|
|
19
20
|
reachable_files: safe_paths(manifest && manifest.reachable_files),
|
|
20
21
|
used_identifiers: safe_array(manifest && manifest.used_identifiers),
|
|
22
|
+
selected_root_features: safe_array(manifest && manifest.selected_root_features),
|
|
21
23
|
selected_controls: safe_array(manifest && manifest.selected_controls),
|
|
22
24
|
unmatched_identifiers: safe_array(manifest && manifest.unmatched_identifiers),
|
|
23
25
|
package_aliases: safe_array(manifest && manifest.package_aliases),
|
package/tests/end-to-end.test.js
CHANGED
|
@@ -12,6 +12,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
12
12
|
|
|
13
13
|
let server;
|
|
14
14
|
let serverPort = 3001; // Use a different port for testing
|
|
15
|
+
const test_host = '127.0.0.1';
|
|
15
16
|
let testControl;
|
|
16
17
|
let test_client_path;
|
|
17
18
|
let base_serve_options;
|
|
@@ -36,7 +37,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
36
37
|
const server_instance = await Server.serve(serve_options);
|
|
37
38
|
const port = server_instance.port || serve_options.port;
|
|
38
39
|
if (port) {
|
|
39
|
-
await wait_for_route(`http
|
|
40
|
+
await wait_for_route(`http://${test_host}:${port}/js/js.js`);
|
|
40
41
|
}
|
|
41
42
|
return server_instance;
|
|
42
43
|
};
|
|
@@ -86,7 +87,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
86
87
|
}
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
const js_identity_response = await makeRequest(`http
|
|
90
|
+
const js_identity_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
90
91
|
'Accept-Encoding': 'identity'
|
|
91
92
|
});
|
|
92
93
|
|
|
@@ -97,7 +98,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
97
98
|
);
|
|
98
99
|
|
|
99
100
|
// Test gzip compressed JavaScript
|
|
100
|
-
const js_gzip_response = await makeRequest(`http
|
|
101
|
+
const js_gzip_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
101
102
|
'Accept-Encoding': 'gzip'
|
|
102
103
|
});
|
|
103
104
|
|
|
@@ -112,7 +113,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
112
113
|
);
|
|
113
114
|
|
|
114
115
|
// Test brotli compressed JavaScript
|
|
115
|
-
const br_js_response = await makeRequest(`http
|
|
116
|
+
const br_js_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
116
117
|
'Accept-Encoding': 'br'
|
|
117
118
|
});
|
|
118
119
|
|
|
@@ -148,7 +149,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
148
149
|
});
|
|
149
150
|
|
|
150
151
|
// Test JavaScript with sourcemaps
|
|
151
|
-
const js_gzip_response = await makeRequest(`http
|
|
152
|
+
const js_gzip_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
152
153
|
'Accept-Encoding': 'gzip'
|
|
153
154
|
});
|
|
154
155
|
|
|
@@ -179,7 +180,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
179
180
|
}
|
|
180
181
|
});
|
|
181
182
|
|
|
182
|
-
const css_identity_response = await makeRequest(`http
|
|
183
|
+
const css_identity_response = await makeRequest(`http://${test_host}:${serverPort}/css/css.css`, {
|
|
183
184
|
'Accept-Encoding': 'identity'
|
|
184
185
|
});
|
|
185
186
|
|
|
@@ -190,7 +191,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
190
191
|
);
|
|
191
192
|
|
|
192
193
|
// Test gzip compressed CSS
|
|
193
|
-
const css_gzip_response = await makeRequest(`http
|
|
194
|
+
const css_gzip_response = await makeRequest(`http://${test_host}:${serverPort}/css/css.css`, {
|
|
194
195
|
'Accept-Encoding': 'gzip'
|
|
195
196
|
});
|
|
196
197
|
|
|
@@ -223,9 +224,9 @@ describe('End-to-End Integration Tests', function() {
|
|
|
223
224
|
});
|
|
224
225
|
|
|
225
226
|
// Test HTML (should not be compressed due to threshold)
|
|
226
|
-
const htmlResponse = await makeRequest(`http
|
|
227
|
-
'Accept-Encoding': 'gzip'
|
|
228
|
-
});
|
|
227
|
+
const htmlResponse = await makeRequest(`http://${test_host}:${serverPort}/`, {
|
|
228
|
+
'Accept-Encoding': 'gzip'
|
|
229
|
+
});
|
|
229
230
|
|
|
230
231
|
assert.strictEqual(htmlResponse.statusCode, 200);
|
|
231
232
|
assert(!htmlResponse.headers['content-encoding'], 'HTML should not be compressed due to threshold');
|
|
@@ -250,7 +251,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
250
251
|
}
|
|
251
252
|
});
|
|
252
253
|
|
|
253
|
-
const baseline_response = await makeRequest(`http
|
|
254
|
+
const baseline_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
254
255
|
'Accept-Encoding': 'identity'
|
|
255
256
|
});
|
|
256
257
|
assert.strictEqual(baseline_response.statusCode, 200);
|
|
@@ -276,7 +277,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
276
277
|
});
|
|
277
278
|
|
|
278
279
|
// Test JavaScript
|
|
279
|
-
const minified_response = await makeRequest(`http
|
|
280
|
+
const minified_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
280
281
|
'Accept-Encoding': 'identity'
|
|
281
282
|
});
|
|
282
283
|
|
|
@@ -305,9 +306,9 @@ describe('End-to-End Integration Tests', function() {
|
|
|
305
306
|
});
|
|
306
307
|
|
|
307
308
|
// Test with gzip request but compression disabled
|
|
308
|
-
const jsResponse = await makeRequest(`http
|
|
309
|
-
'Accept-Encoding': 'gzip'
|
|
310
|
-
});
|
|
309
|
+
const jsResponse = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
310
|
+
'Accept-Encoding': 'gzip'
|
|
311
|
+
});
|
|
311
312
|
|
|
312
313
|
assert.strictEqual(jsResponse.statusCode, 200);
|
|
313
314
|
assert(!jsResponse.headers['content-encoding'], 'Should not have content-encoding when compression disabled');
|
|
@@ -332,11 +333,11 @@ describe('End-to-End Integration Tests', function() {
|
|
|
332
333
|
|
|
333
334
|
// Make multiple concurrent requests
|
|
334
335
|
const requests = [];
|
|
335
|
-
for (let i = 0; i < 5; i++) {
|
|
336
|
-
requests.push(makeRequest(`http
|
|
337
|
-
'Accept-Encoding': 'gzip'
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
336
|
+
for (let i = 0; i < 5; i++) {
|
|
337
|
+
requests.push(makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
|
|
338
|
+
'Accept-Encoding': 'gzip'
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
340
341
|
|
|
341
342
|
const responses = await Promise.all(requests);
|
|
342
343
|
|
|
@@ -366,7 +367,7 @@ describe('End-to-End Integration Tests', function() {
|
|
|
366
367
|
];
|
|
367
368
|
|
|
368
369
|
for (const test of tests) {
|
|
369
|
-
const response = await makeRequest(`http
|
|
370
|
+
const response = await makeRequest(`http://${test_host}:${serverPort}${test.path}`);
|
|
370
371
|
assert.strictEqual(response.statusCode, 200);
|
|
371
372
|
assert(response.headers['content-type'].includes(test.expectedType),
|
|
372
373
|
`Should serve correct content type for ${test.path}`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"entry_file": "temp_control_scan_static_alias_client.js",
|
|
4
4
|
"uses_jsgui3_html": true,
|
|
5
5
|
"dynamic_control_access_detected": false,
|
|
6
|
+
"dynamic_resource_access_detected": false,
|
|
6
7
|
"reachable_files": [
|
|
7
8
|
"temp_control_scan_static_alias_client.js"
|
|
8
9
|
],
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
"Active_HTML_Document",
|
|
11
12
|
"Button"
|
|
12
13
|
],
|
|
14
|
+
"selected_root_features": [],
|
|
13
15
|
"selected_controls": [
|
|
14
16
|
"Active_HTML_Document",
|
|
15
17
|
"Button"
|
|
@@ -19,7 +21,6 @@
|
|
|
19
21
|
"ui"
|
|
20
22
|
],
|
|
21
23
|
"controls_aliases": [
|
|
22
|
-
"controls",
|
|
23
24
|
"ui_controls"
|
|
24
25
|
]
|
|
25
26
|
},
|
|
@@ -27,12 +28,14 @@
|
|
|
27
28
|
"entry_file": "temp_control_scan_dynamic_alias_client.js",
|
|
28
29
|
"uses_jsgui3_html": true,
|
|
29
30
|
"dynamic_control_access_detected": true,
|
|
31
|
+
"dynamic_resource_access_detected": false,
|
|
30
32
|
"reachable_files": [
|
|
31
33
|
"temp_control_scan_dynamic_alias_client.js"
|
|
32
34
|
],
|
|
33
35
|
"used_identifiers": [
|
|
34
36
|
"Active_HTML_Document"
|
|
35
37
|
],
|
|
38
|
+
"selected_root_features": [],
|
|
36
39
|
"selected_controls": [
|
|
37
40
|
"Active_HTML_Document"
|
|
38
41
|
],
|
|
@@ -41,7 +44,6 @@
|
|
|
41
44
|
"ui"
|
|
42
45
|
],
|
|
43
46
|
"controls_aliases": [
|
|
44
|
-
"controls",
|
|
45
47
|
"ui_controls"
|
|
46
48
|
]
|
|
47
49
|
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const Server = require('../../server');
|
|
5
|
+
const { get_free_port } = require('../../port-utils');
|
|
6
|
+
|
|
7
|
+
const default_viewport = {
|
|
8
|
+
width: 1366,
|
|
9
|
+
height: 900
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ensure_playwright_module = () => {
|
|
13
|
+
try {
|
|
14
|
+
return require('playwright');
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const launch_playwright_browser = async (playwright_module, launch_options = {}) => {
|
|
21
|
+
const resolved_playwright = playwright_module || ensure_playwright_module();
|
|
22
|
+
if (!resolved_playwright || !resolved_playwright.chromium) {
|
|
23
|
+
throw new Error('Playwright Chromium is unavailable in this environment.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalized_options = {
|
|
27
|
+
headless: true,
|
|
28
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
29
|
+
...launch_options
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (!normalized_options.executablePath && process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
|
|
33
|
+
normalized_options.executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return resolved_playwright.chromium.launch(normalized_options);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const start_control_example_server = async ({
|
|
40
|
+
examples_root_path,
|
|
41
|
+
dir_name,
|
|
42
|
+
ctrl_name,
|
|
43
|
+
server_name_prefix = 'examples/controls'
|
|
44
|
+
}) => {
|
|
45
|
+
const example_dir_path = path.join(examples_root_path, dir_name);
|
|
46
|
+
const example_client_path = path.join(example_dir_path, 'client.js');
|
|
47
|
+
|
|
48
|
+
const jsgui_module = require(example_client_path);
|
|
49
|
+
const ctrl_constructor = jsgui_module.controls && jsgui_module.controls[ctrl_name];
|
|
50
|
+
assert(ctrl_constructor, `Missing exported control jsgui.controls.${ctrl_name} in ${example_client_path}`);
|
|
51
|
+
|
|
52
|
+
const server_instance = new Server({
|
|
53
|
+
Ctrl: ctrl_constructor,
|
|
54
|
+
src_path_client_js: example_client_path,
|
|
55
|
+
name: `${server_name_prefix}/${dir_name}`
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
server_instance.allowed_addresses = ['127.0.0.1'];
|
|
59
|
+
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
const timeout_handle = setTimeout(() => reject(new Error('Publisher ready timeout')), 60000);
|
|
62
|
+
server_instance.on('ready', () => {
|
|
63
|
+
clearTimeout(timeout_handle);
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const port = await get_free_port();
|
|
69
|
+
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
server_instance.start(port, (error) => {
|
|
72
|
+
if (error) reject(error);
|
|
73
|
+
else resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
server_instance,
|
|
79
|
+
port,
|
|
80
|
+
example_client_path
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const stop_server_instance = async (server_instance) => {
|
|
85
|
+
if (!server_instance) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await new Promise((resolve) => {
|
|
90
|
+
server_instance.close(() => resolve());
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const attach_page_probe = (page) => {
|
|
95
|
+
const probe = {
|
|
96
|
+
console_errors: [],
|
|
97
|
+
page_errors: [],
|
|
98
|
+
request_failures: []
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const console_handler = (message) => {
|
|
102
|
+
if (message.type() === 'error') {
|
|
103
|
+
probe.console_errors.push(message.text());
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const page_error_handler = (error) => {
|
|
108
|
+
probe.page_errors.push(error instanceof Error ? error.message : String(error));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const request_failed_handler = (request) => {
|
|
112
|
+
const failure = request.failure();
|
|
113
|
+
probe.request_failures.push({
|
|
114
|
+
url: request.url(),
|
|
115
|
+
method: request.method(),
|
|
116
|
+
error_text: failure ? failure.errorText : 'unknown'
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
page.on('console', console_handler);
|
|
121
|
+
page.on('pageerror', page_error_handler);
|
|
122
|
+
page.on('requestfailed', request_failed_handler);
|
|
123
|
+
|
|
124
|
+
probe.detach = () => {
|
|
125
|
+
page.off('console', console_handler);
|
|
126
|
+
page.off('pageerror', page_error_handler);
|
|
127
|
+
page.off('requestfailed', request_failed_handler);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return probe;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const assert_clean_page_probe = (probe, {
|
|
134
|
+
allowed_console_error_patterns = [],
|
|
135
|
+
allowed_page_error_patterns = [],
|
|
136
|
+
allowed_request_failure_patterns = []
|
|
137
|
+
} = {}) => {
|
|
138
|
+
const is_allowed_message = (message, allowed_patterns) => {
|
|
139
|
+
return allowed_patterns.some((pattern) => {
|
|
140
|
+
if (pattern instanceof RegExp) {
|
|
141
|
+
return pattern.test(message);
|
|
142
|
+
}
|
|
143
|
+
return String(message).includes(String(pattern));
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const blocked_console_errors = probe.console_errors.filter((message) => {
|
|
148
|
+
return !is_allowed_message(message, allowed_console_error_patterns);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const blocked_page_errors = probe.page_errors.filter((message) => {
|
|
152
|
+
return !is_allowed_message(message, allowed_page_error_patterns);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const blocked_request_failures = probe.request_failures.filter((failure_item) => {
|
|
156
|
+
const label = `${failure_item.method} ${failure_item.url} ${failure_item.error_text}`;
|
|
157
|
+
return !is_allowed_message(label, allowed_request_failure_patterns);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
assert.strictEqual(
|
|
161
|
+
blocked_console_errors.length,
|
|
162
|
+
0,
|
|
163
|
+
`Unexpected browser console errors: ${blocked_console_errors.join(' | ')}`
|
|
164
|
+
);
|
|
165
|
+
assert.strictEqual(
|
|
166
|
+
blocked_page_errors.length,
|
|
167
|
+
0,
|
|
168
|
+
`Unexpected browser page errors: ${blocked_page_errors.join(' | ')}`
|
|
169
|
+
);
|
|
170
|
+
assert.strictEqual(
|
|
171
|
+
blocked_request_failures.length,
|
|
172
|
+
0,
|
|
173
|
+
`Unexpected browser request failures: ${JSON.stringify(blocked_request_failures)}`
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const open_page = async (browser_instance, target_url, options = {}) => {
|
|
178
|
+
const viewport = options.viewport || default_viewport;
|
|
179
|
+
|
|
180
|
+
const page = await browser_instance.newPage({ viewport });
|
|
181
|
+
|
|
182
|
+
const page_probe = attach_page_probe(page);
|
|
183
|
+
|
|
184
|
+
await page.goto(target_url, {
|
|
185
|
+
waitUntil: options.wait_until || 'load'
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
page,
|
|
190
|
+
page_probe
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const close_page_with_probe = async (page, page_probe) => {
|
|
195
|
+
if (page_probe && typeof page_probe.detach === 'function') {
|
|
196
|
+
page_probe.detach();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (page && !page.isClosed()) {
|
|
200
|
+
await page.close();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const set_input_value_with_events = async (
|
|
205
|
+
page,
|
|
206
|
+
selector,
|
|
207
|
+
next_value,
|
|
208
|
+
event_names = ['input', 'change']
|
|
209
|
+
) => {
|
|
210
|
+
await page.$eval(selector, (element, value, events) => {
|
|
211
|
+
element.value = String(value);
|
|
212
|
+
for (const event_name of events) {
|
|
213
|
+
element.dispatchEvent(new Event(event_name, { bubbles: true }));
|
|
214
|
+
}
|
|
215
|
+
}, next_value, event_names);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const wait_for_text_content = async (page, selector, expected_text_or_regex, timeout_ms = 6000) => {
|
|
219
|
+
if (expected_text_or_regex instanceof RegExp) {
|
|
220
|
+
await page.waitForFunction(
|
|
221
|
+
(selector_arg, regex_source, regex_flags) => {
|
|
222
|
+
const element = document.querySelector(selector_arg);
|
|
223
|
+
if (!element) return false;
|
|
224
|
+
const value = (element.textContent || '').trim();
|
|
225
|
+
const expected_regex = new RegExp(regex_source, regex_flags);
|
|
226
|
+
return expected_regex.test(value);
|
|
227
|
+
},
|
|
228
|
+
{ timeout: timeout_ms },
|
|
229
|
+
selector,
|
|
230
|
+
expected_text_or_regex.source,
|
|
231
|
+
expected_text_or_regex.flags
|
|
232
|
+
);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const expected_text = String(expected_text_or_regex);
|
|
237
|
+
await page.waitForFunction(
|
|
238
|
+
(selector_arg, expected_text_arg) => {
|
|
239
|
+
const element = document.querySelector(selector_arg);
|
|
240
|
+
if (!element) return false;
|
|
241
|
+
return (element.textContent || '').trim() === expected_text_arg;
|
|
242
|
+
},
|
|
243
|
+
{ timeout: timeout_ms },
|
|
244
|
+
selector,
|
|
245
|
+
expected_text
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const drag_by = async (page, selector, delta_x, delta_y) => {
|
|
250
|
+
const locator = page.locator(selector).first();
|
|
251
|
+
const box = await locator.boundingBox();
|
|
252
|
+
assert(box, `Missing bounding box for selector: ${selector}`);
|
|
253
|
+
|
|
254
|
+
const start_x = box.x + box.width / 2;
|
|
255
|
+
const start_y = box.y + box.height / 2;
|
|
256
|
+
|
|
257
|
+
await page.mouse.move(start_x, start_y);
|
|
258
|
+
await page.mouse.down();
|
|
259
|
+
await page.mouse.move(start_x + delta_x, start_y + delta_y, { steps: 12 });
|
|
260
|
+
await page.mouse.up();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const run_interaction_story = async ({
|
|
264
|
+
page,
|
|
265
|
+
story_name,
|
|
266
|
+
steps
|
|
267
|
+
}) => {
|
|
268
|
+
assert(Array.isArray(steps) && steps.length > 0, 'Interaction story requires at least one step');
|
|
269
|
+
|
|
270
|
+
const step_results = [];
|
|
271
|
+
|
|
272
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
273
|
+
const step_spec = steps[index] || {};
|
|
274
|
+
const step_name = step_spec.name || `step_${index + 1}`;
|
|
275
|
+
const started_at = Date.now();
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
if (typeof step_spec.run === 'function') {
|
|
279
|
+
await step_spec.run(page);
|
|
280
|
+
}
|
|
281
|
+
if (typeof step_spec.assert === 'function') {
|
|
282
|
+
await step_spec.assert(page);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
step_results.push({
|
|
286
|
+
step_name,
|
|
287
|
+
duration_ms: Date.now() - started_at
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
error.message = `[${story_name} :: ${step_name}] ${error.message}`;
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return step_results;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const wait_for_condition = async (condition_fn, timeout_ms = 6000, interval_ms = 25) => {
|
|
299
|
+
const started_at = Date.now();
|
|
300
|
+
|
|
301
|
+
while ((Date.now() - started_at) <= timeout_ms) {
|
|
302
|
+
const condition_result = await condition_fn();
|
|
303
|
+
if (condition_result) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, interval_ms));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return false;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
ensure_playwright_module,
|
|
314
|
+
launch_playwright_browser,
|
|
315
|
+
start_control_example_server,
|
|
316
|
+
stop_server_instance,
|
|
317
|
+
open_page,
|
|
318
|
+
close_page_with_probe,
|
|
319
|
+
attach_page_probe,
|
|
320
|
+
assert_clean_page_probe,
|
|
321
|
+
set_input_value_with_events,
|
|
322
|
+
wait_for_text_content,
|
|
323
|
+
drag_by,
|
|
324
|
+
run_interaction_story,
|
|
325
|
+
wait_for_condition
|
|
326
|
+
};
|
|
@@ -301,6 +301,64 @@ const wait_for_condition = async (condition_fn, timeout_ms = 6000, interval_ms =
|
|
|
301
301
|
return false;
|
|
302
302
|
};
|
|
303
303
|
|
|
304
|
+
const extract_esbuild_warning_headers_from_warning_records = (warning_records = []) => {
|
|
305
|
+
if (!Array.isArray(warning_records)) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return warning_records.map((warning_record) => {
|
|
310
|
+
if (!warning_record || typeof warning_record !== 'object') {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
const warning_text = String(warning_record.text || '').trim();
|
|
314
|
+
const warning_id = warning_record.id ? `[${String(warning_record.id)}]` : '';
|
|
315
|
+
return warning_id ? `${warning_text} ${warning_id}`.trim() : warning_text;
|
|
316
|
+
}).filter((warning_header) => warning_header.length > 0);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const extract_esbuild_warning_headers_from_bundle = (bundle) => {
|
|
320
|
+
const warning_records = bundle &&
|
|
321
|
+
bundle.bundle_analysis &&
|
|
322
|
+
bundle.bundle_analysis.esbuild_warnings;
|
|
323
|
+
return extract_esbuild_warning_headers_from_warning_records(warning_records);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const assert_no_unexpected_esbuild_warning_headers = (warning_headers, {
|
|
327
|
+
allowed_warning_patterns = [],
|
|
328
|
+
required_warning_patterns = []
|
|
329
|
+
} = {}) => {
|
|
330
|
+
const headers = Array.isArray(warning_headers) ? warning_headers : [];
|
|
331
|
+
const is_pattern_match = (value, pattern) => {
|
|
332
|
+
if (pattern instanceof RegExp) {
|
|
333
|
+
return pattern.test(value);
|
|
334
|
+
}
|
|
335
|
+
return value.includes(String(pattern));
|
|
336
|
+
};
|
|
337
|
+
const is_allowed_warning = (warning_header) => {
|
|
338
|
+
return allowed_warning_patterns.some((pattern) => is_pattern_match(warning_header, pattern));
|
|
339
|
+
};
|
|
340
|
+
const unexpected_warning_headers = headers.filter((warning_header) => !is_allowed_warning(warning_header));
|
|
341
|
+
assert.strictEqual(
|
|
342
|
+
unexpected_warning_headers.length,
|
|
343
|
+
0,
|
|
344
|
+
`Unexpected esbuild warnings detected:\n${unexpected_warning_headers.join('\n')}`
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const missing_required_patterns = required_warning_patterns.filter((pattern) => {
|
|
348
|
+
return !headers.some((warning_header) => is_pattern_match(warning_header, pattern));
|
|
349
|
+
});
|
|
350
|
+
assert.strictEqual(
|
|
351
|
+
missing_required_patterns.length,
|
|
352
|
+
0,
|
|
353
|
+
`Missing required esbuild warning patterns: ${missing_required_patterns.map((pattern) => String(pattern)).join(', ')}`
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
warning_headers: headers,
|
|
358
|
+
unexpected_warning_headers
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
304
362
|
module.exports = {
|
|
305
363
|
ensure_puppeteer_module,
|
|
306
364
|
launch_puppeteer_browser,
|
|
@@ -313,5 +371,8 @@ module.exports = {
|
|
|
313
371
|
wait_for_text_content,
|
|
314
372
|
drag_by,
|
|
315
373
|
run_interaction_story,
|
|
316
|
-
wait_for_condition
|
|
374
|
+
wait_for_condition,
|
|
375
|
+
extract_esbuild_warning_headers_from_warning_records,
|
|
376
|
+
extract_esbuild_warning_headers_from_bundle,
|
|
377
|
+
assert_no_unexpected_esbuild_warning_headers
|
|
317
378
|
};
|