@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,329 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import RichTextEditor from "../components/RichTextEditor/RichTextEditor";
|
|
6
|
+
import { enderecoSeguro, normalizarEndereco, sanitizarHtml } from "../utils/html-seguro";
|
|
7
|
+
|
|
8
|
+
/** jsdom não traz `execCommand`: o teste fala com um espião e confere o que o editor pede ao navegador. */
|
|
9
|
+
let execCommand: ReturnType<typeof vi.fn>;
|
|
10
|
+
let estados: Record<string, boolean>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
estados = {};
|
|
14
|
+
execCommand = vi.fn(() => true);
|
|
15
|
+
(document as any).execCommand = execCommand;
|
|
16
|
+
(document as any).queryCommandState = (comando: string) => !!estados[comando];
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const area = () => screen.getByRole("textbox") as HTMLElement;
|
|
20
|
+
|
|
21
|
+
/** Escreve na área como o navegador faz: muda o conteúdo e dispara `input`. */
|
|
22
|
+
const escrever = (html: string) => {
|
|
23
|
+
area().innerHTML = html;
|
|
24
|
+
fireEvent.input(area());
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const selecionarTudoNaArea = () => {
|
|
28
|
+
const selecao = window.getSelection() as Selection;
|
|
29
|
+
const faixa = document.createRange();
|
|
30
|
+
faixa.selectNodeContents(area());
|
|
31
|
+
selecao.removeAllRanges();
|
|
32
|
+
selecao.addRange(faixa);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("RichTextEditor: estrutura e nomes", () => {
|
|
36
|
+
it("a barra é um toolbar nomeado com as ferramentas padrão", () => {
|
|
37
|
+
render(<RichTextEditor ariaLabel="Despacho" />);
|
|
38
|
+
const barra = screen.getByRole("toolbar", { name: "Formatação" });
|
|
39
|
+
for (const nome of ["Negrito", "Itálico", "Sublinhado", "Lista com marcadores", "Lista numerada", "Link", "Desfazer", "Refazer"]) {
|
|
40
|
+
expect(screen.getByRole("button", { name: nome })).toBeInTheDocument();
|
|
41
|
+
}
|
|
42
|
+
expect(barra.querySelectorAll('[role="separator"]')).toHaveLength(3);
|
|
43
|
+
expect(screen.queryByRole("button", { name: "Tachado" })).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("a área de escrita é um textbox multilinha com nome e placeholder", () => {
|
|
47
|
+
render(<RichTextEditor ariaLabel="Despacho" placeholder="Escreva o despacho..." />);
|
|
48
|
+
const caixa = screen.getByRole("textbox", { name: "Despacho" });
|
|
49
|
+
expect(caixa).toHaveAttribute("aria-multiline", "true");
|
|
50
|
+
expect(caixa).toHaveAttribute("contenteditable", "true");
|
|
51
|
+
expect(caixa).toHaveAttribute("data-placeholder", "Escreva o despacho...");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("label visível nomeia a área", () => {
|
|
55
|
+
render(<RichTextEditor label="Descrição" id="rte" />);
|
|
56
|
+
expect(screen.getByRole("textbox", { name: "Descrição" })).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("toolbar escolhe as ferramentas e os separadores", () => {
|
|
60
|
+
render(<RichTextEditor ariaLabel="x" toolbar={["bold", "|", "strike", "clear"]} />);
|
|
61
|
+
expect(screen.getAllByRole("button").map((b) => b.getAttribute("aria-label"))).toEqual(["Negrito", "Tachado", "Limpar formatação"]);
|
|
62
|
+
expect(screen.getByRole("toolbar").querySelectorAll('[role="separator"]')).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("labels e toolbarLabel traduzem a barra", () => {
|
|
66
|
+
render(<RichTextEditor ariaLabel="x" toolbarLabel="Formatting" toolbar={["bold", "link"]} labels={{ bold: "Bold", link: "Link (Ctrl+K)" }} />);
|
|
67
|
+
expect(screen.getByRole("toolbar", { name: "Formatting" })).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByRole("button", { name: "Bold" })).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByRole("button", { name: "Link (Ctrl+K)" })).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("errorMessage e helperText ligam a área por aria-describedby", () => {
|
|
73
|
+
render(<RichTextEditor ariaLabel="x" id="r" errorMessage="Escreva o despacho" helperText="Obrigatório" />);
|
|
74
|
+
expect(area()).toHaveAttribute("aria-invalid", "true");
|
|
75
|
+
expect(area().getAttribute("aria-describedby")).toBe("r-error r-helper");
|
|
76
|
+
expect(screen.getByText("Escreva o despacho")).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("minHeight e resizable", () => {
|
|
80
|
+
const { container } = render(<RichTextEditor ariaLabel="x" minHeight={200} resizable />);
|
|
81
|
+
expect(area().style.minHeight).toBe("200px");
|
|
82
|
+
expect(container.querySelector(".umds-rte--resizable")).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("o slot footer aparece dentro da moldura", () => {
|
|
86
|
+
render(<RichTextEditor ariaLabel="x" footer={<span data-testid="assinatura">Ana Paula Souza</span>} />);
|
|
87
|
+
expect(screen.getByTestId("assinatura").closest(".umds-rte__footer")).not.toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("RichTextEditor: valor e higienização", () => {
|
|
92
|
+
it("defaultValue entra na área", () => {
|
|
93
|
+
render(<RichTextEditor ariaLabel="x" defaultValue="<p>Olá <b>mundo</b></p>" />);
|
|
94
|
+
expect(area().innerHTML).toBe("<p>Olá <b>mundo</b></p>");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("o HTML que entra é higienizado: sem script, onerror, style nem javascript:", () => {
|
|
98
|
+
render(
|
|
99
|
+
<RichTextEditor
|
|
100
|
+
ariaLabel="x"
|
|
101
|
+
defaultValue={'<p style="color:red" class="x" onclick="a()">ok</p><script>alert(1)</script><img src=x onerror="alert(1)"><a href="javascript:alert(1)">mau</a><a href="https://1doc.com.br">bom</a>'}
|
|
102
|
+
/>,
|
|
103
|
+
);
|
|
104
|
+
const html = area().innerHTML;
|
|
105
|
+
expect(html).not.toMatch(/script|onerror|onclick|style=|class=|javascript:|<img/i);
|
|
106
|
+
expect(html).toContain('<a href="https://1doc.com.br" rel="noopener noreferrer">bom</a>');
|
|
107
|
+
expect(html).toContain("<p>ok</p>");
|
|
108
|
+
expect(html).toContain("mau");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("digitar entrega o HTML em onChange; apagar tudo entrega vazio", () => {
|
|
112
|
+
const aoMudar = vi.fn();
|
|
113
|
+
render(<RichTextEditor ariaLabel="x" onChange={aoMudar} />);
|
|
114
|
+
escrever("<b>Oi</b>");
|
|
115
|
+
expect(aoMudar).toHaveBeenLastCalledWith("<b>Oi</b>");
|
|
116
|
+
escrever("<br>");
|
|
117
|
+
expect(aoMudar).toHaveBeenLastCalledWith("");
|
|
118
|
+
expect(area().innerHTML).toBe("");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("colar entra como texto puro (insertText) e não deixa o HTML colado passar", () => {
|
|
122
|
+
render(<RichTextEditor ariaLabel="x" />);
|
|
123
|
+
const evento = new Event("paste", { bubbles: true, cancelable: true });
|
|
124
|
+
Object.defineProperty(evento, "clipboardData", { value: { getData: (tipo: string) => (tipo === "text/plain" ? "texto puro" : "<b>html</b>") } });
|
|
125
|
+
area().dispatchEvent(evento);
|
|
126
|
+
expect(evento.defaultPrevented).toBe(true);
|
|
127
|
+
expect(execCommand).toHaveBeenCalledWith("insertText", false, "texto puro");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("controlado: um value novo de fora reescreve; o mesmo que o editor emitiu não", () => {
|
|
131
|
+
function Pai() {
|
|
132
|
+
const [html, setHtml] = useState("<p>um</p>");
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
<RichTextEditor ariaLabel="x" value={html} onChange={setHtml} />
|
|
136
|
+
<button onClick={() => setHtml("<p>dois</p>")}>trocar</button>
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
render(<Pai />);
|
|
141
|
+
expect(area().innerHTML).toBe("<p>um</p>");
|
|
142
|
+
escrever("<p>um mais</p>");
|
|
143
|
+
expect(area().innerHTML).toBe("<p>um mais</p>");
|
|
144
|
+
fireEvent.click(screen.getByText("trocar"));
|
|
145
|
+
expect(area().innerHTML).toBe("<p>dois</p>");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("RichTextEditor: ferramentas", () => {
|
|
150
|
+
it("clicar em Negrito pede o comando ao navegador e avisa onChange", async () => {
|
|
151
|
+
const usuario = userEvent.setup();
|
|
152
|
+
const aoMudar = vi.fn();
|
|
153
|
+
render(<RichTextEditor ariaLabel="x" onChange={aoMudar} defaultValue="<p>x</p>" />);
|
|
154
|
+
await usuario.click(screen.getByRole("button", { name: "Negrito" }));
|
|
155
|
+
expect(execCommand).toHaveBeenCalledWith("bold", false, undefined);
|
|
156
|
+
expect(aoMudar).toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("cada ferramenta pede o seu comando", async () => {
|
|
160
|
+
const usuario = userEvent.setup();
|
|
161
|
+
render(<RichTextEditor ariaLabel="x" toolbar={["bold", "italic", "underline", "strike", "ul", "ol", "undo", "redo", "clear"]} />);
|
|
162
|
+
const esperado: Record<string, string> = {
|
|
163
|
+
Negrito: "bold",
|
|
164
|
+
Itálico: "italic",
|
|
165
|
+
Sublinhado: "underline",
|
|
166
|
+
Tachado: "strikeThrough",
|
|
167
|
+
"Lista com marcadores": "insertUnorderedList",
|
|
168
|
+
"Lista numerada": "insertOrderedList",
|
|
169
|
+
Desfazer: "undo",
|
|
170
|
+
Refazer: "redo",
|
|
171
|
+
"Limpar formatação": "removeFormat",
|
|
172
|
+
};
|
|
173
|
+
for (const [nome, comando] of Object.entries(esperado)) {
|
|
174
|
+
execCommand.mockClear();
|
|
175
|
+
await usuario.click(screen.getByRole("button", { name: nome }));
|
|
176
|
+
expect(execCommand).toHaveBeenCalledWith(comando, false, undefined);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("o clique na barra não rouba o foco da área (mousedown é impedido)", () => {
|
|
181
|
+
render(<RichTextEditor ariaLabel="x" />);
|
|
182
|
+
const evento = new MouseEvent("mousedown", { bubbles: true, cancelable: true });
|
|
183
|
+
screen.getByRole("button", { name: "Negrito" }).dispatchEvent(evento);
|
|
184
|
+
expect(evento.defaultPrevented).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("os botões de liga/desliga seguem o cursor (aria-pressed)", () => {
|
|
188
|
+
render(<RichTextEditor ariaLabel="x" defaultValue="<p>x</p>" />);
|
|
189
|
+
const negrito = screen.getByRole("button", { name: "Negrito" });
|
|
190
|
+
expect(negrito).toHaveAttribute("aria-pressed", "false");
|
|
191
|
+
estados.bold = true;
|
|
192
|
+
selecionarTudoNaArea();
|
|
193
|
+
fireEvent(document, new Event("selectionchange"));
|
|
194
|
+
expect(negrito).toHaveAttribute("aria-pressed", "true");
|
|
195
|
+
expect(negrito).toHaveClass("umds-rte__tool--active");
|
|
196
|
+
expect(screen.getByRole("button", { name: "Itálico" })).toHaveAttribute("aria-pressed", "false");
|
|
197
|
+
expect(screen.getByRole("button", { name: "Link" })).not.toHaveAttribute("aria-pressed");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("Tab entra numa parada só; setas, Home e End movem entre as ferramentas", async () => {
|
|
201
|
+
const usuario = userEvent.setup();
|
|
202
|
+
render(<RichTextEditor ariaLabel="x" toolbar={["bold", "italic", "underline"]} />);
|
|
203
|
+
const botoes = screen.getAllByRole("button");
|
|
204
|
+
expect(botoes.map((b) => b.getAttribute("tabindex"))).toEqual(["0", "-1", "-1"]);
|
|
205
|
+
botoes[0].focus();
|
|
206
|
+
await usuario.keyboard("{ArrowRight}");
|
|
207
|
+
expect(botoes[1]).toHaveFocus();
|
|
208
|
+
await usuario.keyboard("{End}");
|
|
209
|
+
expect(botoes[2]).toHaveFocus();
|
|
210
|
+
await usuario.keyboard("{ArrowRight}");
|
|
211
|
+
expect(botoes[0]).toHaveFocus();
|
|
212
|
+
await usuario.keyboard("{ArrowLeft}");
|
|
213
|
+
expect(botoes[2]).toHaveFocus();
|
|
214
|
+
await usuario.keyboard("{Home}");
|
|
215
|
+
expect(botoes[0]).toHaveFocus();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("readOnly: sem barra e a área não edita; disabled: botões desabilitados", () => {
|
|
219
|
+
const { rerender } = render(<RichTextEditor ariaLabel="x" readOnly defaultValue="<p>x</p>" />);
|
|
220
|
+
expect(screen.queryByRole("toolbar")).toBeNull();
|
|
221
|
+
expect(area()).toHaveAttribute("contenteditable", "false");
|
|
222
|
+
rerender(<RichTextEditor ariaLabel="x" disabled />);
|
|
223
|
+
for (const b of screen.getAllByRole("button")) expect(b).toBeDisabled();
|
|
224
|
+
expect(area()).toHaveAttribute("tabindex", "-1");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("RichTextEditor: link", () => {
|
|
229
|
+
it("o botão Link abre o campo, com foco nele; Esc fecha", async () => {
|
|
230
|
+
const usuario = userEvent.setup();
|
|
231
|
+
render(<RichTextEditor ariaLabel="x" defaultValue="<p>texto</p>" />);
|
|
232
|
+
await usuario.click(screen.getByRole("button", { name: "Link" }));
|
|
233
|
+
const campo = screen.getByLabelText("Endereço do link");
|
|
234
|
+
expect(campo).toHaveFocus();
|
|
235
|
+
await usuario.keyboard("{Escape}");
|
|
236
|
+
expect(screen.queryByLabelText("Endereço do link")).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("Ctrl+K abre o campo de link", () => {
|
|
240
|
+
render(<RichTextEditor ariaLabel="x" />);
|
|
241
|
+
fireEvent.keyDown(area(), { key: "k", ctrlKey: true });
|
|
242
|
+
expect(screen.getByLabelText("Endereço do link")).toBeInTheDocument();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("com texto escolhido, aplicar cria o link; endereço sem esquema ganha https://", async () => {
|
|
246
|
+
const usuario = userEvent.setup();
|
|
247
|
+
render(<RichTextEditor ariaLabel="x" defaultValue="<p>texto</p>" />);
|
|
248
|
+
selecionarTudoNaArea();
|
|
249
|
+
await usuario.click(screen.getByRole("button", { name: "Link" }));
|
|
250
|
+
await usuario.type(screen.getByLabelText("Endereço do link"), "exemplo.com.br");
|
|
251
|
+
await usuario.click(screen.getByRole("button", { name: "Aplicar" }));
|
|
252
|
+
expect(execCommand).toHaveBeenCalledWith("createLink", false, "https://exemplo.com.br");
|
|
253
|
+
expect(screen.queryByLabelText("Endereço do link")).toBeNull();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("sem texto escolhido, o endereço vira o texto do link", async () => {
|
|
257
|
+
const usuario = userEvent.setup();
|
|
258
|
+
render(<RichTextEditor ariaLabel="x" defaultValue="<p>texto</p>" />);
|
|
259
|
+
const selecao = window.getSelection() as Selection;
|
|
260
|
+
const faixa = document.createRange();
|
|
261
|
+
faixa.setStart(area().querySelector("p")!.firstChild!, 2);
|
|
262
|
+
faixa.collapse(true);
|
|
263
|
+
selecao.removeAllRanges();
|
|
264
|
+
selecao.addRange(faixa);
|
|
265
|
+
await usuario.click(screen.getByRole("button", { name: "Link" }));
|
|
266
|
+
await usuario.type(screen.getByLabelText("Endereço do link"), "https://1doc.com.br{Enter}");
|
|
267
|
+
expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<a href="https://1doc.com.br">https://1doc.com.br</a>');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("endereço perigoso não vira link: mostra o erro e continua aberto", async () => {
|
|
271
|
+
const usuario = userEvent.setup();
|
|
272
|
+
render(<RichTextEditor ariaLabel="x" defaultValue="<p>texto</p>" />);
|
|
273
|
+
selecionarTudoNaArea();
|
|
274
|
+
await usuario.click(screen.getByRole("button", { name: "Link" }));
|
|
275
|
+
await usuario.type(screen.getByLabelText("Endereço do link"), "javascript:alert(1)");
|
|
276
|
+
await usuario.click(screen.getByRole("button", { name: "Aplicar" }));
|
|
277
|
+
expect(execCommand).not.toHaveBeenCalledWith("createLink", expect.anything(), expect.anything());
|
|
278
|
+
expect(screen.getByText("Use um endereço http, https, mailto ou tel.")).toBeInTheDocument();
|
|
279
|
+
expect(screen.getByLabelText("Endereço do link")).toBeInTheDocument();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("em cima de um link existente, o campo vem preenchido e Remover link aparece", async () => {
|
|
283
|
+
const usuario = userEvent.setup();
|
|
284
|
+
render(<RichTextEditor ariaLabel="x" defaultValue='<p><a href="https://1doc.com.br">site</a></p>' />);
|
|
285
|
+
const selecao = window.getSelection() as Selection;
|
|
286
|
+
const faixa = document.createRange();
|
|
287
|
+
faixa.setStart(area().querySelector("a")!.firstChild!, 1);
|
|
288
|
+
faixa.collapse(true);
|
|
289
|
+
selecao.removeAllRanges();
|
|
290
|
+
selecao.addRange(faixa);
|
|
291
|
+
await usuario.click(screen.getByRole("button", { name: "Link" }));
|
|
292
|
+
expect(screen.getByLabelText("Endereço do link")).toHaveValue("https://1doc.com.br");
|
|
293
|
+
await usuario.click(screen.getByRole("button", { name: "Remover link" }));
|
|
294
|
+
expect(execCommand).toHaveBeenCalledWith("unlink", false, undefined);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("os textos do campo de link vêm por labels", async () => {
|
|
298
|
+
const usuario = userEvent.setup();
|
|
299
|
+
render(<RichTextEditor ariaLabel="x" labels={{ linkAddress: "Link address", linkApply: "Apply", linkCancel: "Cancel" }} />);
|
|
300
|
+
await usuario.click(screen.getByRole("button", { name: "Link" }));
|
|
301
|
+
expect(screen.getByLabelText("Link address")).toBeInTheDocument();
|
|
302
|
+
expect(screen.getByRole("button", { name: "Apply" })).toBeInTheDocument();
|
|
303
|
+
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("html-seguro", () => {
|
|
308
|
+
it("enderecoSeguro aceita http, https, mailto, tel, caminho e âncora; recusa o resto", () => {
|
|
309
|
+
for (const ok of ["https://a.com", "http://a.com", "mailto:a@b.com", "tel:+5584999999999", "/x", "./x", "../x", "#topo"]) {
|
|
310
|
+
expect(enderecoSeguro(ok)).toBe(ok);
|
|
311
|
+
}
|
|
312
|
+
for (const mau of ["javascript:alert(1)", "JaVaScRiPt:alert(1)", "java\nscript:alert(1)", "data:text/html,x", "vbscript:x", "exemplo.com"]) {
|
|
313
|
+
expect(enderecoSeguro(mau)).toBe("");
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("normalizarEndereco põe https:// no que não tem esquema e recusa o perigoso", () => {
|
|
318
|
+
expect(normalizarEndereco("exemplo.com.br")).toBe("https://exemplo.com.br");
|
|
319
|
+
expect(normalizarEndereco(" https://a.com ")).toBe("https://a.com");
|
|
320
|
+
expect(normalizarEndereco("javascript:alert(1)")).toBe("");
|
|
321
|
+
expect(normalizarEndereco("java\tscript:alert(1)")).toBe("");
|
|
322
|
+
expect(normalizarEndereco("")).toBe("");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("sanitizarHtml tira tags desconhecidas mantendo o conteúdo e descarta as perigosas inteiras", () => {
|
|
326
|
+
expect(sanitizarHtml("<font color=red>a</font><iframe src=x>b</iframe><style>p{}</style>c")).toBe("ac");
|
|
327
|
+
expect(sanitizarHtml("<ul><li>um</li><li><b>dois</b></li></ul>")).toBe("<ul><li>um</li><li><b>dois</b></li></ul>");
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import Select from "../components/Select/Select";
|
|
4
|
+
|
|
5
|
+
const OPCOES = [
|
|
6
|
+
{ label: "Urgente", value: "urgente" },
|
|
7
|
+
{ label: "Revisão", value: "revisao" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
afterEach(() => vi.restoreAllMocks());
|
|
11
|
+
|
|
12
|
+
/** jsdom não tem layout: o gatilho "mede" o que o teste mandar. */
|
|
13
|
+
function medirGatilho(topo: number, base: number, alturaDaTela: number) {
|
|
14
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
|
15
|
+
() => ({ top: topo, bottom: base, left: 0, right: 0, width: 0, height: base - topo, x: 0, y: topo, toJSON() {} }) as any,
|
|
16
|
+
);
|
|
17
|
+
Object.defineProperty(window, "innerHeight", { value: alturaDaTela, configurable: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("Select · placement", () => {
|
|
21
|
+
it("padrão abre para baixo", () => {
|
|
22
|
+
const { container } = render(<Select ariaLabel="x" options={OPCOES} />);
|
|
23
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
24
|
+
expect(container.querySelector(".umds-select--placement-top")).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("top abre para cima", () => {
|
|
28
|
+
const { container } = render(<Select ariaLabel="x" options={OPCOES} placement="top" />);
|
|
29
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
30
|
+
expect(container.querySelector(".umds-select--placement-top")).not.toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("auto abre para cima quando falta espaço embaixo e há mais em cima", () => {
|
|
34
|
+
medirGatilho(500, 544, 600);
|
|
35
|
+
const { container } = render(<Select ariaLabel="x" options={OPCOES} placement="auto" />);
|
|
36
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
37
|
+
expect(container.querySelector(".umds-select--placement-top")).not.toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("auto abre para baixo quando cabe", () => {
|
|
41
|
+
medirGatilho(20, 64, 900);
|
|
42
|
+
const { container } = render(<Select ariaLabel="x" options={OPCOES} placement="auto" />);
|
|
43
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
44
|
+
expect(container.querySelector(".umds-select--placement-top")).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("auto abre para baixo quando falta espaço dos dois lados e embaixo é maior", () => {
|
|
48
|
+
medirGatilho(60, 104, 220);
|
|
49
|
+
const { container } = render(<Select ariaLabel="x" options={OPCOES} placement="auto" />);
|
|
50
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
51
|
+
expect(container.querySelector(".umds-select--placement-top")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("Select · variant filter", () => {
|
|
56
|
+
it("escreve a classe do filtro; sem variant não escreve", () => {
|
|
57
|
+
const { container, rerender } = render(<Select ariaLabel="x" options={OPCOES} variant="filter" />);
|
|
58
|
+
expect(container.querySelector(".umds-select--filter")).not.toBeNull();
|
|
59
|
+
rerender(<Select ariaLabel="x" options={OPCOES} />);
|
|
60
|
+
expect(container.querySelector(".umds-select--filter")).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("emptyOptionLabel vem no topo, marcada sem seleção; escolher chama onChange com vazio e volta ao placeholder", () => {
|
|
64
|
+
const aoMudar = vi.fn();
|
|
65
|
+
const { rerender } = render(
|
|
66
|
+
<Select ariaLabel="Marcadores" placeholder="Marcadores" options={OPCOES} emptyOptionLabel="Todos os marcadores" onChange={aoMudar} />,
|
|
67
|
+
);
|
|
68
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
69
|
+
const opcoes = screen.getAllByRole("option");
|
|
70
|
+
expect(opcoes[0]).toHaveTextContent("Todos os marcadores");
|
|
71
|
+
expect(opcoes[0]).toHaveAttribute("aria-selected", "true");
|
|
72
|
+
fireEvent.click(opcoes[1]);
|
|
73
|
+
expect(aoMudar).toHaveBeenLastCalledWith("urgente");
|
|
74
|
+
|
|
75
|
+
rerender(
|
|
76
|
+
<Select ariaLabel="Marcadores" placeholder="Marcadores" options={OPCOES} emptyOptionLabel="Todos os marcadores" value="urgente" onChange={aoMudar} />,
|
|
77
|
+
);
|
|
78
|
+
expect(screen.getByRole("combobox")).toHaveTextContent("Urgente");
|
|
79
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
80
|
+
fireEvent.click(screen.getAllByRole("option")[0]);
|
|
81
|
+
expect(aoMudar).toHaveBeenLastCalledWith("");
|
|
82
|
+
|
|
83
|
+
rerender(
|
|
84
|
+
<Select ariaLabel="Marcadores" placeholder="Marcadores" options={OPCOES} emptyOptionLabel="Todos os marcadores" value="" onChange={aoMudar} />,
|
|
85
|
+
);
|
|
86
|
+
expect(screen.getByRole("combobox")).toHaveTextContent("Marcadores");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("sem emptyOptionLabel não há opção extra", () => {
|
|
90
|
+
render(<Select ariaLabel="x" options={OPCOES} />);
|
|
91
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
92
|
+
expect(screen.getAllByRole("option")).toHaveLength(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("footer aparece dentro da lista aberta, e não fecha ao clicar nele", () => {
|
|
96
|
+
render(<Select ariaLabel="x" options={OPCOES} footer={<button>Gerenciar marcadores</button>} />);
|
|
97
|
+
expect(screen.queryByText("Gerenciar marcadores")).toBeNull();
|
|
98
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
99
|
+
const acao = screen.getByText("Gerenciar marcadores");
|
|
100
|
+
expect(acao.closest(".umds-select__footer")).not.toBeNull();
|
|
101
|
+
fireEvent.mouseDown(acao);
|
|
102
|
+
expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "true");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("sem footer não há rodapé", () => {
|
|
106
|
+
const { container } = render(<Select ariaLabel="x" options={OPCOES} />);
|
|
107
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
108
|
+
expect(container.querySelector(".umds-select__footer")).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import Stack from "../components/Stack/Stack";
|
|
4
|
+
|
|
5
|
+
const TAGS = ["div", "span", "ul", "ol", "li", "section", "nav", "header", "footer", "form"] as const;
|
|
6
|
+
|
|
7
|
+
describe("Stack", () => {
|
|
8
|
+
it("sem props continua um div vertical com gap xxxs", () => {
|
|
9
|
+
const { container } = render(<Stack>x</Stack>);
|
|
10
|
+
const el = container.firstElementChild as HTMLElement;
|
|
11
|
+
expect(el.tagName).toBe("DIV");
|
|
12
|
+
expect(el.className).toBe("umds-stack umds-stack--vertical umds-stack--gap-xxxs");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("aceita gap none", () => {
|
|
16
|
+
const { container } = render(<Stack gap="none">x</Stack>);
|
|
17
|
+
expect(container.firstElementChild).toHaveClass("umds-stack--gap-none");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it.each(TAGS)("as=%s renderiza o elemento certo", (tag) => {
|
|
21
|
+
const { container } = render(<Stack as={tag}>x</Stack>);
|
|
22
|
+
expect(container.firstElementChild?.tagName.toLowerCase()).toBe(tag);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("ul e ol mantêm a semântica de lista", () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<Stack as="ul" aria-label="Itens">
|
|
28
|
+
<Stack as="li">um</Stack>
|
|
29
|
+
</Stack>,
|
|
30
|
+
);
|
|
31
|
+
expect(screen.getByRole("list", { name: "Itens" })).toBeInTheDocument();
|
|
32
|
+
expect(container.querySelectorAll("li")).toHaveLength(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("repassa id, aria-label, title e style ao elemento raiz", () => {
|
|
36
|
+
const { container } = render(
|
|
37
|
+
<Stack as="nav" id="menu" aria-label="Principal" title="Dica" style={{ marginTop: 4 }}>
|
|
38
|
+
x
|
|
39
|
+
</Stack>,
|
|
40
|
+
);
|
|
41
|
+
const el = container.firstElementChild as HTMLElement;
|
|
42
|
+
expect(el.id).toBe("menu");
|
|
43
|
+
expect(el.getAttribute("aria-label")).toBe("Principal");
|
|
44
|
+
expect(el.getAttribute("title")).toBe("Dica");
|
|
45
|
+
expect(el.style.marginTop).toBe("4px");
|
|
46
|
+
expect(screen.getByRole("navigation", { name: "Principal" })).toBe(el);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("as=form chama onSubmit", () => {
|
|
50
|
+
const aoEnviar = vi.fn((e: Event) => e.preventDefault());
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<Stack as="form" onSubmit={aoEnviar}>
|
|
53
|
+
<button type="submit">Enviar</button>
|
|
54
|
+
</Stack>,
|
|
55
|
+
);
|
|
56
|
+
fireEvent.submit(container.querySelector("form") as HTMLFormElement);
|
|
57
|
+
expect(aoEnviar).toHaveBeenCalledTimes(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("escreve as classes de item flex", () => {
|
|
61
|
+
const { container } = render(
|
|
62
|
+
<Stack grow shrink="0" basis="50" minWidth="auto" overflow="hidden" overflowY="auto" fullHeight>
|
|
63
|
+
x
|
|
64
|
+
</Stack>,
|
|
65
|
+
);
|
|
66
|
+
const el = container.firstElementChild as HTMLElement;
|
|
67
|
+
for (const c of [
|
|
68
|
+
"umds-stack--grow",
|
|
69
|
+
"umds-stack--shrink-0",
|
|
70
|
+
"umds-stack--basis-50",
|
|
71
|
+
"umds-stack--min-w-auto",
|
|
72
|
+
"umds-stack--overflow-hidden",
|
|
73
|
+
"umds-stack--overflow-y-auto",
|
|
74
|
+
"umds-stack--full-height",
|
|
75
|
+
]) {
|
|
76
|
+
expect(el).toHaveClass(c);
|
|
77
|
+
}
|
|
78
|
+
expect(el).not.toHaveClass("umds-stack--full-width");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import StatCard from "../components/StatCard/StatCard";
|
|
4
|
+
|
|
5
|
+
const raiz = (c: HTMLElement) => c.firstElementChild as HTMLElement;
|
|
6
|
+
|
|
7
|
+
describe("StatCard · labelPosition", () => {
|
|
8
|
+
it("top escreve a classe; sem a prop o cartão é o de antes", () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<>
|
|
11
|
+
<StatCard value={44} label="Não lidos" labelPosition="top" />
|
|
12
|
+
<StatCard value={44} label="Não lidos" />
|
|
13
|
+
</>,
|
|
14
|
+
);
|
|
15
|
+
expect(container.children[0]).toHaveClass("umds-stat-card--rotulo-topo");
|
|
16
|
+
expect(container.children[1]).not.toHaveClass("umds-stat-card--rotulo-topo");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("o número continua antes do rótulo no HTML (ordem do leitor de tela)", () => {
|
|
20
|
+
const { container } = render(<StatCard value={44} label="Não lidos" labelPosition="top" />);
|
|
21
|
+
const valor = container.querySelector(".umds-stat-card__valor") as HTMLElement;
|
|
22
|
+
const rotulo = container.querySelector(".umds-stat-card__rotulo") as HTMLElement;
|
|
23
|
+
expect(valor.compareDocumentPosition(rotulo) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("StatCard · infoTooltip", () => {
|
|
28
|
+
it("mostra o ⓘ como botão nomeado, dentro do rótulo, descrito pelo texto de ajuda", () => {
|
|
29
|
+
const { container } = render(<StatCard value={44} label="Não lidos" infoTooltip="Ainda não abertos por você." />);
|
|
30
|
+
const info = screen.getByRole("button", { name: "Mais informações" });
|
|
31
|
+
expect(container.querySelector(".umds-stat-card__rotulo")?.contains(info)).toBe(true);
|
|
32
|
+
expect(info).toHaveAccessibleDescription("Ainda não abertos por você.");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("infoLabel traduz o nome do ⓘ", () => {
|
|
36
|
+
render(<StatCard value={1} label="Unread" infoTooltip="Not opened yet." infoLabel="More info" />);
|
|
37
|
+
expect(screen.getByRole("button", { name: "More info" })).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("sem infoTooltip não há botão", () => {
|
|
41
|
+
render(<StatCard value={1} label="x" />);
|
|
42
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|