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

docs(auth): running Lighthouse on authenticated pages #9628

Merged
merged 60 commits into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
aecfd9f
init
connorjclark Aug 30, 2019
6aa9c08
start doc
connorjclark Aug 30, 2019
69adf71
update
connorjclark Aug 30, 2019
5757985
update
connorjclark Aug 30, 2019
0ae9040
update
connorjclark Aug 30, 2019
5469beb
lint
connorjclark Aug 30, 2019
59aa12f
fix test
connorjclark Aug 30, 2019
eadc8e0
cdt
connorjclark Aug 30, 2019
6ac8d37
Update README.md
connorjclark Aug 30, 2019
f268134
Update docs/auth/README.md
connorjclark Aug 30, 2019
06dd0b0
changes
connorjclark Aug 30, 2019
5e460a5
changes
connorjclark Aug 30, 2019
b54a4ac
changes
connorjclark Aug 30, 2019
ae5f2e5
just run seo
connorjclark Aug 30, 2019
d9feca1
link changes, optional
connorjclark Sep 4, 2019
3d171bf
Update docs/auth/README.md
connorjclark Sep 4, 2019
60cef1d
Merge branch 'auth-docs' of github.com:GoogleChrome/lighthouse into a…
connorjclark Sep 4, 2019
d7b0688
-we
connorjclark Sep 4, 2019
a3425e7
-json middleware
connorjclark Sep 4, 2019
e36b5e1
CHROME_DEBUG_PORT
connorjclark Sep 4, 2019
25ff0f9
await server stuff
connorjclark Sep 4, 2019
ec7f7bb
$eval
connorjclark Sep 4, 2019
836e64a
move
connorjclark Sep 5, 2019
915ad58
fix
connorjclark Sep 5, 2019
6a20cb1
mv
connorjclark Sep 5, 2019
b4f456e
link
connorjclark Sep 5, 2019
56f59c0
Update docs/recipes/auth/README.md
connorjclark Sep 9, 2019
57f2bd2
remove mustache
connorjclark Sep 9, 2019
59bd132
rm loginRequired middleware
connorjclark Sep 9, 2019
cb64b52
stragglers
connorjclark Sep 9, 2019
80e8fb0
lint
connorjclark Sep 9, 2019
a917c02
license, strict
connorjclark Sep 9, 2019
fec4450
rm lock
connorjclark Sep 9, 2019
41e85cf
fileoverview
connorjclark Sep 9, 2019
1de9c39
reduce nestig in jest test
connorjclark Sep 9, 2019
a86d68b
custom matcher
connorjclark Sep 9, 2019
d5f9b5b
remove lh run on 401 page
connorjclark Sep 9, 2019
4a3bc3b
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
ed6bc34
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
558ab99
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
c2669ff
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
b4afaf7
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
05fb347
Apply suggestions from code review
connorjclark Sep 13, 2019
012c5b2
tweaks
connorjclark Sep 13, 2019
0a25f61
move the thing
connorjclark Sep 13, 2019
e43436b
headless false
connorjclark Sep 13, 2019
51c5bb6
some h1s
connorjclark Sep 13, 2019
5e1d786
one more option
connorjclark Sep 13, 2019
dfe971b
integration test docs
connorjclark Sep 13, 2019
a9da287
yarn
connorjclark Sep 13, 2019
0e43b70
lock
connorjclark Sep 13, 2019
d9d6c67
cut out the integration docs
connorjclark Sep 13, 2019
cfa14b9
Apply suggestions from code review
connorjclark Sep 16, 2019
826f855
pr
connorjclark Sep 16, 2019
3dc265b
Merge branch 'auth-docs' of github.com:GoogleChrome/lighthouse into a…
connorjclark Sep 16, 2019
c5d3cda
pr
connorjclark Sep 16, 2019
e3b520f
files
connorjclark Sep 16, 2019
e389e28
pr
connorjclark Sep 16, 2019
97b4dac
use local lh
connorjclark Sep 18, 2019
a6ceaa2
update
connorjclark Sep 19, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/authenticated-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Running Lighthouse on Authenticated Pages

