chore: testes de ponta a ponta no CI e limpeza do repositório
Job e2e separado no CI; testes de teclado, rolagem, sem JS, axe no celular e menu do celular; limites de metadados derivados do sitemap; sai @react-three/fiber e @react-three/drei; pastas de ferramentas (.augment, .kiro, .claude/settings.local.json) fora do Git.
This commit is contained in:
1 parent
6ac87824f3
commit
1a60795107
57 files changed
+625
-12090
No files matched your search
+103
-37
@@ -1,62 +1,128 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, type Page, test } from "@playwright/test";
|
||||
import { DESKTOP } from "./helpers";
|
||||
|
||||
/**
|
||||
* (c) Do topo da home, o Tab chega ao campo de nome do contato em no máximo 30 toques.
|
||||
* O teste continua apertando Tab (até 120) depois do limite só para dizer, na falha, quantos toques faltavam.
|
||||
* (c) Teclado, no computador:
|
||||
* (a) atalho: Tab até "Falar sobre meu projeto" no cabeçalho, Enter, e o campo de nome do contato em até 4 Tabs.
|
||||
* Só no computador: no celular o botão do hero abre o WhatsApp, não o formulário.
|
||||
* (b) caminho linear: do topo da home, o Tab chega ao campo de nome do contato em até 40 toques
|
||||
* (a vitrine de projetos é uma parada só, navegada com as setas).
|
||||
* Os testes continuam apertando Tab depois do limite só para dizer, na falha, quantos toques faltavam.
|
||||
*/
|
||||
|
||||
const MAX_TABS = 30;
|
||||
const GIVE_UP_AFTER = 120;
|
||||
const CTA_LABEL = "Falar sobre meu projeto";
|
||||
const SHORTCUT_MAX_TABS_TO_CTA = 20;
|
||||
const SHORTCUT_MAX_TABS_AFTER_ENTER = 4;
|
||||
const LINEAR_MAX_TABS = 40;
|
||||
const LINEAR_GIVE_UP_AFTER = 120;
|
||||
|
||||
test.use({ viewport: DESKTOP });
|
||||
|
||||
test(`Tab do topo da home chega ao campo de nome do contato em até ${MAX_TABS} toques`, async ({
|
||||
page,
|
||||
}) => {
|
||||
/** Descreve o elemento com foco e diz se é o botão de contato do cabeçalho ou o campo de nome do contato. */
|
||||
async function focusedElement(page: Page) {
|
||||
return page.evaluate((ctaLabel) => {
|
||||
const element = document.activeElement as HTMLElement | null;
|
||||
if (!element || element === document.body)
|
||||
return { isCta: false, isName: false, label: "(nada)" };
|
||||
const label = (
|
||||
element.getAttribute("aria-label") ||
|
||||
element.textContent ||
|
||||
element.getAttribute("name") ||
|
||||
element.tagName
|
||||
)
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.slice(0, 40);
|
||||
return {
|
||||
isCta:
|
||||
Boolean(element.closest("header")) &&
|
||||
(element.textContent ?? "").trim() === ctaLabel,
|
||||
isName:
|
||||
element.getAttribute("name") === "name" &&
|
||||
Boolean(element.closest("#contact")),
|
||||
label: `${element.tagName.toLowerCase()} "${label}"`,
|
||||
};
|
||||
}, CTA_LABEL);
|
||||
}
|
||||
|
||||
/** Visita nova: foco no documento, topo da página. */
|
||||
async function openHomeAsNewVisitor(page: Page) {
|
||||
await page.goto("/", { waitUntil: "load" });
|
||||
await page.waitForTimeout(800);
|
||||
// o foco começa no documento, como numa visita nova
|
||||
await page.evaluate(() => (document.activeElement as HTMLElement)?.blur());
|
||||
await page.evaluate(() => window.scrollTo({ top: 0, behavior: "instant" }));
|
||||
}
|
||||
|
||||
/** Aperta Tab até o foco satisfazer `stop`, no máximo `giveUpAfter` vezes. `reached` é 0 se nunca satisfez. */
|
||||
async function tabUntil(
|
||||
page: Page,
|
||||
stop: (where: Awaited<ReturnType<typeof focusedElement>>) => boolean,
|
||||
giveUpAfter: number,
|
||||
) {
|
||||
const visited: string[] = [];
|
||||
let reached = 0;
|
||||
for (let tab = 1; tab <= GIVE_UP_AFTER; tab++) {
|
||||
for (let tab = 1; tab <= giveUpAfter; tab++) {
|
||||
await page.keyboard.press("Tab");
|
||||
const where = await page.evaluate(() => {
|
||||
const element = document.activeElement as HTMLElement | null;
|
||||
if (!element) return { isName: false, label: "(nada)" };
|
||||
const label = (
|
||||
element.getAttribute("aria-label") ||
|
||||
element.textContent ||
|
||||
element.getAttribute("name") ||
|
||||
element.tagName
|
||||
)
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.slice(0, 40);
|
||||
return {
|
||||
isName:
|
||||
element.getAttribute("name") === "name" &&
|
||||
Boolean(element.closest("#contact")),
|
||||
label: `${element.tagName.toLowerCase()} "${label}"`,
|
||||
};
|
||||
});
|
||||
const where = await focusedElement(page);
|
||||
visited.push(`${tab}. ${where.label}`);
|
||||
if (where.isName) {
|
||||
reached = tab;
|
||||
break;
|
||||
}
|
||||
if (stop(where)) return { reached: tab, visited };
|
||||
}
|
||||
return { reached: 0, visited };
|
||||
}
|
||||
|
||||
test(`atalho: Tab até "${CTA_LABEL}" do cabeçalho, Enter, e o campo de nome em até ${SHORTCUT_MAX_TABS_AFTER_ENTER} Tabs`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomeAsNewVisitor(page);
|
||||
|
||||
const toCta = await tabUntil(
|
||||
page,
|
||||
({ isCta }) => isCta,
|
||||
SHORTCUT_MAX_TABS_TO_CTA,
|
||||
);
|
||||
expect(
|
||||
toCta.reached,
|
||||
`"${CTA_LABEL}" do cabeçalho não recebeu foco em ${SHORTCUT_MAX_TABS_TO_CTA} Tabs; passou por:\n${toCta.visited.join("\n")}`,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForURL(/#contact$/);
|
||||
// a rolagem até o contato e o foco da âncora assentam
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const afterEnter = await tabUntil(
|
||||
page,
|
||||
({ isName }) => isName,
|
||||
SHORTCUT_MAX_TABS_AFTER_ENTER + 20,
|
||||
);
|
||||
expect(
|
||||
afterEnter.reached,
|
||||
`o campo de nome não apareceu depois do Enter; passou por:\n${afterEnter.visited.join("\n")}`,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
afterEnter.reached,
|
||||
`depois do Enter, o campo de nome só foi alcançado no Tab ${afterEnter.reached} (limite ${SHORTCUT_MAX_TABS_AFTER_ENTER}); passou por:\n${afterEnter.visited.join("\n")}`,
|
||||
).toBeLessThanOrEqual(SHORTCUT_MAX_TABS_AFTER_ENTER);
|
||||
await expect(page.locator("#contact input[name='name']")).toBeFocused();
|
||||
});
|
||||
|
||||
test(`caminho linear: do topo da home, Tab chega ao campo de nome do contato em até ${LINEAR_MAX_TABS} toques`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomeAsNewVisitor(page);
|
||||
|
||||
const { reached, visited } = await tabUntil(
|
||||
page,
|
||||
({ isName }) => isName,
|
||||
LINEAR_GIVE_UP_AFTER,
|
||||
);
|
||||
|
||||
expect(
|
||||
reached,
|
||||
`o campo de nome não apareceu nem em ${GIVE_UP_AFTER} Tabs`,
|
||||
`o campo de nome não apareceu nem em ${LINEAR_GIVE_UP_AFTER} Tabs`,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
reached,
|
||||
`o campo de nome só foi alcançado no Tab ${reached} (limite ${MAX_TABS}); passou por:\n${visited.join("\n")}`,
|
||||
).toBeLessThanOrEqual(MAX_TABS);
|
||||
`o campo de nome só foi alcançado no Tab ${reached} (limite ${LINEAR_MAX_TABS}); passou por:\n${visited.join("\n")}`,
|
||||
).toBeLessThanOrEqual(LINEAR_MAX_TABS);
|
||||
await expect(page.locator("#contact input[name='name']")).toBeFocused();
|
||||
});
|
||||
Reference in new issue
Block a user