Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture service worker intercepted requests #1443

Merged
Merged
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 @@
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 @@

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 @@ -170,7 +171,7 @@
this.#pending.delete(requestId);

// guard against redirects with the same requestId
pending?.request.url === event.request.url &&

Check warning on line 174 in packages/core/src/network.js

View workflow job for this annotation

GitHub Actions / Lint

Expected an assignment or function call and instead saw an expression
pending.request.method === event.request.method &&
await this._handleRequest(session, { ...pending, resourceType, interceptId });
}
Expand All @@ -178,13 +179,16 @@
// 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 @@
// 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 @@
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();
nilshah98 marked this conversation as resolved.
Show resolved Hide resolved

// 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