jsgui3-server 0.0.151 → 0.0.152
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/api-reference.md +120 -2
- 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/configuration-reference.md +54 -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 +11 -8
- 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/query-resource.js +131 -0
- package/serve-factory.js +728 -18
- package/server.js +421 -103
- package/tests/README.md +23 -1
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -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 +1 -0
package/tests/README.md
CHANGED
|
@@ -36,11 +36,13 @@ tests/
|
|
|
36
36
|
├── control-optimizer-cache-behavior.test.js # Optimizer cache enable/disable behavior
|
|
37
37
|
├── examples-controls.e2e.test.js # Example apps regression (controls)
|
|
38
38
|
├── sass-controls.e2e.test.js # Sass/CSS controls E2E coverage
|
|
39
|
+
├── playwright-smoke.test.js # Playwright browser smoke test for local page serving
|
|
39
40
|
├── jsgui3-html-examples.puppeteer.test.js # Puppeteer interaction tests (jsgui3-html examples)
|
|
40
41
|
├── bundling-default-control-elimination.puppeteer.test.js # Puppeteer: default control elimination bundle checks
|
|
41
42
|
├── window-examples.puppeteer.test.js # Puppeteer interaction tests (window examples)
|
|
42
43
|
├── window-resource-integration.puppeteer.test.js # Browser E2E: controls + resource APIs + SSE
|
|
43
44
|
├── helpers/puppeteer-e2e-harness.js # Shared Puppeteer story runner + probes
|
|
45
|
+
├── helpers/playwright-e2e-harness.js # Shared Playwright story runner + probes
|
|
44
46
|
├── test-runner.js # Custom test runner with reporting
|
|
45
47
|
└── README.md # This file
|
|
46
48
|
```
|
|
@@ -87,6 +89,16 @@ npm run test:puppeteer:bundling
|
|
|
87
89
|
npm run test:puppeteer:resources
|
|
88
90
|
```
|
|
89
91
|
|
|
92
|
+
### Install Playwright Browser Runtime
|
|
93
|
+
```bash
|
|
94
|
+
npm run test:playwright:install
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Run Playwright Smoke Test
|
|
98
|
+
```bash
|
|
99
|
+
npm run test:playwright:smoke
|
|
100
|
+
```
|
|
101
|
+
|
|
90
102
|
### Run Tests with Options
|
|
91
103
|
```bash
|
|
92
104
|
# Debug mode (enables sourcemaps)
|
|
@@ -105,7 +117,8 @@ node tests/test-runner.js --test=end-to-end.test.js --debug
|
|
|
105
117
|
2. Run the example regression suite next (HTML/CSS/JS smoke checks).
|
|
106
118
|
3. Run Puppeteer interaction tests last (heavier, requires a browser).
|
|
107
119
|
4. Run the resource integration Puppeteer suite when changing resources/SSE APIs.
|
|
108
|
-
5. Run the
|
|
120
|
+
5. Run the Playwright smoke test to validate alternate browser automation coverage.
|
|
121
|
+
6. Run the full suite only when changes are broad or before release.
|
|
109
122
|
|
|
110
123
|
Suggested sequence:
|
|
111
124
|
```bash
|
|
@@ -114,6 +127,7 @@ npm run test:examples:controls
|
|
|
114
127
|
npm run test:puppeteer:bundling
|
|
115
128
|
npm run test:puppeteer:windows
|
|
116
129
|
npm run test:puppeteer:resources
|
|
130
|
+
npm run test:playwright:smoke
|
|
117
131
|
npm test
|
|
118
132
|
```
|
|
119
133
|
|
|
@@ -133,6 +147,14 @@ Key patterns:
|
|
|
133
147
|
- SSE propagation (`/events`) reflected in client UI/debug state.
|
|
134
148
|
- Keep selectors stable (`id` or `data-test`) so interaction tests remain robust.
|
|
135
149
|
|
|
150
|
+
## Playwright Support
|
|
151
|
+
|
|
152
|
+
Playwright support follows the same server-first test model as Puppeteer:
|
|
153
|
+
|
|
154
|
+
- `tests/playwright-smoke.test.js` validates that Playwright can open a served page and load JS/CSS bundles.
|
|
155
|
+
- `tests/helpers/playwright-e2e-harness.js` mirrors probe/story helpers so new Playwright interaction tests can be added with minimal boilerplate.
|
|
156
|
+
- Use `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` if you need to force a specific Chromium binary.
|
|
157
|
+
|
|
136
158
|
## Patterns That Work
|
|
137
159
|
|
|
138
160
|
- Use per-test temporary client files and delete them in `finally`.
|
|
@@ -154,7 +154,18 @@ describe('Admin UI jsgui control integration', function() {
|
|
|
154
154
|
it('renders settings snapshot with key-value rows and logout button control', async () => {
|
|
155
155
|
const admin_shell = create_admin_shell();
|
|
156
156
|
admin_shell._fetch_json = () => Promise.resolve({
|
|
157
|
-
server: {
|
|
157
|
+
server: {
|
|
158
|
+
name: 'Example Server',
|
|
159
|
+
primary_endpoint: 'http://127.0.0.1:52000/',
|
|
160
|
+
listening_endpoints: [
|
|
161
|
+
{ url: 'http://127.0.0.1:52000/' },
|
|
162
|
+
{ url: 'http://192.168.1.2:52000/' }
|
|
163
|
+
],
|
|
164
|
+
startup_diagnostics: {
|
|
165
|
+
requested_port: 52000,
|
|
166
|
+
addresses_attempted: ['127.0.0.1', '192.168.1.2']
|
|
167
|
+
}
|
|
168
|
+
},
|
|
158
169
|
process: {
|
|
159
170
|
node_version: 'v22.0.0',
|
|
160
171
|
platform: 'linux',
|
|
@@ -169,6 +180,10 @@ describe('Admin UI jsgui control integration', function() {
|
|
|
169
180
|
assert(dynamic_html.includes('Example Server'));
|
|
170
181
|
assert(dynamic_html.includes('v22.0.0'));
|
|
171
182
|
assert(dynamic_html.includes('as-logout-btn'));
|
|
183
|
+
assert(dynamic_html.includes('Startup & Network'));
|
|
184
|
+
assert(dynamic_html.includes('Primary Endpoint'));
|
|
185
|
+
assert(dynamic_html.includes('127.0.0.1:52000'));
|
|
186
|
+
assert(dynamic_html.includes('Requested Port'));
|
|
172
187
|
});
|
|
173
188
|
|
|
174
189
|
it('renders custom section data for array, object, and scalar payloads', () => {
|
|
@@ -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
|
+
};
|