service-worker-mock
Advanced tools
Comparing version 1.12.0 to 2.0.0
@@ -1,1 +0,8 @@ | ||
module.exports = () => ({ fetch: Promise.resolve() }); | ||
module.exports = async (request) => { | ||
const response = new Response('Response from service-worker-mock/fetch.js', { | ||
status: 200, | ||
statusText: 'ok.' | ||
}); | ||
response.url = request.url; | ||
return response; | ||
}; |
84
index.js
@@ -5,2 +5,10 @@ // If the WHATWG URL implementation is available via the first-party `url` | ||
const URL = require('url').URL || require('dom-urls'); | ||
const { | ||
IDBFactory, | ||
IDBKeyRange, | ||
IDBDatabase, | ||
IDBObjectStore, | ||
reset: resetIDB | ||
} = require('shelving-mock-indexeddb'); | ||
const { Performance } = require('w3c-hr-time'); | ||
@@ -14,5 +22,12 @@ const Blob = require('./models/Blob'); | ||
const Clients = require('./models/Clients'); | ||
const DOMException = require('./models/DOMException'); | ||
const ExtendableEvent = require('./models/ExtendableEvent'); | ||
const ExtendableMessageEvent = require('./models/ExtendableMessageEvent'); | ||
const Event = require('./models/Event'); | ||
const EventTarget = require('./models/EventTarget'); | ||
const FetchEvent = require('./models/FetchEvent'); | ||
const Headers = require('./models/Headers'); | ||
const MessageEvent = require('./models/MessageEvent'); | ||
const MessageChannel = require('./models/MessageChannel'); | ||
const MessagePort = require('./models/MessagePort'); | ||
const Notification = require('./models/Notification'); | ||
@@ -26,3 +41,6 @@ const NotificationEvent = require('./models/NotificationEvent'); | ||
const ServiceWorkerRegistration = require('./models/ServiceWorkerRegistration'); | ||
const MessageEvent = require('./models/MessageEvent'); | ||
const SyncEvent = require('./models/SyncEvent'); | ||
const URLSearchParams = require('url-search-params'); | ||
const BroadcastChannel = require('./models/BroadcastChannel'); | ||
const FileReader = require('./models/FileReader'); | ||
@@ -32,12 +50,11 @@ const eventHandler = require('./utils/eventHandler'); | ||
const defaults = (envOptions) => Object.assign({ | ||
locationUrl: 'https://www.test.com' | ||
locationUrl: 'https://www.test.com/sw.js', | ||
userAgent: 'Mock User Agent', | ||
useRawRequestUrl: false | ||
}, envOptions); | ||
const makeListenersWithReset = () => { | ||
const listeners = {}; | ||
const makeListenersWithReset = (listeners, resetEventListeners) => { | ||
Object.defineProperty(listeners, 'reset', { | ||
enumerable: false, | ||
value: () => { | ||
self.listeners = makeListenersWithReset(); | ||
} | ||
value: resetEventListeners | ||
}); | ||
@@ -47,6 +64,15 @@ return listeners; | ||
class ServiceWorkerGlobalScope { | ||
class ServiceWorkerGlobalScope extends EventTarget { | ||
constructor(envOptions) { | ||
super(); | ||
const options = defaults(envOptions); | ||
this.listeners = makeListenersWithReset(); | ||
// For backwards compatibility, resetting global scope listeners | ||
// will reset ExtenableEvents as well | ||
this.listeners = makeListenersWithReset(this.listeners, () => { | ||
this.resetEventListeners(); | ||
ExtendableEvent._allExtendableEvents.clear(); | ||
}); | ||
this.useRawRequestUrl = options.useRawRequestUrl; | ||
this.location = new URL(options.locationUrl, options.locationBase); | ||
@@ -61,9 +87,22 @@ this.skipWaiting = () => Promise.resolve(); | ||
this.Body = Body; | ||
this.BroadcastChannel = BroadcastChannel; | ||
this.Cache = Cache; | ||
this.Client = Client; | ||
this.WindowClient = WindowClient; | ||
this.Event = ExtendableEvent; | ||
this.DOMException = DOMException; | ||
this.Event = Event; | ||
this.EventTarget = EventTarget; | ||
this.ExtendableEvent = ExtendableEvent; | ||
this.ExtendableMessageEvent = ExtendableMessageEvent; | ||
this.FetchEvent = FetchEvent; | ||
this.FileReader = FileReader; | ||
this.Headers = Headers; | ||
this.importScripts = () => {}; | ||
this.indexedDB = new IDBFactory(); | ||
this.IDBKeyRange = IDBKeyRange; | ||
this.IDBDatabase = IDBDatabase; | ||
this.IDBObjectStore = IDBObjectStore; | ||
this.resetIDB = resetIDB; | ||
this.MessageEvent = MessageEvent; | ||
this.MessageChannel = MessageChannel; | ||
this.MessagePort = MessagePort; | ||
this.Notification = Notification; | ||
@@ -74,20 +113,21 @@ this.NotificationEvent = NotificationEvent; | ||
this.PushSubscription = PushSubscription; | ||
this.performance = new Performance(); | ||
this.Request = Request; | ||
this.Response = Response; | ||
this.SyncEvent = SyncEvent; | ||
this.ServiceWorkerGlobalScope = ServiceWorkerGlobalScope; | ||
this.URL = URL; | ||
this.MessageEvent = MessageEvent; | ||
this.URLSearchParams = URLSearchParams; | ||
this.navigator = {}; | ||
this.navigator.userAgent = options.userAgent; | ||
// Instance variable to avoid issues with `this` | ||
this.addEventListener = (name, callback) => { | ||
if (!this.listeners[name]) { | ||
this.listeners[name] = []; | ||
} | ||
this.listeners[name].push(callback); | ||
}; | ||
this.WindowClient = WindowClient; | ||
// Instance variable to avoid issues with `this` | ||
this.trigger = (name, args) => { | ||
if (this.listeners[name]) { | ||
return eventHandler(name, args, this.listeners[name]); | ||
if (this.listeners.has(name)) { | ||
return eventHandler( | ||
name, | ||
args, | ||
Array.from(this.listeners.get(name).values()) | ||
); | ||
} | ||
@@ -94,0 +134,0 @@ return Promise.resolve(); |
@@ -0,9 +1,11 @@ | ||
// Blob | ||
// https://w3c.github.io/FileAPI/#dom-blob-blob | ||
class Blob { | ||
constructor(parts, options) { | ||
if (parts && !Array.isArray(parts)) { | ||
constructor(parts = [], options = {}) { | ||
if (!Array.isArray(parts)) { | ||
throw new TypeError('Blob requires an array'); | ||
} | ||
this.parts = parts || []; | ||
this.type = (options && options.type) || ''; | ||
this.parts = parts; | ||
this.type = options.type || ''; | ||
} | ||
@@ -13,13 +15,26 @@ | ||
return this.parts.reduce((size, part) => { | ||
return size + (part.length || part.size); | ||
return size + (part instanceof Blob ? part.size : String(part).length); | ||
}, 0); | ||
} | ||
get text() { | ||
// Warning: non-standard, but used in other mocks for simplicity. | ||
get _text() { | ||
return this.parts.reduce((text, part) => { | ||
return text + (typeof part === 'string' ? part : part.text); | ||
return text + (part instanceof Blob ? part._text : String(part)); | ||
}, ''); | ||
} | ||
clone() { | ||
return new Blob(this.parts.slice(), { | ||
type: this.type | ||
}); | ||
} | ||
slice(start, end, type) { | ||
const bodyString = this._text; | ||
const slicedBodyString = bodyString.substring(start, end); | ||
return new Blob([slicedBodyString], { type }); | ||
} | ||
} | ||
module.exports = Blob; |
@@ -10,3 +10,3 @@ const Blob = require('./Blob'); | ||
this.bodyUsed = false; | ||
this.body = body === null || body instanceof Blob ? body : new Blob([body]); | ||
this.body = body === null || body instanceof Blob ? body : new Blob([].concat(body)); | ||
} | ||
@@ -18,11 +18,11 @@ arrayBuffer() { | ||
blob() { | ||
return this.resolve('blob', body => new Blob([body])); | ||
return this.resolve('blob', body => body); | ||
} | ||
json() { | ||
return this.resolve('json', body => JSON.parse(body.text)); | ||
return this.resolve('json', body => JSON.parse(body._text)); | ||
} | ||
text() { | ||
return this.resolve('text', body => body.text); | ||
return this.resolve('text', body => body._text); | ||
} | ||
@@ -29,0 +29,0 @@ |
@@ -41,3 +41,3 @@ // https://developer.mozilla.org/en-US/docs/Web/API/Cache | ||
request = new Request(request); | ||
// Add relative url as well | ||
// Add relative url as well (non-standard) | ||
this.store.set(relativeUrl, { request, response }); | ||
@@ -55,4 +55,29 @@ } | ||
keys() { | ||
// https://w3c.github.io/ServiceWorker/#dom-cache-keys | ||
keys(request, options = {}) { | ||
let req = null; | ||
if (request instanceof Request) { | ||
req = request; | ||
if (request.method !== 'GET' && !options.ignoreMethod) { | ||
return Promise.resolve([]); | ||
} | ||
} else if (typeof request === 'string') { | ||
try { | ||
req = new Request(request); | ||
} catch (err) { | ||
return Promise.reject(err); | ||
} | ||
} | ||
const values = Array.from(this.store.values()); | ||
if (req) { | ||
return Promise.resolve(values | ||
.filter((value) => { | ||
return value.request.url === req.url; | ||
}) | ||
.map((value) => value.request) | ||
); | ||
} | ||
return Promise.resolve(values.map((value) => value.request)); | ||
@@ -59,0 +84,0 @@ } |
@@ -9,4 +9,10 @@ const Cache = require('./Cache'); | ||
match(request) { | ||
async match(request, options = {}) { | ||
const url = request.url || request; | ||
if (options.cacheName) { | ||
const cache = await this.open(options.cacheName); | ||
return cache.match(request); | ||
} | ||
const keys = Object.keys(this.caches); | ||
@@ -19,3 +25,3 @@ for (let i = 0; i < keys.length; i += 1) { | ||
} | ||
return Promise.resolve(null); | ||
return null; | ||
} | ||
@@ -22,0 +28,0 @@ |
const generateRandomId = require('../utils/generateRandomId'); | ||
const EventTarget = require('./EventTarget'); | ||
const MessageEvent = require('./MessageEvent'); | ||
const MessagePort = require('./MessagePort'); | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Client | ||
class Client { | ||
class Client extends EventTarget { | ||
constructor(url, type, frameType) { | ||
super(); | ||
this.id = generateRandomId(); | ||
@@ -12,4 +17,10 @@ this.url = url; | ||
postMessage() { | ||
throw new Error('METHOD NOT IMPLEMENTED'); | ||
// TODO: Implement Transferable | ||
postMessage(message, transfer = []) { | ||
const ports = transfer.filter(objOrPort => (objOrPort instanceof MessagePort)); | ||
const event = new MessageEvent('message', { | ||
data: message, | ||
ports | ||
}); | ||
this.dispatchEvent(event); | ||
} | ||
@@ -16,0 +27,0 @@ |
@@ -0,13 +1,47 @@ | ||
// Derived from https://github.com/GoogleChrome/workbox | ||
const Event = require('./Event'); | ||
// ExtendableEvent | ||
// https://www.w3.org/TR/service-workers-1/#extendable-event | ||
class ExtendableEvent { | ||
constructor() { | ||
this.promise = null; | ||
class ExtendableEvent extends Event { | ||
constructor(...args) { | ||
super(...args); | ||
this.response = null; | ||
// https://www.w3.org/TR/service-workers-1/#dfn-extend-lifetime-promises | ||
this._extendLifetimePromises = new Set(); | ||
// Used to keep track of all ExtendableEvent instances. | ||
_allExtendableEvents.add(this); | ||
} | ||
waitUntil(promise) { | ||
this.promise = promise; | ||
this._extendLifetimePromises.add(promise); | ||
} | ||
} | ||
// WORKBOX TODO: if workbox wants to use service-worker-mocks only, | ||
// it needs to migrate at https://github.com/GoogleChrome/workbox/blob/912080a1bf3255c61151ca3d0ebd0895aaf377e2/test/workbox-google-analytics/node/test-index.mjs#L19 | ||
// and import `eventsDoneWaiting` from the `ExtendableEvent` | ||
let _allExtendableEvents = new Set(); | ||
ExtendableEvent._allExtendableEvents = _allExtendableEvents; | ||
ExtendableEvent.eventsDoneWaiting = () => { | ||
const allExtendLifetimePromises = []; | ||
// Create a single list of _extendLifetimePromises values in all events. | ||
// Also add `catch` handlers to each promise so all of them are run, rather | ||
// that the normal behavior `Promise.all` erroring at the first error. | ||
for (const event of _allExtendableEvents) { | ||
const extendLifetimePromisesOrErrors = [...event._extendLifetimePromises] | ||
.map((promise) => promise.catch((err) => err)); | ||
allExtendLifetimePromises.push(...extendLifetimePromisesOrErrors); | ||
} | ||
return Promise.all(allExtendLifetimePromises); | ||
}; | ||
module.exports = ExtendableEvent; |
@@ -0,9 +1,17 @@ | ||
// Derived from https://github.com/GoogleChrome/workbox | ||
const ExtendableEvent = require('./ExtendableEvent'); | ||
const Request = require('./Request'); | ||
// FetchEvent | ||
// https://www.w3.org/TR/service-workers-1/#fetch-event-section | ||
class FetchEvent extends ExtendableEvent { | ||
constructor(type, init) { | ||
super(); | ||
this.type = type; | ||
this.isReload = init.isReload || false; | ||
constructor(type, init = {}) { | ||
super(type, init); | ||
if (!init.request) { | ||
throw new TypeError('Failed to construct \'FetchEvent\': ' + | ||
'Missing required member(s): request.'); | ||
} | ||
if (init.request instanceof Request) { | ||
@@ -14,5 +22,15 @@ this.request = init.request; | ||
} | ||
this.clientId = init.clientId || null; | ||
this.isReload = init.isReload || false; | ||
this._respondWithEnteredFlag = false; | ||
} | ||
respondWith(response) { | ||
this.promise = response; | ||
respondWith(promise) { | ||
if (this._respondWithEnteredFlag) { | ||
throw new DOMException('Failed to execute \'respondWith\' on ' + | ||
'\'FetchEvent\': The event has already been responded to.'); | ||
} | ||
this._respondWithEnteredFlag = true; | ||
this._extendLifetimePromises.add(promise); | ||
} | ||
@@ -19,0 +37,0 @@ } |
@@ -5,6 +5,15 @@ // stubs https://developer.mozilla.org/en-US/docs/Web/API/Headers | ||
constructor(meta) { | ||
if (typeof meta === 'undefined') { | ||
// https://github.com/GoogleChrome/workbox/issues/1461 | ||
console.warn('Constructing headers with an undefined argument fails in ' | ||
+ 'Chrome <= 56 and Samsung Internet ~6.4. You should use `new Headers({})`.' | ||
); | ||
} | ||
if (meta && meta instanceof Headers) { | ||
this._map = new Map(meta._map); | ||
} else if (meta && typeof meta === 'object') { | ||
this._map = new Map(Object.entries(meta)); | ||
this._map = new Map(Object.entries(meta) | ||
.map(entry => [entry[0].toLowerCase(), entry[1]]) | ||
); | ||
} else { | ||
@@ -11,0 +20,0 @@ this._map = new Map(); |
@@ -13,3 +13,3 @@ const ExtendableEvent = require('./ExtendableEvent'); | ||
constructor(type, init) { | ||
super(); | ||
super(type); | ||
Object.assign(this, defaults(), init); | ||
@@ -16,0 +16,0 @@ } |
// stubs https://developer.mozilla.org/en-US/docs/Web/API/Request | ||
const Body = require('./Body'); | ||
const Headers = require('./Headers'); | ||
const URL = require('dom-urls'); | ||
const URL = require('url').URL || require('dom-urls'); | ||
@@ -16,10 +16,40 @@ | ||
class Request extends Body { | ||
constructor(url, options) { | ||
super(options ? options.body : undefined, options); | ||
this.url = ((url instanceof URL) ? url : new URL(url, self.location.href)).href; | ||
this.method = (options && options.method) || 'GET'; | ||
this.mode = (options && options.mode) || 'same-origin'; // FF defaults to cors | ||
constructor(urlOrRequest, options = {}) { | ||
let url = urlOrRequest; | ||
if (urlOrRequest instanceof Request) { | ||
url = urlOrRequest.url; | ||
options = Object.assign({}, { | ||
body: urlOrRequest.body, | ||
credentials: urlOrRequest.credentials, | ||
headers: urlOrRequest.headers, | ||
method: urlOrRequest.method, | ||
mode: urlOrRequest.mode | ||
}, options); | ||
} else if (typeof url === 'string' && url.length === 0) { | ||
url = '/'; | ||
} | ||
if (!url) { | ||
throw new TypeError(`Invalid url: ${urlOrRequest}`); | ||
} | ||
super(options.body, options); | ||
if (url instanceof URL) { | ||
this.url = url.href; | ||
} else if (self.useRawRequestUrl) { | ||
this.url = url; | ||
} else { | ||
this.url = new URL(url, self.location.href).href; | ||
} | ||
this.method = options.method || 'GET'; | ||
this.mode = options.mode || 'same-origin'; // FF defaults to cors | ||
// See https://fetch.spec.whatwg.org/#concept-request-credentials-mode | ||
this.credentials = options.credentials || (this.mode === 'navigate' | ||
? 'include' | ||
: 'omit'); | ||
// Transform options.headers to Headers object | ||
if (options && options.headers) { | ||
if (options.headers) { | ||
if (options.headers instanceof Headers) { | ||
@@ -38,7 +68,11 @@ this.headers = options.headers; | ||
clone() { | ||
if (this.bodyUsed) throwBodyUsed(); | ||
if (this.bodyUsed) { | ||
throwBodyUsed(); | ||
} | ||
return new Request(this.url, { | ||
method: this.method, | ||
mode: this.mode, | ||
headers: this.headers | ||
headers: this.headers, | ||
body: this.body ? this.body.clone() : this.body | ||
}); | ||
@@ -45,0 +79,0 @@ } |
// stubs https://developer.mozilla.org/en-US/docs/Web/API/Response | ||
const Body = require('./Body'); | ||
const Headers = require('./Headers'); | ||
@@ -10,15 +11,29 @@ const isSupportedBodyType = (body) => | ||
class Response extends Body { | ||
constructor(body = null, init) { | ||
constructor(body = null, options = {}) { | ||
if (!isSupportedBodyType(body)) { | ||
throw new TypeError('Response body must be one of: Blob, USVString, null'); | ||
} | ||
super(body); | ||
this.status = (init && typeof init.status === 'number') ? init.status : 200; | ||
super(body, options); | ||
this.status = typeof options.status === 'number' | ||
? options.status | ||
: 200; | ||
this.ok = this.status >= 200 && this.status < 300; | ||
this.statusText = (init && init.statusText) || 'OK'; | ||
this.headers = (init && init.headers); | ||
this.statusText = options.statusText || 'OK'; | ||
if (options.headers) { | ||
if (options.headers instanceof Headers) { | ||
this.headers = options.headers; | ||
} else if (typeof options.headers === 'object') { | ||
this.headers = new Headers(options.headers); | ||
} else { | ||
throw new TypeError('Cannot construct response.headers: invalid data'); | ||
} | ||
} else { | ||
this.headers = new Headers({}); | ||
} | ||
this.type = this.status === 0 ? 'opaque' : 'basic'; | ||
this.redirected = false; | ||
this.url = (init && init.url) || 'http://example.com/asset'; | ||
this.url = options.url || 'http://example.com/asset'; | ||
this.method = options.method || 'GET'; | ||
} | ||
@@ -25,0 +40,0 @@ |
@@ -5,6 +5,10 @@ const PushManager = require('./PushManager'); | ||
const NotificationEvent = require('./NotificationEvent'); | ||
const SyncManager = require('./SyncManager'); | ||
const EventTarget = require('./EventTarget'); | ||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration | ||
class ServiceWorkerRegistration { | ||
class ServiceWorkerRegistration extends EventTarget { | ||
constructor() { | ||
super(); | ||
this.active = null; | ||
@@ -15,2 +19,3 @@ this.installing = null; | ||
this.navigationPreload = new NavigationPreloadManager(); | ||
this.sync = new SyncManager(); | ||
this.scope = '/'; | ||
@@ -17,0 +22,0 @@ this.waiting = null; |
{ | ||
"name": "service-worker-mock", | ||
"version": "1.12.0", | ||
"version": "2.0.0", | ||
"main": "index.js", | ||
@@ -26,4 +26,7 @@ "repository": { | ||
"dependencies": { | ||
"dom-urls": "^1.1.0" | ||
"dom-urls": "^1.1.0", | ||
"shelving-mock-indexeddb": "github:philipwalton/shelving-mock-indexeddb#621b7a275568051846d20e3e557fa1101418b1d1", | ||
"url-search-params": "^0.10.0", | ||
"w3c-hr-time": "^1.0.1" | ||
} | ||
} |
@@ -26,3 +26,3 @@ const ExtendableEvent = require('../models/ExtendableEvent'); | ||
callback(event); | ||
return Promise.resolve(event.promise); | ||
return Promise.all(Array.from(event._extendLifetimePromises.values())); | ||
} | ||
@@ -29,0 +29,0 @@ |
GitHub dependency
Supply chain riskContains a dependency which resolves to a GitHub URL. Dependencies fetched from GitHub specifiers are not immutable can be used to inject untrusted code or reduce the likelihood of a reproducible install.
Found 1 instance in 1 package
38036
37
1052
4
1
+ Addedurl-search-params@^0.10.0
+ Addedw3c-hr-time@^1.0.1
+ Addedbrowser-process-hrtime@1.0.0(transitive)
+ Addedurl-search-params@0.10.2(transitive)
+ Addedw3c-hr-time@1.0.2(transitive)