@vizzly-testing/cli 0.39.0 → 0.39.2

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 (195) hide show
  1. package/README.md +12 -3
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +50 -35
  4. package/dist/adapters/cli/setup.js +34 -0
  5. package/dist/{client → adapters/client}/index.js +6 -7
  6. package/dist/adapters/http/cloud-proxy.js +29 -0
  7. package/dist/{server → adapters/http}/http-server.js +12 -13
  8. package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
  9. package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
  10. package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
  11. package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
  12. package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
  13. package/dist/adapters/sdk/index.d.ts +89 -0
  14. package/dist/{sdk → adapters/sdk}/index.js +22 -19
  15. package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
  16. package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
  17. package/dist/domains/auth/auth-errors.js +50 -0
  18. package/dist/domains/auth/auth-headers.js +41 -0
  19. package/dist/domains/auth/auth.normalizers.js +50 -0
  20. package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
  21. package/dist/domains/auth/auth.rules.js +19 -0
  22. package/dist/domains/auth/auth.serializers.js +32 -0
  23. package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
  24. package/dist/domains/auth/auth.validations.js +21 -0
  25. package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
  26. package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
  27. package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
  28. package/dist/domains/auth/token-store.js +81 -0
  29. package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
  30. package/dist/domains/baselines/baseline-download.js +192 -0
  31. package/dist/domains/baselines/baseline-write.js +22 -0
  32. package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
  33. package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
  34. package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
  35. package/dist/domains/baselines/cloud-baselines.js +9 -0
  36. package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
  37. package/dist/domains/builds/build.normalizers.js +35 -0
  38. package/dist/domains/builds/build.serializers.js +164 -0
  39. package/dist/domains/builds/build.service.js +119 -0
  40. package/dist/domains/builds/build.validations.js +39 -0
  41. package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
  42. package/dist/domains/builds/cloud-builds.routes.js +31 -0
  43. package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
  44. package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
  45. package/dist/domains/comparisons/cloud-comparisons.js +43 -0
  46. package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
  47. package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
  48. package/dist/domains/config/config-defaults.js +27 -0
  49. package/dist/{utils → domains/config}/config-discovery.js +1 -9
  50. package/dist/{utils → domains/config}/config-loader.js +10 -9
  51. package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
  52. package/dist/{types → domains/config}/config.d.ts +3 -3
  53. package/dist/domains/config/config.normalizers.js +126 -0
  54. package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
  55. package/dist/domains/config/config.rules.js +47 -0
  56. package/dist/domains/config/config.serializers.js +202 -0
  57. package/dist/domains/config/config.service.js +625 -0
  58. package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
  59. package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
  60. package/dist/domains/context/compact-context.validations.js +60 -0
  61. package/dist/domains/context/context-presentation.js +368 -0
  62. package/dist/domains/context/context.commands.js +385 -0
  63. package/dist/domains/context/context.serializers.js +157 -0
  64. package/dist/domains/context/context.service.js +76 -0
  65. package/dist/domains/context/context.validations.js +100 -0
  66. package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
  67. package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
  68. package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
  69. package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
  70. package/dist/domains/previews/preview.service.js +37 -0
  71. package/dist/domains/projects/cloud-projects.routes.js +28 -0
  72. package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
  73. package/dist/{utils → domains/projects}/project-binding.js +3 -3
  74. package/dist/domains/projects/project-errors.js +61 -0
  75. package/dist/{utils → domains/projects}/project-link-store.js +2 -2
  76. package/dist/domains/projects/project-requests.js +61 -0
  77. package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
  78. package/dist/domains/projects/project.normalizers.js +69 -0
  79. package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
  80. package/dist/domains/projects/project.validations.js +31 -0
  81. package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
  82. package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
  83. package/dist/domains/reports/dashboard.routes.js +96 -0
  84. package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
  85. package/dist/domains/reports/report-assets.routes.js +81 -0
  86. package/dist/domains/reports/report-state.js +51 -0
  87. package/dist/domains/reports/report-store.js +85 -0
  88. package/dist/domains/reports/report.serializers.js +99 -0
  89. package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
  90. package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
  91. package/dist/domains/runs/run-environment.js +69 -0
  92. package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
  93. package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
  94. package/dist/domains/runs/run.normalizers.js +8 -0
  95. package/dist/domains/runs/run.rules.js +56 -0
  96. package/dist/domains/runs/run.serializers.js +101 -0
  97. package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
  98. package/dist/domains/runs/run.validations.js +17 -0
  99. package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
  100. package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
  101. package/dist/domains/screenshots/cloud-screenshots.js +109 -0
  102. package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
  103. package/dist/domains/screenshots/screenshot-hash.js +53 -0
  104. package/dist/domains/screenshots/screenshot-server-options.js +28 -0
  105. package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
  106. package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
  107. package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
  108. package/dist/domains/screenshots/screenshot.serializers.js +146 -0
  109. package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
  110. package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
  112. package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
  113. package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
  114. package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
  115. package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
  116. package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
  117. package/dist/domains/updates/cli-update.js +103 -0
  118. package/dist/domains/updates/update-check.js +2 -0
  119. package/dist/domains/updates/update-notifications.js +175 -0
  120. package/dist/domains/uploads/upload-defaults.js +4 -0
  121. package/dist/domains/uploads/upload-files.js +32 -0
  122. package/dist/domains/uploads/upload-settings.js +21 -0
  123. package/dist/domains/uploads/upload-timing.js +18 -0
  124. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  125. package/dist/domains/uploads/upload.normalizers.js +60 -0
  126. package/dist/domains/uploads/upload.serializers.js +179 -0
  127. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  128. package/dist/domains/uploads/upload.validations.js +77 -0
  129. package/dist/index.js +12 -12
  130. package/dist/reporter/reporter-bundle.iife.js +86 -86
  131. package/dist/reporter-ssr/ssr-entry.js +3 -6
  132. package/dist/shared/cloud-protocol.js +171 -0
  133. package/dist/{utils → shared}/file-changes.js +1 -1
  134. package/dist/shared/files.js +9 -0
  135. package/dist/{utils → shared}/global-config.js +7 -78
  136. package/dist/shared/log-level.js +7 -0
  137. package/dist/{utils → shared}/output.js +1 -1
  138. package/dist/{utils → shared}/package-info.js +5 -4
  139. package/dist/shared/paths.js +57 -0
  140. package/package.json +37 -36
  141. package/dist/api/core.js +0 -382
  142. package/dist/api/endpoints.js +0 -421
  143. package/dist/api/index.js +0 -19
  144. package/dist/auth/core.js +0 -237
  145. package/dist/auth/index.js +0 -31
  146. package/dist/commands/context.js +0 -1066
  147. package/dist/config/core.js +0 -439
  148. package/dist/config/operations.js +0 -327
  149. package/dist/project/core.js +0 -211
  150. package/dist/screenshot-server/core.js +0 -161
  151. package/dist/screenshot-server/index.js +0 -11
  152. package/dist/server/routers/assets.js +0 -95
  153. package/dist/server/routers/cloud-proxy.js +0 -146
  154. package/dist/server/routers/dashboard.js +0 -172
  155. package/dist/services/auth-service.js +0 -117
  156. package/dist/services/config-service.js +0 -313
  157. package/dist/services/project-service.js +0 -88
  158. package/dist/test-runner/core.js +0 -293
  159. package/dist/test-runner/index.js +0 -13
  160. package/dist/types/sdk.d.ts +0 -186
  161. package/dist/uploader/core.js +0 -432
  162. package/dist/uploader/index.js +0 -88
  163. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  164. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  166. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  167. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  168. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  169. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  170. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  171. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  172. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  173. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  174. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  175. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  176. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  177. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  178. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  179. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  180. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  181. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  182. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  183. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  184. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  185. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  186. /package/dist/{utils → shared}/api-url.js +0 -0
  187. /package/dist/{utils → shared}/async-utils.js +0 -0
  188. /package/dist/{utils → shared}/browser.js +0 -0
  189. /package/dist/{utils → shared}/ci-env.js +0 -0
  190. /package/dist/{utils → shared}/colors.js +0 -0
  191. /package/dist/{utils → shared}/fetch-utils.js +0 -0
  192. /package/dist/{utils → shared}/git.js +0 -0
  193. /package/dist/{utils → shared}/patterns.js +0 -0
  194. /package/dist/{utils → shared}/secret-store.js +0 -0
  195. /package/dist/{errors → shared}/vizzly-error.js +0 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Convert absolute file paths to web-accessible URLs
