@1doc/1ds-react 0.0.2 → 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 +659 -0
- package/README.md +137 -6
- package/package.json +43 -28
- 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 -55
- package/dist/components/Icon/Icon.d.ts +0 -13
- package/dist/components/Icon/Icon.js +0 -41
- 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,265 @@
|
|
|
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 { useState } from "react";
|
|
5
|
+
import Drawer from "../components/Drawer/Drawer";
|
|
6
|
+
import Button from "../components/Button/Button";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* O Drawer é overlay modal: prende o foco, fecha no Escape e devolve o foco a
|
|
10
|
+
* quem abriu. Nada disso aparece numa foto, e três dos casos guardados aqui são
|
|
11
|
+
* defeitos que a revisão encontrou e que já estavam escritos no componente:
|
|
12
|
+
*
|
|
13
|
+
* - desmontar COM o painel aberto (`{aberto && <Drawer open …/>}`, o consumo
|
|
14
|
+
* mais comum) deixava o foco no `<body>`;
|
|
15
|
+
* - o trap contava focáveis invisíveis, e `.focus()` neles é no-op — um
|
|
16
|
+
* Accordion fechado dentro da gaveta matava o trap no meio da sessão;
|
|
17
|
+
* - o diálogo podia ficar sem nome acessível.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Monta o Drawer atrás de um gatilho, como o consumo real. */
|
|
21
|
+
function ComGatilho(props: Record<string, any> = {}) {
|
|
22
|
+
const [aberto, setAberto] = useState(false);
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<Button variant="primary" onClick={() => setAberto(true)}>
|
|
26
|
+
Abrir
|
|
27
|
+
</Button>
|
|
28
|
+
<Drawer
|
|
29
|
+
open={aberto}
|
|
30
|
+
onClose={() => setAberto(false)}
|
|
31
|
+
title="Detalhes"
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
<Button variant="secondary">Primeiro</Button>
|
|
35
|
+
<Button variant="secondary">Último</Button>
|
|
36
|
+
</Drawer>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const gatilho = () => screen.getByRole("button", { name: "Abrir" });
|
|
42
|
+
|
|
43
|
+
describe("Drawer · abrir e fechar", () => {
|
|
44
|
+
it("abre pelo gatilho e fecha no Escape, devolvendo o foco", async () => {
|
|
45
|
+
const usuario = userEvent.setup();
|
|
46
|
+
render(<ComGatilho />);
|
|
47
|
+
|
|
48
|
+
await usuario.click(gatilho());
|
|
49
|
+
await waitFor(() =>
|
|
50
|
+
expect(screen.getByRole("dialog", { name: "Detalhes" })).toBeInTheDocument(),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
await usuario.keyboard("{Escape}");
|
|
54
|
+
await waitFor(() =>
|
|
55
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
|
|
56
|
+
);
|
|
57
|
+
expect(gatilho()).toHaveFocus();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("o botão de fechar aciona onClose", async () => {
|
|
61
|
+
const usuario = userEvent.setup();
|
|
62
|
+
const aoFechar = vi.fn();
|
|
63
|
+
render(
|
|
64
|
+
<Drawer open title="Detalhes" onClose={aoFechar}>
|
|
65
|
+
<p>corpo</p>
|
|
66
|
+
</Drawer>,
|
|
67
|
+
);
|
|
68
|
+
await usuario.click(screen.getByRole("button", { name: "Fechar" }));
|
|
69
|
+
expect(aoFechar).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("closeOnEsc={false} não fecha no Escape", async () => {
|
|
73
|
+
const usuario = userEvent.setup();
|
|
74
|
+
const aoFechar = vi.fn();
|
|
75
|
+
render(
|
|
76
|
+
<Drawer open title="Detalhes" closeOnEsc={false} onClose={aoFechar}>
|
|
77
|
+
<p>corpo</p>
|
|
78
|
+
</Drawer>,
|
|
79
|
+
);
|
|
80
|
+
await usuario.keyboard("{Escape}");
|
|
81
|
+
expect(aoFechar).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `closable` ausente precisa valer `true`. No Vue este é o caso que some,
|
|
86
|
+
* porque prop booleana ausente vira `false` e `!== false` nasce falso — por
|
|
87
|
+
* isso as booleanas do componente são tipadas `any`.
|
|
88
|
+
*/
|
|
89
|
+
it("sem a prop, o botão de fechar existe; com closable={false}, não", () => {
|
|
90
|
+
const { rerender } = render(
|
|
91
|
+
<Drawer open title="Detalhes">
|
|
92
|
+
<p>corpo</p>
|
|
93
|
+
</Drawer>,
|
|
94
|
+
);
|
|
95
|
+
expect(screen.getByRole("button", { name: "Fechar" })).toBeInTheDocument();
|
|
96
|
+
|
|
97
|
+
rerender(
|
|
98
|
+
<Drawer open title="Detalhes" closable={false}>
|
|
99
|
+
<p>corpo</p>
|
|
100
|
+
</Drawer>,
|
|
101
|
+
);
|
|
102
|
+
expect(
|
|
103
|
+
screen.queryByRole("button", { name: "Fechar" }),
|
|
104
|
+
).not.toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("Drawer · foco", () => {
|
|
109
|
+
/**
|
|
110
|
+
* `{aberto && <Drawer open …/>}` é o consumo mais comum, e ali o componente
|
|
111
|
+
* inteiro desmonta com o painel aberto: o ramo de fechamento do
|
|
112
|
+
* `syncOpenState` nunca roda, e sem a devolução no `onUnMount` o foco cai no
|
|
113
|
+
* `<body>`.
|
|
114
|
+
*/
|
|
115
|
+
it("desmontar com o painel aberto ainda devolve o foco ao gatilho", async () => {
|
|
116
|
+
const usuario = userEvent.setup();
|
|
117
|
+
|
|
118
|
+
function ComMontagemCondicional() {
|
|
119
|
+
const [aberto, setAberto] = useState(false);
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<Button variant="primary" onClick={() => setAberto(true)}>
|
|
123
|
+
Abrir
|
|
124
|
+
</Button>
|
|
125
|
+
{aberto && (
|
|
126
|
+
<Drawer open title="Detalhes" onClose={() => setAberto(false)}>
|
|
127
|
+
<Button variant="secondary">Dentro</Button>
|
|
128
|
+
</Drawer>
|
|
129
|
+
)}
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
render(<ComMontagemCondicional />);
|
|
135
|
+
await usuario.click(gatilho());
|
|
136
|
+
await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument());
|
|
137
|
+
|
|
138
|
+
await usuario.click(screen.getByRole("button", { name: "Fechar" }));
|
|
139
|
+
await waitFor(() =>
|
|
140
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
|
|
141
|
+
);
|
|
142
|
+
expect(gatilho()).toHaveFocus();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("o foco entra no painel ao abrir", async () => {
|
|
146
|
+
const usuario = userEvent.setup();
|
|
147
|
+
render(<ComGatilho />);
|
|
148
|
+
await usuario.click(gatilho());
|
|
149
|
+
await waitFor(() => expect(screen.getByRole("dialog")).toHaveFocus());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("o Tab circula dentro do painel e não vaza para a página", async () => {
|
|
153
|
+
const usuario = userEvent.setup();
|
|
154
|
+
render(<ComGatilho />);
|
|
155
|
+
await usuario.click(gatilho());
|
|
156
|
+
await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument());
|
|
157
|
+
|
|
158
|
+
const dentro = screen.getAllByRole("button");
|
|
159
|
+
const ultimo = dentro[dentro.length - 1];
|
|
160
|
+
ultimo.focus();
|
|
161
|
+
|
|
162
|
+
await usuario.tab();
|
|
163
|
+
// Circulou: o foco continua dentro do painel, nunca no gatilho de fora.
|
|
164
|
+
expect(screen.getByRole("dialog").contains(document.activeElement)).toBe(
|
|
165
|
+
true,
|
|
166
|
+
);
|
|
167
|
+
expect(gatilho()).not.toHaveFocus();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("Drawer · nome acessível", () => {
|
|
172
|
+
it("com título, o nome é o título", () => {
|
|
173
|
+
render(
|
|
174
|
+
<Drawer open title="Detalhes do processo">
|
|
175
|
+
<p>corpo</p>
|
|
176
|
+
</Drawer>,
|
|
177
|
+
);
|
|
178
|
+
expect(
|
|
179
|
+
screen.getByRole("dialog", { name: "Detalhes do processo" }),
|
|
180
|
+
).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("sem título, o nome é o ariaLabel", () => {
|
|
184
|
+
render(
|
|
185
|
+
<Drawer open ariaLabel="Prévia do documento">
|
|
186
|
+
<p>corpo</p>
|
|
187
|
+
</Drawer>,
|
|
188
|
+
);
|
|
189
|
+
expect(
|
|
190
|
+
screen.getByRole("dialog", { name: "Prévia do documento" }),
|
|
191
|
+
).toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/** Diálogo anônimo é reprovação direta do axe (aria-dialog-name). */
|
|
195
|
+
it("sem título e sem ariaLabel, ainda assim tem nome", () => {
|
|
196
|
+
render(
|
|
197
|
+
<Drawer open closable={false}>
|
|
198
|
+
<p>corpo</p>
|
|
199
|
+
</Drawer>,
|
|
200
|
+
);
|
|
201
|
+
expect(screen.getByRole("dialog")).toHaveAccessibleName();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("Drawer · semântica", () => {
|
|
206
|
+
it("o título é h2, não h6 — não salta nível de cabeçalho", () => {
|
|
207
|
+
render(
|
|
208
|
+
<Drawer open title="Detalhes">
|
|
209
|
+
<p>corpo</p>
|
|
210
|
+
</Drawer>,
|
|
211
|
+
);
|
|
212
|
+
const titulo = screen.getByRole("heading", { name: "Detalhes" });
|
|
213
|
+
expect(titulo.tagName).toBe("H2");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("o lado vira classe na raiz", () => {
|
|
217
|
+
const { container } = render(
|
|
218
|
+
<Drawer open side="left" title="X">
|
|
219
|
+
<p>corpo</p>
|
|
220
|
+
</Drawer>,
|
|
221
|
+
);
|
|
222
|
+
expect(
|
|
223
|
+
container.querySelector(".umds-drawer-root--left"),
|
|
224
|
+
).toBeInTheDocument();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("Drawer · modo contido", () => {
|
|
229
|
+
/**
|
|
230
|
+
* `inline` existe para a página de catálogo: sem ele, cada story vira um
|
|
231
|
+
* overlay `position: fixed` sobre a viewport inteira, e várias na mesma
|
|
232
|
+
* página se empilham — o catálogo do Drawer estava assim.
|
|
233
|
+
*/
|
|
234
|
+
it("inline troca a raiz de overlay por contida", () => {
|
|
235
|
+
const { container, rerender } = render(
|
|
236
|
+
<Drawer open title="X">
|
|
237
|
+
<p>corpo</p>
|
|
238
|
+
</Drawer>,
|
|
239
|
+
);
|
|
240
|
+
expect(container.querySelector(".umds-drawer-root")).toBeInTheDocument();
|
|
241
|
+
expect(
|
|
242
|
+
container.querySelector(".umds-drawer-root--inline"),
|
|
243
|
+
).not.toBeInTheDocument();
|
|
244
|
+
|
|
245
|
+
rerender(
|
|
246
|
+
<Drawer open inline title="X">
|
|
247
|
+
<p>corpo</p>
|
|
248
|
+
</Drawer>,
|
|
249
|
+
);
|
|
250
|
+
expect(
|
|
251
|
+
container.querySelector(".umds-drawer-root--inline"),
|
|
252
|
+
).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("contido ou não, continua sendo um diálogo nomeado", () => {
|
|
256
|
+
render(
|
|
257
|
+
<Drawer open inline title="Detalhes">
|
|
258
|
+
<p>corpo</p>
|
|
259
|
+
</Drawer>,
|
|
260
|
+
);
|
|
261
|
+
expect(
|
|
262
|
+
screen.getByRole("dialog", { name: "Detalhes" }),
|
|
263
|
+
).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeAll } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import Dropdown from "../components/Dropdown/Dropdown";
|
|
5
|
+
import Button from "../components/Button/Button";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* O `@floating-ui/dom` observa o tamanho do gatilho para reposicionar o menu.
|
|
9
|
+
* O jsdom não implementa `ResizeObserver`, e sem este stub o `autoUpdate`
|
|
10
|
+
* estoura no primeiro `open()` — o teste falharia por falta de ambiente, não
|
|
11
|
+
* por defeito do componente.
|
|
12
|
+
*/
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
if (!(globalThis as any).ResizeObserver) {
|
|
15
|
+
(globalThis as any).ResizeObserver = class {
|
|
16
|
+
observe() {}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
disconnect() {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* O que estes testes prendem é o contrato de ABERTURA, que foi exatamente o
|
|
25
|
+
* que quebrou no Vue: o Dropdown sem `open` é NÃO-CONTROLADO, e abrir tem de
|
|
26
|
+
* ser um estado estável — não um menu que aparece e some no update seguinte.
|
|
27
|
+
*
|
|
28
|
+
* O defeito original só se manifesta no Vue (lá a prop booleana ausente chega
|
|
29
|
+
* como `false`, o componente se acha controlado e o `syncOpenState` o fecha no
|
|
30
|
+
* mesmo flush). Em React não dá para reproduzir. O que dá, e é o que importa,
|
|
31
|
+
* é prender os dois lados que a correção mexeu — não-controlado e controlado —
|
|
32
|
+
* para que a próxima mudança em `isOpen`/`syncOpenState` não os troque de
|
|
33
|
+
* novo. Ver `.agent/troubleshooting/vue-boolean-prop-default.md`.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const ITENS = [
|
|
37
|
+
{ key: "editar", label: "Editar" },
|
|
38
|
+
{ key: "duplicar", label: "Duplicar" },
|
|
39
|
+
{ key: "excluir", label: "Excluir", danger: true },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const menu = (extra?: Record<string, any>) => (
|
|
43
|
+
<Dropdown items={ITENS} ariaLabel="Ações do processo" {...extra}>
|
|
44
|
+
<Button variant="secondary">Abrir</Button>
|
|
45
|
+
</Dropdown>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const gatilho = () => screen.getByRole("button", { name: "Abrir" });
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* O menu só trata Esc/setas quando o foco já está DENTRO dele — quem escuta é o
|
|
52
|
+
* `onKeyDown` do `<ul>`. E esse foco só migra depois que o painel é posicionado,
|
|
53
|
+
* dois `requestAnimationFrame` adiante. Mandar tecla assim que o `<ul>` aparece
|
|
54
|
+
* no DOM é corrida: passava no Windows e falhava no runner Linux do CI, onde os
|
|
55
|
+
* frames caem em outro ritmo. Esperar o foco entrar torna o teste determinístico
|
|
56
|
+
* e, de quebra, prende o contrato — menu que abre sem receber foco é menu que
|
|
57
|
+
* não responde ao teclado.
|
|
58
|
+
*/
|
|
59
|
+
const esperarMenuComFoco = async () => {
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
const menuAberto = screen.getByRole("menu");
|
|
62
|
+
expect(menuAberto.contains(document.activeElement)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe("Dropdown · modo não-controlado", () => {
|
|
67
|
+
/**
|
|
68
|
+
* O caso do bug: sem `open`, o clique no gatilho abre o menu e ele FICA
|
|
69
|
+
* aberto. No Vue, `isControlled` nascia verdadeiro e o menu fechava sozinho.
|
|
70
|
+
*/
|
|
71
|
+
it("o clique no gatilho abre o menu, e ele continua aberto", async () => {
|
|
72
|
+
const usuario = userEvent.setup();
|
|
73
|
+
render(menu());
|
|
74
|
+
|
|
75
|
+
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
|
76
|
+
|
|
77
|
+
await usuario.click(gatilho());
|
|
78
|
+
await waitFor(() =>
|
|
79
|
+
expect(
|
|
80
|
+
screen.getByRole("menu", { name: "Ações do processo" }),
|
|
81
|
+
).toBeInTheDocument(),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Um re-render a mais não pode fechar o que o usuário acabou de abrir:
|
|
85
|
+
// era aqui que o `syncOpenState` derrubava o menu no Vue.
|
|
86
|
+
await usuario.hover(gatilho());
|
|
87
|
+
expect(screen.getByRole("menu")).toBeInTheDocument();
|
|
88
|
+
expect(screen.getAllByRole("menuitem")).toHaveLength(3);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("o segundo clique no gatilho fecha", async () => {
|
|
92
|
+
const usuario = userEvent.setup();
|
|
93
|
+
render(menu());
|
|
94
|
+
|
|
95
|
+
await usuario.click(gatilho());
|
|
96
|
+
await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
|
|
97
|
+
|
|
98
|
+
await usuario.click(gatilho());
|
|
99
|
+
await waitFor(() =>
|
|
100
|
+
expect(screen.queryByRole("menu")).not.toBeInTheDocument(),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("o Escape fecha e devolve o foco ao gatilho", async () => {
|
|
105
|
+
const usuario = userEvent.setup();
|
|
106
|
+
render(menu());
|
|
107
|
+
|
|
108
|
+
await usuario.click(gatilho());
|
|
109
|
+
await esperarMenuComFoco();
|
|
110
|
+
|
|
111
|
+
await usuario.keyboard("{Escape}");
|
|
112
|
+
await waitFor(() =>
|
|
113
|
+
expect(screen.queryByRole("menu")).not.toBeInTheDocument(),
|
|
114
|
+
);
|
|
115
|
+
expect(gatilho()).toHaveFocus();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("selecionar um item avisa o pai e fecha o menu", async () => {
|
|
119
|
+
const usuario = userEvent.setup();
|
|
120
|
+
const aoSelecionar = vi.fn();
|
|
121
|
+
render(menu({ onSelect: aoSelecionar }));
|
|
122
|
+
|
|
123
|
+
await usuario.click(gatilho());
|
|
124
|
+
await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
|
|
125
|
+
|
|
126
|
+
await usuario.click(screen.getByRole("menuitem", { name: "Duplicar" }));
|
|
127
|
+
expect(aoSelecionar).toHaveBeenCalledWith(
|
|
128
|
+
expect.objectContaining({ key: "duplicar" }),
|
|
129
|
+
);
|
|
130
|
+
await waitFor(() =>
|
|
131
|
+
expect(screen.queryByRole("menu")).not.toBeInTheDocument(),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("avisa o pai quando abre e quando fecha", async () => {
|
|
136
|
+
const usuario = userEvent.setup();
|
|
137
|
+
const aoMudar = vi.fn();
|
|
138
|
+
render(menu({ onOpenChange: aoMudar }));
|
|
139
|
+
|
|
140
|
+
await usuario.click(gatilho());
|
|
141
|
+
await waitFor(() => expect(aoMudar).toHaveBeenCalledWith(true));
|
|
142
|
+
|
|
143
|
+
// O `onOpenChange(true)` dispara dentro do `open()`, ANTES de o foco migrar
|
|
144
|
+
// para o menu — esperar só por ele deixaria a corrida de pé.
|
|
145
|
+
await esperarMenuComFoco();
|
|
146
|
+
await usuario.keyboard("{Escape}");
|
|
147
|
+
await waitFor(() => expect(aoMudar).toHaveBeenLastCalledWith(false));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("Dropdown · semântica do gatilho", () => {
|
|
152
|
+
/**
|
|
153
|
+
* `aria-haspopup` e `aria-expanded` não são atributos globais: num `<span>`
|
|
154
|
+
* de role genérico o axe reprova, e quem recebe o foco é o botão de dentro —
|
|
155
|
+
* que nunca anunciaria o estado. Antes desta correção o Dropdown com um
|
|
156
|
+
* `Button` no slot não tinha NENHUM dos dois, em lugar nenhum.
|
|
157
|
+
*/
|
|
158
|
+
it("os atributos de estado ficam no botão, não no wrapper", async () => {
|
|
159
|
+
const usuario = userEvent.setup();
|
|
160
|
+
const { container } = render(menu());
|
|
161
|
+
|
|
162
|
+
const wrapper = container.querySelector(".umds-dropdown__trigger")!;
|
|
163
|
+
expect(wrapper.hasAttribute("aria-haspopup")).toBe(false);
|
|
164
|
+
expect(wrapper.hasAttribute("aria-expanded")).toBe(false);
|
|
165
|
+
|
|
166
|
+
await waitFor(() =>
|
|
167
|
+
expect(gatilho()).toHaveAttribute("aria-haspopup", "menu"),
|
|
168
|
+
);
|
|
169
|
+
expect(gatilho()).toHaveAttribute("aria-expanded", "false");
|
|
170
|
+
|
|
171
|
+
await usuario.click(gatilho());
|
|
172
|
+
await waitFor(() =>
|
|
173
|
+
expect(gatilho()).toHaveAttribute("aria-expanded", "true"),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
await esperarMenuComFoco();
|
|
177
|
+
await usuario.keyboard("{Escape}");
|
|
178
|
+
await waitFor(() =>
|
|
179
|
+
expect(gatilho()).toHaveAttribute("aria-expanded", "false"),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("Dropdown · modo controlado", () => {
|
|
185
|
+
it("nasce aberto quando o pai manda", async () => {
|
|
186
|
+
render(menu({ open: true }));
|
|
187
|
+
await waitFor(() =>
|
|
188
|
+
expect(
|
|
189
|
+
screen.getByRole("menu", { name: "Ações do processo" }),
|
|
190
|
+
).toBeInTheDocument(),
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("o painel segue a prop do pai", async () => {
|
|
195
|
+
const { rerender } = render(menu({ open: false }));
|
|
196
|
+
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
|
197
|
+
|
|
198
|
+
rerender(menu({ open: true }));
|
|
199
|
+
await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
|
|
200
|
+
|
|
201
|
+
rerender(menu({ open: false }));
|
|
202
|
+
await waitFor(() =>
|
|
203
|
+
expect(screen.queryByRole("menu")).not.toBeInTheDocument(),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `open` é tipada `any` de propósito, para escapar do cast de booleano do
|
|
209
|
+
* Vue — e o preço disso é que `<Dropdown open>` num template Vue entrega a
|
|
210
|
+
* string vazia, que é falsy. Este teste prende o tratamento do `""`, que
|
|
211
|
+
* roda no gerado dos dois frameworks.
|
|
212
|
+
*/
|
|
213
|
+
it('`open=""` (atributo sem valor no Vue) abre o menu', async () => {
|
|
214
|
+
render(menu({ open: "" }));
|
|
215
|
+
await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import Input from "../components/Input/Input";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* O campo numérico do 1DS. Não virou componente próprio: o produto usa
|
|
7
|
+
* `<Input type="number" min max>` em três telas — paginação, ano da busca e
|
|
8
|
+
* tamanho de arquivo — e em todas elas escondia o botão nativo de girar à mão,
|
|
9
|
+
* no CSS da tela. Um `NumberInput` com +/− seria inventar o que o produto
|
|
10
|
+
* rejeitou; o que faltava eram os limites e a supressão do girador.
|
|
11
|
+
*
|
|
12
|
+
* O que estes testes guardam é justamente isso: que `min`/`max`/`step` chegam
|
|
13
|
+
* ao DOM (é o navegador quem valida, não nós) e que o girador vem escondido
|
|
14
|
+
* por padrão.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
describe("Input · numérico", () => {
|
|
18
|
+
it("min, max e step chegam ao input nativo", () => {
|
|
19
|
+
render(
|
|
20
|
+
<Input label="Página" type="number" min={1} max={99} step={1} />,
|
|
21
|
+
);
|
|
22
|
+
const campo = screen.getByLabelText("Página");
|
|
23
|
+
expect(campo).toHaveAttribute("type", "number");
|
|
24
|
+
expect(campo).toHaveAttribute("min", "1");
|
|
25
|
+
expect(campo).toHaveAttribute("max", "99");
|
|
26
|
+
expect(campo).toHaveAttribute("step", "1");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("sem as props, nenhum limite é imposto", () => {
|
|
30
|
+
render(<Input label="Livre" type="number" />);
|
|
31
|
+
const campo = screen.getByLabelText("Livre");
|
|
32
|
+
expect(campo).not.toHaveAttribute("min");
|
|
33
|
+
expect(campo).not.toHaveAttribute("max");
|
|
34
|
+
expect(campo).not.toHaveAttribute("step");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A supressão do girador é CSS, e CSS não é testável aqui. O que dá para
|
|
39
|
+
* guardar é o gancho que o CSS usa: sem `numberSpinner` a classe NÃO está
|
|
40
|
+
* no wrapper, e é a ausência dela que esconde o botão.
|
|
41
|
+
*/
|
|
42
|
+
it("o girador nativo vem escondido por padrão", () => {
|
|
43
|
+
const { container } = render(<Input label="Página" type="number" />);
|
|
44
|
+
expect(
|
|
45
|
+
container.querySelector(".umds-input--number-spinner"),
|
|
46
|
+
).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("numberSpinner devolve o comportamento cru do browser", () => {
|
|
50
|
+
const { container } = render(
|
|
51
|
+
<Input label="Página" type="number" numberSpinner />,
|
|
52
|
+
);
|
|
53
|
+
expect(
|
|
54
|
+
container.querySelector(".umds-input--number-spinner"),
|
|
55
|
+
).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("os limites não aparecem em campo de texto", () => {
|
|
59
|
+
render(<Input label="Nome" type="text" />);
|
|
60
|
+
const campo = screen.getByLabelText("Nome");
|
|
61
|
+
expect(campo).toHaveAttribute("type", "text");
|
|
62
|
+
expect(campo).not.toHaveAttribute("min");
|
|
63
|
+
});
|
|
64
|
+
});
|