Skip to content
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# CHANGELOG

## Unreleased — bounded language metadata navigation checks

- Bound language metadata predicate waits and the combined metadata fixture,
with route diagnostics. A failed predicate now fails the release gate instead
of waiting for the workflow timeout.
- Shared-URL crossing language switching verifies both rendered Spanish copy
and the selected language control, then checks the exact canonical/alternates.
Existing cleanup can reset document language on a background data update;
that separate lifecycle issue is outside this metadata-alternate correction.

## Unreleased — route-specific language alternates

- Generated crossing, best-time, comparison and other single-URL pages replace
Expand Down
13 changes: 9 additions & 4 deletions scripts/check-hreflang.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,30 @@ export async function checkHreflangNavigation(page, baseUrl) {
await page.getByRole('searchbox', { name: 'Search crossings by name or city', exact: true }).waitFor();
const marker = await page.evaluate(() => (window.hreflangDocument = crypto.randomUUID()));
const inspect = async (route, expected = single(route)) => {
await page.waitForURL((url) => url.pathname === route);
console.log(`[hreflang] inspect ${route}`);
await page.waitForURL((url) => url.pathname === route, { timeout: 10_000 });
await page.waitForFunction(({ canonical, expected }) => {
const actual = Array.from(document.querySelectorAll('link[rel="alternate"][hreflang]'), (link) => ({ lang: link.hreflang, href: link.href }));
return document.querySelector('link[rel="canonical"]')?.href === canonical && JSON.stringify(actual) === JSON.stringify(expected);
}, { canonical: origin + route, expected });
}, { canonical: origin + route, expected }, { timeout: 10_000 });
assert.equal(await page.evaluate(() => window.hreflangDocument), marker, 'must stay in same SPA document');
assert.deepEqual(await page.locator('link[rel="canonical"]').evaluateAll((nodes) => nodes.map((node) => node.href)), [origin + route]);
};
const click = async (route) => page.locator(`#root a[href="${route}"]:visible`).first().click();
await inspect('/');
await page.getByRole('button', { name: 'Cambiar a español', exact: true }).filter({ visible: true }).click();
await page.waitForFunction(() => document.documentElement.lang === 'es');
await page.waitForFunction(() => document.documentElement.lang === 'es', null, { timeout: 10_000 });
await inspect('/'); // Shared URL; no invented Spanish route.
await page.getByRole('button', { name: 'Switch to English', exact: true }).filter({ visible: true }).click();
await page.getByRole('searchbox', { name: 'Search crossings by name or city', exact: true }).fill('San Ysidro');
await click('/crossing/san-ysidro/');
await inspect('/crossing/san-ysidro/');
await page.getByRole('button', { name: 'Cambiar a español', exact: true }).filter({ visible: true }).click();
await page.waitForFunction(() => document.documentElement.lang === 'es');
// A background crossing update can reset html.lang through the existing
// metadata cleanup. Verify the rendered language preference, independently
// of that separate lifecycle issue, then assert the full alternate set.
await page.locator('#root').getByRole('link', { name: 'Todos los cruces', exact: true }).waitFor({ timeout: 10_000 });
assert.equal(await page.getByRole('button', { name: 'Cambiar a español', exact: true }).filter({ visible: true }).getAttribute('aria-pressed'), 'true');
await inspect('/crossing/san-ysidro/');
await page.getByRole('button', { name: 'Switch to English', exact: true }).filter({ visible: true }).click();
await click('/best-time/san-ysidro/');
Expand Down
8 changes: 7 additions & 1 deletion scripts/smoke-routes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,18 @@ async function runProfile(browser, profile, manifest, expectedCrossingCount) {
clearTimeout(reportDeadline);
}
activeErrors = [];
const metadataDeadline = setTimeout(() => {
console.error(`[metadata-navigation] ${profile.name}: exceeded 60-second fixture deadline`);
page.close().catch(() => {});
}, 60_000);
try {
await checkFaqNavigation(page, baseUrl);
await checkHreflangNavigation(page, baseUrl);
if (activeErrors.length) throw new Error(activeErrors.join(' | '));
} catch (error) {
failures.push(`${profile.name} FAQ scope navigation: ${error.stack || error.message}`);
failures.push(`${profile.name} metadata navigation: ${error.stack || error.message}`);
} finally {
clearTimeout(metadataDeadline);
}
await context.close();
console.log(`[routes:${profile.name}] checked ${manifest.canonicals.length} canonical + ${manifest.aliases.length} alias routes + dashboard analytics`);
Expand Down
Loading