uidex 0.2.4 → 0.4.0
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 +253 -353
- package/dist/cli/cli.cjs +3324 -0
- package/dist/cli/cli.cjs.map +1 -0
- package/dist/cloud/index.cjs +169 -0
- package/dist/cloud/index.cjs.map +1 -0
- package/dist/cloud/index.js +140 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/headless/index.cjs +4143 -0
- package/dist/headless/index.cjs.map +1 -0
- package/dist/headless/index.d.cts +220 -0
- package/dist/headless/index.d.ts +220 -0
- package/dist/headless/index.js +4130 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/index.cjs +8704 -9883
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +968 -146
- package/dist/index.d.ts +968 -146
- package/dist/index.js +8327 -9492
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +164 -24
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +30 -53
- package/dist/playwright/index.d.ts +30 -53
- package/dist/playwright/index.js +148 -21
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/reporter.cjs +62 -28
- package/dist/playwright/reporter.cjs.map +1 -1
- package/dist/playwright/reporter.d.cts +24 -12
- package/dist/playwright/reporter.d.ts +24 -12
- package/dist/playwright/reporter.js +62 -28
- package/dist/playwright/reporter.js.map +1 -1
- package/dist/react/index.cjs +8706 -9883
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +720 -146
- package/dist/react/index.d.ts +720 -146
- package/dist/react/index.js +8518 -9629
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +3360 -0
- package/dist/scan/index.cjs.map +1 -0
- package/dist/scan/index.d.cts +378 -0
- package/dist/scan/index.d.ts +378 -0
- package/dist/scan/index.js +3303 -0
- package/dist/scan/index.js.map +1 -0
- package/package.json +67 -60
- package/templates/claude/audit.md +43 -0
- package/templates/claude/rules.md +227 -0
- package/claude/audit-command.md +0 -46
- package/claude/rules.md +0 -167
- package/dist/api/index.cjs +0 -254
- package/dist/api/index.cjs.map +0 -1
- package/dist/api/index.d.cts +0 -236
- package/dist/api/index.d.ts +0 -236
- package/dist/api/index.js +0 -226
- package/dist/api/index.js.map +0 -1
- package/dist/core/index.cjs +0 -11045
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -424
- package/dist/core/index.d.ts +0 -424
- package/dist/core/index.global.js +0 -66516
- package/dist/core/index.global.js.map +0 -1
- package/dist/core/index.js +0 -10995
- package/dist/core/index.js.map +0 -1
- package/dist/core/style.css +0 -1529
- package/dist/scripts/cli.cjs +0 -3904
- package/uidex.schema.json +0 -93
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/playwright/fixture.ts","../../src/playwright/index.ts"],"sourcesContent":["import { test as base, expect } from '@playwright/test';\nimport type { Locator } from '@playwright/test';\nimport { uidexSelector, COVERAGE_ATTACHMENT } from './index';\n\ntype UidexLocator<T extends string = string> = (id: T) => Locator;\n\ninterface UidexFixtures {\n /**\n * Create a Playwright locator for a uidex-annotated element.\n * Automatically tracks usage for the coverage reporter.\n *\n * @example\n * ```ts\n * test('add todo', async ({ uidex }) => {\n * await uidex('todo-input').fill('Buy milk');\n * await uidex('todo-add-button').click();\n * });\n * ```\n */\n uidex: UidexLocator;\n}\n\nexport const test = base.extend<UidexFixtures>({\n uidex: async ({ page }, use, testInfo) => {\n const used = new Set<string>();\n\n const locator: UidexLocator = (id: string) => {\n used.add(id);\n return page.locator(uidexSelector(id));\n };\n\n await use(locator);\n\n // Attach coverage data for the reporter to collect\n await testInfo.attach(COVERAGE_ATTACHMENT, {\n body: JSON.stringify([...used]),\n contentType: 'application/json',\n });\n },\n});\n\nexport { expect };\nexport type { UidexLocator, UidexFixtures };\n","import type { Page, Locator } from '@playwright/test';\n\n/** The data attribute used for uidex component selectors. */\nexport const UIDEX_ATTR = 'data-uidex';\n\n/** Build a CSS selector for a uidex-annotated element. */\nexport function uidexSelector(id: string): string {\n return `[${UIDEX_ATTR}=\"${id}\"]`;\n}\n\n/** Attachment name used to pass coverage data from fixture to reporter. */\nexport const COVERAGE_ATTACHMENT = 'uidex-coverage';\n\n// Fixture — provides `uidex` as a Playwright test fixture with coverage tracking\nexport { test, expect } from './fixture';\nexport type { UidexLocator, UidexFixtures } from './fixture';\n\n/**\n * Create a Playwright locator for a uidex-annotated element.\n *\n * @example\n * ```ts\n * import { uidex } from 'uidex/playwright';\n *\n * test('submit form', async ({ page }) => {\n * await uidex(page, 'submit-btn').click();\n * });\n * ```\n */\nexport function uidex(page: Page, id: string): Locator {\n return page.locator(uidexSelector(id));\n}\n\n/**\n * Create a typed locator factory bound to a Page instance.\n * When used with the generated ComponentId type, provides autocomplete\n * for all annotated component IDs.\n *\n * @example\n * ```ts\n * import { createUidexLocators } from 'uidex/playwright';\n * import type { ComponentId } from './uidex.gen.test';\n *\n * test('checkout flow', async ({ page }) => {\n * const u = createUidexLocators<ComponentId>(page);\n * await u('cart-summary').waitFor(); // autocomplete + type checking\n * await u('checkout-btn').click();\n * });\n * ```\n */\nexport function createUidexLocators<T extends string = string>(\n page: Page\n): (id: T) => Locator {\n return (id: T) => page.locator(uidexSelector(id));\n}\n"],"mappings":";AAAA,SAAS,QAAQ,MAAM,cAAc;AAsB9B,IAAM,OAAO,KAAK,OAAsB;AAAA,EAC7C,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK,aAAa;AACxC,UAAM,OAAO,oBAAI,IAAY;AAE7B,UAAM,UAAwB,CAAC,OAAe;AAC5C,WAAK,IAAI,EAAE;AACX,aAAO,KAAK,QAAQ,cAAc,EAAE,CAAC;AAAA,IACvC;AAEA,UAAM,IAAI,OAAO;AAGjB,UAAM,SAAS,OAAO,qBAAqB;AAAA,MACzC,MAAM,KAAK,UAAU,CAAC,GAAG,IAAI,CAAC;AAAA,MAC9B,aAAa;AAAA,IACf,CAAC;AAAA,EACH;AACF,CAAC;;;ACpCM,IAAM,aAAa;AAGnB,SAAS,cAAc,IAAoB;AAChD,SAAO,IAAI,UAAU,KAAK,EAAE;AAC9B;AAGO,IAAM,sBAAsB;AAkB5B,SAAS,MAAM,MAAY,IAAqB;AACrD,SAAO,KAAK,QAAQ,cAAc,EAAE,CAAC;AACvC;AAmBO,SAAS,oBACd,MACoB;AACpB,SAAO,CAAC,OAAU,KAAK,QAAQ,cAAc,EAAE,CAAC;AAClD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/playwright/selector.ts","../../src/playwright/fixture.ts","../../src/playwright/reporter.ts"],"sourcesContent":["const ATTRS = [\n \"data-uidex\",\n \"data-uidex-region\",\n \"data-uidex-widget\",\n \"data-uidex-primitive\",\n] as const\n\nexport const UIDEX_ATTRS = ATTRS\n\nexport function uidexSelector(id: string): string {\n const escaped = id.replace(/\"/g, '\\\\\"')\n return ATTRS.map((a) => `[${a}=\"${escaped}\"]`).join(\", \")\n}\n\nexport function kebab(input: string): string {\n return input\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n}\n\nexport const FLOW_TAG = \"@uidex:flow\"\nexport const NOT_FLOW_TAG = \"@uidex:not-flow\"\nexport const COVERAGE_ATTACHMENT = \"uidex-coverage\"\n\nexport interface CoveragePayload {\n flow: string | null\n notFlow: boolean\n ids: string[]\n title: string\n}\n","import { test as base, expect } from \"@playwright/test\"\nimport type { Locator, TestInfo } from \"@playwright/test\"\nimport {\n COVERAGE_ATTACHMENT,\n FLOW_TAG,\n NOT_FLOW_TAG,\n kebab,\n uidexSelector,\n type CoveragePayload,\n} from \"./selector\"\n\nexport type UidexLocator<T extends string = string> = (id: T) => Locator\n\nexport interface UidexFixtures {\n uidex: UidexLocator\n}\n\ninterface FlowResolution {\n flow: string | null\n notFlow: boolean\n}\n\ntype TestInfoLike = Pick<TestInfo, \"tags\" | \"titlePath\" | \"title\">\n\nexport function resolveFlow(testInfo: TestInfoLike): FlowResolution {\n const tags = testInfo.tags ?? []\n const notFlow = tags.includes(NOT_FLOW_TAG)\n if (notFlow) return { flow: null, notFlow: true }\n if (!tags.includes(FLOW_TAG)) return { flow: null, notFlow: false }\n const describes = describeTitles(testInfo.titlePath ?? [], testInfo.title)\n const source =\n describes.length > 0\n ? describes[describes.length - 1]\n : (testInfo.title ?? \"\")\n return { flow: kebab(source) || null, notFlow: false }\n}\n\nconst FILE_RE = /\\.(spec|test)\\.(t|j)sx?$|\\.(t|j)sx?$|[\\\\/]/\n\nfunction describeTitles(titlePath: string[], testTitle: string): string[] {\n const end =\n titlePath.length > 0 && titlePath[titlePath.length - 1] === testTitle\n ? titlePath.length - 1\n : titlePath.length\n const out: string[] = []\n for (let i = 0; i < end; i++) {\n const entry = titlePath[i]\n if (!entry) continue\n if (FILE_RE.test(entry)) continue\n if (i === 0) continue // project name\n out.push(entry)\n }\n return out\n}\n\ninterface PageLike {\n locator: (selector: string) => Locator\n}\n\nexport interface UidexFixtureHandle {\n locator: UidexLocator\n buildPayload: () => CoveragePayload\n}\n\nexport function createUidexFixture(\n page: PageLike,\n testInfo: TestInfoLike\n): UidexFixtureHandle {\n const used = new Set<string>()\n const locator: UidexLocator = (id: string) => {\n used.add(id)\n return page.locator(uidexSelector(id))\n }\n const buildPayload = (): CoveragePayload => {\n const { flow, notFlow } = resolveFlow(testInfo)\n return {\n flow,\n notFlow,\n ids: [...used].sort(),\n title: testInfo.title,\n }\n }\n return { locator, buildPayload }\n}\n\nexport const test = base.extend<UidexFixtures>({\n uidex: async ({ page }, use, testInfo) => {\n const handle = createUidexFixture(page, testInfo)\n // eslint-disable-next-line react-hooks/rules-of-hooks\n await use(handle.locator)\n await testInfo.attach(COVERAGE_ATTACHMENT, {\n body: JSON.stringify(handle.buildPayload()),\n contentType: \"application/json\",\n })\n },\n})\n\nexport { expect }\n","import * as fs from \"node:fs\"\nimport * as path from \"node:path\"\nimport type {\n FullResult,\n Reporter,\n TestCase,\n TestResult,\n} from \"@playwright/test/reporter\"\nimport { COVERAGE_ATTACHMENT, type CoveragePayload } from \"./selector\"\n\nexport interface UidexReporterOptions {\n outputPath?: string\n entityIds?: readonly string[]\n silent?: boolean\n}\n\nexport interface FlowCoverage {\n flow: string\n ids: string[]\n titles: string[]\n}\n\nexport interface UidexCoverageReport {\n flows: FlowCoverage[]\n untagged: { title: string; ids: string[] }[]\n touched: string[]\n untouched: string[]\n total: number\n percentage: number\n}\n\nexport default class UidexCoverageReporter implements Reporter {\n private readonly outputPath: string\n private readonly entityIds: readonly string[]\n private readonly silent: boolean\n private readonly flows = new Map<\n string,\n { ids: Set<string>; titles: Set<string> }\n >()\n private readonly untagged: { title: string; ids: string[] }[] = []\n private readonly touched = new Set<string>()\n\n constructor(options: UidexReporterOptions = {}) {\n this.outputPath = options.outputPath ?? \"uidex-coverage.json\"\n this.entityIds = options.entityIds ?? []\n this.silent = options.silent ?? false\n }\n\n onTestEnd(_test: TestCase, result: TestResult): void {\n for (const attachment of result.attachments) {\n if (attachment.name !== COVERAGE_ATTACHMENT) continue\n if (!attachment.body) continue\n const payload = parsePayload(attachment.body.toString())\n if (!payload) continue\n if (payload.notFlow) continue\n for (const id of payload.ids) this.touched.add(id)\n if (payload.flow) {\n const entry = this.flows.get(payload.flow) ?? {\n ids: new Set<string>(),\n titles: new Set<string>(),\n }\n for (const id of payload.ids) entry.ids.add(id)\n entry.titles.add(payload.title)\n this.flows.set(payload.flow, entry)\n } else {\n this.untagged.push({ title: payload.title, ids: payload.ids })\n }\n }\n }\n\n async onEnd(_result: FullResult): Promise<void> {\n const flows: FlowCoverage[] = [...this.flows.entries()]\n .map(([flow, entry]) => ({\n flow,\n ids: [...entry.ids].sort(),\n titles: [...entry.titles].sort(),\n }))\n .sort((a, b) => a.flow.localeCompare(b.flow))\n\n const all = [...this.entityIds]\n const touched = all.filter((id) => this.touched.has(id)).sort()\n const untouched = all.filter((id) => !this.touched.has(id)).sort()\n const total = all.length\n const percentage =\n total > 0 ? Math.round((touched.length / total) * 100) : 0\n\n const report: UidexCoverageReport = {\n flows,\n untagged: this.untagged,\n touched,\n untouched,\n total,\n percentage,\n }\n\n fs.mkdirSync(path.dirname(path.resolve(this.outputPath)), {\n recursive: true,\n })\n fs.writeFileSync(\n path.resolve(this.outputPath),\n JSON.stringify(report, null, 2) + \"\\n\"\n )\n\n if (!this.silent) {\n const line =\n total > 0\n ? `uidex coverage: ${touched.length}/${total} entities (${percentage}%) across ${flows.length} flow(s)`\n : `uidex coverage: ${flows.length} flow(s), ${this.touched.size} entity id(s) touched`\n\n console.log(line)\n }\n }\n}\n\nfunction parsePayload(raw: string): CoveragePayload | null {\n try {\n const parsed = JSON.parse(raw) as Partial<CoveragePayload>\n if (!parsed || !Array.isArray(parsed.ids)) return null\n return {\n flow: typeof parsed.flow === \"string\" ? parsed.flow : null,\n notFlow: Boolean(parsed.notFlow),\n ids: parsed.ids.filter((x): x is string => typeof x === \"string\"),\n title: typeof parsed.title === \"string\" ? parsed.title : \"\",\n }\n } catch {\n return null\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -30,59 +30,93 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/playwright/reporter.ts
|
|
31
31
|
var reporter_exports = {};
|
|
32
32
|
__export(reporter_exports, {
|
|
33
|
-
default: () =>
|
|
33
|
+
default: () => UidexCoverageReporter
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(reporter_exports);
|
|
36
36
|
var fs = __toESM(require("fs"), 1);
|
|
37
37
|
var path = __toESM(require("path"), 1);
|
|
38
38
|
|
|
39
|
-
// src/playwright/
|
|
39
|
+
// src/playwright/selector.ts
|
|
40
40
|
var COVERAGE_ATTACHMENT = "uidex-coverage";
|
|
41
41
|
|
|
42
42
|
// src/playwright/reporter.ts
|
|
43
43
|
var UidexCoverageReporter = class {
|
|
44
|
-
interacted = /* @__PURE__ */ new Set();
|
|
45
|
-
componentIds;
|
|
46
44
|
outputPath;
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
entityIds;
|
|
46
|
+
silent;
|
|
47
|
+
flows = /* @__PURE__ */ new Map();
|
|
48
|
+
untagged = [];
|
|
49
|
+
touched = /* @__PURE__ */ new Set();
|
|
50
|
+
constructor(options = {}) {
|
|
49
51
|
this.outputPath = options.outputPath ?? "uidex-coverage.json";
|
|
52
|
+
this.entityIds = options.entityIds ?? [];
|
|
53
|
+
this.silent = options.silent ?? false;
|
|
50
54
|
}
|
|
51
55
|
onTestEnd(_test, result) {
|
|
52
56
|
for (const attachment of result.attachments) {
|
|
53
|
-
if (attachment.name
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
if (attachment.name !== COVERAGE_ATTACHMENT) continue;
|
|
58
|
+
if (!attachment.body) continue;
|
|
59
|
+
const payload = parsePayload(attachment.body.toString());
|
|
60
|
+
if (!payload) continue;
|
|
61
|
+
if (payload.notFlow) continue;
|
|
62
|
+
for (const id of payload.ids) this.touched.add(id);
|
|
63
|
+
if (payload.flow) {
|
|
64
|
+
const entry = this.flows.get(payload.flow) ?? {
|
|
65
|
+
ids: /* @__PURE__ */ new Set(),
|
|
66
|
+
titles: /* @__PURE__ */ new Set()
|
|
67
|
+
};
|
|
68
|
+
for (const id of payload.ids) entry.ids.add(id);
|
|
69
|
+
entry.titles.add(payload.title);
|
|
70
|
+
this.flows.set(payload.flow, entry);
|
|
71
|
+
} else {
|
|
72
|
+
this.untagged.push({ title: payload.title, ids: payload.ids });
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
}
|
|
61
|
-
onEnd(_result) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
async onEnd(_result) {
|
|
77
|
+
const flows = [...this.flows.entries()].map(([flow, entry]) => ({
|
|
78
|
+
flow,
|
|
79
|
+
ids: [...entry.ids].sort(),
|
|
80
|
+
titles: [...entry.titles].sort()
|
|
81
|
+
})).sort((a, b) => a.flow.localeCompare(b.flow));
|
|
82
|
+
const all = [...this.entityIds];
|
|
83
|
+
const touched = all.filter((id) => this.touched.has(id)).sort();
|
|
84
|
+
const untouched = all.filter((id) => !this.touched.has(id)).sort();
|
|
65
85
|
const total = all.length;
|
|
66
|
-
const percentage = total > 0 ? Math.round(
|
|
67
|
-
console.log("");
|
|
68
|
-
console.log(
|
|
69
|
-
`uidex coverage: ${covered.length}/${total} components (${percentage}%)`
|
|
70
|
-
);
|
|
71
|
-
if (uncovered.length > 0) {
|
|
72
|
-
console.log(` uncovered: ${uncovered.join(", ")}`);
|
|
73
|
-
}
|
|
74
|
-
console.log("");
|
|
86
|
+
const percentage = total > 0 ? Math.round(touched.length / total * 100) : 0;
|
|
75
87
|
const report = {
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
flows,
|
|
89
|
+
untagged: this.untagged,
|
|
90
|
+
touched,
|
|
91
|
+
untouched,
|
|
78
92
|
total,
|
|
79
93
|
percentage
|
|
80
94
|
};
|
|
95
|
+
fs.mkdirSync(path.dirname(path.resolve(this.outputPath)), {
|
|
96
|
+
recursive: true
|
|
97
|
+
});
|
|
81
98
|
fs.writeFileSync(
|
|
82
|
-
path.resolve(
|
|
99
|
+
path.resolve(this.outputPath),
|
|
83
100
|
JSON.stringify(report, null, 2) + "\n"
|
|
84
101
|
);
|
|
102
|
+
if (!this.silent) {
|
|
103
|
+
const line = total > 0 ? `uidex coverage: ${touched.length}/${total} entities (${percentage}%) across ${flows.length} flow(s)` : `uidex coverage: ${flows.length} flow(s), ${this.touched.size} entity id(s) touched`;
|
|
104
|
+
console.log(line);
|
|
105
|
+
}
|
|
85
106
|
}
|
|
86
107
|
};
|
|
87
|
-
|
|
108
|
+
function parsePayload(raw) {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(raw);
|
|
111
|
+
if (!parsed || !Array.isArray(parsed.ids)) return null;
|
|
112
|
+
return {
|
|
113
|
+
flow: typeof parsed.flow === "string" ? parsed.flow : null,
|
|
114
|
+
notFlow: Boolean(parsed.notFlow),
|
|
115
|
+
ids: parsed.ids.filter((x) => typeof x === "string"),
|
|
116
|
+
title: typeof parsed.title === "string" ? parsed.title : ""
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
88
122
|
//# sourceMappingURL=reporter.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/playwright/reporter.ts","../../src/playwright/
|
|
1
|
+
{"version":3,"sources":["../../src/playwright/reporter.ts","../../src/playwright/selector.ts"],"sourcesContent":["import * as fs from \"node:fs\"\nimport * as path from \"node:path\"\nimport type {\n FullResult,\n Reporter,\n TestCase,\n TestResult,\n} from \"@playwright/test/reporter\"\nimport { COVERAGE_ATTACHMENT, type CoveragePayload } from \"./selector\"\n\nexport interface UidexReporterOptions {\n outputPath?: string\n entityIds?: readonly string[]\n silent?: boolean\n}\n\nexport interface FlowCoverage {\n flow: string\n ids: string[]\n titles: string[]\n}\n\nexport interface UidexCoverageReport {\n flows: FlowCoverage[]\n untagged: { title: string; ids: string[] }[]\n touched: string[]\n untouched: string[]\n total: number\n percentage: number\n}\n\nexport default class UidexCoverageReporter implements Reporter {\n private readonly outputPath: string\n private readonly entityIds: readonly string[]\n private readonly silent: boolean\n private readonly flows = new Map<\n string,\n { ids: Set<string>; titles: Set<string> }\n >()\n private readonly untagged: { title: string; ids: string[] }[] = []\n private readonly touched = new Set<string>()\n\n constructor(options: UidexReporterOptions = {}) {\n this.outputPath = options.outputPath ?? \"uidex-coverage.json\"\n this.entityIds = options.entityIds ?? []\n this.silent = options.silent ?? false\n }\n\n onTestEnd(_test: TestCase, result: TestResult): void {\n for (const attachment of result.attachments) {\n if (attachment.name !== COVERAGE_ATTACHMENT) continue\n if (!attachment.body) continue\n const payload = parsePayload(attachment.body.toString())\n if (!payload) continue\n if (payload.notFlow) continue\n for (const id of payload.ids) this.touched.add(id)\n if (payload.flow) {\n const entry = this.flows.get(payload.flow) ?? {\n ids: new Set<string>(),\n titles: new Set<string>(),\n }\n for (const id of payload.ids) entry.ids.add(id)\n entry.titles.add(payload.title)\n this.flows.set(payload.flow, entry)\n } else {\n this.untagged.push({ title: payload.title, ids: payload.ids })\n }\n }\n }\n\n async onEnd(_result: FullResult): Promise<void> {\n const flows: FlowCoverage[] = [...this.flows.entries()]\n .map(([flow, entry]) => ({\n flow,\n ids: [...entry.ids].sort(),\n titles: [...entry.titles].sort(),\n }))\n .sort((a, b) => a.flow.localeCompare(b.flow))\n\n const all = [...this.entityIds]\n const touched = all.filter((id) => this.touched.has(id)).sort()\n const untouched = all.filter((id) => !this.touched.has(id)).sort()\n const total = all.length\n const percentage =\n total > 0 ? Math.round((touched.length / total) * 100) : 0\n\n const report: UidexCoverageReport = {\n flows,\n untagged: this.untagged,\n touched,\n untouched,\n total,\n percentage,\n }\n\n fs.mkdirSync(path.dirname(path.resolve(this.outputPath)), {\n recursive: true,\n })\n fs.writeFileSync(\n path.resolve(this.outputPath),\n JSON.stringify(report, null, 2) + \"\\n\"\n )\n\n if (!this.silent) {\n const line =\n total > 0\n ? `uidex coverage: ${touched.length}/${total} entities (${percentage}%) across ${flows.length} flow(s)`\n : `uidex coverage: ${flows.length} flow(s), ${this.touched.size} entity id(s) touched`\n\n console.log(line)\n }\n }\n}\n\nfunction parsePayload(raw: string): CoveragePayload | null {\n try {\n const parsed = JSON.parse(raw) as Partial<CoveragePayload>\n if (!parsed || !Array.isArray(parsed.ids)) return null\n return {\n flow: typeof parsed.flow === \"string\" ? parsed.flow : null,\n notFlow: Boolean(parsed.notFlow),\n ids: parsed.ids.filter((x): x is string => typeof x === \"string\"),\n title: typeof parsed.title === \"string\" ? parsed.title : \"\",\n }\n } catch {\n return null\n }\n}\n","const ATTRS = [\n \"data-uidex\",\n \"data-uidex-region\",\n \"data-uidex-widget\",\n \"data-uidex-primitive\",\n] as const\n\nexport const UIDEX_ATTRS = ATTRS\n\nexport function uidexSelector(id: string): string {\n const escaped = id.replace(/\"/g, '\\\\\"')\n return ATTRS.map((a) => `[${a}=\"${escaped}\"]`).join(\", \")\n}\n\nexport function kebab(input: string): string {\n return input\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n}\n\nexport const FLOW_TAG = \"@uidex:flow\"\nexport const NOT_FLOW_TAG = \"@uidex:not-flow\"\nexport const COVERAGE_ATTACHMENT = \"uidex-coverage\"\n\nexport interface CoveragePayload {\n flow: string | null\n notFlow: boolean\n ids: string[]\n title: string\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
import { Reporter, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
|
-
interface
|
|
4
|
-
/** All known component IDs — pass componentIds from uidex.gen.test.ts */
|
|
5
|
-
componentIds: readonly string[];
|
|
6
|
-
/** Output file path for JSON report (default: "uidex-coverage.json") */
|
|
3
|
+
interface UidexReporterOptions {
|
|
7
4
|
outputPath?: string;
|
|
5
|
+
entityIds?: readonly string[];
|
|
6
|
+
silent?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface FlowCoverage {
|
|
9
|
+
flow: string;
|
|
10
|
+
ids: string[];
|
|
11
|
+
titles: string[];
|
|
8
12
|
}
|
|
9
13
|
interface UidexCoverageReport {
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
flows: FlowCoverage[];
|
|
15
|
+
untagged: {
|
|
16
|
+
title: string;
|
|
17
|
+
ids: string[];
|
|
18
|
+
}[];
|
|
19
|
+
touched: string[];
|
|
20
|
+
untouched: string[];
|
|
12
21
|
total: number;
|
|
13
22
|
percentage: number;
|
|
14
23
|
}
|
|
15
24
|
declare class UidexCoverageReporter implements Reporter {
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
|
|
25
|
+
private readonly outputPath;
|
|
26
|
+
private readonly entityIds;
|
|
27
|
+
private readonly silent;
|
|
28
|
+
private readonly flows;
|
|
29
|
+
private readonly untagged;
|
|
30
|
+
private readonly touched;
|
|
31
|
+
constructor(options?: UidexReporterOptions);
|
|
20
32
|
onTestEnd(_test: TestCase, result: TestResult): void;
|
|
21
|
-
onEnd(_result: FullResult): void
|
|
33
|
+
onEnd(_result: FullResult): Promise<void>;
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
export { type
|
|
36
|
+
export { type FlowCoverage, type UidexCoverageReport, type UidexReporterOptions, UidexCoverageReporter as default };
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
import { Reporter, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
|
-
interface
|
|
4
|
-
/** All known component IDs — pass componentIds from uidex.gen.test.ts */
|
|
5
|
-
componentIds: readonly string[];
|
|
6
|
-
/** Output file path for JSON report (default: "uidex-coverage.json") */
|
|
3
|
+
interface UidexReporterOptions {
|
|
7
4
|
outputPath?: string;
|
|
5
|
+
entityIds?: readonly string[];
|
|
6
|
+
silent?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface FlowCoverage {
|
|
9
|
+
flow: string;
|
|
10
|
+
ids: string[];
|
|
11
|
+
titles: string[];
|
|
8
12
|
}
|
|
9
13
|
interface UidexCoverageReport {
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
flows: FlowCoverage[];
|
|
15
|
+
untagged: {
|
|
16
|
+
title: string;
|
|
17
|
+
ids: string[];
|
|
18
|
+
}[];
|
|
19
|
+
touched: string[];
|
|
20
|
+
untouched: string[];
|
|
12
21
|
total: number;
|
|
13
22
|
percentage: number;
|
|
14
23
|
}
|
|
15
24
|
declare class UidexCoverageReporter implements Reporter {
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
|
|
25
|
+
private readonly outputPath;
|
|
26
|
+
private readonly entityIds;
|
|
27
|
+
private readonly silent;
|
|
28
|
+
private readonly flows;
|
|
29
|
+
private readonly untagged;
|
|
30
|
+
private readonly touched;
|
|
31
|
+
constructor(options?: UidexReporterOptions);
|
|
20
32
|
onTestEnd(_test: TestCase, result: TestResult): void;
|
|
21
|
-
onEnd(_result: FullResult): void
|
|
33
|
+
onEnd(_result: FullResult): Promise<void>;
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
export { type
|
|
36
|
+
export { type FlowCoverage, type UidexCoverageReport, type UidexReporterOptions, UidexCoverageReporter as default };
|
|
@@ -2,56 +2,90 @@
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
|
|
5
|
-
// src/playwright/
|
|
5
|
+
// src/playwright/selector.ts
|
|
6
6
|
var COVERAGE_ATTACHMENT = "uidex-coverage";
|
|
7
7
|
|
|
8
8
|
// src/playwright/reporter.ts
|
|
9
9
|
var UidexCoverageReporter = class {
|
|
10
|
-
interacted = /* @__PURE__ */ new Set();
|
|
11
|
-
componentIds;
|
|
12
10
|
outputPath;
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
entityIds;
|
|
12
|
+
silent;
|
|
13
|
+
flows = /* @__PURE__ */ new Map();
|
|
14
|
+
untagged = [];
|
|
15
|
+
touched = /* @__PURE__ */ new Set();
|
|
16
|
+
constructor(options = {}) {
|
|
15
17
|
this.outputPath = options.outputPath ?? "uidex-coverage.json";
|
|
18
|
+
this.entityIds = options.entityIds ?? [];
|
|
19
|
+
this.silent = options.silent ?? false;
|
|
16
20
|
}
|
|
17
21
|
onTestEnd(_test, result) {
|
|
18
22
|
for (const attachment of result.attachments) {
|
|
19
|
-
if (attachment.name
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
if (attachment.name !== COVERAGE_ATTACHMENT) continue;
|
|
24
|
+
if (!attachment.body) continue;
|
|
25
|
+
const payload = parsePayload(attachment.body.toString());
|
|
26
|
+
if (!payload) continue;
|
|
27
|
+
if (payload.notFlow) continue;
|
|
28
|
+
for (const id of payload.ids) this.touched.add(id);
|
|
29
|
+
if (payload.flow) {
|
|
30
|
+
const entry = this.flows.get(payload.flow) ?? {
|
|
31
|
+
ids: /* @__PURE__ */ new Set(),
|
|
32
|
+
titles: /* @__PURE__ */ new Set()
|
|
33
|
+
};
|
|
34
|
+
for (const id of payload.ids) entry.ids.add(id);
|
|
35
|
+
entry.titles.add(payload.title);
|
|
36
|
+
this.flows.set(payload.flow, entry);
|
|
37
|
+
} else {
|
|
38
|
+
this.untagged.push({ title: payload.title, ids: payload.ids });
|
|
24
39
|
}
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
|
-
onEnd(_result) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
async onEnd(_result) {
|
|
43
|
+
const flows = [...this.flows.entries()].map(([flow, entry]) => ({
|
|
44
|
+
flow,
|
|
45
|
+
ids: [...entry.ids].sort(),
|
|
46
|
+
titles: [...entry.titles].sort()
|
|
47
|
+
})).sort((a, b) => a.flow.localeCompare(b.flow));
|
|
48
|
+
const all = [...this.entityIds];
|
|
49
|
+
const touched = all.filter((id) => this.touched.has(id)).sort();
|
|
50
|
+
const untouched = all.filter((id) => !this.touched.has(id)).sort();
|
|
31
51
|
const total = all.length;
|
|
32
|
-
const percentage = total > 0 ? Math.round(
|
|
33
|
-
console.log("");
|
|
34
|
-
console.log(
|
|
35
|
-
`uidex coverage: ${covered.length}/${total} components (${percentage}%)`
|
|
36
|
-
);
|
|
37
|
-
if (uncovered.length > 0) {
|
|
38
|
-
console.log(` uncovered: ${uncovered.join(", ")}`);
|
|
39
|
-
}
|
|
40
|
-
console.log("");
|
|
52
|
+
const percentage = total > 0 ? Math.round(touched.length / total * 100) : 0;
|
|
41
53
|
const report = {
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
flows,
|
|
55
|
+
untagged: this.untagged,
|
|
56
|
+
touched,
|
|
57
|
+
untouched,
|
|
44
58
|
total,
|
|
45
59
|
percentage
|
|
46
60
|
};
|
|
61
|
+
fs.mkdirSync(path.dirname(path.resolve(this.outputPath)), {
|
|
62
|
+
recursive: true
|
|
63
|
+
});
|
|
47
64
|
fs.writeFileSync(
|
|
48
|
-
path.resolve(
|
|
65
|
+
path.resolve(this.outputPath),
|
|
49
66
|
JSON.stringify(report, null, 2) + "\n"
|
|
50
67
|
);
|
|
68
|
+
if (!this.silent) {
|
|
69
|
+
const line = total > 0 ? `uidex coverage: ${touched.length}/${total} entities (${percentage}%) across ${flows.length} flow(s)` : `uidex coverage: ${flows.length} flow(s), ${this.touched.size} entity id(s) touched`;
|
|
70
|
+
console.log(line);
|
|
71
|
+
}
|
|
51
72
|
}
|
|
52
73
|
};
|
|
53
|
-
|
|
74
|
+
function parsePayload(raw) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
if (!parsed || !Array.isArray(parsed.ids)) return null;
|
|
78
|
+
return {
|
|
79
|
+
flow: typeof parsed.flow === "string" ? parsed.flow : null,
|
|
80
|
+
notFlow: Boolean(parsed.notFlow),
|
|
81
|
+
ids: parsed.ids.filter((x) => typeof x === "string"),
|
|
82
|
+
title: typeof parsed.title === "string" ? parsed.title : ""
|
|
83
|
+
};
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
54
88
|
export {
|
|
55
|
-
|
|
89
|
+
UidexCoverageReporter as default
|
|
56
90
|
};
|
|
57
91
|
//# sourceMappingURL=reporter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/playwright/reporter.ts","../../src/playwright/
|
|
1
|
+
{"version":3,"sources":["../../src/playwright/reporter.ts","../../src/playwright/selector.ts"],"sourcesContent":["import * as fs from \"node:fs\"\nimport * as path from \"node:path\"\nimport type {\n FullResult,\n Reporter,\n TestCase,\n TestResult,\n} from \"@playwright/test/reporter\"\nimport { COVERAGE_ATTACHMENT, type CoveragePayload } from \"./selector\"\n\nexport interface UidexReporterOptions {\n outputPath?: string\n entityIds?: readonly string[]\n silent?: boolean\n}\n\nexport interface FlowCoverage {\n flow: string\n ids: string[]\n titles: string[]\n}\n\nexport interface UidexCoverageReport {\n flows: FlowCoverage[]\n untagged: { title: string; ids: string[] }[]\n touched: string[]\n untouched: string[]\n total: number\n percentage: number\n}\n\nexport default class UidexCoverageReporter implements Reporter {\n private readonly outputPath: string\n private readonly entityIds: readonly string[]\n private readonly silent: boolean\n private readonly flows = new Map<\n string,\n { ids: Set<string>; titles: Set<string> }\n >()\n private readonly untagged: { title: string; ids: string[] }[] = []\n private readonly touched = new Set<string>()\n\n constructor(options: UidexReporterOptions = {}) {\n this.outputPath = options.outputPath ?? \"uidex-coverage.json\"\n this.entityIds = options.entityIds ?? []\n this.silent = options.silent ?? false\n }\n\n onTestEnd(_test: TestCase, result: TestResult): void {\n for (const attachment of result.attachments) {\n if (attachment.name !== COVERAGE_ATTACHMENT) continue\n if (!attachment.body) continue\n const payload = parsePayload(attachment.body.toString())\n if (!payload) continue\n if (payload.notFlow) continue\n for (const id of payload.ids) this.touched.add(id)\n if (payload.flow) {\n const entry = this.flows.get(payload.flow) ?? {\n ids: new Set<string>(),\n titles: new Set<string>(),\n }\n for (const id of payload.ids) entry.ids.add(id)\n entry.titles.add(payload.title)\n this.flows.set(payload.flow, entry)\n } else {\n this.untagged.push({ title: payload.title, ids: payload.ids })\n }\n }\n }\n\n async onEnd(_result: FullResult): Promise<void> {\n const flows: FlowCoverage[] = [...this.flows.entries()]\n .map(([flow, entry]) => ({\n flow,\n ids: [...entry.ids].sort(),\n titles: [...entry.titles].sort(),\n }))\n .sort((a, b) => a.flow.localeCompare(b.flow))\n\n const all = [...this.entityIds]\n const touched = all.filter((id) => this.touched.has(id)).sort()\n const untouched = all.filter((id) => !this.touched.has(id)).sort()\n const total = all.length\n const percentage =\n total > 0 ? Math.round((touched.length / total) * 100) : 0\n\n const report: UidexCoverageReport = {\n flows,\n untagged: this.untagged,\n touched,\n untouched,\n total,\n percentage,\n }\n\n fs.mkdirSync(path.dirname(path.resolve(this.outputPath)), {\n recursive: true,\n })\n fs.writeFileSync(\n path.resolve(this.outputPath),\n JSON.stringify(report, null, 2) + \"\\n\"\n )\n\n if (!this.silent) {\n const line =\n total > 0\n ? `uidex coverage: ${touched.length}/${total} entities (${percentage}%) across ${flows.length} flow(s)`\n : `uidex coverage: ${flows.length} flow(s), ${this.touched.size} entity id(s) touched`\n\n console.log(line)\n }\n }\n}\n\nfunction parsePayload(raw: string): CoveragePayload | null {\n try {\n const parsed = JSON.parse(raw) as Partial<CoveragePayload>\n if (!parsed || !Array.isArray(parsed.ids)) return null\n return {\n flow: typeof parsed.flow === \"string\" ? parsed.flow : null,\n notFlow: Boolean(parsed.notFlow),\n ids: parsed.ids.filter((x): x is string => typeof x === \"string\"),\n title: typeof parsed.title === \"string\" ? parsed.title : \"\",\n }\n } catch {\n return null\n }\n}\n","const ATTRS = [\n \"data-uidex\",\n \"data-uidex-region\",\n \"data-uidex-widget\",\n \"data-uidex-primitive\",\n] as const\n\nexport const UIDEX_ATTRS = ATTRS\n\nexport function uidexSelector(id: string): string {\n const escaped = id.replace(/\"/g, '\\\\\"')\n return ATTRS.map((a) => `[${a}=\"${escaped}\"]`).join(\", \")\n}\n\nexport function kebab(input: string): string {\n return input\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n}\n\nexport const FLOW_TAG = \"@uidex:flow\"\nexport const NOT_FLOW_TAG = \"@uidex:not-flow\"\nexport const COVERAGE_ATTACHMENT = \"uidex-coverage\"\n\nexport interface CoveragePayload {\n flow: string | null\n notFlow: boolean\n ids: string[]\n title: string\n}\n"],"mappings":"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","names":[]}
|