@vizzly-testing/cli 0.20.0 → 0.20.1-beta.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.
Files changed (84) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +178 -3
  10. package/dist/client/index.js +144 -77
  11. package/dist/commands/doctor.js +121 -36
  12. package/dist/commands/finalize.js +49 -18
  13. package/dist/commands/init.js +13 -18
  14. package/dist/commands/login.js +49 -55
  15. package/dist/commands/logout.js +17 -9
  16. package/dist/commands/project.js +100 -71
  17. package/dist/commands/run.js +189 -95
  18. package/dist/commands/status.js +101 -66
  19. package/dist/commands/tdd-daemon.js +61 -32
  20. package/dist/commands/tdd.js +104 -98
  21. package/dist/commands/upload.js +78 -34
  22. package/dist/commands/whoami.js +44 -42
  23. package/dist/config/core.js +438 -0
  24. package/dist/config/index.js +13 -0
  25. package/dist/config/operations.js +327 -0
  26. package/dist/index.js +1 -1
  27. package/dist/project/core.js +295 -0
  28. package/dist/project/index.js +13 -0
  29. package/dist/project/operations.js +393 -0
  30. package/dist/reporter/reporter-bundle.css +1 -1
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +191 -53
  38. package/dist/server/http-server.js +9 -3
  39. package/dist/server/routers/baseline.js +58 -0
  40. package/dist/server/routers/dashboard.js +10 -6
  41. package/dist/server/routers/screenshot.js +32 -0
  42. package/dist/server-manager/core.js +186 -0
  43. package/dist/server-manager/index.js +81 -0
  44. package/dist/server-manager/operations.js +209 -0
  45. package/dist/services/build-manager.js +2 -69
  46. package/dist/services/index.js +21 -48
  47. package/dist/services/screenshot-server.js +40 -74
  48. package/dist/services/server-manager.js +45 -80
  49. package/dist/services/test-runner.js +90 -250
  50. package/dist/services/uploader.js +56 -358
  51. package/dist/tdd/core/hotspot-coverage.js +112 -0
  52. package/dist/tdd/core/signature.js +101 -0
  53. package/dist/tdd/index.js +19 -0
  54. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  55. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  56. package/dist/tdd/services/baseline-downloader.js +151 -0
  57. package/dist/tdd/services/baseline-manager.js +166 -0
  58. package/dist/tdd/services/comparison-service.js +230 -0
  59. package/dist/tdd/services/hotspot-service.js +71 -0
  60. package/dist/tdd/services/result-service.js +123 -0
  61. package/dist/tdd/tdd-service.js +1145 -0
  62. package/dist/test-runner/core.js +255 -0
  63. package/dist/test-runner/index.js +13 -0
  64. package/dist/test-runner/operations.js +483 -0
  65. package/dist/types/client.d.ts +25 -2
  66. package/dist/uploader/core.js +396 -0
  67. package/dist/uploader/index.js +11 -0
  68. package/dist/uploader/operations.js +412 -0
  69. package/dist/utils/colors.js +187 -39
  70. package/dist/utils/config-loader.js +3 -6
  71. package/dist/utils/context.js +228 -0
  72. package/dist/utils/output.js +449 -14
  73. package/docs/api-reference.md +173 -8
  74. package/docs/tui-elements.md +560 -0
  75. package/package.json +13 -13
  76. package/dist/services/api-service.js +0 -412
  77. package/dist/services/auth-service.js +0 -226
  78. package/dist/services/config-service.js +0 -369
  79. package/dist/services/html-report-generator.js +0 -455
  80. package/dist/services/project-service.js +0 -326
  81. package/dist/services/report-generator/report.css +0 -411
  82. package/dist/services/report-generator/viewer.js +0 -102
  83. package/dist/services/static-report-generator.js +0 -207
  84. package/dist/services/tdd-service.js +0 -1437
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Baseline Metadata I/O
3
+ *
4
+ * Functions for reading and writing baseline metadata.json files.
5
+ * These handle the local storage of baseline information.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ /**
12
+ * Load baseline metadata from disk
13
+ *
14
+ * @param {string} baselinePath - Path to baselines directory
15
+ * @returns {Object|null} Baseline metadata or null if not found
16
+ */
17
+ export function loadBaselineMetadata(baselinePath) {
18
+ let metadataPath = join(baselinePath, 'metadata.json');
19
+ if (!existsSync(metadataPath)) {
20
+ return null;
21
+ }
22
+ try {
23
+ let content = readFileSync(metadataPath, 'utf8');
24
+ return JSON.parse(content);
25
+ } catch (error) {
26
+ // Log for debugging but return null - caller can handle missing metadata
27
+ console.debug?.(`Failed to parse baseline metadata: ${error.message}`);
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Save baseline metadata to disk
34
+ *
35
+ * @param {string} baselinePath - Path to baselines directory
36
+ * @param {Object} metadata - Metadata object to save
37
+ */
38
+ export function saveBaselineMetadata(baselinePath, metadata) {
39
+ // Ensure directory exists
40
+ if (!existsSync(baselinePath)) {
41
+ mkdirSync(baselinePath, {
42
+ recursive: true
43
+ });
44
+ }
45
+ let metadataPath = join(baselinePath, 'metadata.json');
46
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
47
+ }
48
+
49
+ /**
50
+ * Create empty baseline metadata structure
51
+ *
52
+ * @param {Object} options - Options for the baseline
53
+ * @param {number} options.threshold - Comparison threshold
54
+ * @param {string[]} options.signatureProperties - Custom signature properties
55
+ * @returns {Object} Empty baseline metadata
56
+ */
57
+ export function createEmptyBaselineMetadata(options = {}) {
58
+ return {
59
+ buildId: 'local-baseline',
60
+ buildName: 'Local TDD Baseline',
61
+ environment: 'test',
62
+ branch: 'local',
63
+ threshold: options.threshold ?? 2.0,
64
+ signatureProperties: options.signatureProperties ?? [],
65
+ createdAt: new Date().toISOString(),
66
+ screenshots: []
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Update or add a screenshot entry in the metadata
72
+ *
73
+ * @param {Object} metadata - Baseline metadata object (mutated)
74
+ * @param {Object} screenshotEntry - Screenshot entry to upsert
75
+ * @param {string} signature - Signature to match for updates
76
+ * @returns {Object} The updated metadata (same reference)
77
+ */
78
+ export function upsertScreenshotInMetadata(metadata, screenshotEntry, signature) {
79
+ if (!metadata.screenshots) {
80
+ metadata.screenshots = [];
81
+ }
82
+ let existingIndex = metadata.screenshots.findIndex(s => s.signature === signature);
83
+ if (existingIndex >= 0) {
84
+ metadata.screenshots[existingIndex] = screenshotEntry;
85
+ } else {
86
+ metadata.screenshots.push(screenshotEntry);
87
+ }
88
+ return metadata;
89
+ }
90
+
91
+ /**
92
+ * Find a screenshot in metadata by signature
93
+ *
94
+ * @param {Object} metadata - Baseline metadata object
95
+ * @param {string} signature - Signature to find
96
+ * @returns {Object|null} Screenshot entry or null if not found
97
+ */
98
+ export function findScreenshotBySignature(metadata, signature) {
99
+ if (!metadata?.screenshots) {
100
+ return null;
101
+ }
102
+ return metadata.screenshots.find(s => s.signature === signature) || null;
103
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Hotspot Metadata I/O
3
+ *
4
+ * Functions for reading and writing hotspot data files.
5
+ * Hotspots identify regions of screenshots that frequently change
6
+ * due to dynamic content (timestamps, animations, etc.).
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ /**
13
+ * Load hotspot data from disk
14
+ *
15
+ * @param {string} workingDir - Working directory containing .vizzly folder
16
+ * @returns {Object|null} Hotspot data keyed by screenshot name, or null if not found
17
+ */
18
+ export function loadHotspotMetadata(workingDir) {
19
+ let hotspotsPath = join(workingDir, '.vizzly', 'hotspots.json');
20
+ if (!existsSync(hotspotsPath)) {
21
+ return null;
22
+ }
23
+ try {
24
+ let content = readFileSync(hotspotsPath, 'utf8');
25
+ let data = JSON.parse(content);
26
+ return data.hotspots || null;
27
+ } catch {
28
+ // Return null for parse/read errors
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Save hotspot data to disk
35
+ *
36
+ * @param {string} workingDir - Working directory containing .vizzly folder
37
+ * @param {Object} hotspotData - Hotspot data keyed by screenshot name
38
+ * @param {Object} summary - Summary information about the hotspots
39
+ */
40
+ export function saveHotspotMetadata(workingDir, hotspotData, summary = {}) {
41
+ let vizzlyDir = join(workingDir, '.vizzly');
42
+
43
+ // Ensure directory exists
44
+ if (!existsSync(vizzlyDir)) {
45
+ mkdirSync(vizzlyDir, {
46
+ recursive: true
47
+ });
48
+ }
49
+ let hotspotsPath = join(vizzlyDir, 'hotspots.json');
50
+ let content = {
51
+ downloadedAt: new Date().toISOString(),
52
+ summary,
53
+ hotspots: hotspotData
54
+ };
55
+ writeFileSync(hotspotsPath, JSON.stringify(content, null, 2));
56
+ }
57
+
58
+ /**
59
+ * Get hotspot for a specific screenshot with caching support
60
+ *
61
+ * This is a pure function that takes a cache object as parameter
62
+ * for stateless operation. The cache is mutated if data needs to be loaded.
63
+ *
64
+ * @param {Object} cache - Cache object { data: Object|null, loaded: boolean }
65
+ * @param {string} workingDir - Working directory
66
+ * @param {string} screenshotName - Name of the screenshot
67
+ * @returns {Object|null} Hotspot analysis or null if not available
68
+ */
69
+ export function getHotspotForScreenshot(cache, workingDir, screenshotName) {
70
+ // Check cache first
71
+ if (cache.data?.[screenshotName]) {
72
+ return cache.data[screenshotName];
73
+ }
74
+
75
+ // Load from disk if not yet loaded
76
+ if (!cache.loaded) {
77
+ cache.data = loadHotspotMetadata(workingDir);
78
+ cache.loaded = true;
79
+ }
80
+ return cache.data?.[screenshotName] || null;
81
+ }
82
+
83
+ /**
84
+ * Create an empty hotspot cache object
85
+ *
86
+ * @returns {{ data: null, loaded: boolean }}
87
+ */
88
+ export function createHotspotCache() {
89
+ return {
90
+ data: null,
91
+ loaded: false
92
+ };
93
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Baseline Downloader
3
+ *
4
+ * Functions for downloading baseline images from the cloud API.
5
+ * These are lower-level utilities - orchestration happens in TddService.
6
+ */
7
+
8
+ import { existsSync, writeFileSync } from 'node:fs';
9
+ import { fetchWithTimeout } from '../../utils/fetch-utils.js';
10
+
11
+ /**
12
+ * Download a single baseline image
13
+ *
14
+ * @param {string} url - URL to download from
15
+ * @param {string} destPath - Destination file path
16
+ * @param {Object} options - Options
17
+ * @param {number} options.timeout - Request timeout in ms (default: 30000)
18
+ * @returns {Promise<{ success: boolean, error?: string }>}
19
+ */
20
+ export async function downloadBaselineImage(url, destPath, options = {}) {
21
+ let {
22
+ timeout = 30000
23
+ } = options;
24
+ try {
25
+ let response = await fetchWithTimeout(url, {
26
+ timeout
27
+ });
28
+ if (!response.ok) {
29
+ return {
30
+ success: false,
31
+ error: `HTTP ${response.status}: ${response.statusText}`
32
+ };
33
+ }
34
+ let buffer = Buffer.from(await response.arrayBuffer());
35
+ writeFileSync(destPath, buffer);
36
+ return {
37
+ success: true
38
+ };
39
+ } catch (error) {
40
+ return {
41
+ success: false,
42
+ error: error.message
43
+ };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if a baseline already exists with matching SHA
49
+ *
50
+ * @param {string} filePath - Path to the baseline file
51
+ * @param {string} expectedSha - Expected SHA256 hash
52
+ * @param {Map<string, string>} shaMap - Map of filename -> sha256
53
+ * @returns {boolean}
54
+ */
55
+ export function baselineMatchesSha(filePath, expectedSha, shaMap) {
56
+ if (!existsSync(filePath) || !expectedSha) {
57
+ return false;
58
+ }
59
+ let filename = filePath.split('/').pop();
60
+ let storedSha = shaMap.get(filename);
61
+ return storedSha === expectedSha;
62
+ }
63
+
64
+ /**
65
+ * Download multiple baselines in batches
66
+ *
67
+ * @param {Array} screenshots - Screenshots to download
68
+ * @param {Object} options - Options
69
+ * @param {string} options.baselinePath - Path to baselines directory
70
+ * @param {Map<string, string>} options.existingShaMap - Existing SHA map for skip logic
71
+ * @param {number} options.batchSize - Concurrent downloads (default: 5)
72
+ * @param {Function} options.onProgress - Progress callback (downloaded, skipped, errors, total)
73
+ * @param {Function} options.getFilePath - Function to get file path for a screenshot
74
+ * @returns {Promise<{ downloaded: number, skipped: number, errors: number }>}
75
+ */
76
+ export async function downloadBaselinesInBatches(screenshots, options = {}) {
77
+ let {
78
+ existingShaMap = new Map(),
79
+ batchSize = 5,
80
+ onProgress,
81
+ getFilePath
82
+ } = options;
83
+ let downloaded = 0;
84
+ let skipped = 0;
85
+ let errors = 0;
86
+ let total = screenshots.length;
87
+
88
+ // Process in batches
89
+ for (let i = 0; i < screenshots.length; i += batchSize) {
90
+ let batch = screenshots.slice(i, i + batchSize);
91
+ let batchPromises = batch.map(async screenshot => {
92
+ let filePath = getFilePath(screenshot);
93
+ let url = screenshot.original_url;
94
+ if (!url) {
95
+ errors++;
96
+ return;
97
+ }
98
+
99
+ // Skip if SHA matches
100
+ if (baselineMatchesSha(filePath, screenshot.sha256, existingShaMap)) {
101
+ skipped++;
102
+ downloaded++;
103
+ return;
104
+ }
105
+ let result = await downloadBaselineImage(url, filePath);
106
+ if (result.success) {
107
+ downloaded++;
108
+ } else {
109
+ errors++;
110
+ }
111
+ });
112
+ await Promise.all(batchPromises);
113
+ if (onProgress) {
114
+ onProgress(downloaded, skipped, errors, total);
115
+ }
116
+ }
117
+ return {
118
+ downloaded,
119
+ skipped,
120
+ errors
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Build baseline metadata entry for a downloaded screenshot
126
+ *
127
+ * @param {Object} screenshot - Screenshot data from API
128
+ * @param {string} filename - Local filename
129
+ * @param {string} filePath - Full file path
130
+ * @param {Object} buildInfo - Build information
131
+ * @returns {Object} Metadata entry
132
+ */
133
+ export function buildBaselineMetadataEntry(screenshot, filename, filePath, buildInfo = {}) {
134
+ return {
135
+ name: screenshot.name,
136
+ originalName: screenshot.name,
137
+ sha256: screenshot.sha256,
138
+ id: screenshot.id,
139
+ filename,
140
+ path: filePath,
141
+ browser: screenshot.browser || screenshot.metadata?.browser,
142
+ viewport_width: screenshot.viewport_width || screenshot.metadata?.viewport?.width || screenshot.properties?.viewport?.width,
143
+ originalUrl: screenshot.original_url,
144
+ fileSize: screenshot.file_size,
145
+ dimensions: screenshot.dimensions,
146
+ // Build info for tracking
147
+ buildId: buildInfo.buildId,
148
+ commitSha: buildInfo.commitSha,
149
+ approvalStatus: screenshot.approval_status
150
+ };
151
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Baseline Manager
3
+ *
4
+ * Local baseline CRUD operations - manages the file system aspects
5
+ * of baseline storage without any network operations.
6
+ */
7
+
8
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ /**
12
+ * Initialize TDD directory structure
13
+ *
14
+ * @param {string} workingDir - Working directory
15
+ * @returns {{ baselinePath: string, currentPath: string, diffPath: string }}
16
+ */
17
+ export function initializeDirectories(workingDir) {
18
+ let vizzlyDir = join(workingDir, '.vizzly');
19
+ let baselinePath = join(vizzlyDir, 'baselines');
20
+ let currentPath = join(vizzlyDir, 'current');
21
+ let diffPath = join(vizzlyDir, 'diffs');
22
+ for (let dir of [baselinePath, currentPath, diffPath]) {
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, {
25
+ recursive: true
26
+ });
27
+ }
28
+ }
29
+ return {
30
+ baselinePath,
31
+ currentPath,
32
+ diffPath
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Clear all baseline data for fresh download
38
+ *
39
+ * @param {{ baselinePath: string, currentPath: string, diffPath: string }} paths
40
+ */
41
+ export function clearBaselineData(paths) {
42
+ let {
43
+ baselinePath,
44
+ currentPath,
45
+ diffPath
46
+ } = paths;
47
+ for (let dir of [baselinePath, currentPath, diffPath]) {
48
+ if (existsSync(dir)) {
49
+ rmSync(dir, {
50
+ recursive: true,
51
+ force: true
52
+ });
53
+ mkdirSync(dir, {
54
+ recursive: true
55
+ });
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Save an image as baseline
62
+ *
63
+ * @param {string} baselinePath - Path to baselines directory
64
+ * @param {string} filename - Filename for the baseline
65
+ * @param {Buffer} imageBuffer - Image data
66
+ */
67
+ export function saveBaseline(baselinePath, filename, imageBuffer) {
68
+ let filePath = join(baselinePath, filename);
69
+ writeFileSync(filePath, imageBuffer);
70
+ }
71
+
72
+ /**
73
+ * Save current screenshot
74
+ *
75
+ * @param {string} currentPath - Path to current screenshots directory
76
+ * @param {string} filename - Filename for the screenshot
77
+ * @param {Buffer} imageBuffer - Image data
78
+ * @returns {string} Full path to saved file
79
+ */
80
+ export function saveCurrent(currentPath, filename, imageBuffer) {
81
+ let filePath = join(currentPath, filename);
82
+ writeFileSync(filePath, imageBuffer);
83
+ return filePath;
84
+ }
85
+
86
+ /**
87
+ * Check if baseline exists for a filename
88
+ *
89
+ * @param {string} baselinePath - Path to baselines directory
90
+ * @param {string} filename - Filename to check
91
+ * @returns {boolean}
92
+ */
93
+ export function baselineExists(baselinePath, filename) {
94
+ return existsSync(join(baselinePath, filename));
95
+ }
96
+
97
+ /**
98
+ * Get full path to a baseline file
99
+ *
100
+ * @param {string} baselinePath - Path to baselines directory
101
+ * @param {string} filename - Filename
102
+ * @returns {string}
103
+ */
104
+ export function getBaselinePath(baselinePath, filename) {
105
+ return join(baselinePath, filename);
106
+ }
107
+
108
+ /**
109
+ * Get full path to a current file
110
+ *
111
+ * @param {string} currentPath - Path to current screenshots directory
112
+ * @param {string} filename - Filename
113
+ * @returns {string}
114
+ */
115
+ export function getCurrentPath(currentPath, filename) {
116
+ return join(currentPath, filename);
117
+ }
118
+
119
+ /**
120
+ * Get full path to a diff file
121
+ *
122
+ * @param {string} diffPath - Path to diffs directory
123
+ * @param {string} filename - Filename
124
+ * @returns {string}
125
+ */
126
+ export function getDiffPath(diffPath, filename) {
127
+ return join(diffPath, filename);
128
+ }
129
+
130
+ /**
131
+ * Promote current screenshot to baseline (accept as new baseline)
132
+ *
133
+ * @param {string} currentPath - Path to current screenshots directory
134
+ * @param {string} baselinePath - Path to baselines directory
135
+ * @param {string} filename - Filename
136
+ */
137
+ export function promoteCurrentToBaseline(currentPath, baselinePath, filename) {
138
+ let currentFile = join(currentPath, filename);
139
+ let baselineFile = join(baselinePath, filename);
140
+ if (!existsSync(currentFile)) {
141
+ throw new Error(`Current screenshot not found: ${currentFile}`);
142
+ }
143
+ copyFileSync(currentFile, baselineFile);
144
+ }
145
+
146
+ /**
147
+ * Read baseline image
148
+ *
149
+ * @param {string} baselinePath - Path to baselines directory
150
+ * @param {string} filename - Filename
151
+ * @returns {Buffer}
152
+ */
153
+ export function readBaseline(baselinePath, filename) {
154
+ return readFileSync(join(baselinePath, filename));
155
+ }
156
+
157
+ /**
158
+ * Read current screenshot
159
+ *
160
+ * @param {string} currentPath - Path to current screenshots directory
161
+ * @param {string} filename - Filename
162
+ * @returns {Buffer}
163
+ */
164
+ export function readCurrent(currentPath, filename) {
165
+ return readFileSync(join(currentPath, filename));
166
+ }