playwright-cucumber-ts-steps 0.1.5 โ†’ 0.1.7

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.
Files changed (35) hide show
  1. package/package.json +6 -6
  2. package/src/actions/clickSteps.ts +207 -0
  3. package/src/actions/cookieSteps.ts +29 -0
  4. package/src/actions/debugSteps.ts +7 -0
  5. package/src/actions/elementFindSteps.ts +256 -0
  6. package/src/actions/fillFormSteps.ts +213 -0
  7. package/src/actions/inputSteps.ts +118 -0
  8. package/src/actions/interceptionSteps.ts +87 -0
  9. package/src/actions/miscSteps.ts +414 -0
  10. package/src/actions/mouseSteps.ts +99 -0
  11. package/src/actions/scrollSteps.ts +24 -0
  12. package/src/actions/storageSteps.ts +83 -0
  13. package/src/assertions/buttonAndTextVisibilitySteps.ts +178 -0
  14. package/src/assertions/cookieSteps.ts +52 -0
  15. package/src/assertions/elementSteps.ts +103 -0
  16. package/src/assertions/formInputSteps.ts +110 -0
  17. package/src/assertions/interceptionRequestsSteps.ts +216 -0
  18. package/src/assertions/locationSteps.ts +99 -0
  19. package/src/assertions/roleTestIdSteps.ts +36 -0
  20. package/src/assertions/semanticSteps.ts +79 -0
  21. package/src/assertions/storageSteps.ts +89 -0
  22. package/src/assertions/visualSteps.ts +98 -0
  23. package/src/custom_setups/loginHooks.ts +135 -0
  24. package/src/helpers/checkPeerDeps.ts +19 -0
  25. package/src/helpers/compareSnapshots.ts +35 -0
  26. package/src/helpers/hooks.ts +212 -0
  27. package/src/helpers/utils/fakerUtils.ts +64 -0
  28. package/src/helpers/utils/index.ts +4 -0
  29. package/src/helpers/utils/optionsUtils.ts +104 -0
  30. package/src/helpers/utils/resolveUtils.ts +74 -0
  31. package/src/helpers/utils/sessionUtils.ts +36 -0
  32. package/src/helpers/world.ts +93 -0
  33. package/src/iframes/frames.ts +15 -0
  34. package/src/index.ts +39 -0
  35. package/src/register.ts +4 -0
