playwright-cucumber-ts-steps 0.0.4 → 0.0.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/dist/actions/Interception & Requests.d.ts +1 -0
- package/dist/actions/Interception & Requests.js +39 -0
- package/dist/actions/clickSteps.d.ts +1 -0
- package/dist/actions/clickSteps.js +173 -0
- package/dist/actions/cookieSteps.d.ts +1 -0
- package/dist/actions/cookieSteps.js +26 -0
- package/dist/actions/debugSteps.d.ts +1 -0
- package/dist/actions/debugSteps.js +9 -0
- package/dist/actions/elementFindSteps.d.ts +1 -0
- package/dist/actions/elementFindSteps.js +239 -0
- package/dist/actions/inputSteps.d.ts +1 -0
- package/dist/actions/inputSteps.js +166 -0
- package/dist/actions/interceptionSteps.d.ts +1 -0
- package/dist/actions/interceptionSteps.js +61 -0
- package/dist/actions/miscSteps.d.ts +1 -0
- package/dist/actions/miscSteps.js +277 -0
- package/dist/actions/mouseSteps.d.ts +1 -0
- package/dist/actions/mouseSteps.js +67 -0
- package/dist/actions/scrollSteps.d.ts +1 -0
- package/dist/actions/scrollSteps.js +21 -0
- package/dist/actions/storageSteps.d.ts +1 -0
- package/dist/actions/storageSteps.js +49 -0
- package/dist/assertions/InterceptionRequests.d.ts +1 -0
- package/dist/assertions/InterceptionRequests.js +191 -0
- package/dist/assertions/button_and_text_visibility.d.ts +1 -0
- package/dist/assertions/button_and_text_visibility.js +172 -0
- package/dist/assertions/cookieSteps.d.ts +1 -0
- package/dist/assertions/cookieSteps.js +43 -0
- package/dist/assertions/elementSteps.d.ts +1 -0
- package/dist/assertions/elementSteps.js +84 -0
- package/dist/assertions/formInputSteps.d.ts +1 -0
- package/dist/assertions/formInputSteps.js +85 -0
- package/dist/assertions/locationSteps.d.ts +1 -0
- package/dist/assertions/locationSteps.js +69 -0
- package/dist/assertions/roleTestIdSteps.d.ts +1 -0
- package/dist/assertions/roleTestIdSteps.js +24 -0
- package/dist/assertions/semanticSteps.d.ts +1 -0
- package/dist/assertions/semanticSteps.js +52 -0
- package/dist/assertions/storageSteps.d.ts +1 -0
- package/dist/assertions/storageSteps.js +70 -0
- package/dist/assertions/visualSteps.d.ts +1 -0
- package/dist/assertions/visualSteps.js +70 -0
- package/dist/custom_setups/global-login.d.ts +2 -0
- package/dist/custom_setups/global-login.js +20 -0
- package/dist/custom_setups/loginHooks.d.ts +1 -0
- package/dist/custom_setups/loginHooks.js +139 -0
- package/dist/helpers/compareSnapshots.d.ts +6 -0
- package/dist/helpers/compareSnapshots.js +14 -0
- package/dist/helpers/hooks.d.ts +1 -0
- package/dist/helpers/hooks.js +148 -0
- package/dist/helpers/world.js +9 -7
- package/dist/iframes/frames.d.ts +1 -0
- package/dist/iframes/frames.js +9 -0
- package/dist/index.d.ts +28 -5
- package/dist/index.js +28 -5
- package/dist/register.d.ts +1 -0
- package/dist/register.js +2 -0
- package/package.json +2 -8
- package/index.ts +0 -10
- package/register.ts +0 -31
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Then } from "@cucumber/cucumber";
|
|
2
|
+
import pixelmatch from "pixelmatch";
|
|
3
|
+
import { PNG } from "pngjs";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { expect } from "@playwright/test";
|
|
7
|
+
const BASELINE_DIR = path.resolve("e2e/snapshots/baseline");
|
|
8
|
+
const CURRENT_DIR = path.resolve("e2e/snapshots/current");
|
|
9
|
+
const DIFF_DIR = path.resolve("e2e/snapshots/diff");
|
|
10
|
+
function getSnapshotPaths(name) {
|
|
11
|
+
const safeName = name.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
12
|
+
return {
|
|
13
|
+
baseline: path.join(BASELINE_DIR, `${safeName}.png`),
|
|
14
|
+
current: path.join(CURRENT_DIR, `${safeName}.png`),
|
|
15
|
+
diff: path.join(DIFF_DIR, `${safeName}.diff.png`),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
Then("I should see the page matches the snapshot {string}", async function (name) {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
const { page } = this;
|
|
21
|
+
const paths = getSnapshotPaths(name);
|
|
22
|
+
fs.mkdirSync(BASELINE_DIR, { recursive: true });
|
|
23
|
+
fs.mkdirSync(CURRENT_DIR, { recursive: true });
|
|
24
|
+
fs.mkdirSync(DIFF_DIR, { recursive: true });
|
|
25
|
+
await page.screenshot({ path: paths.current, fullPage: true });
|
|
26
|
+
if (!fs.existsSync(paths.baseline)) {
|
|
27
|
+
fs.copyFileSync(paths.current, paths.baseline);
|
|
28
|
+
(_a = this.log) === null || _a === void 0 ? void 0 : _a.call(this, `📸 Created baseline snapshot: ${paths.baseline}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const baseline = PNG.sync.read(fs.readFileSync(paths.baseline));
|
|
32
|
+
const current = PNG.sync.read(fs.readFileSync(paths.current));
|
|
33
|
+
const { width, height } = baseline;
|
|
34
|
+
const diff = new PNG({ width, height });
|
|
35
|
+
const pixelDiff = pixelmatch(baseline.data, current.data, diff.data, width, height, { threshold: 0.1 });
|
|
36
|
+
if (pixelDiff > 0) {
|
|
37
|
+
fs.writeFileSync(paths.diff, PNG.sync.write(diff));
|
|
38
|
+
(_b = this.log) === null || _b === void 0 ? void 0 : _b.call(this, `❌ Visual mismatch detected, diff: ${paths.diff}`);
|
|
39
|
+
}
|
|
40
|
+
expect(pixelDiff, "Pixels that differ").toBe(0);
|
|
41
|
+
});
|
|
42
|
+
Then("I capture a snapshot of the element {string} as {string}", async function (selector, alias) {
|
|
43
|
+
var _a;
|
|
44
|
+
const element = this.getScope().locator(selector);
|
|
45
|
+
const pathCurrent = path.join(CURRENT_DIR, `${alias}.png`);
|
|
46
|
+
fs.mkdirSync(CURRENT_DIR, { recursive: true });
|
|
47
|
+
await element.screenshot({ path: pathCurrent });
|
|
48
|
+
(_a = this.log) === null || _a === void 0 ? void 0 : _a.call(this, `📸 Snapshot for ${selector} saved as ${alias}`);
|
|
49
|
+
});
|
|
50
|
+
Then("The snapshot {string} should match baseline", async function (alias) {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
const paths = getSnapshotPaths(alias);
|
|
53
|
+
const current = PNG.sync.read(fs.readFileSync(paths.current));
|
|
54
|
+
const baseline = fs.existsSync(paths.baseline)
|
|
55
|
+
? PNG.sync.read(fs.readFileSync(paths.baseline))
|
|
56
|
+
: null;
|
|
57
|
+
if (!baseline) {
|
|
58
|
+
fs.copyFileSync(paths.current, paths.baseline);
|
|
59
|
+
(_a = this.log) === null || _a === void 0 ? void 0 : _a.call(this, `📸 Created new baseline for ${alias}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const { width, height } = baseline;
|
|
63
|
+
const diff = new PNG({ width, height });
|
|
64
|
+
const pixelDiff = pixelmatch(baseline.data, current.data, diff.data, width, height, { threshold: 0.1 });
|
|
65
|
+
if (pixelDiff > 0) {
|
|
66
|
+
fs.writeFileSync(paths.diff, PNG.sync.write(diff));
|
|
67
|
+
(_b = this.log) === null || _b === void 0 ? void 0 : _b.call(this, `⚠️ Snapshot mismatch: ${alias}`);
|
|
68
|
+
}
|
|
69
|
+
expect(pixelDiff).toBe(0);
|
|
70
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// global-setup.ts
|
|
2
|
+
import { chromium } from "@playwright/test";
|
|
3
|
+
import path from "path";
|
|
4
|
+
async function globalSetup() {
|
|
5
|
+
const browser = await chromium.launch();
|
|
6
|
+
const page = await browser.newPage();
|
|
7
|
+
// Navigate to login and log in
|
|
8
|
+
await page.goto(process.env.PLAYWRIGHT_BASE_URL || "http://demoqa.com");
|
|
9
|
+
await page.getByPlaceholder("Email").fill("user@example.com");
|
|
10
|
+
await page.getByPlaceholder("Password").fill("SuperSecret123");
|
|
11
|
+
await page.getByRole("button", { name: "Login" }).click();
|
|
12
|
+
// Wait for navigation or some indicator that login is complete
|
|
13
|
+
await page.waitForURL("**/dashboard");
|
|
14
|
+
// Save session to file
|
|
15
|
+
await page
|
|
16
|
+
.context()
|
|
17
|
+
.storageState({ path: path.resolve(__dirname, "storageState.json") });
|
|
18
|
+
await browser.close();
|
|
19
|
+
}
|
|
20
|
+
export default globalSetup;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// e2e/step_definitions/auth/loginHooks.ts
|
|
2
|
+
import { When } from "@cucumber/cucumber";
|
|
3
|
+
import { expect } from "@playwright/test";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { resolveValue, deriveSessionName, resolveLoginValue, } from "../helpers/utils/resolveUtils";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { log } from "console";
|
|
8
|
+
async function tryRestoreSession(world, sessionName) {
|
|
9
|
+
var _a;
|
|
10
|
+
const storagePath = path.resolve("e2e/support/helper/auth", `${sessionName}.json`);
|
|
11
|
+
if (fs.existsSync(storagePath)) {
|
|
12
|
+
await ((_a = world.context) === null || _a === void 0 ? void 0 : _a.addCookies([]));
|
|
13
|
+
await world.page.context().addInitScript(() => {
|
|
14
|
+
// preload logic if needed
|
|
15
|
+
});
|
|
16
|
+
await world.page.context().storageState({ path: storagePath });
|
|
17
|
+
world.log(`Session for ${sessionName} restored from file.`);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
async function loginAndSaveSession(world, email, password, sessionName) {
|
|
23
|
+
const baseUrl = process.env.BASE_URL;
|
|
24
|
+
if (!baseUrl)
|
|
25
|
+
throw new Error("Missing BASE_URL in environment");
|
|
26
|
+
await pageLogin(world, baseUrl, email, password);
|
|
27
|
+
const sessionFile = path.resolve("e2e/support/helper/auth", `${sessionName}.json`);
|
|
28
|
+
await world.page.context().storageState({ path: sessionFile });
|
|
29
|
+
world.log(`Session saved to ${sessionFile}`);
|
|
30
|
+
}
|
|
31
|
+
const loginStep = async function (user, pass) {
|
|
32
|
+
const email = resolveValue(user);
|
|
33
|
+
const password = resolveValue(pass);
|
|
34
|
+
const sessionName = deriveSessionName(email);
|
|
35
|
+
const restored = await tryRestoreSession(this, sessionName);
|
|
36
|
+
if (!restored) {
|
|
37
|
+
await loginAndSaveSession(this, email, password, sessionName);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
When("I am logged out", async function () {
|
|
41
|
+
const { page } = this;
|
|
42
|
+
await page.goto("/");
|
|
43
|
+
const url = page.url();
|
|
44
|
+
if (!url.includes("/login")) {
|
|
45
|
+
await page.locator("//div[@id=':rti:']//*[name()='svg']").last().click();
|
|
46
|
+
await page.getByText("Logout").click();
|
|
47
|
+
await expect(page.getByText("Log in")).toBeVisible();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
When("I login as a default user", async function () {
|
|
51
|
+
const baseUrl = process.env.BASE_URL;
|
|
52
|
+
const email = process.env.USER_EMAIL;
|
|
53
|
+
const password = process.env.USER_PASSWORD;
|
|
54
|
+
if (!baseUrl || !email || !password) {
|
|
55
|
+
console.warn("Missing base URL or credentials");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await pageLogin(this, baseUrl, email, password);
|
|
59
|
+
});
|
|
60
|
+
When("I login with user {string} and password {string}", loginStep);
|
|
61
|
+
// Given("I login with user {string} and password {string}", loginStep);
|
|
62
|
+
// Special alias step
|
|
63
|
+
When("I login as {string} user", async function (alias) {
|
|
64
|
+
const sessionFile = path.resolve("storage", `${alias}User.json`);
|
|
65
|
+
const email = process.env.USER_EMAIL;
|
|
66
|
+
const password = process.env.USER_PASSWORD;
|
|
67
|
+
const baseUrl = process.env.BASE_URL;
|
|
68
|
+
if (!email || !password) {
|
|
69
|
+
throw new Error("USER_EMAIL or USER_PASSWORD not set in .env");
|
|
70
|
+
}
|
|
71
|
+
if (fs.existsSync(sessionFile)) {
|
|
72
|
+
await this.context.addCookies([]); // optional reset
|
|
73
|
+
await this.context.addInitScript(() => { });
|
|
74
|
+
await this.context.storageState({ path: sessionFile });
|
|
75
|
+
this.log(`🗂️ Restored session from ${alias}User.json`);
|
|
76
|
+
// Wait for page to stabilize
|
|
77
|
+
await this.page.goto(baseUrl);
|
|
78
|
+
await this.page.waitForTimeout(1000);
|
|
79
|
+
const bodyText = await this.page.locator("body").innerText();
|
|
80
|
+
const url = this.page.url();
|
|
81
|
+
const looksLoggedOut = bodyText.match(/sign\s?in|login/i) || /login|signin/.test(url);
|
|
82
|
+
if (!looksLoggedOut) {
|
|
83
|
+
this.log("✅ Session is valid, no login required.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.log("⚠️ Session appears invalid, re-logging in.");
|
|
87
|
+
}
|
|
88
|
+
// Perform login
|
|
89
|
+
await pageLogin(this, baseUrl, email, password);
|
|
90
|
+
await this.context.storageState({ path: sessionFile });
|
|
91
|
+
this.log(`💾 Saved session to ${alias}User.json`);
|
|
92
|
+
});
|
|
93
|
+
When("I perform login with:", async function (dataTable) {
|
|
94
|
+
var _a, _b, _c;
|
|
95
|
+
const loginData = Object.fromEntries(dataTable.raw());
|
|
96
|
+
const email = resolveLoginValue(loginData.email, this);
|
|
97
|
+
const password = (_a = resolveLoginValue(loginData.password, this)) !== null && _a !== void 0 ? _a : process.env.USER_PASSWORD;
|
|
98
|
+
if (!email)
|
|
99
|
+
throw new Error("Missing or invalid email for login");
|
|
100
|
+
if (!password)
|
|
101
|
+
throw new Error("Missing or invalid password for login");
|
|
102
|
+
(_b = this.log) === null || _b === void 0 ? void 0 : _b.call(this, `🔐 Logging in with: ${email}`);
|
|
103
|
+
await this.page.goto(`${process.env.BASE_URL}/login`);
|
|
104
|
+
await this.page.waitForLoadState("networkidle");
|
|
105
|
+
await this.page.fill('input[type="email"]', email);
|
|
106
|
+
await this.page.fill('input[type="password"]', password);
|
|
107
|
+
const loginButton = this.page.getByRole("button", { name: /login/i });
|
|
108
|
+
await loginButton.click();
|
|
109
|
+
await this.page.waitForLoadState("networkidle");
|
|
110
|
+
(_c = this.log) === null || _c === void 0 ? void 0 : _c.call(this, "✅ Login successful");
|
|
111
|
+
});
|
|
112
|
+
async function pageLogin(world, baseUrl, email, password) {
|
|
113
|
+
var _a;
|
|
114
|
+
const { page } = world;
|
|
115
|
+
await page.goto(baseUrl);
|
|
116
|
+
await page.waitForLoadState("networkidle");
|
|
117
|
+
if (page.url().includes("/login")) {
|
|
118
|
+
await page.getByPlaceholder("email").fill(email);
|
|
119
|
+
await page.getByPlaceholder("password").fill(password);
|
|
120
|
+
await page.getByRole("button", { name: "Login" }).click();
|
|
121
|
+
await page.waitForLoadState("networkidle");
|
|
122
|
+
if (await page.getByText("Select an account").isVisible()) {
|
|
123
|
+
log("Login successful, navigating to accounts page");
|
|
124
|
+
await page.goto(`${baseUrl}/indicina`);
|
|
125
|
+
}
|
|
126
|
+
await expect(page.getByText("Total unique customers")).toBeVisible({
|
|
127
|
+
timeout: 10000,
|
|
128
|
+
});
|
|
129
|
+
// ✅ Save session after successful login
|
|
130
|
+
await page.context().storageState({
|
|
131
|
+
path: path.resolve("e2e/support/helper/auth", "session.json"),
|
|
132
|
+
});
|
|
133
|
+
world.data["loggedIn"] = true;
|
|
134
|
+
(_a = world.log) === null || _a === void 0 ? void 0 : _a.call(world, "✅ Logged in and session saved.");
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.log("Already logged in");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import pixelmatch from "pixelmatch";
|
|
3
|
+
import { PNG } from "pngjs";
|
|
4
|
+
export function compareSnapshots({ actualPath, baselinePath, diffPath, threshold = 0.1, }) {
|
|
5
|
+
const actual = PNG.sync.read(fs.readFileSync(actualPath));
|
|
6
|
+
const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
|
|
7
|
+
if (actual.width !== baseline.width || actual.height !== baseline.height) {
|
|
8
|
+
throw new Error("Snapshot size mismatch");
|
|
9
|
+
}
|
|
10
|
+
const diff = new PNG({ width: actual.width, height: actual.height });
|
|
11
|
+
const numDiffPixels = pixelmatch(actual.data, baseline.data, diff.data, actual.width, actual.height, { threshold });
|
|
12
|
+
fs.writeFileSync(diffPath, PNG.sync.write(diff));
|
|
13
|
+
return numDiffPixels;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// support/hooks.ts
|
|
2
|
+
import { Before, After, BeforeAll, AfterAll, } from "@cucumber/cucumber";
|
|
3
|
+
import { chromium, devices } from "playwright";
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { compareSnapshots } from "./compareSnapshots";
|
|
8
|
+
dotenv.config();
|
|
9
|
+
const SESSION_FILE = path.resolve("e2e/support/helper/auth", "session.json");
|
|
10
|
+
const SCREENSHOT_DIR = path.resolve("e2e/screenshots");
|
|
11
|
+
const VIDEO_DIR = path.resolve("e2e/videos");
|
|
12
|
+
const SNAPSHOT_BASELINE_DIR = path.resolve("e2e/snapshots/baseline");
|
|
13
|
+
const SNAPSHOT_DIFF_DIR = path.resolve("e2e/snapshots/diff");
|
|
14
|
+
let sharedBrowser;
|
|
15
|
+
BeforeAll(async () => {
|
|
16
|
+
const dirsToClean = [
|
|
17
|
+
VIDEO_DIR,
|
|
18
|
+
SCREENSHOT_DIR,
|
|
19
|
+
SNAPSHOT_DIFF_DIR, // leave baseline snapshots intact
|
|
20
|
+
];
|
|
21
|
+
for (const dir of dirsToClean) {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(dir)) {
|
|
24
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
console.log(`🧹 Cleaned directory: ${dir}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.warn(`⚠️ Failed to clean directory ${dir}:`, err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
sharedBrowser = await chromium.launch({
|
|
33
|
+
headless: process.env.HEADLESS !== "false",
|
|
34
|
+
});
|
|
35
|
+
console.log("🚀 Launched shared browser for all scenarios");
|
|
36
|
+
});
|
|
37
|
+
AfterAll(async () => {
|
|
38
|
+
await (sharedBrowser === null || sharedBrowser === void 0 ? void 0 : sharedBrowser.close());
|
|
39
|
+
console.log("🧹 Closed shared browser after all scenarios");
|
|
40
|
+
});
|
|
41
|
+
Before(async function (scenario) {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
// 🛡️ Ensure browser is still usable
|
|
44
|
+
if (!sharedBrowser || !sharedBrowser.isConnected()) {
|
|
45
|
+
console.warn("⚠️ Shared browser was disconnected. Restarting...");
|
|
46
|
+
sharedBrowser = await chromium.launch({
|
|
47
|
+
headless: process.env.HEADLESS !== "false",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const isMobile = scenario.pickle.tags.some((t) => t.name === "@mobile");
|
|
51
|
+
const isVisualTest = scenario.pickle.tags.some((t) => t.name === "@visual");
|
|
52
|
+
if (isVisualTest)
|
|
53
|
+
process.env.VISUAL_TEST = "true";
|
|
54
|
+
const contextOptions = {
|
|
55
|
+
recordVideo: { dir: VIDEO_DIR },
|
|
56
|
+
...(isMobile ? devices["iPhone 13 Pro"] : {}),
|
|
57
|
+
};
|
|
58
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
59
|
+
contextOptions.storageState = SESSION_FILE;
|
|
60
|
+
(_a = this.log) === null || _a === void 0 ? void 0 : _a.call(this, "✅ Reusing session from saved file.");
|
|
61
|
+
}
|
|
62
|
+
const context = await sharedBrowser.newContext(contextOptions);
|
|
63
|
+
const page = await context.newPage();
|
|
64
|
+
this.browser = sharedBrowser;
|
|
65
|
+
this.context = context;
|
|
66
|
+
this.page = page;
|
|
67
|
+
if (isMobile)
|
|
68
|
+
(_b = this.log) === null || _b === void 0 ? void 0 : _b.call(this, "📱 Mobile emulation enabled (iPhone 13 Pro)");
|
|
69
|
+
});
|
|
70
|
+
After(async function (scenario) {
|
|
71
|
+
var _a, _b, _c;
|
|
72
|
+
const failed = ((_a = scenario.result) === null || _a === void 0 ? void 0 : _a.status) === "FAILED";
|
|
73
|
+
const name = scenario.pickle.name.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
|
|
74
|
+
// 📸 Screenshot on failure
|
|
75
|
+
if (failed && this.page) {
|
|
76
|
+
const screenshotPath = path.join(SCREENSHOT_DIR, `failed-${name}.png`);
|
|
77
|
+
try {
|
|
78
|
+
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
79
|
+
await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
80
|
+
console.log(`🖼️ Screenshot saved: ${screenshotPath}`);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.warn("❌ Failed to save screenshot:", err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 🎥 Handle video recording
|
|
87
|
+
let rawPath;
|
|
88
|
+
try {
|
|
89
|
+
rawPath = await ((_c = (_b = this.page) === null || _b === void 0 ? void 0 : _b.video()) === null || _c === void 0 ? void 0 : _c.path());
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.warn(`⚠️ Unable to access video path: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
if (rawPath) {
|
|
95
|
+
try {
|
|
96
|
+
const finalPath = path.join(VIDEO_DIR, `failed-${name}.webm`);
|
|
97
|
+
fs.mkdirSync(VIDEO_DIR, { recursive: true });
|
|
98
|
+
if (failed) {
|
|
99
|
+
fs.renameSync(rawPath, finalPath);
|
|
100
|
+
console.log(`🎥 Video saved: ${finalPath}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
fs.unlinkSync(rawPath);
|
|
104
|
+
console.log(`🧹 Deleted video for passed test: ${rawPath}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn(`❌ Error handling video file: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 🧪 Visual regression testing
|
|
112
|
+
if (this.page && process.env.VISUAL_TEST === "true") {
|
|
113
|
+
fs.mkdirSync(SNAPSHOT_BASELINE_DIR, { recursive: true });
|
|
114
|
+
fs.mkdirSync(SNAPSHOT_DIFF_DIR, { recursive: true });
|
|
115
|
+
const baselinePath = path.join(SNAPSHOT_BASELINE_DIR, `${name}.png`);
|
|
116
|
+
const actualPath = path.join(SNAPSHOT_DIFF_DIR, `${name}.actual.png`);
|
|
117
|
+
const diffPath = path.join(SNAPSHOT_DIFF_DIR, `${name}.diff.png`);
|
|
118
|
+
await this.page.screenshot({ path: actualPath, fullPage: true });
|
|
119
|
+
if (!fs.existsSync(baselinePath)) {
|
|
120
|
+
fs.copyFileSync(actualPath, baselinePath);
|
|
121
|
+
console.log(`📸 Created baseline image: ${baselinePath}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
try {
|
|
125
|
+
const diffPixels = compareSnapshots({
|
|
126
|
+
actualPath,
|
|
127
|
+
baselinePath,
|
|
128
|
+
diffPath,
|
|
129
|
+
threshold: 0.1,
|
|
130
|
+
});
|
|
131
|
+
console.log(diffPixels > 0
|
|
132
|
+
? `⚠️ Visual diff found (${diffPixels} pixels): ${diffPath}`
|
|
133
|
+
: "✅ No visual changes detected");
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.warn("❌ Error comparing snapshots:", err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
if (this.context && this.page) {
|
|
142
|
+
await this.cleanup(scenario);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.warn("❌ Error during cleanup:", err);
|
|
147
|
+
}
|
|
148
|
+
});
|
package/dist/helpers/world.js
CHANGED
|
@@ -17,7 +17,7 @@ export class CustomWorld extends World {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
async init(testInfo) {
|
|
20
|
-
const isMobile = testInfo
|
|
20
|
+
const isMobile = testInfo === null || testInfo === void 0 ? void 0 : testInfo.pickle.tags.some((tag) => tag.name === "@mobile");
|
|
21
21
|
const device = isMobile ? devices["Pixel 5"] : undefined;
|
|
22
22
|
this.browser = await chromium.launch({ headless: isHeadless, slowMo });
|
|
23
23
|
this.context = await this.browser.newContext({
|
|
@@ -25,32 +25,34 @@ export class CustomWorld extends World {
|
|
|
25
25
|
recordVideo: { dir: "e2e/test-artifacts/videos" },
|
|
26
26
|
});
|
|
27
27
|
this.page = await this.context.newPage();
|
|
28
|
-
this.testName = testInfo
|
|
28
|
+
this.testName = testInfo === null || testInfo === void 0 ? void 0 : testInfo.pickle.name;
|
|
29
29
|
this.log(`🧪 Initialized context${isMobile ? " (mobile)" : ""}`);
|
|
30
30
|
}
|
|
31
31
|
getScope() {
|
|
32
|
-
|
|
32
|
+
var _a;
|
|
33
|
+
return (_a = this.frame) !== null && _a !== void 0 ? _a : this.page;
|
|
33
34
|
}
|
|
34
35
|
exitIframe() {
|
|
35
36
|
this.frame = undefined;
|
|
36
37
|
this.log("⬅️ Exited iframe, scope is now main page");
|
|
37
38
|
}
|
|
38
39
|
async cleanup(testInfo) {
|
|
39
|
-
|
|
40
|
+
var _a, _b, _c, _d;
|
|
41
|
+
const failed = ((_a = testInfo === null || testInfo === void 0 ? void 0 : testInfo.result) === null || _a === void 0 ? void 0 : _a.status) === "FAILED";
|
|
40
42
|
try {
|
|
41
|
-
await this.page
|
|
43
|
+
await ((_b = this.page) === null || _b === void 0 ? void 0 : _b.close());
|
|
42
44
|
}
|
|
43
45
|
catch (err) {
|
|
44
46
|
this.log(`⚠️ Error closing page: ${err.message}`);
|
|
45
47
|
}
|
|
46
48
|
try {
|
|
47
|
-
await this.context
|
|
49
|
+
await ((_c = this.context) === null || _c === void 0 ? void 0 : _c.close());
|
|
48
50
|
}
|
|
49
51
|
catch (err) {
|
|
50
52
|
this.log(`⚠️ Error closing context: ${err.message}`);
|
|
51
53
|
}
|
|
52
54
|
try {
|
|
53
|
-
await this.browser
|
|
55
|
+
await ((_d = this.browser) === null || _d === void 0 ? void 0 : _d.close());
|
|
54
56
|
}
|
|
55
57
|
catch (err) {
|
|
56
58
|
this.log(`⚠️ Error closing browser: ${err.message}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { When } from "@cucumber/cucumber";
|
|
2
|
+
When("I find href in iframe {string} and store as {string}", async function (iframeSelector, key) {
|
|
3
|
+
const iframe = this.page.frameLocator(iframeSelector);
|
|
4
|
+
const link = await iframe.locator("a[href]").first();
|
|
5
|
+
const href = await link.getAttribute("href");
|
|
6
|
+
if (!href)
|
|
7
|
+
throw new Error("No link found in iframe.");
|
|
8
|
+
this.data[key] = href;
|
|
9
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export
|
|
1
|
+
export * from "./actions/cookieSteps";
|
|
2
|
+
export * from "./actions/miscSteps";
|
|
3
|
+
export * from "./actions/cookieSteps";
|
|
4
|
+
export * from "./actions/clickSteps";
|
|
5
|
+
export * from "./actions/debugSteps";
|
|
6
|
+
export * from "./actions/elementFindSteps";
|
|
7
|
+
export * from "./actions/inputSteps";
|
|
8
|
+
export * from "./actions/mouseSteps";
|
|
9
|
+
export * from "./actions/scrollSteps";
|
|
10
|
+
export * from "./actions/storageSteps";
|
|
11
|
+
export * from "./assertions/InterceptionRequests";
|
|
12
|
+
export * from "./assertions/button_and_text_visibility";
|
|
13
|
+
export * from "./assertions/cookieSteps";
|
|
14
|
+
export * from "./assertions/elementSteps";
|
|
15
|
+
export * from "./assertions/formInputSteps";
|
|
16
|
+
export * from "./assertions/locationSteps";
|
|
17
|
+
export * from "./assertions/roleTestIdSteps";
|
|
18
|
+
export * from "./assertions/semanticSteps";
|
|
19
|
+
export * from "./assertions/storageSteps";
|
|
20
|
+
export * from "./assertions/InterceptionRequests";
|
|
21
|
+
export * from "./custom_setups/loginHooks";
|
|
22
|
+
export * from "./custom_setups/global-login";
|
|
23
|
+
export * from "./iframes/frames";
|
|
24
|
+
export * from "./helpers/utils";
|
|
25
|
+
export * from "./helpers/world";
|
|
26
|
+
export * from "./helpers/hooks";
|
|
27
|
+
export * from "./helpers/compareSnapshots";
|
|
28
|
+
export type { CustomWorld } from "./helpers/world";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
//
|
|
1
|
+
//src/index.ts
|
|
2
|
+
export * from "./actions/cookieSteps";
|
|
3
|
+
export * from "./actions/miscSteps";
|
|
4
|
+
export * from "./actions/cookieSteps";
|
|
5
|
+
export * from "./actions/clickSteps";
|
|
6
|
+
export * from "./actions/debugSteps";
|
|
7
|
+
export * from "./actions/elementFindSteps";
|
|
8
|
+
export * from "./actions/inputSteps";
|
|
9
|
+
export * from "./actions/mouseSteps";
|
|
10
|
+
export * from "./actions/scrollSteps";
|
|
11
|
+
export * from "./actions/storageSteps";
|
|
12
|
+
export * from "./assertions/InterceptionRequests";
|
|
13
|
+
export * from "./assertions/button_and_text_visibility";
|
|
14
|
+
export * from "./assertions/cookieSteps";
|
|
15
|
+
export * from "./assertions/elementSteps";
|
|
16
|
+
export * from "./assertions/formInputSteps";
|
|
17
|
+
export * from "./assertions/locationSteps";
|
|
18
|
+
export * from "./assertions/roleTestIdSteps";
|
|
19
|
+
export * from "./assertions/semanticSteps";
|
|
20
|
+
export * from "./assertions/storageSteps";
|
|
21
|
+
export * from "./assertions/InterceptionRequests";
|
|
22
|
+
export * from "./custom_setups/loginHooks";
|
|
23
|
+
export * from "./custom_setups/global-login";
|
|
24
|
+
export * from "./iframes/frames";
|
|
2
25
|
// Export core utilities and helpers
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
26
|
+
export * from "./helpers/utils";
|
|
27
|
+
export * from "./helpers/world";
|
|
28
|
+
export * from "./helpers/hooks";
|
|
29
|
+
export * from "./helpers/compareSnapshots";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./index";
|
package/dist/register.js
ADDED
package/package.json
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright-cucumber-ts-steps",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "A collection of reusable Playwright step definitions for Cucumber in TypeScript, designed to streamline end-to-end testing across web, API, and mobile applications.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
6
|
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc"
|
|
10
10
|
},
|
|
11
|
-
"exports": {
|
|
12
|
-
".": {
|
|
13
|
-
"import": "./dist/index.js",
|
|
14
|
-
"require": "./dist/index.js"
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
11
|
"repository": {
|
|
18
12
|
"type": "git",
|
|
19
13
|
"url": "git+https://github.com/qaPaschalE/playwright-cucumber-ts-steps.git"
|
package/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// index.ts
|
|
2
|
-
|
|
3
|
-
// Export core utilities and helpers
|
|
4
|
-
export * from "./src/helpers/world";
|
|
5
|
-
export * from "./src/helpers/utils";
|
|
6
|
-
export * from "./src/helpers/hooks";
|
|
7
|
-
export * from "./src/helpers/compareSnapshots";
|
|
8
|
-
|
|
9
|
-
// Optionally expose types
|
|
10
|
-
export type { CustomWorld } from "./src/helpers/world";
|
package/register.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// src/register.ts
|
|
2
|
-
import "./src/helpers/world.js";
|
|
3
|
-
|
|
4
|
-
// Custom setups
|
|
5
|
-
import "./custom_setups/global-login.js";
|
|
6
|
-
import "./custom_setups/loginHooks.js";
|
|
7
|
-
|
|
8
|
-
// Actions
|
|
9
|
-
import "./actions/Interception & Requests.js";
|
|
10
|
-
import "./actions/clickSteps.js";
|
|
11
|
-
import "./actions/cookieSteps.js";
|
|
12
|
-
import "./actions/debugSteps.js";
|
|
13
|
-
import "./actions/elementFindSteps.js";
|
|
14
|
-
import "./actions/inputSteps.js";
|
|
15
|
-
import "./actions/mouseSteps.js";
|
|
16
|
-
import "./actions/miscSteps.js";
|
|
17
|
-
import "./actions/scrollSteps.js";
|
|
18
|
-
import "./actions/storageSteps.js";
|
|
19
|
-
|
|
20
|
-
// Assertions
|
|
21
|
-
import "./assertions/button_and_text_visibility.js";
|
|
22
|
-
import "./assertions/cookieSteps.js";
|
|
23
|
-
import "./assertions/elementSteps.js";
|
|
24
|
-
import "./assertions/formInputSteps.js";
|
|
25
|
-
import "./assertions/locationSteps.js";
|
|
26
|
-
import "./assertions/roleTestIdSteps.js";
|
|
27
|
-
import "./assertions/semanticSteps.js";
|
|
28
|
-
import "./assertions/storageSteps.js";
|
|
29
|
-
|
|
30
|
-
// Iframes
|
|
31
|
-
import "./iframes/frames.js";
|