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.
248 lines
8.4 KiB
TypeScript
248 lines
8.4 KiB
TypeScript
import { act, render, screen, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import Contact from "./Contact";
|
|
import { CONTACT_KIND_EVENT } from "./contact-subject";
|
|
|
|
/**
|
|
* O tipo de projeto do formulário vindo da oferta escolhida na home: pelo evento que o botão da oferta dispara
|
|
* (na própria home) ou pelo `?tipo=` do endereço (ao chegar de outra página). As três ofertas entram no formulário
|
|
* com o nome que têm na home, o formulário avisa "Você escolheu: <oferta>" e o link direto do WhatsApp leva o tipo.
|
|
*/
|
|
|
|
const subjectSelect = () =>
|
|
screen.getByRole("combobox", {
|
|
name: /tipo de projeto/i,
|
|
}) as HTMLSelectElement;
|
|
|
|
const choose = (kind: unknown) =>
|
|
act(() => {
|
|
window.dispatchEvent(new CustomEvent(CONTACT_KIND_EVENT, { detail: kind }));
|
|
});
|
|
|
|
afterEach(() => {
|
|
window.history.replaceState(null, "", "/");
|
|
});
|
|
|
|
describe("Contact: opções do tipo de projeto", () => {
|
|
it("lists the five offers by their names and keeps 'Ainda preciso definir' first and selected", () => {
|
|
render(<Contact />);
|
|
|
|
const options = within(subjectSelect()).getAllByRole("option");
|
|
expect(options.map((option) => option.textContent)).toEqual([
|
|
"Ainda preciso definir",
|
|
"Site que traz pedidos pelo WhatsApp",
|
|
"Sistema para tirar a operação da planilha",
|
|
"Atendimento no WhatsApp com IA sob controle",
|
|
"Aplicativos",
|
|
"Integrações e automações",
|
|
"Evolução de um sistema existente",
|
|
"Revisão de sistema que já existe",
|
|
"Nota fiscal pronta para a reforma tributária",
|
|
]);
|
|
expect(subjectSelect()).toHaveValue("Ainda preciso definir");
|
|
});
|
|
|
|
it("puts the review type in the message that goes to WhatsApp and e-mail", async () => {
|
|
const user = userEvent.setup({ delay: null });
|
|
render(<Contact />);
|
|
|
|
await user.selectOptions(
|
|
subjectSelect(),
|
|
"Revisão de sistema que já existe",
|
|
);
|
|
|
|
const mail = screen.getByRole("link", { name: /e-mail/i });
|
|
const body = new URL(mail.getAttribute("href") ?? "").searchParams.get(
|
|
"body",
|
|
);
|
|
expect(body).toContain("Tipo de projeto: Revisão de sistema que já existe");
|
|
});
|
|
});
|
|
|
|
describe("Contact: tipo escolhido pela oferta", () => {
|
|
it.each([
|
|
["site", "Site que traz pedidos pelo WhatsApp"],
|
|
["sistema", "Sistema para tirar a operação da planilha"],
|
|
["whatsapp", "Atendimento no WhatsApp com IA sob controle"],
|
|
["revisao", "Revisão de sistema que já existe"],
|
|
["fiscal", "Nota fiscal pronta para a reforma tributária"],
|
|
])("the offer event with %s selects %s", (kind, option) => {
|
|
render(<Contact />);
|
|
|
|
choose(kind);
|
|
|
|
expect(subjectSelect()).toHaveValue(option);
|
|
});
|
|
|
|
it("a later offer replaces the earlier choice, and a person's own pick is replaced by the offer too", async () => {
|
|
const user = userEvent.setup({ delay: null });
|
|
render(<Contact />);
|
|
|
|
choose("site");
|
|
await user.selectOptions(subjectSelect(), "Aplicativos");
|
|
expect(subjectSelect()).toHaveValue("Aplicativos");
|
|
choose("revisao");
|
|
|
|
expect(subjectSelect()).toHaveValue("Revisão de sistema que já existe");
|
|
});
|
|
|
|
it.each([
|
|
"desconhecido",
|
|
"constructor",
|
|
"__proto__",
|
|
"",
|
|
undefined,
|
|
null,
|
|
42,
|
|
])("an unknown type %p changes nothing", (kind) => {
|
|
render(<Contact />);
|
|
choose("sistema");
|
|
|
|
choose(kind);
|
|
|
|
expect(subjectSelect()).toHaveValue(
|
|
"Sistema para tirar a operação da planilha",
|
|
);
|
|
});
|
|
|
|
it("reads ?tipo= from the address when the form mounts (arrival from another page)", () => {
|
|
window.history.replaceState(null, "", "/?tipo=revisao#contact-form");
|
|
|
|
render(<Contact />);
|
|
|
|
expect(subjectSelect()).toHaveValue("Revisão de sistema que já existe");
|
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
"Você escolheu: Revisão de sistema que já existe",
|
|
);
|
|
});
|
|
|
|
it("ignores an unknown ?tipo= in the address and keeps the default", () => {
|
|
window.history.replaceState(null, "", "/?tipo=outra-coisa");
|
|
|
|
render(<Contact />);
|
|
|
|
expect(subjectSelect()).toHaveValue("Ainda preciso definir");
|
|
});
|
|
|
|
it("stops listening when it unmounts", () => {
|
|
const add = jest.spyOn(window, "addEventListener");
|
|
const remove = jest.spyOn(window, "removeEventListener");
|
|
const { unmount } = render(<Contact />);
|
|
const registered = add.mock.calls.find(
|
|
([type]) => type === CONTACT_KIND_EVENT,
|
|
);
|
|
expect(registered).toBeDefined();
|
|
|
|
unmount();
|
|
|
|
expect(remove).toHaveBeenCalledWith(CONTACT_KIND_EVENT, registered?.[1]);
|
|
add.mockRestore();
|
|
remove.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("Contact: você escolheu e o WhatsApp direto", () => {
|
|
const directLink = () =>
|
|
screen.getByRole("link", { name: /Prefere conversar direto/ });
|
|
const directMessage = () =>
|
|
new URL(directLink().getAttribute("href") ?? "").searchParams.get("text");
|
|
|
|
it("renders the form card with the id the offers scroll to, and an empty status until an offer is chosen", () => {
|
|
const { container } = render(<Contact />);
|
|
|
|
expect(container.querySelector("form")).toHaveAttribute(
|
|
"id",
|
|
"contact-form",
|
|
);
|
|
expect(screen.getByRole("status")).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it.each([
|
|
["site", "Site que traz pedidos pelo WhatsApp"],
|
|
["sistema", "Sistema para tirar a operação da planilha"],
|
|
["whatsapp", "Atendimento no WhatsApp com IA sob controle"],
|
|
["revisao", "Revisão de sistema que já existe"],
|
|
["fiscal", "Nota fiscal pronta para a reforma tributária"],
|
|
])(
|
|
"after the offer %s the form says 'Você escolheu: <name>'",
|
|
(kind, name) => {
|
|
render(<Contact />);
|
|
|
|
choose(kind);
|
|
|
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
`Você escolheu: ${name}`,
|
|
);
|
|
expect(within(screen.getByRole("status")).getByText(name).tagName).toBe(
|
|
"STRONG",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("drops the message when the person picks another type in the form, and keeps it for an unknown offer", async () => {
|
|
const user = userEvent.setup({ delay: null });
|
|
render(<Contact />);
|
|
|
|
choose("desconhecido");
|
|
expect(screen.getByRole("status")).toBeEmptyDOMElement();
|
|
choose("sistema");
|
|
expect(screen.getByRole("status")).not.toBeEmptyDOMElement();
|
|
await user.selectOptions(subjectSelect(), "Aplicativos");
|
|
|
|
expect(screen.getByRole("status")).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it("takes the chosen type to the direct WhatsApp link of the contact column, as a phrase, and back to 'um projeto'", async () => {
|
|
const user = userEvent.setup({ delay: null });
|
|
render(<Contact />);
|
|
expect(directMessage()).toBe(
|
|
"Olá, Evandro! Quero conversar sobre um projeto. Contato iniciado pelo site evandro.dev.br.",
|
|
);
|
|
|
|
choose("sistema");
|
|
expect(directMessage()).toBe(
|
|
"Olá, Evandro! Quero conversar sobre um sistema para tirar a operação da planilha. Contato iniciado pelo site evandro.dev.br.",
|
|
);
|
|
choose("revisao");
|
|
expect(directMessage()).toContain(
|
|
"Quero conversar sobre a revisão de um sistema que já existe.",
|
|
);
|
|
choose("whatsapp");
|
|
expect(directMessage()).toContain(
|
|
"Quero conversar sobre um atendimento no WhatsApp com IA sob controle.",
|
|
);
|
|
choose("fiscal");
|
|
expect(directMessage()).toContain(
|
|
"Quero conversar sobre a adaptação da nota fiscal à reforma tributária.",
|
|
);
|
|
// quem escolhe o mesmo tipo pelo formulário também o leva para a mensagem
|
|
await user.selectOptions(
|
|
subjectSelect(),
|
|
"Site que traz pedidos pelo WhatsApp",
|
|
);
|
|
expect(directMessage()).toContain(
|
|
"Quero conversar sobre um site que traz pedidos pelo WhatsApp.",
|
|
);
|
|
// um tipo sem oferta volta para a frase de "um projeto"
|
|
await user.selectOptions(subjectSelect(), "Aplicativos");
|
|
expect(directMessage()).toContain("Quero conversar sobre um projeto.");
|
|
// nada de nome de oferta com maiúscula na frase
|
|
expect(directMessage()).not.toMatch(/sobre [A-Z]/);
|
|
});
|
|
});
|
|
|
|
describe("Contact: texto da seção", () => {
|
|
it("keeps 'conversar direto' only on the WhatsApp line, with no other 'fala direto' phrasing", () => {
|
|
const { container } = render(<Contact />);
|
|
|
|
expect(screen.getByText("Prefere conversar direto?")).toBeInTheDocument();
|
|
expect(container.textContent).not.toMatch(/fala direto|fale direto/i);
|
|
expect(
|
|
screen.getByRole("heading", {
|
|
level: 3,
|
|
name: "Como a conversa começa.",
|
|
}),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|