@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/README.md +138 -6
  3. package/package.json +43 -10
  4. package/src/index.ts +161 -16
  5. package/src/teste/AttachmentCard.test.tsx +161 -0
  6. package/src/teste/Chart.test.tsx +233 -0
  7. package/src/teste/Combobox.test.tsx +251 -0
  8. package/src/teste/DataTable.test.tsx +70 -0
  9. package/src/teste/DatePicker.test.tsx +86 -0
  10. package/src/teste/Drawer.test.tsx +265 -0
  11. package/src/teste/Dropdown.test.tsx +217 -0
  12. package/src/teste/Input.test.tsx +64 -0
  13. package/src/teste/Kanban.test.tsx +333 -0
  14. package/src/teste/MultiSelect.test.tsx +73 -0
  15. package/src/teste/Podium.test.tsx +107 -0
  16. package/src/teste/Popover.test.tsx +221 -0
  17. package/src/teste/SegmentedControl.test.tsx +183 -0
  18. package/src/teste/Timeline.test.tsx +100 -0
  19. package/src/teste/Tree.test.tsx +74 -0
  20. package/src/teste/setup.ts +7 -0
  21. package/src/utils/bandeiras.ts +22 -0
  22. package/src/utils/chart.ts +682 -0
  23. package/src/utils/density.ts +27 -0
  24. package/src/utils/overlay-stack.ts +26 -0
  25. package/src/utils/theme.ts +63 -47
  26. package/src/utils/toast.ts +89 -0
  27. package/vitest.config.ts +28 -0
  28. package/dist/components/Alert/Alert.css +0 -188
  29. package/dist/components/Alert/Alert.d.ts +0 -65
  30. package/dist/components/Alert/Alert.js +0 -56
  31. package/dist/components/Avatar/Avatar.css +0 -88
  32. package/dist/components/Avatar/Avatar.d.ts +0 -34
  33. package/dist/components/Avatar/Avatar.js +0 -16
  34. package/dist/components/Avatar/AvatarGroup.d.ts +0 -12
  35. package/dist/components/Avatar/AvatarGroup.js +0 -8
  36. package/dist/components/Badge/Badge.css +0 -155
  37. package/dist/components/Badge/Badge.d.ts +0 -46
  38. package/dist/components/Badge/Badge.js +0 -17
  39. package/dist/components/Button/Button.css +0 -199
  40. package/dist/components/Button/Button.d.ts +0 -23
  41. package/dist/components/Button/Button.js +0 -62
  42. package/dist/components/Checkbox/Checkbox.css +0 -122
  43. package/dist/components/Checkbox/Checkbox.d.ts +0 -21
  44. package/dist/components/Checkbox/Checkbox.js +0 -49
  45. package/dist/components/Divider/Divider.css +0 -81
  46. package/dist/components/Divider/Divider.d.ts +0 -18
  47. package/dist/components/Divider/Divider.js +0 -26
  48. package/dist/components/Grid/Grid.css +0 -154
  49. package/dist/components/Grid/Grid.d.ts +0 -21
  50. package/dist/components/Grid/Grid.js +0 -53
  51. package/dist/components/Icon/Icon.css +0 -41
  52. package/dist/components/Icon/Icon.d.ts +0 -12
  53. package/dist/components/Icon/Icon.js +0 -29
  54. package/dist/components/Input/Input.css +0 -190
  55. package/dist/components/Input/Input.d.ts +0 -32
  56. package/dist/components/Input/Input.js +0 -58
  57. package/dist/components/Link/Link.css +0 -76
  58. package/dist/components/Link/Link.d.ts +0 -15
  59. package/dist/components/Link/Link.js +0 -19
  60. package/dist/components/Logo/Logo.css +0 -32
  61. package/dist/components/Logo/Logo.d.ts +0 -10
  62. package/dist/components/Logo/Logo.js +0 -16
  63. package/dist/components/Modal/Modal.css +0 -155
  64. package/dist/components/Modal/Modal.d.ts +0 -36
  65. package/dist/components/Modal/Modal.js +0 -55
  66. package/dist/components/Radio/Radio.css +0 -114
  67. package/dist/components/Radio/Radio.d.ts +0 -19
  68. package/dist/components/Radio/Radio.js +0 -39
  69. package/dist/components/Select/Select.css +0 -291
  70. package/dist/components/Select/Select.d.ts +0 -38
  71. package/dist/components/Select/Select.js +0 -246
  72. package/dist/components/Spinner/Spinner.css +0 -60
  73. package/dist/components/Spinner/Spinner.d.ts +0 -16
  74. package/dist/components/Spinner/Spinner.js +0 -16
  75. package/dist/components/Switch/Switch.css +0 -127
  76. package/dist/components/Switch/Switch.d.ts +0 -18
  77. package/dist/components/Switch/Switch.js +0 -57
  78. package/dist/components/Tabs/Tab.d.ts +0 -19
  79. package/dist/components/Tabs/Tab.js +0 -76
  80. package/dist/components/Tabs/TabContent.d.ts +0 -15
  81. package/dist/components/Tabs/TabContent.js +0 -11
  82. package/dist/components/Tabs/Tabs.css +0 -124
  83. package/dist/components/Tabs/Tabs.d.ts +0 -20
  84. package/dist/components/Tabs/Tabs.js +0 -14
  85. package/dist/components/Tabs/TabsContent.d.ts +0 -16
  86. package/dist/components/Tabs/TabsContent.js +0 -12
  87. package/dist/components/Tabs/tabs.context.d.ts +0 -2
  88. package/dist/components/Tabs/tabs.context.js +0 -2
  89. package/dist/components/Tag/Tag.css +0 -140
  90. package/dist/components/Tag/Tag.d.ts +0 -63
  91. package/dist/components/Tag/Tag.js +0 -48
  92. package/dist/components/Text/Text.css +0 -1213
  93. package/dist/components/Text/Text.d.ts +0 -70
  94. package/dist/components/Text/Text.js +0 -89
  95. package/dist/components/Textarea/Textarea.css +0 -133
  96. package/dist/components/Textarea/Textarea.d.ts +0 -28
  97. package/dist/components/Textarea/Textarea.js +0 -72
  98. package/dist/index.d.ts +0 -25
  99. package/dist/index.js +0 -27
  100. package/dist/utils/theme.d.ts +0 -30
  101. 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
+ });