@1doc/1ds-react 0.0.1 → 2.7.1
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 +667 -0
- package/README.md +138 -6
- package/package.json +43 -10
- package/src/index.ts +161 -16
- package/src/teste/AttachmentCard.test.tsx +161 -0
- package/src/teste/Chart.test.tsx +233 -0
- package/src/teste/Combobox.test.tsx +251 -0
- package/src/teste/DataTable.test.tsx +70 -0
- package/src/teste/DatePicker.test.tsx +86 -0
- package/src/teste/Drawer.test.tsx +265 -0
- package/src/teste/Dropdown.test.tsx +217 -0
- package/src/teste/Input.test.tsx +64 -0
- package/src/teste/Kanban.test.tsx +333 -0
- package/src/teste/MultiSelect.test.tsx +73 -0
- package/src/teste/Podium.test.tsx +107 -0
- package/src/teste/Popover.test.tsx +221 -0
- package/src/teste/SegmentedControl.test.tsx +183 -0
- package/src/teste/Timeline.test.tsx +100 -0
- package/src/teste/Tree.test.tsx +74 -0
- package/src/teste/setup.ts +7 -0
- package/src/utils/bandeiras.ts +22 -0
- package/src/utils/chart.ts +682 -0
- package/src/utils/density.ts +27 -0
- package/src/utils/overlay-stack.ts +26 -0
- package/src/utils/theme.ts +63 -47
- package/src/utils/toast.ts +89 -0
- package/vitest.config.ts +28 -0
- package/dist/components/Alert/Alert.css +0 -188
- package/dist/components/Alert/Alert.d.ts +0 -65
- package/dist/components/Alert/Alert.js +0 -56
- package/dist/components/Avatar/Avatar.css +0 -88
- package/dist/components/Avatar/Avatar.d.ts +0 -34
- package/dist/components/Avatar/Avatar.js +0 -16
- package/dist/components/Avatar/AvatarGroup.d.ts +0 -12
- package/dist/components/Avatar/AvatarGroup.js +0 -8
- package/dist/components/Badge/Badge.css +0 -155
- package/dist/components/Badge/Badge.d.ts +0 -46
- package/dist/components/Badge/Badge.js +0 -17
- package/dist/components/Button/Button.css +0 -199
- package/dist/components/Button/Button.d.ts +0 -23
- package/dist/components/Button/Button.js +0 -62
- package/dist/components/Checkbox/Checkbox.css +0 -122
- package/dist/components/Checkbox/Checkbox.d.ts +0 -21
- package/dist/components/Checkbox/Checkbox.js +0 -49
- package/dist/components/Divider/Divider.css +0 -81
- package/dist/components/Divider/Divider.d.ts +0 -18
- package/dist/components/Divider/Divider.js +0 -26
- package/dist/components/Grid/Grid.css +0 -154
- package/dist/components/Grid/Grid.d.ts +0 -21
- package/dist/components/Grid/Grid.js +0 -53
- package/dist/components/Icon/Icon.css +0 -41
- package/dist/components/Icon/Icon.d.ts +0 -12
- package/dist/components/Icon/Icon.js +0 -29
- package/dist/components/Input/Input.css +0 -190
- package/dist/components/Input/Input.d.ts +0 -32
- package/dist/components/Input/Input.js +0 -58
- package/dist/components/Link/Link.css +0 -76
- package/dist/components/Link/Link.d.ts +0 -15
- package/dist/components/Link/Link.js +0 -19
- package/dist/components/Logo/Logo.css +0 -32
- package/dist/components/Logo/Logo.d.ts +0 -10
- package/dist/components/Logo/Logo.js +0 -16
- package/dist/components/Modal/Modal.css +0 -155
- package/dist/components/Modal/Modal.d.ts +0 -36
- package/dist/components/Modal/Modal.js +0 -55
- package/dist/components/Radio/Radio.css +0 -114
- package/dist/components/Radio/Radio.d.ts +0 -19
- package/dist/components/Radio/Radio.js +0 -39
- package/dist/components/Select/Select.css +0 -291
- package/dist/components/Select/Select.d.ts +0 -38
- package/dist/components/Select/Select.js +0 -246
- package/dist/components/Spinner/Spinner.css +0 -60
- package/dist/components/Spinner/Spinner.d.ts +0 -16
- package/dist/components/Spinner/Spinner.js +0 -16
- package/dist/components/Switch/Switch.css +0 -127
- package/dist/components/Switch/Switch.d.ts +0 -18
- package/dist/components/Switch/Switch.js +0 -57
- package/dist/components/Tabs/Tab.d.ts +0 -19
- package/dist/components/Tabs/Tab.js +0 -76
- package/dist/components/Tabs/TabContent.d.ts +0 -15
- package/dist/components/Tabs/TabContent.js +0 -11
- package/dist/components/Tabs/Tabs.css +0 -124
- package/dist/components/Tabs/Tabs.d.ts +0 -20
- package/dist/components/Tabs/Tabs.js +0 -14
- package/dist/components/Tabs/TabsContent.d.ts +0 -16
- package/dist/components/Tabs/TabsContent.js +0 -12
- package/dist/components/Tabs/tabs.context.d.ts +0 -2
- package/dist/components/Tabs/tabs.context.js +0 -2
- package/dist/components/Tag/Tag.css +0 -140
- package/dist/components/Tag/Tag.d.ts +0 -63
- package/dist/components/Tag/Tag.js +0 -48
- package/dist/components/Text/Text.css +0 -1213
- package/dist/components/Text/Text.d.ts +0 -70
- package/dist/components/Text/Text.js +0 -89
- package/dist/components/Textarea/Textarea.css +0 -133
- package/dist/components/Textarea/Textarea.d.ts +0 -28
- package/dist/components/Textarea/Textarea.js +0 -72
- package/dist/index.d.ts +0 -25
- package/dist/index.js +0 -27
- package/dist/utils/theme.d.ts +0 -30
- package/dist/utils/theme.js +0 -106
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
3
|
+
import BarChart from "../components/Chart/BarChart";
|
|
4
|
+
import LineChart from "../components/Chart/LineChart";
|
|
5
|
+
import DonutChart from "../components/Chart/DonutChart";
|
|
6
|
+
import {
|
|
7
|
+
CHART_MAX_SERIES,
|
|
8
|
+
RAIO_DA_BARRA,
|
|
9
|
+
caminhoDaBarra,
|
|
10
|
+
escalaRedonda,
|
|
11
|
+
formatarValor,
|
|
12
|
+
montarRosca,
|
|
13
|
+
} from "../utils/chart";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* O desenho dos gráficos quem guarda é a esteira visual. Aqui fica o que uma
|
|
17
|
+
* foto não mostra: a escala redonda que sempre inclui o zero, o teto de
|
|
18
|
+
* cinco cores, a tabela que leva os números ao leitor de tela, e o teclado
|
|
19
|
+
* que anda entre as categorias.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const MESES = [
|
|
23
|
+
{ label: "Jan", value: 120 },
|
|
24
|
+
{ label: "Fev", value: 90 },
|
|
25
|
+
{ label: "Mar", value: 150 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SERIES = [
|
|
29
|
+
{ name: "2025", values: [10, 20, 30] },
|
|
30
|
+
{ name: "2026", values: [15, null, 35] },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
describe("Chart · geometria", () => {
|
|
34
|
+
it("a escala tem números redondos e sempre inclui o zero", () => {
|
|
35
|
+
const escala = escalaRedonda([37, 142]);
|
|
36
|
+
expect(escala.min).toBe(0);
|
|
37
|
+
expect(escala.max).toBeGreaterThanOrEqual(142);
|
|
38
|
+
expect(escala.ticks[0]).toBe(0);
|
|
39
|
+
const passo = escala.ticks[1] - escala.ticks[0];
|
|
40
|
+
for (const t of escala.ticks) expect(t % passo).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("valores negativos puxam a escala para baixo do zero", () => {
|
|
44
|
+
const escala = escalaRedonda([-12, 40]);
|
|
45
|
+
expect(escala.min).toBeLessThan(0);
|
|
46
|
+
expect(escala.ticks).toContain(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("formata em pt-BR, com % encostado e outras unidades com espaço", () => {
|
|
50
|
+
expect(formatarValor(1234.5, {})).toBe("1.234,5");
|
|
51
|
+
expect(formatarValor(12, { unit: "%" })).toBe("12%");
|
|
52
|
+
expect(formatarValor(3, { unit: "dias" })).toBe("3 dias");
|
|
53
|
+
expect(formatarValor(null, {})).toBe("—");
|
|
54
|
+
expect(formatarValor(5, { valueFormatter: (v) => `R$ ${v}`, unit: "%" })).toBe("R$ 5");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("a ponta da barra usa o raio canônico, limitado por metade da largura e da altura", () => {
|
|
58
|
+
const raio = (d: string) => Number(/A([\d.]+),/.exec(d)?.[1]);
|
|
59
|
+
expect(RAIO_DA_BARRA).toBe(8);
|
|
60
|
+
// Barra larga e alta: 8px na ponta, quadrada na base (y0 = 100).
|
|
61
|
+
const alta = caminhoDaBarra(0, 24, 100, 20);
|
|
62
|
+
expect(raio(alta)).toBe(8);
|
|
63
|
+
expect(alta.startsWith("M0,100V28")).toBe(true);
|
|
64
|
+
// Fina: nunca vira pílula.
|
|
65
|
+
expect(raio(caminhoDaBarra(0, 6, 100, 20))).toBe(3);
|
|
66
|
+
// Curta: não passa da metade da altura.
|
|
67
|
+
expect(raio(caminhoDaBarra(0, 24, 100, 94))).toBe(3);
|
|
68
|
+
// Negativa: arredonda embaixo, base quadrada no zero.
|
|
69
|
+
const negativa = caminhoDaBarra(0, 24, 100, 180);
|
|
70
|
+
expect(raio(negativa)).toBe(8);
|
|
71
|
+
expect(negativa.startsWith("M0,100V172")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("a rosca agrupa o excedente em Outros, na cor neutra", () => {
|
|
75
|
+
const partes = Array.from({ length: 7 }, (_, i) => ({ label: `P${i}`, value: 10 }));
|
|
76
|
+
const rosca = montarRosca(partes, 160, {}, "Outros");
|
|
77
|
+
expect(rosca.fatias).toHaveLength(CHART_MAX_SERIES);
|
|
78
|
+
const ultima = rosca.fatias[rosca.fatias.length - 1];
|
|
79
|
+
expect(ultima.nome).toBe("Outros");
|
|
80
|
+
expect(ultima.valor).toBe(30);
|
|
81
|
+
expect(ultima.serie).toBe(0);
|
|
82
|
+
expect(rosca.total).toBe(70);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("BarChart", () => {
|
|
87
|
+
it("desenha uma barra por valor e resume o gráfico no role=img", () => {
|
|
88
|
+
const { container } = render(<BarChart data={MESES} title="Processos por mês" />);
|
|
89
|
+
expect(container.querySelectorAll(".umds-chart__barra")).toHaveLength(3);
|
|
90
|
+
const resumo = screen.getByRole("img").getAttribute("aria-label") || "";
|
|
91
|
+
expect(resumo).toContain("3 categorias");
|
|
92
|
+
// O título já nomeia a figura pela <figcaption>; repetir seria ler duas vezes.
|
|
93
|
+
expect(resumo).not.toContain("Processos por mês");
|
|
94
|
+
expect(container.querySelector("figure > figcaption")?.textContent).toBe("Processos por mês");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("leva os números a uma tabela para o leitor de tela", () => {
|
|
98
|
+
render(<BarChart series={SERIES} labels={["Jan", "Fev", "Mar"]} />);
|
|
99
|
+
const tabela = screen.getByRole("table");
|
|
100
|
+
expect(within(tabela).getAllByRole("columnheader").map((c) => c.textContent)).toEqual([
|
|
101
|
+
"Categoria",
|
|
102
|
+
"2025",
|
|
103
|
+
"2026",
|
|
104
|
+
]);
|
|
105
|
+
const fev = within(tabela).getByRole("rowheader", { name: "Fev" }).closest("tr")!;
|
|
106
|
+
expect(fev.textContent).toContain("20");
|
|
107
|
+
// "—" é lido como "travessão": para o leitor de tela, ausência vira texto.
|
|
108
|
+
expect(fev.textContent).toContain("sem dado");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("série única não tem legenda; duas séries têm", () => {
|
|
112
|
+
const { container, rerender } = render(<BarChart data={MESES} />);
|
|
113
|
+
expect(container.querySelector(".umds-chart__legenda")).toBeNull();
|
|
114
|
+
rerender(<BarChart series={SERIES} labels={["Jan", "Fev", "Mar"]} />);
|
|
115
|
+
expect(container.querySelectorAll(".umds-chart__legenda-item")).toHaveLength(2);
|
|
116
|
+
rerender(<BarChart series={SERIES} labels={["Jan", "Fev", "Mar"]} showLegend={false} />);
|
|
117
|
+
expect(container.querySelector(".umds-chart__legenda")).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("não desenha a sexta série — cor gerada não existe", () => {
|
|
121
|
+
const muitas = Array.from({ length: 7 }, (_, i) => ({ name: `S${i}`, values: [i + 1] }));
|
|
122
|
+
const { container } = render(<BarChart series={muitas} labels={["A"]} />);
|
|
123
|
+
expect(container.querySelectorAll(".umds-chart__barra")).toHaveLength(CHART_MAX_SERIES);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("sem valores mostra o texto de vazio, sem tabela nem desenho", () => {
|
|
127
|
+
render(<BarChart data={[]} emptyText="Nada por aqui" />);
|
|
128
|
+
expect(screen.getByText("Nada por aqui")).toBeTruthy();
|
|
129
|
+
expect(screen.queryByRole("img")).toBeNull();
|
|
130
|
+
expect(screen.queryByRole("table")).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("Tab entra numa categoria só; as setas andam e o tooltip acompanha", () => {
|
|
134
|
+
const { container } = render(<BarChart data={MESES} />);
|
|
135
|
+
const alvos = screen.getAllByRole("listitem");
|
|
136
|
+
expect(alvos.map((a) => a.getAttribute("tabindex"))).toEqual(["0", "-1", "-1"]);
|
|
137
|
+
// O texto está DENTRO do item (oculto), não num aria-label: JAWS ignora
|
|
138
|
+
// aria-label em listitem no modo de navegação.
|
|
139
|
+
expect(alvos[0].textContent).toBe("Jan: 120");
|
|
140
|
+
|
|
141
|
+
fireEvent.focus(alvos[0]);
|
|
142
|
+
expect(container.querySelector(".umds-chart__tooltip")?.textContent).toContain("Jan");
|
|
143
|
+
|
|
144
|
+
fireEvent.keyDown(alvos[0], { key: "ArrowRight" });
|
|
145
|
+
expect(document.activeElement).toBe(alvos[1]);
|
|
146
|
+
fireEvent.focus(alvos[1]);
|
|
147
|
+
expect(container.querySelector(".umds-chart__tooltip")?.textContent).toContain("Fev");
|
|
148
|
+
expect(screen.getAllByRole("listitem")[1].getAttribute("tabindex")).toBe("0");
|
|
149
|
+
|
|
150
|
+
fireEvent.keyDown(alvos[1], { key: "End" });
|
|
151
|
+
expect(document.activeElement).toBe(alvos[2]);
|
|
152
|
+
|
|
153
|
+
fireEvent.focus(alvos[2]);
|
|
154
|
+
// fireEvent devolve false quando o evento teve preventDefault.
|
|
155
|
+
expect(fireEvent.keyDown(alvos[2], { key: "Escape" })).toBe(false);
|
|
156
|
+
expect(container.querySelector(".umds-chart__tooltip")).toBeNull();
|
|
157
|
+
|
|
158
|
+
// Sem tooltip, o Esc passa adiante — é ele que fecha o Modal em volta.
|
|
159
|
+
expect(fireEvent.keyDown(alvos[2], { key: "Escape" })).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("o mouse saindo de outra categoria não apaga o tooltip de quem tem o foco", () => {
|
|
163
|
+
const { container } = render(<BarChart data={MESES} />);
|
|
164
|
+
const alvos = screen.getAllByRole("listitem");
|
|
165
|
+
fireEvent.focus(alvos[0]);
|
|
166
|
+
fireEvent.mouseEnter(alvos[2]);
|
|
167
|
+
expect(container.querySelector(".umds-chart__tooltip")?.textContent).toContain("Mar");
|
|
168
|
+
fireEvent.mouseLeave(alvos[2]);
|
|
169
|
+
expect(container.querySelector(".umds-chart__tooltip")?.textContent).toContain("Jan");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("dados que encolhem não tiram o gráfico da ordem de Tab", () => {
|
|
173
|
+
const { rerender } = render(<BarChart data={MESES} />);
|
|
174
|
+
fireEvent.focus(screen.getAllByRole("listitem")[2]);
|
|
175
|
+
rerender(<BarChart data={MESES.slice(0, 2)} />);
|
|
176
|
+
expect(screen.getAllByRole("listitem").map((a) => a.getAttribute("tabindex"))).toContain("0");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("com uma categoria em foco, as barras das outras recuam", () => {
|
|
180
|
+
const { container } = render(<BarChart data={MESES} />);
|
|
181
|
+
fireEvent.mouseEnter(screen.getAllByRole("listitem")[2]);
|
|
182
|
+
expect(container.querySelectorAll(".umds-chart__barra--recuada")).toHaveLength(2);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("LineChart", () => {
|
|
187
|
+
it("um traço por série e null interrompe a linha", () => {
|
|
188
|
+
const { container } = render(<LineChart series={SERIES} labels={["Jan", "Fev", "Mar"]} />);
|
|
189
|
+
const tracos = container.querySelectorAll(".umds-chart__linha");
|
|
190
|
+
expect(tracos).toHaveLength(2);
|
|
191
|
+
// 2026 tem um buraco em Fev: dois "M" no caminho, nunca um "L" atravessando.
|
|
192
|
+
expect((tracos[1].getAttribute("d") || "").match(/M/g)).toHaveLength(2);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("pontos só na categoria em foco, a menos que showPoints", () => {
|
|
196
|
+
const { container, rerender } = render(<LineChart data={MESES} />);
|
|
197
|
+
expect(container.querySelectorAll(".umds-chart__ponto")).toHaveLength(0);
|
|
198
|
+
fireEvent.mouseEnter(screen.getAllByRole("listitem")[1]);
|
|
199
|
+
expect(container.querySelectorAll(".umds-chart__ponto")).toHaveLength(1);
|
|
200
|
+
expect(container.querySelector(".umds-chart__cursor")).not.toBeNull();
|
|
201
|
+
rerender(<LineChart data={MESES} showPoints />);
|
|
202
|
+
expect(container.querySelectorAll(".umds-chart__ponto")).toHaveLength(3);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("DonutChart", () => {
|
|
207
|
+
const PARTES = [
|
|
208
|
+
{ label: "Concluídos", value: 60 },
|
|
209
|
+
{ label: "Em andamento", value: 30 },
|
|
210
|
+
{ label: "Pendentes", value: 10 },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
it("total no centro e legenda com valor e percentual em texto", () => {
|
|
214
|
+
const { container } = render(<DonutChart data={PARTES} />);
|
|
215
|
+
expect(container.querySelector(".umds-chart__centro-valor")?.textContent).toBe("100");
|
|
216
|
+
const itens = screen.getAllByRole("listitem");
|
|
217
|
+
expect(itens).toHaveLength(3);
|
|
218
|
+
expect(itens[0].textContent).toContain("Concluídos");
|
|
219
|
+
expect(itens[0].textContent).toContain("60%");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("sem legenda visível, a lista continua para o leitor de tela", () => {
|
|
223
|
+
const { container } = render(<DonutChart data={PARTES} showLegend={false} />);
|
|
224
|
+
const lista = container.querySelector(".umds-chart__legenda")!;
|
|
225
|
+
expect(lista.className).toContain("umds-chart__oculto");
|
|
226
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(3);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("total zero é estado vazio", () => {
|
|
230
|
+
render(<DonutChart data={[{ label: "A", value: 0 }]} />);
|
|
231
|
+
expect(screen.getByText("Sem dados para exibir.")).toBeTruthy();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
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 Combobox from "../components/Combobox/Combobox";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* O `Combobox` é o primeiro campo do DS em que digitar MUDA a lista. Três
|
|
8
|
+
* contratos não se veem numa foto e quebram calados:
|
|
9
|
+
*
|
|
10
|
+
* - o filtro roda sobre o texto digitado, e PARA de rodar quando a busca é
|
|
11
|
+
* remota (`onSearchChange`) — filtrar de novo esconderia o que o servidor
|
|
12
|
+
* acabou de mandar;
|
|
13
|
+
* - `allowCustomValue` muda o Enter: sem ela o Enter pega a sugestão
|
|
14
|
+
* destacada, com ela o Enter respeita o que a pessoa escreveu;
|
|
15
|
+
* - `aria-activedescendant` precisa apontar para um id que EXISTE, inclusive
|
|
16
|
+
* quando o filtro esvazia a lista.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const ESTADOS = [
|
|
20
|
+
{ label: "Paraná", value: "PR" },
|
|
21
|
+
{ label: "Rio de Janeiro", value: "RJ" },
|
|
22
|
+
{ label: "Rio Grande do Sul", value: "RS" },
|
|
23
|
+
{ label: "São Paulo", value: "SP" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe("Combobox · filtro", () => {
|
|
27
|
+
it("digitar filtra a lista pelo rótulo, sem diferenciar maiúscula", async () => {
|
|
28
|
+
const usuario = userEvent.setup();
|
|
29
|
+
render(<Combobox label="Estado" options={ESTADOS} />);
|
|
30
|
+
|
|
31
|
+
await usuario.type(screen.getByRole("combobox"), "RIO");
|
|
32
|
+
|
|
33
|
+
const opcoes = screen.getAllByRole("option");
|
|
34
|
+
expect(opcoes).toHaveLength(2);
|
|
35
|
+
expect(opcoes[0]).toHaveTextContent("Rio de Janeiro");
|
|
36
|
+
expect(opcoes[1]).toHaveTextContent("Rio Grande do Sul");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("sem resultado, mostra o texto de vazio e nenhuma opção", async () => {
|
|
40
|
+
const usuario = userEvent.setup();
|
|
41
|
+
render(
|
|
42
|
+
<Combobox label="Estado" options={ESTADOS} emptyLabel="Nada aqui" />,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await usuario.type(screen.getByRole("combobox"), "zzz");
|
|
46
|
+
|
|
47
|
+
expect(screen.queryAllByRole("option")).toHaveLength(0);
|
|
48
|
+
expect(screen.getByText("Nada aqui")).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* O contrato que o Storybook quebrou na primeira versão: `onSearchChange`
|
|
53
|
+
* presente significa "quem filtra é o servidor". Se o componente filtrasse
|
|
54
|
+
* também, um resultado remoto que não casa com o texto digitado sumiria.
|
|
55
|
+
*/
|
|
56
|
+
it("com onSearchChange, NÃO filtra localmente", async () => {
|
|
57
|
+
const usuario = userEvent.setup();
|
|
58
|
+
const aoBuscar = vi.fn();
|
|
59
|
+
render(
|
|
60
|
+
<Combobox label="Estado" options={ESTADOS} onSearchChange={aoBuscar} />,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await usuario.type(screen.getByRole("combobox"), "zzz");
|
|
64
|
+
|
|
65
|
+
expect(screen.getAllByRole("option")).toHaveLength(ESTADOS.length);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("onInputChange apenas observa — o filtro local continua", async () => {
|
|
69
|
+
const usuario = userEvent.setup();
|
|
70
|
+
const aoDigitar = vi.fn();
|
|
71
|
+
render(
|
|
72
|
+
<Combobox label="Estado" options={ESTADOS} onInputChange={aoDigitar} />,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
await usuario.type(screen.getByRole("combobox"), "par");
|
|
76
|
+
|
|
77
|
+
expect(aoDigitar).toHaveBeenLastCalledWith("par");
|
|
78
|
+
expect(screen.getAllByRole("option")).toHaveLength(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("Combobox · teclado", () => {
|
|
83
|
+
it("ArrowDown abre a lista e Enter escolhe a opção destacada", async () => {
|
|
84
|
+
const usuario = userEvent.setup();
|
|
85
|
+
const aoMudar = vi.fn();
|
|
86
|
+
render(<Combobox label="Estado" options={ESTADOS} onChange={aoMudar} />);
|
|
87
|
+
|
|
88
|
+
const campo = screen.getByRole("combobox");
|
|
89
|
+
await usuario.click(campo);
|
|
90
|
+
expect(campo).toHaveAttribute("aria-expanded", "false");
|
|
91
|
+
|
|
92
|
+
await usuario.keyboard("{ArrowDown}");
|
|
93
|
+
expect(campo).toHaveAttribute("aria-expanded", "true");
|
|
94
|
+
|
|
95
|
+
await usuario.keyboard("{ArrowDown}{Enter}");
|
|
96
|
+
|
|
97
|
+
expect(aoMudar).toHaveBeenCalledWith("RJ");
|
|
98
|
+
expect(campo).toHaveValue("Rio de Janeiro");
|
|
99
|
+
expect(campo).toHaveAttribute("aria-expanded", "false");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("ArrowUp na primeira opção dá a volta para a última", async () => {
|
|
103
|
+
const usuario = userEvent.setup();
|
|
104
|
+
const aoMudar = vi.fn();
|
|
105
|
+
render(<Combobox label="Estado" options={ESTADOS} onChange={aoMudar} />);
|
|
106
|
+
|
|
107
|
+
const campo = screen.getByRole("combobox");
|
|
108
|
+
await usuario.click(campo);
|
|
109
|
+
await usuario.keyboard("{ArrowDown}{ArrowUp}{Enter}");
|
|
110
|
+
|
|
111
|
+
expect(aoMudar).toHaveBeenCalledWith("SP");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("Escape fecha a lista; com a lista fechada, limpa o campo", async () => {
|
|
115
|
+
const usuario = userEvent.setup();
|
|
116
|
+
render(
|
|
117
|
+
<Combobox label="Estado" options={ESTADOS} clearable defaultValue="PR" />,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const campo = screen.getByRole("combobox");
|
|
121
|
+
await usuario.click(campo);
|
|
122
|
+
await usuario.keyboard("{ArrowDown}");
|
|
123
|
+
expect(campo).toHaveAttribute("aria-expanded", "true");
|
|
124
|
+
|
|
125
|
+
await usuario.keyboard("{Escape}");
|
|
126
|
+
expect(campo).toHaveAttribute("aria-expanded", "false");
|
|
127
|
+
expect(campo).toHaveValue("Paraná");
|
|
128
|
+
|
|
129
|
+
await usuario.keyboard("{Escape}");
|
|
130
|
+
expect(campo).toHaveValue("");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("Combobox · sair do campo", () => {
|
|
135
|
+
/**
|
|
136
|
+
* Digitar e sair sem escolher deixava o texto meio-digitado no campo com o
|
|
137
|
+
* valor antigo por baixo. O campo passava a mentir — inclusive para o leitor
|
|
138
|
+
* de tela, que lê o texto do input como nome do campo.
|
|
139
|
+
*/
|
|
140
|
+
it("texto meio-digitado volta para o rótulo do valor escolhido", async () => {
|
|
141
|
+
const usuario = userEvent.setup();
|
|
142
|
+
render(
|
|
143
|
+
<>
|
|
144
|
+
<Combobox label="Estado" options={ESTADOS} defaultValue="PR" />
|
|
145
|
+
<button>fora</button>
|
|
146
|
+
</>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const campo = screen.getByRole("combobox");
|
|
150
|
+
await usuario.clear(campo);
|
|
151
|
+
await usuario.type(campo, "rio");
|
|
152
|
+
expect(campo).toHaveValue("rio");
|
|
153
|
+
|
|
154
|
+
await usuario.tab();
|
|
155
|
+
expect(campo).toHaveValue("Paraná");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("com allowCustomValue o texto É o valor, e fica como está", async () => {
|
|
159
|
+
const usuario = userEvent.setup();
|
|
160
|
+
render(
|
|
161
|
+
<>
|
|
162
|
+
<Combobox label="Assunto" options={ESTADOS} allowCustomValue />
|
|
163
|
+
<button>fora</button>
|
|
164
|
+
</>,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const campo = screen.getByRole("combobox");
|
|
168
|
+
await usuario.type(campo, "outro assunto");
|
|
169
|
+
await usuario.tab();
|
|
170
|
+
|
|
171
|
+
expect(campo).toHaveValue("outro assunto");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("Combobox · texto livre", () => {
|
|
176
|
+
it("sem allowCustomValue, o Enter pega a primeira sugestão", async () => {
|
|
177
|
+
const usuario = userEvent.setup();
|
|
178
|
+
const aoMudar = vi.fn();
|
|
179
|
+
render(<Combobox label="Estado" options={ESTADOS} onChange={aoMudar} />);
|
|
180
|
+
|
|
181
|
+
await usuario.type(screen.getByRole("combobox"), "são{Enter}");
|
|
182
|
+
|
|
183
|
+
expect(aoMudar).toHaveBeenCalledWith("SP");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Com texto livre nenhuma opção nasce destacada — senão o Enter trocaria o
|
|
188
|
+
* que a pessoa escreveu pela sugestão de cima, que é exatamente o que ela
|
|
189
|
+
* NÃO quis ao digitar algo fora da lista.
|
|
190
|
+
*/
|
|
191
|
+
it("com allowCustomValue, o Enter respeita o que foi digitado", async () => {
|
|
192
|
+
const usuario = userEvent.setup();
|
|
193
|
+
const aoMudar = vi.fn();
|
|
194
|
+
render(
|
|
195
|
+
<Combobox
|
|
196
|
+
label="Assunto"
|
|
197
|
+
options={ESTADOS}
|
|
198
|
+
allowCustomValue
|
|
199
|
+
onChange={aoMudar}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await usuario.type(screen.getByRole("combobox"), "são{Enter}");
|
|
204
|
+
|
|
205
|
+
expect(aoMudar).toHaveBeenCalledWith("são");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("Combobox · acessibilidade", () => {
|
|
210
|
+
it("o rótulo visível nomeia o campo", () => {
|
|
211
|
+
render(<Combobox label="Estado" options={ESTADOS} />);
|
|
212
|
+
expect(screen.getByRole("combobox", { name: "Estado" })).toBeInTheDocument();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* `aria-activedescendant` apontando para um id inexistente é referência
|
|
217
|
+
* quebrada — o leitor de tela anuncia o nada. O caso que quebra é o filtro
|
|
218
|
+
* esvaziar a lista com um índice ativo ainda de pé.
|
|
219
|
+
*/
|
|
220
|
+
it("aria-activedescendant só existe quando aponta para uma opção real", async () => {
|
|
221
|
+
const usuario = userEvent.setup();
|
|
222
|
+
render(<Combobox label="Estado" options={ESTADOS} />);
|
|
223
|
+
const campo = screen.getByRole("combobox");
|
|
224
|
+
|
|
225
|
+
await usuario.type(campo, "rio");
|
|
226
|
+
const idAtivo = campo.getAttribute("aria-activedescendant");
|
|
227
|
+
expect(idAtivo).toBeTruthy();
|
|
228
|
+
expect(document.getElementById(idAtivo!)).toBeInTheDocument();
|
|
229
|
+
|
|
230
|
+
// Filtro esvaziou a lista: ou o atributo some, ou ele aponta para algo que
|
|
231
|
+
// existe. Apontar para um id ausente é referência quebrada.
|
|
232
|
+
await usuario.type(campo, "zzz");
|
|
233
|
+
expect(screen.queryAllByRole("option")).toHaveLength(0);
|
|
234
|
+
const depois = campo.getAttribute("aria-activedescendant");
|
|
235
|
+
if (depois !== null) {
|
|
236
|
+
expect(document.getElementById(depois)).toBeInTheDocument();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("readOnly não abre a lista", async () => {
|
|
241
|
+
const usuario = userEvent.setup();
|
|
242
|
+
render(<Combobox label="Estado" options={ESTADOS} readOnly />);
|
|
243
|
+
|
|
244
|
+
const campo = screen.getByRole("combobox");
|
|
245
|
+
await usuario.click(campo);
|
|
246
|
+
await usuario.keyboard("{ArrowDown}");
|
|
247
|
+
|
|
248
|
+
expect(campo).toHaveAttribute("aria-expanded", "false");
|
|
249
|
+
expect(screen.queryAllByRole("option")).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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 DataTable from "../components/DataTable/DataTable";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* O que esta suíte guarda: o P0-01 da auditoria de 18/09.
|
|
8
|
+
*
|
|
9
|
+
* Com `selectable` e `onRowClick` juntos, o Espaço dentro do checkbox era
|
|
10
|
+
* engolido pelo `preventDefault` do handler da linha e acionava a linha inteira
|
|
11
|
+
* — marcar por teclado era impossível. Foi corrigido, e nada impedia de voltar:
|
|
12
|
+
* a regressão visual não vê tecla, e o axe não vê comportamento.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const COLUNAS = [
|
|
16
|
+
{ key: "nome", label: "Nome" },
|
|
17
|
+
{ key: "setor", label: "Setor" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const DADOS = [
|
|
21
|
+
{ nome: "Cris Lima", setor: "SADM" },
|
|
22
|
+
{ nome: "Ana Paula", setor: "SEJUS" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
describe("DataTable · seleção por teclado", () => {
|
|
26
|
+
it("o Espaço no checkbox marca a linha e NÃO aciona onRowClick", async () => {
|
|
27
|
+
const usuario = userEvent.setup();
|
|
28
|
+
const aoClicarNaLinha = vi.fn();
|
|
29
|
+
const aoSelecionar = vi.fn();
|
|
30
|
+
|
|
31
|
+
render(
|
|
32
|
+
<DataTable
|
|
33
|
+
columns={COLUNAS}
|
|
34
|
+
data={DADOS}
|
|
35
|
+
selectable
|
|
36
|
+
onRowClick={aoClicarNaLinha}
|
|
37
|
+
onSelect={aoSelecionar}
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// O primeiro checkbox é o "selecionar todas" do cabeçalho; o da linha é o 2º.
|
|
42
|
+
const caixas = screen.getAllByRole("checkbox");
|
|
43
|
+
await usuario.tab();
|
|
44
|
+
caixas[1].focus();
|
|
45
|
+
await usuario.keyboard(" ");
|
|
46
|
+
|
|
47
|
+
expect(aoSelecionar).toHaveBeenCalledWith([0]);
|
|
48
|
+
expect(aoClicarNaLinha).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("o Espaço na própria linha aciona onRowClick", async () => {
|
|
52
|
+
const usuario = userEvent.setup();
|
|
53
|
+
const aoClicarNaLinha = vi.fn();
|
|
54
|
+
|
|
55
|
+
render(<DataTable columns={COLUNAS} data={DADOS} onRowClick={aoClicarNaLinha} />);
|
|
56
|
+
|
|
57
|
+
const linhas = screen.getAllByRole("row");
|
|
58
|
+
// getAllByRole('row') inclui a linha de cabeçalho — a primeira de dados é a 2ª.
|
|
59
|
+
linhas[1].focus();
|
|
60
|
+
await usuario.keyboard(" ");
|
|
61
|
+
|
|
62
|
+
expect(aoClicarNaLinha).toHaveBeenCalledWith(0, DADOS[0]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("sem onRowClick a linha fica fora da ordem de tabulação", () => {
|
|
66
|
+
render(<DataTable columns={COLUNAS} data={DADOS} />);
|
|
67
|
+
const linhas = screen.getAllByRole("row");
|
|
68
|
+
expect(linhas[1]).not.toHaveAttribute("tabindex", "0");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import DatePicker from "../components/DatePicker/DatePicker";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* O que esta suíte guarda: o P0-02 da auditoria de 18/09.
|
|
8
|
+
*
|
|
9
|
+
* O foco nunca entrava no painel. `ArrowDown` abria o calendário e o teclado
|
|
10
|
+
* ficava inerte, porque o painel vive num Portal e o Tab a partir do gatilho ia
|
|
11
|
+
* para o resto da página — o calendário era inoperável sem mouse.
|
|
12
|
+
*
|
|
13
|
+
* O alvo do foco é o dia marcado pelo roving tabindex, que é a verdade do DOM
|
|
14
|
+
* no instante em que o frame roda. O seletor anterior lia um valor de estado da
|
|
15
|
+
* closure do render anterior, que ao abrir ainda era o antigo.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
describe("DatePicker · teclado", () => {
|
|
19
|
+
it("abrir com ArrowDown põe o foco num dia do calendário", async () => {
|
|
20
|
+
const usuario = userEvent.setup();
|
|
21
|
+
render(<DatePicker label="Data de nascimento" />);
|
|
22
|
+
|
|
23
|
+
const gatilho = screen.getByRole("combobox");
|
|
24
|
+
gatilho.focus();
|
|
25
|
+
await usuario.keyboard("{ArrowDown}");
|
|
26
|
+
|
|
27
|
+
const painel = await screen.findByRole("dialog");
|
|
28
|
+
expect(painel).toBeVisible();
|
|
29
|
+
|
|
30
|
+
// O foco tem de estar DENTRO do painel, não no gatilho nem no body.
|
|
31
|
+
await waitFor(() => {
|
|
32
|
+
expect(painel.contains(document.activeElement)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("o dia focável é único — roving tabindex", async () => {
|
|
37
|
+
const usuario = userEvent.setup();
|
|
38
|
+
render(<DatePicker label="Data" />);
|
|
39
|
+
|
|
40
|
+
await usuario.click(screen.getByRole("combobox"));
|
|
41
|
+
const painel = await screen.findByRole("dialog");
|
|
42
|
+
|
|
43
|
+
const focaveis = painel.querySelectorAll('.umds-datepicker__day[tabindex="0"]');
|
|
44
|
+
expect(focaveis.length).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("Escape fecha o painel e devolve o foco ao campo", async () => {
|
|
48
|
+
const usuario = userEvent.setup();
|
|
49
|
+
render(<DatePicker label="Data" />);
|
|
50
|
+
|
|
51
|
+
const gatilho = screen.getByRole("combobox");
|
|
52
|
+
await usuario.click(gatilho);
|
|
53
|
+
await screen.findByRole("dialog");
|
|
54
|
+
|
|
55
|
+
await usuario.keyboard("{Escape}");
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
expect(document.activeElement).toBe(gatilho);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `format` é de EXIBIÇÃO; o `onChange` entrega ISO. Vale fixar em teste
|
|
65
|
+
* porque é a pergunta que todo consumidor faz e a resposta não está óbvia
|
|
66
|
+
* na API: quem monta um formulário quer saber o que vai para o backend.
|
|
67
|
+
*/
|
|
68
|
+
it("escolher um dia devolve ISO no onChange, não o formato de exibição", async () => {
|
|
69
|
+
const usuario = userEvent.setup();
|
|
70
|
+
const aoMudar = vi.fn();
|
|
71
|
+
render(<DatePicker label="Data" format="DD/MM/YYYY" onChange={aoMudar} />);
|
|
72
|
+
|
|
73
|
+
await usuario.click(screen.getByRole("combobox"));
|
|
74
|
+
const painel = await screen.findByRole("dialog");
|
|
75
|
+
|
|
76
|
+
const dia15 = painel.querySelector('[aria-label^="15 de"]') as HTMLElement;
|
|
77
|
+
expect(dia15).toBeTruthy();
|
|
78
|
+
await usuario.click(dia15);
|
|
79
|
+
|
|
80
|
+
expect(aoMudar).toHaveBeenCalled();
|
|
81
|
+
const valor = aoMudar.mock.calls[0][0];
|
|
82
|
+
expect(valor).toMatch(/^\d{4}-\d{2}-15$/);
|
|
83
|
+
// E o campo mostra no formato pedido, que é outra coisa.
|
|
84
|
+
expect(screen.getByRole("combobox")).toHaveTextContent(/15\/\d{2}\/\d{4}/);
|
|
85
|
+
});
|
|
86
|
+
});
|