Skip to content

Commit

Permalink
Update tests/mocks to work after cherry-pick
Browse files Browse the repository at this point in the history
  • Loading branch information
philipwalton committed Mar 2, 2019
1 parent 8b9dc50 commit d822ba9
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 168 deletions.
100 changes: 100 additions & 0 deletions infra/testing/sw-env-mocks/Body.js
@@ -0,0 +1,100 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/

const Blob = require('./Blob');


// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
// Note, the original post uses Uint16Array and allocates 2 bytes per character
// but native implementations of methods that convert strings to ArrayBuffers
// don't do that for ASCII strings, so we'll stick to str.length and only use
// ASCII in tests until we switch our test infrastructure to run in real
// browsers.
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}

function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}

// https://fetch.spec.whatwg.org/#body
class Body {
async arrayBuffer() {
if (this.bodyUsed) {
throw new TypeError('Already read');
} else {
this.bodyUsed = true;

if (typeof this._body === 'undefined') {
return new ArrayBuffer();
}
if (typeof this._body === 'string') {
return str2ab(this._body);
}
if (this._body instanceof ArrayBuffer) {
return this._body;
}
if (this._body instanceof Blob) {
// `_text` is non-standard, but easier for the mocks.
return str2ab(this._body._text);
}
}
}

async blob() {
if (this.bodyUsed) {
throw new TypeError('Already read');
} else {
this.bodyUsed = true;

if (typeof this._body === 'undefined') {
return new Blob();
}
if (typeof this._body === 'string') {
return new Blob([this._body]);
}
if (this._body instanceof ArrayBuffer) {
// `_text` is non-standard, but easier for the mocks.
return new Blob([ab2str(this._body)]);
}
if (this._body instanceof Blob) {
return this._body;
}
}
}

async text() {
if (this.bodyUsed) {
throw new TypeError('Already read');
} else {
this.bodyUsed = true;

if (typeof this._body === 'undefined') {
return new '';
}
if (typeof this._body === 'string') {
return this._body;
}
if (this._body instanceof ArrayBuffer) {
return ab2str(this._body);
}
if (this._body instanceof Blob) {
// `_text` is non-standard, but easier for the mocks.
return this._body._text;
}
}
}
}

module.exports = Body;
34 changes: 9 additions & 25 deletions infra/testing/sw-env-mocks/Request.js
Expand Up @@ -6,13 +6,16 @@
https://opensource.org/licenses/MIT.
*/

const Blob = require('./Blob');
const Body = require('./Body');
const Headers = require('./Headers');


