@itmetsam/samurai-framework 0.2.2 → 0.2.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/dist/locator/locator.d.ts +8 -0
- package/dist/locator/locator.js +46 -0
- package/dist/recorder/capture.d.ts +2 -0
- package/dist/recorder/capture.js +14 -0
- package/dist/recorder/normalise.d.ts +5 -0
- package/dist/recorder/normalise.js +12 -0
- package/dist/recorder/recorder.js +20 -18
- package/dist/steps/emit.js +2 -0
- package/dist/steps/model.d.ts +6 -0
- package/dist/steps/parse.js +4 -0
- package/package.json +1 -1
|
@@ -163,6 +163,14 @@ export default class Locator {
|
|
|
163
163
|
isEditable(): Promise<boolean>;
|
|
164
164
|
/** Wait for certain time (ms). Default: 300ms */
|
|
165
165
|
wait(duration?: number): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Waits until the element is attached, visible and enabled, focuses it and presses `key` with real keyboard input.
|
|
168
|
+
* Named keys are `Enter`, `Escape`, `Tab`, `Backspace`, `Delete`, `Space`, `Home`, `End`, `PageUp`, `PageDown` and the arrows;
|
|
169
|
+
* any other key must be a single character.
|
|
170
|
+
* @example
|
|
171
|
+
* await page.getByLabel("New todo").press("Enter");
|
|
172
|
+
*/
|
|
173
|
+
press(key: string, options?: ActionOptions): Promise<void>;
|
|
166
174
|
/** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
|
|
167
175
|
fill(value: string, options?: ActionOptions): Promise<void>;
|
|
168
176
|
}
|
package/dist/locator/locator.js
CHANGED
|
@@ -42,7 +42,25 @@ const FILL_CHECKS = [
|
|
|
42
42
|
"editable",
|
|
43
43
|
"hit target",
|
|
44
44
|
];
|
|
45
|
+
const PRESS_CHECKS = ["attached", "visible", "enabled"];
|
|
45
46
|
const ATTACHED_ONLY = ["attached"];
|
|
47
|
+
/** WebDriver code points of the named keys `press()` accepts; any other single character is typed as is */
|
|
48
|
+
const NAMED_KEYS = {
|
|
49
|
+
Backspace: "\uE003",
|
|
50
|
+
Tab: "\uE004",
|
|
51
|
+
Enter: "\uE007",
|
|
52
|
+
Escape: "\uE00C",
|
|
53
|
+
Space: " ",
|
|
54
|
+
ArrowLeft: "\uE012",
|
|
55
|
+
ArrowUp: "\uE013",
|
|
56
|
+
ArrowRight: "\uE014",
|
|
57
|
+
ArrowDown: "\uE015",
|
|
58
|
+
Delete: "\uE017",
|
|
59
|
+
Home: "\uE011",
|
|
60
|
+
End: "\uE010",
|
|
61
|
+
PageUp: "\uE00E",
|
|
62
|
+
PageDown: "\uE00F",
|
|
63
|
+
};
|
|
46
64
|
/** Unicode code point WebDriver uses for the Backspace key */
|
|
47
65
|
const BACKSPACE = "";
|
|
48
66
|
export default class Locator {
|
|
@@ -556,6 +574,34 @@ export default class Locator {
|
|
|
556
574
|
],
|
|
557
575
|
});
|
|
558
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Waits until the element is attached, visible and enabled, focuses it and presses `key` with real keyboard input.
|
|
579
|
+
* Named keys are `Enter`, `Escape`, `Tab`, `Backspace`, `Delete`, `Space`, `Home`, `End`, `PageUp`, `PageDown` and the arrows;
|
|
580
|
+
* any other key must be a single character.
|
|
581
|
+
* @example
|
|
582
|
+
* await page.getByLabel("New todo").press("Enter");
|
|
583
|
+
*/
|
|
584
|
+
async press(key, options) {
|
|
585
|
+
const code = NAMED_KEYS[key] ?? (Array.from(key).length === 1 ? key : "");
|
|
586
|
+
if (code === "") {
|
|
587
|
+
throw new Error(`press(): unknown key "${key}". Use one of ${Object.keys(NAMED_KEYS).join(", ")} or a single character`);
|
|
588
|
+
}
|
|
589
|
+
await this.waitForActionable("press", PRESS_CHECKS, { scroll: true, hitTest: false }, options);
|
|
590
|
+
await this.callOnElement(undefined, `el.focus();`);
|
|
591
|
+
await this.biDiConnector.send("input.performActions", {
|
|
592
|
+
context: this.contextId,
|
|
593
|
+
actions: [
|
|
594
|
+
{
|
|
595
|
+
type: "key",
|
|
596
|
+
id: "samurai-keyboard",
|
|
597
|
+
actions: [
|
|
598
|
+
{ type: "keyDown", value: code },
|
|
599
|
+
{ type: "keyUp", value: code },
|
|
600
|
+
],
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
559
605
|
/** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
|
|
560
606
|
async fill(value, options) {
|
|
561
607
|
const state = await this.waitForActionable("fill", FILL_CHECKS, { scroll: true, hitTest: true }, options);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* element (a label click reports its control; the click the browser forwards to the control is dropped).
|
|
7
7
|
* - `{ type: "input", target, candidates, value, secret, secretName }`: text typed into a field. Password
|
|
8
8
|
* values never leave the page: `secret` is set and `value` is left out.
|
|
9
|
+
* - `{ type: "press", target, candidates, pressed }`: Enter, Tab or Escape (Enter in a button, link or textarea is a
|
|
10
|
+
* click or typing, which are reported as such).
|
|
9
11
|
* - `{ type: "assert", target, candidates, text, value }`: an Alt+click, which does not reach the page.
|
|
10
12
|
* While Alt is held, the element under the pointer is outlined.
|
|
11
13
|
*
|
package/dist/recorder/capture.js
CHANGED
|
@@ -8,6 +8,8 @@ const OFF_KEY = "samurai.recorder.off";
|
|
|
8
8
|
* element (a label click reports its control; the click the browser forwards to the control is dropped).
|
|
9
9
|
* - `{ type: "input", target, candidates, value, secret, secretName }`: text typed into a field. Password
|
|
10
10
|
* values never leave the page: `secret` is set and `value` is left out.
|
|
11
|
+
* - `{ type: "press", target, candidates, pressed }`: Enter, Tab or Escape (Enter in a button, link or textarea is a
|
|
12
|
+
* click or typing, which are reported as such).
|
|
11
13
|
* - `{ type: "assert", target, candidates, text, value }`: an Alt+click, which does not reach the page.
|
|
12
14
|
* While Alt is held, the element under the pointer is outlined.
|
|
13
15
|
*
|
|
@@ -184,6 +186,18 @@ export const CAPTURE_SOURCE = String.raw `(send) => {
|
|
|
184
186
|
});
|
|
185
187
|
}, true);
|
|
186
188
|
|
|
189
|
+
// Enter, Tab and Escape change what the page does next (an Enter adds a todo and clears the field), so they are steps
|
|
190
|
+
const RECORDED_KEYS = ["Enter", "Tab", "Escape"];
|
|
191
|
+
// Enter on these is a click, which is recorded as one
|
|
192
|
+
const ACTIVATES = "a[href],button,summary,select,input[type=button],input[type=submit],input[type=reset],input[type=checkbox],input[type=radio]";
|
|
193
|
+
document.addEventListener("keydown", (event) => {
|
|
194
|
+
if (off() || !event.isTrusted || event.isComposing || event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
|
|
195
|
+
if (RECORDED_KEYS.indexOf(event.key) < 0 || !(event.target instanceof Element)) return;
|
|
196
|
+
const el = event.target;
|
|
197
|
+
if (event.key === "Enter" && (el.closest(ACTIVATES) || el.tagName === "TEXTAREA" || el.isContentEditable)) return;
|
|
198
|
+
report({ type: "press", target: el, candidates: candidatesOf(el), pressed: event.key });
|
|
199
|
+
}, true);
|
|
200
|
+
|
|
187
201
|
document.addEventListener("mousemove", (event) => {
|
|
188
202
|
if (off() || !event.altKey || !(event.target instanceof Element) || event.target === outlined) return;
|
|
189
203
|
unhighlight();
|
|
@@ -60,6 +60,18 @@ export class Normaliser {
|
|
|
60
60
|
this.lastAction = at;
|
|
61
61
|
return [this.insert({ kind: "click", locator: observation.locator })];
|
|
62
62
|
}
|
|
63
|
+
if (observation.type === "press") {
|
|
64
|
+
// The page may react by clearing the field: what is typed next is a new fill, not a rewrite of this one
|
|
65
|
+
this.lastFill = undefined;
|
|
66
|
+
this.lastAction = at;
|
|
67
|
+
return [
|
|
68
|
+
this.insert({
|
|
69
|
+
kind: "press",
|
|
70
|
+
locator: observation.locator,
|
|
71
|
+
key: observation.pressed,
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
63
75
|
if (observation.type === "assert") {
|
|
64
76
|
this.lastFill = undefined;
|
|
65
77
|
return [
|
|
@@ -99,24 +99,26 @@ export class Recorder {
|
|
|
99
99
|
const key = message.target.sharedId;
|
|
100
100
|
const observation = message.type === "click"
|
|
101
101
|
? { type: "click", key, locator, textEntry: message.textEntry === true }
|
|
102
|
-
: message.type === "
|
|
103
|
-
? {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
102
|
+
: message.type === "press"
|
|
103
|
+
? { type: "press", key, locator, pressed: message.pressed ?? "" }
|
|
104
|
+
: message.type === "assert"
|
|
105
|
+
? {
|
|
106
|
+
type: "assert",
|
|
107
|
+
key,
|
|
108
|
+
locator,
|
|
109
|
+
text: message.text ?? "",
|
|
110
|
+
...(message.value !== undefined && { value: message.value }),
|
|
111
|
+
}
|
|
112
|
+
: {
|
|
113
|
+
type: "input",
|
|
114
|
+
key,
|
|
115
|
+
locator,
|
|
116
|
+
...(message.value !== undefined && { value: message.value }),
|
|
117
|
+
...(message.secret && { secret: true }),
|
|
118
|
+
...(message.secretName !== undefined && {
|
|
119
|
+
secretName: message.secretName,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
120
122
|
this.emit(this.normaliser.observe(observation, Date.now()));
|
|
121
123
|
}
|
|
122
124
|
async release() {
|
package/dist/steps/emit.js
CHANGED
|
@@ -63,6 +63,8 @@ export function stepToSource(step) {
|
|
|
63
63
|
return `await ${locatorToSource(step.locator)}.click();`;
|
|
64
64
|
case "fill":
|
|
65
65
|
return `await ${locatorToSource(step.locator)}.fill(${valueToSource(step.value)});`;
|
|
66
|
+
case "press":
|
|
67
|
+
return `await ${locatorToSource(step.locator)}.press(${str(step.key)});`;
|
|
66
68
|
case "expect":
|
|
67
69
|
return `await expect(${locatorToSource(step.locator)})${step.not ? ".not" : ""}.${expectationToSource(step.expectation)};`;
|
|
68
70
|
case "waitForNetworkIdle":
|
package/dist/steps/model.d.ts
CHANGED
|
@@ -75,6 +75,12 @@ export type Step = {
|
|
|
75
75
|
locator: LocatorSpec;
|
|
76
76
|
not: boolean;
|
|
77
77
|
expectation: Expectation;
|
|
78
|
+
}
|
|
79
|
+
/** A key pressed while the element has focus, e.g. `Enter` to submit a field. Named as Playwright names keys */
|
|
80
|
+
| {
|
|
81
|
+
kind: "press";
|
|
82
|
+
locator: LocatorSpec;
|
|
83
|
+
key: string;
|
|
78
84
|
} | {
|
|
79
85
|
kind: "waitForNetworkIdle";
|
|
80
86
|
}
|
package/dist/steps/parse.js
CHANGED
|
@@ -214,6 +214,10 @@ function readStep(statement) {
|
|
|
214
214
|
return undefined;
|
|
215
215
|
if (name.text === "click" && args.length === 0)
|
|
216
216
|
return { kind: "click", locator };
|
|
217
|
+
if (name.text === "press" && args.length === 1) {
|
|
218
|
+
const key = stringOf(args[0]);
|
|
219
|
+
return key === undefined ? undefined : { kind: "press", locator, key };
|
|
220
|
+
}
|
|
217
221
|
if (name.text === "fill" && args.length === 1) {
|
|
218
222
|
const value = readValue(args[0]);
|
|
219
223
|
return value ? { kind: "fill", locator, value } : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"exports": {
|