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.
Files changed (53) hide show
  1. package/README.md +10 -15
  2. package/dist/backend/actions/click.d.ts +2 -0
  3. package/dist/backend/actions/click.d.ts.map +1 -0
  4. package/dist/backend/actions/click.js +179 -0
  5. package/dist/backend/actions/find.d.ts +2 -0
  6. package/dist/backend/actions/find.d.ts.map +1 -0
  7. package/dist/backend/actions/find.js +291 -0
  8. package/dist/backend/actions/form.d.ts +2 -0
  9. package/dist/backend/actions/form.d.ts.map +1 -0
  10. package/dist/backend/actions/form.js +185 -0
  11. package/dist/backend/actions/formTable.js +1 -1
  12. package/dist/backend/actions/frames.d.ts +2 -0
  13. package/dist/backend/actions/frames.d.ts.map +1 -0
  14. package/dist/backend/actions/frames.js +60 -0
  15. package/dist/backend/actions/index.d.ts +10 -0
  16. package/dist/backend/actions/index.d.ts.map +1 -1
  17. package/dist/backend/actions/index.js +10 -0
  18. package/dist/backend/actions/inputs.d.ts +2 -0
  19. package/dist/backend/actions/inputs.d.ts.map +1 -0
  20. package/dist/backend/actions/inputs.js +177 -0
  21. package/dist/backend/actions/keyboard.d.ts +2 -0
  22. package/dist/backend/actions/keyboard.d.ts.map +1 -0
  23. package/dist/backend/actions/keyboard.js +62 -0
  24. package/dist/backend/actions/misc.d.ts +2 -0
  25. package/dist/backend/actions/misc.d.ts.map +1 -0
  26. package/dist/backend/actions/misc.js +144 -0
  27. package/dist/backend/actions/mobile.d.ts +2 -0
  28. package/dist/backend/actions/mobile.d.ts.map +1 -0
  29. package/dist/backend/actions/mobile.js +87 -0
  30. package/dist/backend/actions/mouse.d.ts +2 -0
  31. package/dist/backend/actions/mouse.d.ts.map +1 -0
  32. package/dist/backend/actions/mouse.js +105 -0
  33. package/dist/backend/actions/navigation.js +31 -7
  34. package/dist/backend/actions/waits.d.ts +2 -0
  35. package/dist/backend/actions/waits.d.ts.map +1 -0
  36. package/dist/backend/actions/waits.js +51 -0
  37. package/dist/backend/api/index.d.ts +1 -0
  38. package/dist/backend/api/index.d.ts.map +1 -1
  39. package/dist/backend/api/index.js +1 -0
  40. package/dist/backend/api/network.d.ts +2 -0
  41. package/dist/backend/api/network.d.ts.map +1 -0
  42. package/dist/backend/api/network.js +145 -0
  43. package/dist/backend/assertions/pageState.js +25 -14
  44. package/dist/backend/assertions/visibility.js +116 -12
  45. package/dist/backend/utils/state.d.ts +18 -0
  46. package/dist/backend/utils/state.d.ts.map +1 -0
  47. package/dist/backend/utils/state.js +84 -0
  48. package/dist/core/registry.d.ts +14 -14
  49. package/dist/core/registry.d.ts.map +1 -1
  50. package/dist/core/registry.js +13 -4
  51. package/dist/core/runner.d.ts.map +1 -1
  52. package/dist/core/runner.js +91 -37
  53. 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
- // URL Check
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
- // Partial URL Check
10
- (0, registry_1.Step)("I expect the url to contain {string}", async (page, part) => {
11
- // We use a RegExp to allow partial matching safely
12
- await (0, test_1.expect)(page).toHaveURL(new RegExp(part));
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
- // Title Check
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
- // Check if element exists and is visible
6
- (0, registry_1.Step)("I expect {string} to be visible", async (page, selector) => {
7
- await (0, test_1.expect)(page.locator(selector)).toBeVisible();
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
- // Check if element is hidden (or doesn't exist)
10
- (0, registry_1.Step)("I expect {string} to be hidden", async (page, selector) => {
11
- await (0, test_1.expect)(page.locator(selector)).toBeHidden();
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
- // Check if element is enabled (clickable)
14
- (0, registry_1.Step)("I expect {string} to be enabled", async (page, selector) => {
15
- await (0, test_1.expect)(page.locator(selector)).toBeEnabled();
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
- // Check if element is disabled
18
- (0, registry_1.Step)("I expect {string} to be disabled", async (page, selector) => {
19
- await (0, test_1.expect)(page.locator(selector)).toBeDisabled();
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
+ }
@@ -1,23 +1,23 @@
1
1
  import { CucumberExpression } from "@cucumber/cucumber-expressions";
2
2
  import { Page } from "@playwright/test";
3
3
  /**
4
- * Registers a new Step Definition.
5
- * * @param pattern - The Gherkin pattern (e.g., 'I click {string}')
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
- interface StepDefinition {
16
- expression: CucumberExpression;
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
- export declare function Step(pattern: string, fn: StepAction): void;
22
- export {};
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;;;;;;;;;;GAUG;AAGH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE,UAAU,cAAc;IACtB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,EAAE,EAAE,UAAU,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,eAAO,MAAM,YAAY,EAAE,cAAc,EAAO,CAAC;AAMjD,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,QAQnD"}
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"}
@@ -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
- // 3. The Function to Register Steps
12
- // You will use this inside src/backend/actions/...
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
- const expression = new cucumber_expressions_1.CucumberExpression(pattern, parameterTypeRegistry);
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;AAED,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,QAyIpE"}
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"}
@@ -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. LOOSE SCENARIO REGEX
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
- // DEBUG LOG: See what we found
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
- const lines = scenarioBlock
107
- .trim()
108
- .split("\n")
109
- .map((l) => l.trim())
110
- .filter((l) => l);
111
- for (let i = 0; i < lines.length; i++) {
112
- const stepText = lines[i];
113
- if (stepText.startsWith("#") ||
114
- stepText.startsWith("@") ||
115
- stepText === "")
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
- const cleanStep = stepText
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
- const matchResult = findMatchingStep(cleanStep);
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: "${cleanStep}"`);
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
- if (tableData.length > 0)
138
- args.push(tableData);
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: "${stepText}"`);
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.6",
4
+ "version": "1.1.8",
5
5
  "private": false,
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",