@rightkit/qa 0.4.5 → 0.4.6
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 +2 -2
- package/dist/page.d.ts +17 -2
- package/dist/page.js +76 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ try {
|
|
|
93
93
|
- `page.fill(target, value, { replace?, verify? })`. The default inserts at the caret, as `type()` does. `replace: true` selects the whole field first; an empty value then deletes it. `verify: true` (requires `replace`) reads the value back once and throws `FillMismatchError` on a difference. The error gives lengths, not values.
|
|
94
94
|
- `page.mouse.wheel({ deltaX?, deltaY? })` sends one `mouseWheel` event at the tracked pointer position. The pointer does not move.
|
|
95
95
|
- `page.setUserAgent(ua, { platform?, acceptLanguage? })` overrides the user agent for the page and its later navigations. Each call replaces the whole override. `page.resetUserAgent()` restores the `navigator.userAgent` captured before the first override.
|
|
96
|
-
- `waitForFunction(fn, { failureArtifacts: { directory, label? } })` and `waitForSelector(...)` write a screenshot and a JSON state dump
|
|
96
|
+
- `waitForFunction(fn, { failureArtifacts: { directory, label?, extra?, allowInsideRunRoot? } })` and `waitForSelector(...)` write a screenshot and a JSON state dump on timeout: url, title, bounded page text (`pageText`), the focused element's descriptor without its value (`activeElement`), console entries, and `extra()` if given (`extra`, or `extraError` when it throws, never settles or cannot serialise). Paths are on `error.artifacts` (`json`, `screenshot?`). `directory` must be absolute and must not lie inside `RIGHTKIT_RUN_ROOT`, which `disposeQaWorkspace` deletes; `allowInsideRunRoot: true` opts in, and those files then go with the workspace, so copy them out before dispose. A failed write is appended to the error message; the timeout still throws.
|
|
97
97
|
|
|
98
98
|
- `page.send(method, params?, timeout?)` is a page-scoped raw CDP escape hatch: unsupported surface, with Chrome's names, params and results. Prefer the typed methods. `page.setFocusEmulation(enabled)` sends `Emulation.setFocusEmulationEnabled`.
|
|
99
99
|
- Navigation and reloads: `evaluate`, `waitForFunction` and `waitForSelector` re-resolve the main-frame context when Chrome reports it missing, and retry within the caller's timeout.
|
|
@@ -143,5 +143,5 @@ This package launches no native app and no WebView2 window; it never replaces th
|
|
|
143
143
|
|
|
144
144
|
## Known issues
|
|
145
145
|
|
|
146
|
-
- `@rightkit/qa-chrome` is pinned exactly to `0.1.
|
|
146
|
+
- `@rightkit/qa-chrome` is pinned exactly to `0.1.5`. A qa-chrome release therefore needs a `@rightkit/qa` republish to reach consumers (npm audit 2026-10-08, qa finding).
|
|
147
147
|
- Three consumers pin three different versions of `@rightkit/qa-chrome` as dev dependencies (npm audit 2026-10-08).
|
package/dist/page.d.ts
CHANGED
|
@@ -16,10 +16,24 @@ export interface ViewportOptions {
|
|
|
16
16
|
}
|
|
17
17
|
/** Where a timed-out wait writes its screenshot and JSON state dump. */
|
|
18
18
|
export interface FailureArtifactOptions {
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Absolute directory, created if missing. Must lie outside RIGHTKIT_RUN_ROOT, which
|
|
21
|
+
* disposeQaWorkspace deletes, unless allowInsideRunRoot is set.
|
|
22
|
+
*/
|
|
20
23
|
directory: string;
|
|
21
24
|
/** Short name used in the file names and the dump. Default 'wait'. */
|
|
22
25
|
label?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Opt-in: the directory may lie inside RIGHTKIT_RUN_ROOT. These files are deleted with the
|
|
28
|
+
* workspace by disposeQaWorkspace; copy them out before dispose. Default false.
|
|
29
|
+
*/
|
|
30
|
+
allowInsideRunRoot?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Host state to embed in the JSON dump under `extra` (bounded to 64 KiB, cut off after 1 s). A
|
|
33
|
+
* throw, a timeout or an unserialisable result is recorded under `extraError` and never masks the
|
|
34
|
+
* timeout.
|
|
35
|
+
*/
|
|
36
|
+
extra?: () => unknown | Promise<unknown>;
|
|
23
37
|
}
|
|
24
38
|
/** Called once per poll, before the predicate. A throw rejects the wait with that error. */
|
|
25
39
|
export type PollHook = () => void | Promise<void>;
|
|
@@ -263,9 +277,10 @@ export declare class QaPage {
|
|
|
263
277
|
private selectAll;
|
|
264
278
|
/** Throws ElementOccludedError naming the element on top unless the target itself or a composed descendant is hit. */
|
|
265
279
|
private assertUnoccluded;
|
|
280
|
+
/** URL, title, bounded page text and the active element. Each part is null when unreadable; never throws. */
|
|
281
|
+
private failureState;
|
|
266
282
|
/** Writes a PNG and a JSON state dump for a timed-out wait into the caller directory. */
|
|
267
283
|
private captureFailure;
|
|
268
|
-
/** Attaches artifact paths to a timeout error. A write failure is appended to its message; the timeout still throws. */
|
|
269
284
|
/**
|
|
270
285
|
* Writes the failure artifacts for a timed-out wait or a failed onPoll hook. The paths are attached as
|
|
271
286
|
* `artifacts` only on a TimeoutError; for other errors the files are written and the paths are not attached.
|
package/dist/page.js
CHANGED
|
@@ -96,22 +96,53 @@ function exceptionEntry(event) {
|
|
|
96
96
|
function failureTarget(options) {
|
|
97
97
|
if (!options)
|
|
98
98
|
return undefined;
|
|
99
|
-
const { directory, label } = options;
|
|
99
|
+
const { directory, label, extra, allowInsideRunRoot } = options;
|
|
100
100
|
if (typeof directory !== 'string' || !path.isAbsolute(directory))
|
|
101
101
|
throw new Error('failureArtifacts.directory must be an absolute path');
|
|
102
102
|
if (label !== undefined && typeof label !== 'string')
|
|
103
103
|
throw new Error('failureArtifacts.label must be a string');
|
|
104
|
+
if (extra !== undefined && typeof extra !== 'function')
|
|
105
|
+
throw new Error('failureArtifacts.extra must be a function');
|
|
104
106
|
const resolved = path.resolve(directory);
|
|
105
107
|
const runRoot = process.env.RIGHTKIT_RUN_ROOT;
|
|
106
|
-
if (runRoot) {
|
|
108
|
+
if (runRoot && !allowInsideRunRoot) {
|
|
107
109
|
const relative = path.relative(path.resolve(runRoot), resolved);
|
|
108
110
|
const inside = relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
|
|
109
111
|
if (inside)
|
|
110
112
|
throw new Error('failureArtifacts.directory must be outside RIGHTKIT_RUN_ROOT: disposeQaWorkspace deletes that tree');
|
|
111
113
|
}
|
|
112
|
-
return { directory: resolved, label: (label ?? 'wait').replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 60) || 'wait' };
|
|
114
|
+
return { directory: resolved, label: (label ?? 'wait').replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 60) || 'wait', extra };
|
|
113
115
|
}
|
|
114
116
|
let failureSequence = 0;
|
|
117
|
+
/** Bounds for the failure dump: extra state, page text, and the time a host callback may take. */
|
|
118
|
+
const EXTRA_LIMIT = 64 * 1024;
|
|
119
|
+
const EXTRA_TIMEOUT_MS = 1_000;
|
|
120
|
+
const PAGE_TEXT_LIMIT = 20_000;
|
|
121
|
+
function errorText(error) { return error instanceof Error ? error.message : String(error); }
|
|
122
|
+
/** Runs the host's extra callback under a bound, and keeps its JSON within EXTRA_LIMIT. Never throws. */
|
|
123
|
+
async function runExtra(extra) {
|
|
124
|
+
if (!extra)
|
|
125
|
+
return { value: null, error: null };
|
|
126
|
+
let timer;
|
|
127
|
+
try {
|
|
128
|
+
const value = await Promise.race([
|
|
129
|
+
Promise.resolve().then(() => extra()),
|
|
130
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`extra timed out after ${EXTRA_TIMEOUT_MS}ms`)), EXTRA_TIMEOUT_MS); }),
|
|
131
|
+
]);
|
|
132
|
+
const json = JSON.stringify(value);
|
|
133
|
+
if (json === undefined)
|
|
134
|
+
return { value: null, error: null };
|
|
135
|
+
if (json.length > EXTRA_LIMIT)
|
|
136
|
+
return { value: { truncated: true, length: json.length, preview: json.slice(0, EXTRA_LIMIT) }, error: null };
|
|
137
|
+
return { value: JSON.parse(json), error: null };
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return { value: null, error: errorText(error) };
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
115
146
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
116
147
|
function functionSource(fn) {
|
|
117
148
|
const source = fn.toString();
|
|
@@ -949,12 +980,38 @@ export class QaPage {
|
|
|
949
980
|
if (occluder !== null)
|
|
950
981
|
throw new ElementOccludedError(occluder, point);
|
|
951
982
|
}
|
|
983
|
+
/** URL, title, bounded page text and the active element. Each part is null when unreadable; never throws. */
|
|
984
|
+
async failureState() {
|
|
985
|
+
try {
|
|
986
|
+
const raw = resultValue(await this.run(() => {
|
|
987
|
+
const el = document.activeElement;
|
|
988
|
+
const attr = (name) => el?.getAttribute(name) || undefined;
|
|
989
|
+
return {
|
|
990
|
+
url: location.href,
|
|
991
|
+
title: document.title,
|
|
992
|
+
text: document.body ? document.body.innerText : '',
|
|
993
|
+
// Descriptor only: never the value, which may be a secret.
|
|
994
|
+
active: el ? { tag: el.tagName.toLowerCase(), id: el.id || undefined, name: attr('name'), type: attr('type'), role: attr('role'), ariaLabel: attr('aria-label') } : null,
|
|
995
|
+
};
|
|
996
|
+
}, [], true, 2_000)) ?? {};
|
|
997
|
+
const text = typeof raw.text === 'string' ? raw.text : '';
|
|
998
|
+
return {
|
|
999
|
+
url: typeof raw.url === 'string' ? raw.url : null,
|
|
1000
|
+
title: typeof raw.title === 'string' ? raw.title : null,
|
|
1001
|
+
pageText: { text: text.slice(0, PAGE_TEXT_LIMIT), truncated: text.length > PAGE_TEXT_LIMIT, length: text.length },
|
|
1002
|
+
activeElement: raw.active ?? null,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
return { url: null, title: null, pageText: null, activeElement: null };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
952
1009
|
/** Writes a PNG and a JSON state dump for a timed-out wait into the caller directory. */
|
|
953
|
-
async captureFailure(
|
|
954
|
-
await mkdir(directory, { recursive: true });
|
|
955
|
-
const stem = `${label}-${Date.now()}-${++failureSequence}`;
|
|
956
|
-
const json = path.join(directory, `${stem}.json`);
|
|
957
|
-
const png = path.join(directory, `${stem}.png`);
|
|
1010
|
+
async captureFailure(target, predicate, timeout) {
|
|
1011
|
+
await mkdir(target.directory, { recursive: true });
|
|
1012
|
+
const stem = `${target.label}-${Date.now()}-${++failureSequence}`;
|
|
1013
|
+
const json = path.join(target.directory, `${stem}.json`);
|
|
1014
|
+
const png = path.join(target.directory, `${stem}.png`);
|
|
958
1015
|
let screenshot;
|
|
959
1016
|
let screenshotError;
|
|
960
1017
|
try {
|
|
@@ -962,28 +1019,28 @@ export class QaPage {
|
|
|
962
1019
|
screenshot = png;
|
|
963
1020
|
}
|
|
964
1021
|
catch (error) {
|
|
965
|
-
screenshotError =
|
|
966
|
-
}
|
|
967
|
-
let where = {};
|
|
968
|
-
try {
|
|
969
|
-
where = resultValue(await this.run(() => ({ url: location.href, title: document.title }), [], true, 2_000)) ?? {};
|
|
1022
|
+
screenshotError = errorText(error);
|
|
970
1023
|
}
|
|
971
|
-
|
|
1024
|
+
const state = await this.failureState();
|
|
1025
|
+
const extra = await runExtra(target.extra);
|
|
972
1026
|
const dump = {
|
|
973
|
-
label,
|
|
1027
|
+
label: target.label,
|
|
974
1028
|
predicate,
|
|
975
1029
|
timeoutMs: timeout,
|
|
976
1030
|
timestamp: new Date().toISOString(),
|
|
977
|
-
url:
|
|
978
|
-
title:
|
|
1031
|
+
url: state.url,
|
|
1032
|
+
title: state.title,
|
|
1033
|
+
pageText: state.pageText,
|
|
1034
|
+
activeElement: state.activeElement,
|
|
979
1035
|
screenshot: screenshot ? path.basename(screenshot) : null,
|
|
980
1036
|
screenshotError: screenshotError ?? null,
|
|
981
1037
|
console: this.consoleEntries(),
|
|
1038
|
+
extra: extra.value,
|
|
1039
|
+
extraError: extra.error,
|
|
982
1040
|
};
|
|
983
1041
|
await writeFile(json, JSON.stringify(dump, null, 2));
|
|
984
1042
|
return screenshot ? { json, screenshot } : { json };
|
|
985
1043
|
}
|
|
986
|
-
/** Attaches artifact paths to a timeout error. A write failure is appended to its message; the timeout still throws. */
|
|
987
1044
|
/**
|
|
988
1045
|
* Writes the failure artifacts for a timed-out wait or a failed onPoll hook. The paths are attached as
|
|
989
1046
|
* `artifacts` only on a TimeoutError; for other errors the files are written and the paths are not attached.
|
|
@@ -991,7 +1048,7 @@ export class QaPage {
|
|
|
991
1048
|
*/
|
|
992
1049
|
async attachFailureArtifacts(error, target, predicate, timeout) {
|
|
993
1050
|
try {
|
|
994
|
-
const written = await this.captureFailure(target
|
|
1051
|
+
const written = await this.captureFailure(target, predicate, timeout);
|
|
995
1052
|
if (error instanceof TimeoutError)
|
|
996
1053
|
error.artifacts = written;
|
|
997
1054
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/qa",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Shared QA evidence, ownership, browser helpers, and release-backdoor guards for Right Suite apps.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@rightkit/qa-chrome": "0.1.
|
|
27
|
+
"@rightkit/qa-chrome": "0.1.5"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^24.0.0",
|