first load
Some checks failed
Build, Push, Publish / Build & Release (push) Failing after 2s
Sync Repo / sync (push) Failing after 2s

This commit is contained in:
2025-12-16 04:40:00 -03:00
parent 9f33a94e0e
commit 6fa41a771d
856 changed files with 70411 additions and 1 deletions

36
bin/lighthouse/account.js Normal file
View 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
View 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",
},
],
},
},
};

View 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
View 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
View 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;

View 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
View 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;