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.
Files changed (65) hide show
  1. package/README.md +253 -353
  2. package/dist/cli/cli.cjs +3324 -0
  3. package/dist/cli/cli.cjs.map +1 -0
  4. package/dist/cloud/index.cjs +169 -0
  5. package/dist/cloud/index.cjs.map +1 -0
  6. package/dist/cloud/index.js +140 -0
  7. package/dist/cloud/index.js.map +1 -0
  8. package/dist/headless/index.cjs +4143 -0
  9. package/dist/headless/index.cjs.map +1 -0
  10. package/dist/headless/index.d.cts +220 -0
  11. package/dist/headless/index.d.ts +220 -0
  12. package/dist/headless/index.js +4130 -0
  13. package/dist/headless/index.js.map +1 -0
  14. package/dist/index.cjs +8704 -9883
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +968 -146
  17. package/dist/index.d.ts +968 -146
  18. package/dist/index.js +8327 -9492
  19. package/dist/index.js.map +1 -1
  20. package/dist/playwright/index.cjs +164 -24
  21. package/dist/playwright/index.cjs.map +1 -1
  22. package/dist/playwright/index.d.cts +30 -53
  23. package/dist/playwright/index.d.ts +30 -53
  24. package/dist/playwright/index.js +148 -21
  25. package/dist/playwright/index.js.map +1 -1
  26. package/dist/playwright/reporter.cjs +62 -28
  27. package/dist/playwright/reporter.cjs.map +1 -1
  28. package/dist/playwright/reporter.d.cts +24 -12
  29. package/dist/playwright/reporter.d.ts +24 -12
  30. package/dist/playwright/reporter.js +62 -28
  31. package/dist/playwright/reporter.js.map +1 -1
  32. package/dist/react/index.cjs +8706 -9883
  33. package/dist/react/index.cjs.map +1 -1
  34. package/dist/react/index.d.cts +720 -146
  35. package/dist/react/index.d.ts +720 -146
  36. package/dist/react/index.js +8518 -9629
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/scan/index.cjs +3360 -0
  39. package/dist/scan/index.cjs.map +1 -0
  40. package/dist/scan/index.d.cts +378 -0
  41. package/dist/scan/index.d.ts +378 -0
  42. package/dist/scan/index.js +3303 -0
  43. package/dist/scan/index.js.map +1 -0
  44. package/package.json +67 -60
  45. package/templates/claude/audit.md +43 -0
  46. package/templates/claude/rules.md +227 -0
  47. package/claude/audit-command.md +0 -46
  48. package/claude/rules.md +0 -167
  49. package/dist/api/index.cjs +0 -254
  50. package/dist/api/index.cjs.map +0 -1
  51. package/dist/api/index.d.cts +0 -236
  52. package/dist/api/index.d.ts +0 -236
  53. package/dist/api/index.js +0 -226
  54. package/dist/api/index.js.map +0 -1
  55. package/dist/core/index.cjs +0 -11045
  56. package/dist/core/index.cjs.map +0 -1
  57. package/dist/core/index.d.cts +0 -424
  58. package/dist/core/index.d.ts +0 -424
  59. package/dist/core/index.global.js +0 -66516
  60. package/dist/core/index.global.js.map +0 -1
  61. package/dist/core/index.js +0 -10995
  62. package/dist/core/index.js.map +0 -1
  63. package/dist/core/style.css +0 -1529
  64. package/dist/scripts/cli.cjs +0 -3904
  65. 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: () => reporter_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/index.ts
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
- constructor(options) {
48
- this.componentIds = options.componentIds ?? [];
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 === COVERAGE_ATTACHMENT && attachment.body) {
54
- const ids = JSON.parse(attachment.body.toString());
55
- for (const id of ids) {
56
- this.interacted.add(id);
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 all = [...this.componentIds];
63
- const covered = all.filter((id) => this.interacted.has(id)).sort();
64
- const uncovered = all.filter((id) => !this.interacted.has(id)).sort();
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(covered.length / total * 100) : 0;
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
- covered,
77
- uncovered,
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(process.cwd(), this.outputPath),
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
- var reporter_default = UidexCoverageReporter;
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/index.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n Reporter,\n FullResult,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter';\nimport { COVERAGE_ATTACHMENT } from './index';\n\nexport interface UidexCoverageOptions {\n /** All known component IDs — pass componentIds from uidex.gen.test.ts */\n componentIds: readonly string[];\n /** Output file path for JSON report (default: \"uidex-coverage.json\") */\n outputPath?: string;\n}\n\nexport interface UidexCoverageReport {\n covered: string[];\n uncovered: string[];\n total: number;\n percentage: number;\n}\n\nclass UidexCoverageReporter implements Reporter {\n private interacted = new Set<string>();\n private componentIds: readonly string[];\n private outputPath: string;\n\n constructor(options: UidexCoverageOptions) {\n this.componentIds = options.componentIds ?? [];\n this.outputPath = options.outputPath ?? 'uidex-coverage.json';\n }\n\n onTestEnd(_test: TestCase, result: TestResult) {\n for (const attachment of result.attachments) {\n if (attachment.name === COVERAGE_ATTACHMENT && attachment.body) {\n const ids: string[] = JSON.parse(attachment.body.toString());\n for (const id of ids) {\n this.interacted.add(id);\n }\n }\n }\n }\n\n onEnd(_result: FullResult) {\n const all = [...this.componentIds];\n const covered = all.filter((id) => this.interacted.has(id)).sort();\n const uncovered = all.filter((id) => !this.interacted.has(id)).sort();\n const total = all.length;\n const percentage =\n total > 0 ? Math.round((covered.length / total) * 100) : 0;\n\n // Console summary\n console.log('');\n console.log(\n `uidex coverage: ${covered.length}/${total} components (${percentage}%)`\n );\n if (uncovered.length > 0) {\n console.log(` uncovered: ${uncovered.join(', ')}`);\n }\n console.log('');\n\n // JSON report\n const report: UidexCoverageReport = {\n covered,\n uncovered,\n total,\n percentage,\n };\n fs.writeFileSync(\n path.resolve(process.cwd(), this.outputPath),\n JSON.stringify(report, null, 2) + '\\n'\n );\n }\n}\n\nexport default UidexCoverageReporter;\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;AAAA;AAAA;AAAA;AAAA;AAAA,SAAoB;AACpB,WAAsB;;;ACUf,IAAM,sBAAsB;;;ADanC,IAAM,wBAAN,MAAgD;AAAA,EACtC,aAAa,oBAAI,IAAY;AAAA,EAC7B;AAAA,EACA;AAAA,EAER,YAAY,SAA+B;AACzC,SAAK,eAAe,QAAQ,gBAAgB,CAAC;AAC7C,SAAK,aAAa,QAAQ,cAAc;AAAA,EAC1C;AAAA,EAEA,UAAU,OAAiB,QAAoB;AAC7C,eAAW,cAAc,OAAO,aAAa;AAC3C,UAAI,WAAW,SAAS,uBAAuB,WAAW,MAAM;AAC9D,cAAM,MAAgB,KAAK,MAAM,WAAW,KAAK,SAAS,CAAC;AAC3D,mBAAW,MAAM,KAAK;AACpB,eAAK,WAAW,IAAI,EAAE;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,SAAqB;AACzB,UAAM,MAAM,CAAC,GAAG,KAAK,YAAY;AACjC,UAAM,UAAU,IAAI,OAAO,CAAC,OAAO,KAAK,WAAW,IAAI,EAAE,CAAC,EAAE,KAAK;AACjE,UAAM,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,WAAW,IAAI,EAAE,CAAC,EAAE,KAAK;AACpE,UAAM,QAAQ,IAAI;AAClB,UAAM,aACJ,QAAQ,IAAI,KAAK,MAAO,QAAQ,SAAS,QAAS,GAAG,IAAI;AAG3D,YAAQ,IAAI,EAAE;AACd,YAAQ;AAAA,MACN,mBAAmB,QAAQ,MAAM,IAAI,KAAK,gBAAgB,UAAU;AAAA,IACtE;AACA,QAAI,UAAU,SAAS,GAAG;AACxB,cAAQ,IAAI,gBAAgB,UAAU,KAAK,IAAI,CAAC,EAAE;AAAA,IACpD;AACA,YAAQ,IAAI,EAAE;AAGd,UAAM,SAA8B;AAAA,MAClC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,IAAG;AAAA,MACI,aAAQ,QAAQ,IAAI,GAAG,KAAK,UAAU;AAAA,MAC3C,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI;AAAA,IACpC;AAAA,EACF;AACF;AAEA,IAAO,mBAAQ;","names":[]}
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAoB;AACpB,WAAsB;;;ACuBf,IAAM,sBAAsB;;;ADOnC,IAAqB,wBAArB,MAA+D;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ,oBAAI,IAG3B;AAAA,EACe,WAA+C,CAAC;AAAA,EAChD,UAAU,oBAAI,IAAY;AAAA,EAE3C,YAAY,UAAgC,CAAC,GAAG;AAC9C,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,YAAY,QAAQ,aAAa,CAAC;AACvC,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,UAAU,OAAiB,QAA0B;AACnD,eAAW,cAAc,OAAO,aAAa;AAC3C,UAAI,WAAW,SAAS,oBAAqB;AAC7C,UAAI,CAAC,WAAW,KAAM;AACtB,YAAM,UAAU,aAAa,WAAW,KAAK,SAAS,CAAC;AACvD,UAAI,CAAC,QAAS;AACd,UAAI,QAAQ,QAAS;AACrB,iBAAW,MAAM,QAAQ,IAAK,MAAK,QAAQ,IAAI,EAAE;AACjD,UAAI,QAAQ,MAAM;AAChB,cAAM,QAAQ,KAAK,MAAM,IAAI,QAAQ,IAAI,KAAK;AAAA,UAC5C,KAAK,oBAAI,IAAY;AAAA,UACrB,QAAQ,oBAAI,IAAY;AAAA,QAC1B;AACA,mBAAW,MAAM,QAAQ,IAAK,OAAM,IAAI,IAAI,EAAE;AAC9C,cAAM,OAAO,IAAI,QAAQ,KAAK;AAC9B,aAAK,MAAM,IAAI,QAAQ,MAAM,KAAK;AAAA,MACpC,OAAO;AACL,aAAK,SAAS,KAAK,EAAE,OAAO,QAAQ,OAAO,KAAK,QAAQ,IAAI,CAAC;AAAA,MAC/D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,SAAoC;AAC9C,UAAM,QAAwB,CAAC,GAAG,KAAK,MAAM,QAAQ,CAAC,EACnD,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO;AAAA,MACvB;AAAA,MACA,KAAK,CAAC,GAAG,MAAM,GAAG,EAAE,KAAK;AAAA,MACzB,QAAQ,CAAC,GAAG,MAAM,MAAM,EAAE,KAAK;AAAA,IACjC,EAAE,EACD,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAE9C,UAAM,MAAM,CAAC,GAAG,KAAK,SAAS;AAC9B,UAAM,UAAU,IAAI,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,EAAE,CAAC,EAAE,KAAK;AAC9D,UAAM,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,EAAE,CAAC,EAAE,KAAK;AACjE,UAAM,QAAQ,IAAI;AAClB,UAAM,aACJ,QAAQ,IAAI,KAAK,MAAO,QAAQ,SAAS,QAAS,GAAG,IAAI;AAE3D,UAAM,SAA8B;AAAA,MAClC;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,IAAG,aAAe,aAAa,aAAQ,KAAK,UAAU,CAAC,GAAG;AAAA,MACxD,WAAW;AAAA,IACb,CAAC;AACD,IAAG;AAAA,MACI,aAAQ,KAAK,UAAU;AAAA,MAC5B,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI;AAAA,IACpC;AAEA,QAAI,CAAC,KAAK,QAAQ;AAChB,YAAM,OACJ,QAAQ,IACJ,mBAAmB,QAAQ,MAAM,IAAI,KAAK,cAAc,UAAU,aAAa,MAAM,MAAM,aAC3F,mBAAmB,MAAM,MAAM,aAAa,KAAK,QAAQ,IAAI;AAEnE,cAAQ,IAAI,IAAI;AAAA,IAClB;AAAA,EACF;AACF;AAEA,SAAS,aAAa,KAAqC;AACzD,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,CAAC,UAAU,CAAC,MAAM,QAAQ,OAAO,GAAG,EAAG,QAAO;AAClD,WAAO;AAAA,MACL,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,MACtD,SAAS,QAAQ,OAAO,OAAO;AAAA,MAC/B,KAAK,OAAO,IAAI,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AAAA,MAChE,OAAO,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,IAC3D;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,24 +1,36 @@
1
1
  import { Reporter, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
2
 
3
- interface UidexCoverageOptions {
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
- covered: string[];
11
- uncovered: string[];
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 interacted;
17
- private componentIds;
18
- private outputPath;
19
- constructor(options: UidexCoverageOptions);
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 UidexCoverageOptions, type UidexCoverageReport, UidexCoverageReporter as default };
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 UidexCoverageOptions {
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
- covered: string[];
11
- uncovered: string[];
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 interacted;
17
- private componentIds;
18
- private outputPath;
19
- constructor(options: UidexCoverageOptions);
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 UidexCoverageOptions, type UidexCoverageReport, UidexCoverageReporter as default };
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/index.ts
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
- constructor(options) {
14
- this.componentIds = options.componentIds ?? [];
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 === COVERAGE_ATTACHMENT && attachment.body) {
20
- const ids = JSON.parse(attachment.body.toString());
21
- for (const id of ids) {
22
- this.interacted.add(id);
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 all = [...this.componentIds];
29
- const covered = all.filter((id) => this.interacted.has(id)).sort();
30
- const uncovered = all.filter((id) => !this.interacted.has(id)).sort();
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(covered.length / total * 100) : 0;
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
- covered,
43
- uncovered,
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(process.cwd(), this.outputPath),
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
- var reporter_default = UidexCoverageReporter;
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
- reporter_default as default
89
+ UidexCoverageReporter as default
56
90
  };
57
91
  //# sourceMappingURL=reporter.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/playwright/reporter.ts","../../src/playwright/index.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n Reporter,\n FullResult,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter';\nimport { COVERAGE_ATTACHMENT } from './index';\n\nexport interface UidexCoverageOptions {\n /** All known component IDs — pass componentIds from uidex.gen.test.ts */\n componentIds: readonly string[];\n /** Output file path for JSON report (default: \"uidex-coverage.json\") */\n outputPath?: string;\n}\n\nexport interface UidexCoverageReport {\n covered: string[];\n uncovered: string[];\n total: number;\n percentage: number;\n}\n\nclass UidexCoverageReporter implements Reporter {\n private interacted = new Set<string>();\n private componentIds: readonly string[];\n private outputPath: string;\n\n constructor(options: UidexCoverageOptions) {\n this.componentIds = options.componentIds ?? [];\n this.outputPath = options.outputPath ?? 'uidex-coverage.json';\n }\n\n onTestEnd(_test: TestCase, result: TestResult) {\n for (const attachment of result.attachments) {\n if (attachment.name === COVERAGE_ATTACHMENT && attachment.body) {\n const ids: string[] = JSON.parse(attachment.body.toString());\n for (const id of ids) {\n this.interacted.add(id);\n }\n }\n }\n }\n\n onEnd(_result: FullResult) {\n const all = [...this.componentIds];\n const covered = all.filter((id) => this.interacted.has(id)).sort();\n const uncovered = all.filter((id) => !this.interacted.has(id)).sort();\n const total = all.length;\n const percentage =\n total > 0 ? Math.round((covered.length / total) * 100) : 0;\n\n // Console summary\n console.log('');\n console.log(\n `uidex coverage: ${covered.length}/${total} components (${percentage}%)`\n );\n if (uncovered.length > 0) {\n console.log(` uncovered: ${uncovered.join(', ')}`);\n }\n console.log('');\n\n // JSON report\n const report: UidexCoverageReport = {\n covered,\n uncovered,\n total,\n percentage,\n };\n fs.writeFileSync(\n path.resolve(process.cwd(), this.outputPath),\n JSON.stringify(report, null, 2) + '\\n'\n );\n }\n}\n\nexport default UidexCoverageReporter;\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,YAAY,QAAQ;AACpB,YAAY,UAAU;;;ACUf,IAAM,sBAAsB;;;ADanC,IAAM,wBAAN,MAAgD;AAAA,EACtC,aAAa,oBAAI,IAAY;AAAA,EAC7B;AAAA,EACA;AAAA,EAER,YAAY,SAA+B;AACzC,SAAK,eAAe,QAAQ,gBAAgB,CAAC;AAC7C,SAAK,aAAa,QAAQ,cAAc;AAAA,EAC1C;AAAA,EAEA,UAAU,OAAiB,QAAoB;AAC7C,eAAW,cAAc,OAAO,aAAa;AAC3C,UAAI,WAAW,SAAS,uBAAuB,WAAW,MAAM;AAC9D,cAAM,MAAgB,KAAK,MAAM,WAAW,KAAK,SAAS,CAAC;AAC3D,mBAAW,MAAM,KAAK;AACpB,eAAK,WAAW,IAAI,EAAE;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,SAAqB;AACzB,UAAM,MAAM,CAAC,GAAG,KAAK,YAAY;AACjC,UAAM,UAAU,IAAI,OAAO,CAAC,OAAO,KAAK,WAAW,IAAI,EAAE,CAAC,EAAE,KAAK;AACjE,UAAM,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,WAAW,IAAI,EAAE,CAAC,EAAE,KAAK;AACpE,UAAM,QAAQ,IAAI;AAClB,UAAM,aACJ,QAAQ,IAAI,KAAK,MAAO,QAAQ,SAAS,QAAS,GAAG,IAAI;AAG3D,YAAQ,IAAI,EAAE;AACd,YAAQ;AAAA,MACN,mBAAmB,QAAQ,MAAM,IAAI,KAAK,gBAAgB,UAAU;AAAA,IACtE;AACA,QAAI,UAAU,SAAS,GAAG;AACxB,cAAQ,IAAI,gBAAgB,UAAU,KAAK,IAAI,CAAC,EAAE;AAAA,IACpD;AACA,YAAQ,IAAI,EAAE;AAGd,UAAM,SAA8B;AAAA,MAClC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,IAAG;AAAA,MACI,aAAQ,QAAQ,IAAI,GAAG,KAAK,UAAU;AAAA,MAC3C,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI;AAAA,IACpC;AAAA,EACF;AACF;AAEA,IAAO,mBAAQ;","names":[]}
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":[]}