playwright-cucumber-ts-steps 0.1.6 → 1.0.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 +21 -11
- package/package.json +9 -2
- package/src/actions/clickSteps.ts +429 -0
- package/src/actions/cookieSteps.ts +95 -0
- package/src/actions/debugSteps.ts +21 -0
- package/src/actions/elementFindSteps.ts +961 -0
- package/src/actions/fillFormSteps.ts +270 -0
- package/src/actions/index.ts +12 -0
- package/src/actions/inputSteps.ts +354 -0
- package/src/actions/interceptionSteps.ts +325 -0
- package/src/actions/miscSteps.ts +1144 -0
- package/src/actions/mouseSteps.ts +256 -0
- package/src/actions/scrollSteps.ts +122 -0
- package/src/actions/storageSteps.ts +308 -0
- package/src/assertions/buttonAndTextVisibilitySteps.ts +436 -0
- package/src/assertions/cookieSteps.ts +131 -0
- package/src/assertions/elementSteps.ts +432 -0
- package/src/assertions/formInputSteps.ts +377 -0
- package/src/assertions/index.ts +11 -0
- package/src/assertions/interceptionRequestsSteps.ts +640 -0
- package/src/assertions/locationSteps.ts +315 -0
- package/src/assertions/roleTestIdSteps.ts +254 -0
- package/src/assertions/semanticSteps.ts +267 -0
- package/src/assertions/storageSteps.ts +250 -0
- package/src/assertions/visualSteps.ts +275 -0
- package/src/custom_setups/loginHooks.ts +154 -0
- package/src/helpers/checkPeerDeps.ts +19 -0
- package/src/helpers/compareSnapshots.ts +35 -0
- package/src/helpers/hooks.ts +212 -0
- package/src/helpers/utils/fakerUtils.ts +64 -0
- package/src/helpers/utils/optionsUtils.ts +104 -0
- package/src/helpers/utils/resolveUtils.ts +74 -0
- package/src/helpers/utils/sessionUtils.ts +36 -0
- package/src/helpers/world.ts +119 -0
- package/src/iframes/frames.ts +15 -0
- package/src/index.ts +18 -0
- package/src/register.ts +4 -0
- package/lib/actions/clickSteps.d.ts +0 -1
- package/lib/actions/clickSteps.js +0 -165
- package/lib/actions/cookieSteps.d.ts +0 -1
- package/lib/actions/cookieSteps.js +0 -28
- package/lib/actions/debugSteps.d.ts +0 -1
- package/lib/actions/debugSteps.js +0 -8
- package/lib/actions/elementFindSteps.d.ts +0 -1
- package/lib/actions/elementFindSteps.js +0 -217
- package/lib/actions/fillFormSteps.d.ts +0 -1
- package/lib/actions/fillFormSteps.js +0 -130
- package/lib/actions/inputSteps.d.ts +0 -1
- package/lib/actions/inputSteps.js +0 -97
- package/lib/actions/interceptionSteps.d.ts +0 -1
- package/lib/actions/interceptionSteps.js +0 -71
- package/lib/actions/miscSteps.d.ts +0 -1
- package/lib/actions/miscSteps.js +0 -320
- package/lib/actions/mouseSteps.d.ts +0 -1
- package/lib/actions/mouseSteps.js +0 -66
- package/lib/actions/scrollSteps.d.ts +0 -1
- package/lib/actions/scrollSteps.js +0 -23
- package/lib/actions/storageSteps.d.ts +0 -1
- package/lib/actions/storageSteps.js +0 -72
- package/lib/assertions/buttonAndTextVisibilitySteps.d.ts +0 -1
- package/lib/assertions/buttonAndTextVisibilitySteps.js +0 -150
- package/lib/assertions/cookieSteps.d.ts +0 -1
- package/lib/assertions/cookieSteps.js +0 -45
- package/lib/assertions/elementSteps.d.ts +0 -1
- package/lib/assertions/elementSteps.js +0 -90
- package/lib/assertions/formInputSteps.d.ts +0 -1
- package/lib/assertions/formInputSteps.js +0 -87
- package/lib/assertions/interceptionRequestsSteps.d.ts +0 -1
- package/lib/assertions/interceptionRequestsSteps.js +0 -201
- package/lib/assertions/locationSteps.d.ts +0 -1
- package/lib/assertions/locationSteps.js +0 -87
- package/lib/assertions/roleTestIdSteps.d.ts +0 -1
- package/lib/assertions/roleTestIdSteps.js +0 -26
- package/lib/assertions/semanticSteps.d.ts +0 -1
- package/lib/assertions/semanticSteps.js +0 -67
- package/lib/assertions/storageSteps.d.ts +0 -1
- package/lib/assertions/storageSteps.js +0 -74
- package/lib/assertions/visualSteps.d.ts +0 -1
- package/lib/assertions/visualSteps.js +0 -76
- package/lib/custom_setups/loginHooks.d.ts +0 -1
- package/lib/custom_setups/loginHooks.js +0 -113
- package/lib/helpers/checkPeerDeps.d.ts +0 -1
- package/lib/helpers/checkPeerDeps.js +0 -19
- package/lib/helpers/compareSnapshots.d.ts +0 -6
- package/lib/helpers/compareSnapshots.js +0 -20
- package/lib/helpers/hooks.d.ts +0 -1
- package/lib/helpers/hooks.js +0 -210
- package/lib/helpers/utils/fakerUtils.d.ts +0 -1
- package/lib/helpers/utils/fakerUtils.js +0 -60
- package/lib/helpers/utils/index.js +0 -20
- package/lib/helpers/utils/optionsUtils.d.ts +0 -24
- package/lib/helpers/utils/optionsUtils.js +0 -88
- package/lib/helpers/utils/resolveUtils.d.ts +0 -6
- package/lib/helpers/utils/resolveUtils.js +0 -72
- package/lib/helpers/utils/sessionUtils.d.ts +0 -3
- package/lib/helpers/utils/sessionUtils.js +0 -40
- package/lib/helpers/world.d.ts +0 -31
- package/lib/helpers/world.js +0 -104
- package/lib/iframes/frames.d.ts +0 -1
- package/lib/iframes/frames.js +0 -11
- package/lib/index.d.ts +0 -28
- package/lib/index.js +0 -48
- package/lib/register.d.ts +0 -1
- package/lib/register.js +0 -6
- /package/{lib/helpers/utils/index.d.ts → src/helpers/utils/index.ts} +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { faker } from "@faker-js/faker";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
|
|
4
|
+
// Supports optional parameters
|
|
5
|
+
type FakerMapping = Record<string, ((param?: string) => string) | (() => string)>;
|
|
6
|
+
|
|
7
|
+
const fakerMapping: FakerMapping = {
|
|
8
|
+
"First Name": () => faker.person.middleName(),
|
|
9
|
+
Name: () => faker.person.middleName(),
|
|
10
|
+
"Last Name": () => faker.person.middleName(),
|
|
11
|
+
Email: () => faker.internet.email(),
|
|
12
|
+
"Phone Number": () => faker.string.numeric(10),
|
|
13
|
+
Number: () => faker.string.numeric(11),
|
|
14
|
+
"Complete Number": () => faker.string.numeric(11),
|
|
15
|
+
"App Colour": () => faker.color.rgb(),
|
|
16
|
+
"App Name": () => faker.commerce.productName(),
|
|
17
|
+
"Role Name": () => faker.person.jobTitle(),
|
|
18
|
+
"Company Name": () => faker.company.name(),
|
|
19
|
+
"Full Name": () => faker.person.fullName(),
|
|
20
|
+
"Disposable Email": () => faker.internet.email({ provider: "inboxkitten.com" }),
|
|
21
|
+
"ALpha Numeric": () => faker.string.numeric(11) + "e",
|
|
22
|
+
"Lorem Word": () => faker.lorem.sentences({ min: 1, max: 3 }),
|
|
23
|
+
Word: () => faker.lorem.word({ length: { min: 5, max: 11 } }),
|
|
24
|
+
"Current Date": () => dayjs().format("YYYY-MM-DD"),
|
|
25
|
+
"Current Date2": () => new Date().toISOString().split("T")[0],
|
|
26
|
+
|
|
27
|
+
MonthsFromNow: (months?: string) => {
|
|
28
|
+
const monthsToAdd = parseInt(months || "0", 10);
|
|
29
|
+
return dayjs().add(monthsToAdd, "month").format("YYYY-MM-DD");
|
|
30
|
+
},
|
|
31
|
+
MonthsAgo: (months?: string) => {
|
|
32
|
+
const monthsToSubtract = parseInt(months || "0", 10);
|
|
33
|
+
return dayjs().subtract(monthsToSubtract, "month").format("YYYY-MM-DD");
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
WeeksFromNow: (weeks?: string) => {
|
|
37
|
+
const weeksToAdd = parseInt(weeks || "0", 10);
|
|
38
|
+
return dayjs().add(weeksToAdd, "week").format("YYYY-MM-DD");
|
|
39
|
+
},
|
|
40
|
+
WeeksAgo: (weeks?: string) => {
|
|
41
|
+
const weeksToSubtract = parseInt(weeks || "0", 10);
|
|
42
|
+
return dayjs().subtract(weeksToSubtract, "week").format("YYYY-MM-DD");
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
DaysFromNow: (days?: string) => {
|
|
46
|
+
const daysToAdd = parseInt(days || "0", 10);
|
|
47
|
+
return dayjs().add(daysToAdd, "day").format("YYYY-MM-DD");
|
|
48
|
+
},
|
|
49
|
+
DaysAgo: (days?: string) => {
|
|
50
|
+
const daysToSubtract = parseInt(days || "0", 10);
|
|
51
|
+
return dayjs().subtract(daysToSubtract, "day").format("YYYY-MM-DD");
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function evaluateFaker(value: string): string {
|
|
56
|
+
const [key, param] = value.split(":");
|
|
57
|
+
|
|
58
|
+
const fn = fakerMapping[key];
|
|
59
|
+
if (typeof fn === "function") {
|
|
60
|
+
return fn(param);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return value; // fallback to raw value if not mapped
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//optionsUtils.ts
|
|
2
|
+
import type { DataTable } from "@cucumber/cucumber";
|
|
3
|
+
import type { Locator } from "@playwright/test";
|
|
4
|
+
|
|
5
|
+
type ClickOptions = Parameters<Locator["click"]>[0];
|
|
6
|
+
type DblClickOptions = Parameters<Locator["dblclick"]>[0];
|
|
7
|
+
type HoverOptions = Parameters<Locator["hover"]>[0];
|
|
8
|
+
type FillOptions = Parameters<Locator["fill"]>[1];
|
|
9
|
+
type TypeOptions = Parameters<Locator["type"]>[1];
|
|
10
|
+
type CheckOptions = Parameters<Locator["check"]>[0];
|
|
11
|
+
type UncheckOptions = Parameters<Locator["uncheck"]>[0];
|
|
12
|
+
type SelectOptionOptions = Parameters<Locator["selectOption"]>[1];
|
|
13
|
+
|
|
14
|
+
type KeyboardModifier = NonNullable<NonNullable<ClickOptions>["modifiers"]>[number];
|
|
15
|
+
|
|
16
|
+
export function parseClickOptions(table?: DataTable): Partial<ClickOptions> {
|
|
17
|
+
return parseGenericOptions(table) as Partial<ClickOptions>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseDblClickOptions(table?: DataTable): Partial<DblClickOptions> {
|
|
21
|
+
return parseGenericOptions(table) as Partial<DblClickOptions>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseHoverOptions(table?: DataTable): Partial<HoverOptions> {
|
|
25
|
+
return parseGenericOptions(table) as Partial<HoverOptions>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseTypeOptions(table?: DataTable): Partial<TypeOptions> {
|
|
29
|
+
return parseGenericOptions(table) as unknown as Partial<TypeOptions>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseFillOptions(table?: DataTable): Partial<FillOptions> {
|
|
33
|
+
return parseGenericOptions(table) as Partial<FillOptions>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseCheckOptions(table?: DataTable): Partial<CheckOptions> {
|
|
37
|
+
return parseGenericOptions(table) as Partial<CheckOptions>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseUncheckOptions(table?: DataTable): Partial<UncheckOptions> {
|
|
41
|
+
return parseGenericOptions(table) as Partial<UncheckOptions>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseSelectOptions(table?: DataTable): Partial<SelectOptionOptions> {
|
|
45
|
+
return parseGenericOptions(table) as Partial<SelectOptionOptions>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseGenericOptions(table?: DataTable): Record<string, any> {
|
|
49
|
+
if (!table) return {};
|
|
50
|
+
|
|
51
|
+
const options: Record<string, any> = {};
|
|
52
|
+
const rows = table.raw();
|
|
53
|
+
|
|
54
|
+
for (const [key, value] of rows) {
|
|
55
|
+
switch (key) {
|
|
56
|
+
case "timeout":
|
|
57
|
+
case "delay":
|
|
58
|
+
case "clickCount":
|
|
59
|
+
options[key] = Number(value);
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case "force":
|
|
63
|
+
case "noWaitAfter":
|
|
64
|
+
case "strict":
|
|
65
|
+
case "trial":
|
|
66
|
+
options[key] = value === "true";
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case "modifiers":
|
|
70
|
+
options.modifiers = value.split(",").map((v) => v.trim() as KeyboardModifier);
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case "button":
|
|
74
|
+
if (["left", "middle", "right"].includes(value)) {
|
|
75
|
+
options.button = value;
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(`Invalid button option: "${value}"`);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case "position":
|
|
82
|
+
const [x, y] = value.split(",").map((n) => Number(n.trim()));
|
|
83
|
+
if (isNaN(x) || isNaN(y)) {
|
|
84
|
+
throw new Error(`Invalid position format: "${value}"`);
|
|
85
|
+
}
|
|
86
|
+
options.position = { x, y };
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
default:
|
|
90
|
+
console.warn(`[⚠️ parseGenericOptions] Unknown option "${key}"`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return options;
|
|
96
|
+
}
|
|
97
|
+
export function parseExpectOptions(table?: DataTable): { timeout?: number; log?: boolean } {
|
|
98
|
+
if (!table) return {};
|
|
99
|
+
const obj = Object.fromEntries(table.rows());
|
|
100
|
+
return {
|
|
101
|
+
timeout: obj.timeout ? Number(obj.timeout) : undefined,
|
|
102
|
+
log: obj.log === "true",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/helpers/utils/resolveUtils.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { devices } from "@playwright/test";
|
|
5
|
+
import type { CustomWorld } from "../world";
|
|
6
|
+
// Dynamic resolver
|
|
7
|
+
export function resolveValue(input: string): string {
|
|
8
|
+
// Uppercase = environment variable (e.g. TEST_USER)
|
|
9
|
+
if (/^[A-Z0-9_]+$/.test(input)) {
|
|
10
|
+
const envVal = process.env[input];
|
|
11
|
+
if (!envVal) {
|
|
12
|
+
throw new Error(`Environment variable ${input} not found.`);
|
|
13
|
+
}
|
|
14
|
+
return envVal;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Dot = JSON reference (e.g. userData.email)
|
|
18
|
+
if (input.includes(".")) {
|
|
19
|
+
const [fileName, fieldName] = input.split(".");
|
|
20
|
+
const jsonPath = path.resolve("e2e/support/helper/test-data", `${fileName}.json`);
|
|
21
|
+
if (fs.existsSync(jsonPath)) {
|
|
22
|
+
const json = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
23
|
+
const value = json[fieldName];
|
|
24
|
+
if (value !== undefined) return value;
|
|
25
|
+
throw new Error(`Field "${fieldName}" not found in ${fileName}.json`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Default to hardcoded value
|
|
30
|
+
return input;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveLoginValue(raw: string, world: CustomWorld): string | undefined {
|
|
34
|
+
// ✅ Alias: @aliasName
|
|
35
|
+
if (raw.startsWith("@")) {
|
|
36
|
+
return world.data[raw.slice(1)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ✅ JSON: user.json:key
|
|
40
|
+
if (raw.includes(".json:")) {
|
|
41
|
+
const [filename, key] = raw.split(".json:");
|
|
42
|
+
const filePath = path.resolve("test-data", `${filename}.json`);
|
|
43
|
+
if (!fs.existsSync(filePath)) {
|
|
44
|
+
throw new Error(`JSON fixture not found: ${filename}.json`);
|
|
45
|
+
}
|
|
46
|
+
const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
47
|
+
return fileData[key];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ✅ Fallback to raw value
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
// Determine session name from email or username
|
|
54
|
+
export function deriveSessionName(emailOrUser: string, fallback = "default"): string {
|
|
55
|
+
if (!emailOrUser) return `${fallback}User`;
|
|
56
|
+
|
|
57
|
+
const base = emailOrUser.includes("@") ? emailOrUser.split("@")[0] : emailOrUser;
|
|
58
|
+
|
|
59
|
+
return `${base}User`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveSessionPath(world: CustomWorld, name: string) {
|
|
63
|
+
const dir = path.resolve(world.data.artifactDir || "test-artifacts", "auth-cookies");
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
return path.resolve(dir, name.endsWith(".json") ? name : `${name}.json`);
|
|
66
|
+
}
|
|
67
|
+
export function normalizeDeviceName(name: string): string {
|
|
68
|
+
return (
|
|
69
|
+
Object.keys(devices).find(
|
|
70
|
+
(device) =>
|
|
71
|
+
device.toLowerCase().replace(/\s+/g, "") === name.toLowerCase().replace(/[-_\s]+/g, "")
|
|
72
|
+
) || ""
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//sessionUtils.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { BrowserContext } from "playwright";
|
|
5
|
+
|
|
6
|
+
export async function injectStorage(world: any, type: string, key: string, value: string) {
|
|
7
|
+
const page = world.page;
|
|
8
|
+
|
|
9
|
+
if (type === "localStorage") {
|
|
10
|
+
await page.evaluate(([k, v]: [string, string]) => localStorage.setItem(k, v), [key, value]);
|
|
11
|
+
} else if (type === "sessionStorage") {
|
|
12
|
+
await page.evaluate(([k, v]: [string, string]) => sessionStorage.setItem(k, v), [key, value]);
|
|
13
|
+
} else if (type === "cookie") {
|
|
14
|
+
await page.context.addCookies([
|
|
15
|
+
{
|
|
16
|
+
name: key,
|
|
17
|
+
value,
|
|
18
|
+
domain: new URL(page.url()).hostname,
|
|
19
|
+
path: "/",
|
|
20
|
+
httpOnly: false,
|
|
21
|
+
secure: true,
|
|
22
|
+
sameSite: "Lax",
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function saveSessionData(context: BrowserContext, file: string) {
|
|
29
|
+
const storageState = await context.storageState();
|
|
30
|
+
const outDir = "test-artifacts/auth-cookies";
|
|
31
|
+
const outPath = path.resolve(outDir, file);
|
|
32
|
+
|
|
33
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
34
|
+
fs.writeFileSync(outPath, JSON.stringify(storageState, null, 2));
|
|
35
|
+
console.log(`💾 Saved session to ${outPath}`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { setWorldConstructor, World, ITestCaseHookParameter } from "@cucumber/cucumber";
|
|
2
|
+
import { Browser, Page, BrowserContext, Locator, FrameLocator, devices } from "@playwright/test";
|
|
3
|
+
import * as dotenv from "dotenv";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
const isHeadless = process.env.HEADLESS !== "false";
|
|
9
|
+
const slowMo = process.env.SLOWMO ? Number(process.env.SLOWMO) : 0;
|
|
10
|
+
|
|
11
|
+
// Define a minimal interface for the clock object you expect
|
|
12
|
+
// This is to help TypeScript understand the shape of context.clock
|
|
13
|
+
// interface PlaywrightClock {
|
|
14
|
+
// setFixedTime(time: number | Date): Promise<void>;
|
|
15
|
+
// tick(ms: number): Promise<void>;
|
|
16
|
+
// restore(): Promise<void>;
|
|
17
|
+
// }
|
|
18
|
+
|
|
19
|
+
// If you need to extend the Clock type, do it via module augmentation with compatible types.
|
|
20
|
+
// Otherwise, do not redeclare the 'clock' property to avoid type conflicts.
|
|
21
|
+
// Remove the BrowserContext augmentation for 'clock' to resolve the type error.
|
|
22
|
+
|
|
23
|
+
export interface TestConfig {
|
|
24
|
+
enableScreenshots: boolean;
|
|
25
|
+
enableVisualTest: boolean;
|
|
26
|
+
artifactDir: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class CustomWorld extends World {
|
|
30
|
+
browser!: Browser;
|
|
31
|
+
context!: BrowserContext;
|
|
32
|
+
page!: Page;
|
|
33
|
+
|
|
34
|
+
elements?: Locator;
|
|
35
|
+
element?: Locator;
|
|
36
|
+
frame?: FrameLocator;
|
|
37
|
+
currentLocator?: Locator;
|
|
38
|
+
|
|
39
|
+
data: Record<string, any> = {};
|
|
40
|
+
logs: string[] = [];
|
|
41
|
+
testName?: string;
|
|
42
|
+
|
|
43
|
+
fakeTimersActive: boolean;
|
|
44
|
+
|
|
45
|
+
config: TestConfig = {
|
|
46
|
+
enableScreenshots: true,
|
|
47
|
+
enableVisualTest: false,
|
|
48
|
+
artifactDir: "test-artifacts",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
constructor(options: import("@cucumber/cucumber").IWorldOptions) {
|
|
52
|
+
super(options);
|
|
53
|
+
this.fakeTimersActive = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async init(testInfo?: ITestCaseHookParameter) {
|
|
57
|
+
const info =
|
|
58
|
+
testInfo ?? ((this as any).parameters?.testInfo as ITestCaseHookParameter | undefined);
|
|
59
|
+
const isMobile = info?.pickle.tags.some((tag) => tag.name === "@mobile");
|
|
60
|
+
const device = isMobile ? devices["Pixel 5"] : undefined;
|
|
61
|
+
|
|
62
|
+
this.browser = await chromium.launch({ headless: isHeadless, slowMo });
|
|
63
|
+
|
|
64
|
+
this.context = await this.browser.newContext({
|
|
65
|
+
...(device || {}),
|
|
66
|
+
recordVideo: { dir: `${this.config.artifactDir}/videos` },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Important: Initialize clock mocking *before* navigating or interacting with the page
|
|
70
|
+
// if you intend to use page.clock. This typically involves addInitScript.
|
|
71
|
+
// However, the context.clock API does not usually require an init script for its methods.
|
|
72
|
+
// If you explicitly loaded a mocking library, you'd do it here.
|
|
73
|
+
// For just context.clock methods, they should be available.
|
|
74
|
+
|
|
75
|
+
this.page = await this.context.newPage();
|
|
76
|
+
this.testName = info?.pickle.name;
|
|
77
|
+
this.log(`🧪 Initialized context${isMobile ? " (mobile)" : ""}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the current interaction scope: either the main page or active frame.
|
|
82
|
+
*/
|
|
83
|
+
getScope(): Page | FrameLocator {
|
|
84
|
+
return this.frame ?? this.page;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns a Locator scoped to the current iframe or page.
|
|
89
|
+
*/
|
|
90
|
+
getLocator(selector: string): Locator {
|
|
91
|
+
return this.getScope().locator(selector);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exitIframe() {
|
|
95
|
+
this.frame = undefined;
|
|
96
|
+
this.log("⬅️ Exited iframe, scope is now main page");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
log = (message: string) => {
|
|
100
|
+
this.logs.push(message);
|
|
101
|
+
console.log(`[LOG] ${message}`);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
async cleanup(testInfo?: ITestCaseHookParameter) {
|
|
105
|
+
try {
|
|
106
|
+
await this.page?.close();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
this.log(`⚠️ Error closing page: ${(err as Error).message}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await this.context?.close();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
this.log(`⚠️ Error closing context: ${(err as Error).message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setWorldConstructor(CustomWorld);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { When } from "@cucumber/cucumber";
|
|
2
|
+
// import { expect } from "@playwright/test";
|
|
3
|
+
import type { CustomWorld } from "../helpers/world";
|
|
4
|
+
|
|
5
|
+
When(
|
|
6
|
+
"I find href in iframe {string} and store as {string}",
|
|
7
|
+
async function (this: CustomWorld, iframeSelector: string, key: string) {
|
|
8
|
+
const iframe = this.page.frameLocator(iframeSelector);
|
|
9
|
+
const link = await iframe.locator("a[href]").first();
|
|
10
|
+
const href = await link.getAttribute("href");
|
|
11
|
+
|
|
12
|
+
if (!href) throw new Error("No link found in iframe.");
|
|
13
|
+
this.data[key] = href;
|
|
14
|
+
}
|
|
15
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Iframes
|
|
2
|
+
export * from "./iframes/frames";
|
|
3
|
+
|
|
4
|
+
// Setup (custom hooks, login, etc.)
|
|
5
|
+
export * from "./custom_setups/loginHooks";
|
|
6
|
+
|
|
7
|
+
// Core helpers and utilities
|
|
8
|
+
export * from "./helpers/compareSnapshots";
|
|
9
|
+
export * from "./helpers/hooks";
|
|
10
|
+
export * from "./helpers/utils";
|
|
11
|
+
export * from "./helpers/world";
|
|
12
|
+
|
|
13
|
+
// Types
|
|
14
|
+
export type { CustomWorld } from "./helpers/world";
|
|
15
|
+
|
|
16
|
+
export * from "./actions";
|
|
17
|
+
export * from "./assertions";
|
|
18
|
+
export * from "./custom_setups/loginHooks";
|
package/src/register.ts
ADDED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
// e2e/step_definitions/common/actions/clickSteps.ts
|
|
4
|
-
const cucumber_1 = require("@cucumber/cucumber");
|
|
5
|
-
const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
6
|
-
(0, cucumber_1.When)("I click", async function (...rest) {
|
|
7
|
-
const maybeTable = rest[0];
|
|
8
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
9
|
-
if (!this.element)
|
|
10
|
-
throw new Error("❌ No stored element to click.");
|
|
11
|
-
await this.element.click(options);
|
|
12
|
-
this.log?.("🖱️ Clicked on stored element");
|
|
13
|
-
});
|
|
14
|
-
(0, cucumber_1.When)("I click on element {string}", async function (selector, ...rest) {
|
|
15
|
-
const maybeTable = rest[0];
|
|
16
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
17
|
-
const element = this.getLocator(selector);
|
|
18
|
-
await element.click(options);
|
|
19
|
-
this.element = element;
|
|
20
|
-
this.log?.(`🖱️ Clicked on element "${selector}"`);
|
|
21
|
-
});
|
|
22
|
-
(0, cucumber_1.When)("I click on button {string}", async function (label, ...rest) {
|
|
23
|
-
const maybeTable = rest[0];
|
|
24
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
25
|
-
const button = await this.page.getByRole("button", { name: label });
|
|
26
|
-
await button.click(options);
|
|
27
|
-
this.element = button;
|
|
28
|
-
this.log?.(`🖱️ Clicked on button "${label}"`);
|
|
29
|
-
});
|
|
30
|
-
(0, cucumber_1.When)("I click on link {string}", async function (text, ...rest) {
|
|
31
|
-
const maybeTable = rest[0];
|
|
32
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
33
|
-
const link = await this.page.getByRole("link", { name: text });
|
|
34
|
-
await link.click(options);
|
|
35
|
-
this.element = link;
|
|
36
|
-
this.log?.(`✅ Clicked on link "${text}"`);
|
|
37
|
-
});
|
|
38
|
-
(0, cucumber_1.When)("I click on label {string}", async function (labelText, ...rest) {
|
|
39
|
-
const maybeTable = rest[0];
|
|
40
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
41
|
-
const label = await this.page.getByLabel(labelText);
|
|
42
|
-
await label.click(options);
|
|
43
|
-
this.element = label;
|
|
44
|
-
this.log?.(`🏷️ Clicked on label "${labelText}"`);
|
|
45
|
-
});
|
|
46
|
-
(0, cucumber_1.When)("I click on text {string}", async function (rawText, ...rest) {
|
|
47
|
-
const maybeTable = rest[0];
|
|
48
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
49
|
-
let text = rawText;
|
|
50
|
-
if (rawText.startsWith("@")) {
|
|
51
|
-
const alias = rawText.slice(1);
|
|
52
|
-
text = this.data[alias];
|
|
53
|
-
if (!text)
|
|
54
|
-
throw new Error(`❌ No value found for alias "@${alias}"`);
|
|
55
|
-
}
|
|
56
|
-
const locator = this.page.getByText(text, { exact: false });
|
|
57
|
-
await locator.first().waitFor({ state: "visible", timeout: 5000 });
|
|
58
|
-
await locator.first().click(options);
|
|
59
|
-
this.element = locator.first();
|
|
60
|
-
this.log?.(`🖱️ Clicked on text "${text}"`);
|
|
61
|
-
});
|
|
62
|
-
(0, cucumber_1.When)("I click on exact text {string}", async function (exactText, ...rest) {
|
|
63
|
-
const maybeTable = rest[0];
|
|
64
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
65
|
-
const locator = this.page.getByText(exactText, { exact: true });
|
|
66
|
-
await locator.waitFor({ state: "visible", timeout: 5000 });
|
|
67
|
-
await locator.click(options);
|
|
68
|
-
this.element = locator;
|
|
69
|
-
this.log?.(`🖱️ Clicked on exact text "${exactText}"`);
|
|
70
|
-
});
|
|
71
|
-
(0, cucumber_1.When)("I click all", async function (...rest) {
|
|
72
|
-
const maybeTable = rest[0];
|
|
73
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
74
|
-
if (!this.elements)
|
|
75
|
-
throw new Error("❌ No stored elements to click.");
|
|
76
|
-
const count = await this.elements.count();
|
|
77
|
-
if (count === 0)
|
|
78
|
-
throw new Error("⚠️ No elements found to click.");
|
|
79
|
-
for (let i = 0; i < count; i++) {
|
|
80
|
-
const el = this.elements.nth(i);
|
|
81
|
-
await el.waitFor({ state: "visible", timeout: 5000 });
|
|
82
|
-
await el.click(options);
|
|
83
|
-
this.log?.(`🖱️ Clicked element #${i + 1}`);
|
|
84
|
-
}
|
|
85
|
-
this.log?.(`✅ Clicked all ${count} elements.`);
|
|
86
|
-
});
|
|
87
|
-
(0, cucumber_1.When)("I double click on text {string}", async function (text, ...rest) {
|
|
88
|
-
const maybeTable = rest[0];
|
|
89
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
90
|
-
const element = this.element || this.page.getByText(text);
|
|
91
|
-
await element.dblclick(options);
|
|
92
|
-
this.log?.(`🖱️ Double-clicked on text "${text}"`);
|
|
93
|
-
});
|
|
94
|
-
(0, cucumber_1.When)("I double click position {int} {int}", async function (x, y, ...rest) {
|
|
95
|
-
const maybeTable = rest[0];
|
|
96
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
97
|
-
await this.page.mouse.dblclick(x, y, options);
|
|
98
|
-
this.log?.(`🖱️ Double-clicked at (${x}, ${y})`);
|
|
99
|
-
});
|
|
100
|
-
(0, cucumber_1.When)("I double click", async function (...rest) {
|
|
101
|
-
const maybeTable = rest[0];
|
|
102
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
103
|
-
if (!this.element)
|
|
104
|
-
throw new Error("❌ No stored element to double-click.");
|
|
105
|
-
await this.element.dblclick(options);
|
|
106
|
-
this.log?.("🖱️ Double-clicked on stored element");
|
|
107
|
-
});
|
|
108
|
-
(0, cucumber_1.When)("I right click", async function (...rest) {
|
|
109
|
-
const maybeTable = rest[0];
|
|
110
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
111
|
-
if (!this.element)
|
|
112
|
-
throw new Error("❌ No stored element to right-click.");
|
|
113
|
-
await this.element.click({ button: "right", ...options });
|
|
114
|
-
this.log?.("🖱️ Right-clicked on stored element");
|
|
115
|
-
});
|
|
116
|
-
(0, cucumber_1.When)("I right click on text {string}", async function (text, ...rest) {
|
|
117
|
-
const maybeTable = rest[0];
|
|
118
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
119
|
-
const element = this.page.getByText(text);
|
|
120
|
-
await element.click({ button: "right", ...options });
|
|
121
|
-
this.log?.(`🖱️ Right-clicked on text "${text}"`);
|
|
122
|
-
});
|
|
123
|
-
(0, cucumber_1.When)("I right click position {int} {int}", async function (x, y, ...rest) {
|
|
124
|
-
const maybeTable = rest[0];
|
|
125
|
-
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
126
|
-
await this.page.mouse.click(x, y, { button: "right", ...options });
|
|
127
|
-
this.log?.(`🖱️ Right-clicked at (${x}, ${y})`);
|
|
128
|
-
});
|
|
129
|
-
(0, cucumber_1.When)("I blur", async function () {
|
|
130
|
-
await this.page.evaluate(() => {
|
|
131
|
-
const active = document.activeElement;
|
|
132
|
-
if (active && typeof active.blur === "function") {
|
|
133
|
-
active.blur();
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
this.log?.("🌀 Blurred active element");
|
|
137
|
-
});
|
|
138
|
-
(0, cucumber_1.When)("I focus", async function () {
|
|
139
|
-
if (!this.element)
|
|
140
|
-
throw new Error("❌ No stored element to focus.");
|
|
141
|
-
await this.element.focus();
|
|
142
|
-
this.log?.("🎯 Focused stored element");
|
|
143
|
-
});
|
|
144
|
-
(0, cucumber_1.When)("I click all", async function (dataTable) {
|
|
145
|
-
const options = (0, optionsUtils_1.parseClickOptions)(dataTable);
|
|
146
|
-
if (!this.elements) {
|
|
147
|
-
throw new Error("❌ No elements stored. Use a 'find' step before 'I click all'.");
|
|
148
|
-
}
|
|
149
|
-
const count = await this.elements.count();
|
|
150
|
-
if (count === 0) {
|
|
151
|
-
throw new Error("⚠️ Stored elements are empty. Nothing to click.");
|
|
152
|
-
}
|
|
153
|
-
for (let i = 0; i < count; i++) {
|
|
154
|
-
const element = this.elements.nth(i);
|
|
155
|
-
await element.waitFor({ state: "visible", timeout: 5000 });
|
|
156
|
-
await element.click(options);
|
|
157
|
-
this.log?.(`🖱️ Clicked element #${i + 1}`);
|
|
158
|
-
}
|
|
159
|
-
this.log?.(`✅ Clicked all ${count} stored elements.`);
|
|
160
|
-
});
|
|
161
|
-
(0, cucumber_1.When)(/^I click on selector "([^"]+)"$/, async function (selector) {
|
|
162
|
-
const locator = this.getLocator(selector);
|
|
163
|
-
await locator.click();
|
|
164
|
-
this.log?.(`🖱️ Clicked on selector: ${selector}`);
|
|
165
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
// e2e/step_definitions/common/actions/cookieSteps.ts
|
|
4
|
-
const cucumber_1 = require("@cucumber/cucumber");
|
|
5
|
-
(0, cucumber_1.When)("I clear all cookies", async function () {
|
|
6
|
-
await this.page.context().clearCookies();
|
|
7
|
-
this.log("Cleared all cookies");
|
|
8
|
-
});
|
|
9
|
-
(0, cucumber_1.When)("I clear cookies", async function () {
|
|
10
|
-
await this.page.context().clearCookies();
|
|
11
|
-
this.log("Cleared cookies (alias)");
|
|
12
|
-
});
|
|
13
|
-
(0, cucumber_1.When)("I clear cookie {string}", async function (name) {
|
|
14
|
-
await this.page.context().addCookies([
|
|
15
|
-
{
|
|
16
|
-
name,
|
|
17
|
-
value: "",
|
|
18
|
-
domain: new URL(this.page.url()).hostname,
|
|
19
|
-
path: "/",
|
|
20
|
-
expires: 0,
|
|
21
|
-
},
|
|
22
|
-
]);
|
|
23
|
-
this.log(`Cleared cookie: ${name}`);
|
|
24
|
-
});
|
|
25
|
-
(0, cucumber_1.When)("I log all cookies", async function () {
|
|
26
|
-
const cookies = await this.page.context().cookies();
|
|
27
|
-
console.log("Cookies:", cookies);
|
|
28
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
// e2e/step_definitions/common/actions/debugSteps.ts
|
|
4
|
-
const cucumber_1 = require("@cucumber/cucumber");
|
|
5
|
-
(0, cucumber_1.When)("I debug with message {string}", async function (message) {
|
|
6
|
-
await this.page.pause();
|
|
7
|
-
this.log(`Paused test for debugging: ${message}`);
|
|
8
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|