@1doc/1ds-react 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1106 -0
- package/dist/components/AILabel/AILabel.js +1 -1
- package/dist/components/Accordion/Accordion.d.ts +6 -2
- package/dist/components/AttachmentCard/AttachmentCard.css +16 -0
- package/dist/components/AttachmentCard/AttachmentCard.d.ts +19 -2
- package/dist/components/AttachmentCard/AttachmentCard.js +40 -3
- package/dist/components/Avatar/Avatar.css +7 -2
- package/dist/components/Avatar/AvatarGroup.d.ts +17 -0
- package/dist/components/Avatar/AvatarGroup.js +22 -8
- package/dist/components/Badge/Badge.css +54 -2
- package/dist/components/Badge/Badge.d.ts +14 -4
- package/dist/components/Badge/Badge.js +1 -0
- package/dist/components/Box/Box.css +83 -0
- package/dist/components/Box/Box.d.ts +82 -2
- package/dist/components/Box/Box.js +42 -2
- package/dist/components/Button/Button.css +29 -0
- package/dist/components/Button/Button.d.ts +4 -4
- package/dist/components/Button/Button.js +4 -1
- package/dist/components/Calendar/Calendar.css +245 -0
- package/dist/components/Calendar/Calendar.d.ts +125 -0
- package/dist/components/Calendar/Calendar.js +320 -0
- package/dist/components/Chips/Chips.css +38 -0
- package/dist/components/Chips/Chips.d.ts +5 -2
- package/dist/components/Chips/Chips.js +2 -1
- package/dist/components/Coachmark/Coachmark.css +118 -0
- package/dist/components/Coachmark/Coachmark.d.ts +75 -0
- package/dist/components/Coachmark/Coachmark.js +321 -0
- package/dist/components/CodeBlock/CodeBlock.css +96 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +47 -0
- package/dist/components/CodeBlock/CodeBlock.js +103 -0
- package/dist/components/ColorPicker/ColorPicker.css +133 -0
- package/dist/components/ColorPicker/ColorPicker.d.ts +73 -0
- package/dist/components/ColorPicker/ColorPicker.js +219 -0
- package/dist/components/Combobox/Combobox.css +8 -0
- package/dist/components/DataTable/DataTable.css +80 -0
- package/dist/components/DataTable/DataTable.d.ts +19 -4
- package/dist/components/DataTable/DataTable.js +59 -15
- package/dist/components/DatePicker/DatePicker.css +8 -0
- package/dist/components/DatePicker/DatePicker.js +10 -1
- package/dist/components/DescriptionList/DescriptionList.css +113 -0
- package/dist/components/DescriptionList/DescriptionList.d.ts +59 -0
- package/dist/components/DescriptionList/DescriptionList.js +51 -0
- package/dist/components/FileInput/FileInput.css +16 -0
- package/dist/components/FileInput/FileInput.d.ts +4 -2
- package/dist/components/FileInput/FileInput.js +8 -2
- package/dist/components/Grid/Grid.js +5 -5
- package/dist/components/IconPicker/IconPicker.css +107 -0
- package/dist/components/IconPicker/IconPicker.d.ts +74 -0
- package/dist/components/IconPicker/IconPicker.js +194 -0
- package/dist/components/Input/Input.css +38 -0
- package/dist/components/Input/Input.d.ts +10 -1
- package/dist/components/Input/Input.js +66 -5
- package/dist/components/Kanban/Kanban.css +4 -0
- package/dist/components/Kbd/Kbd.css +66 -0
- package/dist/components/Kbd/Kbd.d.ts +32 -0
- package/dist/components/Kbd/Kbd.js +40 -0
- package/dist/components/List/List.css +136 -0
- package/dist/components/List/List.d.ts +82 -0
- package/dist/components/List/List.js +53 -0
- package/dist/components/List/ListItem.d.ts +85 -0
- package/dist/components/List/ListItem.js +81 -0
- package/dist/components/Modal/Modal.css +80 -0
- package/dist/components/Modal/Modal.d.ts +33 -6
- package/dist/components/Modal/Modal.js +101 -6
- package/dist/components/MultiSelect/MultiSelect.css +8 -0
- package/dist/components/MultiSelect/MultiSelect.js +10 -1
- package/dist/components/OrgChart/OrgChart.css +149 -0
- package/dist/components/OrgChart/OrgChart.d.ts +83 -0
- package/dist/components/OrgChart/OrgChart.js +297 -0
- package/dist/components/OtpInput/OtpInput.css +93 -0
- package/dist/components/OtpInput/OtpInput.d.ts +60 -0
- package/dist/components/OtpInput/OtpInput.js +215 -0
- package/dist/components/PhoneInput/PhoneInput.css +8 -0
- package/dist/components/Popover/Popover.d.ts +7 -0
- package/dist/components/Popover/Popover.js +11 -1
- package/dist/components/RichTextEditor/RichTextEditor.css +203 -0
- package/dist/components/RichTextEditor/RichTextEditor.d.ts +83 -0
- package/dist/components/RichTextEditor/RichTextEditor.js +494 -0
- package/dist/components/SegmentedControl/SegmentedControl.js +7 -2
- package/dist/components/Select/Select.css +70 -0
- package/dist/components/Select/Select.d.ts +12 -0
- package/dist/components/Select/Select.js +59 -4
- package/dist/components/Stack/Stack.css +49 -0
- package/dist/components/Stack/Stack.d.ts +65 -7
- package/dist/components/Stack/Stack.js +30 -2
- package/dist/components/StatCard/StatCard.css +73 -0
- package/dist/components/StatCard/StatCard.d.ts +17 -0
- package/dist/components/StatCard/StatCard.js +4 -1
- package/dist/components/Tabs/Tab.d.ts +15 -0
- package/dist/components/Tabs/Tab.js +30 -1
- package/dist/components/Tabs/Tabs.css +132 -0
- package/dist/components/Tabs/Tabs.d.ts +7 -0
- package/dist/components/Tabs/Tabs.js +13 -2
- package/dist/components/Tag/Tag.css +85 -2
- package/dist/components/Tag/Tag.d.ts +13 -6
- package/dist/components/Tag/Tag.js +1 -0
- package/dist/components/Text/Text.js +2 -2
- package/dist/components/Textarea/Textarea.css +8 -0
- package/dist/components/TimePicker/TimePicker.css +8 -0
- package/dist/components/Tooltip/Tooltip.css +25 -0
- package/dist/components/Tooltip/Tooltip.d.ts +11 -0
- package/dist/components/Tooltip/Tooltip.js +18 -3
- package/dist/index.d.ts +25 -0
- package/dist/index.js +14 -0
- package/dist/teste/AILabel.test.d.ts +1 -0
- package/dist/teste/AILabel.test.js +33 -0
- package/dist/teste/AccordionConteudoLivre.test.d.ts +1 -0
- package/dist/teste/AccordionConteudoLivre.test.js +29 -0
- package/dist/teste/AtributosRepassados.test.d.ts +1 -0
- package/dist/teste/AtributosRepassados.test.js +52 -0
- package/dist/teste/AttachmentCardIA.test.d.ts +1 -0
- package/dist/teste/AttachmentCardIA.test.js +89 -0
- package/dist/teste/AvatarGroupOverlap.test.d.ts +1 -0
- package/dist/teste/AvatarGroupOverlap.test.js +56 -0
- package/dist/teste/BadgeTag.test.d.ts +1 -0
- package/dist/teste/BadgeTag.test.js +42 -0
- package/dist/teste/BoxItemFlex.test.d.ts +1 -0
- package/dist/teste/BoxItemFlex.test.js +54 -0
- package/dist/teste/BoxLayout.test.d.ts +1 -0
- package/dist/teste/BoxLayout.test.js +35 -0
- package/dist/teste/Button.test.d.ts +1 -0
- package/dist/teste/Button.test.js +27 -0
- package/dist/teste/Calendar.test.d.ts +1 -0
- package/dist/teste/Calendar.test.js +221 -0
- package/dist/teste/ChipsXs.test.d.ts +1 -0
- package/dist/teste/ChipsXs.test.js +31 -0
- package/dist/teste/Coachmark.test.d.ts +1 -0
- package/dist/teste/Coachmark.test.js +149 -0
- package/dist/teste/ColorPicker.test.d.ts +1 -0
- package/dist/teste/ColorPicker.test.js +132 -0
- package/dist/teste/DataTableCelulaLivre.test.d.ts +1 -0
- package/dist/teste/DataTableCelulaLivre.test.js +76 -0
- package/dist/teste/DataTableDocumento.test.d.ts +1 -0
- package/dist/teste/DataTableDocumento.test.js +92 -0
- package/dist/teste/DatePicker.test.js +23 -0
- package/dist/teste/FileInputTrigger.test.d.ts +1 -0
- package/dist/teste/FileInputTrigger.test.js +53 -0
- package/dist/teste/IconPicker.test.d.ts +1 -0
- package/dist/teste/IconPicker.test.js +130 -0
- package/dist/teste/InputMascaraEAfins.test.d.ts +1 -0
- package/dist/teste/InputMascaraEAfins.test.js +99 -0
- package/dist/teste/KbdCodeBlock.test.d.ts +1 -0
- package/dist/teste/KbdCodeBlock.test.js +95 -0
- package/dist/teste/List.test.d.ts +1 -0
- package/dist/teste/List.test.js +153 -0
- package/dist/teste/ModalVariantes.test.d.ts +1 -0
- package/dist/teste/ModalVariantes.test.js +151 -0
- package/dist/teste/NomeAcessivelSsr.test.d.ts +1 -0
- package/dist/teste/NomeAcessivelSsr.test.js +88 -0
- package/dist/teste/OrgChart.test.d.ts +1 -0
- package/dist/teste/OrgChart.test.js +162 -0
- package/dist/teste/OtpInput.test.d.ts +1 -0
- package/dist/teste/OtpInput.test.js +143 -0
- package/dist/teste/PopoverMaxHeight.test.d.ts +1 -0
- package/dist/teste/PopoverMaxHeight.test.js +38 -0
- package/dist/teste/RichTextEditor.test.d.ts +1 -0
- package/dist/teste/RichTextEditor.test.js +287 -0
- package/dist/teste/SelectPlacementFiltro.test.d.ts +1 -0
- package/dist/teste/SelectPlacementFiltro.test.js +88 -0
- package/dist/teste/Stack.test.d.ts +1 -0
- package/dist/teste/Stack.test.js +57 -0
- package/dist/teste/StatCardRotuloTopo.test.d.ts +1 -0
- package/dist/teste/StatCardRotuloTopo.test.js +34 -0
- package/dist/teste/TabsContadorFechavel.test.d.ts +1 -0
- package/dist/teste/TabsContadorFechavel.test.js +79 -0
- package/dist/teste/TooltipDark.test.d.ts +1 -0
- package/dist/teste/TooltipDark.test.js +31 -0
- package/dist/utils/calendario.d.ts +28 -0
- package/dist/utils/calendario.js +73 -0
- package/dist/utils/html-seguro.d.ts +27 -0
- package/dist/utils/html-seguro.js +97 -0
- package/dist/utils/paleta-de-cores.d.ts +26 -0
- package/dist/utils/paleta-de-cores.js +10 -0
- package/dist/utils/texto.d.ts +5 -0
- package/dist/utils/texto.js +5 -0
- package/package.json +3 -3
- package/src/index.ts +26 -0
- package/src/teste/AILabel.test.tsx +41 -0
- package/src/teste/AccordionConteudoLivre.test.tsx +39 -0
- package/src/teste/AtributosRepassados.test.tsx +73 -0
- package/src/teste/AttachmentCardIA.test.tsx +126 -0
- package/src/teste/AvatarGroupOverlap.test.tsx +74 -0
- package/src/teste/BadgeTag.test.tsx +64 -0
- package/src/teste/BoxItemFlex.test.tsx +81 -0
- package/src/teste/BoxLayout.test.tsx +50 -0
- package/src/teste/Button.test.tsx +39 -0
- package/src/teste/Calendar.test.tsx +282 -0
- package/src/teste/ChipsXs.test.tsx +41 -0
- package/src/teste/Coachmark.test.tsx +198 -0
- package/src/teste/ColorPicker.test.tsx +153 -0
- package/src/teste/DataTableCelulaLivre.test.tsx +103 -0
- package/src/teste/DataTableDocumento.test.tsx +102 -0
- package/src/teste/DatePicker.test.tsx +38 -0
- package/src/teste/FileInputTrigger.test.tsx +89 -0
- package/src/teste/IconPicker.test.tsx +145 -0
- package/src/teste/InputMascaraEAfins.test.tsx +124 -0
- package/src/teste/KbdCodeBlock.test.tsx +106 -0
- package/src/teste/List.test.tsx +269 -0
- package/src/teste/ModalVariantes.test.tsx +173 -0
- package/src/teste/NomeAcessivelSsr.test.tsx +110 -0
- package/src/teste/OrgChart.test.tsx +183 -0
- package/src/teste/OtpInput.test.tsx +163 -0
- package/src/teste/PopoverMaxHeight.test.tsx +47 -0
- package/src/teste/RichTextEditor.test.tsx +329 -0
- package/src/teste/SelectPlacementFiltro.test.tsx +110 -0
- package/src/teste/Stack.test.tsx +80 -0
- package/src/teste/StatCardRotuloTopo.test.tsx +44 -0
- package/src/teste/TabsContadorFechavel.test.tsx +144 -0
- package/src/teste/TooltipDark.test.tsx +51 -0
- package/src/utils/calendario.ts +78 -0
- package/src/utils/html-seguro.ts +94 -0
- package/src/utils/paleta-de-cores.ts +34 -0
- package/src/utils/texto.ts +5 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { act, render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import Kbd from "../components/Kbd/Kbd";
|
|
5
|
+
import CodeBlock from "../components/CodeBlock/CodeBlock";
|
|
6
|
+
|
|
7
|
+
describe("Kbd", () => {
|
|
8
|
+
it("uma tecla é um kbd com o texto", () => {
|
|
9
|
+
const { container } = render(<Kbd>Esc</Kbd>);
|
|
10
|
+
const tecla = container.querySelector("kbd") as HTMLElement;
|
|
11
|
+
expect(tecla).toHaveTextContent("Esc");
|
|
12
|
+
expect(tecla).toHaveClass("umds-kbd", "umds-kbd--md");
|
|
13
|
+
expect(container.querySelectorAll("kbd")).toHaveLength(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("keys desenha uma tecla por item, com o separador entre elas", () => {
|
|
17
|
+
const { container } = render(<Kbd keys={["Ctrl", "Shift", "P"]} />);
|
|
18
|
+
const conjunto = container.querySelector("kbd.umds-kbd-combo") as HTMLElement;
|
|
19
|
+
expect(conjunto).not.toBeNull();
|
|
20
|
+
expect(conjunto.querySelectorAll("kbd.umds-kbd")).toHaveLength(3);
|
|
21
|
+
expect(conjunto.textContent).toBe("Ctrl+Shift+P");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("separator troca o texto entre as teclas; size vira classe", () => {
|
|
25
|
+
const { container } = render(<Kbd keys={["Ctrl", "K"]} separator=" + " size="sm" />);
|
|
26
|
+
expect(container.querySelector("kbd")).toHaveClass("umds-kbd--sm");
|
|
27
|
+
expect(container.textContent).toBe("Ctrl + K");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("atributos HTML vão para o kbd", () => {
|
|
31
|
+
const { container } = render(<Kbd title="Atalho" data-x="1">Tab</Kbd>);
|
|
32
|
+
const tecla = container.querySelector("kbd") as HTMLElement;
|
|
33
|
+
expect(tecla).toHaveAttribute("title", "Atalho");
|
|
34
|
+
expect(tecla).toHaveAttribute("data-x", "1");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("CodeBlock", () => {
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.useRealTimers();
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("mostra o código em pre/code, mantendo as quebras, numa região focável e nomeada", () => {
|
|
45
|
+
const { container } = render(<CodeBlock code={'{\n "ok": true\n}'} ariaLabel="Resposta" />);
|
|
46
|
+
const regiao = screen.getByRole("region", { name: "Resposta" });
|
|
47
|
+
expect(regiao.tagName).toBe("PRE");
|
|
48
|
+
expect(regiao).toHaveAttribute("tabindex", "0");
|
|
49
|
+
expect(container.querySelector("code")?.textContent).toBe('{\n "ok": true\n}');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("o texto também vem como conteúdo; o nome padrão é a linguagem, ou Código", () => {
|
|
53
|
+
const { rerender } = render(<CodeBlock language="JSON">{"{}"}</CodeBlock>);
|
|
54
|
+
expect(screen.getByRole("region", { name: "JSON" })).toHaveTextContent("{}");
|
|
55
|
+
expect(screen.getByText("JSON", { selector: ".umds-code-block__language" })).toBeInTheDocument();
|
|
56
|
+
rerender(<CodeBlock code="x" />);
|
|
57
|
+
expect(screen.getByRole("region", { name: "Código" })).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("wrap e maxHeight", () => {
|
|
61
|
+
const { container } = render(<CodeBlock code="linha longa" wrap maxHeight={120} />);
|
|
62
|
+
expect(container.querySelector(".umds-code-block--wrap")).not.toBeNull();
|
|
63
|
+
expect((container.querySelector("pre") as HTMLElement).style.maxHeight).toBe("120px");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("sem copyable e sem linguagem, não há cabeçalho nem botão", () => {
|
|
67
|
+
render(<CodeBlock code="x" />);
|
|
68
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("copiar manda o texto inteiro à área de transferência, confirma e volta depois de 2 s", async () => {
|
|
72
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
73
|
+
const escrever = vi.fn(() => Promise.resolve());
|
|
74
|
+
const aoCopiar = vi.fn();
|
|
75
|
+
const usuario = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
76
|
+
// O userEvent.setup instala a própria área de transferência: o espião entra depois.
|
|
77
|
+
Object.defineProperty(navigator, "clipboard", { value: { writeText: escrever }, configurable: true });
|
|
78
|
+
render(<CodeBlock code={'{"a":1}'} copyable onCopy={aoCopiar} />);
|
|
79
|
+
await usuario.click(screen.getByRole("button", { name: "Copiar" }));
|
|
80
|
+
expect(escrever).toHaveBeenCalledWith('{"a":1}');
|
|
81
|
+
expect(await screen.findByRole("button", { name: "Copiado" })).toBeInTheDocument();
|
|
82
|
+
expect(aoCopiar).toHaveBeenCalledWith('{"a":1}');
|
|
83
|
+
expect(screen.getByRole("status")).toHaveTextContent("Copiado");
|
|
84
|
+
await act(async () => {
|
|
85
|
+
vi.advanceTimersByTime(2100);
|
|
86
|
+
});
|
|
87
|
+
expect(screen.getByRole("button", { name: "Copiar" })).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("copiar com o texto vindo como conteúdo lê o texto do bloco", async () => {
|
|
91
|
+
const escrever = vi.fn(() => Promise.resolve());
|
|
92
|
+
const usuario = userEvent.setup();
|
|
93
|
+
Object.defineProperty(navigator, "clipboard", { value: { writeText: escrever }, configurable: true });
|
|
94
|
+
render(<CodeBlock copyable>{"npm run build"}</CodeBlock>);
|
|
95
|
+
await usuario.click(screen.getByRole("button", { name: "Copiar" }));
|
|
96
|
+
expect(escrever).toHaveBeenCalledWith("npm run build");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("copyLabel e copiedLabel por prop", async () => {
|
|
100
|
+
const usuario = userEvent.setup();
|
|
101
|
+
Object.defineProperty(navigator, "clipboard", { value: { writeText: () => Promise.resolve() }, configurable: true });
|
|
102
|
+
render(<CodeBlock code="x" copyable copyLabel="Copy" copiedLabel="Copied" />);
|
|
103
|
+
await usuario.click(screen.getByRole("button", { name: "Copy" }));
|
|
104
|
+
expect(await screen.findByRole("button", { name: "Copied" })).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { faInbox } from "@fortawesome/free-solid-svg-icons";
|
|
5
|
+
import List from "../components/List/List";
|
|
6
|
+
import ListItem from "../components/List/ListItem";
|
|
7
|
+
|
|
8
|
+
describe("ListItem", () => {
|
|
9
|
+
it("sem as nem href é um button dentro de um li", () => {
|
|
10
|
+
const { container } = render(
|
|
11
|
+
<ul>
|
|
12
|
+
<ListItem title="Memorando 114/2025" />
|
|
13
|
+
</ul>,
|
|
14
|
+
);
|
|
15
|
+
const botao = screen.getByRole("button", { name: /Memorando 114\/2025/ });
|
|
16
|
+
expect(botao.tagName).toBe("BUTTON");
|
|
17
|
+
expect(botao).toHaveAttribute("type", "button");
|
|
18
|
+
expect(botao.parentElement?.tagName).toBe("LI");
|
|
19
|
+
expect(container.querySelectorAll("li")).toHaveLength(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("com href vira link; target _blank ganha rel seguro", () => {
|
|
23
|
+
render(
|
|
24
|
+
<ul>
|
|
25
|
+
<ListItem title="Ofício" href="/oficios/31" target="_blank" />
|
|
26
|
+
</ul>,
|
|
27
|
+
);
|
|
28
|
+
const link = screen.getByRole("link", { name: /Ofício/ });
|
|
29
|
+
expect(link).toHaveAttribute("href", "/oficios/31");
|
|
30
|
+
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("as='div' sem onClick é só leitura: sem role nem tabindex", () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<ul>
|
|
36
|
+
<ListItem as="div" title="Somente leitura" />
|
|
37
|
+
</ul>,
|
|
38
|
+
);
|
|
39
|
+
const linha = container.querySelector(".umds-list-item") as HTMLElement;
|
|
40
|
+
expect(linha.tagName).toBe("DIV");
|
|
41
|
+
expect(linha).not.toHaveAttribute("role");
|
|
42
|
+
expect(linha).not.toHaveAttribute("tabindex");
|
|
43
|
+
expect(linha).not.toHaveClass("umds-list-item--interactive");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("as='div' com onClick é acionável por Enter e Espaço", async () => {
|
|
47
|
+
const usuario = userEvent.setup();
|
|
48
|
+
const aoClicar = vi.fn();
|
|
49
|
+
render(
|
|
50
|
+
<ul>
|
|
51
|
+
<ListItem as="div" title="Acionável" onClick={aoClicar} />
|
|
52
|
+
</ul>,
|
|
53
|
+
);
|
|
54
|
+
const linha = screen.getByRole("button", { name: /Acionável/ });
|
|
55
|
+
linha.focus();
|
|
56
|
+
await usuario.keyboard("{Enter}");
|
|
57
|
+
await usuario.keyboard(" ");
|
|
58
|
+
expect(aoClicar).toHaveBeenCalledTimes(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("monta título, subtítulo, meta, início e fim nos lugares certos", () => {
|
|
62
|
+
const { container } = render(
|
|
63
|
+
<ul>
|
|
64
|
+
<ListItem
|
|
65
|
+
title="Memorando 114/2025"
|
|
66
|
+
subtitle="Novo contrato - Governo"
|
|
67
|
+
meta="23 de agosto de 2025"
|
|
68
|
+
start={<span data-testid="inicio">AV</span>}
|
|
69
|
+
end={<span data-testid="fim">novo</span>}
|
|
70
|
+
>
|
|
71
|
+
<span data-testid="livre">conteúdo livre</span>
|
|
72
|
+
</ListItem>
|
|
73
|
+
</ul>,
|
|
74
|
+
);
|
|
75
|
+
expect(container.querySelector(".umds-list-item__title")).toHaveTextContent("Memorando 114/2025");
|
|
76
|
+
expect(container.querySelector(".umds-list-item__subtitle")).toHaveTextContent("Novo contrato - Governo");
|
|
77
|
+
expect(container.querySelector(".umds-list-item__meta")).toHaveTextContent("23 de agosto de 2025");
|
|
78
|
+
expect(container.querySelector(".umds-list-item__start")).toContainElement(screen.getByTestId("inicio"));
|
|
79
|
+
expect(container.querySelector(".umds-list-item__end")).toContainElement(screen.getByTestId("fim"));
|
|
80
|
+
expect(container.querySelector(".umds-list-item__body")).toContainElement(screen.getByTestId("livre"));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("title aceita nó e não vira o atributo title do HTML", () => {
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<ul>
|
|
86
|
+
<ListItem title={<b data-testid="rico">Rico</b>} />
|
|
87
|
+
</ul>,
|
|
88
|
+
);
|
|
89
|
+
expect(screen.getByTestId("rico")).toBeInTheDocument();
|
|
90
|
+
expect(container.querySelector("[title]")).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("sem start, end e textos, não desenha as regiões vazias", () => {
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<ul>
|
|
96
|
+
<ListItem title="Só título" />
|
|
97
|
+
</ul>,
|
|
98
|
+
);
|
|
99
|
+
expect(container.querySelector(".umds-list-item__start")).toBeNull();
|
|
100
|
+
expect(container.querySelector(".umds-list-item__end")).toBeNull();
|
|
101
|
+
expect(container.querySelector(".umds-list-item__subtitle")).toBeNull();
|
|
102
|
+
expect(container.querySelector(".umds-list-item__meta")).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("selected marca aria-current e a classe de seleção", () => {
|
|
106
|
+
render(
|
|
107
|
+
<ul>
|
|
108
|
+
<ListItem title="Atual" selected />
|
|
109
|
+
</ul>,
|
|
110
|
+
);
|
|
111
|
+
const botao = screen.getByRole("button", { name: /Atual/ });
|
|
112
|
+
expect(botao).toHaveAttribute("aria-current", "true");
|
|
113
|
+
expect(botao).toHaveClass("umds-list-item--selected");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("disabled bloqueia o clique e o foco do botão", async () => {
|
|
117
|
+
const usuario = userEvent.setup();
|
|
118
|
+
const aoClicar = vi.fn();
|
|
119
|
+
render(
|
|
120
|
+
<ul>
|
|
121
|
+
<ListItem title="Bloqueada" disabled onClick={aoClicar} />
|
|
122
|
+
</ul>,
|
|
123
|
+
);
|
|
124
|
+
const botao = screen.getByRole("button", { name: /Bloqueada/ });
|
|
125
|
+
expect(botao).toBeDisabled();
|
|
126
|
+
await usuario.click(botao);
|
|
127
|
+
expect(aoClicar).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("link disabled perde o href e declara aria-disabled", () => {
|
|
131
|
+
render(
|
|
132
|
+
<ul>
|
|
133
|
+
<ListItem title="Link bloqueado" href="/x" disabled />
|
|
134
|
+
</ul>,
|
|
135
|
+
);
|
|
136
|
+
const link = screen.getByText("Link bloqueado").closest("a") as HTMLElement;
|
|
137
|
+
expect(link).not.toHaveAttribute("href");
|
|
138
|
+
expect(link).toHaveAttribute("aria-disabled", "true");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("clique chama onClick com o evento", async () => {
|
|
142
|
+
const usuario = userEvent.setup();
|
|
143
|
+
const aoClicar = vi.fn();
|
|
144
|
+
render(
|
|
145
|
+
<ul>
|
|
146
|
+
<ListItem title="Abrir" onClick={aoClicar} />
|
|
147
|
+
</ul>,
|
|
148
|
+
);
|
|
149
|
+
await usuario.click(screen.getByRole("button", { name: /Abrir/ }));
|
|
150
|
+
expect(aoClicar).toHaveBeenCalledTimes(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("repassa aria-label, id, data-* e style ao elemento interativo", () => {
|
|
154
|
+
render(
|
|
155
|
+
<ul>
|
|
156
|
+
<ListItem aria-label="Abrir memorando" id="item-1" data-origem="home" style={{ marginTop: 4 }} />
|
|
157
|
+
</ul>,
|
|
158
|
+
);
|
|
159
|
+
const botao = screen.getByRole("button", { name: "Abrir memorando" });
|
|
160
|
+
expect(botao).toHaveAttribute("id", "item-1");
|
|
161
|
+
expect(botao).toHaveAttribute("data-origem", "home");
|
|
162
|
+
expect(botao).toHaveStyle({ marginTop: "4px" });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("List", () => {
|
|
167
|
+
it("é uma ul com role list, nome acessível e gap padrão", () => {
|
|
168
|
+
render(
|
|
169
|
+
<List ariaLabel="Documentos não lidos">
|
|
170
|
+
<ListItem title="A" />
|
|
171
|
+
<ListItem title="B" />
|
|
172
|
+
</List>,
|
|
173
|
+
);
|
|
174
|
+
const lista = screen.getByRole("list", { name: "Documentos não lidos" });
|
|
175
|
+
expect(lista.tagName).toBe("UL");
|
|
176
|
+
expect(lista).toHaveClass("umds-list--gap-quarck");
|
|
177
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("as='ol' e gap trocam o elemento e a classe", () => {
|
|
181
|
+
render(
|
|
182
|
+
<List as="ol" gap="nano" ariaLabel="Passos">
|
|
183
|
+
<ListItem title="A" />
|
|
184
|
+
</List>,
|
|
185
|
+
);
|
|
186
|
+
const lista = screen.getByRole("list", { name: "Passos" });
|
|
187
|
+
expect(lista.tagName).toBe("OL");
|
|
188
|
+
expect(lista).toHaveClass("umds-list--gap-nano");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("ariaLabelledby aponta para o título visível", () => {
|
|
192
|
+
render(
|
|
193
|
+
<>
|
|
194
|
+
<h2 id="titulo">Prazos a vencer</h2>
|
|
195
|
+
<List ariaLabelledby="titulo">
|
|
196
|
+
<ListItem title="A" />
|
|
197
|
+
</List>
|
|
198
|
+
</>,
|
|
199
|
+
);
|
|
200
|
+
expect(screen.getByRole("list", { name: "Prazos a vencer" })).toBeInTheDocument();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("sem isEmpty mostra os itens e nunca o estado vazio", () => {
|
|
204
|
+
render(
|
|
205
|
+
<List ariaLabel="Lista" emptyTitle="Nada aqui">
|
|
206
|
+
<ListItem title="A" />
|
|
207
|
+
</List>,
|
|
208
|
+
);
|
|
209
|
+
expect(screen.queryByText("Nada aqui")).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("isEmpty troca os itens pelo EmptyState dentro da própria lista", () => {
|
|
213
|
+
const aoAgir = vi.fn();
|
|
214
|
+
const { container } = render(
|
|
215
|
+
<List
|
|
216
|
+
ariaLabel="Lista"
|
|
217
|
+
isEmpty
|
|
218
|
+
emptyTitle="Nenhum documento"
|
|
219
|
+
emptyDescription="Tudo em dia."
|
|
220
|
+
emptyIcon={faInbox}
|
|
221
|
+
emptyActionLabel="Atualizar"
|
|
222
|
+
onEmptyAction={aoAgir}
|
|
223
|
+
>
|
|
224
|
+
<ListItem title="Não aparece" />
|
|
225
|
+
</List>,
|
|
226
|
+
);
|
|
227
|
+
expect(screen.queryByRole("list")).toBeNull();
|
|
228
|
+
expect(screen.queryByText("Não aparece")).toBeNull();
|
|
229
|
+
const raiz = container.firstElementChild as HTMLElement;
|
|
230
|
+
expect(raiz).toHaveAttribute("role", "status");
|
|
231
|
+
expect(raiz).toHaveClass("umds-list--empty");
|
|
232
|
+
expect(raiz.querySelector(".umds-empty-state")).not.toBeNull();
|
|
233
|
+
expect(screen.getByText("Nenhum documento")).toBeInTheDocument();
|
|
234
|
+
expect(screen.getByText("Tudo em dia.")).toBeInTheDocument();
|
|
235
|
+
expect(raiz.querySelector(".umds-empty-state--sm")).not.toBeNull();
|
|
236
|
+
screen.getByRole("button", { name: "Atualizar" }).click();
|
|
237
|
+
expect(aoAgir).toHaveBeenCalledTimes(1);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("isEmpty sem título usa o texto padrão", () => {
|
|
241
|
+
render(<List ariaLabel="Lista" isEmpty />);
|
|
242
|
+
expect(screen.getByText("Nenhum item encontrado")).toBeInTheDocument();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("o conteúdo ao redor da lista não é desmontado ao esvaziar", () => {
|
|
246
|
+
const Tela = ({ vazia }: { vazia: boolean }) => (
|
|
247
|
+
<section>
|
|
248
|
+
<h2>Documentos</h2>
|
|
249
|
+
<input aria-label="Buscar" />
|
|
250
|
+
<List ariaLabel="Documentos" isEmpty={vazia}>
|
|
251
|
+
<ListItem title="A" />
|
|
252
|
+
</List>
|
|
253
|
+
<nav aria-label="Paginação">pág. 1</nav>
|
|
254
|
+
</section>
|
|
255
|
+
);
|
|
256
|
+
const { rerender } = render(<Tela vazia={false} />);
|
|
257
|
+
const busca = screen.getByLabelText("Buscar");
|
|
258
|
+
rerender(<Tela vazia />);
|
|
259
|
+
expect(screen.getByLabelText("Buscar")).toBe(busca);
|
|
260
|
+
expect(screen.getByRole("heading", { name: "Documentos" })).toBeInTheDocument();
|
|
261
|
+
expect(screen.getByRole("navigation", { name: "Paginação" })).toBeInTheDocument();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("slot empty (prop empty no React) substitui o EmptyState", () => {
|
|
265
|
+
render(<List ariaLabel="Lista" isEmpty empty={<p data-testid="vazio">Nada por aqui</p>} />);
|
|
266
|
+
expect(screen.getByTestId("vazio")).toBeInTheDocument();
|
|
267
|
+
expect(document.querySelector(".umds-empty-state")).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import Modal from "../components/Modal/Modal";
|
|
4
|
+
import Select from "../components/Select/Select";
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.unstubAllGlobals();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/** Troca `window.matchMedia` por um que responde `matches` e deixa disparar `change`. */
|
|
11
|
+
function simularTela(matches: boolean) {
|
|
12
|
+
const ouvintes = new Set<(e: { matches: boolean }) => void>();
|
|
13
|
+
vi.stubGlobal("matchMedia", (consulta: string) => ({
|
|
14
|
+
matches,
|
|
15
|
+
media: consulta,
|
|
16
|
+
addEventListener: (_: string, fn: any) => ouvintes.add(fn),
|
|
17
|
+
removeEventListener: (_: string, fn: any) => ouvintes.delete(fn),
|
|
18
|
+
}));
|
|
19
|
+
return (novo: boolean) => ouvintes.forEach((fn) => fn({ matches: novo }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("Modal · variant primary", () => {
|
|
23
|
+
it("fica no modal base (três faixas), com a classe do cabeçalho cheio", () => {
|
|
24
|
+
render(<Modal open inline variant="primary" title="Resolver tarefa" />);
|
|
25
|
+
const dialogo = screen.getByRole("dialog", { name: "Resolver tarefa" });
|
|
26
|
+
expect(dialogo).toHaveClass("umds-modal--primary", "umds-modal--base");
|
|
27
|
+
expect(dialogo).not.toHaveClass("umds-modal--feedback");
|
|
28
|
+
expect(dialogo.querySelector(".umds-modal__header")).not.toBeNull();
|
|
29
|
+
expect(dialogo.querySelector(".umds-modal__icon")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("positive continua sendo diálogo de feedback", () => {
|
|
33
|
+
render(<Modal open inline variant="positive" title="Pronto" />);
|
|
34
|
+
expect(screen.getByRole("dialog")).toHaveClass("umds-modal--feedback");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("Modal · width e maxWidth", () => {
|
|
39
|
+
it("escreve a largura e, sem maxWidth, solta o teto do size", () => {
|
|
40
|
+
render(<Modal open inline title="x" width="600px" />);
|
|
41
|
+
const dialogo = screen.getByRole("dialog");
|
|
42
|
+
expect(dialogo.style.width).toBe("600px");
|
|
43
|
+
expect(dialogo.style.maxWidth).toBe("100%");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("maxWidth explícito vale", () => {
|
|
47
|
+
render(<Modal open inline title="x" width="90%" maxWidth="720px" />);
|
|
48
|
+
expect(screen.getByRole("dialog").style.maxWidth).toBe("720px");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("sem width nem maxWidth não escreve style", () => {
|
|
52
|
+
render(<Modal open inline title="x" />);
|
|
53
|
+
expect(screen.getByRole("dialog").getAttribute("style")).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Modal · Escape", () => {
|
|
58
|
+
it("fecha com Escape no borbulho", () => {
|
|
59
|
+
const aoFechar = vi.fn();
|
|
60
|
+
render(<Modal open inline title="x" onClose={aoFechar} />);
|
|
61
|
+
fireEvent.keyDown(document.body, { key: "Escape" });
|
|
62
|
+
expect(aoFechar).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("não fecha quando outro componente já consumiu a tecla (defaultPrevented)", () => {
|
|
66
|
+
const aoFechar = vi.fn();
|
|
67
|
+
render(<Modal open inline title="x" onClose={aoFechar} />);
|
|
68
|
+
const consumidor = document.createElement("div");
|
|
69
|
+
consumidor.addEventListener("keydown", (e) => e.preventDefault());
|
|
70
|
+
document.body.appendChild(consumidor);
|
|
71
|
+
fireEvent.keyDown(consumidor, { key: "Escape" });
|
|
72
|
+
expect(aoFechar).not.toHaveBeenCalled();
|
|
73
|
+
consumidor.remove();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("Select aberto dentro do modal fecha só a lista, e o modal fica", () => {
|
|
77
|
+
const aoFechar = vi.fn();
|
|
78
|
+
render(
|
|
79
|
+
<Modal open inline title="x" onClose={aoFechar}>
|
|
80
|
+
<Select label="Categoria" options={[{ label: "Administrativa", value: "adm" }]} />
|
|
81
|
+
</Modal>,
|
|
82
|
+
);
|
|
83
|
+
const gatilho = screen.getByRole("combobox");
|
|
84
|
+
fireEvent.click(gatilho);
|
|
85
|
+
expect(gatilho).toHaveAttribute("aria-expanded", "true");
|
|
86
|
+
fireEvent.keyDown(gatilho, { key: "Escape" });
|
|
87
|
+
expect(gatilho).toHaveAttribute("aria-expanded", "false");
|
|
88
|
+
expect(aoFechar).not.toHaveBeenCalled();
|
|
89
|
+
// Com o Select fechado, o Esc seguinte é do modal.
|
|
90
|
+
fireEvent.keyDown(gatilho, { key: "Escape" });
|
|
91
|
+
expect(aoFechar).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("Modal · controles de janela escolhidos um a um", () => {
|
|
96
|
+
it("true liga os quatro, como antes", () => {
|
|
97
|
+
render(<Modal open inline title="x" windowControls />);
|
|
98
|
+
for (const nome of ["Modo normal", "Expandir", "Tela cheia", "Minimizar"]) {
|
|
99
|
+
expect(screen.getByRole("button", { name: nome })).toBeInTheDocument();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("lista escolhe só os pedidos, sem minimizar", () => {
|
|
104
|
+
render(<Modal open inline title="x" windowControls={["expand", "fullscreen"]} />);
|
|
105
|
+
expect(screen.getByRole("button", { name: "Expandir" })).toBeInTheDocument();
|
|
106
|
+
expect(screen.getByRole("button", { name: "Tela cheia" })).toBeInTheDocument();
|
|
107
|
+
expect(screen.queryByRole("button", { name: "Minimizar" })).toBeNull();
|
|
108
|
+
expect(screen.queryByRole("button", { name: "Modo normal" })).toBeNull();
|
|
109
|
+
expect(screen.getByRole("button", { name: "Fechar" })).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("sem windowControls só há o ×", () => {
|
|
113
|
+
render(<Modal open inline title="x" />);
|
|
114
|
+
expect(screen.queryByRole("button", { name: "Expandir" })).toBeNull();
|
|
115
|
+
expect(screen.getByRole("button", { name: "Fechar" })).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("sem o botão normal, clicar de novo no modo ativo volta ao normal; e o normal usa o tamanho do modal", () => {
|
|
119
|
+
render(<Modal open inline title="x" width="600px" windowControls={["expand"]} />);
|
|
120
|
+
const dialogo = screen.getByRole("dialog");
|
|
121
|
+
const expandir = screen.getByRole("button", { name: "Expandir" });
|
|
122
|
+
expect(dialogo).not.toHaveClass("umds-modal--janela-normal");
|
|
123
|
+
expect(dialogo.style.width).toBe("600px");
|
|
124
|
+
fireEvent.click(expandir);
|
|
125
|
+
expect(dialogo).toHaveClass("umds-modal--janela-expanded");
|
|
126
|
+
expect(dialogo.style.width).toBe("");
|
|
127
|
+
fireEvent.click(expandir);
|
|
128
|
+
expect(dialogo).not.toHaveClass("umds-modal--janela-expanded");
|
|
129
|
+
expect(dialogo.style.width).toBe("600px");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("minimizar chama onMinimize", () => {
|
|
133
|
+
const aoMinimizar = vi.fn();
|
|
134
|
+
render(<Modal open inline title="x" windowControls={["minimize"]} onMinimize={aoMinimizar} />);
|
|
135
|
+
fireEvent.click(screen.getByRole("button", { name: "Minimizar" }));
|
|
136
|
+
expect(aoMinimizar).toHaveBeenCalledTimes(1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("Modal · responsive", () => {
|
|
141
|
+
it("em tela larga continua centralizado", () => {
|
|
142
|
+
simularTela(false);
|
|
143
|
+
render(<Modal open inline title="x" responsive />);
|
|
144
|
+
expect(screen.getByRole("dialog")).not.toHaveClass("umds-modal--sheet");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("em tela estreita vira painel de baixo, sem controles de janela e sem a largura livre", () => {
|
|
148
|
+
simularTela(true);
|
|
149
|
+
render(<Modal open inline title="x" responsive width="600px" windowControls />);
|
|
150
|
+
const dialogo = screen.getByRole("dialog");
|
|
151
|
+
expect(dialogo).toHaveClass("umds-modal--sheet");
|
|
152
|
+
expect(dialogo.style.width).toBe("");
|
|
153
|
+
expect(screen.queryByRole("button", { name: "Expandir" })).toBeNull();
|
|
154
|
+
expect(dialogo.querySelector(".umds-modal__grabber")).not.toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("acompanha a mudança de tamanho da tela", () => {
|
|
158
|
+
const mudar = simularTela(false);
|
|
159
|
+
render(<Modal open inline title="x" responsive />);
|
|
160
|
+
const dialogo = screen.getByRole("dialog");
|
|
161
|
+
expect(dialogo).not.toHaveClass("umds-modal--sheet");
|
|
162
|
+
act(() => mudar(true));
|
|
163
|
+
expect(screen.getByRole("dialog")).toHaveClass("umds-modal--sheet");
|
|
164
|
+
act(() => mudar(false));
|
|
165
|
+
expect(screen.getByRole("dialog")).not.toHaveClass("umds-modal--sheet");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("sem responsive nunca vira painel", () => {
|
|
169
|
+
simularTela(true);
|
|
170
|
+
render(<Modal open inline title="x" />);
|
|
171
|
+
expect(screen.getByRole("dialog")).not.toHaveClass("umds-modal--sheet");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { act, screen } from "@testing-library/react";
|
|
3
|
+
import { renderToString } from "react-dom/server";
|
|
4
|
+
import { hydrateRoot } from "react-dom/client";
|
|
5
|
+
import Select from "../components/Select/Select";
|
|
6
|
+
import MultiSelect from "../components/MultiSelect/MultiSelect";
|
|
7
|
+
import SegmentedControl from "../components/SegmentedControl/SegmentedControl";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Select, MultiSelect e SegmentedControl nascem SEM id automático (utils/id):
|
|
11
|
+
* o id só é gerado no onMount, para o servidor e a primeira renderização do
|
|
12
|
+
* cliente saírem idênticos. Só que o nome acessível deles vinha de
|
|
13
|
+
* `aria-labelledby` apontando para esse id — no HTML do servidor, e até a
|
|
14
|
+
* hidratação, o combobox ficava sem nome (axe `aria-input-field-name`, visto
|
|
15
|
+
* no portal Astro com `client:visible`).
|
|
16
|
+
*
|
|
17
|
+
* Aqui o HTML sai do `renderToString`, que não dispara o onMount, e vai para
|
|
18
|
+
* o DOM do jsdom como o navegador receberia. O nome é conferido pelo cálculo
|
|
19
|
+
* do Testing Library, o mesmo algoritmo de nome acessível que o axe usa.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const OPCOES = [
|
|
23
|
+
{ label: "Recursos Humanos", value: "rh" },
|
|
24
|
+
{ label: "Financeiro", value: "fin" },
|
|
25
|
+
];
|
|
26
|
+
const POR_PAGINA = [
|
|
27
|
+
{ label: "10", value: "10" },
|
|
28
|
+
{ label: "25", value: "25" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function comoDoServidor(html: string) {
|
|
32
|
+
const raiz = document.createElement("div");
|
|
33
|
+
raiz.innerHTML = html;
|
|
34
|
+
document.body.appendChild(raiz);
|
|
35
|
+
return raiz;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("Nome acessível no HTML do servidor", () => {
|
|
39
|
+
// As raízes são montadas à mão (fora do render), e o cleanup global não as vê.
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
document.body.innerHTML = "";
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("Select: o combobox tem o nome do label antes do mount", () => {
|
|
45
|
+
comoDoServidor(renderToString(<Select label="Setor" options={OPCOES} />));
|
|
46
|
+
const campo = screen.getByRole("combobox", { name: "Setor" });
|
|
47
|
+
expect(campo).not.toHaveAttribute("aria-labelledby");
|
|
48
|
+
expect(campo).toHaveAttribute("aria-label", "Setor");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Select: `ariaLabel` continua valendo sobre o label", () => {
|
|
52
|
+
comoDoServidor(
|
|
53
|
+
renderToString(<Select label="Setor" ariaLabel="Setor do servidor" options={OPCOES} />),
|
|
54
|
+
);
|
|
55
|
+
expect(
|
|
56
|
+
screen.getByRole("combobox", { name: "Setor do servidor" }),
|
|
57
|
+
).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("Select com `id`: o servidor já liga pelo aria-labelledby", () => {
|
|
61
|
+
const html = renderToString(<Select id="setor" label="Setor" options={OPCOES} />);
|
|
62
|
+
expect(html).toContain('aria-labelledby="setor-label"');
|
|
63
|
+
expect(html).not.toContain('aria-label="Setor"');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("MultiSelect: o combobox tem o nome do label antes do mount", () => {
|
|
67
|
+
comoDoServidor(renderToString(<MultiSelect label="Setores" options={OPCOES} />));
|
|
68
|
+
expect(screen.getByRole("combobox", { name: "Setores" })).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("SegmentedControl: grupo e segmentos têm nome antes do mount", () => {
|
|
72
|
+
comoDoServidor(
|
|
73
|
+
renderToString(<SegmentedControl label="Itens" options={POR_PAGINA} />),
|
|
74
|
+
);
|
|
75
|
+
expect(screen.getByRole("radiogroup", { name: "Itens" })).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByRole("radio", { name: "10" })).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByRole("radio", { name: "25" })).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("hidrata sem divergência e, montado, volta ao aria-labelledby", async () => {
|
|
81
|
+
const arvore = (
|
|
82
|
+
<div>
|
|
83
|
+
<Select label="Setor" options={OPCOES} />
|
|
84
|
+
<MultiSelect label="Setores" options={OPCOES} />
|
|
85
|
+
<SegmentedControl label="Itens" options={POR_PAGINA} />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
const raiz = comoDoServidor(renderToString(arvore));
|
|
89
|
+
const erro = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
90
|
+
const avisosDeHidratacao: unknown[] = [];
|
|
91
|
+
await act(async () => {
|
|
92
|
+
hydrateRoot(raiz, arvore, {
|
|
93
|
+
onRecoverableError: (e) => avisosDeHidratacao.push(e),
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
const chamadas = erro.mock.calls.map((c) => String(c[0]));
|
|
97
|
+
erro.mockRestore();
|
|
98
|
+
expect(avisosDeHidratacao).toEqual([]);
|
|
99
|
+
expect(chamadas.filter((c) => /hydrat|did not match/i.test(c))).toEqual([]);
|
|
100
|
+
|
|
101
|
+
const select = screen.getByRole("combobox", { name: "Setor" });
|
|
102
|
+
expect(select).toHaveAttribute("aria-labelledby");
|
|
103
|
+
expect(select).not.toHaveAttribute("aria-label");
|
|
104
|
+
const multi = screen.getByRole("combobox", { name: "Setores" });
|
|
105
|
+
expect(multi).toHaveAttribute("aria-labelledby");
|
|
106
|
+
const grupo = screen.getByRole("radiogroup", { name: "Itens" });
|
|
107
|
+
expect(grupo).toHaveAttribute("aria-labelledby");
|
|
108
|
+
expect(screen.getByRole("radio", { name: "10" })).not.toHaveAttribute("aria-label");
|
|
109
|
+
});
|
|
110
|
+
});
|