@vizzly-testing/cli 0.20.1-beta.0 → 0.20.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 (38) hide show
  1. package/README.md +16 -18
  2. package/dist/cli.js +177 -2
  3. package/dist/client/index.js +144 -77
  4. package/dist/commands/doctor.js +118 -33
  5. package/dist/commands/finalize.js +8 -3
  6. package/dist/commands/init.js +13 -18
  7. package/dist/commands/login.js +42 -49
  8. package/dist/commands/logout.js +13 -5
  9. package/dist/commands/project.js +95 -67
  10. package/dist/commands/run.js +32 -6
  11. package/dist/commands/status.js +81 -50
  12. package/dist/commands/tdd-daemon.js +61 -32
  13. package/dist/commands/tdd.js +14 -26
  14. package/dist/commands/upload.js +18 -9
  15. package/dist/commands/whoami.js +40 -38
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +204 -22
  18. package/dist/server/handlers/tdd-handler.js +113 -7
  19. package/dist/server/http-server.js +9 -3
  20. package/dist/server/routers/baseline.js +58 -0
  21. package/dist/server/routers/dashboard.js +10 -6
  22. package/dist/server/routers/screenshot.js +32 -0
  23. package/dist/server-manager/core.js +5 -2
  24. package/dist/server-manager/operations.js +2 -1
  25. package/dist/services/config-service.js +306 -0
  26. package/dist/tdd/tdd-service.js +190 -126
  27. package/dist/types/client.d.ts +25 -2
  28. package/dist/utils/colors.js +187 -39
  29. package/dist/utils/config-loader.js +3 -6
  30. package/dist/utils/context.js +228 -0
  31. package/dist/utils/output.js +449 -14
  32. package/docs/api-reference.md +173 -8
  33. package/docs/tui-elements.md +560 -0
  34. package/package.json +13 -7
  35. package/dist/report-generator/core.js +0 -315
  36. package/dist/report-generator/index.js +0 -8
  37. package/dist/report-generator/operations.js +0 -196
  38. package/dist/services/static-report-generator.js +0 -65
