@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.
- package/README.md +16 -18
- package/dist/cli.js +177 -2
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +118 -33
- package/dist/commands/finalize.js +8 -3
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +42 -49
- package/dist/commands/logout.js +13 -5
- package/dist/commands/project.js +95 -67
- package/dist/commands/run.js +32 -6
- package/dist/commands/status.js +81 -50
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +14 -26
- package/dist/commands/upload.js +18 -9
- package/dist/commands/whoami.js +40 -38
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +204 -22
- package/dist/server/handlers/tdd-handler.js +113 -7
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +5 -2
- package/dist/server-manager/operations.js +2 -1
- package/dist/services/config-service.js +306 -0
- package/dist/tdd/tdd-service.js +190 -126
- package/dist/types/client.d.ts +25 -2
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -7
- package/dist/report-generator/core.js +0 -315
- package/dist/report-generator/index.js +0 -8
- package/dist/report-generator/operations.js +0 -196
- package/dist/services/static-report-generator.js +0 -65
package/dist/tdd/tdd-service.js
CHANGED
|
@@ -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('
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
288
|
-
output.info('
|
|
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('
|
|
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(
|
|
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(
|
|
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(
|
|
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('
|
|
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(
|
|
471
|
+
output.info(`All ${skippedCount} baselines up-to-date`);
|
|
472
472
|
} else {
|
|
473
|
-
output.info(
|
|
473
|
+
output.info(`Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
|
|
474
474
|
}
|
|
475
475
|
} else {
|
|
476
|
-
output.info(
|
|
476
|
+
output.info(`Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
|
|
477
477
|
}
|
|
478
478
|
if (errorCount > 0) {
|
|
479
|
-
output.warn(
|
|
479
|
+
output.warn(`${errorCount} screenshots failed to download`);
|
|
480
480
|
}
|
|
481
481
|
return this.baselineData;
|
|
482
482
|
} catch (error) {
|
|
483
|
-
output.error(
|
|
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(
|
|
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('
|
|
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('
|
|
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(
|
|
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
|
|
753
|
-
let diffInfo =
|
|
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}
|
|
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.
|
|
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.
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
881
|
+
|
|
882
|
+
// New screenshots - use Observatory info color
|
|
847
883
|
if (newComparisons.length > 0) {
|
|
848
|
-
|
|
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.
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
let
|
|
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
|
-
|
|
884
|
-
} catch (error) {
|
|
885
|
-
output.warn(`Failed to generate HTML report: ${error.message}`);
|
|
901
|
+
output.blank();
|
|
886
902
|
}
|
|
887
|
-
}
|
|
888
903
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
let
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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(
|
|
997
|
+
output.info(`Updated baseline for ${sanitizedName}`);
|
|
967
998
|
} catch (error) {
|
|
968
|
-
output.error(
|
|
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(
|
|
1005
|
+
output.info(`Updated ${updatedCount} baseline(s)`);
|
|
975
1006
|
} catch (error) {
|
|
976
|
-
output.error(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
1142
|
+
output.info(`Baseline created for ${name}`);
|
|
1079
1143
|
return result;
|
|
1080
1144
|
}
|
|
1081
1145
|
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -42,14 +42,37 @@ export function vizzlyScreenshot(
|
|
|
42
42
|
): Promise<void>;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
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<
|
|
75
|
+
export function vizzlyFlush(): Promise<FlushResult | null>;
|
|
53
76
|
|
|
54
77
|
/**
|
|
55
78
|
* Check if the Vizzly client is initialized and ready
|