Skip to content

Commit

Permalink
✨ Add comparisons API for future SDK usage (#1087)
Browse files Browse the repository at this point in the history
* ✨ Add client methods for new comparisons API endpoints

* ✨ Add core endpoint to upload snapshot comparisons

* ✨ Add comparison sdk-utils helper

* ✨ Return generated comparison redirect link from CLI endpoint

* 🐛 Remove comparison max height constraint
  • Loading branch information
wwilsman authored Oct 6, 2022
1 parent c13f7d1 commit 5f86910
Show file tree
Hide file tree
Showing 14 changed files with 857 additions and 53 deletions.
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

0 comments on commit 5f86910

Please sign in to comment.