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

Auto capture and insert cookie in asset discovery browser #1656

Merged
merged 8 commits into from
Jul 19, 2024
10 changes: 9 additions & 1 deletion packages/core/src/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ async function* captureSnapshotResources(page, snapshot, options) {
const log = logger('core:discovery');
let { discovery, additionalSnapshots = [], ...baseSnapshot } = snapshot;
let { capture, captureWidths, deviceScaleFactor, mobile, captureForDevices } = options;
let cookies;
if (process.env.PERCY_DO_NOT_USE_CAPTURED_COOKIES !== 'true') {
cookies = snapshot?.domSnapshot?.cookies;
}
if (typeof cookies === 'string' && cookies !== '') {
cookies = cookies.split('; ').map(c => c.split('='));
cookies = cookies.map(([key, value]) => { return { name: key, value: value }; });
}

// used to take snapshots and remove any discovered root resource
let takeSnapshot = async (options, width) => {
Expand All @@ -177,7 +185,7 @@ async function* captureSnapshotResources(page, snapshot, options) {

// navigate to the url
yield resizePage(snapshot.widths[0]);
yield page.goto(snapshot.url);
yield page.goto(snapshot.url, { cookies });

if (snapshot.execute) {
// when any execute options are provided, inject snapshot options
Expand Down
27 changes: 24 additions & 3 deletions packages/core/src/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,39 @@ export class Page {
});
}

mergeCookies(userPassedCookie, autoCapturedCookie) {
if (!autoCapturedCookie) return userPassedCookie;
if (userPassedCookie.length === 0) return autoCapturedCookie;

// User passed cookie will be prioritized over auto captured cookie
const mergedCookies = [...userPassedCookie, ...autoCapturedCookie];
const uniqueCookies = [];
const names = new Set();

for (const cookie of mergedCookies) {
if (!names.has(cookie.name)) {
uniqueCookies.push(cookie);
names.add(cookie.name);
}
}

return uniqueCookies;
}

// Go to a URL and wait for navigation to occur
async goto(url, { waitUntil = 'load' } = {}) {
async goto(url, { waitUntil = 'load', cookies } = {}) {
this.log.debug(`Navigate to: ${url}`, this.meta);

let navigate = async () => {
const userPassedCookie = this.session.browser.cookies;
// set cookies before navigation so we can default the domain to this hostname
if (this.session.browser.cookies.length) {
if (userPassedCookie.length || cookies) {
let defaultDomain = hostname(url);
cookies = this.mergeCookies(userPassedCookie, cookies);

await this.session.send('Network.setCookies', {
// spread is used to make a shallow copy of the cookie
cookies: this.session.browser.cookies.map(({ ...cookie }) => {
cookies: cookies.map(({ ...cookie }) => {
if (!cookie.url) cookie.domain ||= defaultDomain;
return cookie;
})
Expand Down
103 changes: 103 additions & 0 deletions packages/core/test/discovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,109 @@ describe('Discovery', () => {

expect(cookie).toEqual('chocolate=654321');
});

it('should merge cookie passed by user', async () => {
// test cookie array
await startWithCookies([{
name: 'test-cookie',
value: '654321'
}, {
name: 'shortbread',
value: '987654'
}]);

await percy.snapshot({
name: 'mmm cookies',
url: 'http://localhost:8000',
domSnapshot: {
html: testDOM,
cookies: 'test-cookie=value; cookie-name=cookie-value'
}
});

expect(logger.stdout).toEqual(jasmine.arrayContaining([
'[percy] Snapshot taken: mmm cookies'
]));

expect(cookie).toEqual('test-cookie=654321; shortbread=987654; cookie-name=cookie-value');
});

it('can send default collected cookies from serialization', async () => {
await percy.stop();

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

await percy.snapshot({
name: 'mmm cookies',
url: 'http://localhost:8000',
domSnapshot: {
html: testDOM,
cookies: 'test-cookie=value'
}
});

expect(logger.stdout).toEqual(jasmine.arrayContaining([
'[percy] Snapshot taken: mmm cookies'
]));

expect(cookie).toEqual('test-cookie=value');
});

it('does not use cookie if empty cookies is passed (in case of httponly)', async () => {
await percy.stop();

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

await percy.snapshot({
name: 'mmm cookies',
url: 'http://localhost:8000',
domSnapshot: {
html: testDOM,
cookies: ''
}
});

expect(logger.stdout).toEqual(jasmine.arrayContaining([
'[percy] Snapshot taken: mmm cookies'
]));

expect(cookie).toEqual(undefined);
});

it('should not use captured cookie when PERCY_DO_NOT_USE_CAPTURED_COOKIES is set', async () => {
process.env.PERCY_DO_NOT_USE_CAPTURED_COOKIES = true;
await percy.stop();

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

await percy.snapshot({
name: 'mmm cookies',
url: 'http://localhost:8000',
domSnapshot: {
html: testDOM,
cookies: 'test-cookie=value'
}
});

expect(logger.stdout).toEqual(jasmine.arrayContaining([
'[percy] Snapshot taken: mmm cookies'
]));

expect(cookie).toEqual(undefined);
delete process.env.PERCY_DO_NOT_USE_CAPTURED_COOKIES;
});
});

describe('protected resources', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/dom/src/serialize-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export function serializeDOM(options) {

let result = {
html: serializeHTML(ctx),
cookies: dom.cookie,
warnings: Array.from(ctx.warnings),
resources: Array.from(ctx.resources),
hints: Array.from(ctx.hints)
Expand Down
1 change: 1 addition & 0 deletions packages/dom/test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function withExample(html, options = { withShadow: true, withRestrictedSh
p.innerText = 'P tag outside body';
document.documentElement.append(p);
}
document.cookie = 'test-cokkie=test-value';
return document;
}

Expand Down
8 changes: 7 additions & 1 deletion packages/dom/test/serialize-dom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ describe('serializeDOM', () => {
it('returns serialied html, warnings, and resources', () => {
expect(serializeDOM()).toEqual({
html: jasmine.any(String),
cookies: jasmine.any(String),
warnings: jasmine.any(Array),
resources: jasmine.any(Array),
hints: jasmine.any(Array)
Expand All @@ -28,7 +29,7 @@ describe('serializeDOM', () => {

it('optionally returns a stringified response', () => {
expect(serializeDOM({ stringifyResponse: true }))
.toMatch('{"html":".*","warnings":\\[\\],"resources":\\[\\],"hints":\\[\\]}');
.toMatch('{"html":".*","cookies":".*","warnings":\\[\\],"resources":\\[\\],"hints":\\[\\]}');
});

it('always has a doctype', () => {
Expand Down Expand Up @@ -76,6 +77,11 @@ describe('serializeDOM', () => {
expect(result.html).not.toContain('loading="lazy"');
});

it('collects cookies', () => {
const result = serializeDOM();
expect(result.cookies).toContain('test-cokkie=test-value');
});

it('clone node is always shallow', () => {
class AttributeCallbackTestElement extends window.HTMLElement {
static get observedAttributes() {
Expand Down
Loading