playwright-cucumber-ts-steps 1.1.6 → 1.1.8
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 +10 -15
- package/dist/backend/actions/click.d.ts +2 -0
- package/dist/backend/actions/click.d.ts.map +1 -0
- package/dist/backend/actions/click.js +179 -0
- package/dist/backend/actions/find.d.ts +2 -0
- package/dist/backend/actions/find.d.ts.map +1 -0
- package/dist/backend/actions/find.js +291 -0
- package/dist/backend/actions/form.d.ts +2 -0
- package/dist/backend/actions/form.d.ts.map +1 -0
- package/dist/backend/actions/form.js +185 -0
- package/dist/backend/actions/formTable.js +1 -1
- package/dist/backend/actions/frames.d.ts +2 -0
- package/dist/backend/actions/frames.d.ts.map +1 -0
- package/dist/backend/actions/frames.js +60 -0
- package/dist/backend/actions/index.d.ts +10 -0
- package/dist/backend/actions/index.d.ts.map +1 -1
- package/dist/backend/actions/index.js +10 -0
- package/dist/backend/actions/inputs.d.ts +2 -0
- package/dist/backend/actions/inputs.d.ts.map +1 -0
- package/dist/backend/actions/inputs.js +177 -0
- package/dist/backend/actions/keyboard.d.ts +2 -0
- package/dist/backend/actions/keyboard.d.ts.map +1 -0
- package/dist/backend/actions/keyboard.js +62 -0
- package/dist/backend/actions/misc.d.ts +2 -0
- package/dist/backend/actions/misc.d.ts.map +1 -0
- package/dist/backend/actions/misc.js +144 -0
- package/dist/backend/actions/mobile.d.ts +2 -0
- package/dist/backend/actions/mobile.d.ts.map +1 -0
- package/dist/backend/actions/mobile.js +87 -0
- package/dist/backend/actions/mouse.d.ts +2 -0
- package/dist/backend/actions/mouse.d.ts.map +1 -0
- package/dist/backend/actions/mouse.js +105 -0
- package/dist/backend/actions/navigation.js +31 -7
- package/dist/backend/actions/waits.d.ts +2 -0
- package/dist/backend/actions/waits.d.ts.map +1 -0
- package/dist/backend/actions/waits.js +51 -0
- package/dist/backend/api/index.d.ts +1 -0
- package/dist/backend/api/index.d.ts.map +1 -1
- package/dist/backend/api/index.js +1 -0
- package/dist/backend/api/network.d.ts +2 -0
- package/dist/backend/api/network.d.ts.map +1 -0
- package/dist/backend/api/network.js +145 -0
- package/dist/backend/assertions/pageState.js +25 -14
- package/dist/backend/assertions/visibility.js +116 -12
- package/dist/backend/utils/state.d.ts +18 -0
- package/dist/backend/utils/state.d.ts.map +1 -0
- package/dist/backend/utils/state.js +84 -0
- package/dist/core/registry.d.ts +14 -14
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +13 -4
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +91 -37
- package/package.json +1 -1
|
@@ -2,24 +2,35 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const test_1 = require("@playwright/test");
|
|
4
4
|
const registry_1 = require("../../core/registry");
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Checks if the URL contains a specific string.
|
|
7
|
+
* Pattern: Then I expect the url to contain "dashboard"
|
|
8
|
+
*/
|
|
9
|
+
(0, registry_1.Step)("I expect the url to contain {string}", async (page, part) => {
|
|
10
|
+
await (0, test_1.expect)(page).toHaveURL(new RegExp(part));
|
|
11
|
+
console.log(`✅ URL contains "${part}"`);
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Checks if the URL is an exact match.
|
|
15
|
+
* Pattern: Then I expect the url to be "https://google.com"
|
|
16
|
+
*/
|
|
6
17
|
(0, registry_1.Step)("I expect the url to be {string}", async (page, url) => {
|
|
7
18
|
await (0, test_1.expect)(page).toHaveURL(url);
|
|
19
|
+
console.log(`✅ URL is "${url}"`);
|
|
8
20
|
});
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the page title contains a string.
|
|
23
|
+
* Pattern: Then I expect the title to contain "Welcome"
|
|
24
|
+
*/
|
|
25
|
+
(0, registry_1.Step)("I expect the title to contain {string}", async (page, part) => {
|
|
26
|
+
await (0, test_1.expect)(page).toHaveTitle(new RegExp(part));
|
|
27
|
+
console.log(`✅ Title contains "${part}"`);
|
|
13
28
|
});
|
|
14
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Checks if the page title is an exact match.
|
|
31
|
+
* Pattern: Then I expect the title to be "Welcome Page"
|
|
32
|
+
*/
|
|
15
33
|
(0, registry_1.Step)("I expect the title to be {string}", async (page, title) => {
|
|
16
34
|
await (0, test_1.expect)(page).toHaveTitle(title);
|
|
17
|
-
});
|
|
18
|
-
// Partial Title Check
|
|
19
|
-
(0, registry_1.Step)("I expect the title to contain {string}", async (page, titlePart) => {
|
|
20
|
-
await (0, test_1.expect)(page).toHaveTitle(new RegExp(titlePart));
|
|
21
|
-
});
|
|
22
|
-
// Screenshot Match (Visual Regression)
|
|
23
|
-
(0, registry_1.Step)("I expect the page screenshot to match {string}", async (page, filename) => {
|
|
24
|
-
await (0, test_1.expect)(page).toHaveScreenshot(filename);
|
|
35
|
+
console.log(`✅ Title is "${title}"`);
|
|
25
36
|
});
|
|
@@ -2,19 +2,123 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const test_1 = require("@playwright/test");
|
|
4
4
|
const registry_1 = require("../../core/registry");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const state_1 = require("../utils/state");
|
|
6
|
+
// ===============================
|
|
7
|
+
// 1. VISIBILITY & STATE CHECKS
|
|
8
|
+
// ===============================
|
|
9
|
+
/**
|
|
10
|
+
* Checks if the stored element is visible.
|
|
11
|
+
* Pattern: Then I expect element to be visible
|
|
12
|
+
*/
|
|
13
|
+
(0, registry_1.Step)("I expect element to be visible", async (page) => {
|
|
14
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
15
|
+
await (0, test_1.expect)(element).toBeVisible();
|
|
16
|
+
console.log("✅ Element is visible");
|
|
8
17
|
});
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Checks if the stored element is hidden.
|
|
20
|
+
* Pattern: Then I expect element to be hidden
|
|
21
|
+
*/
|
|
22
|
+
(0, registry_1.Step)("I expect element to be hidden", async (page) => {
|
|
23
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
24
|
+
await (0, test_1.expect)(element).toBeHidden();
|
|
25
|
+
console.log("✅ Element is hidden");
|
|
12
26
|
});
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Checks if the stored element is enabled.
|
|
29
|
+
* Pattern: Then I expect element to be enabled
|
|
30
|
+
*/
|
|
31
|
+
(0, registry_1.Step)("I expect element to be enabled", async (page) => {
|
|
32
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
33
|
+
await (0, test_1.expect)(element).toBeEnabled();
|
|
34
|
+
console.log("✅ Element is enabled");
|
|
16
35
|
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Checks if the stored element is disabled.
|
|
38
|
+
* Pattern: Then I expect element to be disabled
|
|
39
|
+
*/
|
|
40
|
+
(0, registry_1.Step)("I expect element to be disabled", async (page) => {
|
|
41
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
42
|
+
await (0, test_1.expect)(element).toBeDisabled();
|
|
43
|
+
console.log("✅ Element is disabled");
|
|
44
|
+
});
|
|
45
|
+
// ===============================
|
|
46
|
+
// 2. TEXT & VALUE CHECKS
|
|
47
|
+
// ===============================
|
|
48
|
+
/**
|
|
49
|
+
* Checks if the element contains exact text.
|
|
50
|
+
* Pattern: Then I expect element to have text "Submit"
|
|
51
|
+
*/
|
|
52
|
+
(0, registry_1.Step)("I expect element to have text {string}", async (page, text) => {
|
|
53
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
54
|
+
await (0, test_1.expect)(element).toHaveText(text);
|
|
55
|
+
console.log(`✅ Element has text "${text}"`);
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Checks if the element contains partial text.
|
|
59
|
+
* Pattern: Then I expect element to contain text "Sub"
|
|
60
|
+
*/
|
|
61
|
+
(0, registry_1.Step)("I expect element to contain text {string}", async (page, text) => {
|
|
62
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
63
|
+
await (0, test_1.expect)(element).toContainText(text);
|
|
64
|
+
console.log(`✅ Element contains text "${text}"`);
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Checks if the element has a specific input value.
|
|
68
|
+
* Pattern: Then I expect element to have value "123"
|
|
69
|
+
*/
|
|
70
|
+
(0, registry_1.Step)("I expect element to have value {string}", async (page, value) => {
|
|
71
|
+
// Support aliases (e.g. @myVar)
|
|
72
|
+
if (value.startsWith("@")) {
|
|
73
|
+
const alias = value.slice(1);
|
|
74
|
+
const stored = (0, state_1.getVariable)(page, alias);
|
|
75
|
+
if (!stored)
|
|
76
|
+
throw new Error(`Alias @${alias} not found`);
|
|
77
|
+
value = stored;
|
|
78
|
+
}
|
|
79
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
80
|
+
await (0, test_1.expect)(element).toHaveValue(value);
|
|
81
|
+
console.log(`✅ Element has value "${value}"`);
|
|
82
|
+
});
|
|
83
|
+
// ===============================
|
|
84
|
+
// 3. ATTRIBUTE CHECKS
|
|
85
|
+
// ===============================
|
|
86
|
+
/**
|
|
87
|
+
* Checks if the element has a specific attribute.
|
|
88
|
+
* Pattern: Then I expect element to have attribute "data-test"
|
|
89
|
+
*/
|
|
90
|
+
(0, registry_1.Step)("I expect element to have attribute {string}", async (page, attr) => {
|
|
91
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
92
|
+
await (0, test_1.expect)(element).toHaveAttribute(attr);
|
|
93
|
+
console.log(`✅ Element has attribute "${attr}"`);
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Checks if the element has a specific attribute value.
|
|
97
|
+
* Pattern: Then I expect element to have attribute "type" with value "submit"
|
|
98
|
+
*/
|
|
99
|
+
(0, registry_1.Step)("I expect element to have attribute {string} with value {string}", async (page, attr, value) => {
|
|
100
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
101
|
+
await (0, test_1.expect)(element).toHaveAttribute(attr, value);
|
|
102
|
+
console.log(`✅ Element has attribute "${attr}" = "${value}"`);
|
|
103
|
+
});
|
|
104
|
+
// ===============================
|
|
105
|
+
// 4. VISUAL REGRESSION (Screenshots)
|
|
106
|
+
// ===============================
|
|
107
|
+
/**
|
|
108
|
+
* Verifies the ENTIRE page matches a stored screenshot.
|
|
109
|
+
* Pattern: Then I expect the page screenshot to match "home-page.png"
|
|
110
|
+
* Note: Screenshots are stored in 'tests/{feature-name}/' folder by default.
|
|
111
|
+
*/
|
|
112
|
+
(0, registry_1.Step)("I expect the page screenshot to match {string}", async (page, filename) => {
|
|
113
|
+
await (0, test_1.expect)(page).toHaveScreenshot(filename);
|
|
114
|
+
console.log(`📸 Page matches screenshot: ${filename}`);
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* Verifies the CURRENT ELEMENT matches a stored screenshot.
|
|
118
|
+
* Pattern: Then I expect the element screenshot to match "submit-btn.png"
|
|
119
|
+
*/
|
|
120
|
+
(0, registry_1.Step)("I expect the element screenshot to match {string}", async (page, filename) => {
|
|
121
|
+
const element = (0, state_1.getActiveElement)(page);
|
|
122
|
+
await (0, test_1.expect)(element).toHaveScreenshot(filename);
|
|
123
|
+
console.log(`📸 Element matches screenshot: ${filename}`);
|
|
20
124
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Page, Locator } from "@playwright/test";
|
|
2
|
+
export declare function setActiveElement(page: Page, element: Locator): void;
|
|
3
|
+
export declare function getActiveElement(page: Page): Locator;
|
|
4
|
+
export declare function setActiveElements(page: Page, elements: Locator): void;
|
|
5
|
+
export declare function getActiveElements(page: Page): Locator;
|
|
6
|
+
export declare function setVariable(page: Page, key: string, value: any): void;
|
|
7
|
+
export declare function getVariable(page: Page, key: string): any;
|
|
8
|
+
export declare function parseClickOptions(table: any): {
|
|
9
|
+
force?: boolean;
|
|
10
|
+
button?: "left" | "right" | "middle";
|
|
11
|
+
modifiers?: Array<"Alt" | "Control" | "Meta" | "Shift">;
|
|
12
|
+
position?: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
};
|
|
16
|
+
timeout?: number;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/backend/utils/state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAKjD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,QAE5D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAQpD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,QAE9D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAMrD;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,QAK9D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAGxD;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,GAAG,GAAG;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACrC,SAAS,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACxD,QAAQ,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAwCA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setActiveElement = setActiveElement;
|
|
4
|
+
exports.getActiveElement = getActiveElement;
|
|
5
|
+
exports.setActiveElements = setActiveElements;
|
|
6
|
+
exports.getActiveElements = getActiveElements;
|
|
7
|
+
exports.setVariable = setVariable;
|
|
8
|
+
exports.getVariable = getVariable;
|
|
9
|
+
exports.parseClickOptions = parseClickOptions;
|
|
10
|
+
// 1. STATE MANAGEMENT
|
|
11
|
+
// We attach data to the Playwright Page object so it persists between steps.
|
|
12
|
+
function setActiveElement(page, element) {
|
|
13
|
+
page.__bdd_element = element;
|
|
14
|
+
}
|
|
15
|
+
function getActiveElement(page) {
|
|
16
|
+
const el = page.__bdd_element;
|
|
17
|
+
if (!el) {
|
|
18
|
+
throw new Error("❌ No stored element found. Did you forget a 'When I find...' step?");
|
|
19
|
+
}
|
|
20
|
+
return el;
|
|
21
|
+
}
|
|
22
|
+
function setActiveElements(page, elements) {
|
|
23
|
+
page.__bdd_elements = elements;
|
|
24
|
+
}
|
|
25
|
+
function getActiveElements(page) {
|
|
26
|
+
const els = page.__bdd_elements;
|
|
27
|
+
if (!els) {
|
|
28
|
+
throw new Error("❌ No stored elements list found.");
|
|
29
|
+
}
|
|
30
|
+
return els;
|
|
31
|
+
}
|
|
32
|
+
// 2. DATA / ALIAS MANAGEMENT (for @variable support)
|
|
33
|
+
function setVariable(page, key, value) {
|
|
34
|
+
if (!page.__bdd_data) {
|
|
35
|
+
page.__bdd_data = {};
|
|
36
|
+
}
|
|
37
|
+
page.__bdd_data[key] = value;
|
|
38
|
+
}
|
|
39
|
+
function getVariable(page, key) {
|
|
40
|
+
const data = page.__bdd_data;
|
|
41
|
+
return data ? data[key] : undefined;
|
|
42
|
+
}
|
|
43
|
+
// 3. OPTION PARSERS
|
|
44
|
+
// Converts Gherkin DataTables into Playwright ClickOptions
|
|
45
|
+
function parseClickOptions(table) {
|
|
46
|
+
if (!table)
|
|
47
|
+
return {};
|
|
48
|
+
let hash = {};
|
|
49
|
+
// Handle Cucumber DataTable object (legacy) or raw Array
|
|
50
|
+
if (typeof table.rowsHash === "function") {
|
|
51
|
+
hash = table.rowsHash();
|
|
52
|
+
}
|
|
53
|
+
else if (Array.isArray(table)) {
|
|
54
|
+
// Convert Array of Arrays [['force', 'true']] to Object { force: 'true' }
|
|
55
|
+
table.forEach((row) => {
|
|
56
|
+
if (Array.isArray(row) && row.length >= 2) {
|
|
57
|
+
hash[row[0].toString()] = row[1].toString();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
const options = {};
|
|
65
|
+
// Parse specific boolean/number values
|
|
66
|
+
if (hash["force"] === "true")
|
|
67
|
+
options.force = true;
|
|
68
|
+
if (hash["button"])
|
|
69
|
+
options.button = hash["button"];
|
|
70
|
+
if (hash["timeout"])
|
|
71
|
+
options.timeout = parseInt(hash["timeout"], 10);
|
|
72
|
+
// Handle modifiers (comma separated)
|
|
73
|
+
if (hash["modifiers"]) {
|
|
74
|
+
options.modifiers = hash["modifiers"].split(",").map((m) => m.trim());
|
|
75
|
+
}
|
|
76
|
+
// Handle position (x,y)
|
|
77
|
+
if (hash["x"] && hash["y"]) {
|
|
78
|
+
options.position = {
|
|
79
|
+
x: parseInt(hash["x"], 10),
|
|
80
|
+
y: parseInt(hash["y"], 10),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return options;
|
|
84
|
+
}
|
package/dist/core/registry.d.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { CucumberExpression } from "@cucumber/cucumber-expressions";
|
|
2
2
|
import { Page } from "@playwright/test";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @param fn - The async function to execute. It receives the Playwright Page object as the first argument.
|
|
7
|
-
* * @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* Step('I scroll to bottom', async (page) => {
|
|
10
|
-
* await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
11
|
-
* });
|
|
12
|
-
* ```
|
|
4
|
+
* Define the type of function for our steps.
|
|
5
|
+
* Every step automatically gets 'page' as the first argument.
|
|
13
6
|
*/
|
|
14
7
|
export type StepAction = (page: Page, ...args: any[]) => Promise<void>;
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
/**
|
|
9
|
+
* 1. StepDefinition Interface
|
|
10
|
+
* Updated to allow both CucumberExpression (legacy) AND RegExp (standard)
|
|
11
|
+
*/
|
|
12
|
+
export interface StepDefinition {
|
|
13
|
+
expression: CucumberExpression | RegExp;
|
|
17
14
|
fn: StepAction;
|
|
18
|
-
pattern: string;
|
|
15
|
+
pattern: string | RegExp;
|
|
19
16
|
}
|
|
20
17
|
export declare const stepRegistry: StepDefinition[];
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 3. The Function to Register Steps
|
|
20
|
+
* Supports passing a string (converted to CucumberExpression) OR a direct RegExp.
|
|
21
|
+
*/
|
|
22
|
+
export declare function Step(pattern: string | RegExp, fn: StepAction): void;
|
|
23
23
|
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/core/registry.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAEnB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAExC
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/core/registry.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAEnB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAExC;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAAC;IACxC,EAAE,EAAE,UAAU,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAGD,eAAO,MAAM,YAAY,EAAE,cAAc,EAAO,CAAC;AAIjD;;;GAGG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,EAAE,UAAU,QAgB5D"}
|
package/dist/core/registry.js
CHANGED
|
@@ -5,13 +5,22 @@ exports.Step = Step;
|
|
|
5
5
|
// src/core/registry.ts
|
|
6
6
|
const cucumber_expressions_1 = require("@cucumber/cucumber-expressions");
|
|
7
7
|
// 2. The Global Registry
|
|
8
|
-
// This array stores every step you create in the backend folder
|
|
9
8
|
exports.stepRegistry = [];
|
|
10
9
|
const parameterTypeRegistry = new cucumber_expressions_1.ParameterTypeRegistry();
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 3. The Function to Register Steps
|
|
12
|
+
* Supports passing a string (converted to CucumberExpression) OR a direct RegExp.
|
|
13
|
+
*/
|
|
13
14
|
function Step(pattern, fn) {
|
|
14
|
-
|
|
15
|
+
let expression;
|
|
16
|
+
if (pattern instanceof RegExp) {
|
|
17
|
+
// ✅ If it's a Regex, use it directly (Faster, Standard)
|
|
18
|
+
expression = pattern;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// ⚠️ If it's a String, convert to Cucumber Expression (Legacy)
|
|
22
|
+
expression = new cucumber_expressions_1.CucumberExpression(pattern, parameterTypeRegistry);
|
|
23
|
+
}
|
|
15
24
|
exports.stepRegistry.push({
|
|
16
25
|
expression,
|
|
17
26
|
fn,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/core/runner.ts"],"names":[],"mappings":"AAMA,OAAO,0BAA0B,CAAC;AAClC,OAAO,6BAA6B,CAAC;AACrC,OAAO,2BAA2B,CAAC;AACnC,OAAO,sBAAsB,CAAC;AAC9B,OAAO,uBAAuB,CAAC;AAE/B,OAAO,qBAAqB,CAAC;AAE7B,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CAC3C;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/core/runner.ts"],"names":[],"mappings":"AAMA,OAAO,0BAA0B,CAAC;AAClC,OAAO,6BAA6B,CAAC;AACrC,OAAO,2BAA2B,CAAC;AACnC,OAAO,sBAAsB,CAAC;AAC9B,OAAO,uBAAuB,CAAC;AAE/B,OAAO,qBAAqB,CAAC;AAE7B,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CAC3C;AASD,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QA6LpE"}
|
package/dist/core/runner.js
CHANGED
|
@@ -58,7 +58,6 @@ function runTests(featureGlob, options) {
|
|
|
58
58
|
for (const file of files) {
|
|
59
59
|
const content = fs.readFileSync(file, "utf8");
|
|
60
60
|
// 1. CAPTURE FEATURE TAGS
|
|
61
|
-
// Loose Regex: Matches any lines starting with @ above "Feature:"
|
|
62
61
|
const featureTagMatch = content.match(/((?:@\S+\s*)+)Feature:/);
|
|
63
62
|
const rawFeatureTags = featureTagMatch ? featureTagMatch[1] : "";
|
|
64
63
|
const featureTags = rawFeatureTags.replace(/[\r\n]+/g, " ").trim();
|
|
@@ -67,9 +66,8 @@ function runTests(featureGlob, options) {
|
|
|
67
66
|
? featureMatch[1].trim()
|
|
68
67
|
: "Unnamed Feature";
|
|
69
68
|
test_1.test.describe(featureName, () => {
|
|
70
|
-
// 2.
|
|
69
|
+
// 2. SCENARIO REGEX
|
|
71
70
|
// Matches: Optional Tags -> (Scenario OR Scenario Outline) -> Name
|
|
72
|
-
// Change: @\S+ allows dots/dashes. "Scenario|Scenario Outline" supports outlines.
|
|
73
71
|
const scenarioRegex = /(?:((?:@\S+\s*)+))?(?:Scenario|Scenario Outline):\s*(.+)/g;
|
|
74
72
|
let match;
|
|
75
73
|
let foundCount = 0;
|
|
@@ -83,9 +81,7 @@ function runTests(featureGlob, options) {
|
|
|
83
81
|
const fullName = combinedTags
|
|
84
82
|
? `${scenarioName} ${combinedTags}`
|
|
85
83
|
: scenarioName;
|
|
86
|
-
//
|
|
87
|
-
// console.log(` 👉 Found: "${scenarioName}" [Tags: ${combinedTags}]`);
|
|
88
|
-
// 4. ENV FILTERING (Optional)
|
|
84
|
+
// 4. ENV FILTERING
|
|
89
85
|
const activeFilter = options?.tags || envTag;
|
|
90
86
|
if (activeFilter) {
|
|
91
87
|
const targetGroups = activeFilter.split(",").map((t) => t.trim());
|
|
@@ -96,6 +92,7 @@ function runTests(featureGlob, options) {
|
|
|
96
92
|
if (!isMatch)
|
|
97
93
|
continue;
|
|
98
94
|
}
|
|
95
|
+
// 5. EXTRACT SCENARIO BLOCK
|
|
99
96
|
const startIndex = match.index + match[0].length;
|
|
100
97
|
const nextMatchIndex = content
|
|
101
98
|
.slice(startIndex)
|
|
@@ -103,43 +100,91 @@ function runTests(featureGlob, options) {
|
|
|
103
100
|
const blockEnd = nextMatchIndex === -1 ? content.length : startIndex + nextMatchIndex;
|
|
104
101
|
const scenarioBlock = content.slice(startIndex, blockEnd);
|
|
105
102
|
(0, test_1.test)(fullName, async ({ page }, testInfo) => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
// ==================================================
|
|
104
|
+
// PHASE 1: PARSE GHERKIN (Preserve Formatting)
|
|
105
|
+
// ==================================================
|
|
106
|
+
const rawLines = scenarioBlock.split("\n");
|
|
107
|
+
const steps = [];
|
|
108
|
+
let currentStep = null;
|
|
109
|
+
let docStringBuffer = [];
|
|
110
|
+
let isDocStringOpen = false;
|
|
111
|
+
for (let line of rawLines) {
|
|
112
|
+
const trimmedLine = line.trim();
|
|
113
|
+
// A. Handle DocStrings (""")
|
|
114
|
+
if (trimmedLine.startsWith('"""')) {
|
|
115
|
+
isDocStringOpen = !isDocStringOpen;
|
|
116
|
+
if (!isDocStringOpen && currentStep) {
|
|
117
|
+
// Closing DocString: Save buffer to step
|
|
118
|
+
currentStep.docString = docStringBuffer.join("\n");
|
|
119
|
+
docStringBuffer = [];
|
|
120
|
+
}
|
|
116
121
|
continue;
|
|
117
|
-
// Handle Data Tables
|
|
118
|
-
const tableData = [];
|
|
119
|
-
while (i + 1 < lines.length && lines[i + 1].startsWith("|")) {
|
|
120
|
-
i++;
|
|
121
|
-
const row = lines[i]
|
|
122
|
-
.split("|")
|
|
123
|
-
.map((cell) => cell.trim())
|
|
124
|
-
.filter((cell) => cell !== "");
|
|
125
|
-
tableData.push(row);
|
|
126
122
|
}
|
|
127
|
-
|
|
123
|
+
// B. Inside DocString? Capture raw line (preserve indent)
|
|
124
|
+
if (isDocStringOpen) {
|
|
125
|
+
docStringBuffer.push(line); // Don't trim!
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// C. Skip Empty Lines & Comments
|
|
129
|
+
if (!trimmedLine ||
|
|
130
|
+
trimmedLine.startsWith("#") ||
|
|
131
|
+
trimmedLine.startsWith("@")) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// D. Handle Data Tables (| col | col |)
|
|
135
|
+
if (trimmedLine.startsWith("|")) {
|
|
136
|
+
if (currentStep) {
|
|
137
|
+
if (!currentStep.dataTable)
|
|
138
|
+
currentStep.dataTable = [];
|
|
139
|
+
const row = trimmedLine
|
|
140
|
+
.split("|")
|
|
141
|
+
.map((cell) => cell.trim())
|
|
142
|
+
.filter((cell, index, arr) => index > 0 && index < arr.length - 1);
|
|
143
|
+
// Simple split often leaves empty strings at start/end
|
|
144
|
+
// Better split logic:
|
|
145
|
+
const cleanRow = trimmedLine
|
|
146
|
+
.split("|")
|
|
147
|
+
.slice(1, -1) // Remove first and last empty splits from |...|
|
|
148
|
+
.map((c) => c.trim());
|
|
149
|
+
currentStep.dataTable.push(cleanRow);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// E. It is a New Step
|
|
154
|
+
const cleanText = trimmedLine
|
|
128
155
|
.replace(/^(Given|When|Then|And|But)\s+/i, "")
|
|
129
|
-
.replace(/:$/, "")
|
|
156
|
+
.replace(/:$/, "") // Remove trailing colon
|
|
130
157
|
.trim();
|
|
131
|
-
|
|
158
|
+
currentStep = {
|
|
159
|
+
text: trimmedLine,
|
|
160
|
+
cleanText: cleanText,
|
|
161
|
+
};
|
|
162
|
+
steps.push(currentStep);
|
|
163
|
+
}
|
|
164
|
+
// ==================================================
|
|
165
|
+
// PHASE 2: EXECUTE STEPS
|
|
166
|
+
// ==================================================
|
|
167
|
+
console.log(`\n🔹 Scenario: ${scenarioName}`);
|
|
168
|
+
for (const step of steps) {
|
|
169
|
+
const matchResult = findMatchingStep(step.cleanText);
|
|
132
170
|
if (!matchResult) {
|
|
133
|
-
throw new Error(`❌ Undefined Step: "${
|
|
171
|
+
throw new Error(`❌ Undefined Step: "${step.cleanText}"`);
|
|
134
172
|
}
|
|
135
173
|
try {
|
|
174
|
+
console.log(` executing: ${step.text.trim()}`);
|
|
136
175
|
const args = [...matchResult.args];
|
|
137
|
-
|
|
138
|
-
|
|
176
|
+
// Append Data Table if present
|
|
177
|
+
if (step.dataTable && step.dataTable.length > 0) {
|
|
178
|
+
args.push(step.dataTable);
|
|
179
|
+
}
|
|
180
|
+
// Append DocString if present
|
|
181
|
+
if (step.docString) {
|
|
182
|
+
args.push(step.docString);
|
|
183
|
+
}
|
|
139
184
|
await matchResult.fn(page, ...args);
|
|
140
185
|
}
|
|
141
186
|
catch (error) {
|
|
142
|
-
console.error(`❌ Failed at step: "${
|
|
187
|
+
console.error(`❌ Failed at step: "${step.text.trim()}"`);
|
|
143
188
|
const screenshot = await page.screenshot({
|
|
144
189
|
fullPage: true,
|
|
145
190
|
type: "png",
|
|
@@ -153,32 +198,41 @@ function runTests(featureGlob, options) {
|
|
|
153
198
|
}
|
|
154
199
|
});
|
|
155
200
|
}
|
|
156
|
-
// SAFETY CHECK
|
|
157
201
|
if (foundCount === 0) {
|
|
158
202
|
console.warn(`⚠️ File matched but 0 Scenarios found in: ${file}`);
|
|
159
|
-
console.warn(` Check if you are using 'Scenario Outline' or strange spacing.`);
|
|
160
203
|
}
|
|
161
204
|
});
|
|
162
205
|
}
|
|
163
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Finds the matching step definition from the registry.
|
|
209
|
+
* Supports: RegExp (with capture groups) and CucumberExpressions.
|
|
210
|
+
*/
|
|
164
211
|
function findMatchingStep(text) {
|
|
165
212
|
for (const step of registry_1.stepRegistry) {
|
|
213
|
+
// 1. RegExp Match
|
|
166
214
|
if (step.expression instanceof RegExp) {
|
|
167
215
|
const match = step.expression.exec(text);
|
|
168
|
-
if (match)
|
|
216
|
+
if (match) {
|
|
217
|
+
// match[0] is full string, slice(1) are capture groups
|
|
169
218
|
return { fn: step.fn, args: match.slice(1) };
|
|
219
|
+
}
|
|
170
220
|
}
|
|
221
|
+
// 2. Cucumber Expression Match (if strictly typed)
|
|
171
222
|
else if (typeof step.expression.match === "function") {
|
|
172
223
|
const match = step.expression.match(text);
|
|
173
|
-
if (match)
|
|
224
|
+
if (match) {
|
|
174
225
|
return {
|
|
175
226
|
fn: step.fn,
|
|
176
227
|
args: match.map((arg) => arg.getValue(null)),
|
|
177
228
|
};
|
|
229
|
+
}
|
|
178
230
|
}
|
|
231
|
+
// 3. String Match (Legacy/Simple)
|
|
179
232
|
else if (typeof step.expression === "string") {
|
|
180
|
-
if (step.expression === text)
|
|
233
|
+
if (step.expression === text) {
|
|
181
234
|
return { fn: step.fn, args: [] };
|
|
235
|
+
}
|
|
182
236
|
}
|
|
183
237
|
}
|
|
184
238
|
return null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright-cucumber-ts-steps",
|
|
3
3
|
"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.",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.8",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|