@immense/vue-pom-generator 1.0.30 → 1.0.32

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/README.md CHANGED
@@ -35,15 +35,15 @@ Exported entrypoints:
35
35
  - `injection`: how `data-testid` (or your chosen attribute) is derived/injected
36
36
  - `generation`: how Page Object Models (POMs) and Playwright helpers are generated
37
37
 
38
- The generator emits an aggregated output under `generation.outDir` (default `tests/playwright/generated`):
38
+ The generator emits an aggregated output under `generation.outDir` (default `tests/playwright/__generated__`):
39
39
 
40
- - `tests/playwright/generated/page-object-models.g.ts` (generated; do not edit)
41
- - `tests/playwright/generated/index.ts` (generated stable barrel)
42
- - managed `.gitattributes` files alongside generated outputs so GitHub Linguist treats them as generated by default
40
+ - `tests/playwright/__generated__/page-object-models.g.ts` (generated; do not edit)
41
+ - `tests/playwright/__generated__/index.ts` (generated stable barrel)
42
+ - managed `.gitattributes` files only when you emit outside `__generated__`
43
43
 
44
44
  If `generation.playwright.fixtures` is enabled, it also emits:
45
45
 
46
- - `tests/playwright/generated/fixtures.g.ts` (generated; do not edit)
46
+ - `tests/playwright/__generated__/fixtures.g.ts` (generated; do not edit)
47
47
 
48
48
  ### Vite config example
49
49
 
