-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
extension(tests): Add extension smoketest #4640
Conversation
Fixes #4479 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
really nice job. clean impl and super jazzed you got this running on travis no problem. :)
so so good.
mostly style feedback, but the approach is aces.
return browser.pages().then(pages => { | ||
const page = pages.find(page => page.url().includes('blob:chrome-extension://')); | ||
const assertAudits = (category, expected, selector) => | ||
page.$eval(`#${category}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trying this out to see if it's more clear:
function assertAuditElements(category, expected, selector) {
page.$evaluate(({category, selector}) =>
document.querySelector(`#${category}`).parentNode.querySelectorAll(selector).length,
{category, selector}
).then(elementCount => {
assert.equal(expected, elementCount);
});
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, let's use a keyed object for the method signature?
assertAudits({
category: 'performance',
count: config.categories.performance.audits.length,
selector: '.lh-audit,.lh-timeline-metric,.lh-perf-hint,.lh-filmstrip'
});
`--disable-extensions-except=${lighthouseExtensionPath}`, | ||
`--load-extension=${lighthouseExtensionPath}`, | ||
], | ||
}).then(browser => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do async/await for this file. I'm sorry we didn't sort that out beforehand.
But IMO that'll make this guy a lot more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about node 6 which we run our PR checks on? it doesn't support async/await right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're about to drop node 6 support. :) we'll land this patch in the 3.0 side of things.
@@ -18,6 +18,7 @@ | |||
}, | |||
"permissions": [ | |||
"activeTab", | |||
"tabs", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i remember you telling me, but remind me again why we now need this permission?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's definitely add this on demand as you suggested @wardpeet, grabbing a new permission might still disable the extension on update?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need it. In https://stackoverflow.com/questions/48089670/detect-and-test-chrome-extension-using-puppeteer, I needed it to get the title of tab. We're not doing anything special with the tabs api here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why I need the extension is the following function
const queryOpts = {
active: true,
currentWindow: true,
};
chrome.tabs.query(queryOpts, (tabs => {
without it I couldn't get the url of the tab
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yessssss. I repro'd this.
Basically I believe activeTab isn't enough because of however we're bypassing the popup and such. It's hitting some corner case where activeTab
just isn't enough. +1 to dynamically adding tabs permission just for this case.
.travis.yml
Outdated
- yarn bundlesize | ||
- yarn lint | ||
- yarn unit | ||
- yarn type-check | ||
- yarn closure | ||
- yarn smoke | ||
- yarn smokehouse | ||
- node lighthouse-extension/test/extension.js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add an npm scripts entry to the extensions package.json so it's easier to run on demand
cd lighthouse-extension; yarn test:e2e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sweet!
@@ -18,6 +18,7 @@ | |||
}, | |||
"permissions": [ | |||
"activeTab", | |||
"tabs", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this change the permission model and reprompt users to accept the changes. We saw a lot of uninstalls last time we changed the crx permissions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could change the config just during the test so we don't need to change it for installations?
.travis.yml
Outdated
- yarn | ||
# travis can't handle the parallel install (without caches) | ||
- yarn run install-all:task:windows | ||
before_script: | ||
- export DISPLAY=:99.0 | ||
- export CHROME_PATH="$(pwd)/chrome-linux/chrome" | ||
- export CHROME_PATH="$(which google-chrome-stable)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you skip the pptr chromium download, is this at least chrome 66. PPTR may need that version to work properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just thought it was a bit stupid to install it twice and we moved to chrome stable to test the lowest version we support. I could enable chromium again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, it does appear to work fine with chrome stable.
I'm also fine with using the bundled Chromium, but yes it does seem wasteful to d/l each time.
If it works fine with chrome stable as of now, then moving forward (assuming we infrequently bump the pptr version), it shouldn't regress. So I'm +1 to using the stable.
|
||
puppeteer.launch({ | ||
headless: false, | ||
executablePath: process.env.CHROME_PATH ? process.env.CHROME_PATH : undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can just be: executablePath: process.env.CHROME_PATH,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
], | ||
}).then(browser => { | ||
browser.newPage() | ||
.then(page => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit .then(page => page.goto('https://www.paulirish.com', {waitUntil: 'networkidle2'})
return page.goto('https://www.paulirish.com', {waitUntil: 'networkidle2'}); | ||
}) | ||
.then(() => browser.targets()) | ||
.then(targets => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Man ...it's been a while since I haven't seen await/async
. pptr code look especially painful without it :)
.then(() => browser.targets()) | ||
.then(targets => { | ||
const extensionTarget = targets.find(target => { | ||
return target._targetInfo.title === 'Lighthouse'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: code be 1 line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO should also check _targetInfo.type === 'background_page'
not a one-liner any more. sorryyy
}); | ||
|
||
if (extensionTarget) { | ||
extensionTarget.createCDPSession() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any way to use puppeteer's evaluate
method(s) here
await ctx.evaluate(() => {
return runLighthouseInExtension({restoreCleanState: true}, ['performance', 'pwa', 'accessibility', 'best-practices', 'seo']);
});
Whatever that looks like. Not sure if you can get the crx context, other than what you're doing using an private api.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds neat, will try :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup +1 to using this rather than dropping to the protocol. good call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ebidel not sure how I can get that execution context from the Target class. I tried .page().mainFrame().executionContext()
but page() is of course null.
Any ideas? 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Asking...
return browser.pages().then(pages => { | ||
const page = pages.find(page => page.url().includes('blob:chrome-extension://')); | ||
const assertAudits = (category, expected, selector) => | ||
page.$eval(`#${category}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having a hard time parsing this. Maybe just use function
. And I think selector can just be passed as a string, not in in an obj:
const assertNumAuditsEqual = function(el, selector) {
return page.$eval(`#${category}`, (el, selector) => el.parentNode.querySelectorAll(selector).length, selector));
}
'.lh-audit,.lh-timeline-metric,.lh-perf-hint,.lh-filmstrip' | ||
); | ||
}) | ||
.then( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the formatting of these then's should be the same. I may also break this testing section into a separate function if you can.
.then(() => browser.targets()) | ||
.then(targets => { | ||
const extensionTarget = targets.find(target => { | ||
return target._targetInfo.title === 'Lighthouse'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO should also check _targetInfo.type === 'background_page'
not a one-liner any more. sorryyy
return target._targetInfo.title === 'Lighthouse'; | ||
}); | ||
|
||
if (extensionTarget) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i know the async/await will change this completely, but IMO you should early exit on the !
case here instead.
return client; | ||
}) | ||
.then(client => { | ||
return client.send( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can return obj.exceptionDetails
which means the evaluation failed. if we get that, we need to log the error at least.
@@ -18,6 +18,7 @@ | |||
}, | |||
"permissions": [ | |||
"activeTab", | |||
"tabs", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yessssss. I repro'd this.
Basically I believe activeTab isn't enough because of however we're bypassing the popup and such. It's hitting some corner case where activeTab
just isn't enough. +1 to dynamically adding tabs permission just for this case.
.eslintrc.js
Outdated
@@ -68,7 +68,7 @@ module.exports = { | |||
'arrow-parens': 0, | |||
}, | |||
parserOptions: { | |||
ecmaVersion: 6, | |||
ecmaVersion: 2017, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is needed for eslint async/await
@@ -0,0 +1,5979 @@ | |||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're still yarn folk so we should be updating the yarn lock and not adding this, right?
or did you already point out why we need this and i'm forgetting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no no, it's my mistake . It shouldn't be here. I use yarn in WSL but npm on powershell because i'm to lazy to install yarn on windows as I only need it to run puppeteer (i'll probably create a bug for that in the puppeteer repository).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw npm install -g yarn
is what we do on appveyor. probably not too bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also i think pptr is moving to package-lock soon.
_targetInfo.type === 'background_page'; | ||
}); | ||
|
||
if (extensionTarget) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
early exit instead
const config = require(path.resolve(__dirname, '../../lighthouse-core/config/default.js')); | ||
const lighthouseCategories = Object.keys(config.categories); | ||
|
||
const manifestLocation = path.join(lighthouseExtensionPath, 'manifest.json'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you pull this stuff into a little named function? might as well create another for putting the orig back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually if we use mocha to run this test, then we can use before/after methods for these things.
seems fairly reasonable to use mocha at this point
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any idea how I should name this test? describe and it text😛.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
describe('lighthouse chrome extension', ...
it('completes an end-to-end run', ...
extensionPage = (await browser.pages()) | ||
.find(page => page.url().includes('blob:chrome-extension://')); | ||
|
||
const categories = await extensionPage.$$(`#${lighthouseCategories.join(',#')}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd pull 80-94 into a separate function, so it's clear that this is the test assertions against the report page
lighthouse-extension/package.json
Outdated
@@ -22,6 +23,7 @@ | |||
"gulp-util": "^3.0.7", | |||
"gulp-zip": "^3.2.0", | |||
"package-json-versionify": "^1.0.4", | |||
"puppeteer": "^1.1.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do folks think about moving puppeteer up to a root-level dev dependency? As I was testing viewer yesterday I was thinking it would be great to have a similar (but should be much simpler) e2e test for viewer, and it seems a bit silly to have a copy here and in viewer when yarn install-all
would install both anyway...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea. Viewer UI testing is the perfect use case for 🤹♀️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sgtm
6188f59
to
b1a6fff
Compare
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
@paulirish yarn compile-devtools is failing. How can I fix that? |
230c343
to
644df57
Compare
Can you add in an assertion that the report doesn't include the text "Audit error"? Maybe grab all If we have this we would have caught #4794 |
}); | ||
|
||
it('should contain a filmstrip', async () => { | ||
const filmstrip = await extensionPage.$('lh-filmstrip'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing a dot in the selector here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this sorts it out:
it('should contain a filmstrip', async () => {
const filmstrip = await extensionPage.$('.lh-filmstrip');
assert.equal(true, !!filmstrip, `filmstrip is not available`);
});
it('should contain a filmstrip', async () => { | ||
const filmstrip = await extensionPage.$('lh-filmstrip'); | ||
|
||
assert.equal(null, filmstrip, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait a second... null != page contains a filmstrip... :p
const filmstrip = await extensionPage.$('lh-filmstrip'); | ||
|
||
assert.equal(null, filmstrip, | ||
`filmstrip is not available`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can probably be 1 line
|
||
it('should contain no failed audits', async () => { | ||
const auditErrors = await extensionPage.$$('.lh-debug'); | ||
const failedAudits = auditErrors.filter(error => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
heh. I ran into a similar API misunderstanding. I was doing $$eval(selector, el => ...
) but its actually elems
instead of just el
. Whoopsie daisy.
try something like this out:
it('should not have any audit errors', async () => {
function getDebugStrings(elems) {
return elems.map(el => ({
debugString: el.textContent,
title: el.closest('.lh-audit').querySelector('.lh-score__title').textContent,
}));
}
const auditErrors = await extensionPage.$$eval('.lh-debug', getDebugStrings);
const errors = auditErrors.filter(item => item.debugString.includes('Audit error:'));
assert.deepStrictEqual(errors, [], 'Audit errors found within the report');
});
8c71734
to
259441f
Compare
🎉 this is epic, @wardpeet. thx for making this happen. :D |
This PR will run the extension in chrome and checks if the report has all necessary fields listed.
the manifest change is needed because puppeteer can't detect the active tab. We need to add tabs to the permission list, we could temporary add it for each run so we don't have to update our manifest.
Also a caveat, we need to run puppeteer in non headless mode to let the extensions work