playwright-step-decorator-plugin 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Emanuele Minotto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # playwright-step-decorator-plugin
2
+
3
+ A TypeScript `@step` decorator that wraps Playwright Page Object Model methods in
4
+ [`test.step()`](https://playwright.dev/docs/api/class-test#test-step) and automatically
5
+ generates human-readable step titles — plus an ESLint rule to enforce consistent usage
6
+ across your test suite.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install playwright-step-decorator-plugin
12
+ ```
13
+
14
+ Peer dependencies:
15
+
16
+ ```bash
17
+ npm install --save-dev @playwright/test
18
+ # optional — only needed for the ESLint rule
19
+ npm install --save-dev eslint
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### `@step` decorator
25
+
26
+ The decorator wraps any async Page Object Model method in `test.step()`, generating a
27
+ descriptive title from the class name, method name, and the **actual runtime argument
28
+ values** — all without any manual naming.
29
+
30
+ ```ts
31
+ import { step } from 'playwright-step-decorator-plugin';
32
+
33
+ class LoginPage {
34
+ constructor(private readonly page: Page) {}
35
+
36
+ // Bare usage — fully automatic title
37
+ @step
38
+ async login(username: string, password: string) {
39
+ await this.page.fill('#username', username);
40
+ await this.page.fill('#password', password);
41
+ await this.page.click('#submit');
42
+ }
43
+ // → 'Login page: login using username "admin@acme.com" and password "s3cr3t"'
44
+
45
+ // Custom step name — params are still appended
46
+ @step('Sign in to the application')
47
+ async signIn(username: string) {
48
+ // ...
49
+ }
50
+ // → 'Sign in to the application using username "admin@acme.com"'
51
+
52
+ // Options only — humanizer still runs, but step is boxed
53
+ @step({ box: true, timeout: 10_000 })
54
+ async fillForm(firstName: string, lastName: string) {
55
+ // ...
56
+ }
57
+ // → 'Login page: fill form using first name "Ada" and last name "Lovelace"'
58
+
59
+ // Custom name + options
60
+ @step('Reset password', { box: true })
61
+ async resetPassword(email: string) {
62
+ // ...
63
+ }
64
+ // → 'Reset password using email "ada@example.com"'
65
+
66
+ // Disable the humanizer entirely — raw "ClassName.methodName", no params
67
+ @step({ noHumanizer: true })
68
+ async internalReset() {
69
+ // ...
70
+ }
71
+ // → 'LoginPage.internalReset'
72
+ }
73
+ ```
74
+
75
+ ### `StepOptions`
76
+
77
+ | Option | Type | Default | Description |
78
+ |--------|------|---------|-------------|
79
+ | `box` | `boolean` | `false` | Box the step so errors point to the call site ([docs](https://playwright.dev/docs/api/class-test#test-step)) |
80
+ | `timeout` | `number` | — | Maximum step duration in milliseconds |
81
+ | `noHumanizer` | `boolean` | `false` | Skip humanization; use `ClassName.methodName` as-is with no params |
82
+
83
+ ### How the humanizer builds step titles
84
+
85
+ The step title is assembled at **call time** using real argument values:
86
+
87
+ ```
88
+ "<Humanized class name>: <humanized method name> using <top-3 params>"
89
+ ```
90
+
91
+ Parameter rules:
92
+ - At most **3 parameters** are shown; the rest are collapsed into `"and N more options"`.
93
+ - Parameters are ranked by value type: strings/numbers (highest) → arrays/objects → booleans → null/undefined (lowest).
94
+ - Each value is rendered concisely: strings are quoted, booleans become plain names or `"not <name>"`, arrays show their items (up to 3), and large objects are summarised as `"with N fields"`.
95
+
96
+ ```ts
97
+ // searchProducts("laptop", {brand:"apple"}, "price", 1, 20)
98
+ // → 'Search page: search products using query "laptop", sort by "price" and page 1 and 2 more options'
99
+
100
+ // bulkAddItems([1,2,3], null, {giftWrap:true, note:"birthday"})
101
+ // → 'Cart page: bulk add items using item ids [1, 2, 3], coupon: not provided and options {giftWrap: true, note: "birthday"}'
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Why the humanizer matters for debugging
107
+
108
+ When a Playwright test fails, you open the HTML report or the trace viewer and look for
109
+ the step that broke. Without explicit names, steps show the raw method call —
110
+ `LoginPage.fillCredentials` — which is opaque to everyone except the developer who wrote
111
+ it. With the humanizer you get **"Login page: fill credentials using username
112
+ "admin@acme.com" and password "s3cr3t""** — a sentence that tells you exactly which page,
113
+ which action, and which data were involved, before you even open the trace.
114
+
115
+ **Concrete benefits:**
116
+
117
+ 1. **Instant triage.** The failing step title reads like a sentence.
118
+ Non-technical stakeholders in a report can understand at a glance what went wrong
119
+ without decoding PascalCase identifiers.
120
+
121
+ 2. **Parameters in the title.** The humanizer captures runtime values so the step title
122
+ contains the actual data (`userId "u_42"`, `role "admin"`) rather than placeholder
123
+ names. You know exactly which input triggered the failure.
124
+
125
+ 3. **Consistency without discipline.** Manual naming drifts: one developer writes
126
+ `"Login as user"`, another writes `"loginPage.login"`. The decorator produces a
127
+ uniform format across the entire codebase automatically.
128
+
129
+ 4. **Better trace viewer UX.** Playwright's trace viewer nests steps visually.
130
+ Human-readable titles make the step tree self-documenting, reducing the time spent
131
+ hunting for the right frame.
132
+
133
+ 5. **Regression signal.** When a humanized step title changes between runs (because a
134
+ parameter changed), it stands out immediately in diff-based report tools.
135
+
136
+ ---
137
+
138
+ ## ESLint rule — `require-step-decorator`
139
+
140
+ Enforce that every public method on a POM class is decorated with `@step`.
141
+
142
+ ### Setup (ESLint flat config)
143
+
144
+ ```js
145
+ // eslint.config.js
146
+ import { eslintPlugin } from 'playwright-step-decorator-plugin';
147
+
148
+ export default [
149
+ eslintPlugin.configs.recommended,
150
+ // or configure manually:
151
+ {
152
+ plugins: { 'playwright-step-decorator': eslintPlugin },
153
+ rules: {
154
+ 'playwright-step-decorator/require-step-decorator': 'error',
155
+ },
156
+ },
157
+ ];
158
+ ```
159
+
160
+ ### Import (ESLint plugin only)
161
+
162
+ ```js
163
+ import eslintPlugin from 'playwright-step-decorator-plugin/eslint-plugin';
164
+ ```
165
+
166
+ ### Rule options
167
+
168
+ ```js
169
+ {
170
+ 'playwright-step-decorator/require-step-decorator': ['error', {
171
+ // Regex to identify POM classes by name.
172
+ // Default: "(Page|Component|Widget|Fragment)$"
173
+ pomPattern: '(Page|Component|Widget|Fragment)$',
174
+
175
+ // Name of the decorator to look for.
176
+ // Default: "step"
177
+ decoratorName: 'step',
178
+ }]
179
+ }
180
+ ```
181
+
182
+ ### What the rule checks
183
+
184
+ - Matches classes whose name (or whose parent class name) satisfies `pomPattern`.
185
+ - Reports any **public, non-constructor, non-accessor** method that is missing the
186
+ `@step` decorator.
187
+ - Private methods (`private` modifier or `#name`), getters, setters, and constructors
188
+ are excluded.
189
+
190
+ ```ts
191
+ // ✅ OK
192
+ class LoginPage {
193
+ @step
194
+ async login(username: string) { ... }
195
+ }
196
+
197
+ // ❌ Error: Method "login" in POM class "LoginPage" must be decorated with @step.
198
+ class LoginPage {
199
+ async login(username: string) { ... }
200
+ }
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Separate entry points
206
+
207
+ ```ts
208
+ // Full package (decorator + ESLint plugin)
209
+ import { step, eslintPlugin } from 'playwright-step-decorator-plugin';
210
+
211
+ // Decorator only
212
+ import { step } from 'playwright-step-decorator-plugin/decorator';
213
+
214
+ // ESLint plugin only
215
+ import eslintPlugin from 'playwright-step-decorator-plugin/eslint-plugin';
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Requirements
221
+
222
+ - TypeScript 5.0+ (Stage 3 decorators — no `experimentalDecorators` flag needed)
223
+ - `@playwright/test` ≥ 1.40
224
+ - Node.js ≥ 18
225
+
226
+ ---
227
+
228
+ ## Contributing
229
+
230
+ See [CONTRIBUTING.md](CONTRIBUTING.md). We follow
231
+ [Conventional Commits](https://www.conventionalcommits.org/).
232
+
233
+ ## License
234
+
235
+ [MIT](LICENSE)
@@ -0,0 +1,67 @@
1
+ interface StepOptions {
2
+ /** Box the step in the report so errors point to the call site. Defaults to false. */
3
+ box?: boolean;
4
+ /** Maximum time in milliseconds for the step to finish. */
5
+ timeout?: number;
6
+ /**
7
+ * Skip humanization — use the raw "ClassName.methodName" format as the step title,
8
+ * with no parameter rendering. Defaults to false.
9
+ */
10
+ noHumanizer?: boolean;
11
+ }
12
+
13
+ type AnyMethod = (this: any, ...args: unknown[]) => unknown;
14
+ type StepDecorator = (target: AnyMethod, context: ClassMethodDecoratorContext) => AnyMethod;
15
+ /**
16
+ * Decorator that wraps a Page Object Model method in a `test.step()` call,
17
+ * automatically generating a human-readable step title.
18
+ *
19
+ * @example Bare usage — humanizes class + method name + runtime params
20
+ * ```ts
21
+ * class LoginPage {
22
+ * @step
23
+ * async login(username: string, password: string) { ... }
24
+ * }
25
+ * // → 'Login page: login using username "admin" and password "secret"'
26
+ * ```
27
+ *
28
+ * @example Custom step name — params are still appended
29
+ * ```ts
30
+ * class CheckoutPage {
31
+ * @step('Proceed to payment')
32
+ * async submitOrder(orderId: string) { ... }
33
+ * }
34
+ * // → 'Proceed to payment using order id "ord_99"'
35
+ * ```
36
+ *
37
+ * @example Options only — humanizes with box + timeout
38
+ * ```ts
39
+ * class ProfilePage {
40
+ * @step({ box: true, timeout: 10_000 })
41
+ * async updateAvatar(file: string) { ... }
42
+ * }
43
+ * ```
44
+ *
45
+ * @example Custom name + options
46
+ * ```ts
47
+ * class CartPage {
48
+ * @step('Add item to cart', { box: true })
49
+ * async addItem(product: Product) { ... }
50
+ * }
51
+ * ```
52
+ *
53
+ * @example Disable humanizer — raw "ClassName.methodName", no params
54
+ * ```ts
55
+ * class AdminPage {
56
+ * @step({ noHumanizer: true })
57
+ * async deleteUser(userId: string) { ... }
58
+ * }
59
+ * // → 'AdminPage.deleteUser'
60
+ * ```
61
+ */
62
+ declare function step(target: AnyMethod, context: ClassMethodDecoratorContext): AnyMethod;
63
+ declare function step(name: string, options?: StepOptions): StepDecorator;
64
+ declare function step(options: StepOptions): StepDecorator;
65
+ declare function step(): StepDecorator;
66
+
67
+ export { type StepOptions, step };
@@ -0,0 +1,67 @@
1
+ interface StepOptions {
2
+ /** Box the step in the report so errors point to the call site. Defaults to false. */
3
+ box?: boolean;
4
+ /** Maximum time in milliseconds for the step to finish. */
5
+ timeout?: number;
6
+ /**
7
+ * Skip humanization — use the raw "ClassName.methodName" format as the step title,
8
+ * with no parameter rendering. Defaults to false.
9
+ */
10
+ noHumanizer?: boolean;
11
+ }
12
+
13
+ type AnyMethod = (this: any, ...args: unknown[]) => unknown;
14
+ type StepDecorator = (target: AnyMethod, context: ClassMethodDecoratorContext) => AnyMethod;
15
+ /**
16
+ * Decorator that wraps a Page Object Model method in a `test.step()` call,
17
+ * automatically generating a human-readable step title.
18
+ *
19
+ * @example Bare usage — humanizes class + method name + runtime params
20
+ * ```ts
21
+ * class LoginPage {
22
+ * @step
23
+ * async login(username: string, password: string) { ... }
24
+ * }
25
+ * // → 'Login page: login using username "admin" and password "secret"'
26
+ * ```
27
+ *
28
+ * @example Custom step name — params are still appended
29
+ * ```ts
30
+ * class CheckoutPage {
31
+ * @step('Proceed to payment')
32
+ * async submitOrder(orderId: string) { ... }
33
+ * }
34
+ * // → 'Proceed to payment using order id "ord_99"'
35
+ * ```
36
+ *
37
+ * @example Options only — humanizes with box + timeout
38
+ * ```ts
39
+ * class ProfilePage {
40
+ * @step({ box: true, timeout: 10_000 })
41
+ * async updateAvatar(file: string) { ... }
42
+ * }
43
+ * ```
44
+ *
45
+ * @example Custom name + options
46
+ * ```ts
47
+ * class CartPage {
48
+ * @step('Add item to cart', { box: true })
49
+ * async addItem(product: Product) { ... }
50
+ * }
51
+ * ```
52
+ *
53
+ * @example Disable humanizer — raw "ClassName.methodName", no params
54
+ * ```ts
55
+ * class AdminPage {
56
+ * @step({ noHumanizer: true })
57
+ * async deleteUser(userId: string) { ... }
58
+ * }
59
+ * // → 'AdminPage.deleteUser'
60
+ * ```
61
+ */
62
+ declare function step(target: AnyMethod, context: ClassMethodDecoratorContext): AnyMethod;
63
+ declare function step(name: string, options?: StepOptions): StepDecorator;
64
+ declare function step(options: StepOptions): StepDecorator;
65
+ declare function step(): StepDecorator;
66
+
67
+ export { type StepOptions, step };
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/decorator/index.ts
21
+ var decorator_exports = {};
22
+ __export(decorator_exports, {
23
+ step: () => step
24
+ });
25
+ module.exports = __toCommonJS(decorator_exports);
26
+ var import_test = require("@playwright/test");
27
+
28
+ // src/decorator/humanizer.ts
29
+ var import_string = require("@alduino/humanizer/string");
30
+ function scoreParam(value) {
31
+ if (value === null || value === void 0) return 0;
32
+ if (typeof value === "boolean") return 1;
33
+ if (Array.isArray(value) || typeof value === "object") return 2;
34
+ return 3;
35
+ }
36
+ function renderParamValue(name, value) {
37
+ const humanName = (0, import_string.humanize)(name).toLowerCase();
38
+ if (value === null || value === void 0) {
39
+ return `${humanName}: not provided`;
40
+ }
41
+ if (typeof value === "boolean") {
42
+ return value ? humanName : `not ${humanName}`;
43
+ }
44
+ if (typeof value === "string") {
45
+ return value === "" ? `${humanName}: empty` : `${humanName} "${value}"`;
46
+ }
47
+ if (typeof value === "number") {
48
+ return `${humanName} ${value}`;
49
+ }
50
+ if (Array.isArray(value)) {
51
+ if (value.length === 0) return `no ${humanName}`;
52
+ if (value.length <= 3) return `${humanName} [${value.map((v) => JSON.stringify(v)).join(", ")}]`;
53
+ return `${value.length} ${humanName}`;
54
+ }
55
+ const keys = Object.keys(value);
56
+ if (keys.length === 0) return `empty ${humanName}`;
57
+ if (keys.length <= 2) {
58
+ const entries = keys.map((k) => `${k}: ${JSON.stringify(value[k])}`).join(", ");
59
+ return `${humanName} {${entries}}`;
60
+ }
61
+ return `${humanName} with ${keys.length} fields`;
62
+ }
63
+ function joinParts(parts) {
64
+ if (parts.length === 0) return "";
65
+ if (parts.length === 1) return parts[0];
66
+ return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
67
+ }
68
+ function buildParamString(params) {
69
+ if (params.length === 0) return "";
70
+ let top;
71
+ let remaining = 0;
72
+ if (params.length <= 3) {
73
+ top = params;
74
+ } else {
75
+ const scored = params.map((p, originalIndex) => ({ ...p, score: scoreParam(p.value), originalIndex }));
76
+ scored.sort((a, b) => {
77
+ const scoreDiff = b.score - a.score;
78
+ return scoreDiff !== 0 ? scoreDiff : a.originalIndex - b.originalIndex;
79
+ });
80
+ top = scored.slice(0, 3);
81
+ remaining = params.length - 3;
82
+ }
83
+ const parts = top.map((p) => renderParamValue(p.name, p.value));
84
+ const joined = joinParts(parts);
85
+ if (remaining > 0) {
86
+ return `${joined} and ${remaining} more option${remaining === 1 ? "" : "s"}`;
87
+ }
88
+ return joined;
89
+ }
90
+ function buildHumanStepTitle(className, methodName, params) {
91
+ const humanClass = (0, import_string.humanize)(className);
92
+ const humanMethod = (0, import_string.humanize)(methodName).toLowerCase();
93
+ const base = `${humanClass}: ${humanMethod}`;
94
+ const paramStr = buildParamString(params);
95
+ return paramStr ? `${base} using ${paramStr}` : base;
96
+ }
97
+ function buildCustomStepTitle(customName, params) {
98
+ const paramStr = buildParamString(params);
99
+ return paramStr ? `${customName} using ${paramStr}` : customName;
100
+ }
101
+ function extractParamNames(fn) {
102
+ const fnStr = fn.toString();
103
+ const match = fnStr.match(/\(([^)]*)\)/);
104
+ if (!match || !match[1].trim()) return [];
105
+ return match[1].split(",").map((param) => {
106
+ const name = param.trim().replace(/^\.\.\./, "").split(/[\s=]/)[0].trim();
107
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : null;
108
+ }).filter((name) => name !== null && name.length > 0);
109
+ }
110
+
111
+ // src/decorator/index.ts
112
+ function isDecoratorContext(v) {
113
+ return typeof v === "object" && v !== null && v.kind === "method";
114
+ }
115
+ function isStepOptions(v) {
116
+ if (typeof v !== "object" || v === null) return false;
117
+ const allowedKeys = /* @__PURE__ */ new Set(["box", "timeout", "noHumanizer"]);
118
+ return Object.keys(v).every((k) => allowedKeys.has(k));
119
+ }
120
+ function applyStep(target, context, customName, opts) {
121
+ const paramNames = extractParamNames(target);
122
+ return function(...args) {
123
+ let stepTitle;
124
+ if (opts.noHumanizer) {
125
+ stepTitle = `${this.constructor.name}.${String(context.name)}`;
126
+ } else {
127
+ const params = paramNames.map((name, i) => ({ name, value: args[i] }));
128
+ if (customName !== void 0) {
129
+ stepTitle = buildCustomStepTitle(customName, params);
130
+ } else {
131
+ stepTitle = buildHumanStepTitle(
132
+ this.constructor.name,
133
+ String(context.name),
134
+ params
135
+ );
136
+ }
137
+ }
138
+ return import_test.test.step(stepTitle, () => target.call(this, ...args), {
139
+ box: opts.box,
140
+ timeout: opts.timeout
141
+ });
142
+ };
143
+ }
144
+ function step(nameOrOptionsOrTarget, contextOrOptions) {
145
+ if (typeof nameOrOptionsOrTarget === "function" && isDecoratorContext(contextOrOptions)) {
146
+ return applyStep(nameOrOptionsOrTarget, contextOrOptions, void 0, {});
147
+ }
148
+ const customName = typeof nameOrOptionsOrTarget === "string" ? nameOrOptionsOrTarget : void 0;
149
+ const opts = typeof nameOrOptionsOrTarget === "object" && nameOrOptionsOrTarget !== null && isStepOptions(nameOrOptionsOrTarget) ? nameOrOptionsOrTarget : isStepOptions(contextOrOptions) ? contextOrOptions : {};
150
+ return (target, context) => applyStep(target, context, customName, opts);
151
+ }
152
+ // Annotate the CommonJS export names for ESM import in node:
153
+ 0 && (module.exports = {
154
+ step
155
+ });
156
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/decorator/index.ts","../../src/decorator/humanizer.ts"],"sourcesContent":["import { test } from '@playwright/test';\nimport {\n buildCustomStepTitle,\n buildHumanStepTitle,\n extractParamNames,\n type ParamDescriptor,\n} from './humanizer.js';\nimport type { StepOptions } from './types.js';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyMethod = (this: any, ...args: unknown[]) => unknown;\ntype StepDecorator = (target: AnyMethod, context: ClassMethodDecoratorContext) => AnyMethod;\n\nfunction isDecoratorContext(v: unknown): v is ClassMethodDecoratorContext {\n return typeof v === 'object' && v !== null && (v as ClassMethodDecoratorContext).kind === 'method';\n}\n\nfunction isStepOptions(v: unknown): v is StepOptions {\n if (typeof v !== 'object' || v === null) return false;\n const allowedKeys = new Set(['box', 'timeout', 'noHumanizer']);\n return Object.keys(v).every(k => allowedKeys.has(k));\n}\n\nfunction applyStep(\n target: AnyMethod,\n context: ClassMethodDecoratorContext,\n customName: string | undefined,\n opts: StepOptions\n): AnyMethod {\n const paramNames = extractParamNames(target);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return function (this: any, ...args: unknown[]): unknown {\n let stepTitle: string;\n\n if (opts.noHumanizer) {\n stepTitle = `${this.constructor.name}.${String(context.name)}`;\n } else {\n const params: ParamDescriptor[] = paramNames.map((name, i) => ({ name, value: args[i] }));\n\n if (customName !== undefined) {\n stepTitle = buildCustomStepTitle(customName, params);\n } else {\n stepTitle = buildHumanStepTitle(\n this.constructor.name,\n String(context.name),\n params\n );\n }\n }\n\n return test.step(stepTitle, () => target.call(this, ...args), {\n box: opts.box,\n timeout: opts.timeout,\n });\n };\n}\n\n/**\n * Decorator that wraps a Page Object Model method in a `test.step()` call,\n * automatically generating a human-readable step title.\n *\n * @example Bare usage — humanizes class + method name + runtime params\n * ```ts\n * class LoginPage {\n * @step\n * async login(username: string, password: string) { ... }\n * }\n * // → 'Login page: login using username \"admin\" and password \"secret\"'\n * ```\n *\n * @example Custom step name — params are still appended\n * ```ts\n * class CheckoutPage {\n * @step('Proceed to payment')\n * async submitOrder(orderId: string) { ... }\n * }\n * // → 'Proceed to payment using order id \"ord_99\"'\n * ```\n *\n * @example Options only — humanizes with box + timeout\n * ```ts\n * class ProfilePage {\n * @step({ box: true, timeout: 10_000 })\n * async updateAvatar(file: string) { ... }\n * }\n * ```\n *\n * @example Custom name + options\n * ```ts\n * class CartPage {\n * @step('Add item to cart', { box: true })\n * async addItem(product: Product) { ... }\n * }\n * ```\n *\n * @example Disable humanizer — raw \"ClassName.methodName\", no params\n * ```ts\n * class AdminPage {\n * @step({ noHumanizer: true })\n * async deleteUser(userId: string) { ... }\n * }\n * // → 'AdminPage.deleteUser'\n * ```\n */\nexport function step(target: AnyMethod, context: ClassMethodDecoratorContext): AnyMethod;\nexport function step(name: string, options?: StepOptions): StepDecorator;\nexport function step(options: StepOptions): StepDecorator;\nexport function step(): StepDecorator;\nexport function step(\n nameOrOptionsOrTarget?: string | StepOptions | AnyMethod,\n contextOrOptions?: ClassMethodDecoratorContext | StepOptions\n): AnyMethod | StepDecorator {\n // Case 1: @step (bare, no parentheses)\n if (typeof nameOrOptionsOrTarget === 'function' && isDecoratorContext(contextOrOptions)) {\n return applyStep(nameOrOptionsOrTarget, contextOrOptions, undefined, {});\n }\n\n // Cases 2–4: @step(...) factory\n const customName =\n typeof nameOrOptionsOrTarget === 'string' ? nameOrOptionsOrTarget : undefined;\n\n const opts: StepOptions =\n typeof nameOrOptionsOrTarget === 'object' && nameOrOptionsOrTarget !== null && isStepOptions(nameOrOptionsOrTarget)\n ? nameOrOptionsOrTarget\n : isStepOptions(contextOrOptions)\n ? (contextOrOptions as StepOptions)\n : {};\n\n return (target: AnyMethod, context: ClassMethodDecoratorContext): AnyMethod =>\n applyStep(target, context, customName, opts);\n}\n\nexport type { StepOptions } from './types.js';\n","import { humanize } from '@alduino/humanizer/string';\n\nexport interface ParamDescriptor {\n name: string;\n value: unknown;\n}\n\nfunction scoreParam(value: unknown): number {\n if (value === null || value === undefined) return 0;\n if (typeof value === 'boolean') return 1;\n if (Array.isArray(value) || (typeof value === 'object')) return 2;\n return 3; // string or number\n}\n\nfunction renderParamValue(name: string, value: unknown): string {\n const humanName = humanize(name).toLowerCase();\n\n if (value === null || value === undefined) {\n return `${humanName}: not provided`;\n }\n if (typeof value === 'boolean') {\n return value ? humanName : `not ${humanName}`;\n }\n if (typeof value === 'string') {\n return value === '' ? `${humanName}: empty` : `${humanName} \"${value}\"`;\n }\n if (typeof value === 'number') {\n return `${humanName} ${value}`;\n }\n if (Array.isArray(value)) {\n if (value.length === 0) return `no ${humanName}`;\n if (value.length <= 3) return `${humanName} [${value.map(v => JSON.stringify(v)).join(', ')}]`;\n return `${value.length} ${humanName}`;\n }\n // object\n const keys = Object.keys(value as object);\n if (keys.length === 0) return `empty ${humanName}`;\n if (keys.length <= 2) {\n const entries = keys.map(k => `${k}: ${JSON.stringify((value as Record<string, unknown>)[k])}`).join(', ');\n return `${humanName} {${entries}}`;\n }\n return `${humanName} with ${keys.length} fields`;\n}\n\nfunction joinParts(parts: string[]): string {\n if (parts.length === 0) return '';\n if (parts.length === 1) return parts[0];\n return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;\n}\n\nexport function buildParamString(params: ParamDescriptor[]): string {\n if (params.length === 0) return '';\n\n let top: ParamDescriptor[];\n let remaining = 0;\n\n if (params.length <= 3) {\n // All fit — preserve original order\n top = params;\n } else {\n // Select top 3 by significance score, then by original position\n const scored = params.map((p, originalIndex) => ({ ...p, score: scoreParam(p.value), originalIndex }));\n scored.sort((a, b) => {\n const scoreDiff = b.score - a.score;\n return scoreDiff !== 0 ? scoreDiff : a.originalIndex - b.originalIndex;\n });\n top = scored.slice(0, 3);\n remaining = params.length - 3;\n }\n\n const parts = top.map(p => renderParamValue(p.name, p.value));\n const joined = joinParts(parts);\n\n if (remaining > 0) {\n return `${joined} and ${remaining} more option${remaining === 1 ? '' : 's'}`;\n }\n return joined;\n}\n\n/**\n * Builds a human-readable step title from a class name, method name, and runtime parameters.\n *\n * Format: \"<Humanized class>: <humanized method> using <params>\"\n *\n * @example\n * buildHumanStepTitle('LoginPage', 'fillCredentials', [])\n * // → \"Login page: fill credentials\"\n *\n * @example\n * buildHumanStepTitle('LoginPage', 'login', [\n * { name: 'username', value: 'admin@test.com' },\n * { name: 'password', value: 'secret' },\n * ])\n * // → 'Login page: login using username \"admin@test.com\" and password \"secret\"'\n */\nexport function buildHumanStepTitle(\n className: string,\n methodName: string,\n params: ParamDescriptor[]\n): string {\n const humanClass = humanize(className);\n const humanMethod = humanize(methodName).toLowerCase();\n const base = `${humanClass}: ${humanMethod}`;\n\n const paramStr = buildParamString(params);\n return paramStr ? `${base} using ${paramStr}` : base;\n}\n\n/**\n * Builds a human-readable step title from a custom name and runtime parameters.\n *\n * Format: \"<custom name> using <params>\"\n *\n * @example\n * buildCustomStepTitle('Proceed to payment', [\n * { name: 'orderId', value: 'ord_99' },\n * { name: 'retry', value: false },\n * ])\n * // → 'Proceed to payment using order id \"ord_99\" and not retry'\n */\nexport function buildCustomStepTitle(customName: string, params: ParamDescriptor[]): string {\n const paramStr = buildParamString(params);\n return paramStr ? `${customName} using ${paramStr}` : customName;\n}\n\n/**\n * Extracts parameter names from a function's source code via toString().\n * Handles camelCase, default values and rest params; skips destructured params.\n */\nexport function extractParamNames(fn: (...args: unknown[]) => unknown): string[] {\n const fnStr = fn.toString();\n const match = fnStr.match(/\\(([^)]*)\\)/);\n if (!match || !match[1].trim()) return [];\n\n return match[1]\n .split(',')\n .map(param => {\n const name = param\n .trim()\n .replace(/^\\.\\.\\./, '') // rest params\n .split(/[\\s=]/)[0] // default values\n .trim();\n // Skip destructured params ({ ... } or [ ... ])\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : null;\n })\n .filter((name): name is string => name !== null && name.length > 0);\n}\n"],"mappings":"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","names":[]}