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.
- package/README.md +253 -353
- package/dist/cli/cli.cjs +3243 -0
- package/dist/cli/cli.cjs.map +1 -0
- package/dist/cloud/index.cjs +149 -0
- package/dist/cloud/index.cjs.map +1 -0
- package/dist/cloud/index.d.cts +108 -0
- package/dist/cloud/index.d.ts +108 -0
- package/dist/cloud/index.js +120 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/headless/index.cjs +3580 -0
- package/dist/headless/index.cjs.map +1 -0
- package/dist/headless/index.d.cts +214 -0
- package/dist/headless/index.d.ts +214 -0
- package/dist/headless/index.js +3562 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/index.cjs +6902 -9801
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +901 -146
- package/dist/index.d.ts +901 -146
- package/dist/index.js +6896 -9805
- 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 +6936 -9808
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +673 -146
- package/dist/react/index.d.ts +673 -146
- package/dist/react/index.js +6980 -9811
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +3281 -0
- package/dist/scan/index.cjs.map +1 -0
- package/dist/scan/index.d.cts +373 -0
- package/dist/scan/index.d.ts +373 -0
- package/dist/scan/index.js +3224 -0
- package/dist/scan/index.js.map +1 -0
- package/package.json +71 -65
- package/templates/claude/audit.md +37 -0
- package/templates/claude/rules.md +212 -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,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
|
-
|
|
25
|
-
|
|
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
|
|
38
|
-
|
|
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(
|
|
111
|
+
body: JSON.stringify(handle.buildPayload()),
|
|
45
112
|
contentType: "application/json"
|
|
46
113
|
});
|
|
47
114
|
}
|
|
48
115
|
});
|
|
49
116
|
|
|
50
|
-
// src/playwright/
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 };
|
package/dist/playwright/index.js
CHANGED
|
@@ -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
|
|
6
|
-
|
|
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(
|
|
66
|
+
body: JSON.stringify(handle.buildPayload()),
|
|
13
67
|
contentType: "application/json"
|
|
14
68
|
});
|
|
15
69
|
}
|
|
16
70
|
});
|
|
17
71
|
|
|
18
|
-
// src/playwright/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|