@immense/vue-pom-generator 1.0.43 → 1.0.44

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,23 +1,22 @@
1
1
  ● ## Highlights
2
2
 
3
- - Fixed dev-mode POM generation to achieve parity with build mode behavior
4
- - Added comprehensive regression test suite (284 lines) covering build–serve parity scenarios
5
- - Enhanced plugin system to ensure consistent POM generation across development and production
6
- environments
3
+ - Fixed handling of wrapped editable targets in class generation
4
+ - Enhanced Pointer module to correctly resolve targets through wrapper elements
5
+ - Added comprehensive test suite for pointer functionality (152 new test lines)
6
+ - Improved test coverage for class generation edge cases
7
7
 
8
8
  ## Changes
9
9
 
10
10
  **Bug Fixes**
11
- - Resolved discrepancies between dev-mode and build-mode POM generation (#5)
11
+ - Handle wrapped editable targets correctly in Pointer resolution logic
12
+ - Updated class generation index to support wrapped target elements
12
13
 
13
- **Testing & Quality**
14
- - Added build–serve parity regression tests to prevent future inconsistencies (#7)
15
- - New test file: `tests/build-serve-parity.test.ts` with comprehensive coverage
14
+ **Testing**
15
+ - Added new `tests/pointer.test.ts` with comprehensive pointer test coverage
16
+ - Expanded `tests/class-generation-coverage.test.ts` with 43 additional test lines
16
17
 
17
- **Plugin System**
18
- - Extended build plugin functionality with improved POM generation logic
19
- - Enhanced dev plugin to match build-mode behavior
20
- - Updated support plugins for better parity handling
18
+ **Type Updates**
19
+ - Minor update to `plugin/types.ts`
21
20
 
22
21
  ## Breaking Changes
23
22
 
@@ -25,13 +24,10 @@
25
24
 
26
25
  ## Pull Requests Included
27
26
 
28
- - [#7](https://github.com/immense/vue-pom-generator/pull/7) test: add build–serve parity
29
- regression tests (by @mayfieldiv)
30
- - [#5](https://github.com/immense/vue-pom-generator/pull/5) fix: dev-mode POM generation parity
31
- with build mode (by @mayfieldiv)
27
+ None this release contains direct commits only.
32
28
 
33
29
  ## Testing
34
30
 
35
- Added 284 lines of regression tests ensuring build–serve parity. Tests validate consistent POM
36
- generation behavior across development and production modes.
31
+ Significant test coverage added: new dedicated pointer test suite (152 lines) and expanded class
32
+ generation coverage tests (43 lines).
37
33
 
@@ -5,6 +5,8 @@ import type { PwLocator, PwPage } from "./playwright-types";
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
7
  const __PW_CURSOR_ID__ = "__pw_cursor__";
8
+ const __PW_EDITABLE_DESCENDANT_SELECTOR__
9
+ = "input, textarea, select, [contenteditable=''], [contenteditable='true'], [contenteditable]:not([contenteditable='false'])";
8
10
 
9
11
  // A minimal 16×24 arrow cursor encoded as a base64 PNG.
10
12
  const __PW_CURSOR_PNG__ =
@@ -165,6 +167,44 @@ export class Pointer {
165
167
  return trimmed || undefined;
166
168
  }
167
169
 
170
+ private async isEditableElement(locator: PwLocator): Promise<boolean> {
171
+ try {
172
+ return await locator.first().evaluate((element) => {
173
+ if (!(element instanceof HTMLElement)) {
174
+ return false;
175
+ }
176
+
177
+ const tagName = element.tagName.toLowerCase();
178
+ return tagName === "input"
179
+ || tagName === "textarea"
180
+ || tagName === "select"
181
+ || element.isContentEditable;
182
+ });
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ private async resolveEditableLocator(locator: PwLocator): Promise<PwLocator> {
190
+ const first = locator.first();
191
+ if (await this.isEditableElement(first)) {
192
+ return first;
193
+ }
194
+
195
+ const descendant = first.locator(__PW_EDITABLE_DESCENDANT_SELECTOR__).first();
196
+ try {
197
+ if (await descendant.count() > 0) {
198
+ return descendant;
199
+ }
200
+ }
201
+ catch {
202
+ // Fall back to the original target if descendant lookup fails.
203
+ }
204
+
205
+ return first;
206
+ }
207
+
168
208
  public async animateCursorToElement(
169
209
  target: ElementTarget,
170
210
  executeClick: boolean = true,
@@ -305,15 +345,16 @@ export class Pointer {
305
345
  await this.animateCursorToElement(target, executeClick, delayMs, annotationText, options);
306
346
 
307
347
  const locator = this.toLocator(target);
348
+ const editableLocator = await this.resolveEditableLocator(locator);
308
349
  const typeDelayMs = animationOptions.keyboard?.typeDelayMilliseconds ?? 100;
309
350
 
310
351
  if (animationOptions.enabled !== false && typeDelayMs > 0) {
311
352
  // Clear existing content, then type character-by-character so keystrokes are visible.
312
- await locator.first().clear();
353
+ await editableLocator.clear();
313
354
  await this.page.keyboard.type(text, { delay: typeDelayMs });
314
355
  }
315
356
  else {
316
- await locator.first().fill(text);
357
+ await editableLocator.fill(text);
317
358
  }
318
359
  }
319
360
  }
@@ -758,6 +758,26 @@ function generateAggregatedCSharpFiles(
758
758
  " protected IPage Page { get; }",
759
759
  ` protected ILocator LocatorByTestId(string testId) => Page.Locator($"[${testIdAttribute}=\\"{testId}\\"]");`,
760
760
  " protected ILocator LocatorWithinTestIdByLabel(string rootTestId, string label, bool exact = true) => LocatorByTestId(rootTestId).GetByLabel(label, new() { Exact = exact });",
761
+ " protected async Task<ILocator> ResolveEditableLocatorAsync(ILocator locator)",
762
+ " {",
763
+ " var isEditable = await locator.EvaluateAsync<bool>(@\"el => {",
764
+ " if (!el || !(el instanceof HTMLElement)) return false;",
765
+ " const tagName = el.tagName.toLowerCase();",
766
+ " return tagName === 'input' || tagName === 'textarea' || tagName === 'select' || el.isContentEditable;",
767
+ " }\");",
768
+ " if (isEditable)",
769
+ " {",
770
+ " return locator;",
771
+ " }",
772
+ "",
773
+ " var descendant = locator.Locator(\"input, textarea, select, [contenteditable=''], [contenteditable='true'], [contenteditable]:not([contenteditable='false'])\").First;",
774
+ " if (await descendant.CountAsync() > 0)",
775
+ " {",
776
+ " return descendant;",
777
+ " }",
778
+ "",
779
+ " return locator;",
780
+ " }",
761
781
  " protected async Task ClickWithinTestIdByLabelAsync(string rootTestId, string label, bool exact = true)",
762
782
  " {",
763
783
  " await LocatorWithinTestIdByLabel(rootTestId, label, exact).ClickAsync();",
@@ -889,7 +909,8 @@ function generateAggregatedCSharpFiles(
889
909
 
890
910
  const emitActionCall = (locatorAccess: string) => {
891
911
  if (pom.nativeRole === "input") {
892
- chunks.push(` await ${locatorAccess}.FillAsync(text);`);
912
+ chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
913
+ chunks.push(" await editableLocator.FillAsync(text);");
893
914
  }
894
915
  else if (pom.nativeRole === "select") {
895
916
  chunks.push(` await ${locatorAccess}.SelectOptionAsync(value);`);
@@ -923,7 +944,8 @@ function generateAggregatedCSharpFiles(
923
944
  chunks.push(" if (await locator.CountAsync() > 0)");
924
945
  chunks.push(" {");
925
946
  if (pom.nativeRole === "input") {
926
- chunks.push(" await locator.FillAsync(text);");
947
+ chunks.push(" var editableLocator = await ResolveEditableLocatorAsync(locator);");
948
+ chunks.push(" await editableLocator.FillAsync(text);");
927
949
  }
928
950
  else if (pom.nativeRole === "select") {
929
951
  chunks.push(" await locator.SelectOptionAsync(value);");
@@ -1229,9 +1251,10 @@ function generateViewObjectModelContent(
1229
1251
  return false;
1230
1252
 
1231
1253
  const scope = a.attachTo ?? "views";
1254
+ const scopeMatchesBoth = scope === "both" || scope === "pagesAndComponents";
1232
1255
  const scopeOk = isView
1233
- ? (scope === "views" || scope === "both")
1234
- : (scope === "components" || scope === "both");
1256
+ ? (scope === "views" || scopeMatchesBoth)
1257
+ : (scope === "components" || scopeMatchesBoth);
1235
1258
  if (!scopeOk)
1236
1259
  return false;
1237
1260
  return a.attachWhenUsesComponents.some(c => hasChildComponent(c));
@@ -56,6 +56,8 @@ export declare class Pointer {
56
56
  constructor(page: PwPage, testIdAttribute: string);
57
57
  private toLocator;
58
58
  private getTestId;
59
+ private isEditableElement;
60
+ private resolveEditableLocator;
59
61
  animateCursorToElement(target: ElementTarget, executeClick?: boolean, delayMs?: number, _annotationText?: string, options?: {
60
62
  afterClick?: AfterPointerClick;
61
63
  }): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"Pointer.d.ts","sourceRoot":"","sources":["../../class-generation/Pointer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AA8D5D,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,sDAAsD;IACtD,OAAO,CAAC,EAAE;QACT;;;;WAIG;QACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAE9B;;;WAGG;QACH,eAAe,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,aAAa,CAAC;QAE7E;;;WAGG;QACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;IAEF,uCAAuC;IACvC,QAAQ,CAAC,EAAE;QACV;;;WAGG;QACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;CACF;AASD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAavF;AAED,MAAM,WAAW,qBAAqB;IACrC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEtF,KAAK,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;AAMxC,qBAAa,OAAO;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAEtB,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM;IAKxD,OAAO,CAAC,SAAS;YAIH,SAAS;IAMV,sBAAsB,CAClC,MAAM,EAAE,aAAa,EACrB,YAAY,GAAE,OAAc,EAC5B,OAAO,GAAE,MAAY,EACrB,eAAe,GAAE,MAAW,EAC5B,OAAO,CAAC,EAAE;QACT,UAAU,CAAC,EAAE,iBAAiB,CAAC;KAC/B,GACC,OAAO,CAAC,IAAI,CAAC;IAsHH,qCAAqC,CACjD,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,MAAM,EACZ,YAAY,GAAE,OAAc,EAC5B,OAAO,GAAE,MAAY,EACrB,cAAc,GAAE,MAAW,EAC3B,OAAO,CAAC,EAAE;QACT,UAAU,CAAC,EAAE,iBAAiB,CAAC;KAC/B,GACC,OAAO,CAAC,IAAI,CAAC;CAgBhB"}
1
+ {"version":3,"file":"Pointer.d.ts","sourceRoot":"","sources":["../../class-generation/Pointer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAgE5D,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,sDAAsD;IACtD,OAAO,CAAC,EAAE;QACT;;;;WAIG;QACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAE9B;;;WAGG;QACH,eAAe,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,aAAa,CAAC;QAE7E;;;WAGG;QACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;IAEF,uCAAuC;IACvC,QAAQ,CAAC,EAAE;QACV;;;WAGG;QACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;CACF;AASD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAavF;AAED,MAAM,WAAW,qBAAqB;IACrC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEtF,KAAK,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;AAMxC,qBAAa,OAAO;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAEtB,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM;IAKxD,OAAO,CAAC,SAAS;YAIH,SAAS;YAMT,iBAAiB;YAmBjB,sBAAsB;IAmBvB,sBAAsB,CAClC,MAAM,EAAE,aAAa,EACrB,YAAY,GAAE,OAAc,EAC5B,OAAO,GAAE,MAAY,EACrB,eAAe,GAAE,MAAW,EAC5B,OAAO,CAAC,EAAE;QACT,UAAU,CAAC,EAAE,iBAAiB,CAAC;KAC/B,GACC,OAAO,CAAC,IAAI,CAAC;IAsHH,qCAAqC,CACjD,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,MAAM,EACZ,YAAY,GAAE,OAAc,EAC5B,OAAO,GAAE,MAAY,EACrB,cAAc,GAAE,MAAW,EAC3B,OAAO,CAAC,EAAE;QACT,UAAU,CAAC,EAAE,iBAAiB,CAAC;KAC/B,GACC,OAAO,CAAC,IAAI,CAAC;CAiBhB"}
package/dist/index.cjs CHANGED
@@ -3712,6 +3712,26 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3712
3712
  " protected IPage Page { get; }",
3713
3713
  ` protected ILocator LocatorByTestId(string testId) => Page.Locator($"[${testIdAttribute}=\\"{testId}\\"]");`,
3714
3714
  " protected ILocator LocatorWithinTestIdByLabel(string rootTestId, string label, bool exact = true) => LocatorByTestId(rootTestId).GetByLabel(label, new() { Exact = exact });",
3715
+ " protected async Task<ILocator> ResolveEditableLocatorAsync(ILocator locator)",
3716
+ " {",
3717
+ ' var isEditable = await locator.EvaluateAsync<bool>(@"el => {',
3718
+ " if (!el || !(el instanceof HTMLElement)) return false;",
3719
+ " const tagName = el.tagName.toLowerCase();",
3720
+ " return tagName === 'input' || tagName === 'textarea' || tagName === 'select' || el.isContentEditable;",
3721
+ ' }");',
3722
+ " if (isEditable)",
3723
+ " {",
3724
+ " return locator;",
3725
+ " }",
3726
+ "",
3727
+ ` var descendant = locator.Locator("input, textarea, select, [contenteditable=''], [contenteditable='true'], [contenteditable]:not([contenteditable='false'])").First;`,
3728
+ " if (await descendant.CountAsync() > 0)",
3729
+ " {",
3730
+ " return descendant;",
3731
+ " }",
3732
+ "",
3733
+ " return locator;",
3734
+ " }",
3715
3735
  " protected async Task ClickWithinTestIdByLabelAsync(string rootTestId, string label, bool exact = true)",
3716
3736
  " {",
3717
3737
  " await LocatorWithinTestIdByLabel(rootTestId, label, exact).ClickAsync();",
@@ -3813,7 +3833,8 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3813
3833
  const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
3814
3834
  const emitActionCall = (locatorAccess) => {
3815
3835
  if (pom.nativeRole === "input") {
3816
- chunks.push(` await ${locatorAccess}.FillAsync(text);`);
3836
+ chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
3837
+ chunks.push(" await editableLocator.FillAsync(text);");
3817
3838
  } else if (pom.nativeRole === "select") {
3818
3839
  chunks.push(` await ${locatorAccess}.SelectOptionAsync(value);`);
3819
3840
  } else if (pom.nativeRole === "vselect") {
@@ -3841,7 +3862,8 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3841
3862
  chunks.push(" if (await locator.CountAsync() > 0)");
3842
3863
  chunks.push(" {");
3843
3864
  if (pom.nativeRole === "input") {
3844
- chunks.push(" await locator.FillAsync(text);");
3865
+ chunks.push(" var editableLocator = await ResolveEditableLocatorAsync(locator);");
3866
+ chunks.push(" await editableLocator.FillAsync(text);");
3845
3867
  } else if (pom.nativeRole === "select") {
3846
3868
  chunks.push(" await locator.SelectOptionAsync(value);");
3847
3869
  } else {
@@ -4065,7 +4087,8 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
4065
4087
  if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
4066
4088
  return false;
4067
4089
  const scope = a.attachTo ?? "views";
4068
- const scopeOk = isView ? scope === "views" || scope === "both" : scope === "components" || scope === "both";
4090
+ const scopeMatchesBoth = scope === "both" || scope === "pagesAndComponents";
4091
+ const scopeOk = isView ? scope === "views" || scopeMatchesBoth : scope === "components" || scopeMatchesBoth;
4069
4092
  if (!scopeOk)
4070
4093
  return false;
4071
4094
  return a.attachWhenUsesComponents.some((c) => hasChildComponent(c));