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 +21 -0
- package/README.md +235 -0
- package/dist/decorator/index.d.mts +67 -0
- package/dist/decorator/index.d.ts +67 -0
- package/dist/decorator/index.js +156 -0
- package/dist/decorator/index.js.map +1 -0
- package/dist/decorator/index.mjs +131 -0
- package/dist/decorator/index.mjs.map +1 -0
- package/dist/eslint-plugin/index.d.mts +19 -0
- package/dist/eslint-plugin/index.d.ts +19 -0
- package/dist/eslint-plugin/index.js +11831 -0
- package/dist/eslint-plugin/index.js.map +1 -0
- package/dist/eslint-plugin/index.mjs +11827 -0
- package/dist/eslint-plugin/index.mjs.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11965 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +11956 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
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":[]}
|