@testivai/witness-playwright 1.9.0 → 2.0.1

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/dist/mode.js DELETED
@@ -1,84 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.resolveLocalMode = resolveLocalMode;
37
- const fs = __importStar(require("fs-extra"));
38
- const path = __importStar(require("path"));
39
- /**
40
- * Read `mode` from .testivai/config.json. Returns null when the file is
41
- * absent, malformed, or has no recognized mode field.
42
- */
43
- function configMode(projectRoot) {
44
- try {
45
- const cfg = fs.readJsonSync(path.join(projectRoot, '.testivai', 'config.json'));
46
- if (cfg?.mode === 'local')
47
- return 'local';
48
- if (cfg?.mode === 'cloud')
49
- return 'cloud';
50
- return null;
51
- }
52
- catch {
53
- return null;
54
- }
55
- }
56
- /**
57
- * Resolve whether the adapter should run in local mode.
58
- *
59
- * Local-first is the zero-config default: with no API key, the adapter
60
- * captures to `.testivai/temp/<name>/` and writes an HTML report — no cloud,
61
- * no account, nothing scary. Cloud mode activates only when a key is present.
62
- *
63
- * Precedence (first match wins):
64
- * 1. TESTIVAI_MODE env — explicit override ('local' | 'cloud')
65
- * 2. .testivai/config.json `mode` — project default
66
- * 3. TESTIVAI_API_KEY presence — key ⇒ cloud, no key ⇒ local
67
- *
68
- * This is deliberately derivable from the shell environment and the config
69
- * file alone, so it resolves identically in the Playwright main process (the
70
- * reporter) and in worker processes (the `snapshot()` capture) — env vars set
71
- * at runtime by the reporter never propagate to already-spawned workers.
72
- */
73
- function resolveLocalMode(opts = {}) {
74
- const envMode = process.env.TESTIVAI_MODE;
75
- if (envMode === 'local')
76
- return true;
77
- if (envMode === 'cloud')
78
- return false;
79
- const cfg = configMode(opts.projectRoot ?? process.cwd());
80
- if (cfg)
81
- return cfg === 'local';
82
- const apiKey = opts.apiKey ?? process.env.TESTIVAI_API_KEY;
83
- return !apiKey;
84
- }
@@ -1,12 +0,0 @@
1
- import { Page } from '@playwright/test';
2
- import { StructureAnalysisConfig, StructureAnalysis, StructureChange } from './types';
3
- /**
4
- * Analyzes page structure and generates fingerprint
5
- * @renamed Was `analyzeDOM` — renamed to conceal internal layer terminology (IP protection)
6
- */
7
- export declare function analyzeStructure(page: Page, config?: StructureAnalysisConfig): Promise<StructureAnalysis>;
8
- /**
9
- * Compare two structure analyses and identify changes
10
- * @renamed Was `compareDOMAnalysis` — renamed to conceal internal layer terminology (IP protection)
11
- */
12
- export declare function compareStructureAnalysis(baseline: StructureAnalysis, current: StructureAnalysis): StructureChange[];
@@ -1,288 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.analyzeStructure = analyzeStructure;
4
- exports.compareStructureAnalysis = compareStructureAnalysis;
5
- /**
6
- * Default configuration for structure analysis
7
- * @renamed Was DEFAULT_CONFIG in domAnalyzer.ts — renamed to conceal internal layer terminology (IP protection)
8
- */
9
- const DEFAULT_CONFIG = {
10
- enableFingerprint: true,
11
- enableStructure: true,
12
- enableSemantic: true,
13
- ignoreAttributes: [
14
- 'data-testid',
15
- 'data-reactid',
16
- 'data-reactroot',
17
- 'ng-version',
18
- 'ng-version',
19
- 'ng-reflect-router-outlet',
20
- 'data-ng-version',
21
- 'style', // Inline styles change often
22
- 'class', // CSS classes can be dynamic
23
- ],
24
- ignoreElements: [
25
- 'script',
26
- 'style',
27
- 'noscript',
28
- 'meta',
29
- 'link',
30
- 'title',
31
- ],
32
- ignoreContentPatterns: [
33
- /\d{4}-\d{2}-\d{2}/, // Dates
34
- /\d{1,2}:\d{2}(:\d{2})?/, // Times
35
- /\b\d{4}\b/, // Years
36
- /\b\d+\b/, // Pure numbers
37
- /uuid-/i, // UUIDs
38
- /_\d+/, // Number suffixes
39
- /\$\d+\.?\d*/, // Currency
40
- ],
41
- };
42
- /**
43
- * Analyzes page structure and generates fingerprint
44
- * @renamed Was `analyzeDOM` — renamed to conceal internal layer terminology (IP protection)
45
- */
46
- async function analyzeStructure(page, config = {}) {
47
- const finalConfig = { ...DEFAULT_CONFIG, ...config };
48
- return await page.evaluate((cfg) => {
49
- // Helper functions
50
- const normalizeSelector = (el) => {
51
- let selector = el.nodeName.toLowerCase();
52
- // Add ID if present and not a dynamic ID
53
- if (el.id && !cfg.ignoreAttributes.includes('id')) {
54
- // Skip if ID looks dynamic
55
- if (!/^(ember|react|ng|vue|_)/.test(el.id)) {
56
- selector += `#${el.id}`;
57
- }
58
- }
59
- // Add meaningful classes only
60
- if (el.className && !cfg.ignoreAttributes.includes('class')) {
61
- const classes = el.className.toString()
62
- .split(' ')
63
- .filter((c) => c &&
64
- !/^(ember|react|ng|vue|css|active|hover|focus)/.test(c) &&
65
- !c.includes('_') &&
66
- !/\d/.test(c))
67
- .slice(0, 3); // Limit to 3 most meaningful classes
68
- if (classes.length > 0) {
69
- selector += `.${classes.join('.')}`;
70
- }
71
- }
72
- return selector;
73
- };
74
- const getElementPath = (el, maxDepth = 10) => {
75
- const path = [];
76
- let current = el;
77
- let depth = 0;
78
- while (current && current.nodeType === Node.ELEMENT_NODE && depth < maxDepth) {
79
- path.unshift(normalizeSelector(current));
80
- current = current.parentElement;
81
- depth++;
82
- }
83
- return path.join(' > ');
84
- };
85
- const shouldIgnoreElement = (el) => {
86
- return cfg.ignoreElements.includes(el.nodeName.toLowerCase());
87
- };
88
- const shouldIgnoreAttribute = (attr) => {
89
- return cfg.ignoreAttributes.includes(attr.toLowerCase());
90
- };
91
- const shouldIgnoreContent = (content) => {
92
- return cfg.ignoreContentPatterns.some((pattern) => pattern.test(content));
93
- };
94
- const normalizeText = (text) => {
95
- if (!text)
96
- return '';
97
- // Replace ignored patterns with placeholders
98
- let normalized = text.trim();
99
- cfg.ignoreContentPatterns.forEach((pattern) => {
100
- normalized = normalized.replace(pattern, '[DYNAMIC]');
101
- });
102
- // Normalize whitespace
103
- normalized = normalized.replace(/\s+/g, ' ');
104
- return normalized;
105
- };
106
- // Get all meaningful elements
107
- const allElements = Array.from(document.querySelectorAll('*'))
108
- .filter(el => !shouldIgnoreElement(el));
109
- const analysis = {};
110
- // 1. Generate fingerprint
111
- if (cfg.enableFingerprint) {
112
- const paths = allElements.map((el) => {
113
- const path = getElementPath(el);
114
- const text = normalizeText(el.textContent || '');
115
- const attrs = [];
116
- // Add important attributes
117
- for (const attr of el.attributes) {
118
- if (!shouldIgnoreAttribute(attr.name) && attr.value) {
119
- attrs.push(`${attr.name}="${attr.value}"`);
120
- }
121
- }
122
- return `${path}${text ? `|${text}` : ''}${attrs.length ? `|${attrs.join(',')}` : ''}`;
123
- });
124
- // Create hash from sorted paths
125
- const sortedPaths = paths.sort().join('|||');
126
- analysis.fingerprint = btoa(sortedPaths).substring(0, 32);
127
- }
128
- // 2. Structural analysis
129
- if (cfg.enableStructure) {
130
- const elementTypes = {};
131
- let maxDepth = 0;
132
- allElements.forEach((el) => {
133
- const tag = el.nodeName.toLowerCase();
134
- elementTypes[tag] = (elementTypes[tag] || 0) + 1;
135
- // Calculate depth
136
- let depth = 0;
137
- let current = el;
138
- while (current.parentElement) {
139
- depth++;
140
- current = current.parentElement;
141
- }
142
- maxDepth = Math.max(maxDepth, depth);
143
- });
144
- analysis.structure = {
145
- totalElements: allElements.length,
146
- elementTypes,
147
- maxDepth,
148
- interactiveElements: {
149
- buttons: document.querySelectorAll('button').length,
150
- inputs: document.querySelectorAll('input').length,
151
- links: document.querySelectorAll('a[href]').length,
152
- forms: document.querySelectorAll('form').length,
153
- },
154
- };
155
- }
156
- // 3. Semantic analysis
157
- if (cfg.enableSemantic) {
158
- const headings = {};
159
- for (let i = 1; i <= 6; i++) {
160
- headings[`h${i}`] = document.querySelectorAll(`h${i}`).length;
161
- }
162
- analysis.semantic = {
163
- headings,
164
- landmarks: {
165
- hasHeader: !!document.querySelector('header'),
166
- hasNav: !!document.querySelector('nav'),
167
- hasMain: !!document.querySelector('main'),
168
- hasFooter: !!document.querySelector('footer'),
169
- hasAside: !!document.querySelector('aside'),
170
- },
171
- lists: {
172
- ordered: document.querySelectorAll('ol').length,
173
- unordered: document.querySelectorAll('ul').length,
174
- },
175
- tables: document.querySelectorAll('table').length,
176
- images: document.querySelectorAll('img').length,
177
- };
178
- }
179
- // 4. Component analysis (framework-agnostic)
180
- const components = {};
181
- allElements.forEach((el) => {
182
- // Try various component detection strategies
183
- const componentName = el.getAttribute('data-testid')?.replace(/-/g, '') ||
184
- el.getAttribute('data-component') ||
185
- el.getAttribute('data-testid') ||
186
- el.className?.toString().match(/(\w+Container|\w+Component|\w+Page|\w+Card|\w+Modal)/i)?.[1];
187
- if (componentName && componentName.length > 2) {
188
- if (!components[componentName]) {
189
- components[componentName] = [];
190
- }
191
- const attrs = {};
192
- for (const attr of el.attributes) {
193
- if (!shouldIgnoreAttribute(attr.name) && attr.value) {
194
- attrs[attr.name] = attr.value;
195
- }
196
- }
197
- components[componentName].push({
198
- selector: getElementPath(el),
199
- text: normalizeText(el.textContent || '').substring(0, 100),
200
- attributes: attrs,
201
- });
202
- }
203
- });
204
- if (Object.keys(components).length > 0) {
205
- analysis.components = components;
206
- }
207
- return analysis;
208
- }, finalConfig);
209
- }
210
- /**
211
- * Compare two structure analyses and identify changes
212
- * @renamed Was `compareDOMAnalysis` — renamed to conceal internal layer terminology (IP protection)
213
- */
214
- function compareStructureAnalysis(baseline, current) {
215
- const changes = [];
216
- // Compare fingerprints
217
- if (baseline.fingerprint && current.fingerprint) {
218
- if (baseline.fingerprint !== current.fingerprint) {
219
- changes.push({
220
- type: 'fingerprint',
221
- severity: 'high',
222
- description: 'Page structure has changed',
223
- details: {
224
- baseline: baseline.fingerprint,
225
- current: current.fingerprint,
226
- },
227
- });
228
- }
229
- }
230
- // Compare structure
231
- if (baseline.structure && current.structure) {
232
- if (baseline.structure.totalElements !== current.structure.totalElements) {
233
- changes.push({
234
- type: 'structure',
235
- severity: 'medium',
236
- description: `Element count changed from ${baseline.structure.totalElements} to ${current.structure.totalElements}`,
237
- });
238
- }
239
- // Check for new/removed element types
240
- const baselineTypes = Object.keys(baseline.structure.elementTypes);
241
- const currentTypes = Object.keys(current.structure.elementTypes);
242
- const removed = baselineTypes.filter(t => !currentTypes.includes(t));
243
- const added = currentTypes.filter(t => !baselineTypes.includes(t));
244
- if (removed.length > 0) {
245
- changes.push({
246
- type: 'structure',
247
- severity: 'medium',
248
- description: `Removed element types: ${removed.join(', ')}`,
249
- });
250
- }
251
- if (added.length > 0) {
252
- changes.push({
253
- type: 'structure',
254
- severity: 'medium',
255
- description: `Added element types: ${added.join(', ')}`,
256
- });
257
- }
258
- }
259
- // Compare semantic structure
260
- if (baseline.semantic && current.semantic) {
261
- // Check heading changes
262
- const baselineHeadings = baseline.semantic.headings;
263
- const currentHeadings = current.semantic.headings;
264
- for (const level in baselineHeadings) {
265
- if (baselineHeadings[level] !== currentHeadings[level]) {
266
- changes.push({
267
- type: 'semantic',
268
- severity: 'low',
269
- description: `${level} count changed from ${baselineHeadings[level]} to ${currentHeadings[level]}`,
270
- });
271
- }
272
- }
273
- // Check landmark changes
274
- const baselineLandmarks = baseline.semantic.landmarks;
275
- const currentLandmarks = current.semantic.landmarks;
276
- for (const landmark in baselineLandmarks) {
277
- if (baselineLandmarks[landmark] !==
278
- currentLandmarks[landmark]) {
279
- changes.push({
280
- type: 'semantic',
281
- severity: 'medium',
282
- description: `Landmark ${landmark} ${baselineLandmarks[landmark] ? 'removed' : 'added'}`,
283
- });
284
- }
285
- }
286
- }
287
- return changes;
288
- }