Skip to content

Commit 378cb06

Browse files
authored
[MWPW-179699] [PREFLIGHT] Added a11y links localization check (#5169)
* added a11y links localization check * moved localization checks to the general panel * addressing feedback * hotfix
1 parent bf92e2b commit 378cb06

File tree

5 files changed

+153
-22
lines changed

5 files changed

+153
-22
lines changed

libs/blocks/preflight/checks/constants.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ export const STATUS = {
55
EMPTY: 'empty',
66
};
77

8+
export const STATUS_TO_ICON_MAP = {
9+
[STATUS.PASS]: 'green',
10+
[STATUS.FAIL]: 'red',
11+
[STATUS.LIMBO]: 'orange',
12+
[STATUS.EMPTY]: 'empty',
13+
};
14+
815
export const SEVERITY = {
916
CRITICAL: 'critical',
1017
WARNING: 'warning',
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { STATUS } from './constants.js';
2+
import { getConfig, getLocale } from '../../../utils/utils.js';
3+
4+
async function getStatus(url) {
5+
const tryHead = await fetch(url, { method: 'HEAD', cache: 'no-store' }).catch(() => null);
6+
if (tryHead?.ok || tryHead?.status === 404) return tryHead.status;
7+
const res = await fetch(url, { method: 'GET', cache: 'no-store' }).catch(() => null);
8+
return res?.status || 0;
9+
}
10+
11+
function removeLocale(pathname, locales) {
12+
const { prefix } = getLocale(locales, pathname);
13+
if (!prefix) return pathname;
14+
if (pathname === prefix || pathname === `${prefix}/`) return '/';
15+
return pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
16+
}
17+
18+
function addLocale(basePath, prefix) {
19+
const cleanBase = basePath.startsWith('/') ? basePath : `/${basePath}`;
20+
if (!prefix) return cleanBase;
21+
const cleanPrefix = prefix.startsWith('/') ? prefix : `/${prefix}`;
22+
return `${cleanPrefix}${cleanBase === '/' ? '' : cleanBase}`;
23+
}
24+
25+
function normalizePath(path) {
26+
if (!path || path === '/') return '/';
27+
return path.replace(/\/+$/, '');
28+
}
29+
30+
export async function runChecks({ area = document } = {}) {
31+
const { locales } = getConfig();
32+
const locale = getLocale(locales, window.location.pathname);
33+
const links = Array.from(area.querySelectorAll('a[href]'));
34+
const seen = new Set();
35+
const violations = (await Promise.all(links.map(async (linkEl) => {
36+
const href = linkEl.getAttribute('href');
37+
if (!href || href.startsWith('#')) return null;
38+
const url = new URL(href, window.location.origin);
39+
const basePath = removeLocale(url.pathname, locales);
40+
if (url.hash && normalizePath(basePath) === '/') return null;
41+
const key = `${url.origin}${normalizePath(url.pathname)}`;
42+
if (seen.has(key)) return null;
43+
seen.add(key);
44+
45+
const currentLocalePath = addLocale(basePath, locale.prefix);
46+
const isCurrentLocaleLink = normalizePath(url.pathname)
47+
=== normalizePath(currentLocalePath);
48+
49+
const [localizedStatus, usStatus] = await Promise.all([
50+
getStatus(`${url.origin}${currentLocalePath}`),
51+
getStatus(`${url.origin}${basePath}`),
52+
]);
53+
54+
const shouldFlag = (!isCurrentLocaleLink && [200, 404].includes(localizedStatus))
55+
|| (isCurrentLocaleLink && localizedStatus === 404);
56+
57+
if (!shouldFlag) return null;
58+
return {
59+
url: url.href,
60+
isLocalized: isCurrentLocaleLink,
61+
usStatus,
62+
localizedStatus,
63+
};
64+
}))).filter(Boolean);
65+
66+
const violationsCount = violations.length;
67+
return [{
68+
id: 'link-localization',
69+
title: 'Links',
70+
status: violationsCount === 0 ? STATUS.PASS : STATUS.FAIL,
71+
description: violationsCount === 0
72+
? 'All links are localized or valid.'
73+
: `${violationsCount} link${violationsCount > 1 ? 's' : ''} potentially not localized or invalid.`,
74+
details: { violations },
75+
}];
76+
}
77+
78+
export default { runChecks };

libs/blocks/preflight/panels/general.js

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { html, signal, useEffect } from '../../../deps/htm-preact.js';
2-
import { STATUS, STRUCTURE_TITLES } from '../checks/constants.js';
2+
import { STATUS_TO_ICON_MAP, STRUCTURE_TITLES } from '../checks/constants.js';
33
import { runChecks as runStructureChecks } from '../checks/structure.js';
44
import userCanPublishPage from '../../../tools/utils/publish.js';
5+
import { runChecks as runLocalizationChecks } from '../checks/localization.js';
56

67
const DEF_NOT_FOUND = 'Not found';
78
const DEF_NEVER = 'Never';
@@ -20,6 +21,9 @@ const footerResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.footer, de
2021
const regionSelectorResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.regionSelector, description: 'Checking...' });
2122
const georoutingResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.georouting, description: 'Checking...' });
2223
const breadcrumbsResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.breadcrumbs, description: 'Checking...' });
24+
const localizationResult = signal({ icon: 'purple', title: 'Links', description: 'Checking...' });
25+
const localizationIssues = signal([]);
26+
const localizationClosed = signal(false);
2327

