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