3
+ */
4
+ export let convertPathToUrl = (filePath, vizzlyDir) => {
5
+ if (!filePath) return null;
6
+ // Convert absolute path to relative path from .vizzly directory
7
+ if (filePath.startsWith(vizzlyDir)) {
8
+ let relativePath = filePath.substring(vizzlyDir.length + 1);
9
+ return `/images/${relativePath}`;
10
+ }
11
+ return filePath;
12
+ };
13
+
14
+ /**
15
+ * Group comparisons by screenshot name with variant structure
16
+ * Matches cloud product's grouping logic from comparison.js
17
+ */
18
+ export let groupComparisons = comparisons => {
19
+ let groups = new Map();
20
+
21
+ // Group by screenshot name
22
+ for (let comp of comparisons) {
23
+ if (!groups.has(comp.name)) {
24
+ groups.set(comp.name, {
25
+ name: comp.name,
26
+ comparisons: [],
27
+ browsers: new Set(),
28
+ viewports: new Set(),
29
+ devices: new Set(),
30
+ totalVariants: 0
31
+ });
32
+ }
33
+ let group = groups.get(comp.name);
34
+ group.comparisons.push(comp);
35
+ group.totalVariants++;
36
+
37
+ // Track unique browsers, viewports, devices
38
+ if (comp.properties?.browser) {
39
+ group.browsers.add(comp.properties.browser);
40
+ }
41
+ if (comp.properties?.viewport_width && comp.properties?.viewport_height) {
42
+ group.viewports.add(`${comp.properties.viewport_width}x${comp.properties.viewport_height}`);
43
+ }
44
+ if (comp.properties?.device) {
45
+ group.devices.add(comp.properties.device);
46
+ }
47
+ }
48
+
49
+ // Convert to final structure
50
+ return Array.from(groups.values()).map(group => {
51
+ let browsers = Array.from(group.browsers);
52
+ let viewports = Array.from(group.viewports);
53
+ let devices = Array.from(group.devices);
54
+
55
+ // Build variants structure (browser -> viewport -> comparisons)
56
+ let variants = {};
57
+ group.comparisons.forEach(comp => {
58
+ let browser = comp.properties?.browser || null;
59
+ let viewport = comp.properties?.viewport_width && comp.properties?.viewport_height ? `${comp.properties.viewport_width}x${comp.properties.viewport_height}` : null;
60
+ if (!variants[browser]) variants[browser] = {};
61
+ if (!variants[browser][viewport]) variants[browser][viewport] = [];
62
+ variants[browser][viewport].push(comp);
63
+ });
64
+
65
+ // Determine grouping strategy
66
+ let groupingStrategy = 'flat';
67
+ if (browsers.length > 1) groupingStrategy = 'browser';else if (viewports.length > 1) groupingStrategy = 'viewport';
68
+
69
+ // Sort comparisons by viewport area (largest first)
70
+ group.comparisons.sort((a, b) => {
71
+ let aArea = (a.properties?.viewport_width || 0) * (a.properties?.viewport_height || 0);
72
+ let bArea = (b.properties?.viewport_width || 0) * (b.properties?.viewport_height || 0);
73
+ if (bArea !== aArea) return bArea - aArea;
74
+ return (b.properties?.viewport_width || 0) - (a.properties?.viewport_width || 0);
75
+ });
76
+ return {
77
+ ...group,
78
+ browsers,
79
+ viewports,
80
+ devices: Array.from(devices),
81
+ variants,
82
+ groupingStrategy
83
+ };
84
+ }).sort((a, b) => {
85
+ // Sort groups: multi-variant first (by variant count), then singles alphabetically
86
+ if (a.totalVariants > 1 && b.totalVariants === 1) return -1;
87
+ if (a.totalVariants === 1 && b.totalVariants > 1) return 1;
88
+ if (a.totalVariants > 1 && b.totalVariants > 1) {
89
+ return b.totalVariants - a.totalVariants;
90
+ }
91
+ return a.name.localeCompare(b.name);
92
+ });
93
+ };
94
+ let COMPARISON_DETAIL_FIELDS = new Set(['boundingBox', 'diffClusters', 'intensityStats']);
95
+
96
+ /** Heavy detail files may enrich a comparison, never replace its identity/state. */
97
+ export function presentComparisonDetails(details = {}) {
98
+ return Object.fromEntries(Object.entries(details).filter(([field]) => COMPARISON_DETAIL_FIELDS.has(field)));
99
+ }
@@ -6,22 +6,16 @@
6
6
  */
