@vizzly-testing/cli
Version:
Visual review platform for UI developers and designers
314 lines (290 loc) • 10.3 kB
JavaScript
/**
* API Endpoints - Functions for each API operation
*
* Each function takes a client as the first parameter and returns the API result.
* This keeps the functions pure (no hidden state) and easily testable.
*/
import { VizzlyError } from '../errors/vizzly-error.js';
import * as output from '../utils/output.js';
import { buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildScreenshotCheckObject, buildScreenshotPayload, buildShaCheckPayload, computeSha256, findScreenshotBySha, shaExists } from './core.js';
// ============================================================================
// Build Endpoints
// ============================================================================
/**
* Get build information
* @param {Object} client - API client
* @param {string} buildId - Build ID
* @param {string|null} include - Optional include parameter (e.g., 'screenshots')
* @returns {Promise<Object>} Build data
*/
export async function getBuild(client, buildId, include = null) {
let endpoint = `/api/sdk/builds/${buildId}`;
if (include) {
endpoint = buildEndpointWithParams(endpoint, {
include
});
}
return client.request(endpoint);
}
/**
* Get builds for a project
* @param {Object} client - API client
* @param {Object} filters - Filter options
* @returns {Promise<Array>} List of builds
*/
export async function getBuilds(client, filters = {}) {
let query = buildQueryParams(filters);
let endpoint = `/api/sdk/builds${query ? `?${query}` : ''}`;
return client.request(endpoint);
}
/**
* Create a new build
* @param {Object} client - API client
* @param {Object} metadata - Build metadata
* @returns {Promise<Object>} Created build data
*/
export async function createBuild(client, metadata) {
let payload = buildBuildPayload(metadata);
return client.request('/api/sdk/builds', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
build: payload
})
});
}
/**
* Update build status
* @param {Object} client - API client
* @param {string} buildId - Build ID
* @param {string} status - Build status (pending|running|completed|failed)
* @param {number|null} executionTimeMs - Execution time in milliseconds
* @returns {Promise<Object>} Updated build data
*/
export async function updateBuildStatus(client, buildId, status, executionTimeMs = null) {
let body = {
status
};
if (executionTimeMs != null) {
body.executionTimeMs = executionTimeMs;
}
return client.request(`/api/sdk/builds/${buildId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
}
/**
* Finalize a build (convenience wrapper for updateBuildStatus)
* @param {Object} client - API client
* @param {string} buildId - Build ID
* @param {boolean} success - Whether the build succeeded
* @param {number|null} executionTimeMs - Execution time in milliseconds
* @returns {Promise<Object>} Finalized build data
*/
export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null) {
let status = success ? 'completed' : 'failed';
return updateBuildStatus(client, buildId, status, executionTimeMs);
}
/**
* Get TDD baselines for a build
* @param {Object} client - API client
* @param {string} buildId - Build ID
* @returns {Promise<Object>} { build, screenshots, signatureProperties }
*/
export async function getTddBaselines(client, buildId) {
return client.request(`/api/sdk/builds/${buildId}/tdd-baselines`);
}
// ============================================================================
// Screenshot Endpoints
// ============================================================================
/**
* Check if SHAs already exist on the server
* @param {Object} client - API client
* @param {Array} screenshots - Screenshots to check (objects with sha256, or string SHAs)
* @param {string} buildId - Build ID for screenshot record creation
* @returns {Promise<Object>} { existing, missing, screenshots }
*/
export async function checkShas(client, screenshots, buildId) {
try {
let payload = buildShaCheckPayload(screenshots, buildId);
return await client.request('/api/sdk/check-shas', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
} catch (error) {
// Continue without deduplication on error
output.debug('sha-check', 'failed, continuing without deduplication', {
error: error.message
});
// Extract SHAs for fallback response
let shaList = Array.isArray(screenshots) && screenshots.length > 0 && typeof screenshots[0] === 'object' ? screenshots.map(s => s.sha256) : screenshots;
return {
existing: [],
missing: shaList,
screenshots: []
};
}
}
/**
* Upload a screenshot with SHA deduplication
* @param {Object} client - API client
* @param {string} buildId - Build ID
* @param {string} name - Screenshot name
* @param {Buffer} buffer - Screenshot data
* @param {Object} metadata - Additional metadata
* @param {boolean} skipDedup - Skip SHA deduplication (uploadAll mode)
* @returns {Promise<Object>} Upload result
*/
export async function uploadScreenshot(client, buildId, name, buffer, metadata = {}, skipDedup = false) {
// Skip SHA deduplication if requested
if (skipDedup) {
let payload = buildScreenshotPayload(name, buffer, metadata);
return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
}
// Normal flow with SHA deduplication
let sha256 = computeSha256(buffer);
let checkObj = buildScreenshotCheckObject(sha256, name, metadata);
let checkResult = await checkShas(client, [checkObj], buildId);
if (shaExists(checkResult, sha256)) {
// File already exists, screenshot record was automatically created
let screenshot = findScreenshotBySha(checkResult, sha256);
return {
message: 'Screenshot already exists, skipped upload',
sha256,
skipped: true,
screenshot,
fromExisting: true
};
}
// File doesn't exist, proceed with upload
let payload = buildScreenshotPayload(name, buffer, metadata, sha256);
return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
}
// ============================================================================
// Comparison Endpoints
// ============================================================================
/**
* Get comparison information
* @param {Object} client - API client
* @param {string} comparisonId - Comparison ID
* @returns {Promise<Object>} Comparison data
*/
export async function getComparison(client, comparisonId) {
let response = await client.request(`/api/sdk/comparisons/${comparisonId}`);
return response.comparison;
}
/**
* Search for comparisons by name
* @param {Object} client - API client
* @param {string} name - Screenshot name to search for
* @param {Object} filters - Optional filters (branch, limit, offset)
* @returns {Promise<Object>} Search results with comparisons and pagination
*/
export async function searchComparisons(client, name, filters = {}) {
if (!name || typeof name !== 'string') {
throw new VizzlyError('name is required and must be a non-empty string');
}
let {
branch,
limit = 50,
offset = 0
} = filters;
let params = {
name,
limit: String(limit),
offset: String(offset)
};
if (branch) params.branch = branch;
let endpoint = buildEndpointWithParams('/api/sdk/comparisons/search', params);
return client.request(endpoint);
}
// ============================================================================
// Hotspot Endpoints
// ============================================================================
/**
* Get hotspot analysis for a single screenshot
* @param {Object} client - API client
* @param {string} screenshotName - Screenshot name
* @param {Object} options - Optional settings
* @returns {Promise<Object>} Hotspot analysis data
*/
export async function getScreenshotHotspots(client, screenshotName, options = {}) {
let {
windowSize = 20
} = options;
let encodedName = encodeURIComponent(screenshotName);
let endpoint = buildEndpointWithParams(`/api/sdk/screenshots/${encodedName}/hotspots`, {
windowSize: String(windowSize)
});
return client.request(endpoint);
}
/**
* Batch get hotspot analysis for multiple screenshots
* @param {Object} client - API client
* @param {string[]} screenshotNames - Array of screenshot names
* @param {Object} options - Optional settings
* @returns {Promise<Object>} Hotspots keyed by screenshot name
*/
export async function getBatchHotspots(client, screenshotNames, options = {}) {
let {
windowSize = 20
} = options;
return client.request('/api/sdk/screenshots/hotspots', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
screenshot_names: screenshotNames,
windowSize
})
});
}
// ============================================================================
// Auth/Token Endpoints
// ============================================================================
/**
* Get token context (organization and project info)
* @param {Object} client - API client
* @returns {Promise<Object>} Token context data
*/
export async function getTokenContext(client) {
return client.request('/api/sdk/token/context');
}
// ============================================================================
// Parallel Build Endpoints
// ============================================================================
/**
* Finalize a parallel build
* @param {Object} client - API client
* @param {string} parallelId - Parallel ID to finalize
* @returns {Promise<Object>} Finalization result
*/
export async function finalizeParallelBuild(client, parallelId) {
return client.request(`/api/sdk/parallel/${parallelId}/finalize`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
}