@qa-ai-stlc/explorer 0.3.0 → 0.5.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.
@@ -0,0 +1,24 @@
1
+ import { type Clock } from '@qa-ai-stlc/core';
2
+ import type { SelectorElement } from '@qa-ai-stlc/schemas';
3
+ export interface StaticSourceFile {
4
+ /** Project-relative, forward-slash path (`RelativePathSchema`), recorded on every finding. */
5
+ readonly filePath: string;
6
+ readonly content: string;
7
+ }
8
+ export interface AnalyzeStaticSourceOptions {
9
+ readonly files: readonly StaticSourceFile[];
10
+ readonly clock?: Clock;
11
+ }
12
+ export interface AnalyzeStaticSourceResult {
13
+ readonly elements: readonly SelectorElement[];
14
+ }
15
+ /**
16
+ * Extracts `data-testid`, `aria-label` and `role` from literal (never dynamically bound) HTML-shaped
17
+ * attributes across React, Angular and Vue sources (development plan 6.1.2), and turns each finding
18
+ * into a `SelectorElement` with `source: 'static'` and a `sourceLocation` the "missing test ID"
19
+ * report (P1-13) can point a developer at. Never touches routes (P1-19) or a live page: `kind` comes
20
+ * only from the tag name or, for a custom component, a `role` of `'button'`/`'link'`; every other
21
+ * element the source contains and does not name through one of those signals is not observed at all.
22
+ */
23
+ export declare function analyzeStaticSource(options: AnalyzeStaticSourceOptions): AnalyzeStaticSourceResult;
24
+ //# sourceMappingURL=analyze-static-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-static-source.d.ts","sourceRoot":"","sources":["../src/analyze-static-source.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAA4C,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGrG,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AA4HD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,yBAAyB,CAuBlG"}
@@ -0,0 +1,133 @@
1
+ // Copyright The QA-AI-STLC Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { hashText, systemClock } from '@qa-ai-stlc/core';
4
+ import { createElementNamer } from './naming.js';
5
+ const TAG_KIND = {
6
+ button: 'button',
7
+ a: 'link',
8
+ input: 'input',
9
+ select: 'select',
10
+ textarea: 'textarea',
11
+ };
12
+ // A role value that unambiguously identifies its own element kind, for a custom component
13
+ // (`<div role="button">`, common in React/Vue/Angular design systems) that no tag-name mapping
14
+ // above can classify. Other roles (`textbox`, `combobox`, ...) are still captured as a locator
15
+ // candidate below; they just cannot alone decide the schema's fixed `kind` enum.
16
+ const KIND_BY_ROLE = {
17
+ button: 'button',
18
+ link: 'link',
19
+ };
20
+ // A literal attribute value only: `(?<=\s)` requires the attribute name be preceded by whitespace
21
+ // (never `:`, `.` or `[`), which is how every plain HTML/JSX attribute is written and how Vue's
22
+ // `:name=`/`v-bind:name=` and Angular's `[attr.name]=`/`[name]=` bindings never are — so a bound,
23
+ // non-literal value (a JS expression, not stable text) is never captured as one. The attrs chunk
24
+ // is padded with a leading space so an attribute at the very start of it still matches.
25
+ function literalAttribute(attrs, name) {
26
+ const pattern = new RegExp(`(?<=\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, 'u');
27
+ const match = pattern.exec(` ${attrs}`);
28
+ return match?.[1] ?? match?.[2];
29
+ }
30
+ function lineNumberAt(content, index) {
31
+ let line = 1;
32
+ for (let i = 0; i < index; i += 1) {
33
+ if (content[i] === '\n') {
34
+ line += 1;
35
+ }
36
+ }
37
+ return line;
38
+ }
39
+ // Splits `<button data-testid="x">`'s inner text ("button data-testid=\"x\"") into its tag name
40
+ // and the rest, on plain string search/slice rather than a second regex capture group: unlike a
41
+ // capture group, `.search()`/`.slice()` are never `string | undefined` under
42
+ // `noUncheckedIndexedAccess`, and every opening tag genuinely has a name, so there is no real
43
+ // "missing" case here to guard against.
44
+ function splitTag(tagText) {
45
+ const boundary = tagText.search(/[\s/]/u);
46
+ const nameEnd = boundary === -1 ? tagText.length : boundary;
47
+ return { tagName: tagText.slice(0, nameEnd).toLowerCase(), attrs: tagText.slice(nameEnd) };
48
+ }
49
+ // A lexical scan of the raw text for `<tag ...>`, JSX/Vue/Angular alike (React's JSX, Vue's SFC
50
+ // template block and Angular's component template all put literal HTML-shaped attributes on an
51
+ // opening tag the same way), not a per-framework parser. It stops at the first `>` even inside a
52
+ // quoted attribute value, and can match a `<` that starts a string or comment rather than a real
53
+ // tag — an accepted, documented false-positive risk in exchange for needing no `@babel/parser`,
54
+ // `vue/compiler-sfc` or `@angular/compiler` dependency.
55
+ function findStaticElements(file) {
56
+ const findings = [];
57
+ let cursor = 0;
58
+ for (;;) {
59
+ const tagStart = file.content.indexOf('<', cursor);
60
+ if (tagStart === -1) {
61
+ break;
62
+ }
63
+ const tagEnd = file.content.indexOf('>', tagStart);
64
+ if (tagEnd === -1) {
65
+ break;
66
+ }
67
+ cursor = tagEnd + 1;
68
+ const tagText = file.content.slice(tagStart + 1, tagEnd);
69
+ if (!/^[A-Za-z]/u.test(tagText)) {
70
+ continue; // a closing tag, comment, doctype or fragment, never an opening tag with attributes
71
+ }
72
+ const { tagName, attrs } = splitTag(tagText);
73
+ const role = literalAttribute(attrs, 'role');
74
+ const kind = TAG_KIND[tagName] ?? (role === undefined ? undefined : KIND_BY_ROLE[role]);
75
+ if (kind === undefined) {
76
+ continue;
77
+ }
78
+ findings.push({
79
+ kind,
80
+ tagName,
81
+ testId: literalAttribute(attrs, 'data-testid'),
82
+ ariaLabel: literalAttribute(attrs, 'aria-label'),
83
+ role,
84
+ filePath: file.filePath,
85
+ line: lineNumberAt(file.content, tagStart),
86
+ });
87
+ }
88
+ return findings;
89
+ }
90
+ function findingNameSource(finding) {
91
+ return finding.testId ?? finding.ariaLabel ?? finding.role ?? `${finding.tagName}:${String(finding.line)}`;
92
+ }
93
+ function candidatesFor(finding) {
94
+ const candidates = [];
95
+ if (finding.role !== undefined && finding.ariaLabel !== undefined) {
96
+ candidates.push({
97
+ strategy: 'role',
98
+ value: JSON.stringify({ role: finding.role, name: finding.ariaLabel }),
99
+ fragile: false,
100
+ });
101
+ }
102
+ if (finding.testId !== undefined) {
103
+ candidates.push({ strategy: 'testId', value: finding.testId, fragile: false });
104
+ }
105
+ return candidates;
106
+ }
107
+ /**
108
+ * Extracts `data-testid`, `aria-label` and `role` from literal (never dynamically bound) HTML-shaped
109
+ * attributes across React, Angular and Vue sources (development plan 6.1.2), and turns each finding
110
+ * into a `SelectorElement` with `source: 'static'` and a `sourceLocation` the "missing test ID"
111
+ * report (P1-13) can point a developer at. Never touches routes (P1-19) or a live page: `kind` comes
112
+ * only from the tag name or, for a custom component, a `role` of `'button'`/`'link'`; every other
113
+ * element the source contains and does not name through one of those signals is not observed at all.
114
+ */
115
+ export function analyzeStaticSource(options) {
116
+ const clock = options.clock ?? systemClock;
117
+ const lastVerifiedAt = clock.now().toISOString();
118
+ const nameFor = createElementNamer();
119
+ const elements = options.files.flatMap((file) => findStaticElements(file).map((finding) => ({
120
+ elementId: hashText(`${finding.filePath} ${String(finding.line)} ${finding.kind} ${findingNameSource(finding)}`),
121
+ name: nameFor(findingNameSource(finding), finding.kind),
122
+ kind: finding.kind,
123
+ locatorCandidates: candidatesFor(finding),
124
+ stabilityScore: 0,
125
+ lastVerifiedAt,
126
+ pii: false,
127
+ dynamicText: false,
128
+ source: 'static',
129
+ sourceLocation: { filePath: finding.filePath, line: finding.line },
130
+ })));
131
+ return { elements };
132
+ }
133
+ //# sourceMappingURL=analyze-static-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-static-source.js","sourceRoot":"","sources":["../src/analyze-static-source.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAEtC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAc,MAAM,kBAAkB,CAAC;AAErE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAiBjD,MAAM,QAAQ,GAAqD;IACjE,MAAM,EAAE,QAAQ;IAChB,CAAC,EAAE,MAAM;IACT,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,UAAU;CACrB,CAAC;AAEF,0FAA0F;AAC1F,+FAA+F;AAC/F,+FAA+F;AAC/F,iFAAiF;AACjF,MAAM,YAAY,GAAqD;IACrE,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,MAAM;CACb,CAAC;AAEF,kGAAkG;AAClG,gGAAgG;AAChG,kGAAkG;AAClG,iGAAiG;AACjG,wFAAwF;AACxF,SAAS,gBAAgB,CAAC,KAAa,EAAE,IAAY;IACnD,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,WAAW,IAAI,kCAAkC,EAAE,GAAG,CAAC,CAAC;IACnF,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IACxC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,YAAY,CAAC,OAAe,EAAE,KAAa;IAClD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACxB,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAYD,gGAAgG;AAChG,gGAAgG;AAChG,6EAA6E;AAC7E,8FAA8F;AAC9F,wCAAwC;AACxC,SAAS,QAAQ,CAAC,OAAe;IAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC5D,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;AAC7F,CAAC;AAED,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,iGAAiG;AACjG,gGAAgG;AAChG,wDAAwD;AACxD,SAAS,kBAAkB,CAAC,IAAsB;IAChD,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,SAAS,CAAC;QACR,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACnD,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;YACpB,MAAM;QACR,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACnD,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,MAAM;QACR,CAAC;QACD,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC;QAEpB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QACzD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,SAAS,CAAC,oFAAoF;QAChG,CAAC;QAED,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACxF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI;YACJ,OAAO;YACP,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,aAAa,CAAC;YAC9C,SAAS,EAAE,gBAAgB,CAAC,KAAK,EAAE,YAAY,CAAC;YAChD,IAAI;YACJ,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC;SAC3C,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAsB;IAC/C,OAAO,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;AAC7G,CAAC;AAED,SAAS,aAAa,CAAC,OAAsB;IAC3C,MAAM,UAAU,GAAuB,EAAE,CAAC;IAC1C,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAClE,UAAU,CAAC,IAAI,CAAC;YACd,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;YACtE,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;IACL,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAmC;IACrE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,WAAW,CAAC;IAC3C,MAAM,cAAc,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IACjD,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IAErC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAC9C,kBAAkB,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACzC,SAAS,EAAE,QAAQ,CACjB,GAAG,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAC5F;QACD,IAAI,EAAE,OAAO,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC;QACvD,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,iBAAiB,EAAE,aAAa,CAAC,OAAO,CAAC;QACzC,cAAc,EAAE,CAAC;QACjB,cAAc;QACd,GAAG,EAAE,KAAK;QACV,WAAW,EAAE,KAAK;QAClB,MAAM,EAAE,QAAiB;QACzB,cAAc,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;KACnE,CAAC,CAAC,CACJ,CAAC;IAEF,OAAO,EAAE,QAAQ,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { MissingTestIdReport, SelectorRegistry } from '@qa-ai-stlc/schemas';
2
+ /**
3
+ * Lists every non-deprecated registry element with no `testId` locator candidate (development
4
+ * plan section 6.3 step 9), with its source file and line when the registry knows one — only
5
+ * `source: 'static'` entries (P1-12) ever do; `crawl`/`manual` entries report no `sourceLocation`
6
+ * rather than guessing. Entries with a known location sort first, by file then line; the rest sort
7
+ * by `elementId`, both orders deterministic across runs on an unchanged registry.
8
+ */
9
+ export declare function buildMissingTestIdReport(registry: SelectorRegistry): MissingTestIdReport;
10
+ /**
11
+ * Renders a `MissingTestIdReport` to Markdown (ADR-002: Markdown is rendered from JSON, never
12
+ * hand-written) grouped by source file, with entries whose file is unknown collected under their
13
+ * own heading rather than mixed in or silently dropped.
14
+ */
15
+ export declare function renderMissingTestIdReportMarkdown(report: MissingTestIdReport): string;
16
+ //# sourceMappingURL=build-missing-test-id-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-missing-test-id-report.d.ts","sourceRoot":"","sources":["../src/build-missing-test-id-report.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAsB,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAwBrG;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,mBAAmB,CAaxF;AAQD;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CA+BrF"}
@@ -0,0 +1,79 @@
1
+ // Copyright The QA-AI-STLC Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { SCHEMA_VERSION } from '@qa-ai-stlc/schemas';
4
+ function hasTestId(element) {
5
+ return element.locatorCandidates.some((candidate) => candidate.strategy === 'testId');
6
+ }
7
+ // `sourceLocation` is either present on both `a` and `b`'s underlying type or absent — it comes
8
+ // straight from `SelectorElement.sourceLocation` (selector-registry.ts), whose own two fields are
9
+ // both required together — so once both are known to be present, `a.sourceLocation.line` is never
10
+ // missing and needs no fallback.
11
+ function compareEntries(a, b) {
12
+ if (a.sourceLocation === undefined || b.sourceLocation === undefined) {
13
+ if ((a.sourceLocation === undefined) !== (b.sourceLocation === undefined)) {
14
+ return a.sourceLocation === undefined ? 1 : -1; // a known location sorts first
15
+ }
16
+ return a.elementId.localeCompare(b.elementId);
17
+ }
18
+ return (a.sourceLocation.filePath.localeCompare(b.sourceLocation.filePath) ||
19
+ a.sourceLocation.line - b.sourceLocation.line ||
20
+ a.elementId.localeCompare(b.elementId));
21
+ }
22
+ /**
23
+ * Lists every non-deprecated registry element with no `testId` locator candidate (development
24
+ * plan section 6.3 step 9), with its source file and line when the registry knows one — only
25
+ * `source: 'static'` entries (P1-12) ever do; `crawl`/`manual` entries report no `sourceLocation`
26
+ * rather than guessing. Entries with a known location sort first, by file then line; the rest sort
27
+ * by `elementId`, both orders deterministic across runs on an unchanged registry.
28
+ */
29
+ export function buildMissingTestIdReport(registry) {
30
+ const entries = registry.elements
31
+ .filter((element) => element.deprecatedAt === undefined && !hasTestId(element))
32
+ .map((element) => ({
33
+ elementId: element.elementId,
34
+ ...(element.name === undefined ? {} : { name: element.name }),
35
+ kind: element.kind,
36
+ source: element.source,
37
+ ...(element.sourceLocation === undefined ? {} : { sourceLocation: element.sourceLocation }),
38
+ }))
39
+ .sort(compareEntries);
40
+ return { schemaVersion: SCHEMA_VERSION, generatedAt: registry.generatedAt, entries };
41
+ }
42
+ function entryLine(entry) {
43
+ const label = entry.name === undefined ? entry.kind : `${entry.kind} \`${entry.name}\``;
44
+ const location = entry.sourceLocation === undefined ? '' : ` — line ${String(entry.sourceLocation.line)}`;
45
+ return `- ${label} (${entry.source})${location}`;
46
+ }
47
+ /**
48
+ * Renders a `MissingTestIdReport` to Markdown (ADR-002: Markdown is rendered from JSON, never
49
+ * hand-written) grouped by source file, with entries whose file is unknown collected under their
50
+ * own heading rather than mixed in or silently dropped.
51
+ */
52
+ export function renderMissingTestIdReportMarkdown(report) {
53
+ const count = report.entries.length;
54
+ const header = `# Missing test ID report\n\n_Generated ${report.generatedAt} — ${String(count)} element(s) with no test ID._`;
55
+ if (count === 0) {
56
+ return `${header}\n\nEvery interactive element has a test ID candidate.\n`;
57
+ }
58
+ const sections = [];
59
+ let currentFilePath;
60
+ let currentLines = [];
61
+ const flush = () => {
62
+ if (currentLines.length === 0) {
63
+ return;
64
+ }
65
+ const heading = currentFilePath ?? 'Unknown source';
66
+ sections.push(`## ${heading}\n\n${currentLines.join('\n')}`);
67
+ };
68
+ for (const entry of report.entries) {
69
+ if (entry.sourceLocation?.filePath !== currentFilePath) {
70
+ flush();
71
+ currentFilePath = entry.sourceLocation?.filePath;
72
+ currentLines = [];
73
+ }
74
+ currentLines.push(entryLine(entry));
75
+ }
76
+ flush();
77
+ return `${header}\n\n${sections.join('\n\n')}\n`;
78
+ }
79
+ //# sourceMappingURL=build-missing-test-id-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-missing-test-id-report.js","sourceRoot":"","sources":["../src/build-missing-test-id-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { type BrowserLauncher, type Clock, type ViewportSize } from '@qa-ai-stlc/core';
2
+ import { type InteractiveElement, type PageModelSet, type SelectorElement, type SelectorRegistry } from '@qa-ai-stlc/schemas';
3
+ import { type ExplorerIdentity } from './identity.js';
4
+ import { type LocatorPolicy } from './synthesize-locators.js';
5
+ export interface BuildSelectorRegistryOptions {
6
+ readonly pageModelSet: PageModelSet;
7
+ readonly browserLauncher: BrowserLauncher;
8
+ /** Signs in before re-navigating to score candidates. Omit to score anonymously. */
9
+ readonly identity?: ExplorerIdentity;
10
+ readonly policy?: LocatorPolicy;
11
+ readonly viewports?: readonly ViewportSize[];
12
+ readonly clock?: Clock;
13
+ }
14
+ export interface BuildSelectorRegistryResult {
15
+ readonly registry: SelectorRegistry;
16
+ /** Every non-GET request safe mode intercepted and cancelled; always 0 unless something is broken. */
17
+ readonly blockedRequestCount: number;
18
+ }
19
+ export declare function elementNameForId(element: InteractiveElement): string;
20
+ export declare function computeElementId(url: string, element: InteractiveElement): string;
21
+ /**
22
+ * Builds a `SelectorRegistry` from a `PageModelSet` produced by `analyzePages()` (development
23
+ * plan section 6.3 steps 4-6): synthesizes locator candidates for every interactive element under
24
+ * the configured policy, scores the first (primary) candidate's stability in a fresh live pass
25
+ * over the same URLs, and assigns each element a stable id. Every entry's `source` is `'crawl'` -
26
+ * the only source this pipeline produces (`static`/`manual` come from other tasks).
27
+ */
28
+ export declare function buildSelectorRegistry(options: BuildSelectorRegistryOptions): Promise<BuildSelectorRegistryResult>;
29
+ /**
30
+ * Merges a freshly built registry onto a previously stored one, keeping history (development plan
31
+ * section 6.3.6): every element the fresh crawl found replaces its previous entry outright
32
+ * (un-deprecating it, if it had been). An element the fresh crawl did not find is kept rather than
33
+ * deleted, with `deprecatedAt` set to `generatedAt` the first time it goes missing; once
34
+ * deprecated, its `deprecatedAt` is never overwritten by a later run that still does not find it.
35
+ */
36
+ export declare function mergeSelectorRegistry(previous: SelectorRegistry, fresh: SelectorRegistry, generatedAt: string): SelectorRegistry;
37
+ export interface DegradedSelectorElement {
38
+ readonly elementId: string;
39
+ readonly previousScore: number;
40
+ readonly currentScore: number;
41
+ }
42
+ export interface SelectorRegistryDiff {
43
+ readonly added: readonly SelectorElement[];
44
+ readonly removed: readonly SelectorElement[];
45
+ readonly degraded: readonly DegradedSelectorElement[];
46
+ }
47
+ /**
48
+ * Compares two crawls' registries by `elementId`: elements only the current crawl found, elements
49
+ * only the previous crawl found, and elements both found where the current stability score is
50
+ * lower than before.
51
+ */
52
+ export declare function diffSelectorRegistry(previous: SelectorRegistry, current: SelectorRegistry): SelectorRegistryDiff;
53
+ //# sourceMappingURL=build-selector-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-selector-registry.d.ts","sourceRoot":"","sources":["../src/build-selector-registry.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC9G,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAI3E,OAAO,EAA+B,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE3F,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,sGAAsG;IACtG,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACtC;AAOD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAQpE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAEjF;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAsDtC;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,gBAAgB,EACvB,WAAW,EAAE,MAAM,GAClB,gBAAgB,CAalB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACvD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,oBAAoB,CAoBtB"}
@@ -0,0 +1,116 @@
1
+ // Copyright The QA-AI-STLC Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { hashText, systemClock } from '@qa-ai-stlc/core';
4
+ import { SCHEMA_VERSION, } from '@qa-ai-stlc/schemas';
5
+ import { resolveStorageState } from './identity.js';
6
+ import { createElementNamer } from './naming.js';
7
+ import { createSafeModeRouteHandler } from './safe-mode.js';
8
+ import { scoreLocatorStability } from './stability-scoring.js';
9
+ import { synthesizeLocatorCandidates } from './synthesize-locators.js';
10
+ // The element's best available human-meaningful name, in the same signal-quality order
11
+ // synthesizeLocatorCandidates already uses: the id changes only when every one of those signals
12
+ // changes too, not on a DOM reorder alone. A position-based fallback is last resort, matching the
13
+ // CSS candidate's own fallback (development plan section 6.3.4). Exported for pick mode (P1-14),
14
+ // which assigns a `source: 'manual'` element the same elementId a crawl would have found for it.
15
+ export function elementNameForId(element) {
16
+ return (element.accessibleName ??
17
+ element.label ??
18
+ element.testId ??
19
+ element.placeholder ??
20
+ `${element.tagName}:${String(element.nthOfType)}`);
21
+ }
22
+ export function computeElementId(url, element) {
23
+ return hashText(`${url} ${element.kind} ${elementNameForId(element)}`);
24
+ }
25
+ /**
26
+ * Builds a `SelectorRegistry` from a `PageModelSet` produced by `analyzePages()` (development
27
+ * plan section 6.3 steps 4-6): synthesizes locator candidates for every interactive element under
28
+ * the configured policy, scores the first (primary) candidate's stability in a fresh live pass
29
+ * over the same URLs, and assigns each element a stable id. Every entry's `source` is `'crawl'` -
30
+ * the only source this pipeline produces (`static`/`manual` come from other tasks).
31
+ */
32
+ export async function buildSelectorRegistry(options) {
33
+ const clock = options.clock ?? systemClock;
34
+ const policy = options.policy ?? 'playwright-default';
35
+ const storageState = await resolveStorageState(options.browserLauncher, options.identity);
36
+ const browser = await options.browserLauncher.launch();
37
+ try {
38
+ const context = await browser.newContext(storageState === undefined ? {} : { storageState });
39
+ const page = await context.newPage();
40
+ let blockedRequestCount = 0;
41
+ await page.route('**/*', createSafeModeRouteHandler(() => {
42
+ blockedRequestCount += 1;
43
+ }));
44
+ const generatedAt = clock.now().toISOString();
45
+ const elements = [];
46
+ const nameFor = createElementNamer();
47
+ for (const pageModel of options.pageModelSet.pages) {
48
+ await page.goto(pageModel.url);
49
+ for (const interactiveElement of pageModel.interactiveElements) {
50
+ const candidates = synthesizeLocatorCandidates(interactiveElement, policy);
51
+ const primary = candidates[0];
52
+ const stabilityScore = primary === undefined
53
+ ? 0
54
+ : await scoreLocatorStability(page, primary, options.viewports === undefined ? {} : { viewports: options.viewports });
55
+ elements.push({
56
+ elementId: computeElementId(pageModel.url, interactiveElement),
57
+ name: nameFor(elementNameForId(interactiveElement), interactiveElement.kind),
58
+ kind: interactiveElement.kind,
59
+ locatorCandidates: candidates,
60
+ stabilityScore,
61
+ lastVerifiedAt: generatedAt,
62
+ pii: false,
63
+ dynamicText: false,
64
+ source: 'crawl',
65
+ });
66
+ }
67
+ }
68
+ const registry = { schemaVersion: SCHEMA_VERSION, generatedAt, elements };
69
+ return { registry, blockedRequestCount };
70
+ }
71
+ finally {
72
+ await browser.close();
73
+ }
74
+ }
75
+ /**
76
+ * Merges a freshly built registry onto a previously stored one, keeping history (development plan
77
+ * section 6.3.6): every element the fresh crawl found replaces its previous entry outright
78
+ * (un-deprecating it, if it had been). An element the fresh crawl did not find is kept rather than
79
+ * deleted, with `deprecatedAt` set to `generatedAt` the first time it goes missing; once
80
+ * deprecated, its `deprecatedAt` is never overwritten by a later run that still does not find it.
81
+ */
82
+ export function mergeSelectorRegistry(previous, fresh, generatedAt) {
83
+ const freshIds = new Set(fresh.elements.map((element) => element.elementId));
84
+ const deprecated = previous.elements
85
+ .filter((element) => !freshIds.has(element.elementId))
86
+ .map((element) => element.deprecatedAt === undefined ? { ...element, deprecatedAt: generatedAt } : element);
87
+ return {
88
+ schemaVersion: SCHEMA_VERSION,
89
+ generatedAt,
90
+ elements: [...fresh.elements, ...deprecated],
91
+ };
92
+ }
93
+ /**
94
+ * Compares two crawls' registries by `elementId`: elements only the current crawl found, elements
95
+ * only the previous crawl found, and elements both found where the current stability score is
96
+ * lower than before.
97
+ */
98
+ export function diffSelectorRegistry(previous, current) {
99
+ const previousById = new Map(previous.elements.map((element) => [element.elementId, element]));
100
+ const currentIds = new Set(current.elements.map((element) => element.elementId));
101
+ const added = current.elements.filter((element) => !previousById.has(element.elementId));
102
+ const removed = previous.elements.filter((element) => !currentIds.has(element.elementId));
103
+ const degraded = [];
104
+ for (const element of current.elements) {
105
+ const before = previousById.get(element.elementId);
106
+ if (before !== undefined && element.stabilityScore < before.stabilityScore) {
107
+ degraded.push({
108
+ elementId: element.elementId,
109
+ previousScore: before.stabilityScore,
110
+ currentScore: element.stabilityScore,
111
+ });
112
+ }
113
+ }
114
+ return { added, removed, degraded };
115
+ }
116
+ //# sourceMappingURL=build-selector-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-selector-registry.js","sourceRoot":"","sources":["../src/build-selector-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { SelectorRegistry } from '@qa-ai-stlc/schemas';
2
+ export interface GenerateLocatorModuleOptions {
3
+ /** Stamped into the module header and `GENERATOR_VERSION` so a stale file can be detected. */
4
+ readonly generatorVersion: string;
5
+ }
6
+ export interface MissingLocatorElement {
7
+ readonly elementId: string;
8
+ readonly kind: string;
9
+ }
10
+ export interface GenerateLocatorModuleResult {
11
+ /** The `tests/qa/locators.ts` file contents (ADR-006), ready to write as-is. */
12
+ readonly source: string;
13
+ /** Elements with no export: no locator candidate, or no export name assigned yet. */
14
+ readonly missingLocators: readonly MissingLocatorElement[];
15
+ }
16
+ /**
17
+ * Renders the selector registry into a typed, deterministic `tests/qa/locators.ts` module (ADR-006):
18
+ * one exported function per element, named after its registry `name`, returning a Playwright
19
+ * `Locator` built from the element's primary (highest-preference) candidate. An element with no
20
+ * candidate, or predating the `name` field, gets no export and is reported in `missingLocators`
21
+ * instead of failing generation.
22
+ */
23
+ export declare function generateLocatorModule(registry: SelectorRegistry, options: GenerateLocatorModuleOptions): GenerateLocatorModuleResult;
24
+ //# sourceMappingURL=generate-locator-module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-locator-module.d.ts","sourceRoot":"","sources":["../src/generate-locator-module.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAqC,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE/F,MAAM,WAAW,4BAA4B;IAC3C,8FAA8F;IAC9F,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,QAAQ,CAAC,eAAe,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAC5D;AA+DD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,4BAA4B,GACpC,2BAA2B,CAkC7B"}
@@ -0,0 +1,80 @@
1
+ // Copyright The QA-AI-STLC Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { QaError } from '@qa-ai-stlc/core';
4
+ function roleCall(candidate) {
5
+ const parsed = JSON.parse(candidate.value);
6
+ if (typeof parsed !== 'object' ||
7
+ parsed === null ||
8
+ !('role' in parsed) ||
9
+ !('name' in parsed) ||
10
+ typeof parsed.role !== 'string' ||
11
+ typeof parsed.name !== 'string') {
12
+ throw new QaError('explorer.locator_module.invalid_role_candidate', `role candidate value is not a {role, name} pair: ${candidate.value}`);
13
+ }
14
+ return `page.getByRole(${JSON.stringify(parsed.role)}, { name: ${JSON.stringify(parsed.name)} })`;
15
+ }
16
+ function locatorCall(candidate) {
17
+ switch (candidate.strategy) {
18
+ case 'role':
19
+ return roleCall(candidate);
20
+ case 'testId':
21
+ return `page.getByTestId(${JSON.stringify(candidate.value)})`;
22
+ case 'label':
23
+ return `page.getByLabel(${JSON.stringify(candidate.value)})`;
24
+ case 'placeholder':
25
+ return `page.getByPlaceholder(${JSON.stringify(candidate.value)})`;
26
+ case 'text':
27
+ return `page.getByText(${JSON.stringify(candidate.value)})`;
28
+ case 'css':
29
+ return `page.locator(${JSON.stringify(candidate.value)})`;
30
+ default:
31
+ throw new QaError('explorer.locator_module.unknown_strategy', `unknown locator strategy: ${candidate.strategy}`);
32
+ }
33
+ }
34
+ function isGeneratable(element) {
35
+ return element.name !== undefined && element.deprecatedAt === undefined;
36
+ }
37
+ // Pairs each generatable element with its primary candidate in one pass, narrowing out elements
38
+ // with an empty `locatorCandidates` by the presence of `primary` rather than by re-checking
39
+ // `.length`, so the later render step never needs to handle an "impossible" missing primary.
40
+ function toGeneratableEntry(element) {
41
+ const primary = element.locatorCandidates[0];
42
+ return primary === undefined ? undefined : { element, primary };
43
+ }
44
+ function byName(a, b) {
45
+ return a.element.name.localeCompare(b.element.name);
46
+ }
47
+ /**
48
+ * Renders the selector registry into a typed, deterministic `tests/qa/locators.ts` module (ADR-006):
49
+ * one exported function per element, named after its registry `name`, returning a Playwright
50
+ * `Locator` built from the element's primary (highest-preference) candidate. An element with no
51
+ * candidate, or predating the `name` field, gets no export and is reported in `missingLocators`
52
+ * instead of failing generation.
53
+ */
54
+ export function generateLocatorModule(registry, options) {
55
+ const active = registry.elements.filter((element) => element.deprecatedAt === undefined);
56
+ const missingLocators = active
57
+ .filter((element) => !isGeneratable(element) || element.locatorCandidates.length === 0)
58
+ .map((element) => ({ elementId: element.elementId, kind: element.kind }))
59
+ .sort((a, b) => a.elementId.localeCompare(b.elementId));
60
+ const generatable = active
61
+ .filter((element) => isGeneratable(element))
62
+ .map(toGeneratableEntry)
63
+ .filter((entry) => entry !== undefined)
64
+ .sort(byName);
65
+ const functions = generatable.map(({ element, primary }) => `export function ${element.name}(page: Page): Locator {\n return ${locatorCall(primary)};\n}`);
66
+ const header = [
67
+ '// Copyright The QA-AI-STLC Authors',
68
+ '// SPDX-License-Identifier: Apache-2.0',
69
+ '',
70
+ `// Generated by @qa-ai-stlc/explorer@${options.generatorVersion} from the selector registry`,
71
+ '// (ADR-006). Do not edit by hand; regenerate with `qa explore`.',
72
+ '',
73
+ "import type { Locator, Page } from 'playwright';",
74
+ '',
75
+ `export const GENERATOR_VERSION = ${JSON.stringify(options.generatorVersion)};`,
76
+ ].join('\n');
77
+ const source = `${[header, ...functions].join('\n\n')}\n`;
78
+ return { source, missingLocators };
79
+ }
80
+ //# sourceMappingURL=generate-locator-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-locator-module.js","sourceRoot":"","sources":["../src/generate-locator-module.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,12 +2,19 @@ export * from './accessibility-tree.js';
2
2
  export * from './allowlist.js';
3
3
  export * from './analyze-page.js';
4
4
  export * from './analyze-pages.js';
5
+ export * from './analyze-static-source.js';
6
+ export * from './build-missing-test-id-report.js';
7
+ export * from './build-selector-registry.js';
5
8
  export * from './crawl.js';
6
9
  export * from './extract-links.js';
10
+ export * from './generate-locator-module.js';
7
11
  export * from './identity.js';
12
+ export * from './naming.js';
8
13
  export * from './normalize.js';
9
14
  export * from './page-elements.js';
15
+ export * from './pick-mode.js';
10
16
  export * from './request-log.js';
11
17
  export * from './safe-mode.js';
18
+ export * from './stability-scoring.js';
12
19
  export * from './synthesize-locators.js';
13
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC"}
package/dist/index.js CHANGED
@@ -4,12 +4,19 @@ export * from './accessibility-tree.js';
4
4
  export * from './allowlist.js';
5
5
  export * from './analyze-page.js';
6
6
  export * from './analyze-pages.js';
7
+ export * from './analyze-static-source.js';
8
+ export * from './build-missing-test-id-report.js';
9
+ export * from './build-selector-registry.js';
7
10
  export * from './crawl.js';
8
11
  export * from './extract-links.js';
12
+ export * from './generate-locator-module.js';
9
13
  export * from './identity.js';
14
+ export * from './naming.js';
10
15
  export * from './normalize.js';
11
16
  export * from './page-elements.js';
17
+ export * from './pick-mode.js';
12
18
  export * from './request-log.js';
13
19
  export * from './safe-mode.js';
20
+ export * from './stability-scoring.js';
14
21
  export * from './synthesize-locators.js';
15
22
  //# sourceMappingURL=index.js.map