uidex 0.2.4 → 0.3.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 (67) hide show
  1. package/README.md +253 -353
  2. package/dist/cli/cli.cjs +3243 -0
  3. package/dist/cli/cli.cjs.map +1 -0
  4. package/dist/cloud/index.cjs +149 -0
  5. package/dist/cloud/index.cjs.map +1 -0
  6. package/dist/cloud/index.d.cts +108 -0
  7. package/dist/cloud/index.d.ts +108 -0
  8. package/dist/cloud/index.js +120 -0
  9. package/dist/cloud/index.js.map +1 -0
  10. package/dist/headless/index.cjs +3580 -0
  11. package/dist/headless/index.cjs.map +1 -0
  12. package/dist/headless/index.d.cts +214 -0
  13. package/dist/headless/index.d.ts +214 -0
  14. package/dist/headless/index.js +3562 -0
  15. package/dist/headless/index.js.map +1 -0
  16. package/dist/index.cjs +6902 -9801
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +901 -146
  19. package/dist/index.d.ts +901 -146
  20. package/dist/index.js +6896 -9805
  21. package/dist/index.js.map +1 -1
  22. package/dist/playwright/index.cjs +164 -24
  23. package/dist/playwright/index.cjs.map +1 -1
  24. package/dist/playwright/index.d.cts +30 -53
  25. package/dist/playwright/index.d.ts +30 -53
  26. package/dist/playwright/index.js +148 -21
  27. package/dist/playwright/index.js.map +1 -1
  28. package/dist/playwright/reporter.cjs +62 -28
  29. package/dist/playwright/reporter.cjs.map +1 -1
  30. package/dist/playwright/reporter.d.cts +24 -12
  31. package/dist/playwright/reporter.d.ts +24 -12
  32. package/dist/playwright/reporter.js +62 -28
  33. package/dist/playwright/reporter.js.map +1 -1
  34. package/dist/react/index.cjs +6936 -9808
  35. package/dist/react/index.cjs.map +1 -1
  36. package/dist/react/index.d.cts +673 -146
  37. package/dist/react/index.d.ts +673 -146
  38. package/dist/react/index.js +6980 -9811
  39. package/dist/react/index.js.map +1 -1
  40. package/dist/scan/index.cjs +3281 -0
  41. package/dist/scan/index.cjs.map +1 -0
  42. package/dist/scan/index.d.cts +373 -0
  43. package/dist/scan/index.d.ts +373 -0
  44. package/dist/scan/index.js +3224 -0
  45. package/dist/scan/index.js.map +1 -0
  46. package/package.json +71 -65
  47. package/templates/claude/audit.md +37 -0
  48. package/templates/claude/rules.md +212 -0
  49. package/claude/audit-command.md +0 -46
  50. package/claude/rules.md +0 -167
  51. package/dist/api/index.cjs +0 -254
  52. package/dist/api/index.cjs.map +0 -1
  53. package/dist/api/index.d.cts +0 -236
  54. package/dist/api/index.d.ts +0 -236
  55. package/dist/api/index.js +0 -226
  56. package/dist/api/index.js.map +0 -1
  57. package/dist/core/index.cjs +0 -11045
  58. package/dist/core/index.cjs.map +0 -1
  59. package/dist/core/index.d.cts +0 -424
  60. package/dist/core/index.d.ts +0 -424
  61. package/dist/core/index.global.js +0 -66516
  62. package/dist/core/index.global.js.map +0 -1
  63. package/dist/core/index.js +0 -10995
  64. package/dist/core/index.js.map +0 -1
  65. package/dist/core/style.css +0 -1529
  66. package/dist/scripts/cli.cjs +0 -3904
  67. package/uidex.schema.json +0 -93
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,58 +17,196 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/playwright/index.ts
21
31
  var playwright_exports = {};
22
32
  __export(playwright_exports, {
23
33
  COVERAGE_ATTACHMENT: () => COVERAGE_ATTACHMENT,
24
- UIDEX_ATTR: () => UIDEX_ATTR,
25
- createUidexLocators: () => createUidexLocators,
34
+ FLOW_TAG: () => FLOW_TAG,
35
+ NOT_FLOW_TAG: () => NOT_FLOW_TAG,
36
+ UIDEX_ATTRS: () => UIDEX_ATTRS,
37
+ UidexCoverageReporter: () => UidexCoverageReporter,
38
+ createUidexFixture: () => createUidexFixture,
26
39
  expect: () => import_test.expect,
40
+ resolveFlow: () => resolveFlow,
27
41
  test: () => test,
28
- uidex: () => uidex,
29
42
  uidexSelector: () => uidexSelector
30
43
  });
