playwright-cucumber-ts-steps
Version:
A collection of reusable Playwright step definitions for Cucumber in TypeScript, designed to streamline end-to-end testing across web, API, and mobile applications.
210 lines (209 loc) • 11.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Then_I_should_see_page_matches_snapshot = Then_I_should_see_page_matches_snapshot;
exports.Then_I_capture_element_snapshot_as_alias = Then_I_capture_element_snapshot_as_alias;
exports.Then_the_snapshot_should_match_baseline = Then_the_snapshot_should_match_baseline;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const cucumber_1 = require("@cucumber/cucumber");
const test_1 = require("@playwright/test");
const pixelmatch_1 = __importDefault(require("pixelmatch")); // Ensure pixelmatch is installed: npm install pixelmatch
const pngjs_1 = require("pngjs"); // Ensure pngjs is installed: npm install pngjs
// --- Configuration for Snapshot Directories ---
// It's good practice to make these configurable (e.g., via world.config or env variables)
// For now, keeping them as resolved constants as per your original code.
const SNAPSHOTS_BASE_DIR = path_1.default.resolve("e2e/snapshots"); // Base directory for all snapshots
const BASELINE_DIR = path_1.default.join(SNAPSHOTS_BASE_DIR, "baseline");
const CURRENT_DIR = path_1.default.join(SNAPSHOTS_BASE_DIR, "current");
const DIFF_DIR = path_1.default.join(SNAPSHOTS_BASE_DIR, "diff");
// Helper function to generate standardized snapshot paths
function getSnapshotPaths(name) {
// Sanitize the name for use in filenames
const safeName = name.replace(/[^a-z0-9]/gi, "_").toLowerCase();
return {
baseline: path_1.default.join(BASELINE_DIR, `${safeName}.png`),
current: path_1.default.join(CURRENT_DIR, `${safeName}.png`),
diff: path_1.default.join(DIFF_DIR, `${safeName}.diff.png`),
};
}
// Helper to ensure all necessary directories exist
function ensureSnapshotDirs() {
fs_1.default.mkdirSync(BASELINE_DIR, { recursive: true });
fs_1.default.mkdirSync(CURRENT_DIR, { recursive: true });
fs_1.default.mkdirSync(DIFF_DIR, { recursive: true });
}
// ===================================================================================
// VISUAL REGRESSION ASSERTIONS: PAGE SNAPSHOTS
// ===================================================================================
/**
* Asserts that the current page's visual appearance matches a named baseline snapshot.
* If no baseline exists for the given name, a new one is created from the current page.
* Differences between current and baseline snapshots are highlighted in a 'diff' image.
*
* ```gherkin
* Then I should see the page matches the snapshot {string}
* ```
*
* @param name - A unique name for the snapshot (e.g., "homepage", "product-details-page").
*
* @example
* Then I should see the page matches the snapshot "homepage"
*
* @remarks
* This is a core step for visual regression testing.
* 1. Takes a screenshot of the current page and saves it to the `current` directory.
* 2. If a `baseline` snapshot does not exist, the `current` snapshot is copied to `baseline`,
* and the test passes (a new baseline is established).
* 3. If a `baseline` exists, it compares the `current` and `baseline` snapshots pixel by pixel
* using `pixelmatch`.
* 4. If a mismatch is detected (more than 0 differing pixels based on `threshold`), a `diff`
* image is generated, and the test fails.
*
* All snapshots (baseline, current, diff) are stored in `e2e/snapshots/`.
* Adjust `threshold` for sensitivity (0.1 means 10% difference in pixel color is allowed).
* @category Visual Regression Steps
*/
async function Then_I_should_see_page_matches_snapshot(name) {
const { page } = this;
const paths = getSnapshotPaths(name);
ensureSnapshotDirs(); // Ensure directories exist before taking screenshot
// Take current screenshot
await page.screenshot({ path: paths.current, fullPage: true });
this.log?.(`📸 Captured current snapshot: "${paths.current}".`);
if (!fs_1.default.existsSync(paths.baseline)) {
// If no baseline exists, create one from the current screenshot
fs_1.default.copyFileSync(paths.current, paths.baseline);
this.log?.(`✨ Created new baseline snapshot: "${paths.baseline}".`);
return; // Pass the test if a new baseline was created
}
// Load baseline and current images for comparison
const baselineImg = pngjs_1.PNG.sync.read(fs_1.default.readFileSync(paths.baseline));
const currentImg = pngjs_1.PNG.sync.read(fs_1.default.readFileSync(paths.current));
// Ensure images have the same dimensions for comparison
if (baselineImg.width !== currentImg.width || baselineImg.height !== currentImg.height) {
fs_1.default.writeFileSync(paths.diff, pngjs_1.PNG.sync.write(new pngjs_1.PNG({
width: Math.max(baselineImg.width, currentImg.width),
height: Math.max(baselineImg.height, currentImg.height),
})));
throw new Error(`Visual snapshot mismatch for "${name}": Dimensions differ! ` +
`Baseline: ${baselineImg.width}x${baselineImg.height}, Current: ${currentImg.width}x${currentImg.height}. ` +
`Diff image generated at "${paths.diff}".`);
}
const { width, height } = baselineImg;
const diffImg = new pngjs_1.PNG({ width, height });
// Compare images pixel by pixel
const pixelDiff = (0, pixelmatch_1.default)(baselineImg.data, currentImg.data, diffImg.data, width, height, { threshold: 0.1 } // Adjust threshold for sensitivity (0.1 means 10% difference in pixel color is allowed)
);
if (pixelDiff > 0) {
// If differences found, write the diff image
fs_1.default.writeFileSync(paths.diff, pngjs_1.PNG.sync.write(diffImg));
this.log?.(`❌ Visual mismatch detected for "${name}". ${pixelDiff} pixels differ. Diff image: "${paths.diff}".`);
}
// Assert that no pixels differ
(0, test_1.expect)(pixelDiff, `Visual snapshot "${name}" mismatch: ${pixelDiff} pixels differ.`).toBe(0);
this.log?.(`✅ Visual snapshot "${name}" matches baseline (0 pixels differ).`);
}
(0, cucumber_1.Then)("I should see the page matches the snapshot {string}", Then_I_should_see_page_matches_snapshot);
// ===================================================================================
// VISUAL REGRESSION: ELEMENT SNAPSHOT CAPTURE & MATCH
// ===================================================================================
/**
* Captures a visual snapshot of a specific element identified by its selector and saves it under a given alias.
* This snapshot is saved to the `current` directory and can later be compared against a baseline.
*
* ```gherkin
* Then I capture a snapshot of the element {string} as {string}
* ```
*
* @param selector - The CSS selector of the element to capture.
* @param alias - A unique alias name for this element snapshot (e.g., "logo-image", "product-card").
*
* @example
* Then I capture a snapshot of the element ".header .logo" as "logo-snapshot"
* Then I capture a snapshot of the element "#user-profile" as "user-profile-widget"
*
* @remarks
* This step is typically followed by {@link Then_the_snapshot_should_match_baseline | "Then the snapshot {string} should match baseline"}
* to perform the actual visual comparison.
* @category Visual Regression Steps
*/
async function Then_I_capture_element_snapshot_as_alias(selector, alias) {
const elementLocator = this.getScope().locator(selector);
ensureSnapshotDirs(); // Ensure directories exist
const pathCurrent = path_1.default.join(CURRENT_DIR, `${alias}.png`);
await elementLocator.screenshot({ path: pathCurrent });
this.log?.(`📸 Captured snapshot of element "${selector}" saved as "${alias}".`);
}
(0, cucumber_1.Then)("I capture a snapshot of the element {string} as {string}", Then_I_capture_element_snapshot_as_alias);
/**
* Asserts that a previously captured named snapshot (of an element) matches its baseline.
* If no baseline exists, a new one is created from the current snapshot.
*
* ```gherkin
* Then The snapshot {string} should match baseline
* ```
*
* @param alias - The unique alias name of the snapshot (as used in "Then I capture a snapshot...").
*
* @example
* Then I capture a snapshot of the element ".logo" as "logo-snapshot"
* Then The snapshot "logo-snapshot" should match baseline
*
* @remarks
* This step is designed to be used after a step like
* {@link Then_I_capture_element_snapshot_as_alias | "Then I capture a snapshot of the element {string} as {string}"}.
* It performs the same comparison logic as `Then I should see the page matches the snapshot`,
* but specifically for an element snapshot.
* All snapshots (baseline, current, diff) are stored in `e2e/snapshots/`.
* @category Visual Regression Steps
*/
async function Then_the_snapshot_should_match_baseline(alias) {
const paths = getSnapshotPaths(alias); // Get paths for baseline, current, diff based on alias
ensureSnapshotDirs(); // Ensure directories exist
// Check if the current snapshot file actually exists.
// This is crucial because `Then I capture a snapshot` must have been run first.
if (!fs_1.default.existsSync(paths.current)) {
throw new Error(`Current snapshot file for alias "${alias}" not found at "${paths.current}".` +
`Ensure "Then I capture a snapshot of the element {string} as {string}" was run successfully before this step.`);
}
// Load current image
const currentImg = pngjs_1.PNG.sync.read(fs_1.default.readFileSync(paths.current));
let baselineImg = null;
if (fs_1.default.existsSync(paths.baseline)) {
// Load baseline image if it exists
baselineImg = pngjs_1.PNG.sync.read(fs_1.default.readFileSync(paths.baseline));
}
if (!baselineImg) {
// If no baseline exists, create one from the current snapshot
fs_1.default.copyFileSync(paths.current, paths.baseline);
this.log?.(`✨ Created new baseline for snapshot "${alias}": "${paths.baseline}".`);
return; // Pass the test if a new baseline was created
}
// Ensure images have the same dimensions for comparison
if (baselineImg.width !== currentImg.width || baselineImg.height !== currentImg.height) {
fs_1.default.writeFileSync(paths.diff, pngjs_1.PNG.sync.write(new pngjs_1.PNG({
width: Math.max(baselineImg.width, currentImg.width),
height: Math.max(baselineImg.height, currentImg.height),
})));
throw new Error(`Visual element snapshot mismatch for "${alias}": Dimensions differ! ` +
`Baseline: ${baselineImg.width}x${baselineImg.height}, Current: ${currentImg.width}x${currentImg.height}. ` +
`Diff image generated at "${paths.diff}".`);
}
const { width, height } = baselineImg;
const diffImg = new pngjs_1.PNG({ width, height });
// Compare images pixel by pixel
const pixelDiff = (0, pixelmatch_1.default)(baselineImg.data, currentImg.data, diffImg.data, width, height, { threshold: 0.1 } // Consistent threshold
);
if (pixelDiff > 0) {
// If differences found, write the diff image
fs_1.default.writeFileSync(paths.diff, pngjs_1.PNG.sync.write(diffImg));
this.log?.(`❌ Visual mismatch detected for snapshot "${alias}". ${pixelDiff} pixels differ. Diff image: "${paths.diff}".`);
}
// Assert that no pixels differ
(0, test_1.expect)(pixelDiff, `Visual element snapshot "${alias}" mismatch: ${pixelDiff} pixels differ.`).toBe(0);
this.log?.(`✅ Visual element snapshot "${alias}" matches baseline (0 pixels differ).`);
}
(0, cucumber_1.Then)("The snapshot {string} should match baseline", Then_the_snapshot_should_match_baseline);