Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ builds. Can also be used to query for a project's builds using a read access tok
- [Usage](#usage)
- [Create a build](#create-a-build)
- [Create, upload, and finalize snapshots](#create-upload-and-finalize-snapshots)
- [Create, upload, and finalize comparisons](#create-upload-and-finalize-comparisons)
- [Finalize a build](#finalize-a-build)
- [Query for a build*](#query-for-a-build)
- [Query for a project's builds*](#query-for-a-projects-builds)
Expand Down Expand Up @@ -59,7 +60,39 @@ await client.sendSnapshot(buildId, snapshotOptions)
- `mimetype` — Resource mimetype (**required**)
- `content` — Resource content (**required**)
- `sha` — Resource content sha
- `root` — Boolean indicating a root resource
- `root` — Boolean indicating a root resource## Create, upload, and finalize snapshots

## Create, upload, and finalize comparisons

This method combines the work of creating a snapshot, creating an associated comparison, uploading
associated comparison tiles, and finally finalizing the comparison.

``` js
await client.sendComparison(buildId, comparisonOptions)
```

#### Options

- `name` — Snapshot name (**required**)
- `clientInfo` — Additional client info
- `environmentInfo` — Additional environment info
- `externalDebugUrl` — External debug URL
- `tag` — Tagged information about this comparison
- `name` — The tag name for this comparison, e.g. "iPhone 14 Pro" (**required**)
- `osName` - OS name for the comparison tag; e.g. "iOS"
- `osVersion` - OS version for the comparison tag; e.g. "16"
- `width` - The width for this type of comparison
- `height` - The height for this type of comparison
- `orientation` - Either "portrait" or "landscape"
- `tiles` — Array of comparison tiles
- `sha` — Tile file contents SHA-256 hash
- `filepath` — Tile filepath in the filesystem (required when missing `content`)
- `content` — Tile contents as a string or buffer (required when missing `filepath`)
- `statusBarHeight` — Height of any status bar in this tile
- `navBarHeight` — Height of any nav bar in this tile
- `headerHeight` — Height of any header area in this tile
- `footerHeight` — Height of any footer area in this tile
- `fullscreen` — Boolean indicating this is a fullscreen tile

## Finalize a build

Expand Down
122 changes: 108 additions & 14 deletions packages/client/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import {
const { PERCY_CLIENT_API_URL = 'https://percy.io/api/v1' } = process.env;
const pkg = getPackageJSON(import.meta.url);

// Validate build ID arguments
function validateBuildId(id) {
if (!id) throw new Error('Missing build ID');
// Validate ID arguments
function validateId(type, id) {
if (!id) throw new Error(`Missing ${type} ID`);
if (!(typeof id === 'string' || typeof id === 'number')) {
throw new Error('Invalid build ID');
throw new Error(`Invalid ${type} ID`);
}
}

Expand Down Expand Up @@ -159,15 +159,15 @@ export class PercyClient {
// Finalizes the active build. When `all` is true, `all-shards=true` is
// added as a query param so the API finalizes all other build shards.
async finalizeBuild(buildId, { all = false } = {}) {
validateBuildId(buildId);
validateId('build', buildId);
let qs = all ? 'all-shards=true' : '';
this.log.debug(`Finalizing build ${buildId}...`);
return this.post(`builds/${buildId}/finalize?${qs}`);
}

// Retrieves build data by id. Requires a read access token.
async getBuild(buildId) {
validateBuildId(buildId);
validateId('build', buildId);
this.log.debug(`Get build ${buildId}`);
return this.get(`builds/${buildId}`);
}
Expand Down Expand Up @@ -255,10 +255,9 @@ export class PercyClient {
// `content` is read from the filesystem. The sha is optional and will be
// created from `content` if one is not provided.
async uploadResource(buildId, { url, sha, filepath, content } = {}) {
validateBuildId(buildId);

validateId('build', buildId);
this.log.debug(`Uploading resource: ${url}...`);
if (filepath) content = fs.readFileSync(filepath);
if (filepath) content = await fs.promises.readFile(filepath);

return this.post(`builds/${buildId}/resources`, {
data: {
Expand All @@ -273,8 +272,7 @@ export class PercyClient {

// Uploads resources to the active build concurrently, two at a time.
async uploadResources(buildId, resources) {
validateBuildId(buildId);

validateId('build', buildId);
this.log.debug(`Uploading resources for ${buildId}...`);

return pool(function*() {
Expand All @@ -295,7 +293,7 @@ export class PercyClient {
environmentInfo,
resources = []
} = {}) {
validateBuildId(buildId);
validateId('build', buildId);
this.addClientInfo(clientInfo);
this.addEnvironmentInfo(environmentInfo);

Expand All @@ -305,6 +303,11 @@ export class PercyClient {

this.log.debug(`Creating snapshot: ${name}...`);

for (let resource of resources) {
if (resource.sha || resource.content || !resource.filepath) continue;
resource.content = await fs.promises.readFile(resource.filepath);
}

return this.post(`builds/${buildId}/snapshots`, {
data: {
type: 'snapshots',
Expand All @@ -319,7 +322,7 @@ export class PercyClient {
resources: {
data: resources.map(r => ({
type: 'resources',
id: r.sha || sha256hash(r.content),
id: r.sha ?? (r.content && sha256hash(r.content)),
attributes: {
'resource-url': r.url || null,
'is-root': r.root || null,
Expand All @@ -335,7 +338,7 @@ export class PercyClient {

// Finalizes a snapshot.
async finalizeSnapshot(snapshotId) {
if (!snapshotId) throw new Error('Missing snapshot ID');
validateId('snapshot', snapshotId);
this.log.debug(`Finalizing snapshot ${snapshotId}...`);
return this.post(`snapshots/${snapshotId}/finalize`);
}
Expand All @@ -354,6 +357,97 @@ export class PercyClient {
await this.finalizeSnapshot(snapshot.data.id);
return snapshot;
}

async createComparison(snapshotId, { tag, tiles = [], externalDebugUrl } = {}) {
validateId('snapshot', snapshotId);

this.log.debug(`Creating comparision: ${tag.name}...`);

for (let tile of tiles) {
if (tile.sha || tile.content || !tile.filepath) continue;
tile.content = await fs.promises.readFile(tile.filepath);
}

return this.post(`snapshots/${snapshotId}/comparisons`, {
data: {
type: 'comparisons',
attributes: {
'external-debug-url': externalDebugUrl || null
},
relationships: {
tag: {
data: {
type: 'tag',
attributes: {
name: tag.name || null,
width: tag.width || null,
height: tag.height || null,
'os-name': tag.osName || null,
'os-version': tag.osVersion || null,
orientation: tag.orientation || null
}
}
},
tiles: {
data: tiles.map(t => ({
type: 'tiles',
attributes: {
sha: t.sha || (t.content && sha256hash(t.content)),
'status-bar-height': t.statusBarHeight || null,
'nav-bar-height': t.navBarHeight || null,
'header-height': t.headerHeight || null,
'footer-height': t.footerHeight || null,
fullscreen: t.fullscreen || null
}
}))
}
}
}
});
}

async uploadComparisonTile(comparisonId, { index = 0, total = 1, filepath, content } = {}) {
validateId('comparison', comparisonId);
this.log.debug(`Uploading comparison tile: ${index + 1}/${total} (${comparisonId})...`);
if (filepath) content = await fs.promises.readFile(filepath);

return this.post(`comparisons/${comparisonId}/tiles`, {
data: {
type: 'tiles',
attributes: {
'base64-content': base64encode(content),
index
}
}
});
}

async uploadComparisonTiles(comparisonId, tiles) {
validateId('comparison', comparisonId);
this.log.debug(`Uploading comparison tiles for ${comparisonId}...`);

return pool(function*() {
for (let index = 0; index < tiles.length; index++) {
yield this.uploadComparisonTile(comparisonId, {
index, total: tiles.length, ...tiles[index]
});
}
}, this, 2);
}

async finalizeComparison(comparisonId) {
validateId('comparison', comparisonId);
this.log.debug(`Finalizing comparison ${comparisonId}...`);
return this.post(`comparisons/${comparisonId}/finalize`);
}

async sendComparison(buildId, options) {
let snapshot = await this.createSnapshot(buildId, options);
let comparison = await this.createComparison(snapshot.data.id, options);
await this.uploadComparisonTiles(comparison.data.id, options.tiles);
await this.finalizeComparison(comparison.data.id);
return comparison;
}
}

export default PercyClient;
Loading