@immense/vue-pom-generator 1.0.13 → 1.0.14

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/RELEASE_NOTES.md CHANGED
@@ -1,27 +1,27 @@
1
- ## Highlights
1
+ ```markdown
2
+ # v1.0.14
2
3
 
3
- - Fixed path-based POM naming compatibility with Nuxt 4
4
- - Added support for dynamic `test-id` input elements in class generation
5
- - Introduced dedicated path utilities module for improved modularity
6
- - Enhanced test coverage with new component naming test suite
7
- - Added automated PR release notes preview comments
4
+ ## Highlights
8
5
 
9
- ## Changes
6
+ - **Animated cursor overlay** for Playwright recordings with enhanced visual feedback
7
+ - Added comprehensive PomElement wrapper design and implementation documentation
8
+ - Enhanced Pointer component with 249 lines of improvements
9
+ - Automated PR release notes preview comments via GitHub Actions
10
10
 
11
- **Fixes**
12
- - Path-based POM naming now works correctly with Nuxt 4
13
- - Dynamic `test-id` input elements are properly handled in class generation
11
+ ## Changes
14
12
 
15
- **Refactoring**
16
- - Extracted path utilities into dedicated `plugin/path-utils.ts` module
17
- - Simplified `plugin/vue-plugin.ts` by removing duplicated path logic
13
+ ### Features
14
+ - Animated cursor overlay for Playwright recordings to improve test visualization
15
+ - Enhanced `class-generation/Pointer.ts` with significant improvements (249 lines)
18
16
 
19
- **Testing**
20
- - Added `tests/component-naming.test.ts` with 93 new lines of test coverage
21
- - Enhanced `tests/class-generation-coverage.test.ts` with additional cases
17
+ ### Documentation
18
+ - Added PomElement wrapper design document
19
+ (`docs/plans/2026-03-03-pom-element-wrapper-design.md`)
20
+ - Added PomElement wrapper implementation plan
21
+ (`docs/plans/2026-03-03-pom-element-wrapper-implementation.md`)
22
22
 
23
- **CI/Workflow**
24
- - Automated release notes preview comments on pull requests
23
+ ### Automation
24
+ - PR release notes preview comments automatically generated on pull requests
25
25
 
26
26
  ## Breaking Changes
27
27
 
@@ -34,6 +34,6 @@
34
34
 
35
35
  ## Testing
36
36
 
37
- Comprehensive test coverage added for component naming and class generation edge cases. All
38
- existing tests pass.
37
+ Testing details not provided in release window.
38
+ ```
39
39
 
@@ -1,39 +1,138 @@
1
1
  import type { PwLocator, PwPage } from "./playwright-types";
2
2
 