@@ -0,0 +1,99 @@
1
+ import { Then, DataTable } from "@cucumber/cucumber";
2
+ import { expect } from "@playwright/test";
3
+ import { CustomWorld } from "../helpers/world";
4
+ //
5
+ // ๐ŸŒ URL
6
+ //
7
+
8
+ Then("I see URL {string}", async function (this: CustomWorld, expected: string) {
9
+ const url = this.page.url();
10
+ expect(url).toBe(expected);
11
+ });
12
+
13
+ Then("I do not see URL {string}", async function (this: CustomWorld, notExpected: string) {
14
+ const url = this.page.url();
15
+ if (url === notExpected) throw new Error(`Expected not to be on URL "${notExpected}"`);
16
+ });
17
+
18
+ Then("I see URL contains {string}", async function (this: CustomWorld, expected: string) {
19
+ const url = this.page.url();
20
+ expect(url).toContain(expected);
21
+ });
22
+
23
+ Then("I do not see URL contains {string}", async function (this: CustomWorld, notExpected: string) {
24
+ const url = this.page.url();
25
+ if (url.includes(notExpected)) {
26
+ throw new Error(`URL should not contain "${notExpected}", but got "${url}"`);
27
+ }
28
+ });
29
+
30
+ //
31
+ // ๐Ÿ“ LOCATION PARTS
32
+ //
33
+
34
+ Then("I see location {string}", async function (this: CustomWorld, expected: string) {
35
+ const location = await this.page.evaluate(() => window.location.href);
36
+ if (location !== expected) {
37
+ throw new Error(`Expected location to be "${expected}", but got "${location}"`);
38
+ }
39
+ });
40
+
41
+ Then("I see pathname {string}", async function (this: CustomWorld, expected: string) {
42
+ const pathname = await this.page.evaluate(() => window.location.pathname);
43
+ if (pathname !== expected) {
44
+ throw new Error(`Expected pathname "${expected}", but got "${pathname}"`);
45
+ }
46
+ });
47
+
48
+ Then("I see pathname contains {string}", async function (this: CustomWorld, part: string) {
49
+ const pathname = await this.page.evaluate(() => window.location.pathname);
50
+ if (!pathname.includes(part)) {
51
+ throw new Error(`Pathname does not contain "${part}". Got: "${pathname}"`);
52
+ }
53
+ });
54
+
55
+ Then("I see hash {string}", async function (this: CustomWorld, expected: string) {
56
+ const hash = await this.page.evaluate(() => window.location.hash);
57
+ if (hash !== expected) {
58
+ throw new Error(`Expected hash "${expected}", but got "${hash}"`);
59
+ }
60
+ });
61
+
62
+ Then("I see hash contains {string}", async function (this: CustomWorld, part: string) {
63
+ const hash = await this.page.evaluate(() => window.location.hash);
64
+ if (!hash.includes(part)) {
65
+ throw new Error(`Expected hash to contain "${part}", but got "${hash}"`);
66
+ }
67
+ });
68
+
69
+ Then("I see search {string}", async function (this: CustomWorld, expected: string) {
70
+ const search = await this.page.evaluate(() => window.location.search);
71
+ if (search !== expected) {
72
+ throw new Error(`Expected search to be "${expected}", but got "${search}"`);
73
+ }
74
+ });
75
+
76
+ Then("I see search contains {string}", async function (this: CustomWorld, part: string) {
77
+ const search = await this.page.evaluate(() => window.location.search);
78
+ if (!search.includes(part)) {
79
+ throw new Error(`Search does not contain "${part}". Got: "${search}"`);
80
+ }
81
+ });
82
+
83
+ Then("I see location", async function (table: DataTable) {
84
+ const location = await this.page.evaluate(() => ({
85
+ href: window.location.href,
86
+ origin: window.location.origin,
87
+ protocol: window.location.protocol,
88
+ host: window.location.host,
89
+ hostname: window.location.hostname,
90
+ port: window.location.port,
91
+ pathname: window.location.pathname,
92
+ search: window.location.search,
93
+ hash: window.location.hash,
94
+ }));
95
+
96
+ for (const [key, expected] of table.rows()) {
97
+ expect(location[key as keyof typeof location]).toBe(expected);
98
+ }
99
+ });
@@ -0,0 +1,36 @@
1
+ import { Then } from "@cucumber/cucumber";
2
+ import { expect } from "@playwright/test";
3
+
4
+ //
5
+ // ๐Ÿงฉ ROLE-BASED ELEMENTS
6
+ //
7
+
8
+ Then(
9
+ /^I see role "(.*)" with name "(.*)"$/,
10
+ async function (this: any, role: string, name: string) {
11
+ const el = this.page.getByRole(role, { name, exact: true });
12
+ await expect(el).toBeVisible();
13
+ }
14
+ );
15
+
16
+ Then(
17
+ /^I do not see role "(.*)" with name "(.*)"$/,
18
+ async function (this: any, role: string, name: string) {
19
+ const el = this.page.getByRole(role, { name, exact: true });
20
+ await expect(el).toHaveCount(0);
21
+ }
22
+ );
23
+
24
+ //
25
+ // ๐Ÿท๏ธ TEST ID-BASED ELEMENTS
26
+ //
27
+
28
+ Then(/^I see testid "(.*)"$/, async function (this: any, testId: string) {
29
+ const el = this.page.getByTestId(testId);
30
+ await expect(el).toBeVisible();
31
+ });
32
+
33
+ Then(/^I do not see testid "(.*)"$/, async function (this: any, testId: string) {
34
+ const el = this.page.getByTestId(testId);
35
+ await expect(el).toHaveCount(0);
36
+ });
@@ -0,0 +1,79 @@
1
+ import { Then, DataTable } from "@cucumber/cucumber";
2
+ import { expect } from "@playwright/test";
3
+ import { parseExpectOptions } from "../helpers/utils/optionsUtils";
4
+ import type { CustomWorld } from "../helpers/world";
5
+ //
6
+ // ๐Ÿง  HEADINGS
7
+ //
8
+
9
+ Then("I see heading {string}", async function (this: CustomWorld, text: string) {
10
+ const heading = await this.page.locator("h1, h2, h3, h4, h5, h6", { hasText: text }).first();
11
+ if (!(await heading.isVisible())) {
12
+ throw new Error(`Heading "${text}" not found or not visible`);
13
+ }
14
+ });
15
+
16
+ Then("I do not see heading {string}", async function (this: CustomWorld, text: string) {
17
+ const heading = this.page.locator("h1, h2, h3, h4, h5, h6", {
18
+ hasText: text,
19
+ });
20
+ if ((await heading.count()) > 0) {
21
+ const visible = await heading.first().isVisible();
22
+ if (visible) throw new Error(`Heading "${text}" is visible but should not be`);
23
+ }
24
+ });
25
+
26
+ //
27
+ // ๐Ÿท๏ธ LABELS
28
+ //
29
+
30
+ Then("I see label {string}", async function (this: CustomWorld, text: string) {
31
+ const label = this.page.getByLabel(text);
32
+ if (!(await label.isVisible())) {
33
+ throw new Error(`Label "${text}" not visible`);
34
+ }
35
+ });
36
+
37
+ Then("I do not see label {string}", async function (this: CustomWorld, text: string) {
38
+ const label = this.page.getByLabel(text);
39
+ if ((await label.count()) > 0 && (await label.first().isVisible())) {
40
+ throw new Error(`Label "${text}" is visible but should not be`);
41
+ }
42
+ });
43
+
44
+ //
45
+ // ๐Ÿ”— LINKS
46
+ //
47
+ Then("I see link {string}", async function (this: CustomWorld, text: string) {
48
+ const link = this.page.getByRole("link", { name: text });
49
+ if (!(await link.isVisible())) {
50
+ throw new Error(`Link "${text}" not visible`);
51
+ }
52
+ });
53
+
54
+ Then("I do not see link {string}", async function (this: CustomWorld, text: string) {
55
+ const link = this.page.getByRole("link", { name: text });
56
+ if ((await link.count()) > 0 && (await link.first().isVisible())) {
57
+ throw new Error(`Link "${text}" is visible but should not be`);
58
+ }
59
+ });
60
+ Then("I count {int} element", async function (count: number) {
61
+ const locator = this.currentLocator ?? this.page.locator("*");
62
+ await expect(locator).toHaveCount(count);
63
+ });
64
+ //document title assertions
65
+ Then(
66
+ "I see document title {string}",
67
+ async function (this: CustomWorld, expected: string, table?: DataTable) {
68
+ const options = parseExpectOptions(table);
69
+ await expect(this.page).toHaveTitle(expected, options);
70
+ }
71
+ );
72
+
73
+ Then(
74
+ "I see document title contains {string}",
75
+ async function (this: CustomWorld, substring: string, table?: DataTable) {
76
+ const options = parseExpectOptions(table);
77
+ await expect(this.page).toHaveTitle(new RegExp(substring, "i"), options);
78
+ }
79
+ );
@@ -0,0 +1,89 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { When, Then } from "@cucumber/cucumber";
4
+ import type { CustomWorld } from "../helpers/world";
5
+ //
6
+ // ๐Ÿ—ƒ LOCAL STORAGE
7
+ //
8
+
9
+ Then("I see local storage item {string}", async function (this: CustomWorld, key: string) {
10
+ const value = await this.page.evaluate((k) => localStorage.getItem(k), key);
11
+ if (value === null) throw new Error(`Local storage item "${key}" not found`);
12
+ });
13
+
14
+ Then("I do not see local storage item {string}", async function (this: CustomWorld, key: string) {
15
+ const value = await this.page.evaluate((k) => localStorage.getItem(k), key);
16
+ if (value !== null)
17
+ throw new Error(`Expected localStorage["${key}"] to be null, but got "${value}"`);
18
+ });
19
+
20
+ Then(
21
+ "I see local storage item {string} equals {string}",
22
+ async function (this: CustomWorld, key: string, expected: string) {
23
+ const actual = await this.page.evaluate((k) => localStorage.getItem(k), key);
24
+ if (actual !== expected) {
25
+ throw new Error(`Expected localStorage["${key}"] to be "${expected}", but got "${actual}"`);
26
+ }
27
+ }
28
+ );
29
+
30
+ Then(
31
+ "I see local storage item {string} contains {string}",
32
+ async function (this: CustomWorld, key: string, part: string) {
33
+ const value = await this.page.evaluate((k) => localStorage.getItem(k), key);
34
+ if (!value || !value.includes(part)) {
35
+ throw new Error(`localStorage["${key}"] does not contain "${part}". Got: "${value}"`);
36
+ }
37
+ }
38
+ );
39
+
40
+ //
41
+ // ๐Ÿ—‚ SESSION STORAGE
42
+ //
43
+
44
+ Then("I see session storage item {string}", async function (this: CustomWorld, key: string) {
45
+ const value = await this.page.evaluate((k) => sessionStorage.getItem(k), key);
46
+ if (value === null) throw new Error(`Session storage item "${key}" not found`);
47
+ });
48
+
49
+ Then("I do not see session storage item {string}", async function (this: CustomWorld, key: string) {
50
+ const value = await this.page.evaluate((k) => sessionStorage.getItem(k), key);
51
+ if (value !== null)
52
+ throw new Error(`Expected sessionStorage["${key}"] to be null, but got "${value}"`);
53
+ });
54
+ When("I clear all saved session files", async function (this: CustomWorld) {
55
+ const authDir = path.resolve("e2e/support/helper/auth");
56
+
57
+ if (fs.existsSync(authDir)) {
58
+ const files = fs.readdirSync(authDir);
59
+
60
+ for (const file of files) {
61
+ const filePath = path.join(authDir, file);
62
+ if (fs.lstatSync(filePath).isFile()) {
63
+ fs.unlinkSync(filePath);
64
+ this.log?.(`๐Ÿงน Deleted session file: ${file}`);
65
+ }
66
+ }
67
+ } else {
68
+ this.log?.(`โš ๏ธ Auth directory not found at ${authDir}`);
69
+ }
70
+ });
71
+ Then(
72
+ "I see session storage item {string} equals {string}",
73
+ async function (this: CustomWorld, key: string, expected: string) {
74
+ const actual = await this.page.evaluate((k) => sessionStorage.getItem(k), key);
75
+ if (actual !== expected) {
76
+ throw new Error(`Expected sessionStorage["${key}"] to be "${expected}", but got "${actual}"`);
77
+ }
78
+ }
79
+ );
80
+
81
+ Then(
82
+ "I see session storage item {string} contains {string}",
83
+ async function (this: CustomWorld, key: string, part: string) {
84
+ const value = await this.page.evaluate((k) => sessionStorage.getItem(k), key);
85
+ if (!value || !value.includes(part)) {
86
+ throw new Error(`sessionStorage["${key}"] does not contain "${part}". Got: "${value}"`);
87
+ }
88
+ }
89
+ );
@@ -0,0 +1,98 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Then } from "@cucumber/cucumber";
4
+ import { expect } from "@playwright/test";
5
+ import pixelmatch from "pixelmatch";
6
+ import { PNG } from "pngjs";
7
+ import type { CustomWorld } from "../helpers/world";
8
+
9
+ const BASELINE_DIR = path.resolve("e2e/snapshots/baseline");
10
+ const CURRENT_DIR = path.resolve("e2e/snapshots/current");
11
+ const DIFF_DIR = path.resolve("e2e/snapshots/diff");
12
+
13
+ function getSnapshotPaths(name: string) {
14
+ const safeName = name.replace(/[^a-z0-9]/gi, "_").toLowerCase();
15
+ return {
16
+ baseline: path.join(BASELINE_DIR, `${safeName}.png`),
17
+ current: path.join(CURRENT_DIR, `${safeName}.png`),
18
+ diff: path.join(DIFF_DIR, `${safeName}.diff.png`),
19
+ };
20
+ }
21
+
22
+ Then(
23
+ "I should see the page matches the snapshot {string}",
24
+ async function (this: CustomWorld, name: string) {
25
+ const { page } = this;
26
+ const paths = getSnapshotPaths(name);
27
+
28
+ fs.mkdirSync(BASELINE_DIR, { recursive: true });
29
+ fs.mkdirSync(CURRENT_DIR, { recursive: true });
30
+ fs.mkdirSync(DIFF_DIR, { recursive: true });
31
+
32
+ await page.screenshot({ path: paths.current, fullPage: true });
33
+
34
+ if (!fs.existsSync(paths.baseline)) {
35
+ fs.copyFileSync(paths.current, paths.baseline);
36
+ this.log?.(`๐Ÿ“ธ Created baseline snapshot: ${paths.baseline}`);
37
+ return;
38
+ }
39
+
40
+ const baseline = PNG.sync.read(fs.readFileSync(paths.baseline));
41
+ const current = PNG.sync.read(fs.readFileSync(paths.current));
42
+ const { width, height } = baseline;
43
+
44
+ const diff = new PNG({ width, height });
45
+ const pixelDiff = pixelmatch(baseline.data, current.data, diff.data, width, height, {
46
+ threshold: 0.1,
47
+ });
48
+
49
+ if (pixelDiff > 0) {
50
+ fs.writeFileSync(paths.diff, PNG.sync.write(diff));
51
+ this.log?.(`โŒ Visual mismatch detected, diff: ${paths.diff}`);
52
+ }
53
+
54
+ expect(pixelDiff, "Pixels that differ").toBe(0);
55
+ }
56
+ );
57
+
58
+ Then(
59
+ "I capture a snapshot of the element {string} as {string}",
60
+ async function (this: CustomWorld, selector: string, alias: string) {
61
+ const element = this.getScope().locator(selector);
62
+ const pathCurrent = path.join(CURRENT_DIR, `${alias}.png`);
63
+ fs.mkdirSync(CURRENT_DIR, { recursive: true });
64
+ await element.screenshot({ path: pathCurrent });
65
+ this.log?.(`๐Ÿ“ธ Snapshot for ${selector} saved as ${alias}`);
66
+ }
67
+ );
68
+
69
+ Then(
70
+ "The snapshot {string} should match baseline",
71
+ async function (this: CustomWorld, alias: string) {
72
+ const paths = getSnapshotPaths(alias);
73
+ const current = PNG.sync.read(fs.readFileSync(paths.current));
74
+ const baseline = fs.existsSync(paths.baseline)
75
+ ? PNG.sync.read(fs.readFileSync(paths.baseline))
76
+ : null;
77
+
78
+ if (!baseline) {
79
+ fs.copyFileSync(paths.current, paths.baseline);
80
+ this.log?.(`๐Ÿ“ธ Created new baseline for ${alias}`);
81
+ return;
82
+ }
83
+
84
+ const { width, height } = baseline;
85
+ const diff = new PNG({ width, height });
86
+
87
+ const pixelDiff = pixelmatch(baseline.data, current.data, diff.data, width, height, {
88
+ threshold: 0.1,
89
+ });
90
+
91
+ if (pixelDiff > 0) {
92
+ fs.writeFileSync(paths.diff, PNG.sync.write(diff));
93
+ this.log?.(`โš ๏ธ Snapshot mismatch: ${alias}`);
94
+ }
95
+
96
+ expect(pixelDiff).toBe(0);
97
+ }
98
+ );
@@ -0,0 +1,135 @@
1
+ import fs from "fs";
2
+ import { When } from "@cucumber/cucumber";
3
+ import { resolveSessionPath } from "../helpers/utils/resolveUtils";
4
+ import { CustomWorld } from "../helpers/world";
5
+
6
+ // Step 1: Check and load existing session if valid
7
+ When(
8
+ "I login with a session data {string}",
9
+ async function (this: CustomWorld, sessionName: string) {
10
+ const sessionPath = resolveSessionPath(this, sessionName);
11
+ this.data.sessionFile = sessionPath;
12
+
13
+ if (fs.existsSync(sessionPath)) {
14
+ try {
15
+ await this.context?.addCookies(
16
+ JSON.parse(fs.readFileSync(sessionPath, "utf-8")).cookies || []
17
+ );
18
+ this.log?.(`โœ… Loaded session from ${sessionPath}`);
19
+ } catch (err) {
20
+ this.log?.(`โš ๏ธ Failed to apply session: ${(err as Error).message}`);
21
+ }
22
+ } else {
23
+ this.log?.(`โš ๏ธ Session file not found: ${sessionPath}`);
24
+ }
25
+ }
26
+ );
27
+
28
+ // Step 2: Save current context as session
29
+ When(/^I save session as "([^"]+)"$/, async function (this: CustomWorld, sessionName: string) {
30
+ const sessionPath = resolveSessionPath(this, sessionName);
31
+
32
+ const cookies = await this.context.cookies();
33
+
34
+ const [localStorageData, sessionStorageData]: [
35
+ { origin: string; values: [string, string][] }[],
36
+ { origin: string; values: [string, string][] }[],
37
+ ] = await this.page.evaluate(() => {
38
+ const toPairs = (store: Storage): [string, string][] => Object.entries(store);
39
+
40
+ return [
41
+ [{ origin: location.origin, values: toPairs(localStorage) }],
42
+ [{ origin: location.origin, values: toPairs(sessionStorage) }],
43
+ ];
44
+ });
45
+
46
+ const sessionData = {
47
+ cookies,
48
+ localStorage: localStorageData,
49
+ sessionStorage: sessionStorageData,
50
+ };
51
+
52
+ try {
53
+ fs.writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
54
+ this.log?.(`๐Ÿ’พ Saved session as "${sessionName}"`);
55
+ } catch (err) {
56
+ this.log?.(`โŒ Failed to save session "${sessionName}": ${(err as Error).message}`);
57
+ }
58
+ });
59
+ // Step 3: Remove a session
60
+ When("I clear session {string}", function (this: CustomWorld, sessionName: string) {
61
+ const sessionPath = resolveSessionPath(this, sessionName);
62
+ if (fs.existsSync(sessionPath)) {
63
+ fs.unlinkSync(sessionPath);
64
+ this.log?.(`๐Ÿงน Cleared session: ${sessionPath}`);
65
+ } else {
66
+ this.log?.(`โš ๏ธ Session not found: ${sessionPath}`);
67
+ }
68
+ });
69
+ When(
70
+ /^I restore session cookies "([^"]+)"(?: with reload "(true|false)")?$/,
71
+ async function (this: CustomWorld, sessionName: string, reload = "true") {
72
+ const sessionPath = resolveSessionPath(this, sessionName);
73
+
74
+ if (!fs.existsSync(sessionPath)) {
75
+ this.log?.(`โŒ Session file not found: ${sessionPath}`);
76
+ return;
77
+ }
78
+
79
+ const sessionData = JSON.parse(fs.readFileSync(sessionPath, "utf-8"));
80
+ const { cookies = [], localStorage = [], sessionStorage = [] } = sessionData;
81
+
82
+ try {
83
+ // Clear & set cookies
84
+ if (cookies.length) {
85
+ const existing = await this.context.cookies();
86
+ if (existing.length) await this.context.clearCookies();
87
+ await this.context.addCookies(cookies);
88
+ this.log?.(`๐Ÿช Cookies restored from "${sessionName}"`);
89
+ }
90
+
91
+ // Apply storage into page context
92
+ await this.page.goto("about:blank");
93
+
94
+ if (localStorage.length > 0) {
95
+ for (const entry of localStorage) {
96
+ await this.page.addInitScript(
97
+ ([origin, values]) => {
98
+ if (window.origin === origin) {
99
+ for (const [key, val] of values) {
100
+ localStorage.setItem(key, val);
101
+ }
102
+ }
103
+ },
104
+ [entry.origin, entry.values]
105
+ );
106
+ }
107
+ this.log?.("๐Ÿ“ฆ localStorage restored");
108
+ }
109
+
110
+ if (sessionStorage.length > 0) {
111
+ for (const entry of sessionStorage) {
112
+ await this.page.addInitScript(
113
+ ([origin, values]) => {
114
+ if (window.origin === origin) {
115
+ for (const [key, val] of values) {
116
+ sessionStorage.setItem(key, val);
117
+ }
118
+ }
119
+ },
120
+ [entry.origin, entry.values]
121
+ );
122
+ }
123
+ this.log?.("๐Ÿ—„๏ธ sessionStorage restored");
124
+ }
125
+
126
+ // Final reload to apply context if requested
127
+ if (reload !== "false") {
128
+ await this.page.reload();
129
+ this.log?.("๐Ÿ”„ Page reloaded to apply restored session");
130
+ }
131
+ } catch (err) {
132
+ this.log?.(`โŒ Error restoring session: ${(err as Error).message}`);
133
+ }
134
+ }
135
+ );
@@ -0,0 +1,19 @@
1
+ export function checkPeerDependencies(dependencies: string[]) {
2
+ const missing: string[] = [];
3
+
4
+ for (const dep of dependencies) {
5
+ try {
6
+ require.resolve(dep);
7
+ } catch {
8
+ missing.push(dep);
9
+ }
10
+ }
11
+
12
+ if (missing.length) {
13
+ console.warn(
14
+ `\nโŒ Missing peer dependencies: ${missing.join(", ")}` +
15
+ `\nPlease install them in your project:\n\n` +
16
+ `npm install --save-dev ${missing.join(" ")}\n`
17
+ );
18
+ }
19
+ }
@@ -0,0 +1,35 @@
1
+ import fs from "fs";
2
+ import pixelmatch from "pixelmatch";
3
+ import { PNG } from "pngjs";
4
+
5
+ export function compareSnapshots({
6
+ actualPath,
7
+ baselinePath,
8
+ diffPath,
9
+ threshold = 0.1,
10
+ }: {
11
+ actualPath: string;
12
+ baselinePath: string;
13
+ diffPath: string;
14
+ threshold?: number;
15
+ }): number {
16
+ const actual = PNG.sync.read(fs.readFileSync(actualPath));
17
+ const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
18
+
19
+ if (actual.width !== baseline.width || actual.height !== baseline.height) {
20
+ throw new Error("Snapshot size mismatch");
21
+ }
22
+
23
+ const diff = new PNG({ width: actual.width, height: actual.height });
24
+ const numDiffPixels = pixelmatch(
25
+ actual.data,
26
+ baseline.data,
27
+ diff.data,
28
+ actual.width,
29
+ actual.height,
30
+ { threshold }
31
+ );
32
+
33
+ fs.writeFileSync(diffPath, PNG.sync.write(diff));
34
+ return numDiffPixels;
35
+ }