Skip to content

Commit

Permalink
Capture service worker intercepted requests (#1443)
Browse files Browse the repository at this point in the history
* feat: added new config discovery.captureServiceWorker

* feat: use captureServiceWorker config to capture original requests

* chore: lint fix

* test: added spec for captureServiceWorker

* chore: rename captureServiceWorker -> captureMockedServiceWorker
  • Loading branch information
nilshah98 authored Nov 28, 2023
1 parent 924a32f commit 3582916
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 4 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ export const configSchema = {
disableCache: {
type: 'boolean'
},
captureMockedServiceWorker: {
type: 'boolean',
default: false
},
requestHeaders: {
type: 'object',
normalize: false,
Expand Down Expand Up @@ -249,6 +253,7 @@ export const snapshotSchema = {
requestHeaders: { $ref: '/config/discovery#/properties/requestHeaders' },
authorization: { $ref: '/config/discovery#/properties/authorization' },
disableCache: { $ref: '/config/discovery#/properties/disableCache' },
captureMockedServiceWorker: { $ref: '/config/discovery#/properties/captureMockedServiceWorker' },
userAgent: { $ref: '/config/discovery#/properties/userAgent' },
devicePixelRatio: { $ref: '/config/discovery#/properties/devicePixelRatio' }
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function debugSnapshotOptions(snapshot) {
debugProp(snapshot, 'discovery.requestHeaders', JSON.stringify);
debugProp(snapshot, 'discovery.authorization', JSON.stringify);
debugProp(snapshot, 'discovery.disableCache');
debugProp(snapshot, 'discovery.captureMockedServiceWorker');
debugProp(snapshot, 'discovery.userAgent');
debugProp(snapshot, 'clientInfo');
debugProp(snapshot, 'environmentInfo');
Expand Down Expand Up @@ -288,6 +289,7 @@ export function createDiscoveryQueue(percy) {
requestHeaders: snapshot.discovery.requestHeaders,
authorization: snapshot.discovery.authorization,
userAgent: snapshot.discovery.userAgent,
captureMockedServiceWorker: snapshot.discovery.captureMockedServiceWorker,
meta: snapshot.meta,

// enable network inteception
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class Network {
this.timeout = options.networkIdleTimeout ?? 100;
this.authorization = options.authorization;
this.requestHeaders = options.requestHeaders ?? {};
this.captureMockedServiceWorker = options.captureMockedServiceWorker ?? false;
this.userAgent = options.userAgent ??
// by default, emulate a non-headless browser
page.session.browser.version.userAgent.replace('Headless', '');
Expand All @@ -54,7 +55,7 @@ export class Network {

let commands = [
session.send('Network.enable'),
session.send('Network.setBypassServiceWorker', { bypass: true }),
session.send('Network.setBypassServiceWorker', { bypass: !this.captureMockedServiceWorker }),
session.send('Network.setCacheDisabled', { cacheDisabled: true }),
session.send('Network.setUserAgentOverride', { userAgent: this.userAgent }),
session.send('Network.setExtraHTTPHeaders', { headers: this.requestHeaders })
Expand Down Expand Up @@ -178,13 +179,16 @@ export class Network {
// Called when a request will be sent. If the request has already been intercepted, handle it;
// otherwise set it to be pending until it is paused.
_handleRequestWillBeSent = async event => {
let { requestId, request } = event;
let { requestId, request, type } = event;

// do not handle data urls
if (request.url.startsWith('data:')) return;

if (this.intercept) {
this.#pending.set(requestId, event);
if (this.captureMockedServiceWorker) {
await this._handleRequest(undefined, { ...event, resourceType: type, interceptId: requestId }, true);
}
}
// release request
// note: we are releasing this, even if intercept is not set for network.js
Expand All @@ -195,7 +199,7 @@ export class Network {
// Called when a pending request is paused. Handles associating redirected requests with
// responses and calls this.onrequest with request info and callbacks to continue, respond,
// or abort a request. One of the callbacks is required to be called and only one.
_handleRequest = async (session, event) => {
_handleRequest = async (session, event, serviceWorker = false) => {
let { request, requestId, interceptId, resourceType } = event;
let redirectChain = [];

Expand All @@ -213,7 +217,9 @@ export class Network {
request.redirectChain = redirectChain;
this.#requests.set(requestId, request);

await sendResponseResource(this, request, session);
if (!serviceWorker) {
await sendResponseResource(this, request, session);
}
}

// Called when a response has been received for a specific request. Associates the response with
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function getSnapshotOptions(options, { config, meta }) {
requestHeaders: config.discovery.requestHeaders,
authorization: config.discovery.authorization,
disableCache: config.discovery.disableCache,
captureMockedServiceWorker: config.discovery.captureMockedServiceWorker,
userAgent: config.discovery.userAgent
}
}, options], (path, prev, next) => {
Expand Down
64 changes: 64 additions & 0 deletions packages/core/test/discovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1970,4 +1970,68 @@ describe('Discovery', () => {
});
});
});

describe('Service Worker =>', () => {
it('captures original request', async () => {
server.reply('/sw.js', () => [200, 'text/javascript', dedent`
const fetchUpstream = async(request) => {
return await fetch(request.clone());
}
self.addEventListener('fetch', (event) => {
const { request } = event
event.respondWith(fetchUpstream(request));
});
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
});
`]);

server.reply('/app.js', () => [200, 'text/javascript', dedent`
const registerServiceWorker = async () => {
await navigator.serviceWorker.register('sw.js',{ scope: './', });
};
await registerServiceWorker();
// create and insert image element which will be intercepted and resolved by service worker
// adding a sleep of 1s for service worker to get activated
await new Promise(r => setTimeout(r, 1000));
var img = document.createElement('img');
img.id = 'injected-image';
img.src = './img.gif';
document.getElementById('container').appendChild(img);
`]);

server.reply('/', () => [200, 'text/html', dedent`
<!DOCTYPE html><html><head></head><body>
<div id="container"></div>
<script type="module" src="app.js"></script>
</body></html>
`]);

await percy.snapshot({
name: 'first service worker snapshot',
url: 'http://localhost:8000',
waitForSelector: '#injected-image',
discovery: {
captureMockedServiceWorker: true
}
});

await percy.idle();

let paths = server.requests.map(r => r[0]);
expect(paths).toContain('/img.gif');
expect(captured).toContain(jasmine.arrayContaining([
jasmine.objectContaining({
id: sha256hash(pixel),
attributes: jasmine.objectContaining({
'resource-url': 'http://localhost:8000/img.gif'
})
})
]));
});
});
});
1 change: 1 addition & 0 deletions packages/core/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface DiscoveryOptions {
authorization?: AuthCredentials;
allowedHostnames?: string[];
disableCache?: boolean;
captureMockedServiceWorker?: boolean;
}

interface DiscoveryLaunchOptions {
Expand Down

0 comments on commit 3582916

Please sign in to comment.