@immense/vue-pom-generator 1.0.12 → 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 +24 -13
- package/class-generation/Pointer.ts +222 -27
- package/class-generation/index.ts +19 -1
- package/dist/index.cjs +64 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +64 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
|
-
●
|
|
1
|
+
● ```markdown
|
|
2
|
+
# v1.0.14
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-
|
|
4
|
+
## Highlights
|
|
5
|
+
|
|
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
|
|
6
10
|
|
|
7
11
|
## Changes
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
13
|
+
### Features
|
|
14
|
+
- Animated cursor overlay for Playwright recordings to improve test visualization
|
|
15
|
+
- Enhanced `class-generation/Pointer.ts` with significant improvements (249 lines)
|
|
16
|
+
|
|
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`)
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
-
|
|
23
|
+
### Automation
|
|
24
|
+
- PR release notes preview comments automatically generated on pull requests
|
|
15
25
|
|
|
16
26
|
## Breaking Changes
|
|
17
27
|
|
|
18
|
-
None
|
|
28
|
+
None
|
|
19
29
|
|
|
20
30
|
## Pull Requests Included
|
|
21
31
|
|
|
22
|
-
-
|
|
23
|
-
|
|
32
|
+
- #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
|
|
33
|
+
by @dkattan
|
|
24
34
|
|
|
25
35
|
## Testing
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
Testing details not provided in release window.
|
|
38
|
+
```
|
|
28
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
|
-
*
|
|
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
|
|
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 = {
|
|
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
|
|
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
|
-
//
|
|
183
|
+
// Element may detach during navigation; let the subsequent action surface the error.
|
|
82
184
|
}
|
|
83
185
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
}
|
|
@@ -603,7 +603,25 @@ function generateAggregatedCSharpFiles(
|
|
|
603
603
|
const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
|
|
604
604
|
const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
|
|
605
605
|
const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
|
|
606
|
-
|
|
606
|
+
|
|
607
|
+
// Ensure all template variables referenced in formattedDataTestId (e.g. `${key}`)
|
|
608
|
+
// appear in the C# method signature. utils.ts may omit `key` for input/select
|
|
609
|
+
// elements even when the test ID is dynamic, causing CS0103 compile errors.
|
|
610
|
+
const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
|
|
611
|
+
const templateVars = templateVarMatches.map(m => m[1]);
|
|
612
|
+
const augmentedParams: Record<string, string> = { ...pom.params };
|
|
613
|
+
for (const v of templateVars) {
|
|
614
|
+
if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
|
|
615
|
+
augmentedParams[v] = "string";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Place template vars first so they precede text/value/annotationText.
|
|
619
|
+
const orderedParams: Record<string, string> = Object.fromEntries([
|
|
620
|
+
...templateVars.map(v => [v, augmentedParams[v]] as [string, string]),
|
|
621
|
+
...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k)),
|
|
622
|
+
]);
|
|
623
|
+
|
|
624
|
+
const { signature, argNames } = formatCSharpParams(orderedParams);
|
|
607
625
|
const args = argNames.join(", ");
|
|
608
626
|
|
|
609
627
|
const allTestIds = [pom.formattedDataTestId, ...(pom.alternateFormattedDataTestIds ?? [])]
|
package/dist/index.cjs
CHANGED
|
@@ -1992,18 +1992,18 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
|
|
|
1992
1992
|
case "input":
|
|
1993
1993
|
params.text = "string";
|
|
1994
1994
|
params.annotationText = 'string = ""';
|
|
1995
|
-
delete params.key;
|
|
1995
|
+
if (!isKeyed) delete params.key;
|
|
1996
1996
|
break;
|
|
1997
1997
|
case "select":
|
|
1998
1998
|
params.value = "string";
|
|
1999
1999
|
params.annotationText = 'string = ""';
|
|
2000
|
-
delete params.key;
|
|
2000
|
+
if (!isKeyed) delete params.key;
|
|
2001
2001
|
break;
|
|
2002
2002
|
case "vselect":
|
|
2003
2003
|
params.value = "string";
|
|
2004
2004
|
params.timeOut = "number = 500";
|
|
2005
2005
|
params.annotationText = 'string = ""';
|
|
2006
|
-
delete params.key;
|
|
2006
|
+
if (!isKeyed) delete params.key;
|
|
2007
2007
|
break;
|
|
2008
2008
|
case "radio":
|
|
2009
2009
|
params.annotationText = 'string = ""';
|
|
@@ -2071,11 +2071,11 @@ Fix: make the element identifiable (e.g. add id/name/inner text or use a more sp
|
|
|
2071
2071
|
}
|
|
2072
2072
|
switch (role) {
|
|
2073
2073
|
case "input":
|
|
2074
|
-
return { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
|
|
2074
|
+
return needsKey2 ? { params: `key: ${keyType}, text: string, annotationText: string = ""`, argNames: ["key", "text", "annotationText"] } : { params: 'text: string, annotationText: string = ""', argNames: ["text", "annotationText"] };
|
|
2075
2075
|
case "select":
|
|
2076
|
-
return { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
|
|
2076
|
+
return needsKey2 ? { params: `key: ${keyType}, value: string, annotationText: string = ""`, argNames: ["key", "value", "annotationText"] } : { params: 'value: string, annotationText: string = ""', argNames: ["value", "annotationText"] };
|
|
2077
2077
|
case "vselect":
|
|
2078
|
-
return { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
|
|
2078
|
+
return needsKey2 ? { params: `key: ${keyType}, value: string, timeOut = 500`, argNames: ["key", "value", "timeOut"] } : { params: "value: string, timeOut = 500", argNames: ["value", "timeOut"] };
|
|
2079
2079
|
case "radio":
|
|
2080
2080
|
return needsKey2 ? { params: `key: ${keyType}, annotationText: string = ""`, argNames: ["key", "annotationText"] } : { params: 'annotationText: string = ""', argNames: ["annotationText"] };
|
|
2081
2081
|
default:
|
|
@@ -3084,7 +3084,19 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
|
|
|
3084
3084
|
const baseGetterName = upperFirst(pom.getterNameOverride ?? pom.methodName);
|
|
3085
3085
|
const locatorName = baseGetterName.endsWith(roleSuffix) ? baseGetterName : `${baseGetterName}${roleSuffix}`;
|
|
3086
3086
|
const testIdExpr = toCSharpTestIdExpression(pom.formattedDataTestId);
|
|
3087
|
-
const
|
|
3087
|
+
const templateVarMatches = [...pom.formattedDataTestId.matchAll(/\$\{(\w+)\}/g)];
|
|
3088
|
+
const templateVars = templateVarMatches.map((m) => m[1]);
|
|
3089
|
+
const augmentedParams = { ...pom.params };
|
|
3090
|
+
for (const v of templateVars) {
|
|
3091
|
+
if (!Object.prototype.hasOwnProperty.call(augmentedParams, v)) {
|
|
3092
|
+
augmentedParams[v] = "string";
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
const orderedParams = Object.fromEntries([
|
|
3096
|
+
...templateVars.map((v) => [v, augmentedParams[v]]),
|
|
3097
|
+
...Object.entries(augmentedParams).filter(([k]) => !templateVars.includes(k))
|
|
3098
|
+
]);
|
|
3099
|
+
const { signature, argNames } = formatCSharpParams(orderedParams);
|
|
3088
3100
|
const args = argNames.join(", ");
|
|
3089
3101
|
const allTestIds = [pom.formattedDataTestId, ...pom.alternateFormattedDataTestIds ?? []].filter((v, idx, arr) => v && arr.indexOf(v) === idx);
|
|
3090
3102
|
if (pom.formattedDataTestId.includes("${")) {
|
|
@@ -4599,6 +4611,37 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4599
4611
|
}
|
|
4600
4612
|
};
|
|
4601
4613
|
}
|
|
4614
|
+
function resolveComponentNameFromPath(options) {
|
|
4615
|
+
const { projectRoot, viewsDirAbs, scanDirs, extraRoots = [] } = options;
|
|
4616
|
+
const cleanFilename = options.filename.includes("?") ? options.filename.substring(0, options.filename.indexOf("?")) : options.filename;
|
|
4617
|
+
const absFilename = path.isAbsolute(cleanFilename) ? cleanFilename : path.resolve(projectRoot, cleanFilename);
|
|
4618
|
+
const rootBases = [projectRoot, ...extraRoots.filter((r) => r !== projectRoot)];
|
|
4619
|
+
const roots = [viewsDirAbs, ...rootBases.flatMap((base) => scanDirs.map((d) => path.resolve(base, d)))];
|
|
4620
|
+
for (const base of rootBases) {
|
|
4621
|
+
for (const dir of scanDirs) {
|
|
4622
|
+
const absDir = path.resolve(base, dir);
|
|
4623
|
+
try {
|
|
4624
|
+
const pagesDir = path.join(absDir, "pages");
|
|
4625
|
+
if (fs.existsSync(pagesDir))
|
|
4626
|
+
roots.push(pagesDir);
|
|
4627
|
+
const componentsDir = path.join(absDir, "components");
|
|
4628
|
+
if (fs.existsSync(componentsDir))
|
|
4629
|
+
roots.push(componentsDir);
|
|
4630
|
+
} catch {
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(r)))).sort((a, b) => b.length - a.length);
|
|
4635
|
+
for (const root of potentialRoots) {
|
|
4636
|
+
if (absFilename.startsWith(root + path.sep) || absFilename === root) {
|
|
4637
|
+
const rel = path.relative(root, absFilename);
|
|
4638
|
+
const parsed = path.parse(rel);
|
|
4639
|
+
const segments = path.join(parsed.dir, parsed.name);
|
|
4640
|
+
return toPascalCase(segments);
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
return toPascalCase(path.parse(absFilename).name);
|
|
4644
|
+
}
|
|
4602
4645
|
function createDevProcessorPlugin(options) {
|
|
4603
4646
|
const {
|
|
4604
4647
|
nativeWrappers,
|
|
@@ -4717,7 +4760,13 @@ function createDevProcessorPlugin(options) {
|
|
|
4717
4760
|
const existing = filePathToComponentName.get(normalized);
|
|
4718
4761
|
if (existing)
|
|
4719
4762
|
return existing;
|
|
4720
|
-
const name =
|
|
4763
|
+
const name = resolveComponentNameFromPath({
|
|
4764
|
+
filename: normalized,
|
|
4765
|
+
projectRoot: projectRootRef.current,
|
|
4766
|
+
viewsDirAbs: getViewsDirAbs(),
|
|
4767
|
+
scanDirs,
|
|
4768
|
+
extraRoots: [process.cwd()]
|
|
4769
|
+
});
|
|
4721
4770
|
filePathToComponentName.set(normalized, name);
|
|
4722
4771
|
return name;
|
|
4723
4772
|
};
|
|
@@ -5202,41 +5251,13 @@ function createVuePluginWithTestIds(options) {
|
|
|
5202
5251
|
getProjectRoot
|
|
5203
5252
|
} = options;
|
|
5204
5253
|
const getComponentNameFromPath = (filename) => {
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
try {
|
|
5213
|
-
const pagesDir = path.join(absDir, "pages");
|
|
5214
|
-
if (fs.existsSync(pagesDir)) {
|
|
5215
|
-
roots.push(pagesDir);
|
|
5216
|
-
}
|
|
5217
|
-
const componentsDir = path.join(absDir, "components");
|
|
5218
|
-
if (fs.existsSync(componentsDir)) {
|
|
5219
|
-
roots.push(componentsDir);
|
|
5220
|
-
}
|
|
5221
|
-
} catch {
|
|
5222
|
-
}
|
|
5223
|
-
}
|
|
5224
|
-
const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(r)))).sort((a, b) => b.length - a.length);
|
|
5225
|
-
let componentName = "";
|
|
5226
|
-
for (const root of potentialRoots) {
|
|
5227
|
-
if (absFilename.startsWith(root + path.sep) || absFilename === root) {
|
|
5228
|
-
const rel = path.relative(root, absFilename);
|
|
5229
|
-
const parsed = path.parse(rel);
|
|
5230
|
-
const segments = path.join(parsed.dir, parsed.name);
|
|
5231
|
-
componentName = toPascalCase(segments);
|
|
5232
|
-
break;
|
|
5233
|
-
}
|
|
5234
|
-
}
|
|
5235
|
-
if (!componentName) {
|
|
5236
|
-
const parsed = path.parse(absFilename);
|
|
5237
|
-
componentName = toPascalCase(parsed.name);
|
|
5238
|
-
}
|
|
5239
|
-
return componentName;
|
|
5254
|
+
return resolveComponentNameFromPath({
|
|
5255
|
+
filename,
|
|
5256
|
+
projectRoot: getProjectRoot(),
|
|
5257
|
+
viewsDirAbs: getViewsDirAbs(),
|
|
5258
|
+
scanDirs,
|
|
5259
|
+
extraRoots: [process.cwd()]
|
|
5260
|
+
});
|
|
5240
5261
|
};
|
|
5241
5262
|
const isFileInScope = (filename) => {
|
|
5242
5263
|
if (!filename)
|