@testivai/witness-playwright 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/__tests__/.gitkeep +0 -0
  2. package/__tests__/config-integration.spec.ts +102 -0
  3. package/__tests__/snapshot.spec.d.ts +1 -0
  4. package/__tests__/snapshot.spec.js +81 -0
  5. package/__tests__/snapshot.spec.ts +58 -0
  6. package/__tests__/unit/ci.spec.d.ts +1 -0
  7. package/__tests__/unit/ci.spec.js +35 -0
  8. package/__tests__/unit/ci.spec.ts +40 -0
  9. package/__tests__/unit/reporter.spec.d.ts +1 -0
  10. package/__tests__/unit/reporter.spec.js +37 -0
  11. package/__tests__/unit/reporter.spec.ts +43 -0
  12. package/__tests__/unit/structureAnalyzer.spec.js +212 -0
  13. package/__tests__/unit/types.spec.ts +179 -0
  14. package/dist/__tests__/unit/ci.spec.d.ts +1 -0
  15. package/dist/__tests__/unit/ci.spec.js +226 -0
  16. package/dist/__tests__/unit/compression.spec.d.ts +4 -0
  17. package/dist/__tests__/unit/compression.spec.js +46 -0
  18. package/dist/ci.d.ts +30 -0
  19. package/dist/ci.js +117 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +47 -0
  22. package/dist/cli/init.d.ts +3 -0
  23. package/dist/cli/init.js +158 -0
  24. package/dist/config/loader.d.ts +29 -0
  25. package/dist/config/loader.js +251 -0
  26. package/dist/domAnalyzer.d.ts +10 -0
  27. package/dist/domAnalyzer.js +285 -0
  28. package/dist/index.d.ts +7 -0
  29. package/dist/index.js +11 -0
  30. package/dist/reporter-entry.d.ts +2 -0
  31. package/dist/reporter-entry.js +5 -0
  32. package/dist/reporter-types.d.ts +2 -0
  33. package/dist/reporter-types.js +2 -0
  34. package/dist/reporter.d.ts +21 -0
  35. package/dist/reporter.js +249 -0
  36. package/dist/snapshot.d.ts +12 -0
  37. package/dist/snapshot.js +601 -0
  38. package/dist/structureAnalyzer.d.ts +12 -0
  39. package/dist/structureAnalyzer.js +288 -0
  40. package/dist/types.d.ts +368 -0
  41. package/dist/types.js +10 -0
  42. package/examples/structure-analysis-example.spec.ts +118 -0
  43. package/examples/structure-analysis.config.ts +159 -0
  44. package/jest.config.js +8 -0
  45. package/package.json +51 -0
  46. package/playwright.config.ts +11 -0
  47. package/src/__tests__/unit/ci.spec.ts +257 -0
  48. package/src/__tests__/unit/compression.spec.ts +52 -0
  49. package/src/ci.ts +140 -0
  50. package/src/cli/index.ts +49 -0
  51. package/src/cli/init.ts +131 -0
  52. package/src/config/loader.ts +238 -0
  53. package/src/index.ts +14 -0
  54. package/src/reporter-entry.ts +6 -0
  55. package/src/reporter-types.ts +5 -0
  56. package/src/reporter.ts +251 -0
  57. package/src/snapshot.ts +632 -0
  58. package/src/structureAnalyzer.ts +338 -0
  59. package/src/types.ts +388 -0
  60. package/tsconfig.jest.json +7 -0
  61. package/tsconfig.json +20 -0
