Skip to content

Commit

Permalink
✨ Always wait on discovery but upload snapshots async
Browse files Browse the repository at this point in the history
  • Loading branch information
wwilsman committed Aug 4, 2020
1 parent e9c57d1 commit 1271002
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 130 deletions.
146 changes: 74 additions & 72 deletions packages/core/src/percy.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,15 @@ export default class Percy {
let meta = { build: { id: build.id } };
log.info('Stopping percy...', meta);

// clear queued page captures and wait for any pending
if (this.#captures.clear()) {
log.info(`Waiting for ${this.#captures.length} page(s) to complete`, meta);
await this.#captures.idle();
// log about queued captures or uploads
if (this.#captures.length) {
log.info(`Waiting for ${this.#captures.length} page(s) to finish snapshotting`, meta);
} else if (this.#snapshots.length) {
log.info(`Waiting for ${this.#snapshots.length} snapshot(s) to finish uploading`, meta);
}

// clear queued snapshots and wait for any pending
if (this.#snapshots.clear()) {
log.info(`Waiting for ${this.#snapshots.length} snapshot(s) to complete`, meta);
await this.#snapshots.idle();
}
// wait for any queued captures or snapshots
await this.idle();

// close the server and browser
this.server?.close();
Expand All @@ -161,8 +159,9 @@ export default class Percy {
}

// Handles asset discovery for the URL and DOM snapshot at each requested
// width with the provided options. Resolves when the snapshot is complete,
// although shouldn't be awaited on as snapshots are handled concurrently.
// width with the provided options. Resolves when the snapshot has been taken
// and asset discovery is finished, but does not gaurantee that the snapshot
// will be succesfully uploaded.
snapshot({
url,
name,
Expand Down Expand Up @@ -197,68 +196,70 @@ export default class Percy {
// fallback to instance enable JS flag
enableJavaScript = enableJavaScript ?? this.config.snapshot.enableJavaScript ?? false;

// add this snapshot task to the snapshot queue
return this.#snapshots.push(async () => {
let meta = {
snapshot: { name },
build: { id: this.client.build.id }
};

log.debug('---------');
log.debug('Handling snapshot:', meta);
log.debug(`-> name: ${name}`, meta);
log.debug(`-> url: ${url}`, meta);
log.debug(`-> widths: ${widths.join('px, ')}px`, meta);
log.debug(`-> clientInfo: ${clientInfo}`, meta);
log.debug(`-> environmentInfo: ${environmentInfo}`, meta);
log.debug(`-> requestHeaders: ${JSON.stringify(requestHeaders)}`, meta);
log.debug(`-> domSnapshot:\n${(
domSnapshot.length <= 1024 ? domSnapshot
: (domSnapshot.substr(0, 1024) + '... [truncated]')
)}`, meta);

try {
// inject Percy CSS
let [percyDOM, percyCSSResource] = injectPercyCSS(url, domSnapshot, percyCSS, meta);
// use a map so resources remain unique by url
let resources = new Map([[url, createRootResource(url, percyDOM)]]);

// gather resources at each width concurrently
await Promise.all(widths.map(width => (
this.discoverer.gatherResources(resources, {
rootUrl: url,
rootDom: domSnapshot,
enableJavaScript,
requestHeaders,
width,
meta
})
)));

// convert resource map to array
resources = Array.from(resources.values());
// include the Percy CSS resource if there was one
if (percyCSSResource) resources.push(percyCSSResource);
// include a log resource for debugging
let logs = await log.query({ filter: ({ snapshot: s }) => s?.name === name });
resources.push(createLogResource(logs));

// create, upload, and finalize the snapshot
await this.client.sendSnapshot({
name,
widths,
minimumHeight,
// useful meta info for the logfile
let meta = {
snapshot: { name },
build: { id: this.client.build.id }
};

log.debug('---------');
log.debug('Handling snapshot:', meta);
log.debug(`-> name: ${name}`, meta);
log.debug(`-> url: ${url}`, meta);
log.debug(`-> widths: ${widths.join('px, ')}px`, meta);
log.debug(`-> clientInfo: ${clientInfo}`, meta);
log.debug(`-> environmentInfo: ${environmentInfo}`, meta);
log.debug(`-> requestHeaders: ${JSON.stringify(requestHeaders)}`, meta);
log.debug(`-> domSnapshot:\n${(
domSnapshot.length <= 1024 ? domSnapshot
: (domSnapshot.substr(0, 1024) + '... [truncated]')
)}`, meta);

// use a promise as a try-catch so we can do the remaining work
// asynchronously, but perform the above synchronously
return Promise.resolve().then(async () => {
// inject Percy CSS
let [percyDOM, percyCSSResource] = injectPercyCSS(url, domSnapshot, percyCSS, meta);
// use a map so resources remain unique by url
let resources = new Map([[url, createRootResource(url, percyDOM)]]);
// include the Percy CSS resource if there was one
if (percyCSSResource) resources.set('percy-css', percyCSSResource);

// gather resources at each width concurrently
await Promise.all(widths.map(width => (
this.discoverer.gatherResources(resources, {
rootUrl: url,
rootDom: domSnapshot,
enableJavaScript,
clientInfo,
environmentInfo,
resources
});

log.info(`Snapshot taken: ${name}`, meta);
} catch (error) {
log.error(`Encountered an error for snapshot: ${name}`, meta);
requestHeaders,
width,
meta
})
)));

// include a log resource for debugging
let logs = await log.query({ filter: ({ snapshot: s }) => s?.name === name });
resources.set('percy-logs', createLogResource(logs));

// log that the snapshot has been taken before uploading it
log.info(`Snapshot taken: ${name}`, meta);

// upload within the async snapshot queue
this.#snapshots.push(() => this.client.sendSnapshot({
name,
widths,
minimumHeight,
enableJavaScript,
clientInfo,
environmentInfo,
resources: Array.from(resources.values())
}).catch(error => {
log.error(`Encountered an error uploading snapshot: ${name}`, meta);
log.error(error);
}
}));
}).catch(error => {
log.error(`Encountered an error taking snapshot: ${name}`, meta);
log.error(error);
});
}

Expand All @@ -276,6 +277,7 @@ export default class Percy {
snapshots = name ? [{ name, execute }].concat(snapshots) : snapshots;
assert(snapshots.length && snapshots.every(s => s.name), `Missing name for ${url}`);

// the entire capture process happens within the async capture queue
return this.#captures.push(async () => {
let results = [];
let page;
Expand Down Expand Up @@ -315,7 +317,7 @@ export default class Percy {
log.error(`Encountered an error for page: ${url}`);
log.error(error);
} finally {
// awaiting on resulting snapshots syncs this task with those snapshot tasks
// await on any resulting snapshots
await Promise.all(results);
await page?.close();
}
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ export function createServerApp(percy) {
.get('/percy/dom.js', (_, res) => {
res.sendFile(require.resolve('@percy/dom'));
})
// snapshot requests are concurrent by default
// forward snapshot requests
.post('/percy/snapshot', asyncRoute(async (req, res) => {
let snapshot = percy.snapshot(req.body);
if (req.body.concurrent === false) await snapshot;
await percy.snapshot(req.body);
res.json({ success: true });
}))
// stops the instance
Expand Down
11 changes: 11 additions & 0 deletions packages/core/test/asset-discovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('Asset Discovery', () => {
domSnapshot: testDOM
});

await percy.idle();
let paths = server.requests.map(r => r.path);
// does not request the root url (serves domSnapshot instead)
expect(paths).not.toContain('/');
Expand Down Expand Up @@ -116,6 +117,7 @@ describe('Asset Discovery', () => {
domSnapshot: prefetchDOM
});

await percy.idle();
let paths = server.requests.map(r => r.path);
expect(paths).toContain('/style.css');

Expand Down Expand Up @@ -150,6 +152,7 @@ describe('Asset Discovery', () => {
domSnapshot: dataUrlDOM
});

await percy.idle();
expect(captured[0]).not.toEqual(expect.arrayContaining([
expect.objectContaining({
attributes: expect.objectContaining({
Expand All @@ -170,6 +173,7 @@ describe('Asset Discovery', () => {
domSnapshot: testDOM.replace('style.css', 'stylesheet.css')
});

await percy.idle();
let paths = server.requests.map(r => r.path);
expect(paths).toContain('/stylesheet.css');
expect(paths).toContain('/style.css');
Expand Down Expand Up @@ -199,6 +203,7 @@ describe('Asset Discovery', () => {
})
));

await percy.idle();
expect(captured[0]).toEqual([
expect.objectContaining({
attributes: expect.objectContaining({
Expand Down Expand Up @@ -299,6 +304,8 @@ describe('Asset Discovery', () => {
url: 'http://localhost:8000',
domSnapshot: testDOM
});

await percy.idle();
};

it('caches resource requests', async () => {
Expand Down Expand Up @@ -407,6 +414,7 @@ describe('Asset Discovery', () => {
domSnapshot: testExternalDOM
});

await percy.idle();
let paths = server.requests.map(r => r.path);
expect(paths).toContain('/style.css');
expect(paths).not.toContain('/img.gif');
Expand Down Expand Up @@ -450,6 +458,7 @@ describe('Asset Discovery', () => {
widths: [1000]
});

await percy.idle();
expect(captured[0][3]).toEqual(
expect.objectContaining({
attributes: expect.objectContaining({
Expand Down Expand Up @@ -477,6 +486,7 @@ describe('Asset Discovery', () => {
widths: [1000]
});

await percy.idle();
expect(captured[0][3]).toEqual(
expect.objectContaining({
attributes: expect.objectContaining({
Expand Down Expand Up @@ -504,6 +514,7 @@ describe('Asset Discovery', () => {
widths: [1000]
});

await percy.idle();
expect(captured[0]).toEqual([
expect.objectContaining({
attributes: expect.objectContaining({
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/percy-css.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('Percy CSS', () => {
domSnapshot: testDOM
});

await percy.idle();
let resources = mockAPI.requests['/builds/123/snapshots'][0]
.body.data.relationships.resources.data;

Expand All @@ -58,6 +59,7 @@ describe('Percy CSS', () => {
percyCSS: 'body { color: purple; }'
});

await percy.idle();
let resources = mockAPI.requests['/builds/123/snapshots'][0]
.body.data.relationships.resources.data;

Expand All @@ -73,6 +75,7 @@ describe('Percy CSS', () => {
percyCSS: 'p { font-size: 2rem; }'
});

await percy.idle();
let resources = mockAPI.requests['/builds/123/snapshots'][0]
.body.data.relationships.resources.data;

Expand Down
Loading

0 comments on commit 1271002

Please sign in to comment.