3
+ // ---------------------------------------------------------------------------
4
+ // Cursor visual overlay helpers
5
+ // ---------------------------------------------------------------------------
6
+
7
+ const __PW_CURSOR_ID__ = "__pw_cursor__";
8
+
9
+ // A minimal 16×24 arrow cursor encoded as a base64 PNG.
10
+ const __PW_CURSOR_PNG__ =
11
+ "data:image/png;base64,"
12
+ + "iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAQAAACGG/bgAAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAA"
13
+ + "HsYAAB7GAZEt8iwAAAAHdElNRQfgAwgMIwdxU/i7AAABZklEQVQ4y43TsU4UURSH8W+XmYwkS2I0"
14
+ + "9CRKpKGhsvIJjG9giQmliHFZlkUIGnEF7KTiCagpsYHWhoTQaiUUxLixYZb5KAAZZhbunu7O/PKf"
15
+ + "e+fcA+/pqwb4DuximEqXhT4iI8dMpBWEsWsuGYdpZFttiLSSgTvhZ1W/SvfO1CvYdV1kPghV68a3"
16
+ + "0zzUWZH5pBqEui7dnqlFmLoq0gxC1XfGZdoLal2kea8ahLoqKXNAJQBT2yJzwUTVt0bS6ANqy1ga"
17
+ + "VCEq/oVTtjji4hQVhhnlYBH4WIJV9vlkXLm+10R8oJb79Jl1j9UdazJRGpkrmNkSF9SOz2T71s7M"
18
+ + "SIfD2lmmfjGSRz3hK8l4w1P+bah/HJLN0sys2JSMZQB+jKo6KSc8vLlLn5ikzF4268Wg2+pPOWW6"
19
+ + "ONcpr3PrXy9VfS473M/D7H+TLmrqsXtOGctvxvMv2oVNP+Av0uHbzbxyJaywyUjx8TlnPY2YxqkD"
20
+ + "dAAAAABJRU5ErkJggg==";
21
+
22
+ // Per-page cursor position (viewport coords). WeakMap so pages can be GC'd.
23
+ const __pw_cursor_positions__ = new WeakMap<object, { x: number; y: number }>();
24
+
25
+ function __pw_get_cursor_pos__(page: PwPage): { x: number; y: number } {
26
+ return __pw_cursor_positions__.get(page as object) ?? { x: 0, y: 0 };
27
+ }
28
+
29
+ function __pw_set_cursor_pos__(page: PwPage, x: number, y: number): void {
30
+ __pw_cursor_positions__.set(page as object, { x, y });
31
+ }
32
+
33
+ async function __pw_ensure_cursor__(page: PwPage): Promise<void> {
34
+ const exists = await page.evaluate(
35
+ (id: string) => document.getElementById(id) != null,
36
+ __PW_CURSOR_ID__,
37
+ );
38
+ if (exists) return;
39
+
40
+ // Reset tracked position for this page.
41
+ __pw_set_cursor_pos__(page, 0, 0);
42
+
43
+ await page.evaluate(
44
+ ({ id, src }: { id: string; src: string }) => {
45
+ const img = document.createElement("img");
46
+ img.setAttribute("src", src);
47
+ img.setAttribute("id", id);
48
+ // position:fixed keeps coordinates viewport-relative (matching Playwright boundingBox).
49
+ img.setAttribute(
50
+ "style",
51
+ "position:fixed;z-index:2147483647;pointer-events:none;left:0;top:0;transform-origin:0 0;",
52
+ );
53
+ document.body.appendChild(img);
54
+ },
55
+ { id: __PW_CURSOR_ID__, src: __PW_CURSOR_PNG__ },
56
+ );
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Animation options
61
+ // ---------------------------------------------------------------------------
62
+
3
63
  export interface PlaywrightAnimationOptions {
4
64
  /**
5
- * When false, cursor animations are disabled (but clicks/fills still happen).
6
- *
65
+ * Set to false to disable all animations and delays. Clicks/fills still happen.
7
66
  * Default: true
8
67
  */
9
68
  enabled?: boolean;
10
69
 
11
70
  /**
12
- * Extra delay in ms before performing the action.
13
- *
71
+ * Extra delay (ms) added before every action on top of per-action delays.
14
72
  * Default: 0
15
73
  */
16
74
  extraDelayMs?: number;
75
+
76
+ /** Visual cursor / pointer-movement configuration. */
77
+ pointer?: {
78
+ /**
79
+ * Duration of the CSS-transition cursor glide in ms.
80
+ * Set to 0 to teleport the cursor without animation.
81
+ * Default: 250
82
+ */
83
+ durationMilliseconds?: number;
84
+
85
+ /**
86
+ * CSS transition timing function for the cursor glide.
87
+ * Default: "ease-in-out"
88
+ */
89
+ transitionStyle?: "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out";
90
+
91
+ /**
92
+ * Delay (ms) passed to element.click({ delay }) for a realistic press.
93
+ * Default: 0
94
+ */
95
+ clickDelayMilliseconds?: number;
96
+ };
97
+
98
+ /** Keyboard / typing configuration. */
99
+ keyboard?: {
100
+ /**
101
+ * Delay between keystrokes in ms – makes typing visible on screen / in video.
102
+ * Default: 100
103
+ */
104
+ typeDelayMilliseconds?: number;
105
+ };
17
106
  }
18
107
 
19
- let animationOptions: PlaywrightAnimationOptions = { enabled: true, extraDelayMs: 0 };
108
+ let animationOptions: PlaywrightAnimationOptions = {
109
+ enabled: true,
110
+ extraDelayMs: 0,
111
+ pointer: { durationMilliseconds: 250, transitionStyle: "ease-in-out", clickDelayMilliseconds: 0 },
112
+ keyboard: { typeDelayMilliseconds: 100 },
113
+ };
20
114
 
21
115
  export function setPlaywrightAnimationOptions(options: PlaywrightAnimationOptions): void {
22
116
  animationOptions = {
23
117
  enabled: options?.enabled ?? true,
24
118
  extraDelayMs: options?.extraDelayMs ?? 0,
119
+ pointer: {
120
+ durationMilliseconds: options?.pointer?.durationMilliseconds ?? 250,
121
+ transitionStyle: options?.pointer?.transitionStyle ?? "ease-in-out",
122
+ clickDelayMilliseconds: options?.pointer?.clickDelayMilliseconds ?? 0,
123
+ },
124
+ keyboard: {
125
+ typeDelayMilliseconds: options?.keyboard?.typeDelayMilliseconds ?? 100,
126
+ },
25
127
  };
26
128
  }
27
129
 
28
130
  export interface AfterPointerClickInfo {
29
- /**
30
- * Resolved test id from the clicked element (if present).
31
- */
131
+ /** Resolved test id from the clicked element (if present). */
32
132
  testId?: string;
33
133
 
34
134
  /**
35
- * Whether the click should be considered instrumented”.
36
- *
135
+ * Whether the click should be considered "instrumented".
37
136
  * BasePage uses this flag to decide whether to wait for the injected click event.
38
137
  */
39
138
  instrumented: boolean;
@@ -43,6 +142,10 @@ export type AfterPointerClick = (info: AfterPointerClickInfo) => void | Promise<
43
142
 
44
143
  type ElementTarget = string | PwLocator;
45
144
 
145
+ // ---------------------------------------------------------------------------
146
+ // Pointer class
147
+ // ---------------------------------------------------------------------------
148
+
46
149
  export class Pointer {
47
150
  private readonly page: PwPage;
48
151
  private readonly testIdAttribute: string;
@@ -73,35 +176,118 @@ export class Pointer {
73
176
  ): Promise<void> {
74
177
  const locator = this.toLocator(target);
75
178
 
76
- // Best-effort “animation”: make sure the element is scrolled into view and add a delay.
77
179
  try {
78
180
  await locator.first().scrollIntoViewIfNeeded();
79
181
  }
80
182
  catch {
81
- // If the element detaches during navigation, let the subsequent click/fill surface the error.
183
+ // Element may detach during navigation; let the subsequent action surface the error.
82
184
  }
83
185
 
84
- const totalDelay = Math.max(0, delayMs) + Math.max(0, animationOptions.extraDelayMs ?? 0);
85
- if (animationOptions.enabled !== false && totalDelay > 0) {
86
- await this.page.waitForTimeout(totalDelay);
186
+ const opts = animationOptions;
187
+ const animEnabled = opts.enabled !== false;
188
+
189
+ if (!animEnabled) {
190
+ // Fast path: no animations.
191
+ const extraDelay = Math.max(0, opts.extraDelayMs ?? 0);
192
+ if (extraDelay > 0) await this.page.waitForTimeout(extraDelay);
193
+
194
+ let clickedTestId: string | undefined;
195
+ if (executeClick) {
196
+ try { clickedTestId = await this.getTestId(locator); } catch { /* noop */ }
197
+ await locator.first().click({ force: true });
198
+ }
199
+ if (options?.afterClick) {
200
+ await options.afterClick({ testId: clickedTestId, instrumented: Boolean(clickedTestId) });
201
+ }
202
+ return;
87
203
  }
88
204
 
205
+ // --- Animated path ---
206
+ const moveDurationMs = opts.pointer?.durationMilliseconds ?? 250;
207
+ const transitionStyle = opts.pointer?.transitionStyle ?? "ease-in-out";
208
+ const clickDelayMs = opts.pointer?.clickDelayMilliseconds ?? 0;
209
+ const extraDelayMs = Math.max(0, opts.extraDelayMs ?? 0);
210
+ const actionDelayMs = Math.max(0, delayMs);
211
+
212
+ // Inject the visual cursor if it doesn't exist yet.
213
+ await __pw_ensure_cursor__(this.page);
214
+
215
+ // Move the cursor to the target element.
216
+ const box = await locator.first().boundingBox();
217
+ if (box) {
218
+ const endX = box.x + box.width / 2;
219
+ const endY = box.y + box.height / 2;
220
+ const { x: startX, y: startY } = __pw_get_cursor_pos__(this.page);
221
+ const distance = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2);
222
+
223
+ if (moveDurationMs > 0 && distance > 0) {
224
+ // Glide the cursor image using a CSS transition.
225
+ await this.page.evaluate(
226
+ ({ id, sx, sy, ex, ey, dur, style }: {
227
+ id: string; sx: number; sy: number; ex: number; ey: number; dur: number; style: string;
228
+ }) => {
229
+ const el = document.getElementById(id);
230
+ if (!el) return;
231
+ el.style.transition = "";
232
+ el.style.willChange = "left, top";
233
+ el.style.left = `${sx}px`;
234
+ el.style.top = `${sy}px`;
235
+ // Force reflow so the browser registers the start position before transitioning.
236
+ void el.offsetWidth;
237
+ el.style.transition = `left ${dur}ms ${style}, top ${dur}ms ${style}`;
238
+ el.style.left = `${ex}px`;
239
+ el.style.top = `${ey}px`;
240
+ },
241
+ { id: __PW_CURSOR_ID__, sx: startX, sy: startY, ex: endX, ey: endY, dur: moveDurationMs, style: transitionStyle },
242
+ );
243
+ // Wait for the animation to finish.
244
+ await this.page.waitForTimeout(moveDurationMs + 25);
245
+ }
246
+ else {
247
+ // Teleport (distance 0 or duration 0).
248
+ await this.page.evaluate(
249
+ ({ id, x, y }: { id: string; x: number; y: number }) => {
250
+ const el = document.getElementById(id);
251
+ if (el) { el.style.left = `${x}px`; el.style.top = `${y}px`; }
252
+ },
253
+ { id: __PW_CURSOR_ID__, x: endX, y: endY },
254
+ );
255
+ }
256
+
257
+ __pw_set_cursor_pos__(this.page, endX, endY);
258
+ }
259
+
260
+ // Apply action delay + extra delay.
261
+ const totalDelay = actionDelayMs + extraDelayMs;
262
+ if (totalDelay > 0) await this.page.waitForTimeout(totalDelay);
263
+
89
264
  let clickedTestId: string | undefined;
90
265
  if (executeClick) {
91
- try {
92
- clickedTestId = await this.getTestId(locator);
266
+ // Brief scale-down "press" animation on the cursor image.
267
+ if (moveDurationMs > 0) {
268
+ const pressDur = Math.max(80, Math.round(moveDurationMs / 3));
269
+ await this.page.evaluate(
270
+ ({ id, dur }: { id: string; dur: number }) => {
271
+ const el = document.getElementById(id);
272
+ if (el) {
273
+ el.style.transition = `transform ${dur}ms`;
274
+ el.style.transform = "scale(0.6)";
275
+ setTimeout(() => {
276
+ el.style.transition = `transform ${dur}ms`;
277
+ el.style.transform = "scale(1)";
278
+ }, dur);
279
+ }
280
+ },
281
+ { id: __PW_CURSOR_ID__, dur: pressDur },
282
+ );
93
283
  }
94
- catch {
95
- clickedTestId = undefined;
96
- }
97
- await locator.first().click({ force: true });
284
+
285
+ try { clickedTestId = await this.getTestId(locator); } catch { /* noop */ }
286
+ await locator.first().click({ delay: clickDelayMs, force: true });
98
287
  }
99
288
 
100
289
  if (options?.afterClick) {
101
- await options.afterClick({
102
- testId: clickedTestId,
103
- instrumented: Boolean(clickedTestId),
104
- });
290
+ await options.afterClick({ testId: clickedTestId, instrumented: Boolean(clickedTestId) });
105
291
  }
106
292
  }
107
293
 
@@ -115,10 +301,19 @@ export class Pointer {
115
301
  afterClick?: AfterPointerClick;
116
302
  },
117
303
  ): Promise<void> {
118
- // Reuse the click flow so the afterClick callback observes the click.
304
+ // Animate cursor + click first.
119
305
  await this.animateCursorToElement(target, executeClick, delayMs, annotationText, options);
120
306
 
121
307
  const locator = this.toLocator(target);
122
- await locator.first().fill(text);
308
+ const typeDelayMs = animationOptions.keyboard?.typeDelayMilliseconds ?? 100;
309
+
310
+ if (animationOptions.enabled !== false && typeDelayMs > 0) {
311
+ // Clear existing content, then type character-by-character so keystrokes are visible.
312
+ await locator.first().clear();
313
+ await this.page.keyboard.type(text, { delay: typeDelayMs });
314
+ }
315
+ else {
316
+ await locator.first().fill(text);
317
+ }
123
318
  }
124
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immense/vue-pom-generator",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Injects data-testid attributes for all interactive elements and generates page object models for every page.",
5
5
  "type": "module",
6
6
  "repository": {