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
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { describe, it, before, after } = require('mocha');
|
|
5
|
+
|
|
6
|
+
const Server = require('../server');
|
|
7
|
+
const { get_free_port } = require('../port-utils');
|
|
8
|
+
const {
|
|
9
|
+
ensure_puppeteer_module,
|
|
10
|
+
launch_puppeteer_browser,
|
|
11
|
+
start_control_example_server,
|
|
12
|
+
stop_server_instance,
|
|
13
|
+
open_page,
|
|
14
|
+
assert_clean_page_probe,
|
|
15
|
+
set_input_value_with_events,
|
|
16
|
+
wait_for_text_content,
|
|
17
|
+
drag_by,
|
|
18
|
+
run_interaction_story,
|
|
19
|
+
wait_for_condition
|
|
20
|
+
} = require('./helpers/puppeteer-e2e-harness');
|
|
21
|
+
|
|
22
|
+
const repo_root_path = path.join(__dirname, '..');
|
|
23
|
+
const examples_controls_root_path = path.join(repo_root_path, 'examples', 'controls');
|
|
24
|
+
const fixture_client_path = path.join(__dirname, 'fixtures', 'resource-monitor-client.js');
|
|
25
|
+
const dummy_client_path = path.join(__dirname, 'dummy-client.js');
|
|
26
|
+
|
|
27
|
+
const read_resource_state = (server_instance, resource_name) => {
|
|
28
|
+
const resource = server_instance.resource_pool.get_resource(resource_name);
|
|
29
|
+
return resource ? resource.status.state : null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const fetch_text_response = (port, route_path = '/') => {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const request = http.get({
|
|
35
|
+
hostname: '127.0.0.1',
|
|
36
|
+
port,
|
|
37
|
+
path: route_path
|
|
38
|
+
}, (response) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
41
|
+
response.on('end', () => {
|
|
42
|
+
resolve({
|
|
43
|
+
status_code: response.statusCode,
|
|
44
|
+
body_text: Buffer.concat(chunks).toString('utf8')
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
request.on('error', reject);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const post_json_response = (port, route_path, payload = {}) => {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const body_text = JSON.stringify(payload);
|
|
56
|
+
const request = http.request({
|
|
57
|
+
hostname: '127.0.0.1',
|
|
58
|
+
port,
|
|
59
|
+
path: route_path,
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'Content-Length': Buffer.byteLength(body_text, 'utf8')
|
|
64
|
+
}
|
|
65
|
+
}, (response) => {
|
|
66
|
+
const chunks = [];
|
|
67
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
68
|
+
response.on('end', () => {
|
|
69
|
+
const response_text = Buffer.concat(chunks).toString('utf8');
|
|
70
|
+
let parsed_body = null;
|
|
71
|
+
try {
|
|
72
|
+
parsed_body = response_text.length ? JSON.parse(response_text) : null;
|
|
73
|
+
} catch {
|
|
74
|
+
parsed_body = response_text;
|
|
75
|
+
}
|
|
76
|
+
resolve({
|
|
77
|
+
status_code: response.statusCode,
|
|
78
|
+
body: parsed_body
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
request.on('error', reject);
|
|
84
|
+
request.write(body_text);
|
|
85
|
+
request.end();
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
describe('Window Controls + Resource Integration Puppeteer Tests', function () {
|
|
90
|
+
this.timeout(180000);
|
|
91
|
+
|
|
92
|
+
let puppeteer_module = null;
|
|
93
|
+
let browser_instance = null;
|
|
94
|
+
|
|
95
|
+
before(async function () {
|
|
96
|
+
this.timeout(60000);
|
|
97
|
+
|
|
98
|
+
puppeteer_module = ensure_puppeteer_module();
|
|
99
|
+
if (!puppeteer_module) {
|
|
100
|
+
this.skip();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
browser_instance = await launch_puppeteer_browser(puppeteer_module);
|
|
106
|
+
} catch {
|
|
107
|
+
this.skip();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
after(async function () {
|
|
112
|
+
if (browser_instance) {
|
|
113
|
+
await browser_instance.close();
|
|
114
|
+
browser_instance = null;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('supports step-based, precise date/datetime control interactions', async () => {
|
|
119
|
+
const { server_instance, port } = await start_control_example_server({
|
|
120
|
+
examples_root_path: examples_controls_root_path,
|
|
121
|
+
dir_name: '9) window, date picker',
|
|
122
|
+
ctrl_name: 'Demo_UI'
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
let page = null;
|
|
126
|
+
let page_probe = null;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const open_result = await open_page(browser_instance, `http://127.0.0.1:${port}/`);
|
|
130
|
+
page = open_result.page;
|
|
131
|
+
page_probe = open_result.page_probe;
|
|
132
|
+
|
|
133
|
+
const initial_datetime_text = { value: '' };
|
|
134
|
+
const initial_time_display = { value: '' };
|
|
135
|
+
|
|
136
|
+
const story_steps = [
|
|
137
|
+
{
|
|
138
|
+
name: 'wait_for_controls',
|
|
139
|
+
run: async (target_page) => {
|
|
140
|
+
await target_page.waitForSelector('input.date-picker[type="date"]');
|
|
141
|
+
await target_page.waitForSelector('.demo-datetime-picker.datetime-picker');
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'set_primary_date',
|
|
146
|
+
run: async (target_page) => {
|
|
147
|
+
await set_input_value_with_events(target_page, 'input.date-picker', '2026-07-15', ['input', 'change']);
|
|
148
|
+
},
|
|
149
|
+
assert: async (target_page) => {
|
|
150
|
+
await target_page.waitForFunction(() => {
|
|
151
|
+
const output_element = document.querySelector('.demo-date-output');
|
|
152
|
+
return output_element && String(output_element.textContent || '').includes('2026-07-15');
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'switch_to_time_tab',
|
|
158
|
+
run: async (target_page) => {
|
|
159
|
+
initial_datetime_text.value = await target_page.$eval('.demo-datetime-output', (element) => {
|
|
160
|
+
return String(element.textContent || '').trim();
|
|
161
|
+
});
|
|
162
|
+
initial_time_display.value = await target_page.$eval('.demo-datetime-picker .tp-display-time', (element) => {
|
|
163
|
+
return String(element.textContent || '').trim();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const clicked = await target_page.evaluate(() => {
|
|
167
|
+
const root_element = document.querySelector('.demo-datetime-picker');
|
|
168
|
+
if (!root_element) return false;
|
|
169
|
+
const time_tab = Array.from(root_element.querySelectorAll('.dtp-tab')).find((tab_element) => {
|
|
170
|
+
return String(tab_element.textContent || '').toLowerCase().includes('time');
|
|
171
|
+
});
|
|
172
|
+
if (!time_tab) return false;
|
|
173
|
+
time_tab.click();
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assert.strictEqual(clicked, true, 'Expected to click DateTime time tab');
|
|
178
|
+
await target_page.waitForSelector('.demo-datetime-picker .time-picker');
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'advance_time_spinners',
|
|
183
|
+
run: async (target_page) => {
|
|
184
|
+
const did_click_spinners = await target_page.evaluate(() => {
|
|
185
|
+
const hour_spinner = document.querySelector('.demo-datetime-picker .tp-spinner-up.tp-h-up');
|
|
186
|
+
const minute_spinner = document.querySelector('.demo-datetime-picker .tp-spinner-up.tp-m-up');
|
|
187
|
+
if (!hour_spinner || !minute_spinner) return false;
|
|
188
|
+
hour_spinner.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
189
|
+
minute_spinner.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
assert.strictEqual(did_click_spinners, true, 'Expected DateTime spinner controls to be present');
|
|
193
|
+
},
|
|
194
|
+
assert: async (target_page) => {
|
|
195
|
+
await target_page.waitForFunction((before_output_text, before_time_text) => {
|
|
196
|
+
const output_element = document.querySelector('.demo-datetime-output');
|
|
197
|
+
const time_element = document.querySelector('.demo-datetime-picker .tp-display-time');
|
|
198
|
+
if (!output_element && !time_element) return false;
|
|
199
|
+
const next_output_text = String((output_element && output_element.textContent) || '').trim();
|
|
200
|
+
const next_time_text = String((time_element && time_element.textContent) || '').trim();
|
|
201
|
+
return next_output_text !== before_output_text || next_time_text !== before_time_text;
|
|
202
|
+
}, {}, initial_datetime_text.value, initial_time_display.value);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'drag_window_and_keep_controls_live',
|
|
207
|
+
run: async (target_page) => {
|
|
208
|
+
await drag_by(target_page, '.window .title.bar', 90, 55);
|
|
209
|
+
await set_input_value_with_events(target_page, 'input.date-picker', '2026-10-04', ['input', 'change']);
|
|
210
|
+
},
|
|
211
|
+
assert: async (target_page) => {
|
|
212
|
+
await target_page.waitForFunction(() => {
|
|
213
|
+
const output_element = document.querySelector('.demo-date-output');
|
|
214
|
+
return output_element && String(output_element.textContent || '').includes('Date value: 2026-10-04');
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const step_results = await run_interaction_story({
|
|
221
|
+
page,
|
|
222
|
+
story_name: 'date_time_control_story',
|
|
223
|
+
steps: story_steps
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
assert.strictEqual(step_results.length, story_steps.length);
|
|
227
|
+
|
|
228
|
+
assert_clean_page_probe(page_probe);
|
|
229
|
+
} finally {
|
|
230
|
+
if (page_probe && typeof page_probe.detach === 'function') {
|
|
231
|
+
page_probe.detach();
|
|
232
|
+
}
|
|
233
|
+
if (page) {
|
|
234
|
+
await page.close();
|
|
235
|
+
}
|
|
236
|
+
await stop_server_instance(server_instance);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('verifies client-side resource lifecycle controls against server resource state and SSE events', async () => {
|
|
241
|
+
const fixture_module = require(fixture_client_path);
|
|
242
|
+
const resource_monitor_ctrl = fixture_module.controls && fixture_module.controls.Resource_Monitor_App;
|
|
243
|
+
assert(resource_monitor_ctrl, 'Expected Resource_Monitor_App control export in test fixture');
|
|
244
|
+
|
|
245
|
+
const port = await get_free_port();
|
|
246
|
+
let server_instance = null;
|
|
247
|
+
|
|
248
|
+
server_instance = await Server.serve({
|
|
249
|
+
host: '127.0.0.1',
|
|
250
|
+
port,
|
|
251
|
+
ctrl: resource_monitor_ctrl,
|
|
252
|
+
clientPath: fixture_client_path,
|
|
253
|
+
resources: {
|
|
254
|
+
e2e_worker: {
|
|
255
|
+
type: 'process',
|
|
256
|
+
command: process.execPath,
|
|
257
|
+
args: ['-e', 'setInterval(() => {}, 1000);'],
|
|
258
|
+
autoRestart: false,
|
|
259
|
+
processManager: {
|
|
260
|
+
type: 'direct'
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
events: true,
|
|
265
|
+
api: {
|
|
266
|
+
'resource/status': () => {
|
|
267
|
+
const resource = server_instance.resource_pool.get_resource('e2e_worker');
|
|
268
|
+
return {
|
|
269
|
+
ok: !!resource,
|
|
270
|
+
status: resource ? resource.status : null,
|
|
271
|
+
summary: server_instance.resource_pool.summary
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
'resource/start': async () => {
|
|
275
|
+
const resource = server_instance.resource_pool.get_resource('e2e_worker');
|
|
276
|
+
if (!resource) {
|
|
277
|
+
return { ok: false, error: 'missing_resource' };
|
|
278
|
+
}
|
|
279
|
+
await resource.start();
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
status: resource.status
|
|
283
|
+
};
|
|
284
|
+
},
|
|
285
|
+
'resource/stop': async () => {
|
|
286
|
+
const resource = server_instance.resource_pool.get_resource('e2e_worker');
|
|
287
|
+
if (!resource) {
|
|
288
|
+
return { ok: false, error: 'missing_resource' };
|
|
289
|
+
}
|
|
290
|
+
await resource.stop();
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
status: resource.status
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
'resource/restart': async () => {
|
|
297
|
+
const resource = server_instance.resource_pool.get_resource('e2e_worker');
|
|
298
|
+
if (!resource) {
|
|
299
|
+
return { ok: false, error: 'missing_resource' };
|
|
300
|
+
}
|
|
301
|
+
await resource.restart();
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
status: resource.status
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
let page = null;
|
|
311
|
+
let page_probe = null;
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const started_running = await wait_for_condition(() => {
|
|
315
|
+
return read_resource_state(server_instance, 'e2e_worker') === 'running';
|
|
316
|
+
}, 6000, 25);
|
|
317
|
+
assert.strictEqual(started_running, true, 'Expected e2e_worker to start in running state');
|
|
318
|
+
|
|
319
|
+
const root_route_ready = await wait_for_condition(async () => {
|
|
320
|
+
try {
|
|
321
|
+
const response = await fetch_text_response(port, '/');
|
|
322
|
+
return response.status_code === 200 && response.body_text.includes('resource-monitor-root');
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}, 20000, 200);
|
|
327
|
+
assert.strictEqual(root_route_ready, true, 'Expected root route to be ready before browser navigation');
|
|
328
|
+
|
|
329
|
+
const open_result = await open_page(browser_instance, `http://127.0.0.1:${port}/`);
|
|
330
|
+
page = open_result.page;
|
|
331
|
+
page_probe = open_result.page_probe;
|
|
332
|
+
|
|
333
|
+
await page.waitForSelector('#resource-state');
|
|
334
|
+
await wait_for_text_content(page, '#resource-state', 'running');
|
|
335
|
+
|
|
336
|
+
const resource_story_steps = [
|
|
337
|
+
{
|
|
338
|
+
name: 'stop_via_client_button',
|
|
339
|
+
run: async (target_page) => {
|
|
340
|
+
await target_page.click('#resource-stop-btn');
|
|
341
|
+
},
|
|
342
|
+
assert: async (target_page) => {
|
|
343
|
+
await wait_for_text_content(target_page, '#resource-state', 'stopped');
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: 'start_via_client_button',
|
|
348
|
+
run: async (target_page) => {
|
|
349
|
+
await target_page.click('#resource-start-btn');
|
|
350
|
+
},
|
|
351
|
+
assert: async (target_page) => {
|
|
352
|
+
await wait_for_text_content(target_page, '#resource-state', 'running');
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: 'restart_via_client_button',
|
|
357
|
+
run: async (target_page) => {
|
|
358
|
+
await target_page.click('#resource-restart-btn');
|
|
359
|
+
},
|
|
360
|
+
assert: async (target_page) => {
|
|
361
|
+
const ready_running = await target_page.waitForFunction(() => {
|
|
362
|
+
const state_element = document.getElementById('resource-state');
|
|
363
|
+
const events_count_element = document.getElementById('resource-events-count');
|
|
364
|
+
if (!state_element || !events_count_element) return false;
|
|
365
|
+
|
|
366
|
+
const state_text = String(state_element.textContent || '').trim();
|
|
367
|
+
const events_count = Number.parseInt(String(events_count_element.textContent || '0'), 10) || 0;
|
|
368
|
+
return state_text === 'running' && events_count >= 2;
|
|
369
|
+
}, { timeout: 8000 });
|
|
370
|
+
|
|
371
|
+
assert(ready_running, 'Expected running state and recorded resource events after restart');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const story_result = await run_interaction_story({
|
|
377
|
+
page,
|
|
378
|
+
story_name: 'resource_control_story',
|
|
379
|
+
steps: resource_story_steps
|
|
380
|
+
});
|
|
381
|
+
assert.strictEqual(story_result.length, resource_story_steps.length);
|
|
382
|
+
|
|
383
|
+
const client_debug_state = await page.evaluate(() => {
|
|
384
|
+
return window.__resource_client_debug || { api_calls: [], sse_events: [] };
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const invoked_routes = client_debug_state.api_calls.map((entry) => entry.route);
|
|
388
|
+
assert(invoked_routes.includes('/api/resource/status'), 'Expected status API route invocation from client');
|
|
389
|
+
assert(invoked_routes.includes('/api/resource/stop'), 'Expected stop API route invocation from client');
|
|
390
|
+
assert(invoked_routes.includes('/api/resource/start'), 'Expected start API route invocation from client');
|
|
391
|
+
assert(invoked_routes.includes('/api/resource/restart'), 'Expected restart API route invocation from client');
|
|
392
|
+
|
|
393
|
+
const has_sse_state_event = client_debug_state.sse_events.some((entry) => {
|
|
394
|
+
return entry && entry.event_name === 'resource_state_change';
|
|
395
|
+
});
|
|
396
|
+
assert.strictEqual(has_sse_state_event, true, 'Expected client to receive resource_state_change SSE events');
|
|
397
|
+
|
|
398
|
+
const settled_running = await wait_for_condition(() => {
|
|
399
|
+
return read_resource_state(server_instance, 'e2e_worker') === 'running';
|
|
400
|
+
}, 6000, 25);
|
|
401
|
+
assert.strictEqual(settled_running, true, 'Expected e2e_worker to settle back to running state');
|
|
402
|
+
|
|
403
|
+
assert_clean_page_probe(page_probe, {
|
|
404
|
+
allowed_request_failure_patterns: [
|
|
405
|
+
'/events',
|
|
406
|
+
'ERR_ABORTED'
|
|
407
|
+
]
|
|
408
|
+
});
|
|
409
|
+
} finally {
|
|
410
|
+
if (page_probe && typeof page_probe.detach === 'function') {
|
|
411
|
+
page_probe.detach();
|
|
412
|
+
}
|
|
413
|
+
if (page) {
|
|
414
|
+
await page.close();
|
|
415
|
+
}
|
|
416
|
+
await stop_server_instance(server_instance);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('replays SSE events after reconnect using lastEventId continuity', async () => {
|
|
421
|
+
const dummy_module = require(dummy_client_path);
|
|
422
|
+
const dummy_ctrl = dummy_module.controls && dummy_module.controls.Dummy_Control;
|
|
423
|
+
assert(dummy_ctrl, 'Expected Dummy_Control export in dummy test client');
|
|
424
|
+
|
|
425
|
+
const port = await get_free_port();
|
|
426
|
+
let server_instance = null;
|
|
427
|
+
|
|
428
|
+
server_instance = await Server.serve({
|
|
429
|
+
host: '127.0.0.1',
|
|
430
|
+
port,
|
|
431
|
+
ctrl: dummy_ctrl,
|
|
432
|
+
clientPath: dummy_client_path,
|
|
433
|
+
events: true,
|
|
434
|
+
api: {
|
|
435
|
+
'events/push': (input) => {
|
|
436
|
+
const payload = input && typeof input === 'object' ? input : {};
|
|
437
|
+
const sequence = Number(payload.seq) || 0;
|
|
438
|
+
const publish_result = server_instance.sse_publisher.broadcast('manual_test', {
|
|
439
|
+
seq: sequence
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
eventId: publish_result.eventId
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
let page = null;
|
|
450
|
+
let page_probe = null;
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const root_route_ready = await wait_for_condition(async () => {
|
|
454
|
+
try {
|
|
455
|
+
const response = await fetch_text_response(port, '/');
|
|
456
|
+
return response.status_code === 200;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}, 20000, 200);
|
|
461
|
+
assert.strictEqual(root_route_ready, true, 'Expected root route to be ready before browser navigation');
|
|
462
|
+
|
|
463
|
+
const open_result = await open_page(browser_instance, `http://127.0.0.1:${port}/`);
|
|
464
|
+
page = open_result.page;
|
|
465
|
+
page_probe = open_result.page_probe;
|
|
466
|
+
|
|
467
|
+
await page.evaluate(() => {
|
|
468
|
+
window.__sse_replay_state = {
|
|
469
|
+
events: [],
|
|
470
|
+
connected: false
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
window.__connect_replay_source = (last_event_id) => {
|
|
474
|
+
if (window.__replay_event_source) {
|
|
475
|
+
try {
|
|
476
|
+
window.__replay_event_source.close();
|
|
477
|
+
} catch {
|
|
478
|
+
// Ignore close errors.
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const query_suffix = last_event_id
|
|
483
|
+
? `?clientId=replay_client&lastEventId=${encodeURIComponent(String(last_event_id))}`
|
|
484
|
+
: '?clientId=replay_client';
|
|
485
|
+
|
|
486
|
+
const event_source = new EventSource(`/events${query_suffix}`);
|
|
487
|
+
window.__replay_event_source = event_source;
|
|
488
|
+
window.__sse_replay_state.connected = false;
|
|
489
|
+
|
|
490
|
+
event_source.onopen = () => {
|
|
491
|
+
window.__sse_replay_state.connected = true;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
event_source.addEventListener('manual_test', (event) => {
|
|
495
|
+
let payload = {};
|
|
496
|
+
try {
|
|
497
|
+
payload = JSON.parse(String(event.data || '{}'));
|
|
498
|
+
} catch {
|
|
499
|
+
payload = { raw: event.data };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
window.__sse_replay_state.events.push({
|
|
503
|
+
id: Number(event.lastEventId),
|
|
504
|
+
seq: Number(payload.seq)
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
window.__close_replay_source = () => {
|
|
510
|
+
if (window.__replay_event_source) {
|
|
511
|
+
window.__replay_event_source.close();
|
|
512
|
+
window.__replay_event_source = null;
|
|
513
|
+
}
|
|
514
|
+
window.__sse_replay_state.connected = false;
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await page.evaluate(() => window.__connect_replay_source());
|
|
519
|
+
const did_connect = await wait_for_condition(async () => {
|
|
520
|
+
return page.evaluate(() => window.__sse_replay_state && window.__sse_replay_state.connected === true);
|
|
521
|
+
}, 8000, 25);
|
|
522
|
+
assert.strictEqual(did_connect, true, 'Expected replay EventSource to connect');
|
|
523
|
+
|
|
524
|
+
const first_push = await post_json_response(port, '/api/events/push', { seq: 1 });
|
|
525
|
+
assert.strictEqual(first_push.status_code, 200);
|
|
526
|
+
assert.strictEqual(first_push.body.ok, true);
|
|
527
|
+
|
|
528
|
+
const first_event_received = await wait_for_condition(async () => {
|
|
529
|
+
return page.evaluate(() => {
|
|
530
|
+
const replay_state = window.__sse_replay_state || { events: [] };
|
|
531
|
+
return replay_state.events.some((entry) => entry.seq === 1);
|
|
532
|
+
});
|
|
533
|
+
}, 8000, 25);
|
|
534
|
+
assert.strictEqual(first_event_received, true, 'Expected seq=1 to arrive before disconnect');
|
|
535
|
+
|
|
536
|
+
const first_event_id = await page.evaluate(() => {
|
|
537
|
+
const replay_state = window.__sse_replay_state || { events: [] };
|
|
538
|
+
const first_entry = replay_state.events.find((entry) => entry.seq === 1);
|
|
539
|
+
return first_entry ? first_entry.id : null;
|
|
540
|
+
});
|
|
541
|
+
assert(Number.isFinite(first_event_id), 'Expected first replay event id');
|
|
542
|
+
|
|
543
|
+
await page.evaluate(() => window.__close_replay_source());
|
|
544
|
+
|
|
545
|
+
const second_push = await post_json_response(port, '/api/events/push', { seq: 2 });
|
|
546
|
+
const third_push = await post_json_response(port, '/api/events/push', { seq: 3 });
|
|
547
|
+
assert.strictEqual(second_push.status_code, 200);
|
|
548
|
+
assert.strictEqual(third_push.status_code, 200);
|
|
549
|
+
|
|
550
|
+
await page.evaluate((last_event_id) => window.__connect_replay_source(last_event_id), first_event_id);
|
|
551
|
+
const replay_received = await wait_for_condition(async () => {
|
|
552
|
+
return page.evaluate(() => {
|
|
553
|
+
const replay_state = window.__sse_replay_state || { events: [] };
|
|
554
|
+
const has_seq_2 = replay_state.events.some((entry) => entry.seq === 2);
|
|
555
|
+
const has_seq_3 = replay_state.events.some((entry) => entry.seq === 3);
|
|
556
|
+
return has_seq_2 && has_seq_3;
|
|
557
|
+
});
|
|
558
|
+
}, 8000, 25);
|
|
559
|
+
assert.strictEqual(replay_received, true, 'Expected replayed seq=2 and seq=3 after reconnect');
|
|
560
|
+
|
|
561
|
+
const replay_event_ids = await page.evaluate(() => {
|
|
562
|
+
const replay_state = window.__sse_replay_state || { events: [] };
|
|
563
|
+
return replay_state.events
|
|
564
|
+
.filter((entry) => entry.seq === 2 || entry.seq === 3)
|
|
565
|
+
.map((entry) => entry.id);
|
|
566
|
+
});
|
|
567
|
+
assert(replay_event_ids.every((event_id) => Number.isFinite(event_id) && event_id > first_event_id));
|
|
568
|
+
|
|
569
|
+
assert_clean_page_probe(page_probe, {
|
|
570
|
+
allowed_request_failure_patterns: [
|
|
571
|
+
'/events',
|
|
572
|
+
'ERR_ABORTED'
|
|
573
|
+
]
|
|
574
|
+
});
|
|
575
|
+
} finally {
|
|
576
|
+
if (page_probe && typeof page_probe.detach === 'function') {
|
|
577
|
+
page_probe.detach();
|
|
578
|
+
}
|
|
579
|
+
if (page) {
|
|
580
|
+
await page.close();
|
|
581
|
+
}
|
|
582
|
+
await stop_server_instance(server_instance);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
});
|
package/tests/temp_invalid.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
���
|