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

CRX: persistent background page -> event page #1487

Merged
merged 4 commits into from
Jan 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lighthouse-extension/app/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"scripts": [
"scripts/chromereload.js",
"scripts/lighthouse-background.js"
]
],
"persistent": false
},
"permissions": [
"activeTab",
Expand Down
3 changes: 2 additions & 1 deletion lighthouse-extension/app/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ <h2 class="header-titles__url">...</h2>
<button class="button button--generate" id="generate-report">Generate report</button>
</main>

<aside class="status subpage">
<!-- Show running view initially (.subpage--visible). -->
<aside class="status subpage subpage--visible">
<div class="lighthouse-effects">
<img src="images/lh_logo_bg_no-light.png" alt="LH logo">
<div class="lighthouse-effects__wrapper">
Expand Down
331 changes: 175 additions & 156 deletions lighthouse-extension/app/src/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,188 +16,203 @@
*/
'use strict';

document.addEventListener('DOMContentLoaded', _ => {
const background = chrome.extension.getBackgroundPage();
const defaultAggregations = background.getDefaultAggregations();

const siteNameEl = window.document.querySelector('header h2');
const generateReportEl = document.getElementById('generate-report');
const subpageVisibleClass = 'subpage--visible';

const statusEl = document.body.querySelector('.status');
const statusMessageEl = document.body.querySelector('.status__msg');
const statusDetailsMessageEl = document.body.querySelector('.status__detailsmsg');
const feedbackEl = document.body.querySelector('.feedback');

const generateOptionsEl = document.getElementById('configure-options');
const optionsEl = document.body.querySelector('.options');
const optionsList = document.body.querySelector('.options__list');
const okButton = document.getElementById('ok');

const MAX_ISSUE_ERROR_LENGTH = 60;

let siteURL = null;

/**
* Error strings that indicate a problem in how Lighthouse was run, not in
* Lighthouse itself, mapped to more useful strings to report to the user.
*/
const NON_BUG_ERROR_MESSAGES = {
'Another debugger': 'You probably have DevTools open. Close DevTools to use Lighthouse',
'multiple tabs': 'You probably have multiple tabs open to the same origin. ' +
'Close the other tabs to use Lighthouse.',
// The extension debugger API is forbidden from attaching to the web store.
// @see https://chromium.googlesource.com/chromium/src/+/5d1f214db0f7996f3c17cd87093d439ce4c7f8f1/chrome/common/extensions/chrome_extensions_client.cc#232
'The extensions gallery cannot be scripted': 'The Lighthouse extension cannot audit the ' +
'Chrome Web Store. If necessary, use the Lighthouse CLI to do so.',
// The user tries to review an error page or has network issues
'Unable to load the page': 'Unable to load the page. Please verify the url you ' +
'are trying to review.'
};

function getLighthouseVersion() {
return chrome.runtime.getManifest().version;
}
/**
* Error strings that indicate a problem in how Lighthouse was run, not in
* Lighthouse itself, mapped to more useful strings to report to the user.
*/
const NON_BUG_ERROR_MESSAGES = {
'Another debugger': 'You probably have DevTools open. Close DevTools to use Lighthouse',
'multiple tabs': 'You probably have multiple tabs open to the same origin. ' +
'Close the other tabs to use Lighthouse.',
// The extension debugger API is forbidden from attaching to the web store.
// @see https://chromium.googlesource.com/chromium/src/+/5d1f214db0f7996f3c17cd87093d439ce4c7f8f1/chrome/common/extensions/chrome_extensions_client.cc#232
'The extensions gallery cannot be scripted': 'The Lighthouse extension cannot audit the ' +
'Chrome Web Store. If necessary, use the Lighthouse CLI to do so.',
// The user tries to review an error page or has network issues
'Unable to load the page': 'Unable to load the page. Please verify the url you ' +
'are trying to review.'
};

const MAX_ISSUE_ERROR_LENGTH = 60;

const subpageVisibleClass = 'subpage--visible';

const getBackgroundPage = new Promise((resolve, reject) => {
chrome.runtime.getBackgroundPage(resolve);
});

function getChromeVersion() {
return /Chrome\/([0-9.]+)/.exec(navigator.userAgent)[1];
}
let siteURL = null;

function buildReportErrorLink(err) {
const reportErrorEl = document.createElement('a');
reportErrorEl.className = 'button button--report-error';
function getLighthouseVersion() {
return chrome.runtime.getManifest().version;
}

let qsBody = '**Lighthouse Version**: ' + getLighthouseVersion() + '\n';
qsBody += '**Chrome Version**: ' + getChromeVersion() + '\n';
function getChromeVersion() {
return /Chrome\/([0-9.]+)/.exec(navigator.userAgent)[1];
}

if (siteURL) {
qsBody += '**URL**: ' + siteURL + '\n';
}
function showRunningSubpage() {
document.querySelector('.status').classList.add(subpageVisibleClass);
}

qsBody += '**Error Message**: ' + err.message + '\n';
qsBody += '**Stack Trace**:\n ```' + err.stack + '```';
function hideRunningSubpage() {
document.querySelector('.status').classList.remove(subpageVisibleClass);
}

const base = 'https://github.com/googlechrome/lighthouse/issues/new?';
let titleError = err.message;
if (titleError.length > MAX_ISSUE_ERROR_LENGTH) {
titleError = `${titleError.substring(0, MAX_ISSUE_ERROR_LENGTH - 3)}...`;
}
const title = encodeURI('title=Extension Error: ' + titleError);
const body = '&body=' + encodeURI(qsBody);
function buildReportErrorLink(err) {
let qsBody = '**Lighthouse Version**: ' + getLighthouseVersion() + '\n';
qsBody += '**Chrome Version**: ' + getChromeVersion() + '\n';

reportErrorEl.href = base + title + body;
reportErrorEl.textContent = 'Report Error';
reportErrorEl.target = '_blank';
return reportErrorEl;
if (siteURL) {
qsBody += '**URL**: ' + siteURL + '\n';
}

function startSpinner() {
statusEl.classList.add(subpageVisibleClass);
}
qsBody += '**Error Message**: ' + err.message + '\n';
qsBody += '**Stack Trace**:\n ```' + err.stack + '```';

function stopSpinner() {
statusEl.classList.remove(subpageVisibleClass);
}
const base = 'https://github.com/googlechrome/lighthouse/issues/new?';
let titleError = err.message;

function logstatus([, message, details]) {
statusMessageEl.textContent = message;
statusDetailsMessageEl.textContent = details;
if (titleError.length > MAX_ISSUE_ERROR_LENGTH) {
titleError = `${titleError.substring(0, MAX_ISSUE_ERROR_LENGTH - 3)}...`;
}
const title = encodeURI('title=Extension Error: ' + titleError);
const body = '&body=' + encodeURI(qsBody);

const reportErrorEl = document.createElement('a');
reportErrorEl.className = 'button button--report-error';
reportErrorEl.href = base + title + body;
reportErrorEl.textContent = 'Report Error';
reportErrorEl.target = '_blank';

return reportErrorEl;
}

function logStatus([, message, details]) {
document.querySelector('.status__msg').textContent = message;
const statusDetailsMessageEl = document.querySelector('.status__detailsmsg');
statusDetailsMessageEl.textContent = details;
}

function createOptionItem(text, isChecked) {
const input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('value', text);
if (isChecked) {
input.setAttribute('checked', 'checked');
}

function createOptionItem(text, isChecked) {
const input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('value', text);
if (isChecked) {
input.setAttribute('checked', 'checked');
}
const label = document.createElement('label');
label.appendChild(input);
label.appendChild(document.createTextNode(text));
const listItem = document.createElement('li');
listItem.appendChild(label);

const label = document.createElement('label');
label.appendChild(input);
label.appendChild(document.createTextNode(text));
const listItem = document.createElement('li');
listItem.appendChild(label);
return listItem;
}

return listItem;
}
/**
* Click event handler for Generate Report button.
* @param {!Window} background Reference to the extension's background page.
* @param {!Object<boolean>} selectedAggregations
*/
function onGenerateReportButtonClick(background, selectedAggregations) {
showRunningSubpage();

const feedbackEl = document.querySelector('.feedback');
feedbackEl.textContent = '';

background.runLighthouseInExtension({
flags: {
disableCpuThrottling: true
},
restoreCleanState: true
}, selectedAggregations).catch(err => {
let message = err.message;
let includeReportLink = true;

// Check for errors in how the user ran Lighthouse and replace with a more
// helpful message (and remove 'Report Error' link).
for (const [test, replacement] of Object.entries(NON_BUG_ERROR_MESSAGES)) {
if (message.includes(test)) {
message = replacement;
includeReportLink = false;
break;
}
}

/**
* Generates a document fragment containing a list of checkboxes and labels
* for the aggregation categories.
* @param {!Object<boolean>} selectedAggregations
* @return {!DocumentFragment}
*/
function generateOptionsList(list, selectedAggregations) {
const frag = document.createDocumentFragment();

defaultAggregations.forEach(aggregation => {
const isChecked = selectedAggregations[aggregation.name];
frag.appendChild(createOptionItem(aggregation.name, isChecked));
});
feedbackEl.textContent = message;

return frag;
}
if (includeReportLink) {
feedbackEl.className = 'feedback-error';
feedbackEl.appendChild(buildReportErrorLink(err));
}

if (background.isRunning()) {
startSpinner();
}
hideRunningSubpage();
background.console.error(err);
});
}

/**
* Generates a document fragment containing a list of checkboxes and labels
* for the aggregation categories.
* @param {!Window} background Reference to the extension's background page.
* @param {!Object<boolean>} selectedAggregations
*/
function generateOptionsList(background, selectedAggregations) {
const frag = document.createDocumentFragment();

background.listenForStatus(logstatus);
background.loadSelectedAggregations().then(aggregations => {
const frag = generateOptionsList(optionsList, aggregations);
optionsList.appendChild(frag);
background.getDefaultAggregations().forEach(aggregation => {
const isChecked = selectedAggregations[aggregation.name];
frag.appendChild(createOptionItem(aggregation.name, isChecked));
});

generateReportEl.addEventListener('click', () => {
startSpinner();
feedbackEl.textContent = '';

background.loadSelectedAggregations()
.then(selectedAggregations => {
return background.runLighthouseInExtension({
flags: {
disableCpuThrottling: true
},
restoreCleanState: true
}, selectedAggregations);
})
.catch(err => {
let message = err.message;
let includeReportLink = true;

// Check for errors in how the user ran Lighthouse and replace with a more
// helpful message (and remove 'Report Error' link).
for (const [test, replacement] of Object.entries(NON_BUG_ERROR_MESSAGES)) {
if (message.includes(test)) {
message = replacement;
includeReportLink = false;
break;
}
}
const optionsList = document.querySelector('.options__list');
optionsList.appendChild(frag);
}
Copy link
Member

Choose a reason for hiding this comment

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

this doesn't use the first list argument in here.

I was thinking you could also just have it fetch the background itself (though it doesn't really matter), and since selectedAggregations is only used in here (not in the parent), you could even absorb that too:

/**
 * Generates a document fragment containing a list of checkboxes and labels
 * for the aggregation categories.
 * @return {!Promise<!DocumentFragment>}
 */
function generateOptionsList() {
  const frag = document.createDocumentFragment();

  return getBackgroundPage.then(background => {
    return background.loadSelectedAggregations().then(selectedAggregations => {
      const defaultAggregations = background.getDefaultAggregations();
      defaultAggregations.forEach(aggregation => {
        const isChecked = selectedAggregations[aggregation.name];
        frag.appendChild(createOptionItem(aggregation.name, isChecked));
      });
      return frag;
}

then below it would be
generateOptionsList().then(frag => optionsList.appendChild(frag));

Copy link
Member

Choose a reason for hiding this comment

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

(could still just pass in background, of course, since the caller already has a reference to it)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

redid this function a bit.


/**
* Initializes the popup's state and UI elements.
* @param {!Window} background Reference to the extension's background page.
*/
function initPopup() {
getBackgroundPage.then(background => {
// To prevent visual hiccups when opening the popup, we default the subpage
// to the "running" view and switch to the default view once we're sure
// Lighthouse is not already auditing the page. This change was necessary
// now that fetching the background event page is async.
if (background.isRunning()) {
Copy link
Member

Choose a reason for hiding this comment

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

move all the rest of this to a named setup function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

showRunningSubpage();
} else {
hideRunningSubpage();
}

feedbackEl.textContent = message;
background.listenForStatus(logStatus);

if (includeReportLink) {
feedbackEl.className = 'feedback-error';
feedbackEl.appendChild(buildReportErrorLink(err));
}
background.loadSelectedAggregations().then(selectedAggregations => {
generateOptionsList(background, selectedAggregations);

stopSpinner();
background.console.error(err);
const generateReportButton = document.getElementById('generate-report');
generateReportButton.addEventListener('click', () => {
onGenerateReportButtonClick(background, selectedAggregations);
});
});
});

generateOptionsEl.addEventListener('click', () => {
optionsEl.classList.add(subpageVisibleClass);
});
const generateOptionsEl = document.getElementById('configure-options');
const optionsEl = document.querySelector('.options');
generateOptionsEl.addEventListener('click', () => {
optionsEl.classList.add(subpageVisibleClass);
});

okButton.addEventListener('click', () => {
// Save selected aggregation categories on options page close.
const checkedAggregations = Array.from(optionsEl.querySelectorAll(':checked'))
.map(input => input.value);
background.saveSelectedAggregations(checkedAggregations);
const okButton = document.getElementById('ok');
okButton.addEventListener('click', () => {
// Save selected aggregation categories on options page close.
const checkedAggregations = Array.from(optionsEl.querySelectorAll(':checked'))
.map(input => input.value);
background.saveSelectedAggregations(checkedAggregations);

optionsEl.classList.remove(subpageVisibleClass);
optionsEl.classList.remove(subpageVisibleClass);
});
});

chrome.tabs.query({active: true, lastFocusedWindow: true}, function(tabs) {
Expand All @@ -206,6 +221,10 @@ document.addEventListener('DOMContentLoaded', _ => {
}

siteURL = new URL(tabs[0].url);
siteNameEl.textContent = siteURL.origin;

// Show the user what URL is going to be tested.
document.querySelector('header h2').textContent = siteURL.origin;
});
});
}

initPopup();