first load
This commit is contained in:
36
bin/lighthouse/account.js
Normal file
36
bin/lighthouse/account.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Reads account information from env variables or .a11yrc.json file
|
||||
*/
|
||||
const fs = require("fs");
|
||||
|
||||
let a11yAccount = {};
|
||||
const a11yrcFilePath = ".a11yrc.json";
|
||||
|
||||
if (fs.existsSync(a11yrcFilePath)) {
|
||||
a11yAccount = JSON.parse(fs.readFileSync(a11yrcFilePath));
|
||||
}
|
||||
|
||||
function isValid(account) {
|
||||
return account.subdomain && account.email && account.password;
|
||||
}
|
||||
|
||||
function getAccount() {
|
||||
// Reads account from the env or .a11yrc.json file if present
|
||||
let account = {
|
||||
subdomain: process.env.subdomain || a11yAccount.subdomain,
|
||||
email: process.env.end_user_email || a11yAccount.username,
|
||||
password: process.env.end_user_password || a11yAccount.password,
|
||||
urls: process.env?.urls?.trim()?.split(/\s+/) || a11yAccount.urls
|
||||
};
|
||||
|
||||
if (!isValid(account)) {
|
||||
console.error(
|
||||
"No account specified. Please create a .a11yrc.json file or set subdomain, end_user_email and end_user_password as environment variables"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
module.exports = getAccount;
|
||||
78
bin/lighthouse/config.js
Normal file
78
bin/lighthouse/config.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Sets lighthouse configuration
|
||||
*/
|
||||
module.exports = {
|
||||
lighthouse: {
|
||||
extends: "lighthouse:default",
|
||||
settings: {
|
||||
maxWaitForLoad: 10000,
|
||||
onlyCategories: ["accessibility"],
|
||||
disableFullPageScreenshot: true,
|
||||
},
|
||||
},
|
||||
// custom properties
|
||||
custom: {
|
||||
// turn audit errors into warnings
|
||||
ignore: {
|
||||
tabindex: [
|
||||
{
|
||||
path: "*",
|
||||
selector: "body.community-enabled > a.skip-navigation",
|
||||
},
|
||||
],
|
||||
"link-in-text-block": [
|
||||
{
|
||||
path: "/hc/:locale/community/topics/360000644279",
|
||||
selector:
|
||||
"li > section.striped-list-item > span.striped-list-info > a#title-360006766859",
|
||||
},
|
||||
{
|
||||
path: "/hc/:locale/community/topics/360000644279",
|
||||
selector:
|
||||
"li > section.striped-list-item > span.striped-list-info > a#title-360006766799",
|
||||
},
|
||||
{
|
||||
path: "/hc/:locale/community/posts",
|
||||
selector:
|
||||
"li > section.striped-list-item > span.striped-list-info > a#title-360006766799",
|
||||
},
|
||||
{
|
||||
path: "/hc/:locale/community/posts",
|
||||
selector:
|
||||
"li > section.striped-list-item > span.striped-list-info > a#title-360006766859",
|
||||
},
|
||||
],
|
||||
"aria-allowed-attr": [
|
||||
{
|
||||
path: "/hc/:locale/community/posts/new",
|
||||
selector:
|
||||
"div#main-content > form.new_community_post > div.form-field > a.nesty-input",
|
||||
},
|
||||
],
|
||||
"td-has-header": [
|
||||
{
|
||||
path: "/hc/:locale/subscriptions",
|
||||
selector: "main > div.container > div#main-content > table.table",
|
||||
},
|
||||
],
|
||||
"label-content-name-mismatch": [
|
||||
{
|
||||
path: "/hc/:locale/articles/:id",
|
||||
selector:
|
||||
"footer > div.article-votes > div.article-votes-controls > button.button",
|
||||
},
|
||||
],
|
||||
"target-size": [
|
||||
{
|
||||
path: "/hc/:locale/search",
|
||||
selector:
|
||||
"header > div.search-result-title-container > h2.search-result-title > a",
|
||||
},
|
||||
{
|
||||
path: "/hc/:locale/search",
|
||||
selector: "nav > ol.breadcrumbs > li > a",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
7
bin/lighthouse/constants.js
Normal file
7
bin/lighthouse/constants.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = Object.freeze({
|
||||
ERROR: 0,
|
||||
SUCCESS: 1,
|
||||
WARNING: 2,
|
||||
SKIPPED: 3,
|
||||
UNKNOWN: 4
|
||||
});
|
||||
119
bin/lighthouse/index.js
Normal file
119
bin/lighthouse/index.js
Normal file
@@ -0,0 +1,119 @@
|
||||
require("dotenv").config();
|
||||
const lighthouse = require("lighthouse/core/index.cjs");
|
||||
const puppeteer = require("puppeteer");
|
||||
const config = require("./config");
|
||||
const getAccount = require("./account");
|
||||
const buildUrlsFromAPI = require("./urls");
|
||||
const login = require("./login");
|
||||
const processResults = require("./processor");
|
||||
const { ERROR, WARNING } = require("./constants");
|
||||
|
||||
const outputAudit = (
|
||||
{ title, description, url, id, selector, snippet, explanation },
|
||||
emoji
|
||||
) => {
|
||||
console.log("");
|
||||
console.log(`${emoji} ${id}: ${title}`);
|
||||
console.log(description, "\n");
|
||||
console.log({ url, selector, snippet }, "\n");
|
||||
console.log(explanation, "\n");
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// TODO: If dev, check if zcli is running
|
||||
const isDev = process.argv[2] === "-d";
|
||||
const results = {
|
||||
stats: {},
|
||||
audits: [],
|
||||
};
|
||||
|
||||
// Reading account
|
||||
console.log("Reading account...", "\n");
|
||||
const account = getAccount();
|
||||
|
||||
// Build list of urls to audit
|
||||
if (!account.urls || account.urls.length === 0) {
|
||||
console.log(
|
||||
"No urls were found in .a11yrc.json or as env variable. Building urls from the API...",
|
||||
"\n"
|
||||
);
|
||||
account.urls = await buildUrlsFromAPI(account);
|
||||
}
|
||||
|
||||
// Set login URL
|
||||
account.loginUrl = isDev
|
||||
? `https://${account.subdomain}.zendesk.com/hc/admin/local_preview/start`
|
||||
: `https://${account.subdomain}.zendesk.com/hc/en-us/signin`;
|
||||
|
||||
// Output account
|
||||
console.log("Account:");
|
||||
console.log(
|
||||
{
|
||||
subdomain: account.subdomain,
|
||||
email: account.email,
|
||||
urls: account.urls,
|
||||
loginUrl: account.loginUrl,
|
||||
},
|
||||
"\n"
|
||||
);
|
||||
|
||||
// Use Puppeteer to launch headless Chrome
|
||||
console.log("Starting headless Chrome...");
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
|
||||
// Login
|
||||
console.log(`Logging in using ${account.loginUrl}...`, "\n");
|
||||
await login(browser, account);
|
||||
|
||||
// Run lighthouse on all pages
|
||||
for (let url of account.urls) {
|
||||
console.log(`Running lighthouse in ${url}`);
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
const { lhr } = await lighthouse(
|
||||
url,
|
||||
{ port: new URL(browser.wsEndpoint()).port, logLevel: "silent" },
|
||||
config.lighthouse,
|
||||
page
|
||||
);
|
||||
|
||||
// Output run warnings
|
||||
lhr.runWarnings.forEach((message) => console.warn(message));
|
||||
|
||||
// Analyze / process results
|
||||
const { stats, audits } = processResults(lhr);
|
||||
results.stats[url] = stats;
|
||||
results.audits = [...results.audits, ...audits];
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
// Close browser
|
||||
await browser.close();
|
||||
|
||||
// Output table with stats for each url
|
||||
console.table(results.stats);
|
||||
|
||||
// Output warnings
|
||||
const warnings = results.audits.filter((audit) => audit.result === WARNING);
|
||||
warnings.forEach((audit) => outputAudit(audit, "⚠️"));
|
||||
|
||||
// Output errors
|
||||
const errors = results.audits.filter((audit) => audit.result === ERROR);
|
||||
errors.forEach((audit) => outputAudit(audit, "❌"));
|
||||
|
||||
// Output totals
|
||||
console.log(
|
||||
"\n",
|
||||
`Total of ${errors.length} errors and ${warnings.length} warnings`,
|
||||
"\n"
|
||||
);
|
||||
|
||||
// Exit with error if there is at least one audit with an error
|
||||
if (errors.length > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
16
bin/lighthouse/login.js
Normal file
16
bin/lighthouse/login.js
Normal file
@@ -0,0 +1,16 @@
|
||||
async function login(browser, account) {
|
||||
const { email, password, loginUrl } = account;
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.goto(loginUrl);
|
||||
await page.waitForSelector("input#user_email", { visible: true });
|
||||
await page.type("input#user_email", email);
|
||||
await page.type("input#user_password", password);
|
||||
await Promise.all([
|
||||
page.click("#sign-in-submit-button"),
|
||||
page.waitForNavigation(),
|
||||
]);
|
||||
await page.close();
|
||||
}
|
||||
|
||||
module.exports = login;
|
||||
84
bin/lighthouse/processor.js
Normal file
84
bin/lighthouse/processor.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Filters and maps lighthouse results for a simplified output
|
||||
* If an error should be ignored, it will be converted into a warning
|
||||
*/
|
||||
const UrlPattern = require("url-pattern");
|
||||
const config = require("./config");
|
||||
const { ERROR, WARNING, SKIPPED, SUCCESS, UNKNOWN } = require("./constants");
|
||||
|
||||
function shouldIgnoreError(auditId, url, selector) {
|
||||
const path = new URL(url).pathname;
|
||||
|
||||
return config.custom.ignore[auditId]?.some((ignore) => {
|
||||
const pattern = new UrlPattern(ignore.path);
|
||||
return Boolean(pattern.match(path)) && selector === ignore.selector;
|
||||
});
|
||||
}
|
||||
|
||||
function processResults(lhr) {
|
||||
const url = lhr.mainDocumentUrl;
|
||||
const pageScore = lhr.categories.accessibility.score;
|
||||
|
||||
const audits = Object.values(lhr.audits)
|
||||
// filter and flatten data
|
||||
.map(({ id, title, description, score, details }) => {
|
||||
const newItem = {
|
||||
id,
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
score,
|
||||
};
|
||||
|
||||
return details === undefined || details.items.length === 0
|
||||
? newItem
|
||||
: details.items.map((item) => ({
|
||||
...newItem,
|
||||
selector: item.node.selector,
|
||||
snippet: item.node.snippet,
|
||||
explanation: item.node.explanation,
|
||||
}));
|
||||
})
|
||||
.flat()
|
||||
// map lighthouse score to a result
|
||||
.map(({ score, ...audit }) => {
|
||||
const newItem = { ...audit };
|
||||
|
||||
switch (score) {
|
||||
case 1:
|
||||
newItem.result = SUCCESS;
|
||||
break;
|
||||
case 0:
|
||||
newItem.result = shouldIgnoreError(
|
||||
audit.id,
|
||||
audit.url,
|
||||
audit.selector
|
||||
)
|
||||
? WARNING
|
||||
: ERROR;
|
||||
break;
|
||||
case null:
|
||||
newItem.result = SKIPPED;
|
||||
break;
|
||||
default:
|
||||
console.error(`Error: unexpected score for audit ${audit.id}`);
|
||||
newItem.result = UNKNOWN;
|
||||
}
|
||||
|
||||
return newItem;
|
||||
});
|
||||
|
||||
return {
|
||||
audits,
|
||||
stats: {
|
||||
success: audits.filter((audit) => audit.result === SUCCESS).length,
|
||||
error: audits.filter((audit) => audit.result === ERROR).length,
|
||||
skipped: audits.filter((audit) => audit.result === SKIPPED).length,
|
||||
warning: audits.filter((audit) => audit.result === WARNING).length,
|
||||
unknown: audits.filter((audit) => audit.result === UNKNOWN).length,
|
||||
score: pageScore
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = processResults;
|
||||
167
bin/lighthouse/urls.js
Normal file
167
bin/lighthouse/urls.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Builds urls to audit by fetching the required ids
|
||||
* Account's API should have basic authentication enabled
|
||||
*/
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const fetchCategoryId = async (subdomain) => {
|
||||
const response = await fetch(
|
||||
`https://${subdomain}.zendesk.com/api/v2/help_center/categories`
|
||||
);
|
||||
const data = await response.json();
|
||||
const categoryId = data?.categories[0]?.id;
|
||||
|
||||
if (!categoryId) {
|
||||
throw new Error(
|
||||
"No category found. Please make sure the account has at least one category."
|
||||
);
|
||||
}
|
||||
|
||||
return categoryId;
|
||||
};
|
||||
|
||||
const fetchSectionId = async (subdomain) => {
|
||||
const response = await fetch(
|
||||
`https://${subdomain}.zendesk.com/api/v2/help_center/sections`
|
||||
);
|
||||
const data = await response.json();
|
||||
const sectionId = data?.sections[0]?.id;
|
||||
|
||||
if (!sectionId) {
|
||||
throw new Error(
|
||||
"No section found. Please make sure the account has at least one section."
|
||||
);
|
||||
}
|
||||
|
||||
return sectionId;
|
||||
};
|
||||
|
||||
const fetchArticleId = async (subdomain) => {
|
||||
const response = await fetch(
|
||||
`https://${subdomain}.zendesk.com/api/v2/help_center/articles`
|
||||
);
|
||||
const data = await response.json();
|
||||
const articleId = data?.articles[0]?.id;
|
||||
|
||||
if (!articleId) {
|
||||
throw new Error(
|
||||
"No article found. Please make sure the account has at least one article."
|
||||
);
|
||||
}
|
||||
|
||||
return articleId;
|
||||
};
|
||||
|
||||
const fetchTopicId = async (subdomain) => {
|
||||
const response = await fetch(
|
||||
`https://${subdomain}.zendesk.com/api/v2/community/topics`
|
||||
);
|
||||
const data = await response.json();
|
||||
const topicId = data?.topics[0]?.id;
|
||||
|
||||
if (!topicId) {
|
||||
throw new Error(
|
||||
"No community topic found. Please make sure the account has at least one community topic."
|
||||
);
|
||||
}
|
||||
|
||||
return topicId;
|
||||
};
|
||||
|
||||
const fetchPostId = async (subdomain) => {
|
||||
const response = await fetch(
|
||||
`https://${subdomain}.zendesk.com/api/v2/community/posts`
|
||||
);
|
||||
const data = await response.json();
|
||||
const postId = data?.posts[0]?.id;
|
||||
|
||||
if (!postId) {
|
||||
throw new Error(
|
||||
"No community post found. Please make sure the account has at least one community post."
|
||||
);
|
||||
}
|
||||
|
||||
return postId;
|
||||
};
|
||||
|
||||
const fetchUserId = async (subdomain, email, password) => {
|
||||
const response = await fetch(
|
||||
`https://${subdomain}.zendesk.com/api/v2/users/me`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Basic ${Buffer.from(
|
||||
`${email}:${password}`,
|
||||
"binary"
|
||||
).toString("base64")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
const userId = data?.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error(
|
||||
"Fetching the user id failed. Please make sure this account's API has password access enabled. [Learn more](https://developer.zendesk.com/api-reference/introduction/security-and-auth/#basic-authentication)"
|
||||
);
|
||||
}
|
||||
|
||||
return userId;
|
||||
};
|
||||
|
||||
const fetchRequestId = async (subdomain, email, password) => {
|
||||
const response = await fetch(
|
||||
`https://${subdomain}.zendesk.com/api/v2/requests`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Basic ${Buffer.from(
|
||||
`${email}:${password}`,
|
||||
"binary"
|
||||
).toString("base64")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
const requestId = data?.requests?.[0]?.id;
|
||||
|
||||
if (!requestId) {
|
||||
throw new Error(
|
||||
"No request id found. Please make sure the user has at least one request and this account's API has password access enabled. [Learn more](https://developer.zendesk.com/api-reference/introduction/security-and-auth/#basic-authentication)"
|
||||
);
|
||||
}
|
||||
|
||||
return requestId;
|
||||
};
|
||||
|
||||
const buildUrlsFromAPI = async ({ subdomain, email, password }) => {
|
||||
const [categoryId, sectionId, articleId, topicId, postId, userId, requestId] =
|
||||
await Promise.all([
|
||||
fetchCategoryId(subdomain),
|
||||
fetchSectionId(subdomain),
|
||||
fetchArticleId(subdomain),
|
||||
fetchTopicId(subdomain),
|
||||
fetchPostId(subdomain),
|
||||
fetchUserId(subdomain, email, password),
|
||||
fetchRequestId(subdomain, email, password),
|
||||
]);
|
||||
|
||||
return [
|
||||
`https://${subdomain}.zendesk.com/hc/en-us`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/categories/${categoryId}`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/sections/${sectionId}`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/articles/${articleId}`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/requests/new`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/search?utf8=%E2%9C%93&query=Help+Center`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/community/topics`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/community/topics/${topicId}`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/community/posts`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/community/posts/${postId}`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/profiles/${userId}`,
|
||||
`https://${subdomain}.zendesk.com/hc/contributions/posts?locale=en-us`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/subscriptions`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/requests`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/requests/${requestId}`,
|
||||
`https://${subdomain}.zendesk.com/hc/en-us/community/posts/new`,
|
||||
];
|
||||
};
|
||||
|
||||
module.exports = buildUrlsFromAPI;
|
||||
Reference in New Issue
Block a user