@@ -98,8 +98,8 @@ export default defineConfig(() => {
98
98
  namespace: "MyProject.Tests.Generated",
99
99
  },
100
100
 
101
- // Default: tests/playwright/generated
102
- outDir: "tests/playwright/generated",
101
+ // Default: tests/playwright/__generated__
102
+ outDir: "tests/playwright/__generated__",
103
103
 
104
104
  // Controls how to handle duplicate generated member names within a single POM class.
105
105
  // - "error": fail compilation
package/RELEASE_NOTES.md CHANGED
@@ -1,44 +1,38 @@
1
- ● # Release Notes: v1.0.30
1
+ ● # Release Notes: v1.0.32
2
2
 
3
3
  ## Highlights
4
4
 
5
- - Refactored wrapper search roots configuration for improved flexibility
6
- - Enhanced plugin system with better type definitions and support
7
- - Improved test coverage with expanded transform test cases
8
- - Added CI automation for PR release note previews
5
+ - Fixed handling of missing helpers and keyed ID branches in class generation
6
+ - Added robust error tolerance for edge cases in Vue component transformation
7
+ - Expanded test coverage with 109 new test lines across generated TypeScript and utility
8
+ coverage tests
9
+ - Introduced PR release-notes preview comments via GitHub Actions
9
10
 
10
11
  ## Changes
11
12
 
12
- ### Core Functionality
13
- - Refactored wrapper search roots configuration in transform logic
14
- - Enhanced transform logic with improved structure (198 additions, significant reorganization)
13
+ **Bug Fixes**
14
+ - Tolerate missing helpers and keyed ID branches in class generation logic
15
+ (`class-generation/index.ts`, `utils.ts`)
15
16
 
16
- ### Plugin System
17
- - Updated plugin types with refined interfaces
18
- - Improved Vue plugin implementation
19
- - Enhanced plugin creation utilities
20
- - Better dev plugin support
17
+ **Testing**
18
+ - Added comprehensive tests for generated TypeScript output (`tests/generated-tsc.test.ts`: +74
19
+ lines)
20
+ - Expanded utility function coverage tests (`tests/utils-coverage.test.ts`: +35 lines)
21
21
 
22
- ### Testing & Quality
23
- - Expanded transform test suite with additional test cases
24
- - Updated test assertions for improved coverage
25
-
26
- ### Tooling & CI
27
- - Added automated PR release-notes preview comments
28
-
29
- ### Documentation
30
- - Updated README with additional information
22
+ **CI/Automation**
23
+ - Added automated PR release-notes preview comments to pull requests
31
24
 
32
25
  ## Breaking Changes
33
26
 
34
- None identified in this release.
27
+ None
35
28
 
36
29
  ## Pull Requests Included
37
30
 
38
- - #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
39
- by @dkattan
31
+ - [#1](https://github.com/immense/vue-pom-generator/pull/1) Add PR release-notes preview
32
+ comments (@dkattan)
40
33
 
41
34
  ## Testing
42
35
 
43
- Existing test suite updated and expanded with new test cases in `transform.test.ts`.
36
+ Significant test additions: 74 lines in generated TypeScript tests and 35 lines in utility
37
+ coverage tests validate the fix for missing helpers and keyed ID branches.
44
38
 
@@ -407,6 +407,7 @@ interface GenerateContentOptions {
407
407
  customPomDir?: string;
408
408
  customPomImportAliases?: Record<string, string>;
409
409
  customPomClassIdentifierMap?: Record<string, string>;
410
+ customPomAvailableClassIdentifiers?: Set<string>;
410
411
 
411
412
  /** Attribute name to treat as the test id. Defaults to `data-testid`. */
412
413
  testIdAttribute?: string;
@@ -524,6 +525,11 @@ function escapeGitAttributesPattern(value: string): string {
524
525
  return output;
525
526
  }
526
527
 
528
+ function pathUsesGeneratedHeuristic(filePath: string): boolean {
529
+ const normalized = path.normalize(filePath);
530
+ return normalized.split(path.sep).includes("__generated__");
531
+ }
532
+
527
533
  function buildManagedGitAttributesBlock(entries: string[]): string {
528
534
  return [
529
535
  GENERATED_GITATTRIBUTES_BLOCK_START,
@@ -581,6 +587,10 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths: string[]): Generat
581
587
  continue;
582
588
  }
583
589
 
590
+ if (pathUsesGeneratedHeuristic(resolvedFilePath)) {
591
+ continue;
592
+ }
593
+
584
594
  const dir = path.dirname(resolvedFilePath);
585
595
  const entry = `${escapeGitAttributesPattern(path.basename(resolvedFilePath))} linguist-generated`;
586
596
  const entries = entriesByDir.get(dir) ?? new Set<string>();
@@ -1144,8 +1154,14 @@ function generateViewObjectModelContent(
1144
1154
  return false;
1145
1155
  };
1146
1156
 
1157
+ const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
1158
+ const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? new Set<string>();
1159
+
1147
1160
  const attachmentsForThisClass = customPomAttachments
1148
1161
  .filter((a) => {
1162
+ if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
1163
+ return false;
1164
+
1149
1165
  const scope = a.attachTo ?? "views";
1150
1166
  const scopeOk = isView
1151
1167
  ? (scope === "views" || scope === "both")
@@ -1155,7 +1171,7 @@ function generateViewObjectModelContent(
1155
1171
  return a.attachWhenUsesComponents.some(c => hasChildComponent(c));
1156
1172
  })
1157
1173
  .map(a => ({
1158
- className: options.customPomClassIdentifierMap?.[a.className] ?? a.className,
1174
+ className: customPomClassIdentifierMap[a.className]!,
1159
1175
  propertyName: a.propertyName,
1160
1176
  }));
1161
1177
 
@@ -1209,7 +1225,7 @@ function generateViewObjectModelContent(
1209
1225
  content += `\nexport class ${className} extends BasePage {\n`;
1210
1226
 
1211
1227
  const widgetInstances = isView
1212
- ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet)
1228
+ ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers)
1213
1229
  : [];
1214
1230
 
1215
1231
  // For views, `childrenComponentSet` only includes component tags on which we applied a data-testid.
@@ -1480,6 +1496,7 @@ async function generateAggregatedFiles(
1480
1496
  };
1481
1497
 
1482
1498
  const customPomClassIdentifierMap = addCustomPomImports();
1499
+ const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap ?? {}));
1483
1500
 
1484
1501
  // Collect any navigation return types referenced by generated methods so we can emit
1485
1502
  // stub classes when the destination view has no generated test ids (and therefore no
@@ -1681,6 +1698,7 @@ async function generateAggregatedFiles(
1681
1698
 
1682
1699
  customPomAttachments: options.customPomAttachments ?? [],
1683
1700
  customPomClassIdentifierMap,
1701
+ customPomAvailableClassIdentifiers,
1684
1702
  testIdAttribute: options.testIdAttribute,
1685
1703
  vueRouterFluentChaining: options.vueRouterFluentChaining,
1686
1704
  routeMetaByComponent: options.routeMetaByComponent,
@@ -1808,7 +1826,11 @@ interface WidgetInstance {
1808
1826
  testId: string;
1809
1827
  }
1810
1828
 
1811
- function getWidgetInstancesForView(componentName: string, dataTestIdSet: Set<IDataTestId>): WidgetInstance[] {
1829
+ function getWidgetInstancesForView(
1830
+ componentName: string,
1831
+ dataTestIdSet: Set<IDataTestId>,
1832
+ availableClassIdentifiers: Set<string>,
1833
+ ): WidgetInstance[] {
1812
1834
  const out: WidgetInstance[] = [];
1813
1835
  const usedPropNames = new Set<string>();
1814
1836
 
@@ -1849,6 +1871,9 @@ function getWidgetInstancesForView(componentName: string, dataTestIdSet: Set<IDa
1849
1871
  continue;
1850
1872
  }
1851
1873
 
1874
+ if (!availableClassIdentifiers.has(className))
1875
+ continue;
1876
+
1852
1877
  // Prefer stripping the view prefix (e.g. PreferencesPage-) for cleaner member names.
1853
1878
  const viewPrefix = `${componentName}-`;
1854
1879
  const descriptorRaw = stem.startsWith(viewPrefix) ? stem.slice(viewPrefix.length) : stem;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAoE,MAAM,UAAU,CAAC;AAQpH,OAAO,EAAE,oCAAoC,EAAE,CAAC;AA8ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAyOD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,oCAAoC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEzD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,KAAK,CAAC;QAC3B,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;QAEnC;;;WAGG;QACH,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC;KAC5C,CAAC,CAAC;IAEH,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAEnC,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AAuCD,wBAAsB,aAAa,CACjC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,oBAAyB,iBA2EnC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAoE,MAAM,UAAU,CAAC;AAQpH,OAAO,EAAE,oCAAoC,EAAE,CAAC;AA8ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAyOD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,oCAAoC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEzD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,KAAK,CAAC;QAC3B,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;QAEnC;;;WAGG;QACH,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC;KAC5C,CAAC,CAAC;IAEH,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAEnC,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AAwCD,wBAAsB,aAAa,CACjC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,oBAAyB,iBA2EnC"}
package/dist/index.cjs CHANGED
@@ -606,6 +606,124 @@ function getTemplateSlotScope(node) {
606
606
  }
607
607
  return null;
608
608
  }
609
+ function isSimpleScopeIdentifier(value) {
610
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
611
+ }
612
+ function buildSlotScopeFallbackKeyExpression(identifier) {
613
+ return `${identifier}.key ?? ${identifier}.data?.id ?? ${identifier}.id ?? ${identifier}.value ?? ${identifier}`;
614
+ }
615
+ function tryGetBindingIdentifierName(node) {
616
+ if (!node) {
617
+ return null;
618
+ }
619
+ if (types.isIdentifier(node)) {
620
+ return node.name;
621
+ }
622
+ if (types.isAssignmentPattern(node)) {
623
+ return tryGetBindingIdentifierName(node.left);
624
+ }
625
+ if (types.isRestElement(node)) {
626
+ return tryGetBindingIdentifierName(node.argument);
627
+ }
628
+ return null;
629
+ }
630
+ function getSlotScopeObjectPropertyKeyName(node) {
631
+ if (types.isIdentifier(node)) {
632
+ return node.name;
633
+ }
634
+ if (types.isStringLiteral(node)) {
635
+ return node.value;
636
+ }
637
+ return null;
638
+ }
639
+ function tryGetSlotScopeKeyCandidate(node) {
640
+ if (!node) {
641
+ return null;
642
+ }
643
+ if (types.isIdentifier(node)) {
644
+ return {
645
+ priority: 2,
646
+ expression: buildSlotScopeFallbackKeyExpression(node.name)
647
+ };
648
+ }
649
+ if (types.isAssignmentPattern(node)) {
650
+ return tryGetSlotScopeKeyCandidate(node.left);
651
+ }
652
+ if (types.isRestElement(node)) {
653
+ return tryGetSlotScopeKeyCandidate(node.argument);
654
+ }
655
+ if (!types.isObjectPattern(node)) {
656
+ return null;
657
+ }
658
+ let best = null;
659
+ for (const property of node.properties) {
660
+ let candidate = null;
661
+ if (types.isRestElement(property)) {
662
+ candidate = tryGetSlotScopeKeyCandidate(property.argument);
663
+ if (candidate) {
664
+ candidate = { ...candidate, priority: Math.max(candidate.priority, 2) };
665
+ }
666
+ } else if (types.isObjectProperty(property)) {
667
+ const keyName = getSlotScopeObjectPropertyKeyName(property.key);
668
+ const bindingName = tryGetBindingIdentifierName(property.value) ?? tryGetBindingIdentifierName(property.key);
669
+ if (bindingName) {
670
+ if (keyName === "key" || bindingName === "key") {
671
+ candidate = {
672
+ priority: 0,
673
+ expression: bindingName
674
+ };
675
+ } else {
676
+ candidate = {
677
+ priority: keyName === "data" ? 1 : 2,
678
+ expression: buildSlotScopeFallbackKeyExpression(bindingName)
679
+ };
680
+ }
681
+ }
682
+ }
683
+ if (candidate && (!best || candidate.priority < best.priority)) {
684
+ best = candidate;
685
+ }
686
+ }
687
+ return best;
688
+ }
689
+ function tryGetTemplateSlotScopeKeyExpression(scope) {
690
+ const trimmed = scope.trim();
691
+ if (!trimmed) {
692
+ return null;
693
+ }
694
+ if (isSimpleScopeIdentifier(trimmed)) {
695
+ return buildSlotScopeFallbackKeyExpression(trimmed);
696
+ }
697
+ try {
698
+ const parsed = parser.parse(`(${trimmed}) => {}`, {
699
+ sourceType: "module",
700
+ plugins: ["typescript"]
701
+ });
702
+ const statement = parsed.program.body[0];
703
+ if (statement && types.isExpressionStatement(statement) && types.isArrowFunctionExpression(statement.expression)) {
704
+ return tryGetSlotScopeKeyCandidate(statement.expression.params[0])?.expression ?? null;
705
+ }
706
+ } catch {
707
+ }
708
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
709
+ const inner = trimmed.slice(1, -1).trim();
710
+ let cutIdx = -1;
711
+ const commaIdx = inner.indexOf(",");
712
+ const colonIdx = inner.indexOf(":");
713
+ if (commaIdx !== -1 && colonIdx !== -1) {
714
+ cutIdx = Math.min(commaIdx, colonIdx);
715
+ } else if (commaIdx !== -1) {
716
+ cutIdx = commaIdx;
717
+ } else if (colonIdx !== -1) {
718
+ cutIdx = colonIdx;
719
+ }
720
+ const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
721
+ if (first && isSimpleScopeIdentifier(first)) {
722
+ return buildSlotScopeFallbackKeyExpression(first);
723
+ }
724
+ }
725
+ return trimmed;
726
+ }
609
727
  function nodeHasToDirective(node) {
610
728
  const toDirective = findDirectiveByName(node, "bind", "to");
611
729
  if (toDirective?.exp) {
@@ -681,25 +799,7 @@ function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
681
799
  if (parent.type === compilerCore.NodeTypes.ELEMENT && parent.tag === "template") {
682
800
  const scope = getTemplateSlotScope(parent);
683
801
  if (scope) {
684
- let key = scope.trim();
685
- if (key.startsWith("{") && key.endsWith("}")) {
686
- const inner = key.slice(1, -1).trim();
687
- let cutIdx = -1;
688
- const commaIdx = inner.indexOf(",");
689
- const colonIdx = inner.indexOf(":");
690
- if (commaIdx !== -1 && colonIdx !== -1) {
691
- cutIdx = Math.min(commaIdx, colonIdx);
692
- } else if (commaIdx !== -1) {
693
- cutIdx = commaIdx;
694
- } else if (colonIdx !== -1) {
695
- cutIdx = colonIdx;
696
- }
697
- const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
698
- if (first) {
699
- key = first;
700
- }
701
- }
702
- return key;
802
+ return tryGetTemplateSlotScopeKeyExpression(scope);
703
803
  }
704
804
  }
705
805
  parent = getParent(hierarchyMap2, parent);
@@ -1350,11 +1450,11 @@ function getStableClickHandlerNameFromExpression(exp) {
1350
1450
  return extractNameFromCallee(callee);
1351
1451
  }
1352
1452
  if (types.isAssignmentExpression(exp)) {
1353
- const left = exp.left;
1354
- if (types.isIdentifier(left))
1355
- return left.name;
1356
- if (types.isMemberExpression(left) || types.isOptionalMemberExpression(left))
1357
- return extractMemberPropertyName(left);
1453
+ const left = getAssignmentTargetNameFromBabel(exp.left);
1454
+ if (left) {
1455
+ const right = getStableAssignmentValueSuffixFromBabel(exp.right);
1456
+ return `Set${toPascalCase(left)}${right}`;
1457
+ }
1358
1458
  return "";
1359
1459
  }
1360
1460
  if (types.isOptionalMemberExpression(exp)) {
@@ -1429,6 +1529,71 @@ function extractMemberPropertyName(member) {
1429
1529
  }
1430
1530
  return "";
1431
1531
  }
1532
+ function getLastIdentifierFromMemberChainBabel(node) {
1533
+ if (!node) {
1534
+ return "";
1535
+ }
1536
+ if (types.isIdentifier(node)) {
1537
+ return node.name;
1538
+ }
1539
+ if (types.isMemberExpression(node) || types.isOptionalMemberExpression(node)) {
1540
+ if (!node.computed) {
1541
+ if (types.isIdentifier(node.property)) {
1542
+ return node.property.name;
1543
+ }
1544
+ } else if (types.isStringLiteral(node.property)) {
1545
+ return node.property.value;
1546
+ }
1547
+ }
1548
+ return "";
1549
+ }
1550
+ function getAssignmentTargetNameFromBabel(node) {
1551
+ if (!node) {
1552
+ return "";
1553
+ }
1554
+ if (types.isIdentifier(node)) {
1555
+ return node.name;
1556
+ }
1557
+ if (types.isMemberExpression(node) || types.isOptionalMemberExpression(node)) {
1558
+ if (!node.computed && types.isIdentifier(node.property) && node.property.name === "value") {
1559
+ return getLastIdentifierFromMemberChainBabel(node.object);
1560
+ }
1561
+ return getLastIdentifierFromMemberChainBabel(node);
1562
+ }
1563
+ return "";
1564
+ }
1565
+ function getStableAssignmentValueSuffixFromBabel(node) {
1566
+ if (!node) {
1567
+ return "";
1568
+ }
1569
+ if (types.isBooleanLiteral(node)) {
1570
+ return node.value ? "True" : "False";
1571
+ }
1572
+ if (types.isNumericLiteral(node)) {
1573
+ return `Value${String(node.value)}`;
1574
+ }
1575
+ if (types.isNullLiteral(node)) {
1576
+ return "Null";
1577
+ }
1578
+ if (types.isStringLiteral(node)) {
1579
+ const cleaned = (node.value ?? "").trim();
1580
+ return cleaned ? toPascalCase(cleaned.slice(0, 24)) : "";
1581
+ }
1582
+ if (types.isTemplateLiteral(node) && node.expressions.length === 0) {
1583
+ const value = node.quasis.map((q) => q.value?.cooked ?? "").join("").trim();
1584
+ return value ? toPascalCase(value.slice(0, 24)) : "";
1585
+ }
1586
+ if (types.isMemberExpression(node) || types.isOptionalMemberExpression(node)) {
1587
+ const name = getLastIdentifierFromMemberChainBabel(node);
1588
+ return name ? toPascalCase(name.slice(0, 24)) : "";
1589
+ }
1590
+ if (types.isIdentifier(node)) {
1591
+ const firstChar = node.name.charAt(0);
1592
+ const isUpperAlpha = firstChar !== "" && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
1593
+ return isUpperAlpha ? toPascalCase(node.name.slice(0, 24)) : "";
1594
+ }
1595
+ return "";
1596
+ }
1432
1597
  function isCompilerGeneratedReferenceRoot(name) {
1433
1598
  return name.startsWith("_") || name.startsWith("$");
1434
1599
  }
@@ -1694,6 +1859,12 @@ function applyResolvedDataTestId(args) {
1694
1859
  const existingIdBehavior = args.existingIdBehavior ?? "preserve";
1695
1860
  const nameCollisionBehavior = args.nameCollisionBehavior ?? "suffix";
1696
1861
  const warn = args.warn;
1862
+ const getBestKeyAccessCandidates = (expr) => {
1863
+ if (!expr) {
1864
+ return [];
1865
+ }
1866
+ return expr.split("??").map((part) => part.trim()).filter(Boolean);
1867
+ };
1697
1868
  let dataTestId = args.preferredGeneratedValue;
1698
1869
  let fromExisting = false;
1699
1870
  const existing = tryGetExistingElementDataTestId(args.element, testIdAttribute);
@@ -1716,6 +1887,7 @@ Bulk cleanup: run ESLint with the @immense/vue-pom-generator/remove-existing-tes
1716
1887
  if (existingIdBehavior === "preserve") {
1717
1888
  if (existing.isDynamic) {
1718
1889
  if (existing.template) {
1890
+ const existingTemplate = existing.template;
1719
1891
  if ((existing.templateExpressionCount ?? 0) !== 1) {
1720
1892
  throw new Error(
1721
1893
  `[vue-pom-generator] Existing ${attrLabel} is a template literal with multiple interpolations and cannot be preserved safely.
@@ -1726,8 +1898,8 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
1726
1898
  Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
1727
1899
  );
1728
1900
  }
1729
- const hasExact = args.bestKeyPlaceholder && existing.template.includes(args.bestKeyPlaceholder);
1730
- const hasVarAccess = args.bestKeyVariable && existing.template.includes(args.bestKeyVariable);
1901
+ const hasExact = args.bestKeyPlaceholder && existingTemplate.includes(args.bestKeyPlaceholder);
1902
+ const hasVarAccess = getBestKeyAccessCandidates(args.bestKeyVariable).some((candidate) => existingTemplate.includes(candidate));
1731
1903
  if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
1732
1904
  throw new Error(
1733
1905
  `[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
@@ -3240,6 +3412,10 @@ function escapeGitAttributesPattern(value) {
3240
3412
  }
3241
3413
  return output;
3242
3414
  }
3415
+ function pathUsesGeneratedHeuristic(filePath) {
3416
+ const normalized = path.normalize(filePath);
3417
+ return normalized.split(path.sep).includes("__generated__");
3418
+ }
3243
3419
  function buildManagedGitAttributesBlock(entries) {
3244
3420
  return [
3245
3421
  GENERATED_GITATTRIBUTES_BLOCK_START,
@@ -3287,6 +3463,9 @@ function buildGeneratedGitAttributesFiles(generatedFilePaths) {
3287
3463
  if (path.basename(resolvedFilePath) === ".gitattributes") {
3288
3464
  continue;
3289
3465
  }
3466
+ if (pathUsesGeneratedHeuristic(resolvedFilePath)) {
3467
+ continue;
3468
+ }
3290
3469
  const dir = path.dirname(resolvedFilePath);
3291
3470
  const entry = `${escapeGitAttributesPattern(path.basename(resolvedFilePath))} linguist-generated`;
3292
3471
  const entries = entriesByDir.get(dir) ?? /* @__PURE__ */ new Set();
@@ -3711,14 +3890,18 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
3711
3890
  }
3712
3891
  return false;
3713
3892
  };
3893
+ const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
3894
+ const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? /* @__PURE__ */ new Set();
3714
3895
  const attachmentsForThisClass = customPomAttachments.filter((a) => {
3896
+ if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
3897
+ return false;
3715
3898
  const scope = a.attachTo ?? "views";
3716
3899
  const scopeOk = isView ? scope === "views" || scope === "both" : scope === "components" || scope === "both";
3717
3900
  if (!scopeOk)
3718
3901
  return false;
3719
3902
  return a.attachWhenUsesComponents.some((c) => hasChildComponent(c));
3720
3903
  }).map((a) => ({
3721
- className: options.customPomClassIdentifierMap?.[a.className] ?? a.className,
3904
+ className: customPomClassIdentifierMap[a.className],
3722
3905
  propertyName: a.propertyName
3723
3906
  }));
3724
3907
  let content = "";
@@ -3758,7 +3941,7 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
3758
3941
  content += `
3759
3942
  export class ${className} extends BasePage {
3760
3943
  `;
3761
- const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet) : [];
3944
+ const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers) : [];
3762
3945
  const componentRefsForInstances = isView ? usedComponentSet?.size ? usedComponentSet : childrenComponentSet : childrenComponentSet;
3763
3946
  if (isView && (componentRefsForInstances.size > 0 || attachmentsForThisClass.length > 0 || widgetInstances.length > 0)) {
3764
3947
  content += getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances);
@@ -3916,6 +4099,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
3916
4099
  return customPomClassIdentifierMap2;
3917
4100
  };
3918
4101
  const customPomClassIdentifierMap = addCustomPomImports();
4102
+ const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap ?? {}));
3919
4103
  const referencedTargets = /* @__PURE__ */ new Set();
3920
4104
  for (const [, deps] of items) {
3921
4105
  for (const dt of deps.dataTestIdSet) {
@@ -4068,6 +4252,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4068
4252
  aggregated: true,
4069
4253
  customPomAttachments: options.customPomAttachments ?? [],
4070
4254
  customPomClassIdentifierMap,
4255
+ customPomAvailableClassIdentifiers,
4071
4256
  testIdAttribute: options.testIdAttribute,
4072
4257
  vueRouterFluentChaining: options.vueRouterFluentChaining,
4073
4258
  routeMetaByComponent: options.routeMetaByComponent
@@ -4162,7 +4347,7 @@ function toPascalCaseLocal(str) {
4162
4347
  return preserveInternalCaps ? upperFirst(word) : upperFirst(word.toLowerCase());
4163
4348
  }).join("");
4164
4349
  }
4165
- function getWidgetInstancesForView(componentName, dataTestIdSet) {
4350
+ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassIdentifiers) {
4166
4351
  const out = [];
4167
4352
  const usedPropNames = /* @__PURE__ */ new Set();
4168
4353
  const ensureUnique = (base) => {
@@ -4193,6 +4378,8 @@ function getWidgetInstancesForView(componentName, dataTestIdSet) {
4193
4378
  } else {
4194
4379
  continue;
4195
4380
  }
4381
+ if (!availableClassIdentifiers.has(className))
4382
+ continue;
4196
4383
  const viewPrefix = `${componentName}-`;
4197
4384
  const descriptorRaw = stem.startsWith(viewPrefix) ? stem.slice(viewPrefix.length) : stem;
4198
4385
  const descriptorPascal = toPascalCaseLocal(descriptorRaw);
@@ -6197,7 +6384,7 @@ function createVuePomGeneratorPlugins(options = {}) {
6197
6384
  const excludedComponents = injection.excludeComponents ?? [];
6198
6385
  const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
6199
6386
  const existingIdBehavior = injection.existingIdBehavior ?? "preserve";
6200
- const outDir = (generationOptions?.outDir ?? "tests/playwright/generated").trim();
6387
+ const outDir = (generationOptions?.outDir ?? "tests/playwright/__generated__").trim();
6201
6388
  const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
6202
6389
  const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
6203
6390
  const routerEntry = generationOptions?.router?.entry;