jsgui3-server 0.0.148 → 0.0.150
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/.github/agents/Mobile Developer.agent.md +89 -0
- package/.github/workflows/control-scan-manifest-check.yml +31 -0
- package/AGENTS.md +4 -0
- package/README.md +215 -3
- package/admin-ui/client.js +81 -51
- package/admin-ui/v1/admin_auth_service.js +197 -0
- package/admin-ui/v1/admin_user_store.js +71 -0
- package/admin-ui/v1/client.js +17 -0
- package/admin-ui/v1/controls/admin_shell.js +1399 -0
- package/admin-ui/v1/controls/group_box.js +84 -0
- package/admin-ui/v1/controls/stat_card.js +125 -0
- package/admin-ui/v1/server.js +658 -0
- package/admin-ui/v1/utils/formatters.js +68 -0
- package/dev-status.svg +139 -0
- package/docs/admin-extension-guide.md +345 -0
- package/docs/api-reference.md +301 -43
- package/docs/books/adaptive-control-improvements/01-control-candidate-matrix.md +122 -0
- package/docs/books/adaptive-control-improvements/02-tier-1-layout-playbooks.md +207 -0
- package/docs/books/adaptive-control-improvements/03-tier-2-navigation-form-overlay.md +140 -0
- package/docs/books/adaptive-control-improvements/04-cross-cutting-platform-functionality.md +141 -0
- package/docs/books/adaptive-control-improvements/05-styling-theming-density-upgrades.md +114 -0
- package/docs/books/adaptive-control-improvements/06-testing-quality-gates.md +97 -0
- package/docs/books/adaptive-control-improvements/07-delivery-roadmap-and-ownership.md +137 -0
- package/docs/books/adaptive-control-improvements/08-appendix-tier1-acceptance-and-pr-templates.md +261 -0
- package/docs/books/adaptive-control-improvements/README.md +66 -0
- package/docs/books/admin-ui-authentication/01-threat-model-and-goals.md +124 -0
- package/docs/books/admin-ui-authentication/02-session-model-and-token-model.md +75 -0
- package/docs/books/admin-ui-authentication/03-auth-middleware-patterns.md +77 -0
- package/docs/books/admin-ui-authentication/README.md +25 -0
- package/docs/books/creating-a-new-admin-ui/01-introduction-and-vision.md +130 -0
- package/docs/books/creating-a-new-admin-ui/02-architecture-and-data-flow.md +298 -0
- package/docs/books/creating-a-new-admin-ui/03-server-introspection.md +381 -0
- package/docs/books/creating-a-new-admin-ui/04-admin-module-adapter-layer.md +592 -0
- package/docs/books/creating-a-new-admin-ui/05-domain-controls-stat-cards-and-gauges.md +513 -0
- package/docs/books/creating-a-new-admin-ui/06-domain-controls-process-manager.md +544 -0
- package/docs/books/creating-a-new-admin-ui/07-domain-controls-resource-pool-inspector.md +493 -0
- package/docs/books/creating-a-new-admin-ui/08-domain-controls-route-table-and-api-explorer.md +586 -0
- package/docs/books/creating-a-new-admin-ui/09-domain-controls-log-viewer-and-activity-feed.md +490 -0
- package/docs/books/creating-a-new-admin-ui/10-domain-controls-build-status-and-bundle-inspector.md +526 -0
- package/docs/books/creating-a-new-admin-ui/11-domain-controls-configuration-panel.md +808 -0
- package/docs/books/creating-a-new-admin-ui/12-admin-shell-layout-sidebar-navigation.md +210 -0
- package/docs/books/creating-a-new-admin-ui/13-telemetry-integration.md +556 -0
- package/docs/books/creating-a-new-admin-ui/14-realtime-sse-observable-integration.md +485 -0
- package/docs/books/creating-a-new-admin-ui/15-styling-theming-aero-design-system.md +521 -0
- package/docs/books/creating-a-new-admin-ui/16-testing-and-quality-assurance.md +147 -0
- package/docs/books/creating-a-new-admin-ui/17-next-steps-process-resource-roadmap.md +356 -0
- package/docs/books/creating-a-new-admin-ui/README.md +68 -0
- package/docs/books/device-adaptive-composition/01-platform-feature-audit.md +177 -0
- package/docs/books/device-adaptive-composition/02-responsive-composition-model.md +187 -0
- package/docs/books/device-adaptive-composition/03-data-model-vs-view-model.md +231 -0
- package/docs/books/device-adaptive-composition/04-styling-theme-breakpoints.md +234 -0
- package/docs/books/device-adaptive-composition/05-showcase-app-multi-device-assessment.md +193 -0
- package/docs/books/device-adaptive-composition/06-implementation-patterns-and-apis.md +346 -0
- package/docs/books/device-adaptive-composition/07-testing-harness-and-quality-gates.md +265 -0
- package/docs/books/device-adaptive-composition/08-roadmap-and-adoption-plan.md +250 -0
- package/docs/books/device-adaptive-composition/README.md +47 -0
- package/docs/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
- package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
- package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
- package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
- package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
- package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
- package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
- package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
- package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
- package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
- package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
- package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
- package/docs/bundling-system-deep-dive.md +9 -4
- package/docs/comparison-report-express-plex-cpanel.md +549 -0
- package/docs/comprehensive-documentation.md +49 -18
- package/docs/configuration-reference.md +152 -27
- package/docs/core/README.md +19 -0
- package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
- package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
- package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
- package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
- package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
- package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
- package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
- package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
- package/docs/designs/server-admin-interface-aero.svg +611 -0
- package/docs/publishers-guide.md +59 -4
- package/docs/resources-guide.md +184 -35
- package/docs/simple-server-api-design.md +72 -17
- package/docs/system-architecture.md +18 -14
- package/docs/troubleshooting.md +84 -53
- package/examples/controls/15) window, observable SSE/server.js +6 -1
- package/examples/controls/19) window, auto observable ui/server.js +9 -0
- package/examples/controls/20) window, task manager app/README.md +133 -0
- package/examples/controls/20) window, task manager app/client.js +797 -0
- package/examples/controls/20) window, task manager app/server.js +178 -0
- package/examples/controls/6) window, color_palette/client.js +165 -68
- package/examples/controls/9) window, date picker/client.js +362 -76
- package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
- package/examples/jsgui3-html/06) theming/client.js +22 -1
- package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
- package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
- package/lab/experiments/capture-color-controls.js +196 -0
- package/lab/results/screenshots/color-controls/full_page.png +0 -0
- package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
- package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
- package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
- package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
- package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
- package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
- package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
- package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
- package/lab/screenshot-utils.js +248 -0
- package/module.js +12 -0
- package/package.json +12 -2
- package/publishers/Publishers.js +4 -3
- package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
- package/publishers/http-sse-publisher.js +341 -0
- package/resources/process-resource.js +950 -0
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +129 -33
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
- package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
- package/resources/remote-process-resource.js +355 -0
- package/resources/server-resource-pool.js +354 -41
- package/serve-factory.js +442 -259
- package/server.js +288 -13
- package/tests/README.md +71 -4
- package/tests/admin-ui-jsgui-controls.test.js +581 -0
- package/tests/admin-ui-render.test.js +24 -0
- package/tests/assigners.test.js +56 -40
- package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
- package/tests/configuration-validation.test.js +21 -18
- package/tests/content-analysis.test.js +7 -6
- package/tests/control-optimizer-cache-behavior.test.js +52 -0
- package/tests/control-scan-manifest-regression.test.js +144 -0
- package/tests/end-to-end.test.js +15 -14
- package/tests/error-handling.test.js +222 -179
- package/tests/fixtures/bundling-default-button-client.js +37 -0
- package/tests/fixtures/bundling-default-window-client.js +34 -0
- package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
- package/tests/fixtures/resource-monitor-client.js +319 -0
- package/tests/helpers/puppeteer-e2e-harness.js +317 -0
- package/tests/http-sse-publisher.test.js +136 -0
- package/tests/performance.test.js +69 -65
- package/tests/process-resource.test.js +138 -0
- package/tests/publishers.test.js +7 -7
- package/tests/remote-process-resource.test.js +160 -0
- package/tests/sass-controls.e2e.test.js +7 -1
- package/tests/serve-resources.test.js +270 -0
- package/tests/serve.test.js +120 -50
- package/tests/server-resource-pool.test.js +106 -0
- package/tests/small-controls-bundle-size.test.js +252 -0
- package/tests/test-runner.js +14 -1
- package/tests/window-examples.puppeteer.test.js +204 -1
- package/tests/window-resource-integration.puppeteer.test.js +585 -0
- package/tests/temp_invalid.js +0 -7
- package/tests/temp_invalid_utf8.js +0 -1
- package/tests/temp_malformed.js +0 -10
package/tests/assigners.test.js
CHANGED
|
@@ -39,8 +39,12 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
describe('Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner', function() {
|
|
42
|
-
it('should compress content with default gzip settings', async function() {
|
|
43
|
-
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner(
|
|
42
|
+
it('should compress content with default gzip settings', async function() {
|
|
43
|
+
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
44
|
+
compression: {
|
|
45
|
+
threshold: 0
|
|
46
|
+
}
|
|
47
|
+
});
|
|
44
48
|
|
|
45
49
|
await assigner.assign(mockBundleItems);
|
|
46
50
|
|
|
@@ -56,14 +60,18 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
56
60
|
}
|
|
57
61
|
});
|
|
58
62
|
|
|
59
|
-
// Verify compression statistics
|
|
60
|
-
assert.strictEqual(assigner.compression_stats.total_items, 3, 'Should track 3 total items');
|
|
61
|
-
assert.strictEqual(assigner.compression_stats.gzip_compressed, 3, 'Should have compressed 3 items with gzip');
|
|
62
|
-
assert(assigner.compression_stats.gzip_savings
|
|
63
|
+
// Verify compression statistics
|
|
64
|
+
assert.strictEqual(assigner.compression_stats.total_items, 3, 'Should track 3 total items');
|
|
65
|
+
assert.strictEqual(assigner.compression_stats.gzip_compressed, 3, 'Should have compressed 3 items with gzip');
|
|
66
|
+
assert.strictEqual(typeof assigner.compression_stats.gzip_savings, 'number', 'Gzip savings should be numeric');
|
|
63
67
|
});
|
|
64
68
|
|
|
65
|
-
it('should compress content with default brotli settings', async function() {
|
|
66
|
-
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner(
|
|
69
|
+
it('should compress content with default brotli settings', async function() {
|
|
70
|
+
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
71
|
+
compression: {
|
|
72
|
+
threshold: 0
|
|
73
|
+
}
|
|
74
|
+
});
|
|
67
75
|
|
|
68
76
|
await assigner.assign(mockBundleItems);
|
|
69
77
|
|
|
@@ -79,18 +87,19 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
79
87
|
}
|
|
80
88
|
});
|
|
81
89
|
|
|
82
|
-
// Verify compression statistics
|
|
83
|
-
assert.strictEqual(assigner.compression_stats.brotli_compressed, 3, 'Should have compressed 3 items with brotli');
|
|
84
|
-
assert(assigner.compression_stats.brotli_savings
|
|
90
|
+
// Verify compression statistics
|
|
91
|
+
assert.strictEqual(assigner.compression_stats.brotli_compressed, 3, 'Should have compressed 3 items with brotli');
|
|
92
|
+
assert.strictEqual(typeof assigner.compression_stats.brotli_savings, 'number', 'Brotli savings should be numeric');
|
|
85
93
|
});
|
|
86
94
|
|
|
87
95
|
it('should respect compression configuration - gzip only', async function() {
|
|
88
|
-
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
89
|
-
compression: {
|
|
90
|
-
enabled: true,
|
|
91
|
-
algorithms: ['gzip']
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
97
|
+
compression: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
algorithms: ['gzip'],
|
|
100
|
+
threshold: 0
|
|
101
|
+
}
|
|
102
|
+
});
|
|
94
103
|
|
|
95
104
|
await assigner.assign(mockBundleItems);
|
|
96
105
|
|
|
@@ -105,12 +114,13 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
105
114
|
});
|
|
106
115
|
|
|
107
116
|
it('should respect compression configuration - brotli only', async function() {
|
|
108
|
-
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
109
|
-
compression: {
|
|
110
|
-
enabled: true,
|
|
111
|
-
algorithms: ['br']
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
118
|
+
compression: {
|
|
119
|
+
enabled: true,
|
|
120
|
+
algorithms: ['br'],
|
|
121
|
+
threshold: 0
|
|
122
|
+
}
|
|
123
|
+
});
|
|
114
124
|
|
|
115
125
|
await assigner.assign(mockBundleItems);
|
|
116
126
|
|
|
@@ -125,13 +135,14 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
125
135
|
});
|
|
126
136
|
|
|
127
137
|
it('should respect compression configuration - custom gzip level', async function() {
|
|
128
|
-
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
129
|
-
compression: {
|
|
130
|
-
enabled: true,
|
|
131
|
-
algorithms: ['gzip'],
|
|
132
|
-
gzip: { level: 1 } // Fastest compression
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
139
|
+
compression: {
|
|
140
|
+
enabled: true,
|
|
141
|
+
algorithms: ['gzip'],
|
|
142
|
+
gzip: { level: 1 }, // Fastest compression
|
|
143
|
+
threshold: 0
|
|
144
|
+
}
|
|
145
|
+
});
|
|
135
146
|
|
|
136
147
|
await assigner.assign(mockBundleItems);
|
|
137
148
|
|
|
@@ -143,13 +154,14 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
143
154
|
});
|
|
144
155
|
|
|
145
156
|
it('should respect compression configuration - custom brotli quality', async function() {
|
|
146
|
-
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
147
|
-
compression: {
|
|
148
|
-
enabled: true,
|
|
149
|
-
algorithms: ['br'],
|
|
150
|
-
brotli: { quality: 1 } // Lowest quality (fastest)
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
158
|
+
compression: {
|
|
159
|
+
enabled: true,
|
|
160
|
+
algorithms: ['br'],
|
|
161
|
+
brotli: { quality: 1 }, // Lowest quality (fastest)
|
|
162
|
+
threshold: 0
|
|
163
|
+
}
|
|
164
|
+
});
|
|
153
165
|
|
|
154
166
|
await assigner.assign(mockBundleItems);
|
|
155
167
|
|
|
@@ -288,8 +300,12 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
288
300
|
});
|
|
289
301
|
|
|
290
302
|
describe('Compression Statistics', function() {
|
|
291
|
-
it('should track compression statistics accurately', async function() {
|
|
292
|
-
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner(
|
|
303
|
+
it('should track compression statistics accurately', async function() {
|
|
304
|
+
const assigner = new Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner({
|
|
305
|
+
compression: {
|
|
306
|
+
threshold: 0
|
|
307
|
+
}
|
|
308
|
+
});
|
|
293
309
|
|
|
294
310
|
await assigner.assign(mockBundleItems);
|
|
295
311
|
|
|
@@ -313,4 +329,4 @@ describe('Assigner Component Isolation Tests', function() {
|
|
|
313
329
|
assert.strictEqual(assigner.compression_stats.total_items, 2);
|
|
314
330
|
});
|
|
315
331
|
});
|
|
316
|
-
});
|
|
332
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { describe, it, before, after } = require('mocha');
|
|
4
|
+
|
|
5
|
+
const Server = require('../server');
|
|
6
|
+
const { get_free_port } = require('../port-utils');
|
|
7
|
+
const {
|
|
8
|
+
ensure_puppeteer_module,
|
|
9
|
+
launch_puppeteer_browser,
|
|
10
|
+
open_page,
|
|
11
|
+
stop_server_instance,
|
|
12
|
+
assert_clean_page_probe
|
|
13
|
+
} = require('./helpers/puppeteer-e2e-harness');
|
|
14
|
+
|
|
15
|
+
const button_fixture_client_path = path.join(__dirname, 'fixtures', 'bundling-default-button-client.js');
|
|
16
|
+
const window_fixture_client_path = path.join(__dirname, 'fixtures', 'bundling-default-window-client.js');
|
|
17
|
+
|
|
18
|
+
const window_markers = [
|
|
19
|
+
'Minimize window',
|
|
20
|
+
'window_manager',
|
|
21
|
+
'__type_name = "window"',
|
|
22
|
+
"__type_name = 'window'",
|
|
23
|
+
"__type_name='window'",
|
|
24
|
+
'add_class("window")',
|
|
25
|
+
"add_class('window')"
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const find_window_markers = (bundle_text = '') => {
|
|
29
|
+
return window_markers.filter((marker) => bundle_text.includes(marker));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const read_js_bundle_from_page = async (page) => {
|
|
33
|
+
return page.evaluate(async () => {
|
|
34
|
+
const response = await fetch('/js/js.js', { cache: 'no-store' });
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
return {
|
|
37
|
+
status_code: response.status,
|
|
38
|
+
body_text: text
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const load_fixture_ctrl = (client_path, ctrl_name) => {
|
|
44
|
+
const resolved_client_path = require.resolve(client_path);
|
|
45
|
+
delete require.cache[resolved_client_path];
|
|
46
|
+
|
|
47
|
+
const fixture_module = require(resolved_client_path);
|
|
48
|
+
const ctrl_constructor = fixture_module.controls && fixture_module.controls[ctrl_name];
|
|
49
|
+
assert(ctrl_constructor, `Missing exported control jsgui.controls.${ctrl_name} in ${client_path}`);
|
|
50
|
+
return ctrl_constructor;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const start_fixture_server = async ({ client_path, ctrl_name, bundler }) => {
|
|
54
|
+
const ctrl_constructor = load_fixture_ctrl(client_path, ctrl_name);
|
|
55
|
+
|
|
56
|
+
const server_instance = new Server({
|
|
57
|
+
Ctrl: ctrl_constructor,
|
|
58
|
+
src_path_client_js: client_path,
|
|
59
|
+
name: `tests/bundling/${ctrl_name}`,
|
|
60
|
+
bundler
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
server_instance.allowed_addresses = ['127.0.0.1'];
|
|
64
|
+
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
const timeout_handle = setTimeout(() => reject(new Error('Publisher ready timeout')), 60000);
|
|
67
|
+
server_instance.on('ready', () => {
|
|
68
|
+
clearTimeout(timeout_handle);
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const port = await get_free_port();
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
server_instance.start(port, (error) => {
|
|
76
|
+
if (error) reject(error);
|
|
77
|
+
else resolve();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
server_instance,
|
|
83
|
+
port
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const close_page_with_probe = async (page, page_probe) => {
|
|
88
|
+
if (page_probe && typeof page_probe.detach === 'function') {
|
|
89
|
+
page_probe.detach();
|
|
90
|
+
}
|
|
91
|
+
if (page) {
|
|
92
|
+
await page.close();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const stop_server_instance_with_timeout = async (server_instance, timeout_ms = 12000) => {
|
|
97
|
+
if (!server_instance) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await Promise.race([
|
|
102
|
+
stop_server_instance(server_instance),
|
|
103
|
+
new Promise((resolve) => setTimeout(resolve, timeout_ms))
|
|
104
|
+
]);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe('Bundling Default Control Elimination Puppeteer Tests', function () {
|
|
108
|
+
this.timeout(420000);
|
|
109
|
+
|
|
110
|
+
let puppeteer_module = null;
|
|
111
|
+
let browser_instance = null;
|
|
112
|
+
|
|
113
|
+
before(async function () {
|
|
114
|
+
this.timeout(60000);
|
|
115
|
+
|
|
116
|
+
puppeteer_module = ensure_puppeteer_module();
|
|
117
|
+
if (!puppeteer_module) {
|
|
118
|
+
this.skip();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
browser_instance = await launch_puppeteer_browser(puppeteer_module);
|
|
124
|
+
} catch {
|
|
125
|
+
this.skip();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
after(async function () {
|
|
130
|
+
if (browser_instance) {
|
|
131
|
+
await browser_instance.close();
|
|
132
|
+
browser_instance = null;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('removes unused Window code by default and restores it when elimination is disabled', async function () {
|
|
137
|
+
this.timeout(360000);
|
|
138
|
+
let default_server_instance = null;
|
|
139
|
+
let disabled_server_instance = null;
|
|
140
|
+
let default_page = null;
|
|
141
|
+
let disabled_page = null;
|
|
142
|
+
let default_page_probe = null;
|
|
143
|
+
let disabled_page_probe = null;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const default_server = await start_fixture_server({
|
|
147
|
+
client_path: button_fixture_client_path,
|
|
148
|
+
ctrl_name: 'Bundling_Default_Button_App'
|
|
149
|
+
});
|
|
150
|
+
default_server_instance = default_server.server_instance;
|
|
151
|
+
|
|
152
|
+
const default_open_result = await open_page(
|
|
153
|
+
browser_instance,
|
|
154
|
+
`http://127.0.0.1:${default_server.port}/`,
|
|
155
|
+
{ wait_until: 'domcontentloaded' }
|
|
156
|
+
);
|
|
157
|
+
default_page = default_open_result.page;
|
|
158
|
+
default_page_probe = default_open_result.page_probe;
|
|
159
|
+
|
|
160
|
+
await default_page.waitForSelector('[data-test="bundle-test-button"]');
|
|
161
|
+
|
|
162
|
+
const default_bundle_response = await read_js_bundle_from_page(default_page);
|
|
163
|
+
assert.strictEqual(default_bundle_response.status_code, 200, 'Expected /js/js.js to load in default mode');
|
|
164
|
+
const default_markers = find_window_markers(default_bundle_response.body_text);
|
|
165
|
+
assert.strictEqual(
|
|
166
|
+
default_markers.length,
|
|
167
|
+
0,
|
|
168
|
+
`Unexpected Window markers in default bundle: ${default_markers.join(', ')}`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
assert_clean_page_probe(default_page_probe);
|
|
172
|
+
|
|
173
|
+
await close_page_with_probe(default_page, default_page_probe);
|
|
174
|
+
default_page = null;
|
|
175
|
+
default_page_probe = null;
|
|
176
|
+
|
|
177
|
+
const disabled_server = await start_fixture_server({
|
|
178
|
+
client_path: button_fixture_client_path,
|
|
179
|
+
ctrl_name: 'Bundling_Default_Button_App',
|
|
180
|
+
bundler: {
|
|
181
|
+
elimination: {
|
|
182
|
+
enabled: false,
|
|
183
|
+
jsgui3_html_controls: {
|
|
184
|
+
enabled: false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
disabled_server_instance = disabled_server.server_instance;
|
|
190
|
+
|
|
191
|
+
const disabled_open_result = await open_page(
|
|
192
|
+
browser_instance,
|
|
193
|
+
`http://127.0.0.1:${disabled_server.port}/`,
|
|
194
|
+
{ wait_until: 'domcontentloaded' }
|
|
195
|
+
);
|
|
196
|
+
disabled_page = disabled_open_result.page;
|
|
197
|
+
disabled_page_probe = disabled_open_result.page_probe;
|
|
198
|
+
|
|
199
|
+
await disabled_page.waitForSelector('[data-test="bundle-test-button"]');
|
|
200
|
+
|
|
201
|
+
const disabled_bundle_response = await read_js_bundle_from_page(disabled_page);
|
|
202
|
+
assert.strictEqual(disabled_bundle_response.status_code, 200, 'Expected /js/js.js to load in disabled mode');
|
|
203
|
+
const disabled_markers = find_window_markers(disabled_bundle_response.body_text);
|
|
204
|
+
assert(
|
|
205
|
+
disabled_markers.length > 0,
|
|
206
|
+
'Expected Window markers when elimination is explicitly disabled'
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
assert(
|
|
210
|
+
default_bundle_response.body_text.length < disabled_bundle_response.body_text.length,
|
|
211
|
+
'Expected default bundle to be smaller than elimination-disabled bundle'
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
assert_clean_page_probe(disabled_page_probe);
|
|
215
|
+
} finally {
|
|
216
|
+
await close_page_with_probe(default_page, default_page_probe);
|
|
217
|
+
await close_page_with_probe(disabled_page, disabled_page_probe);
|
|
218
|
+
await stop_server_instance_with_timeout(default_server_instance);
|
|
219
|
+
await stop_server_instance_with_timeout(disabled_server_instance);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('keeps Window code in the default bundle when Window control is used', async () => {
|
|
224
|
+
let server_instance = null;
|
|
225
|
+
let page = null;
|
|
226
|
+
let page_probe = null;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const started_server = await start_fixture_server({
|
|
230
|
+
client_path: window_fixture_client_path,
|
|
231
|
+
ctrl_name: 'Bundling_Default_Window_App'
|
|
232
|
+
});
|
|
233
|
+
server_instance = started_server.server_instance;
|
|
234
|
+
|
|
235
|
+
const open_result = await open_page(
|
|
236
|
+
browser_instance,
|
|
237
|
+
`http://127.0.0.1:${started_server.port}/`,
|
|
238
|
+
{ wait_until: 'domcontentloaded' }
|
|
239
|
+
);
|
|
240
|
+
page = open_result.page;
|
|
241
|
+
page_probe = open_result.page_probe;
|
|
242
|
+
|
|
243
|
+
await page.waitForSelector('.bundle-test-window');
|
|
244
|
+
await page.waitForSelector('.bundle-test-window-content');
|
|
245
|
+
|
|
246
|
+
const window_buttons = await page.$$('.bundle-test-window .title.bar button.button');
|
|
247
|
+
assert(window_buttons.length >= 2, 'Expected window title bar controls to be rendered');
|
|
248
|
+
|
|
249
|
+
const bundle_response = await read_js_bundle_from_page(page);
|
|
250
|
+
assert.strictEqual(bundle_response.status_code, 200, 'Expected /js/js.js to load');
|
|
251
|
+
const markers = find_window_markers(bundle_response.body_text);
|
|
252
|
+
assert(markers.length > 0, 'Expected Window markers when Window control is used');
|
|
253
|
+
|
|
254
|
+
assert_clean_page_probe(page_probe);
|
|
255
|
+
} finally {
|
|
256
|
+
await close_page_with_probe(page, page_probe);
|
|
257
|
+
await stop_server_instance_with_timeout(server_instance);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -309,23 +309,26 @@ describe('Configuration Validation Tests', function() {
|
|
|
309
309
|
assert.strictEqual(bundler.minify_config.enabled, false);
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
-
it('should default to enabled minification', function() {
|
|
313
|
-
const bundler = new Core_JS_Single_File_Minifying_Bundler_Using_ESBuild({
|
|
314
|
-
minify: {
|
|
315
|
-
level: 'aggressive'
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
assert.strictEqual(bundler.minify_config.
|
|
320
|
-
assert.
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('should handle missing minify configuration', function() {
|
|
324
|
-
const bundler = new Core_JS_Single_File_Minifying_Bundler_Using_ESBuild({});
|
|
325
|
-
|
|
326
|
-
assert.deepStrictEqual(bundler.minify_config, {
|
|
327
|
-
|
|
328
|
-
|
|
312
|
+
it('should default to enabled minification', function() {
|
|
313
|
+
const bundler = new Core_JS_Single_File_Minifying_Bundler_Using_ESBuild({
|
|
314
|
+
minify: {
|
|
315
|
+
level: 'aggressive'
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
assert.strictEqual(bundler.minify_config.level, 'aggressive');
|
|
320
|
+
assert.notStrictEqual(bundler.get_minify_options(), false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should handle missing minify configuration', function() {
|
|
324
|
+
const bundler = new Core_JS_Single_File_Minifying_Bundler_Using_ESBuild({});
|
|
325
|
+
|
|
326
|
+
assert.deepStrictEqual(bundler.minify_config, {
|
|
327
|
+
enabled: true,
|
|
328
|
+
level: 'normal'
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
329
332
|
|
|
330
333
|
describe('Core_JS_Non_Minifying_Bundler_Using_ESBuild', function() {
|
|
331
334
|
it('should accept valid sourcemap configuration', function() {
|
|
@@ -527,4 +530,4 @@ describe('Configuration Validation Tests', function() {
|
|
|
527
530
|
assert.deepStrictEqual(minimalPublisher.bundler_config, {});
|
|
528
531
|
});
|
|
529
532
|
});
|
|
530
|
-
});
|
|
533
|
+
});
|
|
@@ -571,12 +571,13 @@ describe('Content Analysis Tests', function() {
|
|
|
571
571
|
})
|
|
572
572
|
};
|
|
573
573
|
|
|
574
|
-
const results = {};
|
|
575
|
-
|
|
576
|
-
for (const [name, bundler] of Object.entries(bundlers)) {
|
|
577
|
-
const startTime = Date.now();
|
|
578
|
-
const
|
|
579
|
-
const
|
|
574
|
+
const results = {};
|
|
575
|
+
|
|
576
|
+
for (const [name, bundler] of Object.entries(bundlers)) {
|
|
577
|
+
const startTime = Date.now();
|
|
578
|
+
const bundle_input = name === 'minifying' ? testJsContent : testJsFile;
|
|
579
|
+
const result = await bundler.bundle(bundle_input);
|
|
580
|
+
const endTime = Date.now();
|
|
580
581
|
|
|
581
582
|
const bundle = result[0];
|
|
582
583
|
const jsItem = bundle._arr.find(item => item.type === 'JavaScript');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { describe, it } = require('mocha');
|
|
4
|
+
|
|
5
|
+
const JSGUI3_HTML_Control_Optimizer = require('../resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer');
|
|
6
|
+
|
|
7
|
+
const entry_file_path = path.join(__dirname, 'fixtures', 'bundling-default-button-client.js');
|
|
8
|
+
|
|
9
|
+
describe('Control Optimizer Cache Behavior Tests', function () {
|
|
10
|
+
this.timeout(60000);
|
|
11
|
+
|
|
12
|
+
it('records cache hits on repeated optimize calls when cache is enabled', async function () {
|
|
13
|
+
const optimizer = new JSGUI3_HTML_Control_Optimizer({
|
|
14
|
+
package_name: 'jsgui3-html',
|
|
15
|
+
cacheEnabled: true,
|
|
16
|
+
sharedCache: false
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const first_result = await optimizer.optimize(entry_file_path);
|
|
20
|
+
const second_result = await optimizer.optimize(entry_file_path);
|
|
21
|
+
|
|
22
|
+
assert.strictEqual(first_result.enabled, true);
|
|
23
|
+
assert.strictEqual(second_result.enabled, true);
|
|
24
|
+
assert(first_result.manifest.selected_controls.includes('Button'));
|
|
25
|
+
|
|
26
|
+
const cache_stats = optimizer.cache_stats;
|
|
27
|
+
assert(cache_stats.entry_analysis_misses >= 1, 'Expected at least one entry analysis cache miss');
|
|
28
|
+
assert(cache_stats.entry_analysis_hits >= 1, 'Expected at least one entry analysis cache hit');
|
|
29
|
+
assert(cache_stats.controls_map_hits >= 1, 'Expected cached controls map reuse');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('avoids cache hits when cache is disabled', async function () {
|
|
33
|
+
const optimizer = new JSGUI3_HTML_Control_Optimizer({
|
|
34
|
+
package_name: 'jsgui3-html',
|
|
35
|
+
cacheEnabled: false,
|
|
36
|
+
sharedCache: false
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const first_result = await optimizer.optimize(entry_file_path);
|
|
40
|
+
const second_result = await optimizer.optimize(entry_file_path);
|
|
41
|
+
|
|
42
|
+
assert.strictEqual(first_result.enabled, true);
|
|
43
|
+
assert.strictEqual(second_result.enabled, true);
|
|
44
|
+
|
|
45
|
+
const cache_stats = optimizer.cache_stats;
|
|
46
|
+
assert.strictEqual(cache_stats.entry_analysis_hits, 0, 'Expected no entry analysis cache hits');
|
|
47
|
+
assert.strictEqual(cache_stats.file_scan_hits, 0, 'Expected no file scan cache hits');
|
|
48
|
+
assert.strictEqual(cache_stats.controls_map_hits, 0, 'Expected no controls map cache hits');
|
|
49
|
+
assert(cache_stats.file_scan_misses >= 2, 'Expected repeated uncached file scans');
|
|
50
|
+
assert(cache_stats.controls_map_misses >= 2, 'Expected repeated uncached controls map reads');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { describe, it, before, after } = require('mocha');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const Advanced_JS_Bundler_Using_ESBuild = require('../resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild');
|
|
7
|
+
|
|
8
|
+
const fixture_file_path = path.join(__dirname, 'fixtures', 'control_scan_manifest_expectations.json');
|
|
9
|
+
const update_snapshots = process.env.UPDATE_CONTROL_SCAN_MANIFEST === '1';
|
|
10
|
+
|
|
11
|
+
const normalize_manifest = (manifest) => {
|
|
12
|
+
const safe_array = (value) => Array.isArray(value) ? Array.from(value).sort() : [];
|
|
13
|
+
const safe_paths = (value) => Array.isArray(value) ? value.map(file_path => path.basename(file_path)).sort() : [];
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
entry_file: manifest && manifest.entry_file_path ? path.basename(manifest.entry_file_path) : null,
|
|
17
|
+
uses_jsgui3_html: Boolean(manifest && manifest.uses_jsgui3_html),
|
|
18
|
+
dynamic_control_access_detected: Boolean(manifest && manifest.dynamic_control_access_detected),
|
|
19
|
+
reachable_files: safe_paths(manifest && manifest.reachable_files),
|
|
20
|
+
used_identifiers: safe_array(manifest && manifest.used_identifiers),
|
|
21
|
+
selected_controls: safe_array(manifest && manifest.selected_controls),
|
|
22
|
+
unmatched_identifiers: safe_array(manifest && manifest.unmatched_identifiers),
|
|
23
|
+
package_aliases: safe_array(manifest && manifest.package_aliases),
|
|
24
|
+
controls_aliases: safe_array(manifest && manifest.controls_aliases)
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const extract_scan_manifest = (bundle_result) => {
|
|
29
|
+
const bundle = bundle_result[0];
|
|
30
|
+
const analysis = bundle && bundle.bundle_analysis && bundle.bundle_analysis.jsgui3_html_control_scan;
|
|
31
|
+
assert(analysis, 'Expected bundle_analysis.jsgui3_html_control_scan metadata');
|
|
32
|
+
return analysis;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('Control Scan Manifest Regression Tests', function () {
|
|
36
|
+
this.timeout(120000);
|
|
37
|
+
|
|
38
|
+
const temp_fixture_paths = [];
|
|
39
|
+
|
|
40
|
+
const write_temp_fixture = async (file_name, source_text) => {
|
|
41
|
+
const temp_path = path.join(__dirname, file_name);
|
|
42
|
+
await fs.writeFile(temp_path, source_text, 'utf8');
|
|
43
|
+
temp_fixture_paths.push(temp_path);
|
|
44
|
+
return temp_path;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
before(async function () {
|
|
48
|
+
// Ensure fixture directory exists when update mode writes snapshots.
|
|
49
|
+
await fs.mkdir(path.dirname(fixture_file_path), { recursive: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
after(async function () {
|
|
53
|
+
await Promise.all(temp_fixture_paths.map(async (temp_path) => {
|
|
54
|
+
try {
|
|
55
|
+
await fs.unlink(temp_path);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Ignore missing temp files.
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should match strict manifest snapshot for static and dynamic alias usage', async function () {
|
|
63
|
+
const static_alias_fixture_path = await write_temp_fixture('temp_control_scan_static_alias_client.js', `
|
|
64
|
+
const ui = require('jsgui3-html');
|
|
65
|
+
const ui_controls = ui.controls;
|
|
66
|
+
const { Button: Button_Control } = ui_controls;
|
|
67
|
+
|
|
68
|
+
class Tiny_Static_Alias_App extends ui_controls.Active_HTML_Document {
|
|
69
|
+
constructor(spec = {}) {
|
|
70
|
+
super(spec);
|
|
71
|
+
if (!spec.el) {
|
|
72
|
+
const button = new Button_Control({ context: this.context });
|
|
73
|
+
button.add('ok');
|
|
74
|
+
this.body.add(button);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { Tiny_Static_Alias_App };
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
const dynamic_alias_fixture_path = await write_temp_fixture('temp_control_scan_dynamic_alias_client.js', `
|
|
83
|
+
const ui = require('jsgui3-html');
|
|
84
|
+
const ui_controls = ui.controls;
|
|
85
|
+
|
|
86
|
+
class Tiny_Dynamic_Alias_App extends ui_controls.Active_HTML_Document {
|
|
87
|
+
constructor(spec = {}) {
|
|
88
|
+
super(spec);
|
|
89
|
+
if (!spec.el) {
|
|
90
|
+
const control_name = 'Button';
|
|
91
|
+
const Dynamic_Control = ui_controls[control_name];
|
|
92
|
+
const button = new Dynamic_Control({ context: this.context });
|
|
93
|
+
button.add('ok');
|
|
94
|
+
this.body.add(button);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { Tiny_Dynamic_Alias_App };
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
const bundler = new Advanced_JS_Bundler_Using_ESBuild({
|
|
103
|
+
debug: false,
|
|
104
|
+
bundler: {
|
|
105
|
+
minify: {
|
|
106
|
+
enabled: true,
|
|
107
|
+
level: 'normal'
|
|
108
|
+
},
|
|
109
|
+
elimination: {
|
|
110
|
+
enabled: true,
|
|
111
|
+
jsgui3_html_controls: {
|
|
112
|
+
enabled: true
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const static_manifest = extract_scan_manifest(await bundler.bundle(static_alias_fixture_path));
|
|
119
|
+
const dynamic_manifest = extract_scan_manifest(await bundler.bundle(dynamic_alias_fixture_path));
|
|
120
|
+
|
|
121
|
+
const snapshot = {
|
|
122
|
+
static_alias: normalize_manifest(static_manifest),
|
|
123
|
+
dynamic_alias: normalize_manifest(dynamic_manifest)
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (update_snapshots) {
|
|
127
|
+
await fs.writeFile(fixture_file_path, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let expected_snapshot;
|
|
132
|
+
try {
|
|
133
|
+
const expected_text = await fs.readFile(fixture_file_path, 'utf8');
|
|
134
|
+
expected_snapshot = JSON.parse(expected_text);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
assert.fail(
|
|
137
|
+
`Missing manifest expectation fixture at ${fixture_file_path}. ` +
|
|
138
|
+
'Run with UPDATE_CONTROL_SCAN_MANIFEST=1 to generate it.'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
assert.deepStrictEqual(snapshot, expected_snapshot);
|
|
143
|
+
});
|
|
144
|
+
});
|