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

extension(tests): Add extension smoketest #4640

Merged
merged 27 commits into from
Mar 20, 2018
Merged

Conversation

wardpeet
Copy link
Collaborator

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

@wardpeet wardpeet changed the title Add extension smoketest extension(tests): Add extension smoketest Feb 27, 2018
@wardpeet
Copy link
Collaborator Author

Fixes #4479

Copy link
Member

@paulirish paulirish left a 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}`,
Copy link
Member

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);
  });
}

Copy link
Member

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 => {
Copy link
Member

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.

Copy link
Collaborator Author

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?

Copy link
Member

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",
Copy link
Member

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?

Copy link
Collaborator

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?

Copy link
Contributor

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.

Copy link
Collaborator Author

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

Copy link
Member

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
Copy link
Member

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

ebidel
ebidel previously requested changes Mar 1, 2018
Copy link
Contributor

@ebidel ebidel left a 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",
Copy link
Contributor

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.

Copy link
Collaborator Author

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)"
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Member

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,
Copy link
Contributor

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,

Copy link
Collaborator Author

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 => {
Copy link
Contributor

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 => {
Copy link
Contributor

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';
Copy link
Contributor

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

Copy link
Member

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()
Copy link
Contributor

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

https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#executioncontextevaluatepagefunction-args maybe?

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sounds neat, will try :)

Copy link
Member

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.

Copy link
Collaborator Author

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? 😛

Copy link
Contributor

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}`,
Copy link
Contributor

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(
Copy link
Contributor

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';
Copy link
Member

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) {
Copy link
Member

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(
Copy link
Member

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",
Copy link
Member

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,
Copy link
Collaborator Author

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 @@
{
Copy link
Member

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?

Copy link
Collaborator Author

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).

Copy link
Member

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.

Copy link
Member

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) {
Copy link
Member

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');
Copy link
Member

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.

Copy link
Member

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

Copy link
Collaborator Author

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😛.

Copy link
Member

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(',#')}`);
Copy link
Member

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

@@ -22,6 +23,7 @@
"gulp-util": "^3.0.7",
"gulp-zip": "^3.2.0",
"package-json-versionify": "^1.0.4",
"puppeteer": "^1.1.1",
Copy link
Member

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

Copy link
Contributor

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 🤹‍♀️

Copy link
Member

Choose a reason for hiding this comment

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

sgtm

@googlebot
Copy link

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 cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@wardpeet
Copy link
Collaborator Author

@paulirish yarn compile-devtools is failing. How can I fix that?

@paulirish
Copy link
Member

Can you add in an assertion that the report doesn't include the text "Audit error"? Maybe grab all .lh-error and look within their textContent?

If we have this we would have caught #4794

});

it('should contain a filmstrip', async () => {
const filmstrip = await extensionPage.$('lh-filmstrip');
Copy link
Member

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?

Copy link
Member

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,
Copy link
Member

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`);
Copy link
Member

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 =>
Copy link
Member

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');
  });

gives a nice purdy error:
image

@paulirish paulirish merged commit e90f9ac into master Mar 20, 2018
@paulirish paulirish deleted the tests/lighthouse-extension branch March 20, 2018 23:24
@paulirish
Copy link
Member

🎉 this is epic, @wardpeet. thx for making this happen. :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants