@rightkit/qa 0.4.2 → 0.4.3

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 CHANGED
@@ -86,6 +86,28 @@ try {
86
86
 
87
87
  `evaluate` throws the page's JavaScript exception (or a `TimeoutError`) rather than returning an error value. Wrap the call if you need the error as data.
88
88
 
89
+ ## Keyboard keys
90
+
91
+ `page.keyboard.press(chord)` accepts named keys (`Enter`, `Tab`, `Escape`, arrows, `Home`, `End`, `PageUp`, `PageDown`, `Backspace`, `Delete`, `Space`, `Shift`, `Control`, `Alt`, `Meta`), function keys `F1` through `F24`, letters, digits, and every printable US-layout symbol, including backslash.
92
+
93
+ - Shifted symbols send Shift. `?` is Shift+`/`, `+` is Shift+`=`, `!` is Shift+`1`. Such a key carries `modifiers` 8 and its shifted `key` and `text`; no separate Shift key-down is sent.
94
+ - Chords join parts with `+`. A literal plus key goes last: `+` and `Control++`.
95
+ - `code` follows the US layout (`?` has code `Slash`). Other layouts are not modelled.
96
+
97
+ ## Browser escape hatch (unsupported surface)
98
+
99
+ These methods expose Chrome's own DevTools protocol. Their names, params and results follow the browser version, not this package, and may break when Chrome changes. Use the typed `QaPage` API where it exists.
100
+
101
+ - `browser.send(method, params?)` sends one browser-scoped command with no `sessionId` and resolves with its raw result.
102
+ - `browser.version()` returns `Browser.getVersion`.
103
+ - `browser.commandLine()` returns the browser argv from `Browser.getBrowserCommandLine` as `string[]`.
104
+
105
+ `commandLine()` needs the browser launched with `--enable-automation`. `launchChromeForTesting` does not pass that flag, so launch with `launchChromeForTesting({ args: ["--enable-automation"] })`. Without it, the call rejects with an error that names the requirement; the browser is unchanged.
106
+
107
+ ## Run root and workspace disposal
108
+
109
+ `createQaWorkspace` sets `RIGHTKIT_RUN_ROOT` to the workspace `home`. `disposeQaWorkspace` deletes that home, and deletes the workspace root too unless `keepEvidence` is set. A host must never map a directory that holds artifacts it wants to keep onto `RIGHTKIT_RUN_ROOT`, or onto the workspace root or home. Copy artifacts out before disposal, or use `keepEvidence: true` and keep anything written under `evidence` or `captures`.
110
+
89
111
  ## Ownership: this package versus the Rust `rightkit-qa` crate
90
112
 
91
113
  Both runners stay. Pick by what the test drives.
@@ -4,6 +4,9 @@ export declare function keyDefinition(key: string, modifiers: number): CdpParams
4
4
  export declare class QaKeyboard {
5
5
  private send;
6
6
  constructor(send: (method: string, params: CdpParams) => Promise<CdpParams>);
7
- /** Single key or chord, e.g. Control+A, Meta+A, Shift+ArrowLeft. */
7
+ /**
8
+ * Single key or chord, e.g. Control+A, Meta+A, Shift+ArrowLeft, F3, ?, Control+/.
9
+ * Function keys F1-F24 and US-layout printable keys (shifted ones included) are supported.
10
+ */
8
11
  press(chord: string): Promise<void>;
9
12
  }
package/dist/keyboard.js CHANGED
@@ -6,45 +6,89 @@ const special = {
6
6
  Shift: ['ShiftLeft', 16], Control: ['ControlLeft', 17], Alt: ['AltLeft', 18], Meta: ['MetaLeft', 91],
7
7
  };
8
8
  export const modifierBits = { Alt: 1, Control: 2, Meta: 4, Shift: 8 };
9
+ /** Function keys F1-F24: code "F<n>", Windows virtual key 111 + n (VK_F1 = 112 ... VK_F24 = 135). */
10
+ const functionKey = /^F(?:[1-9]|1\d|2[0-4])$/;
11
+ /** [base, shifted, code, Windows virtual key] for each US-layout punctuation key. */
12
+ const punctuation = [
13
+ ['`', '~', 'Backquote', 192], ['-', '_', 'Minus', 189], ['=', '+', 'Equal', 187],
14
+ ['[', '{', 'BracketLeft', 219], [']', '}', 'BracketRight', 221], ['\\', '|', 'Backslash', 220],
15
+ [';', ':', 'Semicolon', 186], ["'", '"', 'Quote', 222], [',', '<', 'Comma', 188],
16
+ ['.', '>', 'Period', 190], ['/', '?', 'Slash', 191],
17
+ ];
18
+ const shiftedDigits = ')!@#$%^&*(';
19
+ /** Keyed by the character the key prints, so "?" and "/" both resolve, the first with Shift forced. */
20
+ const printable = new Map();
21
+ for (const [base, shifted, code, keyCode] of punctuation) {
22
+ printable.set(base, { base, shifted, code, keyCode, requiresShift: false });
23
+ printable.set(shifted, { base, shifted, code, keyCode, requiresShift: true });
24
+ }
25
+ for (let digit = 0; digit <= 9; digit++) {
26
+ const base = String(digit), code = `Digit${digit}`, keyCode = 48 + digit;
27
+ printable.set(base, { base, shifted: shiftedDigits[digit], code, keyCode, requiresShift: false });
28
+ printable.set(shiftedDigits[digit], { base, shifted: shiftedDigits[digit], code, keyCode, requiresShift: true });
29
+ }
9
30
  export function keyDefinition(key, modifiers) {
10
31
  const named = special[key];
11
- let code, windowsVirtualKeyCode, text;
12
- if (named)
32
+ let code, windowsVirtualKeyCode, text, unmodifiedText;
33
+ let effective = modifiers;
34
+ if (named) {
13
35
  [code, windowsVirtualKeyCode, text] = named;
36
+ unmodifiedText = text;
37
+ }
38
+ else if (functionKey.test(key)) {
39
+ code = key;
40
+ windowsVirtualKeyCode = 111 + Number(key.slice(1));
41
+ }
14
42
  else if (/^[a-z]$/i.test(key)) {
15
43
  code = `Key${key.toUpperCase()}`;
16
44
  windowsVirtualKeyCode = key.toUpperCase().charCodeAt(0);
17
45
  key = modifiers & 8 ? key.toUpperCase() : modifiers & 7 ? key.toLowerCase() : key;
18
46
  text = key;
47
+ unmodifiedText = code.slice(3).toLowerCase();
19
48
  }
20
- else if (/^[0-9]$/.test(key)) {
21
- code = `Digit${key}`;
22
- windowsVirtualKeyCode = key.charCodeAt(0);
23
- key = modifiers & 8 ? ')!@#$%^&*('[Number(key)] : key;
49
+ else if (printable.has(key)) {
50
+ const symbol = printable.get(key);
51
+ code = symbol.code;
52
+ windowsVirtualKeyCode = symbol.keyCode;
53
+ if (symbol.requiresShift)
54
+ effective |= 8;
55
+ key = effective & 8 ? symbol.shifted : symbol.base;
24
56
  text = key;
57
+ unmodifiedText = symbol.base;
25
58
  }
26
59
  else if (key === ' ') {
27
60
  code = 'Space';
28
61
  windowsVirtualKeyCode = 32;
29
62
  text = ' ';
63
+ unmodifiedText = ' ';
30
64
  }
31
65
  else
32
66
  throw new Error(`Unsupported key: ${key}`);
33
67
  if (key === 'Space')
34
68
  key = ' ';
35
- if (modifiers & (1 | 2 | 4))
69
+ if (effective & (1 | 2 | 4))
36
70
  text = undefined;
37
- const unmodifiedText = code.startsWith('Key') ? code.slice(3).toLowerCase() : code.startsWith('Digit') ? code.slice(5) : text;
38
- return { key, code, windowsVirtualKeyCode, modifiers, ...(text ? { text, unmodifiedText } : {}) };
71
+ return { key, code, windowsVirtualKeyCode, modifiers: effective, ...(text ? { text, unmodifiedText } : {}) };
72
+ }
73
+ /** Splits "Control+A" into parts. A literal plus key is the last part: "+" and "Control++". */
74
+ function splitChord(chord) {
75
+ if (chord === '+')
76
+ return ['+'];
77
+ if (chord.endsWith('++'))
78
+ return [...chord.slice(0, -2).split('+'), '+'];
79
+ return chord.split('+');
39
80
  }
40
81
  export class QaKeyboard {
41
82
  send;
42
83
  constructor(send) {
43
84
  this.send = send;
44
85
  }
45
- /** Single key or chord, e.g. Control+A, Meta+A, Shift+ArrowLeft. */
86
+ /**
87
+ * Single key or chord, e.g. Control+A, Meta+A, Shift+ArrowLeft, F3, ?, Control+/.
88
+ * Function keys F1-F24 and US-layout printable keys (shifted ones included) are supported.
89
+ */
46
90
  async press(chord) {
47
- const parts = chord.split('+');
91
+ const parts = splitChord(chord);
48
92
  if (parts.some((part, i) => i < parts.length - 1 && !modifierBits[part]))
49
93
  throw new Error(`Invalid key chord: ${chord}`);
50
94
  keyDefinition(parts.at(-1), 0); // Validate before holding any modifier.
package/dist/page.d.ts CHANGED
@@ -85,6 +85,21 @@ export declare class QaBrowserSession {
85
85
  private attachExisting;
86
86
  newPage(): Promise<QaPage>;
87
87
  private createPage;
88
+ /**
89
+ * UNSUPPORTED escape hatch: sends one browser-scoped CDP command (no sessionId) and resolves
90
+ * with its raw result. The protocol is Chrome's, not this package's: method names, params and
91
+ * results may change or disappear with a browser version. Prefer the typed QaPage API.
92
+ */
93
+ send(method: string, params?: CdpParams): Promise<CdpParams>;
94
+ /** Browser.getVersion (unsupported surface, see send). Read only. */
95
+ version(): Promise<CdpParams>;
96
+ /**
97
+ * Browser argv via Browser.getBrowserCommandLine (unsupported surface, see send). The browser
98
+ * answers only when it was launched with --enable-automation; launchChromeForTesting passes no
99
+ * such flag by default, so pass { args: ['--enable-automation'] } to use this. A refusal rejects
100
+ * with an error that names the requirement; the browser is not changed.
101
+ */
102
+ commandLine(): Promise<string[]>;
88
103
  close(): Promise<void>;
89
104
  }
90
105
  export declare class QaPage {
package/dist/page.js CHANGED
@@ -211,6 +211,45 @@ export class QaBrowserSession {
211
211
  throw error;
212
212
  }
213
213
  }
214
+ /**
215
+ * UNSUPPORTED escape hatch: sends one browser-scoped CDP command (no sessionId) and resolves
216
+ * with its raw result. The protocol is Chrome's, not this package's: method names, params and
217
+ * results may change or disappear with a browser version. Prefer the typed QaPage API.
218
+ */
219
+ send(method, params = {}) {
220
+ if (this.closed)
221
+ return Promise.reject(new Error('Browser session closed'));
222
+ if (typeof method !== 'string' || !method)
223
+ return Promise.reject(new Error('CDP method must be a non-empty string'));
224
+ return this.client.send(method, params);
225
+ }
226
+ /** Browser.getVersion (unsupported surface, see send). Read only. */
227
+ version() {
228
+ return this.send('Browser.getVersion');
229
+ }
230
+ /**
231
+ * Browser argv via Browser.getBrowserCommandLine (unsupported surface, see send). The browser
232
+ * answers only when it was launched with --enable-automation; launchChromeForTesting passes no
233
+ * such flag by default, so pass { args: ['--enable-automation'] } to use this. A refusal rejects
234
+ * with an error that names the requirement; the browser is not changed.
235
+ */
236
+ async commandLine() {
237
+ let reply;
238
+ try {
239
+ reply = await this.send('Browser.getBrowserCommandLine');
240
+ }
241
+ catch (error) {
242
+ // CdpClient prefixes errors the browser itself returned with "CDP: "; timeouts and closed sockets pass through.
243
+ if (error instanceof Error && error.message.startsWith('CDP: ')) {
244
+ throw new Error(`Browser.getBrowserCommandLine was refused (${error.message.slice(5)}). It requires the browser to be launched with --enable-automation, e.g. launchChromeForTesting({ args: ['--enable-automation'] }).`, { cause: error });
245
+ }
246
+ throw error;
247
+ }
248
+ const argv = reply.arguments;
249
+ if (!Array.isArray(argv) || !argv.every((arg) => typeof arg === 'string'))
250
+ throw new Error('Browser.getBrowserCommandLine did not return an argument list');
251
+ return argv;
252
+ }
214
253
  close() {
215
254
  if (this.closing)
216
255
  return this.closing;
@@ -31,6 +31,11 @@ export interface CreateQaWorkspaceOptions {
31
31
  leaseMs?: number;
32
32
  retention?: import('./storage.js').RetentionPolicy;
33
33
  }
34
+ /**
35
+ * Create an isolated run home. `env.RIGHTKIT_RUN_ROOT` is set to `home`, which lives inside the
36
+ * workspace that {@link disposeQaWorkspace} deletes. A host must never map a directory holding
37
+ * artifacts it wants to keep onto RIGHTKIT_RUN_ROOT: disposal removes that tree.
38
+ */
34
39
  export declare function createQaWorkspace(options: CreateQaWorkspaceOptions): QaWorkspace;
35
40
  export interface DisposeOptions {
36
41
  /** Keep the recorded evidence and remove only the volatile profile. */
@@ -43,6 +48,11 @@ export interface DisposeOptions {
43
48
  * Remove the workspace, retrying through the brief window in which an exiting
44
49
  * app still holds its files open. Anything still present afterwards is returned,
45
50
  * never silently ignored.
51
+ *
52
+ * Deletes `workspace.home`, which is what RIGHTKIT_RUN_ROOT points at, and also
53
+ * `workspace.root` unless `keepEvidence` is set. Anything a host maps under
54
+ * RIGHTKIT_RUN_ROOT is deleted: never map a directory holding artifacts you want
55
+ * to keep onto it.
46
56
  */
47
57
  export declare function disposeQaWorkspace(workspace: QaWorkspace, options?: DisposeOptions): DisposeReport;
48
58
  /**
package/dist/workspace.js CHANGED
@@ -18,6 +18,11 @@ import crypto from 'node:crypto';
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { createManagedRun, createManagedAppRoot, finishManagedRun, pruneManagedRuns, resolveManagedRoot, } from './storage.js';
21
+ /**
22
+ * Create an isolated run home. `env.RIGHTKIT_RUN_ROOT` is set to `home`, which lives inside the
23
+ * workspace that {@link disposeQaWorkspace} deletes. A host must never map a directory holding
24
+ * artifacts it wants to keep onto RIGHTKIT_RUN_ROOT: disposal removes that tree.
25
+ */
21
26
  export function createQaWorkspace(options) {
22
27
  const appKey = requireText(options.appKey, 'appKey');
23
28
  const managed = createManagedRun({ managedRoot: options.root ? requireText(options.root, 'root') : resolveManagedRoot(), app: appKey, runId: options.runId ?? crypto.randomUUID(), leaseMs: options.leaseMs, retention: options.retention });
@@ -77,6 +82,11 @@ export function createQaWorkspace(options) {
77
82
  * Remove the workspace, retrying through the brief window in which an exiting
78
83
  * app still holds its files open. Anything still present afterwards is returned,
79
84
  * never silently ignored.
85
+ *
86
+ * Deletes `workspace.home`, which is what RIGHTKIT_RUN_ROOT points at, and also
87
+ * `workspace.root` unless `keepEvidence` is set. Anything a host maps under
88
+ * RIGHTKIT_RUN_ROOT is deleted: never map a directory holding artifacts you want
89
+ * to keep onto it.
80
90
  */
81
91
  export function disposeQaWorkspace(workspace, options = {}) {
82
92
  const attempts = Math.max(1, options.attempts ?? 5);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/qa",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",