// Stub missing/broken Request API methods in `service-worker-mock`.
// https://fetch.spec.whatwg.org/#request-class
class Request {
class Request extends Body {
constructor(urlOrRequest, options = {}) {
super();

let url = urlOrRequest;
if (urlOrRequest instanceof Request) {
url = urlOrRequest.url;
Expand All @@ -29,15 +32,15 @@ class Request {
throw new TypeError(`Invalid url: ${urlOrRequest}`);
}

this.url = url;
this.url = new URL(url, location).href;
this.method = options.method || 'GET';
this.mode = options.mode || 'cors';
// See https://fetch.spec.whatwg.org/#concept-request-credentials-mode
this.credentials = options.credentials || (this.mode === 'navigate' ?
'include' : 'omit');
this.credentials = options.credentials ||
(this.mode === 'navigate' ? 'include' : 'omit');
this.headers = new Headers(options.headers);

this._body = new Blob('body' in options ? [options.body] : []);
this._body = options.body;
}

clone() {
Expand All @@ -48,25 +51,6 @@ class Request {
return new Request(this.url, Object.assign({body: this._body}, this));
}
}

async blob() {
if (this.bodyUsed) {
throw new TypeError('Already read');
} else {
this.bodyUsed = true;
return this._body;
}
}

async text() {
if (this.bodyUsed) {
throw new TypeError('Already read');
} else {
this.bodyUsed = true;
// Limitation: this assumes the stored Blob is text-based.
return this._body._text;
}
}
}

module.exports = Request;
29 changes: 7 additions & 22 deletions infra/testing/sw-env-mocks/Response.js
Expand Up @@ -6,14 +6,18 @@
https://opensource.org/licenses/MIT.
*/

const Blob = require('./Blob');
const Body = require('./Body');
const Headers = require('./Headers');


// Stub missing/broken Response API methods in `service-worker-mock`.
// https://fetch.spec.whatwg.org/#response-class
class Response {
class Response extends Body {
constructor(body, options = {}) {
this._body = new Blob([body]);
super();

this._body = body;

this.status = typeof options.status === 'number' ? options.status : 200;
this.ok = this.status >= 200 && this.status < 300;
this.statusText = options.statusText || 'OK';
Expand All @@ -32,25 +36,6 @@ class Response {
return new Response(this._body, this);
}
}

async blob() {
if (this.bodyUsed) {
throw new TypeError('Already read');
} else {
this.bodyUsed = true;
return this._body;
}
}

async text() {
if (this.bodyUsed) {
throw new TypeError('Already read');
} else {
this.bodyUsed = true;
// Limitionation: this assumes the stored Blob is text-based.
return this._body._text;
}
}
}

module.exports = Response;
103 changes: 69 additions & 34 deletions test/workbox-background-sync/node/test-Queue.mjs
Expand Up @@ -12,7 +12,6 @@ import expectError from '../../../infra/testing/expectError';
import {Queue} from '../../../packages/workbox-background-sync/Queue.mjs';
import {QueueStore} from '../../../packages/workbox-background-sync/lib/QueueStore.mjs';
import {DBWrapper} from '../../../packages/workbox-core/_private/DBWrapper.mjs';
import {deleteDatabase} from '../../../packages/workbox-core/_private/deleteDatabase.mjs';


const MINUTES = 60 * 1000;
Expand All @@ -21,25 +20,58 @@ const getObjectStoreEntries = async () => {
return await new DBWrapper('workbox-background-sync', 3).getAll('requests');
};

const createSyncEvent = (tag) => {
const createSyncEventStub = (tag) => {
const event = new SyncEvent('sync', {tag});

// Safari doesn't recognize prototype methods when extending Event for
// some reason.
if (!event.waitUntil) {
event.waitUntil = SyncEvent.prototype.waitUntil;
// Default to resolving in the next microtask.
let done = Promise.resolve();

// Browsers will throw if code tries to call `waitUntil()` on a user-created
// sync event, so we have to stub it.
event.waitUntil = (promise) => {
// If `waitUntil` is called, defer `done` until after it resolves.
if (promise) {
done = promise.then(done);
}
};

return {event, done};
};

const clearIndexedDBEntries = async () => {
// Open a conection to the database (at whatever version exists) and
// clear out all object stores. This strategy is used because deleting
// databases inside service worker is flaky in FF and Safari.
// TODO(philipwalton): the version is not needed in real browsers, so it
// can be removed when we move away from running tests in node.
const db = await new DBWrapper('workbox-background-sync', 3).open();

// Edge cannot convert a DOMStringList to an array via `[...list]`.
for (const store of Array.from(db.db.objectStoreNames)) {
await db.clear(store);
}
return event;
await db.close();
};


describe(`Queue`, function() {
const sandbox = sinon.createSandbox();

beforeEach(async function() {
sandbox.restore();
Queue._queueNames.clear();
await deleteDatabase('workbox-background-sync');
await clearIndexedDBEntries();

// Don't actually register for a sync event in any test, as it could
// make the tests non-deterministic.
if ('sync' in registration) {
sandbox.stub(registration.sync, 'register');
}

sandbox.stub(Queue.prototype, 'replayRequests');
});

afterEach(function() {
sandbox.restore();
});

describe(`constructor`, function() {
Expand Down Expand Up @@ -67,29 +99,38 @@ describe(`Queue`, function() {
expect(self.addEventListener.calledOnce).to.be.true;
expect(self.addEventListener.calledWith('sync')).to.be.true;

self.dispatchEvent(createSyncEvent('workbox-background-sync:foo'));
const sync1 = createSyncEventStub('workbox-background-sync:foo');
self.dispatchEvent(sync1.event);
await sync1.done;

// replayRequests should not be called for this due to incorrect tag name
self.dispatchEvent(createSyncEvent('workbox-background-sync:bar'));
// `onSync` should not be called because the tag won't match.
const sync2 = createSyncEventStub('workbox-background-sync:bar');
self.dispatchEvent(sync2.event);
await sync2.done;

expect(onSync.callCount).to.equal(1);
expect(onSync.firstCall.args[0].queue).to.equal(queue);
});

it(`defaults to calling replayRequests when no onSync function is passed`, async function() {
sandbox.spy(self, 'addEventListener');
sandbox.stub(Queue.prototype, 'replayRequests');

const queue = new Queue('foo');

expect(self.addEventListener.calledOnce).to.be.true;
expect(self.addEventListener.calledWith('sync')).to.be.true;

self.dispatchEvent(createSyncEvent('workbox-background-sync:foo'));
const sync1 = createSyncEventStub('workbox-background-sync:foo');
self.dispatchEvent(sync1.event);
await sync1.done;

// replayRequests should not be called for this due to incorrect tag name
self.dispatchEvent(createSyncEvent('workbox-background-sync:bar'));
// `replayRequests` should not be called because the tag won't match.
const sync2 = createSyncEventStub('workbox-background-sync:bar');
self.dispatchEvent(sync2.event);
await sync2.done;

// `replayRequsets` is stubbed in beforeEach, so we don't have to
// re-stub in this test, and we can just assert it was called.
expect(Queue.prototype.replayRequests.callCount).to.equal(1);
expect(Queue.prototype.replayRequests.firstCall.args[0].queue)
.to.equal(queue);
Expand Down Expand Up @@ -385,8 +426,13 @@ describe(`Queue`, function() {
});

describe(`replayRequests`, function() {
beforeEach(function() {
// Unstub replayRequests for all tests in this group.
Queue.prototype.replayRequests.restore();
});

it(`should try to re-fetch all requests in the queue`, async function() {
sandbox.spy(self, 'fetch');
sandbox.stub(self, 'fetch');

const queue1 = new Queue('foo');
const queue2 = new Queue('bar');
Expand Down Expand Up @@ -522,32 +568,21 @@ describe(`Queue`, function() {
});

describe(`registerSync()`, function() {
it(`should support registerSync() in supporting browsers`, async function() {
const queue = new Queue('foo');
it(`should succeed regardless of browser support for sync`, async function() {
const queue = new Queue('a');
await queue.registerSync();
});

it(`should support registerSync() in non-supporting browsers`, async function() {
// Delete the SyncManager interface to mock a non-supporting browser.
const originalSyncManager = registration.sync;
delete registration.sync;

// We need to set the `onSync` function to a no-op, otherwise creating
// the Queue instance in a non-supporting browser will try to access
// IndexedDB and we don't have a way to await that completion.
const onSync = sandbox.spy();
const queue = new Queue('foo', {onSync});
await queue.registerSync();
it(`should handle thrown errors in sync registration`, async function() {
if (!('sync' in registration)) this.skip();

registration.sync = originalSyncManager;
});
registration.sync.register.restore();

it(`should handle thrown errors in sync registration`, async function() {
sandbox.stub(registration.sync, 'register').callsFake(() => {
return Promise.reject(new Error('Injected Error'));
});

const queue = new Queue('foo');
const queue = new Queue('a');
await queue.registerSync();
});
});
Expand Down

0 comments on commit d822ba9

Please sign in to comment.