7
7
 
8
8
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
9
- import { dirname, join } from 'node:path';
10
- import { fileURLToPath, pathToFileURL } from 'node:url';
11
- let __dirname = dirname(fileURLToPath(import.meta.url));
9
+ import { join } from 'node:path';
10
+ import { pathToFileURL } from 'node:url';
11
+ import { getPackageRoot } from '../../shared/package-info.js';
12
+ import { createReportStore } from './report-store.js';
12
13
 
13
14
  /**
14
15
  * Get the path to the dist/reporter directory (for CSS)
15
16
  */
16
17
  function getReporterDistPath() {
17
- // Try production path first (when running from dist/)
18
- let distPath = join(__dirname, '..', 'reporter');
19
- if (existsSync(join(distPath, 'reporter-bundle.css'))) {
20
- return distPath;
21
- }
22
-
23
- // Fall back to development path (when running from src/)
24
- distPath = join(__dirname, '..', '..', 'dist', 'reporter');
18
+ let distPath = join(getPackageRoot(), 'dist', 'reporter');
25
19
  if (existsSync(join(distPath, 'reporter-bundle.css'))) {
26
20
  return distPath;
27
21
  }
@@ -32,14 +26,7 @@ function getReporterDistPath() {
32
26
  * Get the path to the SSR module
33
27
  */
34
28
  function getSSRModulePath() {
35
- // Try production path first (when running from dist/)
36
- let ssrPath = join(__dirname, '..', 'reporter-ssr', 'ssr-entry.js');
37
- if (existsSync(ssrPath)) {
38
- return ssrPath;
39
- }
40
-
41
- // Fall back to development path (when running from src/)
42
- ssrPath = join(__dirname, '..', '..', 'dist', 'reporter-ssr', 'ssr-entry.js');
29
+ let ssrPath = join(getPackageRoot(), 'dist', 'reporter-ssr', 'ssr-entry.js');
43
30
  if (existsSync(ssrPath)) {
44
31
  return ssrPath;
45
32
  }
@@ -139,26 +126,17 @@ export async function generateStaticReport(workingDir, options = {}) {
139
126
  let outputDir = options.outputDir || join(workingDir, '.vizzly', 'report');
140
127
  let vizzlyDir = join(workingDir, '.vizzly');
141
128
  try {
142
- // Read report data
143
- let reportDataPath = join(vizzlyDir, 'report-data.json');
144
- if (!existsSync(reportDataPath)) {
129
+ let reports = createReportStore(workingDir);
130
+ let reportData = reports.readReport();
131
+ if (!reportData) {
145
132
  return {
146
133
  success: false,
147
134
  reportPath: null,
148
135
  error: 'No report data found. Run tests first.'
149
136
  };
150
137
  }
151
- let reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
152
-
153
- // Read baseline metadata if available
154
- let metadataPath = join(vizzlyDir, 'baselines', 'metadata.json');
155
- if (existsSync(metadataPath)) {
156
- try {
157
- reportData.baseline = JSON.parse(readFileSync(metadataPath, 'utf8'));
158
- } catch {
159
- // Ignore metadata read errors
160
- }
161
- }
138
+ let baseline = reports.readBaselineMetadata();
139
+ if (baseline !== null) reportData.baseline = baseline;
162
140
 
163
141
  // Transform image URLs to relative paths
164
142
  let transformedData = transformImageUrls(reportData);
@@ -3,10 +3,10 @@
3
3
  */
4
4
 
5
5
  import { randomUUID as defaultRandomUUID } from 'node:crypto';
6
- import { createApiClient as defaultCreateApiClient } from '../api/index.js';
7
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
8
- import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
9
- import * as defaultOutput from '../utils/output.js';
6
+ import * as defaultOutput from '../../shared/output.js';
7
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
8
+ import { getAccessToken as defaultGetAccessToken } from '../auth/token-store.js';
9
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
10
10
  function createReviewDeps(deps = {}) {
11
11
  return {
12
12
  loadConfig: deps.loadConfig || defaultLoadConfig,
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Build environment variables for test process execution
3
+ * @param {Object} options - Options
4
+ * @param {number} options.port - Server port
5
+ * @param {string} options.buildId - Build ID
6
+ * @param {boolean} [options.setBaseline] - Whether to set baseline
7
+ * @param {boolean} [options.failOnDiff] - Whether visual diffs should fail tests
8
+ * @param {Object} [options.baseEnv] - Base environment (defaults to process.env)
9
+ * @returns {Object} Environment variables object
10
+ */
11
+ export function buildTestEnv({
12
+ port,
13
+ buildId,
14
+ setBaseline = false,
15
+ failOnDiff = false,
16
+ baseEnv = process.env
17
+ }) {
18
+ return {
19
+ ...baseEnv,
20
+ VIZZLY_SERVER_URL: `http://localhost:${port}`,
21
+ VIZZLY_BUILD_ID: buildId,
22
+ VIZZLY_ENABLED: 'true',
23
+ VIZZLY_SET_BASELINE: setBaseline ? 'true' : 'false',
24
+ VIZZLY_FAIL_ON_DIFF: failOnDiff ? 'true' : 'false'
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Build environment for disabled Vizzly (allowNoToken mode)
30
+ * @param {Object} [baseEnv] - Base environment (defaults to process.env)
31
+ * @returns {Object} Environment variables object
32
+ */
33
+ export function buildDisabledEnv(baseEnv = process.env) {
34
+ return {
35
+ ...baseEnv,
36
+ VIZZLY_ENABLED: 'false'
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Build API client options from config
42
+ * @param {Object} config - Configuration object
43
+ * @returns {Object|null} Client options or null if no API key
44
+ */
45
+ export function buildClientOptions(config) {
46
+ if (!config?.apiKey) {
47
+ return null;
48
+ }
49
+ return {
50
+ baseUrl: config.apiUrl,
51
+ token: config.apiKey,
52
+ command: 'run'
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Build spawn options for test command execution
58
+ * @param {Object} env - Environment variables
59
+ * @param {Object} [options] - Spawn display options
60
+ * @param {boolean} [options.json] - Keep stdout reserved for CLI JSON output
61
+ * @returns {Object} Spawn options
62
+ */
63
+ export function buildSpawnOptions(env, options = {}) {
64
+ return {
65
+ env,
66
+ stdio: options.json ? ['inherit', 'pipe', 'pipe'] : 'inherit',
67
+ shell: true
68
+ };
69
+ }
@@ -1,19 +1,21 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+ import * as output from '../../shared/output.js';
4
+ import { VizzlyError } from '../../shared/vizzly-error.js';
5
+ import { createApiClient } from '../auth/cloud-client.js';
6
+ import { createBuild as createApiBuild, finalizeBuild as finalizeApiBuild, getBuild } from '../builds/build.service.js';
7
+ import { createBuildObject } from '../builds/build-info.js';
8
+ import { cancelTests, createBuild, fetchBuildUrl, finalizeBuild, initializeDaemon, runTests } from './run.service.js';
9
+ import { writeSession as defaultWriteSession } from './session-store.js';
10
+
1
11
  /**
2
12
  * Test Runner Service
3
13
  * Orchestrates the test execution flow
4
14
  *
5
15
  * This EventEmitter adapter keeps the stable plugin-facing runner contract
6
- * while delegating execution to the functional operations in src/test-runner/.
16
+ * while delegating execution to the domain's run.service.js operations.
7
17
  */
8
18
 
9
- import { spawn } from 'node:child_process';
10
- import { EventEmitter } from 'node:events';
11
- import { createBuild as createApiBuild, createApiClient, finalizeBuild as finalizeApiBuild, getBuild } from '../api/index.js';
12
- import { VizzlyError } from '../errors/vizzly-error.js';
13
- import { cancelTests, createBuild, fetchBuildUrl, finalizeBuild, initializeDaemon, runTests } from '../test-runner/index.js';
14
- import * as output from '../utils/output.js';
15
- import { writeSession as defaultWriteSession } from '../utils/session.js';
16
- import { createBuildObject } from './build-manager.js';
17
19
  export class TestRunner extends EventEmitter {
18
20
  constructor(config, serverManager, options = {}) {
19
21
  super();
@@ -1,3 +1,19 @@
1
+ import { spawn as defaultSpawn } from 'node:child_process';
2
+ import { createServerManager as defaultCreateServerManager } from '../../adapters/http/server-manager.js';
3
+ import { getAppBaseUrl } from '../../shared/api-url.js';
4
+ import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitAuthor as defaultDetectCommitAuthor, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../../shared/git.js';
5
+ import * as defaultOutput from '../../shared/output.js';
6
+ import { VizzlyError } from '../../shared/vizzly-error.js';
7
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
8
+ import { createBuild as defaultCreateApiBuild, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild } from '../builds/build.service.js';
9
+ import { createBuildObject as defaultCreateBuildObject } from '../builds/build-info.js';
10
+ import { CONFIG_DEFAULTS } from '../config/config-defaults.js';
11
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
12
+ import { getTokenContext as defaultGetTokenContext } from '../context/context.service.js';
13
+ import { createUploader as defaultCreateUploader } from '../uploads/upload.service.js';
14
+ import { finalizeBuild as defaultFinalizeBuild, runTests as defaultRunTests } from './run.service.js';
15
+ import { writeSession as defaultWriteSession } from './session-store.js';
16
+
1
17
  /**
2
18
  * Run tests and report their visual results in terminal or JSON output.
3
19
  *
@@ -8,19 +24,6 @@
8
24
  * Uses functional operations directly - no class wrappers needed
9
25
  */
10
26
 
11
- import { spawn as defaultSpawn } from 'node:child_process';
12
- import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
13
- import { CONFIG_DEFAULTS } from '../config/core.js';
14
- import { VizzlyError } from '../errors/vizzly-error.js';
15
- import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
16
- import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
17
- import { finalizeBuild as defaultFinalizeBuild, runTests as defaultRunTests } from '../test-runner/index.js';
18
- import { createUploader as defaultCreateUploader } from '../uploader/index.js';
19
- import { getAppBaseUrl } from '../utils/api-url.js';
20
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
21
- import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitAuthor as defaultDetectCommitAuthor, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
22
- import * as defaultOutput from '../utils/output.js';
23
- import { writeSession as defaultWriteSession } from '../utils/session.js';
24
27
  function formatUploadCounts({
25
28
  total,
26
29
  uploaded,
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Normalize setBaseline option (handles both camelCase and kebab-case)
3
+ * @param {Object} options - Options object
4
+ * @returns {boolean} Whether to set baseline
5
+ */
6
+ export function normalizeSetBaseline(options) {
7
+ return Boolean(options?.setBaseline || options?.['set-baseline']);
8
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Determine if we should skip Vizzly integration entirely
3
+ * @param {Object} options - Options
4
+ * @param {boolean} options.allowNoToken - Whether to allow running without token
5
+ * @param {boolean} options.hasApiKey - Whether an API key is available
6
+ * @param {boolean} options.tdd - Whether in TDD mode
7
+ * @returns {boolean} True if Vizzly should be disabled
8
+ */
9
+ export function shouldDisableVizzly({
10
+ allowNoToken,
11
+ hasApiKey,
12
+ tdd
13
+ }) {
14
+ return allowNoToken && !hasApiKey && !tdd;
15
+ }
16
+
17
+ /**
18
+ * Determine build mode
19
+ * @param {boolean} tdd - Whether TDD mode is enabled
20
+ * @returns {'tdd'|'api'} Build mode
21
+ */
22
+ export function determineBuildMode(tdd) {
23
+ return tdd ? 'tdd' : 'api';
24
+ }
25
+
26
+ /**
27
+ * Validate daemon mode requirements
28
+ * @param {Object} options - Options
29
+ * @param {boolean} options.tdd - Whether TDD mode is enabled
30
+ * @param {boolean} options.daemon - Whether daemon mode is enabled
31
+ * @returns {{ valid: boolean, error: string|null }}
32
+ */
33
+ export function validateDaemonMode({
34
+ tdd,
35
+ daemon
36
+ }) {
37
+ if (!tdd || !daemon) {
38
+ return {
39
+ valid: false,
40
+ error: 'Initialize method is only for TDD daemon mode'
41
+ };
42
+ }
43
+ return {
44
+ valid: true,
45
+ error: null
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Check if API key is available
51
+ * @param {Object} config - Configuration object
52
+ * @returns {boolean} Whether API key exists
53
+ */
54
+ export function hasApiKey(config) {
55
+ return Boolean(config?.apiKey);
56
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Build API build payload from options
3
+ * @param {Object} options - Run options
4
+ * @param {Object} [comparisonConfig] - Comparison configuration (threshold, minClusterSize)
5
+ * @returns {Object} Build payload for API
6
+ */
7
+ export function buildApiBuildPayload(options, comparisonConfig = null) {
8
+ let payload = {
9
+ name: options.buildName || `Test Run ${new Date().toISOString()}`,
10
+ branch: options.branch || 'main',
11
+ environment: options.environment || 'test',
12
+ commit_sha: options.commit,
13
+ commit_message: options.message,
14
+ commit_author_name: options.commitAuthorName,
15
+ commit_author_email: options.commitAuthorEmail,
16
+ github_pull_request_number: options.pullRequestNumber,
17
+ parallel_id: options.parallelId
18
+ };
19
+
20
+ // Only include metadata if we have meaningful config to send
21
+ if (comparisonConfig?.threshold != null || comparisonConfig?.minClusterSize != null) {
22
+ payload.metadata = {
23
+ comparison: {
24
+ threshold: comparisonConfig.threshold,
25
+ minClusterSize: comparisonConfig.minClusterSize
26
+ }
27
+ };
28
+ }
29
+ return payload;
30
+ }
31
+
32
+ /**
33
+ * Build result object for disabled run (no Vizzly integration)
34
+ * @returns {Object} Result object
35
+ */
36
+ export function buildDisabledRunResult(reason = null) {
37
+ return {
38
+ testsPassed: 1,
39
+ testsFailed: 0,
40
+ screenshotsCaptured: 0,
41
+ ...(reason ? {
42
+ capture: {
43
+ setup: 'disabled',
44
+ reason,
45
+ accepted: 0,
46
+ rejected: 0,
47
+ errors: 0
48
+ }
49
+ } : {})
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Build final run result object
55
+ * @param {Object} options - Options
56
+ * @param {string|null} options.buildId - Build ID
57
+ * @param {string|null} options.buildUrl - Build URL
58
+ * @param {boolean} options.testSuccess - Whether tests passed
59
+ * @param {number} options.screenshotCount - Number of screenshots
60
+ * @param {Object|null} options.tddResults - TDD results (comparisons, etc.)
61
+ * @param {Object|null} [options.uploadStats] - Cumulative cloud upload outcomes,
62
+ * exposed as `uploads` independently of test and local comparison results.
63
+ * @returns {Object} Final result object
64
+ */
65
+ export function buildRunResult({
66
+ buildId,
67
+ buildUrl,
68
+ testSuccess,
69
+ screenshotCount,
70
+ tddResults,
71
+ uploadStats
72
+ }) {
73
+ return {
74
+ buildId,
75
+ url: buildUrl,
76
+ testsPassed: testSuccess ? 1 : 0,
77
+ testsFailed: testSuccess ? 0 : 1,
78
+ screenshotsCaptured: screenshotCount,
79
+ ...(uploadStats ? {
80
+ uploads: uploadStats
81
+ } : {}),
82
+ comparisons: tddResults?.comparisons || null,
83
+ failed: (tddResults?.failed || 0) > 0,
84
+ ...(tddResults ? {
85
+ summary: {
86
+ total: tddResults.total ?? screenshotCount,
87
+ passed: tddResults.passed || 0,
88
+ failed: tddResults.failed || 0,
89
+ new: tddResults.new || 0,
90
+ errors: tddResults.errors || 0
91
+ },
92
+ capture: {
93
+ setup: 'ready',
94
+ reason: tddResults.error || null,
95
+ accepted: tddResults.capture?.accepted ?? screenshotCount,
96
+ rejected: tddResults.capture?.rejected || 0,
97
+ errors: tddResults.errors || 0
98
+ }
99
+ } : {})
100
+ };
101
+ }
@@ -1,5 +1,11 @@
1
+ import { normalizeSetBaseline } from './run.normalizers.js';
2
+ import { hasApiKey, shouldDisableVizzly, validateDaemonMode } from './run.rules.js';
3
+ import { buildApiBuildPayload, buildDisabledRunResult, buildRunResult } from './run.serializers.js';
4
+ import { validateTestCommand } from './run.validations.js';
5
+ import { buildClientOptions, buildDisabledEnv, buildSpawnOptions, buildTestEnv } from './run-environment.js';
6
+
1
7
  /**
2
- * Test Runner Operations - Test execution operations with dependency injection
8
+ * Test execution and build lifecycle services
3
9
  *
4
10
  * Each operation takes its dependencies as parameters:
5
11
  * - spawn: child_process.spawn for executing commands
@@ -11,8 +17,6 @@
11
17
  * This makes them trivially testable without mocking modules.
12
18
  */
13
19
 
14
- import { buildApiBuildPayload, buildClientOptions, buildDisabledEnv, buildDisabledRunResult, buildRunResult, buildSpawnOptions, buildTestEnv, hasApiKey, normalizeSetBaseline, shouldDisableVizzly, validateDaemonMode, validateTestCommand } from './core.js';
15
-
16
20
  // ============================================================================
17
21
  // Build Operations
18
22
  // ============================================================================
@@ -111,9 +115,10 @@ export async function fetchBuildUrl({
111
115
  * @param {boolean} options.tdd - Whether in TDD mode
112
116
  * @param {boolean} options.success - Whether tests passed
113
117
  * @param {number} options.executionTime - Execution time in ms
118
+ * @param {string} [options.failureReason] - Explanation for a failed build
114
119
  * @param {Object} options.config - Configuration object
115
120
  * @param {Object} options.deps - Dependencies
116
- * @param {Object} options.deps.serverManager - Server manager
121
+ * @param {Object} [options.deps.serverManager] - Server manager when uploads exist
117
122
  * @param {Function} options.deps.createApiClient - API client factory
118
123
  * @param {Function} options.deps.finalizeApiBuild - API finalize function
119
124
  * @param {Object} options.deps.output - Output utilities
@@ -126,6 +131,7 @@ export async function finalizeBuild({
126
131
  tdd,
127
132
  success,
128
133
  executionTime,
134
+ failureReason,
129
135
  config,
130
136
  deps
131
137
  }) {
@@ -143,7 +149,7 @@ export async function finalizeBuild({
143
149
  try {
144
150
  if (tdd) {
145
151
  // TDD mode: use server handler to finalize (local-only)
146
- if (serverManager.server?.finishBuild) {
152
+ if (serverManager?.server?.finishBuild) {
147
153
  await serverManager.server.finishBuild(buildId);
148
154
  output.debug('build', 'finalized', {
149
155
  success
@@ -151,7 +157,7 @@ export async function finalizeBuild({
151
157
  }
152
158
  } else {
153
159
  // API mode: flush uploads first, then finalize build
154
- if (serverManager.server?.finishBuild) {
160
+ if (serverManager?.server?.finishBuild) {
155
161
  uploadStats = await serverManager.server.finishBuild(buildId);
156
162
  }
157
163
 
@@ -161,7 +167,7 @@ export async function finalizeBuild({
161
167
  let client = createApiClient(clientOptions);
162
168
  let uploadsFailed = uploadStats?.failed > 0;
163
169
  let buildSucceeded = success && !uploadsFailed;
164
- await finalizeApiBuild(client, buildId, buildSucceeded, executionTime, uploadsFailed ? `${uploadStats.failed} screenshot(s) failed to upload. Visual testing is incomplete.` : undefined);
170
+ await finalizeApiBuild(client, buildId, buildSucceeded, executionTime, uploadsFailed ? `${uploadStats.failed} screenshot(s) failed to upload. Visual testing is incomplete.` : failureReason);
165
171
  output.debug('build', 'finalized via api', {
166
172
  success: buildSucceeded
167
173
  });
@@ -395,6 +401,28 @@ export async function runTests({
395
401
  error: stopError.message
396
402
  });
397
403
  }
404
+ if (buildId && !tdd) {
405
+ try {
406
+ await finalizeBuild({
407
+ buildId,
408
+ tdd: false,
409
+ success: false,
410
+ executionTime: Date.now() - startTime,
411
+ failureReason: error.message,
412
+ config,
413
+ deps: {
414
+ createApiClient,
415
+ finalizeApiBuild,
416
+ output,
417
+ onFinalizeFailed
418
+ }
419
+ });
420
+ } catch (finalizeError) {
421
+ output.debug('run', 'failed to finalize build after setup error', {
422
+ error: finalizeError.message
423
+ });
424
+ }
425
+ }
398
426
  return executeDisabledTestRun({
399
427
  testCommand,
400
428
  json: runOptions.json,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Validate test command is provided
3
+ * @param {string|undefined} testCommand - Test command to validate
4
+ * @returns {{ valid: boolean, error: string|null }}
5
+ */
6
+ export function validateTestCommand(testCommand) {
7
+ if (!testCommand) {
8
+ return {
9
+ valid: false,
10
+ error: 'No test command provided'
11
+ };
12
+ }
13
+ return {
14
+ valid: true,
15
+ error: null
16
+ };
17
+ }
@@ -1,3 +1,6 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+
1
4
  /**
2
5
  * Session management for build context
3
6
  *
@@ -9,8 +12,6 @@
9
12
  * 2. GitHub Actions env ($GITHUB_ENV) - for cross-step persistence in GHA
10
13
  */
11
14
 
12
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
- import { dirname, join } from 'node:path';
14
15
  let SESSION_DIR = '.vizzly';
15
16
  let SESSION_FILE = 'session.json';
16
17
  let SESSION_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
@@ -1,10 +1,10 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { uploadScreenshot as defaultUploadScreenshot } from '../../api/index.js';
5
- import { detectImageInputType } from '../../utils/image-input-detector.js';
6
- import * as output from '../../utils/output.js';
7
- import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
4
+ import * as output from '../../shared/output.js';
5
+ import { uploadScreenshot as defaultUploadScreenshot } from './cloud-screenshots.js';
6
+ import { detectImageInputType } from './image-input.js';
7
+ import { normalizeScreenshotOptions } from './screenshot-options.js';
8
8
 
9
9
  /**
10
10
  * Create a cloud screenshot handler for one run.