Skip to content

Commit

Permalink
✨ Expose deferred uploads (#1095)
Browse files Browse the repository at this point in the history
* ✨ Make task processing automatically start the respective queue

* ✨ Allow flushing individual snapshots from internal queues

* ✨ Add core API endpoint to flush snapshots

* ✨ Add sdk-utils flush helper

* ✨ Expose deferUploads as core config option
  • Loading branch information
wwilsman authored Oct 5, 2022
1 parent 2ffaf87 commit 9082762
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 29 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export function createPercyServer(percy, port) {
if (!req.url.searchParams.has('async')) await snapshot;
return res.json(200, { success: true });
})
// flushes one or more snapshots from the internal queue
.route('post', '/percy/flush', async (req, res) => res.json(200, {
success: await percy.flush(req.body).then(() => true)
}))
// stops percy at the end of the current event loop
.route('/percy/stop', (req, res) => {
setImmediate(() => percy.stop());
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// Common config options used in Percy commands
export const configSchema = {
percy: {
type: 'object',
additionalProperties: false,
properties: {
deferUploads: {
type: 'boolean'
}
}
},
snapshot: {
type: 'object',
additionalProperties: false,
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/percy.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class Percy {
// implies `dryRun`, silent logs, and adds extra api endpoints
testing,
// configuration filepath
config,
config: configFile,
// provided to @percy/client
token,
clientInfo = '',
Expand All @@ -67,11 +67,14 @@ export class Percy {
// options which will become accessible via the `.config` property
...options
} = {}) {
this.config = PercyConfig.load({
let { percy, ...config } = PercyConfig.load({
overrides: options,
path: config
path: configFile
});

deferUploads ??= percy?.deferUploads;
this.config = config;

if (testing) loglevel = 'silent';
if (loglevel) this.loglevel(loglevel);

Expand Down Expand Up @@ -179,20 +182,24 @@ export class Percy {
}

// Wait for currently queued snapshots then run and wait for resulting uploads
async *flush(callback) {
async *flush(options) {
if (!this.readyState || this.readyState > 2) return;
let callback = typeof options === 'function' ? options : null;
options &&= !callback ? [].concat(options) : null;

// wait until the next event loop for synchronous snapshots
yield new Promise(r => setImmediate(r));

// flush and log progress for discovery before snapshots
if (!this.skipDiscovery && this.#discovery.size) {
yield* this.#discovery.flush(size => callback?.('Processing', size));
if (options) yield* yieldAll(options.map(o => this.#discovery.process(o)));
else yield* this.#discovery.flush(size => callback?.('Processing', size));
}

// flush and log progress for snapshot uploads
if (!this.skipUploads && this.#snapshots.size) {
yield* this.#snapshots.flush(size => callback?.('Uploading', size));
if (options) yield* yieldAll(options.map(o => this.#snapshots.process(o)));
else yield* this.#snapshots.flush(size => callback?.('Uploading', size));
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ export class Queue {
// process a single item task when started
process(item) {
let task = this.#find(item);
if (this.readyState) this.#process(task);
if (task && !this.#start) this.start();
this.#start?.promise.then(() => this.#process(task));
return task?.deferred;
}

Expand Down
14 changes: 14 additions & 0 deletions packages/core/test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ describe('API Server', () => {
].join(' ')]);
});

it('has a /flush endpoint that calls #flush()', async () => {
spyOn(percy, 'flush').and.resolveTo();
await percy.start();

await expectAsync(request('/percy/flush', {
body: { name: 'Snapshot name' },
method: 'post'
})).toBeResolvedTo({ success: true });

expect(percy.flush).toHaveBeenCalledWith({
name: 'Snapshot name'
});
});

it('has a /stop endpoint that calls #stop()', async () => {
spyOn(percy, 'stop').and.resolveTo();
await percy.start();
Expand Down
79 changes: 79 additions & 0 deletions packages/core/test/percy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,85 @@ describe('Percy', () => {
});
});

describe('#flush()', () => {
let snapshots;

beforeEach(async () => {
snapshots = [];

percy = await Percy.start({
token: 'PERCY_TOKEN',
snapshot: { widths: [1000] },
deferUploads: true
});

for (let i = 0; i < 3; i++) {
let resolve, deferred = new Promise(r => (resolve = r));
deferred = deferred.then(() => [200, 'text/html', `#${i}`]);
server.reply(`/deferred/${i}`, () => deferred);

let promise = percy.snapshot(`http://localhost:8000/deferred/${i}`);
snapshots.push({ resolve, deferred, promise });
}
});

afterEach(() => {
// no hanging promises
for (let { resolve } of snapshots) resolve();
});

it('resolves after flushing all snapshots', async () => {
let all = Promise.all(snapshots.map(s => s.promise));
let flush = percy.flush();

await expectAsync(flush).toBePending();
await expectAsync(all).toBePending();

snapshots[0].resolve();
snapshots[1].resolve();
await expectAsync(flush).toBePending();
await expectAsync(all).toBePending();

snapshots[2].resolve();
await expectAsync(flush).toBeResolved();
await expectAsync(all).toBeResolved();
});

it('resolves after flushing one or more named snapshots', async () => {
let flush1 = percy.flush(
{ name: '/deferred/1' }
);

await expectAsync(flush1).toBePending();
await expectAsync(snapshots[0].promise).toBePending();
await expectAsync(snapshots[1].promise).toBePending();
await expectAsync(snapshots[2].promise).toBePending();

snapshots[1].resolve();
await expectAsync(flush1).toBeResolved();
await expectAsync(snapshots[0].promise).toBePending();
await expectAsync(snapshots[1].promise).toBeResolved();
await expectAsync(snapshots[2].promise).toBePending();

let flush2 = percy.flush([
{ name: '/deferred/0' },
{ name: '/deferred/2' }
]);

snapshots[2].resolve();
await expectAsync(flush2).toBePending();
await expectAsync(snapshots[0].promise).toBePending();
await expectAsync(snapshots[1].promise).toBeResolved();
await expectAsync(snapshots[2].promise).toBeResolved();

snapshots[0].resolve();
await expectAsync(flush2).toBeResolved();
await expectAsync(snapshots[0].promise).toBeResolved();
await expectAsync(snapshots[1].promise).toBeResolved();
await expectAsync(snapshots[2].promise).toBeResolved();
});
});

describe('#upload()', () => {
it('errors when not running', async () => {
await percy.stop();
Expand Down
35 changes: 14 additions & 21 deletions packages/core/test/unit/queue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,38 +94,31 @@ describe('Unit / Tasks Queue', () => {
.toBeRejectedWithError('This operation was aborted');
});

it('can process any queued item once started', async () => {
it('can add a start handler that is called when starting', async () => {
let start = jasmine.createSpy('start');
q.handle('start', start);

expect(start).not.toHaveBeenCalled();
await q.start();
await q.start();
expect(start).toHaveBeenCalled();
});

it('can process any queued item after starting', async () => {
let p1 = q.push('item #1');
let p2 = q.push('item #2');
expect(q.size).toBe(2);

await expectAsync(p1).toBePending();
await expectAsync(p2).toBePending();

let p2$2 = q.process('item #2');
expect(q.readyState).toBe(0);
expect(q.size).toBe(2);
expect(p2$2).toBe(p2);

await expectAsync(p1).toBePending();
await expectAsync(p2).toBePending();

await q.start();
expect(q.size).toBe(2);
await q.process('item #2');
expect(q.size).toBe(1);

await expectAsync(p1).toBePending();
await expectAsync(p2).toBeResolved();
});

it('can add a start handler that is called when starting', async () => {
let start = jasmine.createSpy('start');
q.handle('start', start);

expect(start).not.toHaveBeenCalled();
await q.start();
await q.start();
expect(start).toHaveBeenCalled();
expect(q.readyState).toBe(1);
expect(q.size).toBe(1);
});

it('can add a task handler for processing queued items', async () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/sdk-utils/src/flush-snapshots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import percy from './percy-info.js';
import request from './request.js';

// Posts to the local Percy server one or more snapshots to flush. Given no arguments, all snapshots
// will be flushed. Does nothing when Percy is not enabled.
export async function flushSnapshots(options) {
if (percy.enabled) {
// accept one or more snapshot names
options &&= [].concat(options).map(o => (
typeof o === 'string' ? { name: o } : o
));

await request.post('/percy/flush', options);
}
}

export default flushSnapshots;
4 changes: 3 additions & 1 deletion packages/sdk-utils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import isPercyEnabled from './percy-enabled.js';
import waitForPercyIdle from './percy-idle.js';
import fetchPercyDOM from './percy-dom.js';
import postSnapshot from './post-snapshot.js';
import flushSnapshots from './flush-snapshots.js';

export {
logger,
Expand All @@ -13,7 +14,8 @@ export {
isPercyEnabled,
waitForPercyIdle,
fetchPercyDOM,
postSnapshot
postSnapshot,
flushSnapshots
};

// export the namespace by default
Expand Down
23 changes: 23 additions & 0 deletions packages/sdk-utils/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,29 @@ describe('SDK Utils', () => {
});
});

describe('flushSnapshots([options])', () => {
let { flushSnapshots } = utils;

it('does nothing when percy is not enabled', async () => {
await expectAsync(flushSnapshots()).toBeResolved();
await expectAsync(helpers.get('requests')).toBeResolvedTo({});
});

it('posts options to the CLI API flush endpoint', async () => {
utils.percy.enabled = true;

await expectAsync(flushSnapshots()).toBeResolved();
await expectAsync(flushSnapshots({ name: 'foo' })).toBeResolved();
await expectAsync(flushSnapshots(['bar', 'baz'])).toBeResolved();

await expectAsync(helpers.get('requests')).toBeResolvedTo([
{ url: '/percy/flush', method: 'POST' },
{ url: '/percy/flush', method: 'POST', body: [{ name: 'foo' }] },
{ url: '/percy/flush', method: 'POST', body: [{ name: 'bar' }, { name: 'baz' }] }
]);
});
});

describe('logger()', () => {
let browser = process.env.__PERCY_BROWSERIFIED__;
let log, err, stdout, stderr;
Expand Down

0 comments on commit 9082762

Please sign in to comment.