31
44
  module.exports = __toCommonJS(playwright_exports);
32
45
 
46
+ // src/playwright/selector.ts
47
+ var ATTRS = [
48
+ "data-uidex",
49
+ "data-uidex-region",
50
+ "data-uidex-widget",
51
+ "data-uidex-primitive"
52
+ ];
53
+ var UIDEX_ATTRS = ATTRS;
54
+ function uidexSelector(id) {
55
+ const escaped = id.replace(/"/g, '\\"');
56
+ return ATTRS.map((a) => `[${a}="${escaped}"]`).join(", ");
57
+ }
58
+ function kebab(input) {
59
+ return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
60
+ }
61
+ var FLOW_TAG = "@uidex:flow";
62
+ var NOT_FLOW_TAG = "@uidex:not-flow";
63
+ var COVERAGE_ATTACHMENT = "uidex-coverage";
64
+
33
65
  // src/playwright/fixture.ts
34
66
  var import_test = require("@playwright/test");
67
+ function resolveFlow(testInfo) {
68
+ const tags = testInfo.tags ?? [];
69
+ const notFlow = tags.includes(NOT_FLOW_TAG);
70
+ if (notFlow) return { flow: null, notFlow: true };
71
+ if (!tags.includes(FLOW_TAG)) return { flow: null, notFlow: false };
72
+ const describes = describeTitles(testInfo.titlePath ?? [], testInfo.title);
73
+ const source = describes.length > 0 ? describes[describes.length - 1] : testInfo.title ?? "";
74
+ return { flow: kebab(source) || null, notFlow: false };
75
+ }
76
+ var FILE_RE = /\.(spec|test)\.(t|j)sx?$|\.(t|j)sx?$|[\\/]/;
77
+ function describeTitles(titlePath, testTitle) {
78
+ const end = titlePath.length > 0 && titlePath[titlePath.length - 1] === testTitle ? titlePath.length - 1 : titlePath.length;
79
+ const out = [];
80
+ for (let i = 0; i < end; i++) {
81
+ const entry = titlePath[i];
82
+ if (!entry) continue;
83
+ if (FILE_RE.test(entry)) continue;
84
+ if (i === 0) continue;
85
+ out.push(entry);
86
+ }
87
+ return out;
88
+ }
89
+ function createUidexFixture(page, testInfo) {
90
+ const used = /* @__PURE__ */ new Set();
91
+ const locator = (id) => {
92
+ used.add(id);
93
+ return page.locator(uidexSelector(id));
94
+ };
95
+ const buildPayload = () => {
96
+ const { flow, notFlow } = resolveFlow(testInfo);
97
+ return {
98
+ flow,
99
+ notFlow,
100
+ ids: [...used].sort(),
101
+ title: testInfo.title
102
+ };
103
+ };
104
+ return { locator, buildPayload };
105
+ }
35
106
  var test = import_test.test.extend({
36
107
  uidex: async ({ page }, use, testInfo) => {
37
- const used = /* @__PURE__ */ new Set();
38
- const locator = (id) => {
39
- used.add(id);
40
- return page.locator(uidexSelector(id));
41
- };
42
- await use(locator);
108
+ const handle = createUidexFixture(page, testInfo);
109
+ await use(handle.locator);
43
110
  await testInfo.attach(COVERAGE_ATTACHMENT, {
44
- body: JSON.stringify([...used]),
111
+ body: JSON.stringify(handle.buildPayload()),
45
112
  contentType: "application/json"
46
113
  });
47
114
  }
48
115
  });
49
116
 
50
- // src/playwright/index.ts
51
- var UIDEX_ATTR = "data-uidex";
52
- function uidexSelector(id) {
53
- return `[${UIDEX_ATTR}="${id}"]`;
54
- }
55
- var COVERAGE_ATTACHMENT = "uidex-coverage";
56
- function uidex(page, id) {
57
- return page.locator(uidexSelector(id));
58
- }
59
- function createUidexLocators(page) {
60
- return (id) => page.locator(uidexSelector(id));
117
+ // src/playwright/reporter.ts
118
+ var fs = __toESM(require("fs"), 1);
119
+ var path = __toESM(require("path"), 1);
120
+ var UidexCoverageReporter = class {
121
+ outputPath;
122
+ entityIds;
123
+ silent;
124
+ flows = /* @__PURE__ */ new Map();
125
+ untagged = [];
126
+ touched = /* @__PURE__ */ new Set();
127
+ constructor(options = {}) {
128
+ this.outputPath = options.outputPath ?? "uidex-coverage.json";
129
+ this.entityIds = options.entityIds ?? [];
130
+ this.silent = options.silent ?? false;
131
+ }
132
+ onTestEnd(_test, result) {
133
+ for (const attachment of result.attachments) {
134
+ if (attachment.name !== COVERAGE_ATTACHMENT) continue;
135
+ if (!attachment.body) continue;
136
+ const payload = parsePayload(attachment.body.toString());
137
+ if (!payload) continue;
138
+ if (payload.notFlow) continue;
139
+ for (const id of payload.ids) this.touched.add(id);
140
+ if (payload.flow) {
141
+ const entry = this.flows.get(payload.flow) ?? {
142
+ ids: /* @__PURE__ */ new Set(),
143
+ titles: /* @__PURE__ */ new Set()
144
+ };
145
+ for (const id of payload.ids) entry.ids.add(id);
146
+ entry.titles.add(payload.title);
147
+ this.flows.set(payload.flow, entry);
148
+ } else {
149
+ this.untagged.push({ title: payload.title, ids: payload.ids });
150
+ }
151
+ }
152
+ }
153
+ async onEnd(_result) {
154
+ const flows = [...this.flows.entries()].map(([flow, entry]) => ({
155
+ flow,
156
+ ids: [...entry.ids].sort(),
157
+ titles: [...entry.titles].sort()
158
+ })).sort((a, b) => a.flow.localeCompare(b.flow));
159
+ const all = [...this.entityIds];
160
+ const touched = all.filter((id) => this.touched.has(id)).sort();
161
+ const untouched = all.filter((id) => !this.touched.has(id)).sort();
162
+ const total = all.length;
163
+ const percentage = total > 0 ? Math.round(touched.length / total * 100) : 0;
164
+ const report = {
165
+ flows,
166
+ untagged: this.untagged,
167
+ touched,
168
+ untouched,
169
+ total,
170
+ percentage
171
+ };
172
+ fs.mkdirSync(path.dirname(path.resolve(this.outputPath)), {
173
+ recursive: true
174
+ });
175
+ fs.writeFileSync(
176
+ path.resolve(this.outputPath),
177
+ JSON.stringify(report, null, 2) + "\n"
178
+ );
179
+ if (!this.silent) {
180
+ 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`;
181
+ console.log(line);
182
+ }
183
+ }
184
+ };
185
+ function parsePayload(raw) {
186
+ try {
187
+ const parsed = JSON.parse(raw);
188
+ if (!parsed || !Array.isArray(parsed.ids)) return null;
189
+ return {
190
+ flow: typeof parsed.flow === "string" ? parsed.flow : null,
191
+ notFlow: Boolean(parsed.notFlow),
192
+ ids: parsed.ids.filter((x) => typeof x === "string"),
193
+ title: typeof parsed.title === "string" ? parsed.title : ""
194
+ };
195
+ } catch {
196
+ return null;
197
+ }
61
198
  }
62
199
  // Annotate the CommonJS export names for ESM import in node:
63
200
  0 && (module.exports = {
64
201
  COVERAGE_ATTACHMENT,
65
- UIDEX_ATTR,
66
- createUidexLocators,
202
+ FLOW_TAG,
203
+ NOT_FLOW_TAG,
204
+ UIDEX_ATTRS,
205
+ UidexCoverageReporter,
206
+ createUidexFixture,
67
207
  expect,
208
+ resolveFlow,
68
209
  test,
69
- uidex,
70
210
  uidexSelector
71
211
  });
72
212
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/playwright/index.ts","../../src/playwright/fixture.ts"],"sourcesContent":["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","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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAqC;AAsB9B,IAAM,OAAO,YAAAA,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;;;ADpCM,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":["base"]}
1
+ {"version":3,"sources":["../../src/playwright/index.ts","../../src/playwright/selector.ts","../../src/playwright/fixture.ts","../../src/playwright/reporter.ts"],"sourcesContent":["export {\n COVERAGE_ATTACHMENT,\n FLOW_TAG,\n NOT_FLOW_TAG,\n UIDEX_ATTRS,\n uidexSelector,\n type CoveragePayload,\n} from \"./selector\"\nexport { test, expect, resolveFlow, createUidexFixture } from \"./fixture\"\nexport type { UidexFixtures, UidexLocator, UidexFixtureHandle } from \"./fixture\"\nexport {\n default as UidexCoverageReporter,\n type FlowCoverage,\n type UidexCoverageReport,\n type UidexReporterOptions,\n} from \"./reporter\"\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","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":["base"]}
@@ -1,62 +1,39 @@
1
1
  import * as _playwright_test from '@playwright/test';
2
- import { Locator, Page } from '@playwright/test';
2
+ import { Locator, TestInfo } from '@playwright/test';
3
3
  export { expect } from '@playwright/test';
4
+ export { FlowCoverage, UidexCoverageReport, default as UidexCoverageReporter, UidexReporterOptions } from './reporter.cjs';
5
+ import '@playwright/test/reporter';
6
+
7
+ declare const UIDEX_ATTRS: readonly ["data-uidex", "data-uidex-region", "data-uidex-widget", "data-uidex-primitive"];
8
+ declare function uidexSelector(id: string): string;
9
+ declare const FLOW_TAG = "@uidex:flow";
10
+ declare const NOT_FLOW_TAG = "@uidex:not-flow";
11
+ declare const COVERAGE_ATTACHMENT = "uidex-coverage";
12
+ interface CoveragePayload {
13
+ flow: string | null;
14
+ notFlow: boolean;
15
+ ids: string[];
16
+ title: string;
17
+ }
4
18
 
5
19
  type UidexLocator<T extends string = string> = (id: T) => Locator;
6
20
  interface UidexFixtures {
7
- /**
8
- * Create a Playwright locator for a uidex-annotated element.
9
- * Automatically tracks usage for the coverage reporter.
10
- *
11
- * @example
12
- * ```ts
13
- * test('add todo', async ({ uidex }) => {
14
- * await uidex('todo-input').fill('Buy milk');
15
- * await uidex('todo-add-button').click();
16
- * });
17
- * ```
18
- */
19
21
  uidex: UidexLocator;
20
22
  }
23
+ interface FlowResolution {
24
+ flow: string | null;
25
+ notFlow: boolean;
26
+ }
27
+ type TestInfoLike = Pick<TestInfo, "tags" | "titlePath" | "title">;
28
+ declare function resolveFlow(testInfo: TestInfoLike): FlowResolution;
29
+ interface PageLike {
30
+ locator: (selector: string) => Locator;
31
+ }
32
+ interface UidexFixtureHandle {
33
+ locator: UidexLocator;
34
+ buildPayload: () => CoveragePayload;
35
+ }
36
+ declare function createUidexFixture(page: PageLike, testInfo: TestInfoLike): UidexFixtureHandle;
21
37
  declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & UidexFixtures, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
22
38
 
23
- /** The data attribute used for uidex component selectors. */
24
- declare const UIDEX_ATTR = "data-uidex";
25
- /** Build a CSS selector for a uidex-annotated element. */
26
- declare function uidexSelector(id: string): string;
27
- /** Attachment name used to pass coverage data from fixture to reporter. */
28
- declare const COVERAGE_ATTACHMENT = "uidex-coverage";
29
-
30
- /**
31
- * Create a Playwright locator for a uidex-annotated element.
32
- *
33
- * @example
34
- * ```ts
35
- * import { uidex } from 'uidex/playwright';
36
- *
37
- * test('submit form', async ({ page }) => {
38
- * await uidex(page, 'submit-btn').click();
39
- * });
40
- * ```
41
- */
42
- declare function uidex(page: Page, id: string): Locator;
43
- /**
44
- * Create a typed locator factory bound to a Page instance.
45
- * When used with the generated ComponentId type, provides autocomplete
46
- * for all annotated component IDs.
47
- *
48
- * @example
49
- * ```ts
50
- * import { createUidexLocators } from 'uidex/playwright';
51
- * import type { ComponentId } from './uidex.gen.test';
52
- *
53
- * test('checkout flow', async ({ page }) => {
54
- * const u = createUidexLocators<ComponentId>(page);
55
- * await u('cart-summary').waitFor(); // autocomplete + type checking
56
- * await u('checkout-btn').click();
57
- * });
58
- * ```
59
- */
60
- declare function createUidexLocators<T extends string = string>(page: Page): (id: T) => Locator;
61
-
62
- export { COVERAGE_ATTACHMENT, UIDEX_ATTR, type UidexFixtures, type UidexLocator, createUidexLocators, test, uidex, uidexSelector };
39
+ export { COVERAGE_ATTACHMENT, type CoveragePayload, FLOW_TAG, NOT_FLOW_TAG, UIDEX_ATTRS, type UidexFixtureHandle, type UidexFixtures, type UidexLocator, createUidexFixture, resolveFlow, test, uidexSelector };
@@ -1,62 +1,39 @@
1
1
  import * as _playwright_test from '@playwright/test';
2
- import { Locator, Page } from '@playwright/test';
2
+ import { Locator, TestInfo } from '@playwright/test';
3
3
  export { expect } from '@playwright/test';
4
+ export { FlowCoverage, UidexCoverageReport, default as UidexCoverageReporter, UidexReporterOptions } from './reporter.js';
5
+ import '@playwright/test/reporter';
6
+
7
+ declare const UIDEX_ATTRS: readonly ["data-uidex", "data-uidex-region", "data-uidex-widget", "data-uidex-primitive"];
8
+ declare function uidexSelector(id: string): string;
9
+ declare const FLOW_TAG = "@uidex:flow";
10
+ declare const NOT_FLOW_TAG = "@uidex:not-flow";
11
+ declare const COVERAGE_ATTACHMENT = "uidex-coverage";
12
+ interface CoveragePayload {
13
+ flow: string | null;
14
+ notFlow: boolean;
15
+ ids: string[];
16
+ title: string;
17
+ }
4
18
 
5
19
  type UidexLocator<T extends string = string> = (id: T) => Locator;
6
20
  interface UidexFixtures {
7
- /**
8
- * Create a Playwright locator for a uidex-annotated element.
9
- * Automatically tracks usage for the coverage reporter.
10
- *
11
- * @example
12
- * ```ts
13
- * test('add todo', async ({ uidex }) => {
14
- * await uidex('todo-input').fill('Buy milk');
15
- * await uidex('todo-add-button').click();
16
- * });
17
- * ```
18
- */
19
21
  uidex: UidexLocator;
20
22
  }
23
+ interface FlowResolution {
24
+ flow: string | null;
25
+ notFlow: boolean;
26
+ }
27
+ type TestInfoLike = Pick<TestInfo, "tags" | "titlePath" | "title">;
28
+ declare function resolveFlow(testInfo: TestInfoLike): FlowResolution;
29
+ interface PageLike {
30
+ locator: (selector: string) => Locator;
31
+ }
32
+ interface UidexFixtureHandle {
33
+ locator: UidexLocator;
34
+ buildPayload: () => CoveragePayload;
35
+ }
36
+ declare function createUidexFixture(page: PageLike, testInfo: TestInfoLike): UidexFixtureHandle;
21
37
  declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & UidexFixtures, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
22
38
 
23
- /** The data attribute used for uidex component selectors. */
24
- declare const UIDEX_ATTR = "data-uidex";
25
- /** Build a CSS selector for a uidex-annotated element. */
26
- declare function uidexSelector(id: string): string;
27
- /** Attachment name used to pass coverage data from fixture to reporter. */
28
- declare const COVERAGE_ATTACHMENT = "uidex-coverage";
29
-
30
- /**
31
- * Create a Playwright locator for a uidex-annotated element.
32
- *
33
- * @example
34
- * ```ts
35
- * import { uidex } from 'uidex/playwright';
36
- *
37
- * test('submit form', async ({ page }) => {
38
- * await uidex(page, 'submit-btn').click();
39
- * });
40
- * ```
41
- */
42
- declare function uidex(page: Page, id: string): Locator;
43
- /**
44
- * Create a typed locator factory bound to a Page instance.
45
- * When used with the generated ComponentId type, provides autocomplete
46
- * for all annotated component IDs.
47
- *
48
- * @example
49
- * ```ts
50
- * import { createUidexLocators } from 'uidex/playwright';
51
- * import type { ComponentId } from './uidex.gen.test';
52
- *
53
- * test('checkout flow', async ({ page }) => {
54
- * const u = createUidexLocators<ComponentId>(page);
55
- * await u('cart-summary').waitFor(); // autocomplete + type checking
56
- * await u('checkout-btn').click();
57
- * });
58
- * ```
59
- */
60
- declare function createUidexLocators<T extends string = string>(page: Page): (id: T) => Locator;
61
-
62
- export { COVERAGE_ATTACHMENT, UIDEX_ATTR, type UidexFixtures, type UidexLocator, createUidexLocators, test, uidex, uidexSelector };
39
+ export { COVERAGE_ATTACHMENT, type CoveragePayload, FLOW_TAG, NOT_FLOW_TAG, UIDEX_ATTRS, type UidexFixtureHandle, type UidexFixtures, type UidexLocator, createUidexFixture, resolveFlow, test, uidexSelector };
@@ -1,39 +1,166 @@
1
+ // src/playwright/selector.ts
2
+ var ATTRS = [
3
+ "data-uidex",
4
+ "data-uidex-region",
5
+ "data-uidex-widget",
6
+ "data-uidex-primitive"
7
+ ];
8
+ var UIDEX_ATTRS = ATTRS;
9
+ function uidexSelector(id) {
10
+ const escaped = id.replace(/"/g, '\\"');
11
+ return ATTRS.map((a) => `[${a}="${escaped}"]`).join(", ");
12
+ }
13
+ function kebab(input) {
14
+ return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15
+ }
16
+ var FLOW_TAG = "@uidex:flow";
17
+ var NOT_FLOW_TAG = "@uidex:not-flow";
18
+ var COVERAGE_ATTACHMENT = "uidex-coverage";
19
+
1
20
  // src/playwright/fixture.ts
2
21
  import { test as base, expect } from "@playwright/test";
22
+ function resolveFlow(testInfo) {
23
+ const tags = testInfo.tags ?? [];
24
+ const notFlow = tags.includes(NOT_FLOW_TAG);
25
+ if (notFlow) return { flow: null, notFlow: true };
26
+ if (!tags.includes(FLOW_TAG)) return { flow: null, notFlow: false };
27
+ const describes = describeTitles(testInfo.titlePath ?? [], testInfo.title);
28
+ const source = describes.length > 0 ? describes[describes.length - 1] : testInfo.title ?? "";
29
+ return { flow: kebab(source) || null, notFlow: false };
30
+ }
31
+ var FILE_RE = /\.(spec|test)\.(t|j)sx?$|\.(t|j)sx?$|[\\/]/;
32
+ function describeTitles(titlePath, testTitle) {
33
+ const end = titlePath.length > 0 && titlePath[titlePath.length - 1] === testTitle ? titlePath.length - 1 : titlePath.length;
34
+ const out = [];
35
+ for (let i = 0; i < end; i++) {
36
+ const entry = titlePath[i];
37
+ if (!entry) continue;
38
+ if (FILE_RE.test(entry)) continue;
39
+ if (i === 0) continue;
40
+ out.push(entry);
41
+ }
42
+ return out;
43
+ }
44
+ function createUidexFixture(page, testInfo) {
45
+ const used = /* @__PURE__ */ new Set();
46
+ const locator = (id) => {
47
+ used.add(id);
48
+ return page.locator(uidexSelector(id));
49
+ };
50
+ const buildPayload = () => {
51
+ const { flow, notFlow } = resolveFlow(testInfo);
52
+ return {
53
+ flow,
54
+ notFlow,
55
+ ids: [...used].sort(),
56
+ title: testInfo.title
57
+ };
58
+ };
59
+ return { locator, buildPayload };
60
+ }
3
61
  var test = base.extend({
4
62
  uidex: async ({ page }, use, testInfo) => {
5
- const used = /* @__PURE__ */ new Set();
6
- const locator = (id) => {
7
- used.add(id);
8
- return page.locator(uidexSelector(id));
9
- };
10
- await use(locator);
63
+ const handle = createUidexFixture(page, testInfo);
64
+ await use(handle.locator);
11
65
  await testInfo.attach(COVERAGE_ATTACHMENT, {
12
- body: JSON.stringify([...used]),
66
+ body: JSON.stringify(handle.buildPayload()),
13
67
  contentType: "application/json"
14
68
  });
15
69
  }
16
70
  });
17
71
 
18
- // src/playwright/index.ts
19
- var UIDEX_ATTR = "data-uidex";
20
- function uidexSelector(id) {
21
- return `[${UIDEX_ATTR}="${id}"]`;
22
- }
23
- var COVERAGE_ATTACHMENT = "uidex-coverage";
24
- function uidex(page, id) {
25
- return page.locator(uidexSelector(id));
26
- }
27
- function createUidexLocators(page) {
28
- return (id) => page.locator(uidexSelector(id));
72
+ // src/playwright/reporter.ts
73
+ import * as fs from "fs";
74
+ import * as path from "path";
75
+ var UidexCoverageReporter = class {
76
+ outputPath;
77
+ entityIds;
78
+ silent;
79
+ flows = /* @__PURE__ */ new Map();
80
+ untagged = [];
81
+ touched = /* @__PURE__ */ new Set();
82
+ constructor(options = {}) {
83
+ this.outputPath = options.outputPath ?? "uidex-coverage.json";
84
+ this.entityIds = options.entityIds ?? [];
85
+ this.silent = options.silent ?? false;
86
+ }
87
+ onTestEnd(_test, result) {
88
+ for (const attachment of result.attachments) {
89
+ if (attachment.name !== COVERAGE_ATTACHMENT) continue;
90
+ if (!attachment.body) continue;
91
+ const payload = parsePayload(attachment.body.toString());
92
+ if (!payload) continue;
93
+ if (payload.notFlow) continue;
94
+ for (const id of payload.ids) this.touched.add(id);
95
+ if (payload.flow) {
96
+ const entry = this.flows.get(payload.flow) ?? {
97
+ ids: /* @__PURE__ */ new Set(),
98
+ titles: /* @__PURE__ */ new Set()
99
+ };
100
+ for (const id of payload.ids) entry.ids.add(id);
101
+ entry.titles.add(payload.title);
102
+ this.flows.set(payload.flow, entry);
103
+ } else {
104
+ this.untagged.push({ title: payload.title, ids: payload.ids });
105
+ }
106
+ }
107
+ }
108
+ async onEnd(_result) {
109
+ const flows = [...this.flows.entries()].map(([flow, entry]) => ({
110
+ flow,
111
+ ids: [...entry.ids].sort(),
112
+ titles: [...entry.titles].sort()
113
+ })).sort((a, b) => a.flow.localeCompare(b.flow));
114
+ const all = [...this.entityIds];
115
+ const touched = all.filter((id) => this.touched.has(id)).sort();
116
+ const untouched = all.filter((id) => !this.touched.has(id)).sort();
117
+ const total = all.length;
118
+ const percentage = total > 0 ? Math.round(touched.length / total * 100) : 0;
119
+ const report = {
120
+ flows,
121
+ untagged: this.untagged,
122
+ touched,
123
+ untouched,
124
+ total,
125
+ percentage
126
+ };
127
+ fs.mkdirSync(path.dirname(path.resolve(this.outputPath)), {
128
+ recursive: true
129
+ });
130
+ fs.writeFileSync(
131
+ path.resolve(this.outputPath),
132
+ JSON.stringify(report, null, 2) + "\n"
133
+ );
134
+ if (!this.silent) {
135
+ 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`;
136
+ console.log(line);
137
+ }
138
+ }
139
+ };
140
+ function parsePayload(raw) {
141
+ try {
142
+ const parsed = JSON.parse(raw);
143
+ if (!parsed || !Array.isArray(parsed.ids)) return null;
144
+ return {
145
+ flow: typeof parsed.flow === "string" ? parsed.flow : null,
146
+ notFlow: Boolean(parsed.notFlow),
147
+ ids: parsed.ids.filter((x) => typeof x === "string"),
148
+ title: typeof parsed.title === "string" ? parsed.title : ""
149
+ };
150
+ } catch {
151
+ return null;
152
+ }
29
153
  }
30
154
  export {
31
155
  COVERAGE_ATTACHMENT,
32
- UIDEX_ATTR,
33
- createUidexLocators,
156
+ FLOW_TAG,
157
+ NOT_FLOW_TAG,
158
+ UIDEX_ATTRS,
159
+ UidexCoverageReporter,
160
+ createUidexFixture,
34
161
  expect,
162
+ resolveFlow,
35
163
  test,
36
- uidex,
37
164
  uidexSelector
38
165
  };
39
166
  //# sourceMappingURL=index.js.map