Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
137 lines
5.2 KiB
TypeScript
137 lines
5.2 KiB
TypeScript
import { render, screen, within } from "@testing-library/react";
|
|
import ClientStrip from "./ClientStrip";
|
|
|
|
const clients = [
|
|
{ id: "sev-modaintima", name: "SEV Moda Íntima" },
|
|
{ id: "lavacao-047", name: "Lavação 047" },
|
|
];
|
|
|
|
it("lists each client as a link to its case", () => {
|
|
render(<ClientStrip clients={clients} />);
|
|
|
|
expect(screen.getByText("Já entregue para:")).toBeInTheDocument();
|
|
const list = screen.getByRole("list");
|
|
const links = within(list).getAllByRole("link");
|
|
expect(links.map((link) => link.textContent)).toEqual([
|
|
"SEV Moda Íntima",
|
|
"Lavação 047",
|
|
]);
|
|
expect(links.map((link) => link.getAttribute("href"))).toEqual([
|
|
"/cases/sev-modaintima",
|
|
"/cases/lavacao-047",
|
|
]);
|
|
});
|
|
|
|
it("renders nothing without clients", () => {
|
|
const { container } = render(<ClientStrip clients={[]} />);
|
|
expect(container).toBeEmptyDOMElement();
|
|
|
|
const withoutProp = render(<ClientStrip />);
|
|
expect(withoutProp.container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it("puts the years of experience in the same line, after the clients", () => {
|
|
render(<ClientStrip clients={clients} years={5} />);
|
|
|
|
const experience = screen.getByText("anos de experiência");
|
|
expect(screen.getByText("5")).toBeInTheDocument();
|
|
const lastClient = screen.getByRole("link", { name: "Lavação 047" });
|
|
expect(
|
|
lastClient.compareDocumentPosition(experience) &
|
|
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
).toBeTruthy();
|
|
expect(experience.closest("div")).toBe(
|
|
screen.getByText("Já entregue para:").parentElement,
|
|
);
|
|
});
|
|
|
|
it("shows only the experience when there are no clients, and nothing without either", () => {
|
|
const only = render(<ClientStrip years={5} />);
|
|
expect(screen.queryByText("Já entregue para:")).toBeNull();
|
|
expect(screen.getByText("anos de experiência")).toBeInTheDocument();
|
|
only.unmount();
|
|
|
|
const none = render(<ClientStrip years={0} />);
|
|
expect(none.container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it("gives every client link 44 px of touch target on the phone (min-h-11) and 24 px from 640 px on, where the sector shares the item", () => {
|
|
render(<ClientStrip clients={clients} />);
|
|
|
|
for (const link of screen.getAllByRole("link")) {
|
|
expect(link).toHaveClass("min-h-11", "sm:min-h-6");
|
|
}
|
|
});
|
|
|
|
it("puts the separator only BETWEEN items: none before the first, one before each of the others (years included)", () => {
|
|
const withYears = render(<ClientStrip clients={clients} years={5} />);
|
|
const items = withYears.container.querySelectorAll("li");
|
|
expect(items).toHaveLength(clients.length + 1);
|
|
expect(
|
|
Array.from(items).map(
|
|
(item) => item.querySelector("[aria-hidden='true']") !== null,
|
|
),
|
|
).toEqual([false, true, true]);
|
|
// nenhum item termina com o ponto: ele sempre abre o item, para a margem negativa escondê-lo no começo de uma linha
|
|
for (const item of items) {
|
|
expect(item.lastElementChild).not.toHaveAttribute("aria-hidden");
|
|
}
|
|
withYears.unmount();
|
|
|
|
// zero anos não escreve um "0" solto nem um ponto a mais
|
|
const zero = render(<ClientStrip clients={clients} years={0} />);
|
|
expect(
|
|
zero.container.querySelectorAll("li [aria-hidden='true']"),
|
|
).toHaveLength(clients.length - 1);
|
|
expect(zero.container).not.toHaveTextContent(/anos de experiência/);
|
|
expect(screen.queryByText("0")).toBeNull();
|
|
});
|
|
|
|
it("hides the separator of an item that wraps to the next line: negative margin on every item but the first, and the list clips the overflow on the x axis", () => {
|
|
const { container } = render(<ClientStrip clients={clients} years={5} />);
|
|
|
|
const list = container.querySelector("ul") as HTMLElement;
|
|
expect(list).toHaveClass("flex-wrap", "overflow-x-clip");
|
|
const items = Array.from(list.querySelectorAll("li"));
|
|
expect(items[0]).not.toHaveClass("-ml-4");
|
|
for (const item of items.slice(1)) {
|
|
expect(item).toHaveClass("-ml-4");
|
|
// o ponto ocupa os mesmos 16 px que a margem negativa
|
|
expect(item.querySelector("[aria-hidden='true']")).toHaveClass("w-4");
|
|
}
|
|
for (const item of items) expect(item).toHaveClass("pr-4");
|
|
});
|
|
|
|
it("keeps the years number in the display font (the mono font is not downloaded above the fold)", () => {
|
|
render(<ClientStrip clients={clients} years={5} />);
|
|
|
|
expect(screen.getByText("5").className).not.toContain("font-mono");
|
|
});
|
|
|
|
it("stacks the sector under each name from 640 px on, outside the link, and shows only the name for a client without one", () => {
|
|
render(
|
|
<ClientStrip
|
|
clients={[
|
|
{
|
|
id: "sev-modaintima",
|
|
name: "SEV Moda Íntima",
|
|
sector: "loja virtual",
|
|
},
|
|
{ id: "lavacao-047", name: "Lavação 047" },
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
const sector = screen.getByText("loja virtual");
|
|
// 13 px e apagado, e só de 640 px em diante (abaixo, só os nomes)
|
|
expect(sector).toHaveClass("text-[13px]", "max-sm:hidden");
|
|
const link = screen.getByRole("link", { name: "SEV Moda Íntima" });
|
|
expect(link).not.toContainElement(sector);
|
|
expect(link.parentElement).toContainElement(sector);
|
|
expect(link.parentElement).toHaveClass("flex-col");
|
|
// o nome continua levando ao case
|
|
expect(link).toHaveAttribute("href", "/cases/sev-modaintima");
|
|
const plain = screen.getByRole("link", { name: "Lavação 047" });
|
|
expect(plain.parentElement?.childElementCount).toBe(1);
|
|
});
|