@vizzly-testing/cli 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3 -1
- package/dist/plugin-api.js +43 -0
- package/dist/server/handlers/tdd-handler.js +11 -10
- package/dist/services/tdd-service.js +65 -29
- package/dist/types/index.d.ts +84 -0
- package/docs/plugins.md +60 -25
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { runDaemonChild, tddStartCommand, tddStatusCommand, tddStopCommand } fro
|
|
|
14
14
|
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
15
15
|
import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
|
|
16
16
|
import { loadPlugins } from './plugin-loader.js';
|
|
17
|
+
import { createPluginServices } from './plugin-api.js';
|
|
17
18
|
import { createServices } from './services/index.js';
|
|
18
19
|
import { loadConfig } from './utils/config-loader.js';
|
|
19
20
|
import * as output from './utils/output.js';
|
|
@@ -47,6 +48,7 @@ output.configure({
|
|
|
47
48
|
});
|
|
48
49
|
const config = await loadConfig(configPath, {});
|
|
49
50
|
const services = createServices(config);
|
|
51
|
+
const pluginServices = createPluginServices(services);
|
|
50
52
|
let plugins = [];
|
|
51
53
|
try {
|
|
52
54
|
plugins = await loadPlugins(configPath, config);
|
|
@@ -55,7 +57,7 @@ try {
|
|
|
55
57
|
// Add timeout protection for plugin registration (5 seconds)
|
|
56
58
|
const registerPromise = plugin.register(program, {
|
|
57
59
|
config,
|
|
58
|
-
services,
|
|
60
|
+
services: pluginServices,
|
|
59
61
|
output,
|
|
60
62
|
// Backwards compatibility alias for plugins using old API
|
|
61
63
|
logger: output
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin API - Stable interface for Vizzly plugins
|
|
3
|
+
*
|
|
4
|
+
* This module defines the stable API contract for plugins. Only methods
|
|
5
|
+
* exposed here are considered part of the public API and are guaranteed
|
|
6
|
+
* to not break between minor versions.
|
|
7
|
+
*
|
|
8
|
+
* Internal services (apiService, uploader, buildManager, etc.) are NOT
|
|
9
|
+
* exposed to plugins to prevent coupling to implementation details.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a stable plugin services object from the internal services
|
|
14
|
+
*
|
|
15
|
+
* Only exposes:
|
|
16
|
+
* - testRunner: Build lifecycle management (createBuild, finalizeBuild, events)
|
|
17
|
+
* - serverManager: Screenshot server control (start, stop)
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} services - Internal services from createServices()
|
|
20
|
+
* @returns {Object} Frozen plugin services object
|
|
21
|
+
*/
|
|
22
|
+
export function createPluginServices(services) {
|
|
23
|
+
let {
|
|
24
|
+
testRunner,
|
|
25
|
+
serverManager
|
|
26
|
+
} = services;
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
testRunner: Object.freeze({
|
|
29
|
+
// EventEmitter methods for build lifecycle events
|
|
30
|
+
once: testRunner.once.bind(testRunner),
|
|
31
|
+
on: testRunner.on.bind(testRunner),
|
|
32
|
+
off: testRunner.off.bind(testRunner),
|
|
33
|
+
// Build lifecycle
|
|
34
|
+
createBuild: testRunner.createBuild.bind(testRunner),
|
|
35
|
+
finalizeBuild: testRunner.finalizeBuild.bind(testRunner)
|
|
36
|
+
}),
|
|
37
|
+
serverManager: Object.freeze({
|
|
38
|
+
// Server lifecycle
|
|
39
|
+
start: serverManager.start.bind(serverManager),
|
|
40
|
+
stop: serverManager.stop.bind(serverManager)
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -245,17 +245,18 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// Extract
|
|
249
|
-
// This ensures signature generation works correctly
|
|
248
|
+
// Extract ALL properties to top-level (matching cloud API behavior)
|
|
249
|
+
// This ensures signature generation works correctly for custom properties like theme, device, etc.
|
|
250
|
+
// Spread all validated properties first, then normalize viewport/browser for cloud format
|
|
250
251
|
const extractedProperties = {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
252
|
+
...validatedProperties,
|
|
253
|
+
// Normalize viewport to top-level viewport_width/height (cloud format)
|
|
254
|
+
// Use nullish coalescing to preserve any existing top-level values
|
|
255
|
+
viewport_width: validatedProperties.viewport?.width ?? validatedProperties.viewport_width ?? null,
|
|
256
|
+
viewport_height: validatedProperties.viewport?.height ?? validatedProperties.viewport_height ?? null,
|
|
257
|
+
browser: validatedProperties.browser ?? null,
|
|
258
|
+
// Preserve nested structure in metadata for backward compatibility
|
|
259
|
+
// Signature generation checks multiple locations: top-level, metadata.*, metadata.properties.*
|
|
259
260
|
metadata: validatedProperties
|
|
260
261
|
};
|
|
261
262
|
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD Service - Local Visual Testing
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ CRITICAL: Signature/filename generation MUST stay in sync with the cloud!
|
|
5
|
+
*
|
|
6
|
+
* Cloud counterpart: vizzly/src/utils/screenshot-identity.js
|
|
7
|
+
* - generateScreenshotSignature()
|
|
8
|
+
* - generateBaselineFilename()
|
|
9
|
+
*
|
|
10
|
+
* Contract tests: Both repos have golden tests that must produce identical values:
|
|
11
|
+
* - Cloud: tests/contracts/signature-parity.test.js
|
|
12
|
+
* - CLI: tests/contracts/signature-parity.spec.js
|
|
13
|
+
*
|
|
14
|
+
* If you modify signature or filename generation here, you MUST:
|
|
15
|
+
* 1. Make the same change in the cloud repo
|
|
16
|
+
* 2. Update golden test values in BOTH repos
|
|
17
|
+
* 3. Run contract tests in both repos to verify parity
|
|
18
|
+
*
|
|
19
|
+
* The signature format is: name|viewport_width|browser|custom1|custom2|...
|
|
20
|
+
* The filename format is: {sanitized-name}_{12-char-sha256-hash}.png
|
|
21
|
+
*/
|
|
22
|
+
|
|
1
23
|
import crypto from 'node:crypto';
|
|
2
24
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
25
|
import { join } from 'node:path';
|
|
@@ -13,13 +35,10 @@ import { HtmlReportGenerator } from './html-report-generator.js';
|
|
|
13
35
|
|
|
14
36
|
/**
|
|
15
37
|
* Generate a screenshot signature for baseline matching
|
|
16
|
-
* Uses same logic as screenshot-identity.js: name + viewport_width + browser + custom properties
|
|
17
38
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* - screenshot.browser (top-level property)
|
|
22
|
-
* - custom properties from project's baseline_signature_properties setting
|
|
39
|
+
* ⚠️ SYNC WITH: vizzly/src/utils/screenshot-identity.js - generateScreenshotSignature()
|
|
40
|
+
*
|
|
41
|
+
* Uses same logic as cloud: name + viewport_width + browser + custom properties
|
|
23
42
|
*
|
|
24
43
|
* @param {string} name - Screenshot name
|
|
25
44
|
* @param {Object} properties - Screenshot properties (viewport, browser, metadata, etc.)
|
|
@@ -62,17 +81,23 @@ function generateScreenshotSignature(name, properties = {}, customProperties = [
|
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
84
|
+
* Generate a stable, filesystem-safe filename for a screenshot baseline
|
|
85
|
+
* Uses a hash of the signature to avoid character encoding issues
|
|
86
|
+
* Matches the cloud's generateBaselineFilename implementation exactly
|
|
67
87
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
88
|
+
* @param {string} name - Screenshot name
|
|
89
|
+
* @param {string} signature - Full signature string
|
|
90
|
+
* @returns {string} Filename like "homepage_a1b2c3d4e5f6.png"
|
|
71
91
|
*/
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
function generateBaselineFilename(name, signature) {
|
|
93
|
+
const hash = crypto.createHash('sha256').update(signature).digest('hex').slice(0, 12);
|
|
94
|
+
|
|
95
|
+
// Sanitize the name for filesystem safety
|
|
96
|
+
const safeName = name.replace(/[/\\:*?"<>|]/g, '') // Remove unsafe chars
|
|
97
|
+
.replace(/\s+/g, '-') // Spaces to hyphens
|
|
98
|
+
.slice(0, 50); // Limit length
|
|
99
|
+
|
|
100
|
+
return `${safeName}_${hash}.png`;
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
/**
|
|
@@ -322,8 +347,11 @@ export class TddService {
|
|
|
322
347
|
...(screenshot.metadata || screenshot.properties || {})
|
|
323
348
|
});
|
|
324
349
|
const signature = generateScreenshotSignature(sanitizedName, properties, this.signatureProperties);
|
|
325
|
-
|
|
326
|
-
|
|
350
|
+
|
|
351
|
+
// Use API-provided filename if available, otherwise generate hash-based filename
|
|
352
|
+
// Both return the full filename with .png extension
|
|
353
|
+
const filename = screenshot.filename || generateBaselineFilename(sanitizedName, signature);
|
|
354
|
+
const imagePath = safePath(this.baselinePath, filename);
|
|
327
355
|
|
|
328
356
|
// Check if we already have this file with the same SHA (using metadata)
|
|
329
357
|
if (existsSync(imagePath) && screenshot.sha256) {
|
|
@@ -447,7 +475,7 @@ export class TddService {
|
|
|
447
475
|
...(s.metadata || s.properties || {})
|
|
448
476
|
});
|
|
449
477
|
const signature = generateScreenshotSignature(sanitizedName, properties, this.signatureProperties);
|
|
450
|
-
const filename =
|
|
478
|
+
const filename = generateBaselineFilename(sanitizedName, signature);
|
|
451
479
|
return {
|
|
452
480
|
name: sanitizedName,
|
|
453
481
|
originalName: s.name,
|
|
@@ -455,7 +483,7 @@ export class TddService {
|
|
|
455
483
|
// Store remote SHA for quick comparison
|
|
456
484
|
id: s.id,
|
|
457
485
|
properties: properties,
|
|
458
|
-
path: safePath(this.baselinePath,
|
|
486
|
+
path: safePath(this.baselinePath, filename),
|
|
459
487
|
signature: signature,
|
|
460
488
|
originalUrl: s.original_url,
|
|
461
489
|
fileSize: s.file_size_bytes,
|
|
@@ -735,8 +763,9 @@ export class TddService {
|
|
|
735
763
|
...screenshot.metadata
|
|
736
764
|
});
|
|
737
765
|
const signature = generateScreenshotSignature(sanitizedName, properties, this.signatureProperties);
|
|
738
|
-
|
|
739
|
-
const
|
|
766
|
+
// Use API-provided filename if available, otherwise generate hash-based filename
|
|
767
|
+
const filename = screenshot.filename || generateBaselineFilename(sanitizedName, signature);
|
|
768
|
+
const filePath = safePath(this.baselinePath, filename);
|
|
740
769
|
|
|
741
770
|
// Check if we can skip via SHA comparison
|
|
742
771
|
if (screenshot.sha256 && existingShaMap.get(signature) === screenshot.sha256) {
|
|
@@ -900,6 +929,12 @@ export class TddService {
|
|
|
900
929
|
validatedProperties = {};
|
|
901
930
|
}
|
|
902
931
|
|
|
932
|
+
// Preserve metadata object through validation (validateScreenshotProperties strips non-primitives)
|
|
933
|
+
// This is needed because signature generation checks properties.metadata.* for custom properties
|
|
934
|
+
if (properties.metadata && typeof properties.metadata === 'object') {
|
|
935
|
+
validatedProperties.metadata = properties.metadata;
|
|
936
|
+
}
|
|
937
|
+
|
|
903
938
|
// Normalize properties to match backend format (viewport_width at top level)
|
|
904
939
|
// This ensures signature generation matches backend's screenshot-identity.js
|
|
905
940
|
if (validatedProperties.viewport?.width && !validatedProperties.viewport_width) {
|
|
@@ -908,10 +943,11 @@ export class TddService {
|
|
|
908
943
|
|
|
909
944
|
// Generate signature for baseline matching (name + viewport_width + browser + custom props)
|
|
910
945
|
const signature = generateScreenshotSignature(sanitizedName, validatedProperties, this.signatureProperties);
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
const
|
|
914
|
-
const
|
|
946
|
+
// Use hash-based filename for reliable matching (matches cloud format)
|
|
947
|
+
const filename = generateBaselineFilename(sanitizedName, signature);
|
|
948
|
+
const currentImagePath = safePath(this.currentPath, filename);
|
|
949
|
+
const baselineImagePath = safePath(this.baselinePath, filename);
|
|
950
|
+
const diffImagePath = safePath(this.diffPath, filename);
|
|
915
951
|
|
|
916
952
|
// Save current screenshot
|
|
917
953
|
writeFileSync(currentImagePath, imageBuffer);
|
|
@@ -1303,8 +1339,8 @@ export class TddService {
|
|
|
1303
1339
|
}
|
|
1304
1340
|
const validatedProperties = validateScreenshotProperties(comparison.properties || {});
|
|
1305
1341
|
const signature = generateScreenshotSignature(sanitizedName, validatedProperties, this.signatureProperties);
|
|
1306
|
-
const filename =
|
|
1307
|
-
const baselineImagePath = safePath(this.baselinePath,
|
|
1342
|
+
const filename = generateBaselineFilename(sanitizedName, signature);
|
|
1343
|
+
const baselineImagePath = safePath(this.baselinePath, filename);
|
|
1308
1344
|
try {
|
|
1309
1345
|
// Copy current screenshot to baseline
|
|
1310
1346
|
const currentBuffer = readFileSync(current);
|
|
@@ -1479,10 +1515,10 @@ export class TddService {
|
|
|
1479
1515
|
const sanitizedName = comparison.name;
|
|
1480
1516
|
const properties = comparison.properties || {};
|
|
1481
1517
|
const signature = generateScreenshotSignature(sanitizedName, properties, this.signatureProperties);
|
|
1482
|
-
const filename =
|
|
1518
|
+
const filename = generateBaselineFilename(sanitizedName, signature);
|
|
1483
1519
|
|
|
1484
1520
|
// Find the current screenshot file
|
|
1485
|
-
const currentImagePath = safePath(this.currentPath,
|
|
1521
|
+
const currentImagePath = safePath(this.currentPath, filename);
|
|
1486
1522
|
if (!existsSync(currentImagePath)) {
|
|
1487
1523
|
output.error(`Current screenshot not found at: ${currentImagePath}`);
|
|
1488
1524
|
throw new Error(`Current screenshot not found: ${sanitizedName} (looked at ${currentImagePath})`);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -351,6 +351,90 @@ export interface Services {
|
|
|
351
351
|
testRunner: unknown;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Plugin API Types (Stable Contract)
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Stable TestRunner interface for plugins.
|
|
360
|
+
* Only these methods are guaranteed to remain stable across minor versions.
|
|
361
|
+
*/
|
|
362
|
+
export interface PluginTestRunner {
|
|
363
|
+
/** Listen for a single event emission */
|
|
364
|
+
once(event: string, callback: (...args: unknown[]) => void): void;
|
|
365
|
+
/** Subscribe to events */
|
|
366
|
+
on(event: string, callback: (...args: unknown[]) => void): void;
|
|
367
|
+
/** Unsubscribe from events */
|
|
368
|
+
off(event: string, callback: (...args: unknown[]) => void): void;
|
|
369
|
+
/** Create a new build and return the build ID */
|
|
370
|
+
createBuild(options: BuildOptions, isTddMode: boolean): Promise<string>;
|
|
371
|
+
/** Finalize a build after all screenshots are captured */
|
|
372
|
+
finalizeBuild(
|
|
373
|
+
buildId: string,
|
|
374
|
+
isTddMode: boolean,
|
|
375
|
+
success: boolean,
|
|
376
|
+
executionTime: number
|
|
377
|
+
): Promise<void>;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Stable ServerManager interface for plugins.
|
|
382
|
+
* Only these methods are guaranteed to remain stable across minor versions.
|
|
383
|
+
*/
|
|
384
|
+
export interface PluginServerManager {
|
|
385
|
+
/** Start the screenshot server */
|
|
386
|
+
start(buildId: string, tddMode: boolean, setBaseline: boolean): Promise<void>;
|
|
387
|
+
/** Stop the screenshot server */
|
|
388
|
+
stop(): Promise<void>;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Stable services interface for plugins.
|
|
393
|
+
* This is the public API contract - internal services are NOT exposed.
|
|
394
|
+
*/
|
|
395
|
+
export interface PluginServices {
|
|
396
|
+
testRunner: PluginTestRunner;
|
|
397
|
+
serverManager: PluginServerManager;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Build options for createBuild()
|
|
402
|
+
*/
|
|
403
|
+
export interface BuildOptions {
|
|
404
|
+
port?: number;
|
|
405
|
+
timeout?: number;
|
|
406
|
+
buildName?: string;
|
|
407
|
+
branch?: string;
|
|
408
|
+
commit?: string;
|
|
409
|
+
message?: string;
|
|
410
|
+
environment?: string;
|
|
411
|
+
threshold?: number;
|
|
412
|
+
eager?: boolean;
|
|
413
|
+
allowNoToken?: boolean;
|
|
414
|
+
wait?: boolean;
|
|
415
|
+
uploadAll?: boolean;
|
|
416
|
+
pullRequestNumber?: string;
|
|
417
|
+
parallelId?: string;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Context object passed to plugin register() function.
|
|
422
|
+
* This is the stable plugin API contract.
|
|
423
|
+
*/
|
|
424
|
+
export interface PluginContext {
|
|
425
|
+
/** Merged Vizzly configuration */
|
|
426
|
+
config: VizzlyConfig;
|
|
427
|
+
/** Stable services for plugins */
|
|
428
|
+
services: PluginServices;
|
|
429
|
+
/** Output utilities for logging */
|
|
430
|
+
output: OutputUtils;
|
|
431
|
+
/** @deprecated Use output instead. Alias for backwards compatibility. */
|
|
432
|
+
logger: OutputUtils;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Create stable plugin services from internal services */
|
|
436
|
+
export function createPluginServices(services: Services): PluginServices;
|
|
437
|
+
|
|
354
438
|
// ============================================================================
|
|
355
439
|
// Output Utilities
|
|
356
440
|
// ============================================================================
|
package/docs/plugins.md
CHANGED
|
@@ -104,8 +104,8 @@ export default {
|
|
|
104
104
|
.action(async (arg, options) => {
|
|
105
105
|
output.info(`Running my-command with ${arg}`);
|
|
106
106
|
|
|
107
|
-
// Access shared services
|
|
108
|
-
let apiService =
|
|
107
|
+
// Access shared services directly
|
|
108
|
+
let apiService = services.apiService;
|
|
109
109
|
|
|
110
110
|
// Your command logic here
|
|
111
111
|
});
|
|
@@ -134,26 +134,55 @@ The `register` function receives two arguments:
|
|
|
134
134
|
- `output` - Unified output module with `.debug()`, `.info()`, `.warn()`, `.error()`, `.success()` methods
|
|
135
135
|
- `services` - Service container with access to internal Vizzly services
|
|
136
136
|
|
|
137
|
-
### Available Services
|
|
137
|
+
### Available Services (Stable API)
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
The `services` object provides a stable API for plugins. Only these services and methods are
|
|
140
|
+
guaranteed to remain stable across minor versions:
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
- **`uploader`** - Screenshot upload service
|
|
143
|
-
- **`buildManager`** - Build lifecycle management
|
|
144
|
-
- **`serverManager`** - Screenshot server management
|
|
145
|
-
- **`tddService`** - TDD mode services
|
|
146
|
-
- **`testRunner`** - Test execution service
|
|
142
|
+
#### `services.testRunner`
|
|
147
143
|
|
|
148
|
-
|
|
144
|
+
Manages build lifecycle and emits events:
|
|
145
|
+
|
|
146
|
+
- **`once(event, callback)`** - Listen for a single event emission
|
|
147
|
+
- **`on(event, callback)`** - Subscribe to events
|
|
148
|
+
- **`off(event, callback)`** - Unsubscribe from events
|
|
149
|
+
- **`createBuild(options, isTddMode)`** - Create a new build, returns `Promise<buildId>`
|
|
150
|
+
- **`finalizeBuild(buildId, isTddMode, success, executionTime)`** - Finalize a build
|
|
151
|
+
|
|
152
|
+
Events emitted:
|
|
153
|
+
- `build-created` - Emitted with `{ url }` when a build is created
|
|
154
|
+
|
|
155
|
+
#### `services.serverManager`
|
|
156
|
+
|
|
157
|
+
Controls the screenshot capture server:
|
|
158
|
+
|
|
159
|
+
- **`start(buildId, tddMode, setBaseline)`** - Start the screenshot server
|
|
160
|
+
- **`stop()`** - Stop the screenshot server
|
|
161
|
+
|
|
162
|
+
Example accessing services:
|
|
149
163
|
|
|
150
164
|
```javascript
|
|
151
165
|
register(program, { config, output, services }) {
|
|
152
166
|
program
|
|
153
|
-
.command('
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
.command('capture')
|
|
168
|
+
.description('Capture screenshots with custom workflow')
|
|
169
|
+
.action(async () => {
|
|
170
|
+
let { testRunner, serverManager } = services;
|
|
171
|
+
|
|
172
|
+
// Listen for build creation
|
|
173
|
+
testRunner.once('build-created', ({ url }) => {
|
|
174
|
+
output.info(`Build created: ${url}`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Create build and start server
|
|
178
|
+
let buildId = await testRunner.createBuild({ buildName: 'Custom' }, false);
|
|
179
|
+
await serverManager.start(buildId, false, false);
|
|
180
|
+
|
|
181
|
+
// ... capture screenshots ...
|
|
182
|
+
|
|
183
|
+
// Finalize and cleanup
|
|
184
|
+
await testRunner.finalizeBuild(buildId, false, true, Date.now());
|
|
185
|
+
await serverManager.stop();
|
|
157
186
|
});
|
|
158
187
|
}
|
|
159
188
|
```
|
|
@@ -290,9 +319,9 @@ Use async/await for asynchronous operations:
|
|
|
290
319
|
|
|
291
320
|
```javascript
|
|
292
321
|
.action(async (options) => {
|
|
293
|
-
let
|
|
294
|
-
let
|
|
295
|
-
output.info(`
|
|
322
|
+
let { testRunner } = services;
|
|
323
|
+
let buildId = await testRunner.createBuild({ buildName: 'Test' }, false);
|
|
324
|
+
output.info(`Created build: ${buildId}`);
|
|
296
325
|
});
|
|
297
326
|
```
|
|
298
327
|
|
|
@@ -384,21 +413,27 @@ export default {
|
|
|
384
413
|
.description('Capture screenshots from Storybook build')
|
|
385
414
|
.option('--viewports <list>', 'Comma-separated viewports', '1280x720')
|
|
386
415
|
.action(async (path, options) => {
|
|
416
|
+
let { testRunner, serverManager } = services;
|
|
417
|
+
let startTime = Date.now();
|
|
418
|
+
|
|
419
|
+
// Create build and start server
|
|
420
|
+
let buildId = await testRunner.createBuild({ buildName: 'Storybook' }, false);
|
|
421
|
+
await serverManager.start(buildId, false, false);
|
|
422
|
+
|
|
387
423
|
output.info(`Crawling Storybook at ${path}`);
|
|
388
424
|
|
|
389
425
|
// Import dependencies lazily
|
|
390
426
|
let { crawlStorybook } = await import('./crawler.js');
|
|
391
427
|
|
|
392
|
-
// Capture screenshots
|
|
393
|
-
|
|
428
|
+
// Capture screenshots (uses vizzlyScreenshot internally)
|
|
429
|
+
await crawlStorybook(path, {
|
|
394
430
|
viewports: options.viewports.split(','),
|
|
395
431
|
});
|
|
396
432
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
await uploader.uploadScreenshots(screenshots);
|
|
433
|
+
// Finalize build
|
|
434
|
+
let executionTime = Date.now() - startTime;
|
|
435
|
+
await testRunner.finalizeBuild(buildId, false, true, executionTime);
|
|
436
|
+
await serverManager.stop();
|
|
402
437
|
|
|
403
438
|
output.success('Upload complete!');
|
|
404
439
|
});
|