@immense/vue-pom-generator 1.0.3 → 1.0.8
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 +85 -71
- package/RELEASE_NOTES.md +38 -0
- package/class-generation/BasePage.ts +17 -2
- package/class-generation/Pointer.ts +1 -1
- package/class-generation/index.ts +77 -24
- package/class-generation/playwright-types.ts +23 -0
- package/dist/index.cjs +583 -208
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +584 -209
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
package/dist/index.mjs
CHANGED
|
@@ -2,17 +2,66 @@ import path from "node:path";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import { JSDOM } from "jsdom";
|
|
5
6
|
import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression } from "@vue/compiler-core";
|
|
6
7
|
import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isOptionalMemberExpression, isObjectExpression, isFile, isArrowFunctionExpression, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isObjectProperty, isProgram, VISITOR_KEYS } from "@babel/types";
|
|
7
8
|
import { parseExpression, parse } from "@babel/parser";
|
|
8
|
-
import { JSDOM } from "jsdom";
|
|
9
9
|
import { performance } from "node:perf_hooks";
|
|
10
10
|
import * as compilerDom from "@vue/compiler-dom";
|
|
11
|
-
import { parse as parse$2
|
|
11
|
+
import { parse as parse$2 } from "@vue/compiler-dom";
|
|
12
12
|
import { parse as parse$1 } from "@vue/compiler-sfc";
|
|
13
13
|
import virtualImport from "vite-plugin-virtual";
|
|
14
14
|
import vue from "@vitejs/plugin-vue";
|
|
15
|
-
|
|
15
|
+
const VUE_POM_GENERATOR_LOG_PREFIX = "[vue-pom-generator]";
|
|
16
|
+
function normalize(message) {
|
|
17
|
+
const trimmed = (message ?? "").trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return VUE_POM_GENERATOR_LOG_PREFIX;
|
|
20
|
+
if (trimmed.startsWith(VUE_POM_GENERATOR_LOG_PREFIX))
|
|
21
|
+
return trimmed;
|
|
22
|
+
return `${VUE_POM_GENERATOR_LOG_PREFIX} ${trimmed}`;
|
|
23
|
+
}
|
|
24
|
+
function createLogger(options) {
|
|
25
|
+
const { verbosity, viteLogger } = options;
|
|
26
|
+
const sinkInfo = (msg) => {
|
|
27
|
+
if (viteLogger) {
|
|
28
|
+
viteLogger.info(normalize(msg));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(normalize(msg));
|
|
32
|
+
};
|
|
33
|
+
const sinkWarn = (msg) => {
|
|
34
|
+
if (viteLogger) {
|
|
35
|
+
viteLogger.warn(normalize(msg));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.warn(normalize(msg));
|
|
39
|
+
};
|
|
40
|
+
const sinkDebug = (msg) => {
|
|
41
|
+
if (viteLogger) {
|
|
42
|
+
viteLogger.info(normalize(msg));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(normalize(msg));
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
info(message) {
|
|
49
|
+
if (verbosity === "silent" || verbosity === "warn")
|
|
50
|
+
return;
|
|
51
|
+
sinkInfo(message);
|
|
52
|
+
},
|
|
53
|
+
debug(message) {
|
|
54
|
+
if (verbosity !== "debug")
|
|
55
|
+
return;
|
|
56
|
+
sinkDebug(message);
|
|
57
|
+
},
|
|
58
|
+
warn(message) {
|
|
59
|
+
if (verbosity === "silent")
|
|
60
|
+
return;
|
|
61
|
+
sinkWarn(message);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
16
65
|
const INDENT = " ";
|
|
17
66
|
const INDENT2 = `${INDENT}${INDENT}`;
|
|
18
67
|
const INDENT3 = `${INDENT2}${INDENT}`;
|
|
@@ -147,10 +196,21 @@ ${INDENT}}
|
|
|
147
196
|
`;
|
|
148
197
|
return content;
|
|
149
198
|
}
|
|
199
|
+
function isAllDigits(value) {
|
|
200
|
+
if (!value)
|
|
201
|
+
return false;
|
|
202
|
+
for (let i = 0; i < value.length; i++) {
|
|
203
|
+
const code = value.charCodeAt(i);
|
|
204
|
+
if (code < 48 || code > 57)
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
150
209
|
function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
|
|
151
210
|
const roleSuffix = upperFirst$1(nativeRole || "Element");
|
|
152
211
|
const baseName = upperFirst$1(methodName);
|
|
153
|
-
const
|
|
212
|
+
const numericSuffix = baseName.startsWith(roleSuffix) ? baseName.slice(roleSuffix.length) : "";
|
|
213
|
+
const hasRoleSuffix = baseName.endsWith(roleSuffix) || baseName.startsWith(roleSuffix) && isAllDigits(numericSuffix);
|
|
154
214
|
const propertyName = hasRoleSuffix ? `${baseName}` : `${baseName}${roleSuffix}`;
|
|
155
215
|
const needsKey = hasParam(params, "key") || formattedDataTestId.includes("${key}");
|
|
156
216
|
if (needsKey) {
|
|
@@ -456,6 +516,23 @@ function tryGetClickDirective(node) {
|
|
|
456
516
|
function nodeHasClickDirective(node) {
|
|
457
517
|
return tryGetClickDirective(node) !== void 0;
|
|
458
518
|
}
|
|
519
|
+
function getTemplateSlotScope(node) {
|
|
520
|
+
if (node.tag !== "template") {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const slotProp = node.props.find((prop) => {
|
|
524
|
+
return prop.type === NodeTypes.DIRECTIVE && prop.name === "slot";
|
|
525
|
+
});
|
|
526
|
+
if (slotProp?.exp) {
|
|
527
|
+
if (slotProp.exp.type === NodeTypes.SIMPLE_EXPRESSION) {
|
|
528
|
+
return slotProp.exp.content;
|
|
529
|
+
}
|
|
530
|
+
if (slotProp.exp.type === NodeTypes.COMPOUND_EXPRESSION) {
|
|
531
|
+
return stringifyExpression(slotProp.exp);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
459
536
|
function nodeHasToDirective(node) {
|
|
460
537
|
const toDirective = findDirectiveByName(node, "bind", "to");
|
|
461
538
|
if (toDirective?.exp) {
|
|
@@ -525,6 +602,37 @@ function getIdOrName(node) {
|
|
|
525
602
|
}
|
|
526
603
|
return identifier;
|
|
527
604
|
}
|
|
605
|
+
function getContainedInSlotDataKeyValue(node, hierarchyMap2) {
|
|
606
|
+
let parent = getParent(hierarchyMap2, node);
|
|
607
|
+
while (parent) {
|
|
608
|
+
if (parent.type === NodeTypes.ELEMENT && parent.tag === "template") {
|
|
609
|
+
const scope = getTemplateSlotScope(parent);
|
|
610
|
+
if (scope) {
|
|
611
|
+
let key = scope.trim();
|
|
612
|
+
if (key.startsWith("{") && key.endsWith("}")) {
|
|
613
|
+
const inner = key.slice(1, -1).trim();
|
|
614
|
+
let cutIdx = -1;
|
|
615
|
+
const commaIdx = inner.indexOf(",");
|
|
616
|
+
const colonIdx = inner.indexOf(":");
|
|
617
|
+
if (commaIdx !== -1 && colonIdx !== -1) {
|
|
618
|
+
cutIdx = Math.min(commaIdx, colonIdx);
|
|
619
|
+
} else if (commaIdx !== -1) {
|
|
620
|
+
cutIdx = commaIdx;
|
|
621
|
+
} else if (colonIdx !== -1) {
|
|
622
|
+
cutIdx = colonIdx;
|
|
623
|
+
}
|
|
624
|
+
const first = (cutIdx === -1 ? inner : inner.slice(0, cutIdx)).trim();
|
|
625
|
+
if (first) {
|
|
626
|
+
key = first;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return key;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
parent = getParent(hierarchyMap2, parent);
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
528
636
|
function getContainedInVForDirectiveKeyValue(context, node, hierarchyMap2) {
|
|
529
637
|
if (!context.scopes.vFor || context.scopes.vFor === 0) {
|
|
530
638
|
return null;
|
|
@@ -614,6 +722,9 @@ function tryGetContainedInStaticVForSourceLiteralValues(context, _node, _hierarc
|
|
|
614
722
|
function getParent(hierarchyMap2, node) {
|
|
615
723
|
return hierarchyMap2.get(node) || null;
|
|
616
724
|
}
|
|
725
|
+
function nodeHandlerAttributeValue(node) {
|
|
726
|
+
return nodeHandlerAttributeInfo(node)?.semanticNameHint ?? null;
|
|
727
|
+
}
|
|
617
728
|
function nodeHandlerAttributeInfo(node) {
|
|
618
729
|
const handlerDirective = findDirectiveByName(node, "bind", "handler");
|
|
619
730
|
if (!handlerDirective?.exp) {
|
|
@@ -972,7 +1083,7 @@ function generateToDirectiveDataTestId(componentName, node, toDirective, context
|
|
|
972
1083
|
}
|
|
973
1084
|
const source = stringifyExpression(toDirective.exp);
|
|
974
1085
|
const toAst = toDirective.exp.ast;
|
|
975
|
-
const interpolated =
|
|
1086
|
+
const interpolated = toAst !== void 0 && toAst !== null && toAst !== false && isTemplateLiteral(toAst);
|
|
976
1087
|
return templateAttributeValue(`${componentName}-\${${source}${interpolated ? ".replaceAll(' ', '')" : "?.name?.replaceAll(' ', '') ?? ''"}}${formatTagName(node, nativeWrappers)}`);
|
|
977
1088
|
} else {
|
|
978
1089
|
const innerText = getInnerText(node);
|
|
@@ -1033,7 +1144,7 @@ function toDirectiveObjectFieldNameValue(node) {
|
|
|
1033
1144
|
return null;
|
|
1034
1145
|
}
|
|
1035
1146
|
}
|
|
1036
|
-
function getComposedClickHandlerContent(node, _context,
|
|
1147
|
+
function getComposedClickHandlerContent(node, _context, innerText, clickDirective, _options = {}) {
|
|
1037
1148
|
const click = clickDirective ?? tryGetClickDirective(node);
|
|
1038
1149
|
if (!click) {
|
|
1039
1150
|
return "";
|
|
@@ -1052,8 +1163,9 @@ function getComposedClickHandlerContent(node, _context, _innerText, clickDirecti
|
|
|
1052
1163
|
}
|
|
1053
1164
|
}
|
|
1054
1165
|
}
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1166
|
+
const hName = normalizeHandlerName(handlerName);
|
|
1167
|
+
const name = hName || innerText || "";
|
|
1168
|
+
const normalizedHandlerSegment = name ? `-${toPascalCase(name)}` : "";
|
|
1057
1169
|
const result = normalizedHandlerSegment;
|
|
1058
1170
|
return result.replace(/[^a-z-]/gi, "");
|
|
1059
1171
|
}
|
|
@@ -1349,6 +1461,31 @@ function tryGetExistingElementDataTestId(node, attributeName = "data-testid") {
|
|
|
1349
1461
|
if (!raw) {
|
|
1350
1462
|
return null;
|
|
1351
1463
|
}
|
|
1464
|
+
try {
|
|
1465
|
+
const ast2 = parseExpression(raw, { plugins: ["typescript"] });
|
|
1466
|
+
if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "TemplateLiteral") {
|
|
1467
|
+
const tl = ast2;
|
|
1468
|
+
const cooked = (tl.quasis ?? []).map((q) => q.value?.cooked ?? "").join("");
|
|
1469
|
+
const expressionCount = (tl.expressions ?? []).length;
|
|
1470
|
+
const isStatic = expressionCount === 0;
|
|
1471
|
+
const unwrappedTemplate = raw.startsWith("`") && raw.endsWith("`") && raw.length >= 2 ? raw.slice(1, -1) : cooked;
|
|
1472
|
+
if (isStatic) {
|
|
1473
|
+
return { value: unwrappedTemplate, isDynamic: false, isStaticLiteral: true };
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
value: unwrappedTemplate,
|
|
1477
|
+
isDynamic: true,
|
|
1478
|
+
isStaticLiteral: false,
|
|
1479
|
+
template: unwrappedTemplate,
|
|
1480
|
+
templateExpressionCount: expressionCount
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
if (ast2 && typeof ast2 === "object" && "type" in ast2 && ast2.type === "StringLiteral") {
|
|
1484
|
+
const sl = ast2;
|
|
1485
|
+
return { value: sl.value ?? "", isDynamic: false, isStaticLiteral: true };
|
|
1486
|
+
}
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1352
1489
|
return { value: raw, isDynamic: true, isStaticLiteral: false, rawExpression: raw };
|
|
1353
1490
|
}
|
|
1354
1491
|
function isTemplatePlaceholder(part) {
|
|
@@ -1476,13 +1613,15 @@ Existing ${attrLabel}: ${JSON.stringify(existing.value)}
|
|
|
1476
1613
|
Fix: reduce the template to a single key-based interpolation, or remove the explicit ${attrLabel} so it can be auto-generated.`
|
|
1477
1614
|
);
|
|
1478
1615
|
}
|
|
1479
|
-
|
|
1616
|
+
const hasExact = args.bestKeyPlaceholder && existing.template.includes(args.bestKeyPlaceholder);
|
|
1617
|
+
const hasVarAccess = args.bestKeyVariable && existing.template.includes(args.bestKeyVariable);
|
|
1618
|
+
if (!hasExact && !hasVarAccess && args.bestKeyPlaceholder) {
|
|
1480
1619
|
throw new Error(
|
|
1481
1620
|
`[vue-pom-generator] Existing ${attrLabel} appears to be missing the key placeholder needed to keep it unique.
|
|
1482
1621
|
Component: ${args.componentName}
|
|
1483
1622
|
File: ${file}:${locationHint}
|
|
1484
1623
|
Existing ${attrLabel}: ${JSON.stringify(existing.value)}
|
|
1485
|
-
Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}
|
|
1624
|
+
Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
|
|
1486
1625
|
|
|
1487
1626
|
Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
|
|
1488
1627
|
);
|
|
@@ -1507,7 +1646,7 @@ If you really need a computed id, do not set existingIdBehavior="preserve".`
|
|
|
1507
1646
|
Component: ${args.componentName}
|
|
1508
1647
|
File: ${file}:${locationHint}
|
|
1509
1648
|
Existing ${attrLabel}: ${JSON.stringify(existing.value)}
|
|
1510
|
-
Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}
|
|
1649
|
+
Required placeholder: ${JSON.stringify(args.bestKeyPlaceholder)}${args.bestKeyVariable ? ` or an access on "${args.bestKeyVariable}"` : ""}
|
|
1511
1650
|
|
|
1512
1651
|
Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} template literal, or (2) remove the explicit ${attrLabel} so it can be auto-generated.`
|
|
1513
1652
|
);
|
|
@@ -1534,6 +1673,7 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
|
|
|
1534
1673
|
case "checkbox":
|
|
1535
1674
|
case "toggle":
|
|
1536
1675
|
case "radio":
|
|
1676
|
+
case "grid":
|
|
1537
1677
|
return role;
|
|
1538
1678
|
default:
|
|
1539
1679
|
return void 0;
|
|
@@ -1628,10 +1768,10 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
|
|
|
1628
1768
|
args.dependencies.__pomPrimaryByGetterName ??= /* @__PURE__ */ new Map();
|
|
1629
1769
|
const primaryByActionName = args.dependencies.__pomPrimaryByActionName;
|
|
1630
1770
|
const hintCandidates = (() => {
|
|
1631
|
-
const baseHints = [
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1771
|
+
const baseHints = [
|
|
1772
|
+
args.semanticNameHint,
|
|
1773
|
+
...args.semanticNameHintAlternates ?? []
|
|
1774
|
+
];
|
|
1635
1775
|
const out = [];
|
|
1636
1776
|
const seen = /* @__PURE__ */ new Set();
|
|
1637
1777
|
for (const h of baseHints) {
|
|
@@ -2380,6 +2520,84 @@ async function ensureDomShim() {
|
|
|
2380
2520
|
if (!g.requestAnimationFrame)
|
|
2381
2521
|
g.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 16);
|
|
2382
2522
|
}
|
|
2523
|
+
async function introspectNuxtPages(projectRoot) {
|
|
2524
|
+
const possiblePagesDirs = ["app/pages", "pages"];
|
|
2525
|
+
let pagesDir = "";
|
|
2526
|
+
for (const dir of possiblePagesDirs) {
|
|
2527
|
+
const abs = path.resolve(projectRoot, dir);
|
|
2528
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
|
|
2529
|
+
pagesDir = abs;
|
|
2530
|
+
break;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
if (!pagesDir) {
|
|
2534
|
+
debugLog(`[router-introspection][nuxt] Could not find pages directory in ${projectRoot}`);
|
|
2535
|
+
return { routeNameMap: /* @__PURE__ */ new Map(), routePathMap: /* @__PURE__ */ new Map(), routeMetaEntries: [] };
|
|
2536
|
+
}
|
|
2537
|
+
const routeMetaEntries = [];
|
|
2538
|
+
const walk = (dir, baseRoute) => {
|
|
2539
|
+
const files = fs.readdirSync(dir);
|
|
2540
|
+
for (const file of files) {
|
|
2541
|
+
const fullPath = path.join(dir, file);
|
|
2542
|
+
const stat = fs.statSync(fullPath);
|
|
2543
|
+
if (stat.isDirectory()) {
|
|
2544
|
+
walk(fullPath, `${baseRoute}/${file}`);
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
if (!file.endsWith(".vue"))
|
|
2548
|
+
continue;
|
|
2549
|
+
const componentName = file.slice(0, -4);
|
|
2550
|
+
if (componentName === "index" && baseRoute === "") {
|
|
2551
|
+
routeMetaEntries.push({
|
|
2552
|
+
componentName: "index",
|
|
2553
|
+
pathTemplate: "/",
|
|
2554
|
+
params: [],
|
|
2555
|
+
query: []
|
|
2556
|
+
});
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
let routePath = componentName === "index" ? baseRoute : `${baseRoute}/${componentName}`;
|
|
2560
|
+
if (!routePath.startsWith("/"))
|
|
2561
|
+
routePath = `/${routePath}`;
|
|
2562
|
+
const params = [];
|
|
2563
|
+
let pathTemplate = "";
|
|
2564
|
+
for (let i = 0; i < routePath.length; i++) {
|
|
2565
|
+
const ch = routePath[i];
|
|
2566
|
+
if (ch !== "[") {
|
|
2567
|
+
pathTemplate += ch;
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
let name = "";
|
|
2571
|
+
i++;
|
|
2572
|
+
while (i < routePath.length) {
|
|
2573
|
+
const c = routePath[i];
|
|
2574
|
+
if (c === "]")
|
|
2575
|
+
break;
|
|
2576
|
+
name += c;
|
|
2577
|
+
i++;
|
|
2578
|
+
}
|
|
2579
|
+
if (name) {
|
|
2580
|
+
params.push({ name, optional: false });
|
|
2581
|
+
pathTemplate += `:${name}`;
|
|
2582
|
+
} else {
|
|
2583
|
+
pathTemplate += "[]";
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
routeMetaEntries.push({
|
|
2587
|
+
componentName,
|
|
2588
|
+
pathTemplate,
|
|
2589
|
+
params,
|
|
2590
|
+
query: []
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
walk(pagesDir, "");
|
|
2595
|
+
return {
|
|
2596
|
+
routeNameMap: /* @__PURE__ */ new Map(),
|
|
2597
|
+
routePathMap: /* @__PURE__ */ new Map(),
|
|
2598
|
+
routeMetaEntries
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2383
2601
|
async function parseRouterFileFromCwd(routerEntryPath) {
|
|
2384
2602
|
return await runRouterIntrospectionExclusive(async () => {
|
|
2385
2603
|
const routerEntry = path.resolve(routerEntryPath);
|
|
@@ -2495,9 +2713,9 @@ function resolveRouterEntry(projectRoot, routerEntry) {
|
|
|
2495
2713
|
const root = projectRoot ?? process.cwd();
|
|
2496
2714
|
return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
|
|
2497
2715
|
}
|
|
2498
|
-
async function getRouteMetaByComponent(projectRoot, routerEntry) {
|
|
2499
|
-
const
|
|
2500
|
-
const { routeMetaEntries } = await parseRouterFileFromCwd(
|
|
2716
|
+
async function getRouteMetaByComponent(projectRoot, routerEntry, routerType) {
|
|
2717
|
+
const root = projectRoot ?? process.cwd();
|
|
2718
|
+
const { routeMetaEntries } = routerType === "nuxt" ? await introspectNuxtPages(root) : await parseRouterFileFromCwd(resolveRouterEntry(root, routerEntry));
|
|
2501
2719
|
const map = /* @__PURE__ */ new Map();
|
|
2502
2720
|
for (const entry of routeMetaEntries) {
|
|
2503
2721
|
const list = map.get(entry.componentName) ?? [];
|
|
@@ -2650,12 +2868,14 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
|
|
|
2650
2868
|
customPomImportAliases,
|
|
2651
2869
|
testIdAttribute,
|
|
2652
2870
|
emitLanguages: emitLanguagesOverride,
|
|
2871
|
+
csharp,
|
|
2653
2872
|
vueRouterFluentChaining,
|
|
2654
|
-
routerEntry
|
|
2873
|
+
routerEntry,
|
|
2874
|
+
routerType
|
|
2655
2875
|
} = options;
|
|
2656
2876
|
const emitLanguages = emitLanguagesOverride?.length ? emitLanguagesOverride : ["ts"];
|
|
2657
2877
|
const outDir = outDirOverride ?? "./pom";
|
|
2658
|
-
const routeMetaByComponent = vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry) : void 0;
|
|
2878
|
+
const routeMetaByComponent = vueRouterFluentChaining ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType) : void 0;
|
|
2659
2879
|
if (emitLanguages.includes("ts")) {
|
|
2660
2880
|
const files = await generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
|
|
2661
2881
|
customPomAttachments,
|
|
@@ -2677,7 +2897,8 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
|
|
|
2677
2897
|
}
|
|
2678
2898
|
if (emitLanguages.includes("csharp")) {
|
|
2679
2899
|
const csFiles = generateAggregatedCSharpFiles(componentHierarchyMap, outDir, {
|
|
2680
|
-
|
|
2900
|
+
testIdAttribute,
|
|
2901
|
+
csharp
|
|
2681
2902
|
});
|
|
2682
2903
|
for (const file of csFiles) {
|
|
2683
2904
|
createFile(file.filePath, file.content);
|
|
@@ -2700,11 +2921,11 @@ function toCSharpParam(paramTypeExpr) {
|
|
|
2700
2921
|
const right = eqIdx >= 0 ? trimmed.slice(eqIdx + 1).trim() : void 0;
|
|
2701
2922
|
const typePart = left.includes("|") ? "string" : left;
|
|
2702
2923
|
let type = "string";
|
|
2703
|
-
if (/(
|
|
2924
|
+
if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
|
|
2704
2925
|
type = "bool";
|
|
2705
|
-
else if (/(
|
|
2926
|
+
else if (/(?:^|\s)string(?:\s|$)/.test(typePart))
|
|
2706
2927
|
type = "string";
|
|
2707
|
-
else if (/(
|
|
2928
|
+
else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
|
|
2708
2929
|
type = "int";
|
|
2709
2930
|
else if (/\d+/.test(typePart) && typePart === "")
|
|
2710
2931
|
type = "int";
|
|
@@ -2741,8 +2962,9 @@ function formatCSharpParams(params) {
|
|
|
2741
2962
|
return { signature: signatureParts.join(", "), argNames };
|
|
2742
2963
|
}
|
|
2743
2964
|
function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options = {}) {
|
|
2744
|
-
const
|
|
2745
|
-
const
|
|
2965
|
+
const outAbs = ensureDir(outDir);
|
|
2966
|
+
const namespace = options.csharp?.namespace ?? "Playwright.Generated";
|
|
2967
|
+
const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
|
|
2746
2968
|
const entries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
2747
2969
|
const header = [
|
|
2748
2970
|
"// <auto-generated>",
|
|
@@ -2756,13 +2978,13 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
|
|
|
2756
2978
|
"using System.Threading.Tasks;",
|
|
2757
2979
|
"using Microsoft.Playwright;",
|
|
2758
2980
|
"",
|
|
2759
|
-
|
|
2981
|
+
`namespace ${namespace};`,
|
|
2760
2982
|
"",
|
|
2761
|
-
"public abstract class BasePage",
|
|
2983
|
+
"public abstract partial class BasePage",
|
|
2762
2984
|
"{",
|
|
2763
2985
|
" protected BasePage(IPage page) => Page = page;",
|
|
2764
2986
|
" protected IPage Page { get; }",
|
|
2765
|
-
|
|
2987
|
+
` protected ILocator LocatorByTestId(string testId) => Page.Locator($"[${testIdAttribute}=\\"{testId}\\"]");`,
|
|
2766
2988
|
"",
|
|
2767
2989
|
" // Minimal vue-select helper mirroring the TS BasePage.selectVSelectByTestId behavior.",
|
|
2768
2990
|
" // Note: annotationText is currently a no-op in C# output (we don't render a cursor overlay).",
|
|
@@ -2786,10 +3008,11 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
|
|
|
2786
3008
|
].join("\n");
|
|
2787
3009
|
const chunks = [header];
|
|
2788
3010
|
for (const [componentName, deps] of entries) {
|
|
3011
|
+
const className = toPascalCaseLocal(componentName);
|
|
2789
3012
|
chunks.push(
|
|
2790
|
-
`public
|
|
3013
|
+
`public partial class ${className} : BasePage
|
|
2791
3014
|
{
|
|
2792
|
-
public ${
|
|
3015
|
+
public ${className}(IPage page) : base(page) { }
|
|
2793
3016
|
`
|
|
2794
3017
|
);
|
|
2795
3018
|
const primaries = Array.from(deps.dataTestIdSet ?? []).map((e) => ({ pom: e.pom, target: e.targetPageObjectModelClass })).filter((x) => !!x.pom).sort((a, b) => a.pom.methodName.localeCompare(b.pom.methodName));
|
|
@@ -2928,7 +3151,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
|
|
|
2928
3151
|
return [{ filePath: outputFile, content: chunks.join("\n") }];
|
|
2929
3152
|
}
|
|
2930
3153
|
function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
|
|
2931
|
-
const { generateFixtures, pomOutDir
|
|
3154
|
+
const { generateFixtures, pomOutDir } = options;
|
|
2932
3155
|
if (!generateFixtures)
|
|
2933
3156
|
return;
|
|
2934
3157
|
const defaultFixtureOutDirRel = pomOutDir;
|
|
@@ -2936,7 +3159,7 @@ function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
|
|
|
2936
3159
|
const looksLikeFilePath = fixtureOutRel.endsWith(".ts") || fixtureOutRel.endsWith(".tsx") || fixtureOutRel.endsWith(".mts") || fixtureOutRel.endsWith(".cts");
|
|
2937
3160
|
const fixtureOutDirRel = looksLikeFilePath ? path.dirname(fixtureOutRel) : fixtureOutRel;
|
|
2938
3161
|
const fixtureFileName = looksLikeFilePath ? path.basename(fixtureOutRel) : "fixtures.g.ts";
|
|
2939
|
-
const root = projectRoot ?? process.cwd();
|
|
3162
|
+
const root = options.projectRoot ?? process.cwd();
|
|
2940
3163
|
const fixtureOutDirAbs = path.isAbsolute(fixtureOutDirRel) ? fixtureOutDirRel : path.resolve(root, fixtureOutDirRel);
|
|
2941
3164
|
const pomDirAbs = path.isAbsolute(pomOutDir) ? pomOutDir : path.resolve(root, pomOutDir);
|
|
2942
3165
|
const pomImport = toPosixRelativePath(fixtureOutDirAbs, pomDirAbs);
|
|
@@ -3177,7 +3400,7 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
|
|
|
3177
3400
|
const views = entries.filter(([, d]) => d.isView);
|
|
3178
3401
|
const components = entries.filter(([, d]) => !d.isView);
|
|
3179
3402
|
const makeAggregatedContent = (header2, outputDir, items) => {
|
|
3180
|
-
const imports = [
|
|
3403
|
+
const imports = [];
|
|
3181
3404
|
if (!basePageClassPath) {
|
|
3182
3405
|
throw new Error("basePageClassPath is required for aggregated generation");
|
|
3183
3406
|
}
|
|
@@ -3190,6 +3413,16 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
|
|
|
3190
3413
|
" err?: string;",
|
|
3191
3414
|
"}"
|
|
3192
3415
|
].join("\n");
|
|
3416
|
+
const inlinePlaywrightTypesModule = () => {
|
|
3417
|
+
const typesPath = fileURLToPath(new URL("./playwright-types.ts", import.meta.url));
|
|
3418
|
+
let typesSource = "";
|
|
3419
|
+
try {
|
|
3420
|
+
typesSource = fs.readFileSync(typesPath, "utf8");
|
|
3421
|
+
} catch {
|
|
3422
|
+
throw new Error(`Failed to read playwright-types.ts at ${typesPath}`);
|
|
3423
|
+
}
|
|
3424
|
+
return typesSource.trim();
|
|
3425
|
+
};
|
|
3193
3426
|
const inlinePointerModule = () => {
|
|
3194
3427
|
const pointerPath = fileURLToPath(new URL("./Pointer.ts", import.meta.url));
|
|
3195
3428
|
let pointerSource = "";
|
|
@@ -3212,6 +3445,10 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
|
|
|
3212
3445
|
/import\s+type\s*\{\s*Locator\s+as\s+PwLocator\s*,\s*Page\s+as\s+PwPage\s*\}\s*from\s*["']@playwright\/test["'];?\s*/,
|
|
3213
3446
|
""
|
|
3214
3447
|
);
|
|
3448
|
+
pointerSource = pointerSource.replace(
|
|
3449
|
+
/import\s+type\s*\{\s*PwLocator\s*,\s*PwPage\s*\}\s*from\s*["']\.\/playwright-types["'];?\s*/,
|
|
3450
|
+
""
|
|
3451
|
+
);
|
|
3215
3452
|
return pointerSource.trim();
|
|
3216
3453
|
};
|
|
3217
3454
|
const inlineBasePageModule = () => {
|
|
@@ -3236,18 +3473,23 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
|
|
|
3236
3473
|
""
|
|
3237
3474
|
);
|
|
3238
3475
|
basePageSource = basePageSource.replace(
|
|
3239
|
-
/import\s*\{\s*
|
|
3476
|
+
/import\s+type\s*\{\s*PwLocator\s*,\s*PwPage\s*\}\s*from\s*["']\.\/playwright-types["'];?\s*/,
|
|
3477
|
+
""
|
|
3478
|
+
);
|
|
3479
|
+
basePageSource = basePageSource.replace(
|
|
3480
|
+
/import\s+(?:type\s*)?\{[\s\S]*?\}\s*from\s*["']\.\/Pointer["'];?\s*/g,
|
|
3240
3481
|
""
|
|
3241
3482
|
);
|
|
3242
3483
|
return basePageSource.trim();
|
|
3243
3484
|
};
|
|
3485
|
+
const playwrightTypesInline = inlinePlaywrightTypesModule();
|
|
3244
3486
|
const pointerInline = inlinePointerModule();
|
|
3245
3487
|
const basePageInline = inlineBasePageModule();
|
|
3246
3488
|
const addCustomPomImports = () => {
|
|
3247
3489
|
const importAliases = {
|
|
3248
3490
|
Toggle: "ToggleWidget",
|
|
3249
3491
|
Checkbox: "CheckboxWidget",
|
|
3250
|
-
...options.customPomImportAliases
|
|
3492
|
+
...options.customPomImportAliases
|
|
3251
3493
|
};
|
|
3252
3494
|
const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
|
|
3253
3495
|
const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
|
|
@@ -3430,6 +3672,8 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
|
|
|
3430
3672
|
header2,
|
|
3431
3673
|
...imports,
|
|
3432
3674
|
"",
|
|
3675
|
+
playwrightTypesInline,
|
|
3676
|
+
"",
|
|
3433
3677
|
pointerInline,
|
|
3434
3678
|
"",
|
|
3435
3679
|
basePageInline,
|
|
@@ -3441,7 +3685,8 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
|
|
|
3441
3685
|
};
|
|
3442
3686
|
const base = ensureDir(outDir);
|
|
3443
3687
|
const outputFile = path.join(base, "page-object-models.g.ts");
|
|
3444
|
-
const header =
|
|
3688
|
+
const header = `/// <reference lib="es2015" />
|
|
3689
|
+
${eslintSuppressionHeader}/**
|
|
3445
3690
|
* Aggregated generated POMs
|
|
3446
3691
|
${AUTO_GENERATED_COMMENT}`;
|
|
3447
3692
|
const content = makeAggregatedContent(header, path.dirname(outputFile), [...views, ...components]);
|
|
@@ -3588,6 +3833,7 @@ function createBuildProcessorPlugin(options) {
|
|
|
3588
3833
|
normalizedBasePagePath,
|
|
3589
3834
|
outDir,
|
|
3590
3835
|
emitLanguages,
|
|
3836
|
+
csharp,
|
|
3591
3837
|
generateFixtures,
|
|
3592
3838
|
customPomAttachments,
|
|
3593
3839
|
projectRootRef,
|
|
@@ -3596,6 +3842,7 @@ function createBuildProcessorPlugin(options) {
|
|
|
3596
3842
|
testIdAttribute,
|
|
3597
3843
|
routerAwarePoms,
|
|
3598
3844
|
resolvedRouterEntry,
|
|
3845
|
+
routerType,
|
|
3599
3846
|
loggerRef
|
|
3600
3847
|
} = options;
|
|
3601
3848
|
let lastGeneratedEntryCount = 0;
|
|
@@ -3610,9 +3857,15 @@ function createBuildProcessorPlugin(options) {
|
|
|
3610
3857
|
setResolveToComponentNameFn(() => null);
|
|
3611
3858
|
return;
|
|
3612
3859
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3860
|
+
let result;
|
|
3861
|
+
if (routerType === "nuxt") {
|
|
3862
|
+
result = await introspectNuxtPages(projectRootRef.current);
|
|
3863
|
+
} else {
|
|
3864
|
+
if (!resolvedRouterEntry)
|
|
3865
|
+
throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
|
|
3866
|
+
result = await parseRouterFileFromCwd(resolvedRouterEntry);
|
|
3867
|
+
}
|
|
3868
|
+
const { routeNameMap, routePathMap } = result;
|
|
3616
3869
|
setRouteNameToComponentNameMap(routeNameMap);
|
|
3617
3870
|
setResolveToComponentNameFn((to) => {
|
|
3618
3871
|
if (typeof to === "string") {
|
|
@@ -3649,6 +3902,7 @@ function createBuildProcessorPlugin(options) {
|
|
|
3649
3902
|
generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
|
|
3650
3903
|
outDir,
|
|
3651
3904
|
emitLanguages,
|
|
3905
|
+
csharp,
|
|
3652
3906
|
generateFixtures,
|
|
3653
3907
|
customPomAttachments,
|
|
3654
3908
|
projectRoot: projectRootRef.current,
|
|
@@ -3656,7 +3910,8 @@ function createBuildProcessorPlugin(options) {
|
|
|
3656
3910
|
customPomImportAliases,
|
|
3657
3911
|
testIdAttribute,
|
|
3658
3912
|
vueRouterFluentChaining: routerAwarePoms,
|
|
3659
|
-
routerEntry: resolvedRouterEntry
|
|
3913
|
+
routerEntry: resolvedRouterEntry,
|
|
3914
|
+
routerType
|
|
3660
3915
|
});
|
|
3661
3916
|
lastGeneratedEntryCount = entryCount;
|
|
3662
3917
|
loggerRef.current.info(`generated POMs (${entryCount} entries)`);
|
|
@@ -3785,7 +4040,7 @@ function tryExtractStableHintFromConditionalExpressionSource(source) {
|
|
|
3785
4040
|
return null;
|
|
3786
4041
|
}
|
|
3787
4042
|
}
|
|
3788
|
-
function tryInferNativeWrapperRoleFromSfc(tag) {
|
|
4043
|
+
function tryInferNativeWrapperRoleFromSfc(tag, vueFilesPathMap) {
|
|
3789
4044
|
const first = tag.charCodeAt(0);
|
|
3790
4045
|
const isUpper = first >= 65 && first <= 90;
|
|
3791
4046
|
if (!isUpper)
|
|
@@ -3794,8 +4049,14 @@ function tryInferNativeWrapperRoleFromSfc(tag) {
|
|
|
3794
4049
|
if (cached)
|
|
3795
4050
|
return cached;
|
|
3796
4051
|
const candidates = [
|
|
3797
|
-
path.resolve(process.cwd(), "src/components", `${tag}.vue`)
|
|
4052
|
+
path.resolve(process.cwd(), "src/components", `${tag}.vue`),
|
|
4053
|
+
path.resolve(process.cwd(), "components", `${tag}.vue`),
|
|
4054
|
+
path.resolve(process.cwd(), "app/components", `${tag}.vue`)
|
|
3798
4055
|
];
|
|
4056
|
+
const registeredPath = vueFilesPathMap?.get(tag);
|
|
4057
|
+
if (registeredPath) {
|
|
4058
|
+
candidates.unshift(path.resolve(process.cwd(), registeredPath));
|
|
4059
|
+
}
|
|
3799
4060
|
const filePath = candidates.find((p) => fs.existsSync(p));
|
|
3800
4061
|
if (!filePath) {
|
|
3801
4062
|
inferredNativeWrapperConfigByTag.set(tag, { role: "" });
|
|
@@ -3836,14 +4097,22 @@ function tryInferNativeWrapperRoleFromSfc(tag) {
|
|
|
3836
4097
|
inferredNativeWrapperConfigByTag.set(tag, { role: "" });
|
|
3837
4098
|
return null;
|
|
3838
4099
|
}
|
|
3839
|
-
if (rootTag === "input" || rootTag === "textarea") {
|
|
4100
|
+
if (rootTag === "input" || rootTag === "textarea" || rootTag === "uinput" || rootTag === "utextarea") {
|
|
3840
4101
|
inferredNativeWrapperConfigByTag.set(tag, { role: "input" });
|
|
3841
4102
|
return { role: "input" };
|
|
3842
4103
|
}
|
|
3843
|
-
if (rootTag === "select") {
|
|
4104
|
+
if (rootTag === "select" || rootTag === "uselect") {
|
|
3844
4105
|
inferredNativeWrapperConfigByTag.set(tag, { role: "select" });
|
|
3845
4106
|
return { role: "select" };
|
|
3846
4107
|
}
|
|
4108
|
+
if (rootTag === "vselect") {
|
|
4109
|
+
inferredNativeWrapperConfigByTag.set(tag, { role: "vselect" });
|
|
4110
|
+
return { role: "vselect" };
|
|
4111
|
+
}
|
|
4112
|
+
if (rootTag === "button" || rootTag === "ubutton") {
|
|
4113
|
+
inferredNativeWrapperConfigByTag.set(tag, { role: "button" });
|
|
4114
|
+
return { role: "button" };
|
|
4115
|
+
}
|
|
3847
4116
|
inferredNativeWrapperConfigByTag.set(tag, { role: "" });
|
|
3848
4117
|
return null;
|
|
3849
4118
|
}
|
|
@@ -4018,6 +4287,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4018
4287
|
const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
|
|
4019
4288
|
const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
|
|
4020
4289
|
const warn = options.warn;
|
|
4290
|
+
const vueFilesPathMap = options.vueFilesPathMap;
|
|
4021
4291
|
const safeRealpath = (p) => {
|
|
4022
4292
|
try {
|
|
4023
4293
|
return fs.existsSync(p) ? fs.realpathSync(p) : p;
|
|
@@ -4069,18 +4339,14 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4069
4339
|
const parentElement = !parentIsRoot && context?.parent?.type === NodeTypes.ELEMENT ? context.parent : null;
|
|
4070
4340
|
hierarchyMap.set(element, parentElement);
|
|
4071
4341
|
const normalizeFilePath = (filePath) => path.normalize(safeRealpath(path.resolve(filePath)));
|
|
4072
|
-
const getParentComponentName = () => {
|
|
4073
|
-
const normalizedFilePath2 = normalizeFilePath(context.filename);
|
|
4074
|
-
return path.basename(normalizedFilePath2, ".vue");
|
|
4075
|
-
};
|
|
4076
|
-
const parentComponentName = getParentComponentName();
|
|
4077
4342
|
const normalizedFilePath = normalizeFilePath(context.filename);
|
|
4078
|
-
const
|
|
4079
|
-
const
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4343
|
+
const parentComponentName = componentName;
|
|
4344
|
+
const dependencies = (() => {
|
|
4345
|
+
let deps = componentHierarchyMap.get(componentName);
|
|
4346
|
+
if (!deps) {
|
|
4347
|
+
const relToViewsDir = path.relative(normalizedViewsDirAbs, normalizedFilePath);
|
|
4348
|
+
const isView = !relToViewsDir.startsWith("..") && !path.isAbsolute(relToViewsDir);
|
|
4349
|
+
deps = {
|
|
4084
4350
|
filePath: context.filename,
|
|
4085
4351
|
childrenComponentSet: /* @__PURE__ */ new Set(),
|
|
4086
4352
|
usedComponentSet: /* @__PURE__ */ new Set(),
|
|
@@ -4088,11 +4354,10 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4088
4354
|
isView,
|
|
4089
4355
|
methodsContent: ""
|
|
4090
4356
|
};
|
|
4091
|
-
componentHierarchyMap.set(
|
|
4357
|
+
componentHierarchyMap.set(componentName, deps);
|
|
4092
4358
|
}
|
|
4093
|
-
return
|
|
4094
|
-
};
|
|
4095
|
-
const dependencies = ensureDependencies(parentComponentName);
|
|
4359
|
+
return deps;
|
|
4360
|
+
})();
|
|
4096
4361
|
const isComponentLikeTag = (tag) => {
|
|
4097
4362
|
if (!tag) return false;
|
|
4098
4363
|
const first = tag.charCodeAt(0);
|
|
@@ -4103,13 +4368,24 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4103
4368
|
dependencies.usedComponentSet.add(element.tag);
|
|
4104
4369
|
}
|
|
4105
4370
|
if (!nativeWrappers[element.tag]) {
|
|
4106
|
-
const inferred = tryInferNativeWrapperRoleFromSfc(element.tag);
|
|
4371
|
+
const inferred = tryInferNativeWrapperRoleFromSfc(element.tag, vueFilesPathMap);
|
|
4107
4372
|
if (inferred?.role) {
|
|
4108
4373
|
nativeWrappers[element.tag] = { role: inferred.role };
|
|
4374
|
+
} else if (element.tag.endsWith("Button") || element.tag === "AylaButton") {
|
|
4375
|
+
nativeWrappers[element.tag] = { role: "button" };
|
|
4376
|
+
} else if (element.tag === "DxDataGrid") {
|
|
4377
|
+
nativeWrappers[element.tag] = { role: "grid" };
|
|
4109
4378
|
}
|
|
4110
4379
|
}
|
|
4111
|
-
const getBestAvailableKeyValue = () =>
|
|
4112
|
-
|
|
4380
|
+
const getBestAvailableKeyValue = () => {
|
|
4381
|
+
const vForKey = getKeyDirectiveValue(element, context) || getSelfClosingForDirectiveKeyAttrValue(element) || getContainedInVForDirectiveKeyValue(context, element, hierarchyMap);
|
|
4382
|
+
if (vForKey) return vForKey;
|
|
4383
|
+
return getContainedInSlotDataKeyValue(element, hierarchyMap);
|
|
4384
|
+
};
|
|
4385
|
+
const bestKeyInferred = getBestAvailableKeyValue();
|
|
4386
|
+
const isSlotKey = bestKeyInferred && !bestKeyInferred.startsWith("${");
|
|
4387
|
+
const bestKeyPlaceholder = isSlotKey ? `\${${bestKeyInferred}}` : bestKeyInferred;
|
|
4388
|
+
const bestKeyVariable = isSlotKey ? bestKeyInferred : null;
|
|
4113
4389
|
const keyValuesOverride = tryGetContainedInStaticVForSourceLiteralValues(context);
|
|
4114
4390
|
const parentKey = !parentIsRoot && context?.parent ? context.parent : null;
|
|
4115
4391
|
const conditional = getConditionalDirectiveInfo(element);
|
|
@@ -4194,6 +4470,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4194
4470
|
nativeRole,
|
|
4195
4471
|
preferredGeneratedValue: args.preferredGeneratedValue,
|
|
4196
4472
|
bestKeyPlaceholder,
|
|
4473
|
+
bestKeyVariable,
|
|
4197
4474
|
keyValuesOverride,
|
|
4198
4475
|
entryOverrides: args.entryOverrides,
|
|
4199
4476
|
semanticNameHint: args.semanticNameHint,
|
|
@@ -4222,16 +4499,18 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4222
4499
|
});
|
|
4223
4500
|
return;
|
|
4224
4501
|
}
|
|
4502
|
+
const innerText = getInnerText(element) || null;
|
|
4225
4503
|
const toDirective = nodeHasToDirective(element);
|
|
4226
4504
|
if (toDirective) {
|
|
4227
4505
|
const dataTestId = generateToDirectiveDataTestId(componentName, element, toDirective, context, hierarchyMap, nativeWrappers);
|
|
4228
4506
|
const target = tryResolveToDirectiveTargetComponentName(toDirective);
|
|
4229
4507
|
const routeNameHint = toDirectiveObjectFieldNameValue(toDirective);
|
|
4230
|
-
const
|
|
4508
|
+
const existing = tryGetExistingElementDataTestId(element, testIdAttribute);
|
|
4509
|
+
const semanticNameHint2 = routeNameHint || target || void 0;
|
|
4510
|
+
const alternates = (target ? [] : [innerText, existing?.value, conditionalHint]).filter(Boolean);
|
|
4231
4511
|
const rawTo = (toDirective.exp?.loc?.source ?? "").trim();
|
|
4232
4512
|
const pomMergeKey = routeNameHint ? `to:name:${routeNameHint}` : rawTo ? `to:expr:${rawTo}` : void 0;
|
|
4233
|
-
const
|
|
4234
|
-
const preferredGeneratedValue = dataTestId ?? (existing ? staticAttributeValue(existing.value) : null);
|
|
4513
|
+
const preferredGeneratedValue = dataTestId ?? (existing ? existing.isDynamic ? templateAttributeValue(existing.template) : staticAttributeValue(existing.value) : null);
|
|
4235
4514
|
if (!preferredGeneratedValue) {
|
|
4236
4515
|
return;
|
|
4237
4516
|
}
|
|
@@ -4239,6 +4518,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4239
4518
|
preferredGeneratedValue,
|
|
4240
4519
|
entryOverrides: target ? { targetPageObjectModelClass: target } : {},
|
|
4241
4520
|
semanticNameHint: semanticNameHint2,
|
|
4521
|
+
semanticNameHintAlternates: alternates,
|
|
4242
4522
|
pomMergeKey
|
|
4243
4523
|
});
|
|
4244
4524
|
return;
|
|
@@ -4253,7 +4533,6 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4253
4533
|
});
|
|
4254
4534
|
return;
|
|
4255
4535
|
}
|
|
4256
|
-
const innerText = getInnerText(element) || null;
|
|
4257
4536
|
const clickDirective = tryGetClickDirective(element);
|
|
4258
4537
|
if (clickDirective) {
|
|
4259
4538
|
const clickSuffix = getComposedClickHandlerContent(element, context, innerText, clickDirective, {
|
|
@@ -4261,7 +4540,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4261
4540
|
});
|
|
4262
4541
|
const clickHint = trimLeadingSeparators(clickSuffix) || void 0;
|
|
4263
4542
|
const idOrName = getIdOrName(element) || void 0;
|
|
4264
|
-
const semanticNameHint2 = clickHint || idOrName || conditionalHint || void 0;
|
|
4543
|
+
const semanticNameHint2 = clickHint || idOrName || innerText || conditionalHint || void 0;
|
|
4265
4544
|
const pomMergeKey = clickHint ? `click:hint:${clickHint}` : void 0;
|
|
4266
4545
|
const testId = getClickDataTestId(clickSuffix);
|
|
4267
4546
|
applyResolvedDataTestIdForElement({
|
|
@@ -4275,15 +4554,16 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
4275
4554
|
return;
|
|
4276
4555
|
}
|
|
4277
4556
|
const existingElementDataTestId = tryGetExistingElementDataTestId(element, testIdAttribute);
|
|
4278
|
-
if (existingElementDataTestId
|
|
4557
|
+
if (existingElementDataTestId) {
|
|
4279
4558
|
const inferredRole = getNativeRoleFromTagSuffix().toLowerCase();
|
|
4280
|
-
const isRecognizedInteractiveRole = inferredRole === "button" || inferredRole === "input" || inferredRole === "select" || inferredRole === "vselect" || inferredRole === "checkbox" || inferredRole === "toggle" || inferredRole === "radio";
|
|
4559
|
+
const isRecognizedInteractiveRole = inferredRole === "button" || inferredRole === "input" || inferredRole === "select" || inferredRole === "vselect" || inferredRole === "checkbox" || inferredRole === "toggle" || inferredRole === "radio" || inferredRole === "grid" || isComponentLikeTag(element.tag);
|
|
4281
4560
|
if (!isRecognizedInteractiveRole) {
|
|
4282
4561
|
return;
|
|
4283
4562
|
}
|
|
4284
|
-
const identifierHint = getIdOrName(element) || conditionalHint || void 0;
|
|
4563
|
+
const identifierHint = getIdOrName(element) || nodeHandlerAttributeValue(element) || innerText || existingElementDataTestId.value || conditionalHint || void 0;
|
|
4564
|
+
const preferredGeneratedValue = existingElementDataTestId.isDynamic ? templateAttributeValue(existingElementDataTestId.template) : staticAttributeValue(existingElementDataTestId.value);
|
|
4285
4565
|
applyResolvedDataTestIdForElement({
|
|
4286
|
-
preferredGeneratedValue
|
|
4566
|
+
preferredGeneratedValue,
|
|
4287
4567
|
semanticNameHint: identifierHint
|
|
4288
4568
|
});
|
|
4289
4569
|
return;
|
|
@@ -4311,11 +4591,13 @@ function createDevProcessorPlugin(options) {
|
|
|
4311
4591
|
nativeWrappers,
|
|
4312
4592
|
excludedComponents,
|
|
4313
4593
|
viewsDir,
|
|
4594
|
+
scanDirs,
|
|
4314
4595
|
projectRootRef,
|
|
4315
4596
|
normalizedBasePagePath,
|
|
4316
4597
|
basePageClassPath,
|
|
4317
4598
|
outDir,
|
|
4318
4599
|
emitLanguages,
|
|
4600
|
+
csharp,
|
|
4319
4601
|
generateFixtures,
|
|
4320
4602
|
customPomAttachments,
|
|
4321
4603
|
customPomDir,
|
|
@@ -4323,6 +4605,7 @@ function createDevProcessorPlugin(options) {
|
|
|
4323
4605
|
testIdAttribute,
|
|
4324
4606
|
routerAwarePoms,
|
|
4325
4607
|
resolvedRouterEntry,
|
|
4608
|
+
routerType,
|
|
4326
4609
|
loggerRef
|
|
4327
4610
|
} = options;
|
|
4328
4611
|
let scheduleVueFileRegen = null;
|
|
@@ -4336,7 +4619,11 @@ function createDevProcessorPlugin(options) {
|
|
|
4336
4619
|
return;
|
|
4337
4620
|
if (!ctx.file.endsWith(".vue"))
|
|
4338
4621
|
return;
|
|
4339
|
-
|
|
4622
|
+
const isContainedInScanDirs = scanDirs.some((dir) => {
|
|
4623
|
+
const absDir = path.resolve(projectRootRef.current, dir);
|
|
4624
|
+
return ctx.file.startsWith(absDir + path.sep);
|
|
4625
|
+
});
|
|
4626
|
+
if (!isContainedInScanDirs)
|
|
4340
4627
|
return;
|
|
4341
4628
|
scheduleVueFileRegen(ctx.file, "hmr");
|
|
4342
4629
|
},
|
|
@@ -4347,9 +4634,15 @@ function createDevProcessorPlugin(options) {
|
|
|
4347
4634
|
setResolveToComponentNameFn(() => null);
|
|
4348
4635
|
return;
|
|
4349
4636
|
}
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4637
|
+
let result;
|
|
4638
|
+
if (routerType === "nuxt") {
|
|
4639
|
+
result = await introspectNuxtPages(projectRootRef.current);
|
|
4640
|
+
} else {
|
|
4641
|
+
if (!resolvedRouterEntry)
|
|
4642
|
+
throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
|
|
4643
|
+
result = await parseRouterFileFromCwd(resolvedRouterEntry);
|
|
4644
|
+
}
|
|
4645
|
+
const { routeNameMap, routePathMap } = result;
|
|
4353
4646
|
setRouteNameToComponentNameMap(routeNameMap);
|
|
4354
4647
|
setResolveToComponentNameFn((to) => {
|
|
4355
4648
|
if (typeof to === "string") {
|
|
@@ -4450,29 +4743,34 @@ function createDevProcessorPlugin(options) {
|
|
|
4450
4743
|
return { componentName, ms: performance.now() - started, compiled: true };
|
|
4451
4744
|
};
|
|
4452
4745
|
const fullRebuildSnapshotFromFilesystem = () => {
|
|
4453
|
-
const srcDir = path.resolve(projectRootRef.current, "src");
|
|
4454
|
-
if (!fs.existsSync(srcDir))
|
|
4455
|
-
return;
|
|
4456
4746
|
const t0 = performance.now();
|
|
4457
4747
|
snapshotHierarchy.clear();
|
|
4458
4748
|
snapshotVuePathMap.clear();
|
|
4459
4749
|
filePathToComponentName.clear();
|
|
4460
|
-
|
|
4461
|
-
logInfo(`initial scan: found ${vueFiles.length} .vue files under src/`);
|
|
4750
|
+
let totalVueFiles = 0;
|
|
4462
4751
|
let compiledCount = 0;
|
|
4463
|
-
for (const
|
|
4464
|
-
const
|
|
4465
|
-
if (
|
|
4466
|
-
|
|
4752
|
+
for (const dir of scanDirs) {
|
|
4753
|
+
const absDir = path.resolve(projectRootRef.current, dir);
|
|
4754
|
+
if (!fs.existsSync(absDir))
|
|
4755
|
+
continue;
|
|
4756
|
+
const vueFiles = walkFilesRecursive(absDir);
|
|
4757
|
+
totalVueFiles += vueFiles.length;
|
|
4758
|
+
for (const file of vueFiles) {
|
|
4759
|
+
const res = compileVueFileIntoSnapshot(file);
|
|
4760
|
+
if (res.compiled)
|
|
4761
|
+
compiledCount++;
|
|
4762
|
+
}
|
|
4467
4763
|
}
|
|
4468
4764
|
const t1 = performance.now();
|
|
4469
|
-
logInfo(`initial
|
|
4765
|
+
logInfo(`initial scan: found ${totalVueFiles} .vue files in ${scanDirs.join(", ")}`);
|
|
4766
|
+
logInfo(`initial compile: ${compiledCount}/${totalVueFiles} files in ${formatMs(t1 - t0)} (components=${snapshotHierarchy.size})`);
|
|
4470
4767
|
};
|
|
4471
4768
|
const generateAggregatedFromSnapshot = (reason) => {
|
|
4472
4769
|
const t0 = performance.now();
|
|
4473
4770
|
generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
|
|
4474
4771
|
outDir,
|
|
4475
4772
|
emitLanguages,
|
|
4773
|
+
csharp,
|
|
4476
4774
|
generateFixtures,
|
|
4477
4775
|
customPomAttachments,
|
|
4478
4776
|
projectRoot: projectRootRef.current,
|
|
@@ -4480,7 +4778,8 @@ function createDevProcessorPlugin(options) {
|
|
|
4480
4778
|
customPomImportAliases,
|
|
4481
4779
|
testIdAttribute,
|
|
4482
4780
|
vueRouterFluentChaining: routerAwarePoms,
|
|
4483
|
-
routerEntry: resolvedRouterEntry
|
|
4781
|
+
routerEntry: resolvedRouterEntry,
|
|
4782
|
+
routerType
|
|
4484
4783
|
});
|
|
4485
4784
|
const t1 = performance.now();
|
|
4486
4785
|
logInfo(`generate(${reason}): components=${snapshotHierarchy.size} in ${formatMs(t1 - t0)}`);
|
|
@@ -4493,9 +4792,9 @@ function createDevProcessorPlugin(options) {
|
|
|
4493
4792
|
const t1 = performance.now();
|
|
4494
4793
|
logInfo(`startup total: ${formatMs(t1 - t0)}`);
|
|
4495
4794
|
})();
|
|
4496
|
-
const
|
|
4795
|
+
const watchedVueGlobs = scanDirs.map((dir) => path.resolve(projectRootRef.current, dir, "**", "*.vue"));
|
|
4497
4796
|
const watchedPluginGlob = path.resolve(projectRootRef.current, "vite-plugins", "vue-pom-generator", "**", "*.ts");
|
|
4498
|
-
server.watcher.add([
|
|
4797
|
+
server.watcher.add([...watchedVueGlobs, watchedPluginGlob, basePageClassPath]);
|
|
4499
4798
|
let timer = null;
|
|
4500
4799
|
let maxWaitTimer = null;
|
|
4501
4800
|
const pendingChangedVueFiles = /* @__PURE__ */ new Set();
|
|
@@ -4594,7 +4893,13 @@ function createDevProcessorPlugin(options) {
|
|
|
4594
4893
|
server.watcher.on("add", (p) => {
|
|
4595
4894
|
if (typeof p !== "string")
|
|
4596
4895
|
return;
|
|
4597
|
-
if (!p.endsWith(".vue")
|
|
4896
|
+
if (!p.endsWith(".vue"))
|
|
4897
|
+
return;
|
|
4898
|
+
const isContainedInScanDirs = scanDirs.some((dir) => {
|
|
4899
|
+
const absDir = path.resolve(projectRootRef.current, dir);
|
|
4900
|
+
return p.startsWith(absDir + path.sep);
|
|
4901
|
+
});
|
|
4902
|
+
if (!isContainedInScanDirs)
|
|
4598
4903
|
return;
|
|
4599
4904
|
void (async () => {
|
|
4600
4905
|
await initialBuildPromise;
|
|
@@ -4605,7 +4910,13 @@ function createDevProcessorPlugin(options) {
|
|
|
4605
4910
|
server.watcher.on("unlink", (p) => {
|
|
4606
4911
|
if (typeof p !== "string")
|
|
4607
4912
|
return;
|
|
4608
|
-
if (!p.endsWith(".vue")
|
|
4913
|
+
if (!p.endsWith(".vue"))
|
|
4914
|
+
return;
|
|
4915
|
+
const isContainedInScanDirs = scanDirs.some((dir) => {
|
|
4916
|
+
const absDir = path.resolve(projectRootRef.current, dir);
|
|
4917
|
+
return p.startsWith(absDir + path.sep);
|
|
4918
|
+
});
|
|
4919
|
+
if (!isContainedInScanDirs)
|
|
4609
4920
|
return;
|
|
4610
4921
|
void (async () => {
|
|
4611
4922
|
await initialBuildPromise;
|
|
@@ -4646,10 +4957,13 @@ function createSupportPlugins(options) {
|
|
|
4646
4957
|
nativeWrappers,
|
|
4647
4958
|
excludedComponents,
|
|
4648
4959
|
viewsDir,
|
|
4960
|
+
scanDirs,
|
|
4649
4961
|
outDir,
|
|
4650
4962
|
emitLanguages,
|
|
4963
|
+
csharp,
|
|
4651
4964
|
routerAwarePoms,
|
|
4652
4965
|
routerEntry,
|
|
4966
|
+
routerType,
|
|
4653
4967
|
generateFixtures,
|
|
4654
4968
|
customPomAttachments,
|
|
4655
4969
|
projectRootRef,
|
|
@@ -4662,6 +4976,8 @@ function createSupportPlugins(options) {
|
|
|
4662
4976
|
const resolveRouterEntry2 = () => {
|
|
4663
4977
|
if (!routerAwarePoms)
|
|
4664
4978
|
return void 0;
|
|
4979
|
+
if (routerType === "nuxt")
|
|
4980
|
+
return void 0;
|
|
4665
4981
|
if (!routerEntry)
|
|
4666
4982
|
throw new Error("[vue-pom-generator] router.entry is required when router introspection is enabled.");
|
|
4667
4983
|
return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(projectRootRef.current, routerEntry);
|
|
@@ -4683,6 +4999,7 @@ function createSupportPlugins(options) {
|
|
|
4683
4999
|
normalizedBasePagePath,
|
|
4684
5000
|
outDir,
|
|
4685
5001
|
emitLanguages,
|
|
5002
|
+
csharp,
|
|
4686
5003
|
generateFixtures,
|
|
4687
5004
|
customPomAttachments,
|
|
4688
5005
|
projectRootRef,
|
|
@@ -4690,6 +5007,7 @@ function createSupportPlugins(options) {
|
|
|
4690
5007
|
customPomImportAliases,
|
|
4691
5008
|
testIdAttribute,
|
|
4692
5009
|
routerAwarePoms,
|
|
5010
|
+
routerType,
|
|
4693
5011
|
resolvedRouterEntry,
|
|
4694
5012
|
loggerRef
|
|
4695
5013
|
});
|
|
@@ -4697,6 +5015,7 @@ function createSupportPlugins(options) {
|
|
|
4697
5015
|
nativeWrappers,
|
|
4698
5016
|
excludedComponents,
|
|
4699
5017
|
viewsDir,
|
|
5018
|
+
scanDirs,
|
|
4700
5019
|
projectRootRef,
|
|
4701
5020
|
normalizedBasePagePath,
|
|
4702
5021
|
basePageClassPath,
|
|
@@ -4708,6 +5027,7 @@ function createSupportPlugins(options) {
|
|
|
4708
5027
|
customPomImportAliases,
|
|
4709
5028
|
testIdAttribute,
|
|
4710
5029
|
routerAwarePoms,
|
|
5030
|
+
routerType,
|
|
4711
5031
|
resolvedRouterEntry,
|
|
4712
5032
|
loggerRef
|
|
4713
5033
|
});
|
|
@@ -4809,7 +5129,7 @@ function tryCreateElementMetadata(args) {
|
|
|
4809
5129
|
};
|
|
4810
5130
|
return metadata;
|
|
4811
5131
|
}
|
|
4812
|
-
function
|
|
5132
|
+
function extractMetadataAfterTransform(ast, componentName, elementMetadata, semanticNameMap, testIdAttribute) {
|
|
4813
5133
|
const componentMetadata = /* @__PURE__ */ new Map();
|
|
4814
5134
|
function traverseNode(node) {
|
|
4815
5135
|
if (node.type === NodeTypes.ELEMENT) {
|
|
@@ -4848,27 +5168,8 @@ function extractMetadataFromAST(ast, componentName, metadataMap, semanticNameMap
|
|
|
4848
5168
|
}
|
|
4849
5169
|
traverseNode(ast);
|
|
4850
5170
|
if (componentMetadata.size > 0) {
|
|
4851
|
-
|
|
4852
|
-
}
|
|
4853
|
-
}
|
|
4854
|
-
function compileWithMetadataExtractionManual(source, options, componentName, metadataMap, semanticNameMap, testIdAttribute = "data-testid") {
|
|
4855
|
-
const normalizedTestIdAttribute = (testIdAttribute ?? "data-testid").trim() || "data-testid";
|
|
4856
|
-
const result = compile(
|
|
4857
|
-
source,
|
|
4858
|
-
extend({}, options, {
|
|
4859
|
-
nodeTransforms: [
|
|
4860
|
-
...options.nodeTransforms || [],
|
|
4861
|
-
(node, _context) => {
|
|
4862
|
-
if (node.type !== NodeTypes.ROOT)
|
|
4863
|
-
return;
|
|
4864
|
-
return () => {
|
|
4865
|
-
extractMetadataFromAST(node, componentName, metadataMap, semanticNameMap, normalizedTestIdAttribute);
|
|
4866
|
-
};
|
|
4867
|
-
}
|
|
4868
|
-
]
|
|
4869
|
-
})
|
|
4870
|
-
);
|
|
4871
|
-
return result;
|
|
5171
|
+
elementMetadata.set(componentName, componentMetadata);
|
|
5172
|
+
}
|
|
4872
5173
|
}
|
|
4873
5174
|
function createVuePluginWithTestIds(options) {
|
|
4874
5175
|
const {
|
|
@@ -4883,27 +5184,91 @@ function createVuePluginWithTestIds(options) {
|
|
|
4883
5184
|
excludedComponents,
|
|
4884
5185
|
getViewsDirAbs,
|
|
4885
5186
|
testIdAttribute,
|
|
4886
|
-
loggerRef
|
|
5187
|
+
loggerRef,
|
|
5188
|
+
scanDirs = ["src"],
|
|
5189
|
+
getProjectRoot
|
|
4887
5190
|
} = options;
|
|
5191
|
+
const getComponentNameFromPath = (filename) => {
|
|
5192
|
+
const cleanPath = filename.includes("?") ? filename.substring(0, filename.indexOf("?")) : filename;
|
|
5193
|
+
const projectRoot = getProjectRoot();
|
|
5194
|
+
const absFilename = path.isAbsolute(cleanPath) ? cleanPath : path.resolve(projectRoot, cleanPath);
|
|
5195
|
+
const viewsDirAbs = getViewsDirAbs();
|
|
5196
|
+
const roots = [viewsDirAbs, ...scanDirs.map((d) => path.resolve(projectRoot, d))];
|
|
5197
|
+
for (const dir of scanDirs) {
|
|
5198
|
+
const absDir = path.resolve(projectRoot, dir);
|
|
5199
|
+
try {
|
|
5200
|
+
const pagesDir = path.join(absDir, "pages");
|
|
5201
|
+
if (fs.existsSync(pagesDir)) {
|
|
5202
|
+
roots.push(pagesDir);
|
|
5203
|
+
}
|
|
5204
|
+
const componentsDir = path.join(absDir, "components");
|
|
5205
|
+
if (fs.existsSync(componentsDir)) {
|
|
5206
|
+
roots.push(componentsDir);
|
|
5207
|
+
}
|
|
5208
|
+
} catch {
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
const potentialRoots = Array.from(new Set(roots.map((r) => path.normalize(r)))).sort((a, b) => b.length - a.length);
|
|
5212
|
+
let componentName = "";
|
|
5213
|
+
for (const root of potentialRoots) {
|
|
5214
|
+
if (absFilename.startsWith(root + path.sep) || absFilename === root) {
|
|
5215
|
+
const rel = path.relative(root, absFilename);
|
|
5216
|
+
const parsed = path.parse(rel);
|
|
5217
|
+
const segments = path.join(parsed.dir, parsed.name);
|
|
5218
|
+
componentName = toPascalCase(segments);
|
|
5219
|
+
break;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
if (!componentName) {
|
|
5223
|
+
const parsed = path.parse(absFilename);
|
|
5224
|
+
componentName = toPascalCase(parsed.name);
|
|
5225
|
+
}
|
|
5226
|
+
return componentName;
|
|
5227
|
+
};
|
|
5228
|
+
const isFileInScope = (filename) => {
|
|
5229
|
+
if (!filename)
|
|
5230
|
+
return false;
|
|
5231
|
+
const cleanPath = filename.includes("?") ? filename.substring(0, filename.indexOf("?")) : filename;
|
|
5232
|
+
const projectRoot = getProjectRoot();
|
|
5233
|
+
const absFilename = path.isAbsolute(cleanPath) ? cleanPath : path.resolve(projectRoot, cleanPath);
|
|
5234
|
+
if (absFilename.includes(`${path.sep}node_modules${path.sep}`) || absFilename.includes("/node_modules/"))
|
|
5235
|
+
return false;
|
|
5236
|
+
const viewsDirAbs = getViewsDirAbs();
|
|
5237
|
+
if (absFilename.startsWith(viewsDirAbs + path.sep) || absFilename === viewsDirAbs)
|
|
5238
|
+
return true;
|
|
5239
|
+
const rootsToTry = [projectRoot, process.cwd()];
|
|
5240
|
+
const matched = scanDirs.some((dir) => {
|
|
5241
|
+
return rootsToTry.some((root) => {
|
|
5242
|
+
const absDir = path.resolve(root, dir);
|
|
5243
|
+
if (absFilename.startsWith(absDir + path.sep) || absFilename === absDir)
|
|
5244
|
+
return true;
|
|
5245
|
+
if (dir.startsWith("app/") && root.endsWith("/app")) {
|
|
5246
|
+
const relativeDir = dir.substring(4);
|
|
5247
|
+
const absDirAlt = path.resolve(root, relativeDir);
|
|
5248
|
+
return absFilename.startsWith(absDirAlt + path.sep) || absFilename === absDirAlt;
|
|
5249
|
+
}
|
|
5250
|
+
return false;
|
|
5251
|
+
});
|
|
5252
|
+
});
|
|
5253
|
+
if (cleanPath.endsWith(".vue") && !matched) {
|
|
5254
|
+
loggerRef.current.debug(`[isFileInScope] REJECTED: ${absFilename} (Clean: ${cleanPath})`);
|
|
5255
|
+
}
|
|
5256
|
+
return matched;
|
|
5257
|
+
};
|
|
4888
5258
|
const userTemplate = vueOptions?.template ?? {};
|
|
4889
5259
|
const userCompilerOptions = userTemplate.compilerOptions ?? {};
|
|
4890
5260
|
const userNodeTransforms = userCompilerOptions.nodeTransforms ?? [];
|
|
4891
5261
|
const perFileTransform = /* @__PURE__ */ new Map();
|
|
4892
|
-
const
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
prefixIdentifiers: true,
|
|
4898
|
-
nodeTransforms: [
|
|
5262
|
+
const getNodeTransforms = (filename, componentNameOverride) => {
|
|
5263
|
+
const cleanPath = filename.includes("?") ? filename.substring(0, filename.indexOf("?")) : filename;
|
|
5264
|
+
const viewsDirAbs = getViewsDirAbs();
|
|
5265
|
+
const componentName = componentNameOverride || getComponentNameFromPath(cleanPath);
|
|
5266
|
+
return [
|
|
4899
5267
|
...userNodeTransforms,
|
|
4900
5268
|
(node, context) => {
|
|
4901
|
-
if (!context.filename)
|
|
4902
|
-
return;
|
|
4903
|
-
const componentName = path.basename(context.filename, ".vue");
|
|
4904
5269
|
if (node.type === NodeTypes.ROOT) {
|
|
4905
5270
|
componentHierarchyMap.delete(componentName);
|
|
4906
|
-
vueFilesPathMap.set(componentName,
|
|
5271
|
+
vueFilesPathMap.set(componentName, filename);
|
|
4907
5272
|
perFileTransform.set(
|
|
4908
5273
|
componentName,
|
|
4909
5274
|
createTestIdTransform(
|
|
@@ -4911,113 +5276,99 @@ function createVuePluginWithTestIds(options) {
|
|
|
4911
5276
|
componentHierarchyMap,
|
|
4912
5277
|
nativeWrappers,
|
|
4913
5278
|
excludedComponents,
|
|
4914
|
-
|
|
5279
|
+
viewsDirAbs,
|
|
4915
5280
|
{
|
|
4916
5281
|
existingIdBehavior,
|
|
4917
5282
|
testIdAttribute,
|
|
4918
5283
|
nameCollisionBehavior,
|
|
4919
|
-
warn: (message) => loggerRef.current.warn(message)
|
|
5284
|
+
warn: (message) => loggerRef.current.warn(message),
|
|
5285
|
+
vueFilesPathMap
|
|
4920
5286
|
}
|
|
4921
5287
|
)
|
|
4922
5288
|
);
|
|
5289
|
+
return () => {
|
|
5290
|
+
extractMetadataAfterTransform(
|
|
5291
|
+
node,
|
|
5292
|
+
componentName,
|
|
5293
|
+
elementMetadata,
|
|
5294
|
+
semanticNameMap,
|
|
5295
|
+
testIdAttribute
|
|
5296
|
+
);
|
|
5297
|
+
};
|
|
4923
5298
|
}
|
|
4924
5299
|
let transform = perFileTransform.get(componentName);
|
|
4925
5300
|
if (!transform) {
|
|
4926
5301
|
componentHierarchyMap.delete(componentName);
|
|
4927
|
-
vueFilesPathMap.set(componentName,
|
|
5302
|
+
vueFilesPathMap.set(componentName, filename);
|
|
4928
5303
|
transform = createTestIdTransform(
|
|
4929
5304
|
componentName,
|
|
4930
5305
|
componentHierarchyMap,
|
|
4931
5306
|
nativeWrappers,
|
|
4932
5307
|
excludedComponents,
|
|
4933
|
-
|
|
5308
|
+
viewsDirAbs,
|
|
4934
5309
|
{
|
|
4935
5310
|
existingIdBehavior,
|
|
4936
5311
|
testIdAttribute,
|
|
4937
5312
|
nameCollisionBehavior,
|
|
4938
|
-
warn: (message) => loggerRef.current.warn(message)
|
|
5313
|
+
warn: (message) => loggerRef.current.warn(message),
|
|
5314
|
+
vueFilesPathMap
|
|
4939
5315
|
}
|
|
4940
5316
|
);
|
|
4941
5317
|
perFileTransform.set(componentName, transform);
|
|
4942
5318
|
}
|
|
4943
5319
|
return transform(node, context);
|
|
4944
5320
|
}
|
|
5321
|
+
];
|
|
5322
|
+
};
|
|
5323
|
+
const templateCompilerOptions = {
|
|
5324
|
+
...userCompilerOptions,
|
|
5325
|
+
prefixIdentifiers: true,
|
|
5326
|
+
nodeTransforms: [
|
|
5327
|
+
// This will be used if the user uses the returned vue plugin.
|
|
5328
|
+
// We'll populate it via the hook below for better compatibility.
|
|
4945
5329
|
]
|
|
4946
5330
|
};
|
|
5331
|
+
const metadataCollectorPlugin = {
|
|
5332
|
+
name: "vue-pom-generator-metadata-collector",
|
|
5333
|
+
enforce: "pre",
|
|
5334
|
+
async transform(code, id) {
|
|
5335
|
+
const cleanPath = id.includes("?") ? id.substring(0, id.indexOf("?")) : id;
|
|
5336
|
+
if (!cleanPath.endsWith(".vue") || !isFileInScope(id)) {
|
|
5337
|
+
return null;
|
|
5338
|
+
}
|
|
5339
|
+
if (id !== cleanPath) {
|
|
5340
|
+
return null;
|
|
5341
|
+
}
|
|
5342
|
+
const componentName = getComponentNameFromPath(cleanPath);
|
|
5343
|
+
loggerRef.current.debug(`Collecting metadata for ${cleanPath} (component: ${componentName})`);
|
|
5344
|
+
try {
|
|
5345
|
+
const { parse: parse2 } = await import("@vue/compiler-sfc");
|
|
5346
|
+
const compilerDom2 = await import("@vue/compiler-dom");
|
|
5347
|
+
const compile = compilerDom2.compile;
|
|
5348
|
+
const { descriptor } = parse2(code, { filename: cleanPath });
|
|
5349
|
+
if (descriptor.template) {
|
|
5350
|
+
compile(descriptor.template.content, {
|
|
5351
|
+
...userCompilerOptions,
|
|
5352
|
+
filename: cleanPath,
|
|
5353
|
+
nodeTransforms: getNodeTransforms(cleanPath, componentName)
|
|
5354
|
+
});
|
|
5355
|
+
loggerRef.current.debug(`Metadata collected for ${cleanPath}`);
|
|
5356
|
+
}
|
|
5357
|
+
} catch (e) {
|
|
5358
|
+
loggerRef.current.warn(`Metadata collection failed for ${cleanPath}: ${e}`);
|
|
5359
|
+
}
|
|
5360
|
+
return null;
|
|
5361
|
+
}
|
|
5362
|
+
};
|
|
4947
5363
|
const template = {
|
|
4948
5364
|
...userTemplate,
|
|
4949
|
-
compiler: {
|
|
4950
|
-
// Preserve the full compiler-dom module behavior (directiveTransforms, nodeTransforms, etc.).
|
|
4951
|
-
// We only override `compile` to run our metadata extraction after transforms.
|
|
4952
|
-
...compilerDom,
|
|
4953
|
-
compile(source, compilerOptions) {
|
|
4954
|
-
const componentName = compilerOptions.filename ? path.basename(compilerOptions.filename, ".vue") : "Unknown";
|
|
4955
|
-
return compileWithMetadataExtractionManual(
|
|
4956
|
-
source,
|
|
4957
|
-
compilerOptions,
|
|
4958
|
-
componentName,
|
|
4959
|
-
elementMetadata,
|
|
4960
|
-
semanticNameMap,
|
|
4961
|
-
testIdAttribute
|
|
4962
|
-
);
|
|
4963
|
-
}
|
|
4964
|
-
},
|
|
4965
5365
|
compilerOptions: templateCompilerOptions
|
|
4966
5366
|
};
|
|
4967
|
-
|
|
5367
|
+
const internalVuePlugin = vue({
|
|
4968
5368
|
...vueOptions,
|
|
4969
5369
|
template
|
|
4970
5370
|
});
|
|
4971
|
-
}
|
|
4972
|
-
const VUE_POM_GENERATOR_LOG_PREFIX = "[vue-pom-generator]";
|
|
4973
|
-
function normalize(message) {
|
|
4974
|
-
const trimmed = (message ?? "").trim();
|
|
4975
|
-
if (!trimmed)
|
|
4976
|
-
return VUE_POM_GENERATOR_LOG_PREFIX;
|
|
4977
|
-
if (trimmed.startsWith(VUE_POM_GENERATOR_LOG_PREFIX))
|
|
4978
|
-
return trimmed;
|
|
4979
|
-
return `${VUE_POM_GENERATOR_LOG_PREFIX} ${trimmed}`;
|
|
4980
|
-
}
|
|
4981
|
-
function createLogger(options) {
|
|
4982
|
-
const { verbosity, viteLogger } = options;
|
|
4983
|
-
const sinkInfo = (msg) => {
|
|
4984
|
-
if (viteLogger) {
|
|
4985
|
-
viteLogger.info(normalize(msg));
|
|
4986
|
-
return;
|
|
4987
|
-
}
|
|
4988
|
-
console.log(normalize(msg));
|
|
4989
|
-
};
|
|
4990
|
-
const sinkWarn = (msg) => {
|
|
4991
|
-
if (viteLogger) {
|
|
4992
|
-
viteLogger.warn(normalize(msg));
|
|
4993
|
-
return;
|
|
4994
|
-
}
|
|
4995
|
-
console.warn(normalize(msg));
|
|
4996
|
-
};
|
|
4997
|
-
const sinkDebug = (msg) => {
|
|
4998
|
-
if (viteLogger) {
|
|
4999
|
-
viteLogger.info(normalize(msg));
|
|
5000
|
-
return;
|
|
5001
|
-
}
|
|
5002
|
-
console.log(normalize(msg));
|
|
5003
|
-
};
|
|
5004
|
-
return {
|
|
5005
|
-
info(message) {
|
|
5006
|
-
if (verbosity === "silent")
|
|
5007
|
-
return;
|
|
5008
|
-
sinkInfo(message);
|
|
5009
|
-
},
|
|
5010
|
-
debug(message) {
|
|
5011
|
-
if (verbosity !== "debug")
|
|
5012
|
-
return;
|
|
5013
|
-
sinkDebug(message);
|
|
5014
|
-
},
|
|
5015
|
-
warn(message) {
|
|
5016
|
-
if (verbosity === "silent")
|
|
5017
|
-
return;
|
|
5018
|
-
sinkWarn(message);
|
|
5019
|
-
}
|
|
5020
|
-
};
|
|
5371
|
+
return { metadataCollectorPlugin, internalVuePlugin };
|
|
5021
5372
|
}
|
|
5022
5373
|
function assertNonEmptyString(value, name) {
|
|
5023
5374
|
if (!value || !value.trim()) {
|
|
@@ -5032,9 +5383,10 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
5032
5383
|
const generationSetting = options.generation;
|
|
5033
5384
|
const generationOptions = generationSetting === false ? null : generationSetting ?? {};
|
|
5034
5385
|
const generationEnabled = generationOptions !== null;
|
|
5035
|
-
const verbosity = options.logging?.verbosity ?? "
|
|
5386
|
+
const verbosity = options.logging?.verbosity ?? "warn";
|
|
5036
5387
|
const vueOptions = options.vueOptions;
|
|
5037
5388
|
const viewsDir = injection.viewsDir ?? "src/views";
|
|
5389
|
+
const scanDirs = injection.scanDirs ?? ["src"];
|
|
5038
5390
|
const nativeWrappers = injection.nativeWrappers ?? {};
|
|
5039
5391
|
const excludedComponents = injection.excludeComponents ?? [];
|
|
5040
5392
|
const testIdAttribute = (injection.attribute ?? "data-testid").trim() || "data-testid";
|
|
@@ -5043,6 +5395,8 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
5043
5395
|
const emitLanguages = generationOptions?.emit && generationOptions.emit.length ? generationOptions.emit : ["ts"];
|
|
5044
5396
|
const nameCollisionBehavior = generationOptions?.nameCollisionBehavior ?? "suffix";
|
|
5045
5397
|
const routerEntry = generationOptions?.router?.entry;
|
|
5398
|
+
const routerType = generationOptions?.router?.type ?? "vue-router";
|
|
5399
|
+
const csharp = generationOptions?.csharp;
|
|
5046
5400
|
const generateFixtures = generationOptions?.playwright?.fixtures;
|
|
5047
5401
|
const customPoms = generationOptions?.playwright?.customPoms;
|
|
5048
5402
|
const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
|
|
@@ -5063,11 +5417,12 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
5063
5417
|
assertNonEmptyString(viewsDir, "[vue-pom-generator] injection.viewsDir");
|
|
5064
5418
|
if (generationEnabled) {
|
|
5065
5419
|
assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
|
|
5066
|
-
if (generationOptions?.router) {
|
|
5420
|
+
if (generationOptions?.router && routerType === "vue-router") {
|
|
5067
5421
|
assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
|
|
5068
5422
|
}
|
|
5069
5423
|
}
|
|
5070
|
-
loggerRef.current.
|
|
5424
|
+
loggerRef.current.info(`projectRoot=${projectRootRef.current}`);
|
|
5425
|
+
loggerRef.current.info(`Active plugins: ${config.plugins.map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
|
|
5071
5426
|
}
|
|
5072
5427
|
};
|
|
5073
5428
|
const getViewsDirAbs = () => resolveFromProjectRoot(projectRootRef.current, viewsDir);
|
|
@@ -5076,7 +5431,7 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
5076
5431
|
const semanticNameMap = /* @__PURE__ */ new Map();
|
|
5077
5432
|
const componentHierarchyMap = /* @__PURE__ */ new Map();
|
|
5078
5433
|
const vueFilesPathMap = /* @__PURE__ */ new Map();
|
|
5079
|
-
const
|
|
5434
|
+
const { metadataCollectorPlugin, internalVuePlugin } = createVuePluginWithTestIds({
|
|
5080
5435
|
vueOptions,
|
|
5081
5436
|
existingIdBehavior,
|
|
5082
5437
|
nameCollisionBehavior,
|
|
@@ -5088,13 +5443,11 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
5088
5443
|
excludedComponents,
|
|
5089
5444
|
getViewsDirAbs,
|
|
5090
5445
|
testIdAttribute,
|
|
5091
|
-
loggerRef
|
|
5446
|
+
loggerRef,
|
|
5447
|
+
scanDirs,
|
|
5448
|
+
getProjectRoot: () => projectRootRef.current
|
|
5092
5449
|
});
|
|
5093
|
-
|
|
5094
|
-
const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
|
|
5095
|
-
return [configPlugin, vuePlugin, virtualModules];
|
|
5096
|
-
}
|
|
5097
|
-
const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0;
|
|
5450
|
+
const routerAwarePoms = typeof routerEntry === "string" && routerEntry.trim().length > 0 || routerType === "nuxt";
|
|
5098
5451
|
const supportPlugins = createSupportPlugins({
|
|
5099
5452
|
componentTestIds,
|
|
5100
5453
|
componentHierarchyMap,
|
|
@@ -5102,8 +5455,10 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
5102
5455
|
nativeWrappers,
|
|
5103
5456
|
excludedComponents,
|
|
5104
5457
|
viewsDir,
|
|
5458
|
+
scanDirs,
|
|
5105
5459
|
outDir,
|
|
5106
5460
|
emitLanguages,
|
|
5461
|
+
csharp,
|
|
5107
5462
|
routerAwarePoms,
|
|
5108
5463
|
routerEntry,
|
|
5109
5464
|
generateFixtures,
|
|
@@ -5113,9 +5468,29 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
5113
5468
|
customPomDir: resolvedCustomPomDir,
|
|
5114
5469
|
customPomImportAliases: resolvedCustomPomImportAliases,
|
|
5115
5470
|
testIdAttribute,
|
|
5116
|
-
loggerRef
|
|
5471
|
+
loggerRef,
|
|
5472
|
+
routerType
|
|
5117
5473
|
});
|
|
5118
|
-
|
|
5474
|
+
const isNuxt = routerType === "nuxt";
|
|
5475
|
+
if (isNuxt) {
|
|
5476
|
+
loggerRef.current.info("Nuxt environment detected. Skipping internal @vitejs/plugin-vue to avoid conflicts.");
|
|
5477
|
+
}
|
|
5478
|
+
const resultPlugins = [
|
|
5479
|
+
configPlugin,
|
|
5480
|
+
metadataCollectorPlugin,
|
|
5481
|
+
...isNuxt ? [] : [internalVuePlugin],
|
|
5482
|
+
...supportPlugins
|
|
5483
|
+
];
|
|
5484
|
+
if (!generationEnabled) {
|
|
5485
|
+
const virtualModules = createTestIdsVirtualModulesPlugin(componentTestIds);
|
|
5486
|
+
return [
|
|
5487
|
+
configPlugin,
|
|
5488
|
+
metadataCollectorPlugin,
|
|
5489
|
+
...isNuxt ? [] : [internalVuePlugin],
|
|
5490
|
+
virtualModules
|
|
5491
|
+
];
|
|
5492
|
+
}
|
|
5493
|
+
return resultPlugins;
|
|
5119
5494
|
}
|
|
5120
5495
|
export {
|
|
5121
5496
|
createVuePomGeneratorPlugins,
|