Default runs of Lighthouse load a page as a "new user", with no previous session or storage data. This means that pages requiring authenticated access do not work without additional setup.
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

## Puppeteer
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

[Puppeteer](https://pptr.dev) is the most flexible approach for auditing pages behind authentication with Lighthouse. See [recipes/auth](./recipes/auth) for more.
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

## Chrome DevTools
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

The Audits panel in Chrome DevTools will never clear your session cookies, so you can log in to the target site and run Lighthouse without being logged out. If `localStorage` or `indexedDB` is important for your authentication purposes, be sure to uncheck `Clear storage`.
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

## Headers
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

CLI:
```sh
lighthouse http://www.example.com --view --extra-headers="{\"Authorization\":\"...\"}"
```

Node:
```js
const result = await lighthouse('http://www.example.com', {
extraHeaders: {
Authorization: '...',
},
});
```

You could also set the `Cookie` header, but beware: it will [override any other Cookies you expect to be there](https://github.com/GoogleChrome/lighthouse/pull/9170). A workaround is to use Puppeteer's [`page.setCookie`](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagesetcookiecookies).

## Chrome User Profile
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this in another comment, but we should pull in the authentication advice in https://github.com/GoogleChrome/lighthouse/blob/master/docs/readme.md#testing-on-a-site-with-authentication over to here since people are using this method to set up a primed user profile

connorjclark marked this conversation as resolved.
Show resolved Hide resolved

TODO: pending [#8957](https://github.com/GoogleChrome/lighthouse/issues/8957).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODOs in the docs make the doc writers look bad when really in this case it's a functionality TODO and when it's done the docs can follow :)

Maybe after mentioning the existing workflow above, it could say that an easier method is being considered for implementation and you can learn more at [issue link] (and that the reader can weigh in there)? I'm not sure how much detail is important to include here vs redirecting over there

18 changes: 18 additions & 0 deletions docs/recipes/auth/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

module.exports = {
extends: '../../../.eslintrc.js',
env: {
jest: true,
},
rules: {
'new-cap': 0,
'no-console': 0,
'no-unused-vars': 0,
},
};
95 changes: 95 additions & 0 deletions docs/recipes/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Running Lighthouse on Authenticated Pages with Puppeteer

If you just want to view the code, see [example-lh-auth.js](./example-lh-auth.js).

## The Example Site

There are two pages on the site:

1. `/` - the homepage
2. `/dashboard`

The homepage shows the login form, but only to users that are not signed in.

The dashboard shows a secret to users that are logged in, but shows an error to users that are not.

The server responds with different HTML for each of these pages and session states, so there are four different pages that must have passable Lighthouse SEO scores.
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

(Optional) To run the server:
```sh
# be in root lighthouse directory
yarn # install global project deps
cd docs/auth
yarn # install deps related to just this documentation
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
yarn start # start the server on http://localhost:8000
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
```

## Process

Puppeteer - a browser automation tool - can be used to programatically setup a session.

1. Launch a new browser.
1. Navigate to the login page.
1. Fill and submit the login form.
1. Run Lighthouse using the same browser.

First, launch Chrome:
```js
// This port will be used by Lighthouse later.
const PORT = 8041;
const browser = await puppeteer.launch({
args: [`--remote-debugging-port=${PORT}`],
});
```

Navigate to the login form:
```js
const page = await browser.newPage();
await page.goto('http://localhost:8000');
```

Given a login form like this:
```html
<form action="/login" method="post">
<label>
Email:
<input type="email" name="email">
</label>
<label>
Password:
<input type="password" name="password">
</label>
<input type="submit">
</form>
```

Direct Puppeteer to fill and submit it:
```js
const emailInput = await page.$('input[type="email"]');
await emailInput.type('admin@example.com');
const passwordInput = await page.$('input[type="password"]');
await passwordInput.type('password');
await Promise.all([
page.$eval('.login-form', form => form.submit()),
page.waitForNavigation(),
]);
```

At this point, the session that Puppeteer is managing is now logged in.

Close the page used to log in:
```js
await page.close();
// The page has been closed, but the browser still has the relevant session.
```

Now run Lighthouse, using the same port as before:
```js
const result = await lighthouse('http://localhost:8000/dashboard', { port: PORT });
await browser.close();
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
const lhr = result.lhr;
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
```

## Puppetter in Your Integration Tests

See [example-lh-auth.test.js](./example-lh-auth.test.js) for an example of how to run Lighthouse in your Jest tests on pages in both an authenticated and non-authenticated session.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe move this to the top of the page alongside the If you just want to view the code? By the time I got down to the bottom through the other example-lh-auth.js example it wasn't clear this was a whole different thing.

And if I'm a new or repeat visitor looking specifically for an integration test example, it's helpful to have the link off at the top rather than deep in the page.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your more recent comment suggests pulling the jest integration thing into its own recipe. I like and agree with that.

@connorjclark i'm thinking a recipes/integration-test folder. the readme would add in "btw: we're building on the [/auth recipe]". i think it makes sense to do this bit in a followup PR.

56 changes: 56 additions & 0 deletions docs/recipes/auth/example-lh-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/**
* @fileoverview Example script for running Lighthouse on an authenticated page.
* See docs/recipes/auth/README.md for more.
*/

const puppeteer = require('puppeteer');
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
const lighthouse = require('lighthouse');

const PORT = 8041;
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param {import('puppeteer').Browser} browser
*/
async function login(browser) {
const page = await browser.newPage();
await page.goto('http://localhost:8000');
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
await page.waitForSelector('input[type="email"]', {visible: true});

// Fill in and submit login form.
const emailInput = await page.$('input[type="email"]');
await emailInput.type('admin@example.com');
const passwordInput = await page.$('input[type="password"]');
await passwordInput.type('password');
await Promise.all([
page.$eval('.login-form', form => form.submit()),
page.waitForNavigation(),
]);

await page.close();
}

async function main() {
// Direct Puppeteer to open Chrome with a specific debugging port.
const browser = await puppeteer.launch({
args: [`--remote-debugging-port=${PORT}`],
});

// Setup the browser session to be logged into our site.
await login(browser);

// Direct Lighthouse to use the same port.
const result = await lighthouse('http://localhost:8000/dashboard', {port: PORT});
await browser.close();
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

// Output the result.
console.log(JSON.stringify(result.lhr, null, 2));
}

main();
183 changes: 183 additions & 0 deletions docs/recipes/auth/example-lh-auth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/**
* @fileoverview Example Jest tests for demonstrating how to run Lighthouse on an authenticated
* page as integration tests. See docs/recipes/auth/README.md for more.
*/

/** @typedef {import('./node_modules/lighthouse/types/lhr')} LH */

const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const server = require('./server/server.js');

const CHROME_DEBUG_PORT = 8042;
const SERVER_PORT = 8000;

jest.setTimeout(30000);

// Provide a nice way to assert a score for a category.
// Note, you could just use `expect(lhr.categories.seo.score).toBeGreaterThanOrEqual(0.9)`,
// but by using a custom matcher a better error report can be generated.
expect.extend({
toHaveLighthouseScoreGreaterThanOrEqual(lhr, category, threshold) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a good example, but maybe belongs in a separate recipe? This one is more about the authentication/pptr automation than nice ways to use jest with lighthouse results :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

const score = lhr.categories[category].score;
const auditsRefsByWeight = [...lhr.categories[category].auditRefs]
.filter((auditRef) => auditRef.weight > 0)
.sort((a, b) => b.weight - a.weight);
const report = auditsRefsByWeight.map((auditRef) => {
const audit = lhr.audits[auditRef.id];
const status = audit.score === 1 ?
this.utils.EXPECTED_COLOR('○') :
this.utils.RECEIVED_COLOR('✕');
const weight = this.utils.DIM_COLOR(`[weight: ${auditRef.weight}]`);
return `\t${status} ${weight} ${audit.id}`;
}).join('\n');

if (score >= threshold) {
return {
pass: true,
message: () =>
`expected category ${category} to be < ${threshold}, but got ${score}\n${report}`,
};
} else {
return {
pass: false,
message: () =>
`expected category ${category} to be >= ${threshold}, but got ${score}\n${report}`,
};
}
},
});

/**
* @param {string} url
* @return {Promise<LH.Result>}
*/
async function runLighthouse(url) {
const result = await lighthouse(url, {
port: CHROME_DEBUG_PORT,
onlyCategories: ['seo'],
});
return result.lhr;
}

/**
* @param {puppeteer.Browser} browser
*/
async function login(browser) {
const page = await browser.newPage();
await page.goto('http://localhost:8000/');
await page.waitForSelector('input[type="email"]', {visible: true});

const emailInput = await page.$('input[type="email"]');
await emailInput.type('admin@example.com');
const passwordInput = await page.$('input[type="password"]');
await passwordInput.type('password');
await Promise.all([
page.$eval('.login-form', form => form.submit()),
page.waitForNavigation(),
]);

await page.close();
}

/**
* @param {puppeteer.Browser} browser
*/
async function logout(browser) {
const page = await browser.newPage();
await page.goto('http://localhost:8000/logout');
await page.close();
}

describe('my site', () => {
/** @type {import('puppeteer').Browser} */
let browser;
/** @type {import('puppeteer').Page} */
let page;

beforeAll(async () => {
await new Promise(resolve => server.listen(SERVER_PORT, resolve));
browser = await puppeteer.launch({
args: [`--remote-debugging-port=${CHROME_DEBUG_PORT}`],
headless: !process.env.DEBUG,
slowMo: process.env.DEBUG ? 50 : undefined,
});
});

afterAll(async () => {
await browser.close();
await new Promise(resolve => server.close(resolve));
});

beforeEach(async () => {
page = await browser.newPage();
});

afterEach(async () => {
await page.close();
await logout(browser);
});

describe('/ logged out', () => {
it('lighthouse', async () => {
await page.goto('http://localhost:8000/');
const lhr = await runLighthouse(page.url());
expect(lhr).toHaveLighthouseScoreGreaterThanOrEqual('seo', 0.9);
});

it('login form should exist', async () => {
await page.goto('http://localhost:8000/');
const emailInput = await page.$('input[type="email"]');
const passwordInput = await page.$('input[type="password"]');
expect(emailInput).toBeTruthy();
expect(passwordInput).toBeTruthy();
});
});

describe('/ logged in', () => {
it('lighthouse', async () => {
await login(browser);
await page.goto('http://localhost:8000/');
const lhr = await runLighthouse(page.url());
expect(lhr).toHaveLighthouseScoreGreaterThanOrEqual('seo', 0.9);
});

it('login form should not exist', async () => {
await login(browser);
await page.goto('http://localhost:8000/');
const emailInput = await page.$('input[type="email"]');
const passwordInput = await page.$('input[type="password"]');
expect(emailInput).toBeFalsy();
expect(passwordInput).toBeFalsy();
});
});

describe('/dashboard logged out', () => {
it('has no secrets', async () => {
await page.goto('http://localhost:8000/dashboard');
expect(await page.content()).not.toContain('secrets');
});
});

describe('/dashboard logged in', () => {
it('lighthouse', async () => {
await login(browser);
await page.goto('http://localhost:8000/dashboard');
const lhr = await runLighthouse(page.url());
expect(lhr).toHaveLighthouseScoreGreaterThanOrEqual('seo', 0.9);
});

it('has secrets', async () => {
await login(browser);
await page.goto('http://localhost:8000/dashboard');
expect(await page.content()).toContain('secrets');
});
});
});