@@ -11,7 +11,6 @@
11
11
  import { existsSync as defaultExistsSync, mkdirSync as defaultMkdirSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
12
12
  import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
13
13
  import { NetworkError } from '../errors/vizzly-error.js';
14
- import { StaticReportGenerator as DefaultStaticReportGenerator } from '../services/static-report-generator.js';
15
14
  import { colors as defaultColors } from '../utils/colors.js';
16
15
  import { fetchWithTimeout as defaultFetchWithTimeout } from '../utils/fetch-utils.js';
17
16
  import { getDefaultBranch as defaultGetDefaultBranch } from '../utils/git.js';
@@ -62,8 +61,7 @@ export class TddService {
62
61
  // Result building
63
62
  results = {},
64
63
  // Other
65
- calculateHotspotCoverage = defaultCalculateHotspotCoverage,
66
- StaticReportGenerator = DefaultStaticReportGenerator
64
+ calculateHotspotCoverage = defaultCalculateHotspotCoverage
67
65
  } = deps;
68
66
 
69
67
  // Merge grouped deps with defaults
@@ -135,7 +133,6 @@ export class TddService {
135
133
  validatePathSecurity,
136
134
  initializeDirectories,
137
135
  calculateHotspotCoverage,
138
- StaticReportGenerator,
139
136
  ...fsOps,
140
137
  ...apiOps,
141
138
  ...metadataOps,
@@ -177,8 +174,11 @@ export class TddService {
177
174
 
178
175
  // Hotspot data (loaded lazily from disk or downloaded from cloud)
179
176
  this.hotspotData = null;
177
+
178
+ // Track whether results have been printed (to avoid duplicate output)
179
+ this._resultsPrinted = false;
180
180
  if (this.setBaseline) {
181
- output.info('🐻 Baseline update mode - will overwrite existing baselines with new ones');
181
+ output.info('[vizzly] Baseline update mode - will overwrite existing baselines with new ones');
182
182
  }
183
183
  }
184
184
 
@@ -191,7 +191,7 @@ export class TddService {
191
191
  branch = await getDefaultBranch();
192
192
  if (!branch) {
193
193
  branch = 'main';
194
- output.warn(`⚠️ Could not detect default branch, using 'main' as fallback`);
194
+ output.warn(`Could not detect default branch, using 'main' as fallback`);
195
195
  } else {
196
196
  output.debug('tdd', `detected default branch: ${branch}`);
197
197
  }
@@ -221,10 +221,10 @@ export class TddService {
221
221
  }
222
222
  baselineBuild = apiResponse.build;
223
223
  if (baselineBuild.status === 'failed') {
224
- output.warn(`⚠️ Build ${buildId} is marked as FAILED - falling back to local baselines`);
224
+ output.warn(`Build ${buildId} is marked as FAILED - falling back to local baselines`);
225
225
  return await this.handleLocalBaselines();
226
226
  } else if (baselineBuild.status !== 'completed') {
227
- output.warn(`⚠️ Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
227
+ output.warn(`Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
228
228
  }
229
229
  baselineBuild.screenshots = apiResponse.screenshots;
230
230
  } else if (comparisonId) {
@@ -284,8 +284,8 @@ export class TddService {
284
284
  limit: 1
285
285
  });
286
286
  if (!builds.data || builds.data.length === 0) {
287
- output.warn(`⚠️ No baseline builds found for ${environment}/${branch}`);
288
- output.info('💡 Run a build in normal mode first to create baselines');
287
+ output.warn(`No baseline builds found for ${environment}/${branch}`);
288
+ output.info('Tip: Run a build in normal mode first to create baselines');
289
289
  return null;
290
290
  }
291
291
  let apiResponse = await getTddBaselines(this.client, builds.data[0].id);
@@ -303,7 +303,7 @@ export class TddService {
303
303
  }
304
304
  let buildDetails = baselineBuild;
305
305
  if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
306
- output.warn('⚠️ No screenshots found in baseline build');
306
+ output.warn('No screenshots found in baseline build');
307
307
  return null;
308
308
  }
309
309
  output.info(`Using baseline from build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
@@ -337,7 +337,7 @@ export class TddService {
337
337
  }
338
338
  let filename = screenshot.filename;
339
339
  if (!filename) {
340
- output.warn(`⚠️ Screenshot ${sanitizedName} has no filename from API - skipping`);
340
+ output.warn(`Screenshot ${sanitizedName} has no filename from API - skipping`);
341
341
  errorCount++;
342
342
  continue;
343
343
  }
@@ -354,7 +354,7 @@ export class TddService {
354
354
  }
355
355
  let downloadUrl = screenshot.original_url || screenshot.url;
356
356
  if (!downloadUrl) {
357
- output.warn(`⚠️ Screenshot ${sanitizedName} has no download URL - skipping`);
357
+ output.warn(`Screenshot ${sanitizedName} has no download URL - skipping`);
358
358
  errorCount++;
359
359
  continue;
360
360
  }
@@ -393,7 +393,7 @@ export class TddService {
393
393
  name: sanitizedName
394
394
  };
395
395
  } catch (error) {
396
- output.warn(`⚠️ Failed to download ${sanitizedName}: ${error.message}`);
396
+ output.warn(`Failed to download ${sanitizedName}: ${error.message}`);
397
397
  return {
398
398
  success: false,
399
399
  name: sanitizedName,
@@ -409,7 +409,7 @@ export class TddService {
409
409
  }
410
410
  }
411
411
  if (downloadedCount === 0 && skippedCount === 0) {
412
- output.error('No screenshots were successfully downloaded');
412
+ output.error('No screenshots were successfully downloaded');
413
413
  return null;
414
414
  }
415
415
 
@@ -468,19 +468,19 @@ export class TddService {
468
468
  let actualDownloads = downloadedCount - skippedCount;
469
469
  if (skippedCount > 0) {
470
470
  if (actualDownloads === 0) {
471
- output.info(`✅ All ${skippedCount} baselines up-to-date`);
471
+ output.info(`All ${skippedCount} baselines up-to-date`);
472
472
  } else {
473
- output.info(`✅ Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
473
+ output.info(`Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
474
474
  }
475
475
  } else {
476
- output.info(`✅ Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
476
+ output.info(`Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
477
477
  }
478
478
  if (errorCount > 0) {
479
- output.warn(`⚠️ ${errorCount} screenshots failed to download`);
479
+ output.warn(`${errorCount} screenshots failed to download`);
480
480
  }
481
481
  return this.baselineData;
482
482
  } catch (error) {
483
- output.error(`❌ Failed to download baseline: ${error.message}`);
483
+ output.error(`Failed to download baseline: ${error.message}`);
484
484
  throw error;
485
485
  }
486
486
  }
@@ -512,10 +512,10 @@ export class TddService {
512
512
  saveHotspotMetadata(this.workingDir, response.hotspots, response.summary);
513
513
  let hotspotCount = Object.keys(response.hotspots).length;
514
514
  let totalRegions = Object.values(response.hotspots).reduce((sum, h) => sum + (h.regions?.length || 0), 0);
515
- output.info(`✅ Downloaded hotspot data for ${hotspotCount} screenshots (${totalRegions} regions total)`);
515
+ output.info(`Downloaded hotspot data for ${hotspotCount} screenshots (${totalRegions} regions total)`);
516
516
  } catch (error) {
517
517
  output.debug('tdd', `Hotspot download failed: ${error.message}`);
518
- output.warn('⚠️ Could not fetch hotspot data - comparisons will run without noise filtering');
518
+ output.warn('Could not fetch hotspot data - comparisons will run without noise filtering');
519
519
  }
520
520
  }
521
521
 
@@ -578,11 +578,11 @@ export class TddService {
578
578
  output.info('📥 No local baseline found, but API key available');
579
579
  output.info('🆕 Current run will create new local baselines');
580
580
  } else {
581
- output.info('📝 No local baseline found - all screenshots will be marked as new');
581
+ output.info('No local baseline found - all screenshots will be marked as new');
582
582
  }
583
583
  return null;
584
584
  } else {
585
- output.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
585
+ output.info(`Using existing baseline: ${colors.cyan(baseline.buildName)}`);
586
586
  return baseline;
587
587
  }
588
588
  }
@@ -637,8 +637,7 @@ export class TddService {
637
637
  buildPassedComparison,
638
638
  buildFailedComparison,
639
639
  buildErrorComparison,
640
- isDimensionMismatchError,
641
- colors
640
+ isDimensionMismatchError
642
641
  } = this._deps;
643
642
 
644
643
  // Sanitize and validate
@@ -749,26 +748,20 @@ export class TddService {
749
748
  hotspotAnalysis
750
749
  });
751
750
 
752
- // Log result
753
- let diffInfo = ` (${honeydiffResult.diffPercentage.toFixed(2)}% different, ${honeydiffResult.diffPixels} pixels)`;
751
+ // Log at debug level only (shown with --verbose)
752
+ let diffInfo = `${honeydiffResult.diffPercentage.toFixed(2)}% diff, ${honeydiffResult.diffPixels} pixels`;
754
753
  if (honeydiffResult.diffClusters?.length > 0) {
755
- diffInfo += `, ${honeydiffResult.diffClusters.length} region${honeydiffResult.diffClusters.length > 1 ? 's' : ''}`;
756
- }
757
- if (result.hotspotAnalysis?.coverage > 0) {
758
- diffInfo += `, ${Math.round(result.hotspotAnalysis.coverage * 100)}% in hotspots`;
759
- }
760
- if (result.status === 'passed') {
761
- output.info(`✅ ${colors.green('PASSED')} ${sanitizedName} - differences in known hotspots${diffInfo}`);
762
- } else {
763
- output.warn(`❌ ${colors.red('FAILED')} ${sanitizedName} - differences detected${diffInfo}`);
764
- output.info(` Diff saved to: ${diffImagePath}`);
754
+ diffInfo += `, ${honeydiffResult.diffClusters.length} regions`;
765
755
  }
756
+ output.debug('comparison', `${sanitizedName}: ${result.status}`, {
757
+ diff: diffInfo
758
+ });
766
759
  this.comparisons.push(result);
767
760
  return result;
768
761
  }
769
762
  } catch (error) {
770
763
  if (isDimensionMismatchError(error)) {
771
- output.warn(`⚠️ Dimension mismatch for ${sanitizedName} - creating new baseline`);
764
+ output.debug('comparison', `${sanitizedName}: dimension mismatch, creating new baseline`);
772
765
  saveBaseline(this.baselinePath, filename, imageBuffer);
773
766
  if (!this.baselineData) {
774
767
  this.baselineData = createEmptyBaselineMetadata({
@@ -784,7 +777,6 @@ export class TddService {
784
777
  };
785
778
  upsertScreenshotInMetadata(this.baselineData, screenshotEntry, signature);
786
779
  saveBaselineMetadata(this.baselinePath, this.baselineData);
787
- output.info(`✅ Created new baseline for ${sanitizedName} (different dimensions)`);
788
780
  let result = buildNewComparison({
789
781
  name: sanitizedName,
790
782
  signature,
@@ -795,7 +787,7 @@ export class TddService {
795
787
  this.comparisons.push(result);
796
788
  return result;
797
789
  }
798
- output.error(`❌ Error comparing ${sanitizedName}: ${error.message}`);
790
+ output.debug('comparison', `${sanitizedName}: error - ${error.message}`);
799
791
  let result = buildErrorComparison({
800
792
  name: sanitizedName,
801
793
  signature,
@@ -821,106 +813,145 @@ export class TddService {
821
813
 
822
814
  /**
823
815
  * Print results to console
816
+ * Only prints once per test run to avoid duplicate output
824
817
  */
825
818
  async printResults() {
826
- let results = this.getResults();
827
- output.info('\n📊 TDD Results:');
828
- output.info(`Total: ${colors.cyan(results.total)}`);
829
- output.info(`Passed: ${colors.green(results.passed)}`);
830
- if (results.failed > 0) {
831
- output.info(`Failed: ${colors.red(results.failed)}`);
832
- }
833
- if (results.new > 0) {
834
- output.info(`New: ${colors.yellow(results.new)}`);
835
- }
836
- if (results.errors > 0) {
837
- output.info(`Errors: ${colors.red(results.errors)}`);
819
+ // Skip if already printed (prevents duplicate output from vizzlyFlush)
820
+ if (this._resultsPrinted) {
821
+ return this.getResults();
838
822
  }
823
+ this._resultsPrinted = true;
824
+ let {
825
+ output,
826
+ colors,
827
+ getFailedComparisons,
828
+ getNewComparisons,
829
+ existsSync,
830
+ readFileSync
831
+ } = this._deps;
832
+ let results = this.getResults();
839
833
  let failedComparisons = getFailedComparisons(this.comparisons);
834
+ let newComparisons = getNewComparisons(this.comparisons);
835
+ let passedComparisons = this.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'baseline-updated');
836
+ let hasChanges = failedComparisons.length > 0 || newComparisons.length > 0;
837
+
838
+ // Header with summary - use bear emoji as Vizzly mascot
839
+ output.blank();
840
+ output.print(`🐻 ${colors.bold(results.total)} screenshot${results.total !== 1 ? 's' : ''} compared`);
841
+ output.blank();
842
+
843
+ // Passed section - use Observatory success color
844
+ if (results.passed > 0) {
845
+ let successColor = colors.brand?.success || colors.green;
846
+ if (output.isVerbose()) {
847
+ // Verbose mode: show each screenshot
848
+ for (let comp of passedComparisons) {
849
+ output.print(` ${successColor('✓')} ${comp.name}`);
850
+ }
851
+ } else {
852
+ // Default mode: just show count with green checkmark and number
853
+ output.print(` ${successColor('✓')} ${successColor(results.passed)} passed`);
854
+ }
855
+ output.blank();
856
+ }
857
+
858
+ // Failed comparisons with diff bars - use Observatory warning/danger colors
840
859
  if (failedComparisons.length > 0) {
841
- output.info('\n❌ Failed comparisons:');
860
+ let warningColor = colors.brand?.warning || colors.yellow;
861
+ let dangerColor = colors.brand?.danger || colors.red;
862
+ output.print(` ${warningColor('◐')} ${warningColor(failedComparisons.length)} visual change${failedComparisons.length !== 1 ? 's' : ''} detected`);
863
+
864
+ // Find longest name for alignment
865
+ let maxNameLen = Math.max(...failedComparisons.map(c => c.name.length));
866
+ let textMuted = colors.brand?.textMuted || colors.dim;
842
867
  for (let comp of failedComparisons) {
843
- output.info(` • ${comp.name}`);
868
+ let diffDisplay = '';
869
+ if (comp.diffPercentage !== undefined) {
870
+ // Use the new diffBar helper for visual representation
871
+ let bar = output.diffBar(comp.diffPercentage, 10);
872
+ let paddedName = comp.name.padEnd(maxNameLen);
873
+ diffDisplay = ` ${bar} ${textMuted(`${comp.diffPercentage.toFixed(1)}%`)}`;
874
+ output.print(` ${dangerColor('✗')} ${paddedName}${diffDisplay}`);
875
+ } else {
876
+ output.print(` ${dangerColor('✗')} ${comp.name}`);
877
+ }
844
878
  }
879
+ output.blank();
845
880
  }
846
- let newComparisons = getNewComparisons(this.comparisons);
881
+
882
+ // New screenshots - use Observatory info color
847
883
  if (newComparisons.length > 0) {
848
- output.info('\n📸 New screenshots:');
884
+ let infoColor = colors.brand?.info || colors.cyan;
885
+ let textMuted = colors.brand?.textMuted || colors.dim;
886
+ output.print(` ${infoColor('+')} ${infoColor(newComparisons.length)} new screenshot${newComparisons.length !== 1 ? 's' : ''}`);
849
887
  for (let comp of newComparisons) {
850
- output.info(` ${comp.name}`);
888
+ output.print(` ${textMuted('○')} ${comp.name}`);
851
889
  }
890
+ output.blank();
852
891
  }
853
- await this.generateHtmlReport(results);
854
- return results;
855
- }
856
892
 
857
- /**
858
- * Generate HTML report using React reporter
859
- */
860
- async generateHtmlReport(results) {
861
- try {
862
- let reportGenerator = new StaticReportGenerator(this.workingDir, this.config);
863
-
864
- // Transform results to React reporter format
865
- let reportData = {
866
- buildId: this.baselineData?.buildId || 'local-tdd',
867
- summary: {
868
- passed: results.passed,
869
- failed: results.failed,
870
- total: results.total,
871
- new: results.new,
872
- errors: results.errors
873
- },
874
- comparisons: results.comparisons,
875
- baseline: this.baselineData,
876
- threshold: this.threshold
877
- };
878
- let reportPath = await reportGenerator.generateReport(reportData);
879
- output.info(`\n🐻 View detailed report: ${colors.cyan(`file://${reportPath}`)}`);
880
- if (this.config.tdd?.openReport) {
881
- await this.openReport(reportPath);
893
+ // Errors - use Observatory danger color
894
+ if (results.errors > 0) {
895
+ let dangerColor = colors.brand?.danger || colors.red;
896
+ let errorComparisons = this.comparisons.filter(c => c.status === 'error');
897
+ output.print(` ${dangerColor('!')} ${dangerColor(results.errors)} error${results.errors !== 1 ? 's' : ''}`);
898
+ for (let comp of errorComparisons) {
899
+ output.print(` ${dangerColor('✗')} ${comp.name}`);
882
900
  }
883
- return reportPath;
884
- } catch (error) {
885
- output.warn(`Failed to generate HTML report: ${error.message}`);
901
+ output.blank();
886
902
  }
887
- }
888
903
 
889
- /**
890
- * Open report in browser
891
- */
892
- async openReport(reportPath) {
893
- try {
894
- let {
895
- exec
896
- } = await import('node:child_process');
897
- let {
898
- promisify
899
- } = await import('node:util');
900
- let execAsync = promisify(exec);
901
- let command;
902
- switch (process.platform) {
903
- case 'darwin':
904
- command = `open "${reportPath}"`;
905
- break;
906
- case 'win32':
907
- command = `start "" "${reportPath}"`;
908
- break;
909
- default:
910
- command = `xdg-open "${reportPath}"`;
911
- break;
904
+ // Dashboard link with prominent styling - detect if server is running
905
+ if (hasChanges) {
906
+ let infoColor = colors.brand?.info || colors.cyan;
907
+ let textTertiary = colors.brand?.textTertiary || colors.dim;
908
+
909
+ // Check if TDD server is already running
910
+ let serverFile = `${this.workingDir}/.vizzly/server.json`;
911
+ let serverRunning = false;
912
+ let serverPort = 47392;
913
+ try {
914
+ if (existsSync(serverFile)) {
915
+ let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
916
+ if (serverInfo.port) {
917
+ serverPort = serverInfo.port;
918
+ serverRunning = true;
919
+ }
920
+ }
921
+ } catch {
922
+ // Ignore errors reading server file
912
923
  }
913
- await execAsync(command);
914
- output.info('📖 Report opened in browser');
915
- } catch {
916
- // Browser open may fail silently
924
+ if (serverRunning) {
925
+ // Server is running - show the dashboard URL
926
+ output.print(` ${textTertiary('→')} Review changes: ${infoColor(colors.underline(`http://localhost:${serverPort}`))}`);
927
+ } else {
928
+ // Server not running - suggest starting it
929
+ output.print(` ${textTertiary('→')} Review changes: ${infoColor(colors.underline('vizzly tdd start --open'))}`);
930
+ }
931
+ output.blank();
917
932
  }
933
+ return results;
918
934
  }
919
935
 
920
936
  /**
921
937
  * Update all baselines with current screenshots
922
938
  */
923
939
  updateBaselines() {
940
+ // Destructure dependencies
941
+ let {
942
+ output,
943
+ generateScreenshotSignature,
944
+ generateBaselineFilename,
945
+ sanitizeScreenshotName,
946
+ validateScreenshotProperties,
947
+ getBaselinePath,
948
+ existsSync,
949
+ readFileSync,
950
+ writeFileSync,
951
+ createEmptyBaselineMetadata,
952
+ upsertScreenshotInMetadata,
953
+ saveBaselineMetadata
954
+ } = this._deps;
924
955
  if (this.comparisons.length === 0) {
925
956
  output.warn('No comparisons found - nothing to update');
926
957
  return 0;
@@ -963,17 +994,17 @@ export class TddService {
963
994
  };
964
995
  upsertScreenshotInMetadata(this.baselineData, screenshotEntry, signature);
965
996
  updatedCount++;
966
- output.info(`✅ Updated baseline for ${sanitizedName}`);
997
+ output.info(`Updated baseline for ${sanitizedName}`);
967
998
  } catch (error) {
968
- output.error(`❌ Failed to update baseline for ${sanitizedName}: ${error.message}`);
999
+ output.error(`Failed to update baseline for ${sanitizedName}: ${error.message}`);
969
1000
  }
970
1001
  }
971
1002
  if (updatedCount > 0) {
972
1003
  try {
973
1004
  saveBaselineMetadata(this.baselinePath, this.baselineData);
974
- output.info(`✅ Updated ${updatedCount} baseline(s)`);
1005
+ output.info(`Updated ${updatedCount} baseline(s)`);
975
1006
  } catch (error) {
976
- output.error(`❌ Failed to save baseline metadata: ${error.message}`);
1007
+ output.error(`Failed to save baseline metadata: ${error.message}`);
977
1008
  }
978
1009
  }
979
1010
  return updatedCount;
@@ -983,6 +1014,21 @@ export class TddService {
983
1014
  * Accept a single baseline
984
1015
  */
985
1016
  async acceptBaseline(idOrComparison) {
1017
+ // Destructure dependencies
1018
+ let {
1019
+ output,
1020
+ generateScreenshotSignature,
1021
+ generateBaselineFilename,
1022
+ sanitizeScreenshotName,
1023
+ safePath,
1024
+ existsSync,
1025
+ readFileSync,
1026
+ mkdirSync,
1027
+ writeFileSync,
1028
+ createEmptyBaselineMetadata,
1029
+ upsertScreenshotInMetadata,
1030
+ saveBaselineMetadata
1031
+ } = this._deps;
986
1032
  let comparison;
987
1033
  if (typeof idOrComparison === 'string') {
988
1034
  comparison = this.comparisons.find(c => c.id === idOrComparison);
@@ -992,7 +1038,15 @@ export class TddService {
992
1038
  } else {
993
1039
  comparison = idOrComparison;
994
1040
  }
995
- let sanitizedName = comparison.name;
1041
+
1042
+ // Sanitize name for consistency, even though comparison.name is typically pre-sanitized
1043
+ let sanitizedName;
1044
+ try {
1045
+ sanitizedName = sanitizeScreenshotName(comparison.name);
1046
+ } catch (error) {
1047
+ output.error(`Invalid screenshot name '${comparison.name}': ${error.message}`);
1048
+ throw new Error(`Screenshot name validation failed: ${error.message}`);
1049
+ }
996
1050
  let properties = comparison.properties || {};
997
1051
 
998
1052
  // Generate signature from properties (don't rely on comparison.signature)
@@ -1016,8 +1070,8 @@ export class TddService {
1016
1070
  });
1017
1071
  }
1018
1072
 
1019
- // Update the baseline
1020
- let baselineImagePath = safePath(this.baselinePath, `${filename}.png`);
1073
+ // Update the baseline (filename already includes .png extension)
1074
+ let baselineImagePath = safePath(this.baselinePath, filename);
1021
1075
  writeFileSync(baselineImagePath, imageBuffer);
1022
1076
 
1023
1077
  // Update baseline metadata
@@ -1047,7 +1101,17 @@ export class TddService {
1047
1101
  * @private
1048
1102
  */
1049
1103
  createNewBaseline(name, imageBuffer, properties, currentImagePath, baselineImagePath) {
1050
- output.info(`🐻 Creating baseline for ${name}`);
1104
+ // Destructure dependencies
1105
+ let {
1106
+ output,
1107
+ generateScreenshotSignature,
1108
+ generateComparisonId,
1109
+ writeFileSync,
1110
+ createEmptyBaselineMetadata,
1111
+ upsertScreenshotInMetadata,
1112
+ saveBaselineMetadata
1113
+ } = this._deps;
1114
+ output.info(`Creating baseline for ${name}`);
1051
1115
  writeFileSync(baselineImagePath, imageBuffer);
1052
1116
  if (!this.baselineData) {
1053
1117
  this.baselineData = createEmptyBaselineMetadata({
@@ -1075,7 +1139,7 @@ export class TddService {
1075
1139
  signature
1076
1140
  };
1077
1141
  this.comparisons.push(result);
1078
- output.info(`✅ Baseline created for ${name}`);
1142
+ output.info(`Baseline created for ${name}`);
1079
1143
  return result;
1080
1144
  }
1081
1145
  }
@@ -42,14 +42,37 @@ export function vizzlyScreenshot(
42
42
  ): Promise<void>;
43
43
 
44
44
  /**
45
- * Wait for all queued screenshots to be processed
45
+ * Flush result summary returned by vizzlyFlush
46
+ */
47
+ export interface FlushResult {
48
+ success: boolean;
49
+ summary: {
50
+ total: number;
51
+ passed: number;
52
+ failed: number;
53
+ new: number;
54
+ errors: number;
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Signal test completion and trigger the results summary.
60
+ * Call this in your test framework's global teardown to see a summary of all visual comparisons.
61
+ *
62
+ * @returns The flush result with summary, or null if no server is connected
63
+ *
64
+ * @example
65
+ * // In Playwright global teardown
66
+ * import { vizzlyFlush } from '@vizzly-testing/cli/client';
67
+ * export default async () => await vizzlyFlush();
46
68
  *
47
69
  * @example
70
+ * // In Jest/Vitest
48
71
  * afterAll(async () => {
49
72
  * await vizzlyFlush();
50
73
  * });
51
74
  */
52
- export function vizzlyFlush(): Promise<void>;
75
+ export function vizzlyFlush(): Promise<FlushResult | null>;
53
76
 
54
77
  /**
55
78
  * Check if the Vizzly client is initialized and ready