File without changes
@@ -0,0 +1,102 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { snapshot } from '../src/snapshot';
3
+ import * as fs from 'fs-extra';
4
+ import * as path from 'path';
5
+
6
+ test.describe('Configuration Integration', () => {
7
+
8
+ test('loads default configuration when no config file exists', async ({ page }) => {
9
+ // Remove any existing config file
10
+ const configPath = path.join(process.cwd(), 'testivai.config.ts');
11
+ if (await fs.pathExists(configPath)) {
12
+ await fs.remove(configPath);
13
+ }
14
+
15
+ await page.goto('data:text/html,<html><body><h1>Test Page</h1></body></html>');
16
+
17
+ // Mock TestInfo object
18
+ const mockTestInfo = {
19
+ title: 'Configuration Test'
20
+ } as any;
21
+
22
+ // This should use default configuration
23
+ await snapshot(page, mockTestInfo, 'test-no-config');
24
+
25
+ // Check that metadata was created with default config
26
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
27
+ const files = await fs.readdir(tempDir);
28
+ const metadataFile = files.find(f => f.endsWith('.json'));
29
+
30
+ expect(metadataFile).toBeDefined();
31
+
32
+ const metadata = await fs.readJson(path.join(tempDir, metadataFile!));
33
+ expect(metadata.testivaiConfig).toBeDefined();
34
+ expect(metadata.testivaiConfig.layout.sensitivity).toBe(2);
35
+ expect(metadata.testivaiConfig.layout.tolerance).toBe(1.0);
36
+ expect(metadata.testivaiConfig.ai.sensitivity).toBe(2);
37
+ expect(metadata.testivaiConfig.ai.confidence).toBe(0.7);
38
+ });
39
+
40
+ test('uses custom configuration when provided', async ({ page }) => {
41
+ await page.goto('data:text/html,<html><body><h1>Test Page</h1></body></html>');
42
+
43
+ // Mock TestInfo object
44
+ const mockTestInfo = {
45
+ title: 'Custom Config Test'
46
+ } as any;
47
+
48
+ // Test with custom configuration
49
+ const customConfig = {
50
+ layout: {
51
+ sensitivity: 0, // Strict
52
+ tolerance: 0.5
53
+ },
54
+ ai: {
55
+ sensitivity: 4, // Aggressive
56
+ confidence: 0.9
57
+ },
58
+ selectors: ['h1']
59
+ };
60
+
61
+ await snapshot(page, mockTestInfo, 'test-custom-config', customConfig);
62
+
63
+ // Check that metadata was created with custom config
64
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
65
+ const files = await fs.readdir(tempDir);
66
+ const metadataFile = files.find(f => f.includes('test_custom_config') && f.endsWith('.json'));
67
+
68
+ expect(metadataFile).toBeDefined();
69
+
70
+ const metadata = await fs.readJson(path.join(tempDir, metadataFile!));
71
+ expect(metadata.testivaiConfig).toBeDefined();
72
+ expect(metadata.testivaiConfig.layout.sensitivity).toBe(0);
73
+ expect(metadata.testivaiConfig.layout.tolerance).toBe(0.5);
74
+ expect(metadata.testivaiConfig.ai.sensitivity).toBe(4);
75
+ expect(metadata.testivaiConfig.ai.confidence).toBe(0.9);
76
+ expect(metadata.testivaiConfig.selectors).toEqual(['h1']);
77
+ });
78
+
79
+ test('CLI generates valid configuration file', async () => {
80
+ // Remove existing config
81
+ const configPath = path.join(process.cwd(), 'testivai.config.ts');
82
+ if (await fs.pathExists(configPath)) {
83
+ await fs.remove(configPath);
84
+ }
85
+
86
+ // Run CLI init using direct file system operations
87
+ const { createConfigFile } = require('../dist/cli/init');
88
+ await createConfigFile();
89
+
90
+ // Verify config file was created
91
+ expect(await fs.pathExists(configPath)).toBe(true);
92
+
93
+ // Verify config file content
94
+ const configContent = await fs.readFile(configPath, 'utf8');
95
+ expect(configContent).toContain('export default');
96
+ expect(configContent).toContain('sensitivity: 2');
97
+ expect(configContent).toContain('tolerance: 1.0');
98
+ expect(configContent).toContain('confidence: 0.7');
99
+ expect(configContent).toContain('Custom Configuration Examples');
100
+ });
101
+
102
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
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
+ const test_1 = require("@playwright/test");
37
+ const fs = __importStar(require("fs-extra"));
38
+ const path = __importStar(require("path"));
39
+ const src_1 = require("../src");
40
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
41
+ test_1.test.describe('testivai.witness()', () => {
42
+ // Ensure the temp directory is clean before each test
43
+ test_1.test.beforeEach(async () => {
44
+ await fs.emptyDir(tempDir);
45
+ });
46
+ test_1.test.afterAll(async () => {
47
+ await fs.emptyDir(tempDir);
48
+ });
49
+ (0, test_1.test)('should create all evidence files for a named snapshot', async ({ page }, testInfo) => {
50
+ await page.setContent('<body><h1>Hello Snapshot!</h1></body>');
51
+ await src_1.testivai.witness(page, testInfo, 'test-snapshot');
52
+ const files = await fs.readdir(tempDir);
53
+ (0, test_1.expect)(files).toHaveLength(3);
54
+ const jsonFile = files.find(f => f.endsWith('.json'));
55
+ const htmlFile = files.find(f => f.endsWith('.html'));
56
+ const pngFile = files.find(f => f.endsWith('.png'));
57
+ (0, test_1.expect)(jsonFile, 'JSON file should exist').toBeDefined();
58
+ (0, test_1.expect)(htmlFile, 'HTML file should exist').toBeDefined();
59
+ (0, test_1.expect)(pngFile, 'PNG file should exist').toBeDefined();
60
+ // Verify JSON metadata content
61
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile));
62
+ (0, test_1.expect)(metadata.snapshotName).toBe('test-snapshot');
63
+ (0, test_1.expect)(metadata.testName).toBe('should create all evidence files for a named snapshot');
64
+ (0, test_1.expect)(metadata.layout.body).toBeDefined();
65
+ (0, test_1.expect)(typeof metadata.layout.body.x).toBe('number');
66
+ // Verify HTML content
67
+ const htmlContent = await fs.readFile(path.join(tempDir, htmlFile), 'utf-8');
68
+ (0, test_1.expect)(htmlContent).toContain('<h1>Hello Snapshot!</h1>');
69
+ });
70
+ (0, test_1.test)('should generate a snapshot name from the URL if not provided', async ({ page }, testInfo) => {
71
+ // Using a data URL is a reliable way to set a page URL in a test environment
72
+ await page.goto('data:text/html,<h2>Page Title</h2>');
73
+ await src_1.testivai.witness(page, testInfo);
74
+ const files = await fs.readdir(tempDir);
75
+ const jsonFile = files.find(f => f.endsWith('.json'));
76
+ (0, test_1.expect)(jsonFile, 'JSON file should exist').toBeDefined();
77
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile));
78
+ // The default name for a data URL or empty path is 'snapshot'
79
+ (0, test_1.expect)(metadata.snapshotName).toBe('snapshot');
80
+ });
81
+ });
@@ -0,0 +1,58 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import * as fs from 'fs-extra';
3
+ import * as path from 'path';
4
+ import { testivai } from '../src';
5
+
6
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
7
+
8
+ test.describe('testivai.witness()', () => {
9
+ // Ensure the temp directory is clean before each test
10
+ test.beforeEach(async () => {
11
+ await fs.emptyDir(tempDir);
12
+ });
13
+
14
+ test.afterAll(async () => {
15
+ await fs.emptyDir(tempDir);
16
+ });
17
+
18
+ test('should create all evidence files for a named snapshot', async ({ page }, testInfo) => {
19
+ await page.setContent('<body><h1>Hello Snapshot!</h1></body>');
20
+ await testivai.witness(page, testInfo, 'test-snapshot');
21
+
22
+ const files = await fs.readdir(tempDir);
23
+ expect(files).toHaveLength(3);
24
+
25
+ const jsonFile = files.find(f => f.endsWith('.json'));
26
+ const htmlFile = files.find(f => f.endsWith('.html'));
27
+ const pngFile = files.find(f => f.endsWith('.png'));
28
+
29
+ expect(jsonFile, 'JSON file should exist').toBeDefined();
30
+ expect(htmlFile, 'HTML file should exist').toBeDefined();
31
+ expect(pngFile, 'PNG file should exist').toBeDefined();
32
+
33
+ // Verify JSON metadata content
34
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile!));
35
+ expect(metadata.snapshotName).toBe('test-snapshot');
36
+ expect(metadata.testName).toBe('should create all evidence files for a named snapshot');
37
+ expect(metadata.layout.body).toBeDefined();
38
+ expect(typeof metadata.layout.body.x).toBe('number');
39
+
40
+ // Verify HTML content
41
+ const htmlContent = await fs.readFile(path.join(tempDir, htmlFile!), 'utf-8');
42
+ expect(htmlContent).toContain('<h1>Hello Snapshot!</h1>');
43
+ });
44
+
45
+ test('should generate a snapshot name from the URL if not provided', async ({ page }, testInfo) => {
46
+ // Using a data URL is a reliable way to set a page URL in a test environment
47
+ await page.goto('data:text/html,<h2>Page Title</h2>');
48
+ await testivai.witness(page, testInfo);
49
+
50
+ const files = await fs.readdir(tempDir);
51
+ const jsonFile = files.find(f => f.endsWith('.json'));
52
+ expect(jsonFile, 'JSON file should exist').toBeDefined();
53
+
54
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile!));
55
+ // The default name for a data URL or empty path is 'snapshot'
56
+ expect(metadata.snapshotName).toBe('snapshot');
57
+ });
58
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ci_1 = require("../../src/ci");
4
+ describe('getCiRunId', () => {
5
+ const OLD_ENV = process.env;
6
+ beforeEach(() => {
7
+ jest.resetModules(); // Clears the module cache
8
+ // Start with a clean environment, preserving only essential vars like PATH
9
+ process.env = { PATH: OLD_ENV.PATH };
10
+ });
11
+ afterEach(() => {
12
+ process.env = OLD_ENV; // Restore original environment
13
+ });
14
+ test('should return GitHub run ID when in GitHub Actions', () => {
15
+ process.env.GITHUB_RUN_ID = 'github123';
16
+ expect((0, ci_1.getCiRunId)()).toBe('github-github123');
17
+ });
18
+ test('should return GitLab pipeline ID when in GitLab CI', () => {
19
+ process.env.GITLAB_CI = 'true';
20
+ process.env.CI_PIPELINE_ID = 'gitlab456';
21
+ expect((0, ci_1.getCiRunId)()).toBe('gitlab-gitlab456');
22
+ });
23
+ test('should return Jenkins build ID when in Jenkins', () => {
24
+ process.env.JENKINS_URL = 'http://jenkins.example.com';
25
+ process.env.BUILD_ID = 'jenkins789';
26
+ expect((0, ci_1.getCiRunId)()).toBe('jenkins-jenkins789');
27
+ });
28
+ test('should return null when not in a known CI environment', () => {
29
+ // Ensure no CI variables are set
30
+ delete process.env.GITHUB_RUN_ID;
31
+ delete process.env.GITLAB_CI;
32
+ delete process.env.JENKINS_URL;
33
+ expect((0, ci_1.getCiRunId)()).toBeNull();
34
+ });
35
+ });
@@ -0,0 +1,40 @@
1
+ import { getCiRunId } from '../../src/ci';
2
+
3
+ describe('getCiRunId', () => {
4
+ const OLD_ENV = process.env;
5
+
6
+ beforeEach(() => {
7
+ jest.resetModules(); // Clears the module cache
8
+ // Start with a clean environment, preserving only essential vars like PATH
9
+ process.env = { PATH: OLD_ENV.PATH };
10
+ });
11
+
12
+ afterEach(() => {
13
+ process.env = OLD_ENV; // Restore original environment
14
+ });
15
+
16
+ test('should return GitHub run ID when in GitHub Actions', () => {
17
+ process.env.GITHUB_RUN_ID = 'github123';
18
+ expect(getCiRunId()).toBe('github-github123');
19
+ });
20
+
21
+ test('should return GitLab pipeline ID when in GitLab CI', () => {
22
+ process.env.GITLAB_CI = 'true';
23
+ process.env.CI_PIPELINE_ID = 'gitlab456';
24
+ expect(getCiRunId()).toBe('gitlab-gitlab456');
25
+ });
26
+
27
+ test('should return Jenkins build ID when in Jenkins', () => {
28
+ process.env.JENKINS_URL = 'http://jenkins.example.com';
29
+ process.env.BUILD_ID = 'jenkins789';
30
+ expect(getCiRunId()).toBe('jenkins-jenkins789');
31
+ });
32
+
33
+ test('should return null when not in a known CI environment', () => {
34
+ // Ensure no CI variables are set
35
+ delete process.env.GITHUB_RUN_ID;
36
+ delete process.env.GITLAB_CI;
37
+ delete process.env.JENKINS_URL;
38
+ expect(getCiRunId()).toBeNull();
39
+ });
40
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const reporter_1 = require("../../src/reporter");
4
+ // Mock the module that imports Playwright-specific types.
5
+ // This prevents a type-checking conflict within the Jest environment.
6
+ jest.mock('../../src/reporter-types', () => ({}));
7
+ describe('TestivAIPlaywrightReporter', () => {
8
+ const OLD_ENV = process.env;
9
+ beforeEach(() => {
10
+ jest.resetModules();
11
+ process.env = { ...OLD_ENV };
12
+ });
13
+ afterAll(() => {
14
+ process.env = OLD_ENV;
15
+ });
16
+ test('should initialize with API URL and key from environment variables', () => {
17
+ process.env.TESTIVAI_API_URL = 'http://env.api';
18
+ process.env.TESTIVAI_API_KEY = 'env-key';
19
+ const reporter = new reporter_1.TestivAIPlaywrightReporter();
20
+ // Accessing private options for testing purposes
21
+ expect(reporter.options.apiUrl).toBe('http://env.api');
22
+ expect(reporter.options.apiKey).toBe('env-key');
23
+ });
24
+ test('should be disabled if API URL is not provided', async () => {
25
+ const reporter = new reporter_1.TestivAIPlaywrightReporter();
26
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
27
+ await reporter.onBegin({}, {});
28
+ expect(reporter.options.apiUrl).toBeUndefined();
29
+ });
30
+ test('should be disabled if API Key is not provided', async () => {
31
+ process.env.TESTIVAI_API_URL = 'http://env.api';
32
+ const reporter = new reporter_1.TestivAIPlaywrightReporter();
33
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
34
+ await reporter.onBegin({}, {});
35
+ expect(reporter.options.apiUrl).toBeUndefined();
36
+ });
37
+ });
@@ -0,0 +1,43 @@
1
+ import { TestivAIPlaywrightReporter } from '../../src/reporter';
2
+
3
+ // Mock the module that imports Playwright-specific types.
4
+ // This prevents a type-checking conflict within the Jest environment.
5
+ jest.mock('../../src/reporter-types', () => ({}));
6
+
7
+ describe('TestivAIPlaywrightReporter', () => {
8
+ const OLD_ENV = process.env;
9
+
10
+ beforeEach(() => {
11
+ jest.resetModules();
12
+ process.env = { ...OLD_ENV };
13
+ });
14
+
15
+ afterAll(() => {
16
+ process.env = OLD_ENV;
17
+ });
18
+
19
+ test('should initialize with API URL and key from environment variables', () => {
20
+ process.env.TESTIVAI_API_URL = 'http://env.api';
21
+ process.env.TESTIVAI_API_KEY = 'env-key';
22
+
23
+ const reporter = new TestivAIPlaywrightReporter();
24
+ // Accessing private options for testing purposes
25
+ expect((reporter as any).options.apiUrl).toBe('http://env.api');
26
+ expect((reporter as any).options.apiKey).toBe('env-key');
27
+ });
28
+
29
+ test('should be disabled if API URL is not provided', async () => {
30
+ const reporter = new TestivAIPlaywrightReporter();
31
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
32
+ await reporter.onBegin({} as any, {} as any);
33
+ expect((reporter as any).options.apiUrl).toBeUndefined();
34
+ });
35
+
36
+ test('should be disabled if API Key is not provided', async () => {
37
+ process.env.TESTIVAI_API_URL = 'http://env.api';
38
+ const reporter = new TestivAIPlaywrightReporter();
39
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
40
+ await reporter.onBegin({} as any, {} as any);
41
+ expect((reporter as any).options.apiUrl).toBeUndefined();
42
+ });
43
+ });
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @jest-environment node
3
+ *
4
+ * Tests for the renamed Structure Analyzer module.
5
+ * @renamed Was `domAnalyzer.spec.js` — renamed to match the new public API (IP protection)
6
+ *
7
+ * Terminology mapping:
8
+ * analyzeDOM() → analyzeStructure()
9
+ * compareDOMAnalysis() → compareStructureAnalysis()
10
+ * DOMAnalysis → StructureAnalysis
11
+ * DOMChange → StructureChange
12
+ * DOMAnalysisConfig → StructureAnalysisConfig
13
+ */
14
+
15
+ describe('Structure Analyzer', () => {
16
+ // Mock the page.evaluate method for testing
17
+ const mockPage = {
18
+ evaluate: jest.fn(),
19
+ };
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ test('analyzeStructure() should generate structure fingerprint and analysis', async () => {
26
+ // Mock the page.evaluate to return structure analysis
27
+ mockPage.evaluate.mockResolvedValue({
28
+ fingerprint: 'dGVzdCBmaW5nZXJwcmludA==', // base64 encoded
29
+ structure: {
30
+ totalElements: 10,
31
+ elementTypes: { html: 1, body: 1, div: 3, p: 2, button: 1 },
32
+ maxDepth: 3,
33
+ interactiveElements: {
34
+ buttons: 1,
35
+ inputs: 0,
36
+ links: 0,
37
+ forms: 0,
38
+ },
39
+ },
40
+ semantic: {
41
+ headings: { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 },
42
+ landmarks: {
43
+ hasHeader: false,
44
+ hasNav: false,
45
+ hasMain: false,
46
+ hasFooter: false,
47
+ hasAside: false,
48
+ },
49
+ lists: { ordered: 0, unordered: 0 },
50
+ tables: 0,
51
+ images: 0,
52
+ },
53
+ });
54
+
55
+ const { analyzeStructure } = require('../../src/structureAnalyzer');
56
+ const result = await analyzeStructure(mockPage, {
57
+ enableFingerprint: true,
58
+ enableStructure: true,
59
+ enableSemantic: true,
60
+ });
61
+
62
+ expect(result.fingerprint).toBe('dGVzdCBmaW5nZXJwcmludA==');
63
+ expect(result.structure.totalElements).toBe(10);
64
+ expect(result.semantic.headings.h1).toBe(0);
65
+ });
66
+
67
+ test('compareStructureAnalysis() should detect changes between baseline and current', () => {
68
+ const { compareStructureAnalysis } = require('../../src/structureAnalyzer');
69
+
70
+ const baseline = {
71
+ fingerprint: 'abc123',
72
+ structure: {
73
+ totalElements: 10,
74
+ elementTypes: { div: 3, p: 2 },
75
+ },
76
+ semantic: {
77
+ headings: { h1: 1, h2: 2 },
78
+ landmarks: { hasHeader: true, hasNav: false },
79
+ },
80
+ };
81
+
82
+ const current = {
83
+ fingerprint: 'def456',
84
+ structure: {
85
+ totalElements: 12,
86
+ elementTypes: { div: 3, p: 2, span: 2 },
87
+ },
88
+ semantic: {
89
+ headings: { h1: 1, h2: 2 },
90
+ landmarks: { hasHeader: true, hasNav: true },
91
+ },
92
+ };
93
+
94
+ const changes = compareStructureAnalysis(baseline, current);
95
+
96
+ expect(changes.length).toBeGreaterThanOrEqual(3);
97
+ expect(changes[0].type).toBe('fingerprint');
98
+ expect(changes[0].severity).toBe('high');
99
+ expect(changes.some(c => c.type === 'structure')).toBe(true);
100
+ expect(changes.some(c => c.type === 'semantic')).toBe(true);
101
+ });
102
+
103
+ test('analyzeStructure() should handle empty configuration', async () => {
104
+ mockPage.evaluate.mockResolvedValue({
105
+ fingerprint: 'base64hash',
106
+ });
107
+
108
+ const { analyzeStructure } = require('../../src/structureAnalyzer');
109
+ const result = await analyzeStructure(mockPage);
110
+
111
+ expect(result.fingerprint).toBe('base64hash');
112
+ expect(mockPage.evaluate).toHaveBeenCalled();
113
+ });
114
+
115
+ test('compareStructureAnalysis() should return empty array when analyses are identical', () => {
116
+ const { compareStructureAnalysis } = require('../../src/structureAnalyzer');
117
+
118
+ const analysis = {
119
+ fingerprint: 'same_hash',
120
+ structure: {
121
+ totalElements: 5,
122
+ elementTypes: { div: 3, p: 2 },
123
+ },
124
+ semantic: {
125
+ headings: { h1: 1 },
126
+ landmarks: { hasHeader: true, hasNav: false },
127
+ },
128
+ };
129
+
130
+ const changes = compareStructureAnalysis(analysis, analysis);
131
+ expect(changes.length).toBe(0);
132
+ });
133
+
134
+ test('compareStructureAnalysis() should detect fingerprint-only changes', () => {
135
+ const { compareStructureAnalysis } = require('../../src/structureAnalyzer');
136
+
137
+ const baseline = { fingerprint: 'hash_a' };
138
+ const current = { fingerprint: 'hash_b' };
139
+
140
+ const changes = compareStructureAnalysis(baseline, current);
141
+ expect(changes.length).toBe(1);
142
+ expect(changes[0].type).toBe('fingerprint');
143
+ expect(changes[0].severity).toBe('high');
144
+ expect(changes[0].description).toBe('Page structure has changed');
145
+ });
146
+
147
+ test('compareStructureAnalysis() should detect removed element types', () => {
148
+ const { compareStructureAnalysis } = require('../../src/structureAnalyzer');
149
+
150
+ const baseline = {
151
+ structure: {
152
+ totalElements: 10,
153
+ elementTypes: { div: 3, p: 2, span: 1 },
154
+ },
155
+ };
156
+ const current = {
157
+ structure: {
158
+ totalElements: 9,
159
+ elementTypes: { div: 3, p: 2 },
160
+ },
161
+ };
162
+
163
+ const changes = compareStructureAnalysis(baseline, current);
164
+ expect(changes.some(c => c.description.includes('Removed element types: span'))).toBe(true);
165
+ });
166
+
167
+ test('compareStructureAnalysis() should detect added element types', () => {
168
+ const { compareStructureAnalysis } = require('../../src/structureAnalyzer');
169
+
170
+ const baseline = {
171
+ structure: {
172
+ totalElements: 5,
173
+ elementTypes: { div: 3 },
174
+ },
175
+ };
176
+ const current = {
177
+ structure: {
178
+ totalElements: 7,
179
+ elementTypes: { div: 3, button: 2 },
180
+ },
181
+ };
182
+
183
+ const changes = compareStructureAnalysis(baseline, current);
184
+ expect(changes.some(c => c.description.includes('Added element types: button'))).toBe(true);
185
+ });
186
+
187
+ test('compareStructureAnalysis() should detect landmark changes', () => {
188
+ const { compareStructureAnalysis } = require('../../src/structureAnalyzer');
189
+
190
+ const baseline = {
191
+ semantic: {
192
+ headings: {},
193
+ landmarks: { hasHeader: false, hasNav: false },
194
+ },
195
+ };
196
+ const current = {
197
+ semantic: {
198
+ headings: {},
199
+ landmarks: { hasHeader: true, hasNav: false },
200
+ },
201
+ };
202
+
203
+ const changes = compareStructureAnalysis(baseline, current);
204
+ expect(changes.some(c => c.type === 'semantic' && c.description.includes('hasHeader'))).toBe(true);
205
+ });
206
+
207
+ test('structureAnalyzer exports are available (old domAnalyzer deleted)', () => {
208
+ const mod = require('../../src/structureAnalyzer');
209
+ expect(typeof mod.analyzeStructure).toBe('function');
210
+ expect(typeof mod.compareStructureAnalysis).toBe('function');
211
+ });
212
+ });