feather-testing-core 0.2.0 → 0.4.0
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 +177 -4
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +12 -0
- package/dist/eslint-plugin/index.d.ts +18 -0
- package/dist/eslint-plugin/index.d.ts.map +1 -0
- package/dist/eslint-plugin/index.js +37 -0
- package/dist/eslint-plugin/rules/no-conditional-skip.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-conditional-skip.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-conditional-skip.js +56 -0
- package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-swallowed-cleanup-catch.js +63 -0
- package/dist/eslint-plugin/rules/no-wait-for-timeout.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-wait-for-timeout.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-wait-for-timeout.js +69 -0
- package/dist/eslint-plugin/rules/no-weak-assertions.d.ts +4 -0
- package/dist/eslint-plugin/rules/no-weak-assertions.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/no-weak-assertions.js +73 -0
- package/dist/eslint-plugin/rules/warn-serial-mode.d.ts +4 -0
- package/dist/eslint-plugin/rules/warn-serial-mode.d.ts.map +1 -0
- package/dist/eslint-plugin/rules/warn-serial-mode.js +59 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/playwright/driver.d.ts +18 -3
- package/dist/playwright/driver.d.ts.map +1 -1
- package/dist/playwright/driver.js +82 -18
- package/dist/playwright/index.d.ts +4 -4
- package/dist/playwright/index.d.ts.map +1 -1
- package/dist/playwright/index.js +1 -1
- package/dist/rtl/driver.d.ts +55 -9
- package/dist/rtl/driver.d.ts.map +1 -1
- package/dist/rtl/driver.js +135 -35
- package/dist/rtl/index.d.ts +5 -4
- package/dist/rtl/index.d.ts.map +1 -1
- package/dist/rtl/index.js +2 -2
- package/dist/session.d.ts +41 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +72 -1
- package/dist/types.d.ts +35 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warn-serial-mode.d.ts","sourceRoot":"","sources":["../../../src/eslint-plugin/rules/warn-serial-mode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAgBnC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAkDhB,CAAC;AAEF,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function memberPath(node) {
|
|
2
|
+
if (node.type === "Identifier")
|
|
3
|
+
return node.name;
|
|
4
|
+
if (node.type === "MemberExpression" &&
|
|
5
|
+
!node.computed &&
|
|
6
|
+
node.property.type === "Identifier") {
|
|
7
|
+
const prefix = memberPath(node.object);
|
|
8
|
+
return prefix === null ? null : `${prefix}.${node.property.name}`;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const rule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "suggestion",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Flag serial mode, which makes each test depend on the ones before it",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
serial: "Serial mode couples these tests: one failure cascades into the rest, " +
|
|
22
|
+
"so the report stops naming the real cause and a flake becomes a wall " +
|
|
23
|
+
"of red. Make them independent — share setup, not state — or keep " +
|
|
24
|
+
"serial with an eslint-disable-next-line comment saying why it is " +
|
|
25
|
+
"required.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
create(context) {
|
|
29
|
+
return {
|
|
30
|
+
CallExpression(node) {
|
|
31
|
+
const path = memberPath(node.callee);
|
|
32
|
+
if (path === null)
|
|
33
|
+
return;
|
|
34
|
+
const segments = path.split(".");
|
|
35
|
+
const isSerialRunner = segments.includes("serial") &&
|
|
36
|
+
["test", "it", "describe", "suite"].includes(segments[0] ?? "");
|
|
37
|
+
if (isSerialRunner) {
|
|
38
|
+
context.report({ node, messageId: "serial" });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// test.describe.configure({ mode: "serial" })
|
|
42
|
+
if (!path.endsWith(".configure"))
|
|
43
|
+
return;
|
|
44
|
+
const [options] = node.arguments;
|
|
45
|
+
if (!options || options.type !== "ObjectExpression")
|
|
46
|
+
return;
|
|
47
|
+
const declaresSerial = options.properties.some((property) => property.type === "Property" &&
|
|
48
|
+
!property.computed &&
|
|
49
|
+
property.key.type === "Identifier" &&
|
|
50
|
+
property.key.name === "mode" &&
|
|
51
|
+
property.value.type === "Literal" &&
|
|
52
|
+
property.value.value === "serial");
|
|
53
|
+
if (declaresSerial)
|
|
54
|
+
context.report({ node, messageId: "serial" });
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export default rule;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { Session } from "./session.js";
|
|
2
|
-
export { StepError } from "./errors.js";
|
|
3
|
-
export type { AssertHasOptions, AssertPathOptions, QueuedStep, TestDriver, } from "./types.js";
|
|
2
|
+
export { StepError, BrowserOnlyVerbError } from "./errors.js";
|
|
3
|
+
export type { AssertHasOptions, AssertPathOptions, DownloadOptions, QueuedStep, TestDriver, UntilOptions, UntilPredicate, } from "./types.js";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC9D,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,UAAU,EACV,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Session } from "./session.js";
|
|
2
|
-
export { StepError } from "./errors.js";
|
|
2
|
+
export { StepError, BrowserOnlyVerbError } from "./errors.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type Page, type Locator } from "@playwright/test";
|
|
2
|
-
import type { AssertHasOptions, AssertPathOptions, TestDriver } from "../types.js";
|
|
2
|
+
import type { AssertHasOptions, AssertPathOptions, DownloadOptions, TestDriver, UntilOptions, UntilPredicate } from "../types.js";
|
|
3
3
|
/** Context handed to custom step() callbacks in the Playwright adapter. */
|
|
4
4
|
export interface PlaywrightStepContext {
|
|
5
5
|
page: Page;
|
|
6
6
|
/** Current scope: the page, or the container locator inside within(). */
|
|
7
7
|
scope: Page | Locator;
|
|
8
8
|
}
|
|
9
|
-
export declare class PlaywrightDriver implements TestDriver<PlaywrightStepContext> {
|
|
9
|
+
export declare class PlaywrightDriver implements TestDriver<PlaywrightStepContext, Page> {
|
|
10
10
|
private page;
|
|
11
11
|
private scope;
|
|
12
12
|
private lastFormLocator;
|
|
@@ -15,6 +15,8 @@ export declare class PlaywrightDriver implements TestDriver<PlaywrightStepContex
|
|
|
15
15
|
click(text: string): Promise<void>;
|
|
16
16
|
clickLink(text: string): Promise<void>;
|
|
17
17
|
clickButton(text: string): Promise<void>;
|
|
18
|
+
/** The single label-addressed lookup: every labelled verb goes through it. */
|
|
19
|
+
private labelled;
|
|
18
20
|
private fieldByLabelOrPlaceholder;
|
|
19
21
|
fillIn(label: string, value: string): Promise<void>;
|
|
20
22
|
selectOption(label: string, option: string): Promise<void>;
|
|
@@ -22,7 +24,11 @@ export declare class PlaywrightDriver implements TestDriver<PlaywrightStepContex
|
|
|
22
24
|
uncheck(label: string): Promise<void>;
|
|
23
25
|
choose(label: string): Promise<void>;
|
|
24
26
|
submit(): Promise<void>;
|
|
27
|
+
attachFile(label: string, path: string): Promise<void>;
|
|
28
|
+
/** @deprecated Older name for {@link PlaywrightDriver.attachFile}. */
|
|
25
29
|
upload(label: string, path: string): Promise<void>;
|
|
30
|
+
pressKey(key: string): Promise<void>;
|
|
31
|
+
hover(text: string): Promise<void>;
|
|
26
32
|
dropFile(selector: string, path: string): Promise<void>;
|
|
27
33
|
assertHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
28
34
|
refuteHas(selector: string, opts?: AssertHasOptions): Promise<void>;
|
|
@@ -35,8 +41,17 @@ export declare class PlaywrightDriver implements TestDriver<PlaywrightStepContex
|
|
|
35
41
|
assertOptions(label: string, optionLabels: string[]): Promise<void>;
|
|
36
42
|
assertPath(path: string, opts?: AssertPathOptions): Promise<void>;
|
|
37
43
|
refutePath(path: string): Promise<void>;
|
|
44
|
+
assertDownload(expected: string | RegExp, trigger: () => Promise<void>, opts?: DownloadOptions): Promise<void>;
|
|
45
|
+
until(description: string, predicate: UntilPredicate<PlaywrightStepContext>, opts?: UntilOptions): Promise<void>;
|
|
38
46
|
step(fn: (context: PlaywrightStepContext) => Promise<unknown>): Promise<void>;
|
|
39
|
-
|
|
47
|
+
/**
|
|
48
|
+
* raw() hands over the page itself, not the within() scope: it exists for
|
|
49
|
+
* the cases the DSL does not model, where re-scoping is the caller's job.
|
|
50
|
+
*/
|
|
51
|
+
raw(fn: (page: Page) => unknown | Promise<unknown>): Promise<void>;
|
|
52
|
+
/** The adapter context handed to step(), until(), and friends. */
|
|
53
|
+
protected context(): PlaywrightStepContext;
|
|
54
|
+
within(selector: string): Promise<TestDriver<PlaywrightStepContext, Page>>;
|
|
40
55
|
debug(): Promise<void>;
|
|
41
56
|
wrapStep(name: string, fn: () => Promise<void>): Promise<void>;
|
|
42
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/playwright/driver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,OAAO,EAAgB,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/playwright/driver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { expect, test } from "@playwright/test";
|
|
4
|
+
/**
|
|
5
|
+
* Playwright matches a bare string name/label/text as a case-insensitive
|
|
6
|
+
* SUBSTRING. That makes every text-addressed verb ambient: `clickButton('Check')`
|
|
7
|
+
* also matches an unrelated "Checklist Run — checklist" control that happens to
|
|
8
|
+
* be on the page, and the run dies on a strict-mode violation whose appearance
|
|
9
|
+
* depends on what else rendered. When a spec names a control it means *that*
|
|
10
|
+
* control, so every addressing matcher here passes `exact: true`. Playwright
|
|
11
|
+
* still normalizes whitespace under exact matching, so multi-line markup and
|
|
12
|
+
* padded labels keep working.
|
|
13
|
+
*
|
|
14
|
+
* Assertions (assertText/refuteText/assertHas) deliberately stay substring:
|
|
15
|
+
* they ask "does this text appear", and an exact `refuteText` would pass while
|
|
16
|
+
* the text is plainly on the page inside a longer string.
|
|
17
|
+
*/
|
|
18
|
+
const EXACT = { exact: true };
|
|
4
19
|
export class PlaywrightDriver {
|
|
5
20
|
page;
|
|
6
21
|
scope;
|
|
@@ -13,23 +28,25 @@ export class PlaywrightDriver {
|
|
|
13
28
|
await this.page.goto(path);
|
|
14
29
|
}
|
|
15
30
|
async click(text) {
|
|
16
|
-
await this.scope.getByText(text).click();
|
|
31
|
+
await this.scope.getByText(text, EXACT).click();
|
|
17
32
|
}
|
|
18
33
|
async clickLink(text) {
|
|
19
|
-
await this.scope.getByRole("link", { name: text }).click();
|
|
34
|
+
await this.scope.getByRole("link", { name: text, ...EXACT }).click();
|
|
20
35
|
}
|
|
21
36
|
async clickButton(text) {
|
|
22
|
-
await this.scope.getByRole("button", { name: text }).click();
|
|
37
|
+
await this.scope.getByRole("button", { name: text, ...EXACT }).click();
|
|
38
|
+
}
|
|
39
|
+
/** The single label-addressed lookup: every labelled verb goes through it. */
|
|
40
|
+
labelled(label) {
|
|
41
|
+
return this.scope.getByLabel(label, EXACT);
|
|
23
42
|
}
|
|
24
43
|
fieldByLabelOrPlaceholder(label) {
|
|
25
44
|
// .or() lets Playwright auto-wait on whichever appears, so
|
|
26
45
|
// async-rendered labeled fields don't fall through to the
|
|
27
|
-
// placeholder branch.
|
|
46
|
+
// placeholder branch. Both branches match exactly — substring
|
|
28
47
|
// matching would collide with labels ("Name" vs placeholder
|
|
29
48
|
// "Nickname") and trip strict mode.
|
|
30
|
-
return this.scope
|
|
31
|
-
.getByLabel(label)
|
|
32
|
-
.or(this.scope.getByPlaceholder(label, { exact: true }));
|
|
49
|
+
return this.labelled(label).or(this.scope.getByPlaceholder(label, EXACT));
|
|
33
50
|
}
|
|
34
51
|
async fillIn(label, value) {
|
|
35
52
|
const field = this.fieldByLabelOrPlaceholder(label);
|
|
@@ -37,22 +54,22 @@ export class PlaywrightDriver {
|
|
|
37
54
|
this.lastFormLocator = this.scope.locator("form", { has: field });
|
|
38
55
|
}
|
|
39
56
|
async selectOption(label, option) {
|
|
40
|
-
const select = this.
|
|
57
|
+
const select = this.labelled(label);
|
|
41
58
|
await select.selectOption({ label: option });
|
|
42
59
|
this.lastFormLocator = this.scope.locator("form", { has: select });
|
|
43
60
|
}
|
|
44
61
|
async check(label) {
|
|
45
|
-
const checkbox = this.
|
|
62
|
+
const checkbox = this.labelled(label);
|
|
46
63
|
await checkbox.check();
|
|
47
64
|
this.lastFormLocator = this.scope.locator("form", { has: checkbox });
|
|
48
65
|
}
|
|
49
66
|
async uncheck(label) {
|
|
50
|
-
const checkbox = this.
|
|
67
|
+
const checkbox = this.labelled(label);
|
|
51
68
|
await checkbox.uncheck();
|
|
52
69
|
this.lastFormLocator = this.scope.locator("form", { has: checkbox });
|
|
53
70
|
}
|
|
54
71
|
async choose(label) {
|
|
55
|
-
const radio = this.scope.getByRole("radio", { name: label });
|
|
72
|
+
const radio = this.scope.getByRole("radio", { name: label, ...EXACT });
|
|
56
73
|
await radio.check();
|
|
57
74
|
this.lastFormLocator = this.scope.locator("form", { has: radio });
|
|
58
75
|
}
|
|
@@ -82,11 +99,21 @@ export class PlaywrightDriver {
|
|
|
82
99
|
.press("Enter");
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
|
-
async
|
|
86
|
-
const input = this.
|
|
102
|
+
async attachFile(label, path) {
|
|
103
|
+
const input = this.labelled(label);
|
|
87
104
|
await input.setInputFiles(path);
|
|
88
105
|
this.lastFormLocator = this.scope.locator("form", { has: input });
|
|
89
106
|
}
|
|
107
|
+
/** @deprecated Older name for {@link PlaywrightDriver.attachFile}. */
|
|
108
|
+
async upload(label, path) {
|
|
109
|
+
await this.attachFile(label, path);
|
|
110
|
+
}
|
|
111
|
+
async pressKey(key) {
|
|
112
|
+
await this.page.keyboard.press(key);
|
|
113
|
+
}
|
|
114
|
+
async hover(text) {
|
|
115
|
+
await this.scope.getByText(text, EXACT).hover();
|
|
116
|
+
}
|
|
90
117
|
async dropFile(selector, path) {
|
|
91
118
|
const content = await readFile(path);
|
|
92
119
|
const name = basename(path);
|
|
@@ -133,17 +160,17 @@ export class PlaywrightDriver {
|
|
|
133
160
|
await expect(this.fieldByLabelOrPlaceholder(label)).toHaveValue(value);
|
|
134
161
|
}
|
|
135
162
|
async assertChecked(label) {
|
|
136
|
-
await expect(this.
|
|
163
|
+
await expect(this.labelled(label)).toBeChecked();
|
|
137
164
|
}
|
|
138
165
|
async refuteChecked(label) {
|
|
139
|
-
await expect(this.
|
|
166
|
+
await expect(this.labelled(label)).not.toBeChecked();
|
|
140
167
|
}
|
|
141
168
|
async assertSelected(label, optionLabel) {
|
|
142
|
-
const select = this.
|
|
169
|
+
const select = this.labelled(label);
|
|
143
170
|
await expect(select.locator("option:checked")).toHaveText(optionLabel);
|
|
144
171
|
}
|
|
145
172
|
async assertOptions(label, optionLabels) {
|
|
146
|
-
const select = this.
|
|
173
|
+
const select = this.labelled(label);
|
|
147
174
|
await expect(select.locator("option")).toHaveText(optionLabels);
|
|
148
175
|
}
|
|
149
176
|
async assertPath(path, opts) {
|
|
@@ -166,8 +193,45 @@ export class PlaywrightDriver {
|
|
|
166
193
|
})
|
|
167
194
|
.not.toBe(path);
|
|
168
195
|
}
|
|
196
|
+
async assertDownload(expected, trigger, opts) {
|
|
197
|
+
// The wait has to be armed before the click that starts the download,
|
|
198
|
+
// which is why the trigger arrives as a callback rather than as an
|
|
199
|
+
// earlier step in the chain.
|
|
200
|
+
const [download] = await Promise.all([
|
|
201
|
+
this.page.waitForEvent("download", { timeout: opts?.timeout }),
|
|
202
|
+
trigger(),
|
|
203
|
+
]);
|
|
204
|
+
const filename = download.suggestedFilename();
|
|
205
|
+
const matched = typeof expected === "string"
|
|
206
|
+
? filename === expected
|
|
207
|
+
: expected.test(filename);
|
|
208
|
+
if (!matched) {
|
|
209
|
+
throw new Error(`assertDownload(${typeof expected === "string" ? `'${expected}'` : String(expected)}): ` +
|
|
210
|
+
`the browser offered a download named '${filename}' instead.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async until(description, predicate, opts) {
|
|
214
|
+
await expect
|
|
215
|
+
.poll(async () => Boolean(await predicate(this.context())), {
|
|
216
|
+
message: `until: ${description}`,
|
|
217
|
+
timeout: opts?.timeout,
|
|
218
|
+
intervals: opts?.interval === undefined ? undefined : [opts.interval],
|
|
219
|
+
})
|
|
220
|
+
.toBe(true);
|
|
221
|
+
}
|
|
169
222
|
async step(fn) {
|
|
170
|
-
await fn(
|
|
223
|
+
await fn(this.context());
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* raw() hands over the page itself, not the within() scope: it exists for
|
|
227
|
+
* the cases the DSL does not model, where re-scoping is the caller's job.
|
|
228
|
+
*/
|
|
229
|
+
async raw(fn) {
|
|
230
|
+
await fn(this.page);
|
|
231
|
+
}
|
|
232
|
+
/** The adapter context handed to step(), until(), and friends. */
|
|
233
|
+
context() {
|
|
234
|
+
return { page: this.page, scope: this.scope };
|
|
171
235
|
}
|
|
172
236
|
async within(selector) {
|
|
173
237
|
const scopedLocator = this.scope.locator(selector);
|
|
@@ -2,12 +2,12 @@ import { type Page } from "@playwright/test";
|
|
|
2
2
|
import { Session } from "../session.js";
|
|
3
3
|
import { type PlaywrightStepContext } from "./driver.js";
|
|
4
4
|
export { Session } from "../session.js";
|
|
5
|
-
export { StepError } from "../errors.js";
|
|
5
|
+
export { StepError, BrowserOnlyVerbError } from "../errors.js";
|
|
6
6
|
export { PlaywrightDriver, type PlaywrightStepContext } from "./driver.js";
|
|
7
|
-
export type { AssertHasOptions, AssertPathOptions, TestDriver, } from "../types.js";
|
|
8
|
-
export declare function createSession(page: Page): Session<PlaywrightStepContext>;
|
|
7
|
+
export type { AssertHasOptions, AssertPathOptions, DownloadOptions, TestDriver, UntilOptions, UntilPredicate, } from "../types.js";
|
|
8
|
+
export declare function createSession(page: Page): Session<PlaywrightStepContext, Page>;
|
|
9
9
|
export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
|
|
10
|
-
session: Session<PlaywrightStepContext>;
|
|
10
|
+
session: Session<PlaywrightStepContext, Page>;
|
|
11
11
|
}, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
|
|
12
12
|
export { expect } from "@playwright/test";
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAoB,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAoB,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC3E,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,wBAAgB,aAAa,CAC3B,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAEtC;AAED,eAAO,MAAM,IAAI;aACN,OAAO,CAAC,qBAAqB,EAAE,IAAI,CAAC;sGAK7C,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/playwright/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { test as base } from "@playwright/test";
|
|
|
2
2
|
import { Session } from "../session.js";
|
|
3
3
|
import { PlaywrightDriver } from "./driver.js";
|
|
4
4
|
export { Session } from "../session.js";
|
|
5
|
-
export { StepError } from "../errors.js";
|
|
5
|
+
export { StepError, BrowserOnlyVerbError } from "../errors.js";
|
|
6
6
|
export { PlaywrightDriver } from "./driver.js";
|
|
7
7
|
export function createSession(page) {
|
|
8
8
|
return new Session(new PlaywrightDriver(page));
|
package/dist/rtl/driver.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { screen, within as rtlWithin } from "@testing-library/react";
|
|
2
2
|
import { type UserEvent } from "@testing-library/user-event";
|
|
3
|
-
import type { AssertHasOptions, TestDriver } from "../types.js";
|
|
3
|
+
import type { AssertHasOptions, DownloadOptions, TestDriver, UntilOptions, UntilPredicate } from "../types.js";
|
|
4
|
+
/** The scoped query object raw() callbacks receive in the RTL adapter. */
|
|
5
|
+
export type RTLQueries = ReturnType<typeof rtlWithin>;
|
|
4
6
|
/** Context handed to custom step() callbacks in the RTL adapter. */
|
|
5
7
|
export interface RTLStepContext {
|
|
6
8
|
user: UserEvent;
|
|
@@ -10,14 +12,36 @@ export interface RTLStepContext {
|
|
|
10
12
|
/**
|
|
11
13
|
* RTL adapter implementing the subset of TestDriver that applies in JSDOM.
|
|
12
14
|
* Navigation methods (visit, assertPath, refutePath) are not supported.
|
|
15
|
+
*
|
|
16
|
+
* Everything a host adapter is likely to specialize is `protected`: the
|
|
17
|
+
* label-to-control lookup (`findField`), the scoped-driver factory
|
|
18
|
+
* (`scoped`), and the `user` / `root` / `lastFormElement` state the verbs
|
|
19
|
+
* share. Subclass it rather than reimplementing the DSL when an app's markup
|
|
20
|
+
* needs a different lookup — that is how feather-testing-postgres binds this
|
|
21
|
+
* driver to markup whose labels are wrapper siblings rather than `htmlFor`
|
|
22
|
+
* targets.
|
|
13
23
|
*/
|
|
14
|
-
export declare class RTLDriver implements TestDriver<RTLStepContext> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
export declare class RTLDriver implements TestDriver<RTLStepContext, RTLQueries> {
|
|
25
|
+
protected user: UserEvent;
|
|
26
|
+
/** The element every query and selector in this driver resolves against. */
|
|
27
|
+
protected root: HTMLElement;
|
|
28
|
+
protected container: ReturnType<typeof rtlWithin>;
|
|
29
|
+
protected lastFormElement: HTMLFormElement | null;
|
|
30
|
+
/** Per-lookup timeout in ms; undefined leaves RTL's own default in place. */
|
|
31
|
+
protected timeout: number | undefined;
|
|
32
|
+
constructor(user?: UserEvent, container?: HTMLElement, timeout?: number);
|
|
33
|
+
protected rootElement(): HTMLElement;
|
|
34
|
+
/** Options forwarded to every async query and waitFor call. */
|
|
35
|
+
protected waitOpts(): {
|
|
36
|
+
timeout?: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The single label-addressed lookup: every labelled verb goes through it,
|
|
40
|
+
* so overriding this one method retargets them all.
|
|
41
|
+
*/
|
|
42
|
+
protected findField(label: string): Promise<HTMLElement>;
|
|
43
|
+
/** Subclasses override this so within() yields a driver of their own type. */
|
|
44
|
+
protected scoped(element: HTMLElement): TestDriver<RTLStepContext, RTLQueries>;
|
|
21
45
|
visit(): Promise<void>;
|
|
22
46
|
click(text: string): Promise<void>;
|
|
23
47
|
clickLink(text: string): Promise<void>;
|
|
@@ -28,7 +52,16 @@ export declare class RTLDriver implements TestDriver<RTLStepContext> {
|
|
|
28
52
|
uncheck(label: string): Promise<void>;
|
|
29
53
|
choose(label: string): Promise<void>;
|
|
30
54
|
submit(): Promise<void>;
|
|
55
|
+
attachFile(label: string, path: string): Promise<void>;
|
|
56
|
+
/** @deprecated Older name for {@link RTLDriver.attachFile}. */
|
|
31
57
|
upload(label: string, path: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Keys are named the Playwright way ('Enter', 'Control+A') and translated
|
|
60
|
+
* to user-event's keyboard syntax, so one spec reads the same on both
|
|
61
|
+
* adapters.
|
|
62
|
+
*/
|
|
63
|
+
pressKey(key: string): Promise<void>;
|
|
64
|
+
hover(text: string): Promise<void>;
|
|
32
65
|
dropFile(selector: string, path: string): Promise<void>;
|
|
33
66
|
assertHas(_selector: string, _opts?: AssertHasOptions): Promise<void>;
|
|
34
67
|
refuteHas(_selector: string, _opts?: AssertHasOptions): Promise<void>;
|
|
@@ -41,8 +74,21 @@ export declare class RTLDriver implements TestDriver<RTLStepContext> {
|
|
|
41
74
|
assertOptions(label: string, optionLabels: string[]): Promise<void>;
|
|
42
75
|
assertPath(): Promise<void>;
|
|
43
76
|
refutePath(): Promise<void>;
|
|
77
|
+
assertDownload(_expected: string | RegExp, _trigger: () => Promise<void>, _opts?: DownloadOptions): Promise<void>;
|
|
78
|
+
until(description: string, predicate: UntilPredicate<RTLStepContext>, opts?: UntilOptions): Promise<void>;
|
|
44
79
|
step(fn: (context: RTLStepContext) => Promise<unknown>): Promise<void>;
|
|
45
|
-
within(
|
|
80
|
+
/** raw() hands over the scoped query object — `within(root)`, i.e. screen
|
|
81
|
+
* when the driver is unscoped. */
|
|
82
|
+
raw(fn: (queries: RTLQueries) => unknown | Promise<unknown>): Promise<void>;
|
|
83
|
+
/** The adapter context handed to step(), until(), and friends. */
|
|
84
|
+
protected context(): RTLStepContext;
|
|
85
|
+
within(selector: string): Promise<TestDriver<RTLStepContext, RTLQueries>>;
|
|
46
86
|
debug(): Promise<void>;
|
|
47
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* 'Enter' -> '{Enter}', 'a' -> 'a', 'Control+A' -> '{Control>}A{/Control}'.
|
|
90
|
+
* Printable characters are typed literally with user-event's own `{` and `[`
|
|
91
|
+
* escapes applied, so a key name never turns into a descriptor by accident.
|
|
92
|
+
*/
|
|
93
|
+
export declare function toKeyboardSyntax(key: string): string;
|
|
48
94
|
//# sourceMappingURL=driver.d.ts.map
|
package/dist/rtl/driver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,MAAM,EAEN,MAAM,IAAI,SAAS,EACpB,MAAM,wBAAwB,CAAC;AAChC,OAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/rtl/driver.ts"],"names":[],"mappings":"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"}
|