2428
async function getStructureResults() {
2529
const signals = [
@@ -31,16 +35,9 @@ async function getStructureResults() {
3135
];
3236
const checks = runStructureChecks({ area: document });
3337

34-
const statusToIconMap = {
35-
[STATUS.PASS]: 'green',
36-
[STATUS.FAIL]: 'red',
37-
[STATUS.LIMBO]: 'orange',
38-
[STATUS.EMPTY]: 'empty',
39-
};
40-
4138
await Promise.all(checks.map((result, index) => Promise.resolve(result)
4239
.then((res) => {
43-
const icon = statusToIconMap[res.status] || 'orange';
40+
const icon = STATUS_TO_ICON_MAP[res.status] || 'orange';
4441
signals[index].value = {
4542
icon,
4643
title: res.title,
@@ -56,6 +53,24 @@ async function getStructureResults() {
5653
})));
5754
}
5855

56+
async function getLocalizationResults() {
57+
try {
58+
const [res] = await runLocalizationChecks({ area: document });
59+
localizationResult.value = {
60+
icon: STATUS_TO_ICON_MAP[res.status] || 'orange',
61+
title: res.title,
62+
description: res.description,
63+
};
64+
localizationIssues.value = res.details?.violations || [];
65+
} catch (error) {
66+
localizationResult.value = {
67+
icon: 'red',
68+
title: 'Links',
69+
description: `Error: ${error.message}`,
70+
};
71+
}
72+
}
73+
5974
function getAdminUrl(url, type) {
6075
if (!(/adobecom\.(hlx|aem)./.test(url.hostname))) return false;
6176
const project = url.hostname === 'localhost' ? 'main--milo--adobecom' : url.hostname.split('.')[0];
@@ -275,17 +290,45 @@ function ContentGroup({ name, group }) {
275290
</div>`;
276291
}
277292

278-
export default function General() {
279-
useEffect(() => { setContent(); getStructureResults(); }, []);
280-
281-
const StructureItem = ({ icon, title, description }) => html`
293+
function StructureItem({ icon, title, description }) {
294+
return html`
282295
<div class="preflight-item">
283296
<div class="result-icon ${icon}"></div>
284297
<div class="preflight-item-text">
285298
<p class="preflight-item-title">${title}</p>
286299
<p class="preflight-item-description">${description}</p>
287300
</div>
288301
</div>`;
302+
}
303+
304+
function LocalizationIssuesList({ issues }) {
305+
return html`
306+
${issues.length > 0 && html`
307+
<div class="preflight-content-group${localizationClosed.value ? ' is-closed' : ''}">
308+
<div class="preflight-group-row preflight-group-heading" onClick=${() => { localizationClosed.value = !localizationClosed.value; }}>
309+
<div class="preflight-group-expand"></div>
310+
<p class=preflight-content-heading>Faulty links</p>
311+
<p class="preflight-content-heading">Loc</p>
312+
<p class="preflight-content-heading">US status</p>
313+
<p class="preflight-content-heading">Loc status</p>
314+
</div>
315+
<div class=preflight-group-items>
316+
${issues.map((v) => html`
317+
<div class="preflight-group-row preflight-group-detail">
318+
<p><a href=${v.url} target=_blank>${v.url}</a></p>
319+
<p>${v.isLocalized ? 'Yes' : 'No'}</p>
320+
<p>${v.usStatus}</p>
321+
<p>${v.localizedStatus}</p>
322+
</div>
323+
`)}
324+
</div>
325+
</div>
326+
`}
327+
`;
328+
}
329+
330+
export default function General() {
331+
useEffect(() => { setContent(); getStructureResults(); getLocalizationResults(); }, []);
289332

290333
const allChecked = Object.values(content.value)
291334
.flatMap((item) => item.items).filter((item) => item.checked);
@@ -313,6 +356,14 @@ export default function General() {
313356
<${StructureItem} ...${breadcrumbsResult.value} />
314357
</div>
315358
</div>
359+
<p class="preflight-structure-title">Localization</p>
360+
<div class=preflight-structure-columns>
361+
<div class=preflight-column>
362+
<${StructureItem} ...${localizationResult.value} />
363+
</div>
364+
</div>
365+
<${LocalizationIssuesList} issues=${localizationIssues.value} />
366+
<p class="preflight-structure-title">Content</p>
316367
${Object.keys(content.value).map((key) => html`<${ContentGroup} name=${key} group=${content.value[key]} />`)}
317368
</div>
318369

libs/blocks/preflight/panels/performance.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { html, signal, useEffect } from '../../../deps/htm-preact.js';
22
import preflightApi from '../checks/preflightApi.js';
3-
import { STATUS } from '../checks/constants.js';
3+
import { STATUS_TO_ICON_MAP } from '../checks/constants.js';
44

55
const { getLcpEntry, runChecks } = preflightApi.performance;
66

@@ -34,12 +34,7 @@ async function getResults() {
3434
const signalResult = signals[index];
3535
return Promise.resolve(resultOrPromise)
3636
.then((result) => {
37-
const statusToIconMap = {
38-
[STATUS.PASS]: 'green',
39-
[STATUS.FAIL]: 'red',
40-
[STATUS.EMPTY]: 'empty',
41-
};
42-
const icon = statusToIconMap[result.status] ?? 'orange';
37+
const icon = STATUS_TO_ICON_MAP[result.status] ?? 'orange';
4338
signalResult.value = {
4439
icon,
4540
title: result.title.replace('Performance - ', ''),

libs/blocks/preflight/preflight.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,15 +333,15 @@ span.preflight-time {
333333

334334
/* GENERAL */
335335
.preflight-structure-title {
336-
margin: 0;
336+
margin: 0 0 24px;
337337
font-weight: 700;
338338
font-size: 32px;
339339
line-height: 1;
340340
padding-left: 24px;
341341
}
342342

343343
.preflight-structure-columns {
344-
margin: 24px 48px;
344+
margin: 0 48px 24px;
345345
display: grid;
346346
grid-template-columns: 1fr 1fr;
347347
gap: 48px;

0 commit comments

Comments
 (0)