@1doc/1ds-react 2.9.0 → 2.10.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +434 -0
  2. package/dist/components/Accordion/Accordion.css +13 -0
  3. package/dist/components/Accordion/Accordion.d.ts +3 -0
  4. package/dist/components/Accordion/Accordion.js +1 -0
  5. package/dist/components/AppSubheader/AppSubheader.css +10 -1
  6. package/dist/components/AttachmentCard/AttachmentCard.css +10 -8
  7. package/dist/components/AttachmentCard/AttachmentCard.d.ts +3 -0
  8. package/dist/components/AttachmentCard/AttachmentCard.js +8 -1
  9. package/dist/components/Badge/Badge.css +14 -1
  10. package/dist/components/Badge/Badge.d.ts +11 -2
  11. package/dist/components/Badge/Badge.js +3 -0
  12. package/dist/components/Calendar/Calendar.d.ts +7 -6
  13. package/dist/components/Coachmark/Coachmark.css +10 -0
  14. package/dist/components/Coachmark/Coachmark.d.ts +12 -0
  15. package/dist/components/Coachmark/Coachmark.js +46 -5
  16. package/dist/components/ColorPicker/ColorPicker.css +8 -1
  17. package/dist/components/ColorPicker/ColorPicker.d.ts +2 -2
  18. package/dist/components/DataTable/DataTable.css +174 -0
  19. package/dist/components/DataTable/DataTable.d.ts +63 -2
  20. package/dist/components/DataTable/DataTable.js +278 -6
  21. package/dist/components/DescriptionList/DescriptionList.css +14 -0
  22. package/dist/components/DescriptionList/DescriptionList.d.ts +3 -0
  23. package/dist/components/DescriptionList/DescriptionList.js +1 -0
  24. package/dist/components/EmptyState/EmptyState.d.ts +4 -2
  25. package/dist/components/EmptyState/EmptyState.js +2 -2
  26. package/dist/components/Input/Input.css +21 -0
  27. package/dist/components/Input/Input.d.ts +7 -0
  28. package/dist/components/Input/Input.js +2 -1
  29. package/dist/components/List/ListItem.d.ts +14 -2
  30. package/dist/components/List/ListItem.js +3 -2
  31. package/dist/components/RichTextEditor/RichTextEditor.css +47 -0
  32. package/dist/components/RichTextEditor/RichTextEditor.d.ts +22 -8
  33. package/dist/components/RichTextEditor/RichTextEditor.js +460 -3
  34. package/dist/components/SegmentedControl/SegmentedControl.css +24 -0
  35. package/dist/components/SegmentedControl/SegmentedControl.d.ts +2 -0
  36. package/dist/components/SegmentedControl/SegmentedControl.js +4 -1
  37. package/dist/components/Select/Select.css +94 -0
  38. package/dist/components/Select/Select.d.ts +26 -4
  39. package/dist/components/Select/Select.js +210 -35
  40. package/dist/components/StatCard/StatCard.css +65 -0
  41. package/dist/components/StatCard/StatCard.d.ts +9 -0
  42. package/dist/components/StatCard/StatCard.js +10 -1
  43. package/dist/components/Toast/Toast.css +32 -0
  44. package/dist/components/Toast/Toast.d.ts +18 -0
  45. package/dist/components/Toast/Toast.js +14 -1
  46. package/dist/components/Toast/ToastStack.js +8 -1
  47. package/dist/teste/AccordionTituloNeutro.test.d.ts +1 -0
  48. package/dist/teste/AccordionTituloNeutro.test.js +23 -0
  49. package/dist/teste/AppSubheaderRegiao.test.d.ts +1 -0
  50. package/dist/teste/AppSubheaderRegiao.test.js +13 -0
  51. package/dist/teste/AttachmentCardLista.test.d.ts +1 -0
  52. package/dist/teste/AttachmentCardLista.test.js +49 -0
  53. package/dist/teste/AttachmentCardSm.test.d.ts +1 -0
  54. package/dist/teste/AttachmentCardSm.test.js +26 -0
  55. package/dist/teste/BadgeNeutroColado.test.d.ts +1 -0
  56. package/dist/teste/BadgeNeutroColado.test.js +11 -0
  57. package/dist/teste/BadgeTag.test.js +13 -0
  58. package/dist/teste/CalendarCabecalho.test.d.ts +1 -0
  59. package/dist/teste/CalendarCabecalho.test.js +11 -0
  60. package/dist/teste/CoachmarkSemVeu.test.d.ts +1 -0
  61. package/dist/teste/CoachmarkSemVeu.test.js +85 -0
  62. package/dist/teste/ColorPickerXs.test.d.ts +1 -0
  63. package/dist/teste/ColorPickerXs.test.js +21 -0
  64. package/dist/teste/DataTableArvoreEVariantes.test.d.ts +1 -0
  65. package/dist/teste/DataTableArvoreEVariantes.test.js +190 -0
  66. package/dist/teste/DescriptionListCompacta.test.d.ts +1 -0
  67. package/dist/teste/DescriptionListCompacta.test.js +22 -0
  68. package/dist/teste/EmptyStateTituloRico.test.d.ts +1 -0
  69. package/dist/teste/EmptyStateTituloRico.test.js +23 -0
  70. package/dist/teste/InputAgrupado.test.d.ts +1 -0
  71. package/dist/teste/InputAgrupado.test.js +24 -0
  72. package/dist/teste/ListItemTooltipStandalone.test.d.ts +1 -0
  73. package/dist/teste/ListItemTooltipStandalone.test.js +39 -0
  74. package/dist/teste/RichTextEditorAlinhaImagemTabela.test.d.ts +1 -0
  75. package/dist/teste/RichTextEditorAlinhaImagemTabela.test.js +212 -0
  76. package/dist/teste/RichTextEditorEstiloDeTexto.test.d.ts +1 -0
  77. package/dist/teste/RichTextEditorEstiloDeTexto.test.js +120 -0
  78. package/dist/teste/SegmentedControlContador.test.d.ts +1 -0
  79. package/dist/teste/SegmentedControlContador.test.js +39 -0
  80. package/dist/teste/SelectAbertoControladoEFlutuante.test.d.ts +1 -0
  81. package/dist/teste/SelectAbertoControladoEFlutuante.test.js +145 -0
  82. package/dist/teste/SelectFiltroCorETamanho.test.d.ts +1 -0
  83. package/dist/teste/SelectFiltroCorETamanho.test.js +77 -0
  84. package/dist/teste/StatCardSecundariaETom.test.d.ts +1 -0
  85. package/dist/teste/StatCardSecundariaETom.test.js +45 -0
  86. package/dist/teste/ToastAcaoEDetalhes.test.d.ts +1 -0
  87. package/dist/teste/ToastAcaoEDetalhes.test.js +94 -0
  88. package/dist/utils/html-seguro.d.ts +19 -1
  89. package/dist/utils/html-seguro.js +113 -2
  90. package/dist/utils/toast.d.ts +10 -1
  91. package/dist/utils/toast.js +2 -1
  92. package/package.json +2 -2
  93. package/src/teste/AccordionTituloNeutro.test.tsx +25 -0
  94. package/src/teste/AppSubheaderRegiao.test.tsx +17 -0
  95. package/src/teste/AttachmentCardLista.test.tsx +53 -0
  96. package/src/teste/AttachmentCardSm.test.tsx +29 -0
  97. package/src/teste/BadgeNeutroColado.test.tsx +15 -0
  98. package/src/teste/BadgeTag.test.tsx +21 -0
  99. package/src/teste/CalendarCabecalho.test.tsx +11 -0
  100. package/src/teste/CoachmarkSemVeu.test.tsx +101 -0
  101. package/src/teste/ColorPickerXs.test.tsx +22 -0
  102. package/src/teste/DataTableArvoreEVariantes.test.tsx +240 -0
  103. package/src/teste/DescriptionListCompacta.test.tsx +24 -0
  104. package/src/teste/EmptyStateTituloRico.test.tsx +34 -0
  105. package/src/teste/InputAgrupado.test.tsx +26 -0
  106. package/src/teste/ListItemTooltipStandalone.test.tsx +64 -0
  107. package/src/teste/RichTextEditorAlinhaImagemTabela.test.tsx +238 -0
  108. package/src/teste/RichTextEditorEstiloDeTexto.test.tsx +137 -0
  109. package/src/teste/SegmentedControlContador.test.tsx +43 -0
  110. package/src/teste/SelectAbertoControladoEFlutuante.test.tsx +180 -0
  111. package/src/teste/SelectFiltroCorETamanho.test.tsx +90 -0
  112. package/src/teste/StatCardSecundariaETom.test.tsx +51 -0
  113. package/src/teste/ToastAcaoEDetalhes.test.tsx +109 -0
  114. package/src/utils/html-seguro.ts +105 -2
  115. package/src/utils/toast.ts +12 -2
@@ -0,0 +1,137 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import RichTextEditor from "../components/RichTextEditor/RichTextEditor";
5
+ import { estiloDeTexto, sanitizarHtml } from "../utils/html-seguro";
6
+
7
+ let execCommand: ReturnType<typeof vi.fn>;
8
+
9
+ beforeEach(() => {
10
+ execCommand = vi.fn(() => true);
11
+ (document as any).execCommand = execCommand;
12
+ (document as any).queryCommandState = () => false;
13
+ });
14
+
15
+ const area = () => screen.getByRole("textbox") as HTMLElement;
16
+ const selecionarTexto = () => {
17
+ const alvo = area().querySelector("p")!.firstChild as Text;
18
+ const faixa = document.createRange();
19
+ faixa.setStart(alvo, 0);
20
+ faixa.setEnd(alvo, alvo.length);
21
+ window.getSelection()!.removeAllRanges();
22
+ window.getSelection()!.addRange(faixa);
23
+ };
24
+ const abrir = (toolbar: string[], props: any = {}) =>
25
+ render(<RichTextEditor ariaLabel="x" toolbar={toolbar as any} defaultValue="<p>Parecer</p>" {...props} />);
26
+
27
+ describe("html-seguro: estilo de texto por token", () => {
28
+ it("estiloDeTexto só aceita token do DS", () => {
29
+ expect(estiloDeTexto("color", "var(--umds-primitive-red-pure)")).toBe("color: var(--umds-primitive-red-pure);");
30
+ expect(estiloDeTexto("color", "var(--umds-semantic-color-feedback-negative-pure)")).toBe("color: var(--umds-semantic-color-feedback-negative-pure);");
31
+ expect(estiloDeTexto("font-size", "var(--umds-primitive-font-size-xs)")).toBe("font-size: var(--umds-primitive-font-size-xs);");
32
+ expect(estiloDeTexto("font-family", "var(--umds-semantic-font-family-mono)")).toBe("font-family: var(--umds-semantic-font-family-mono);");
33
+ for (const mau of ["red", "#ff0000", "rgb(255,0,0)", "var(--outra-coisa)", "var(--umds-primitive-red-pure); background: red", "expression(alert(1))", "url(x)"]) {
34
+ expect(estiloDeTexto("color", mau)).toBe("");
35
+ }
36
+ expect(estiloDeTexto("font-size", "12px")).toBe("");
37
+ expect(estiloDeTexto("font-size", "var(--umds-primitive-red-pure)")).toBe("");
38
+ expect(estiloDeTexto("font-family", "Comic Sans MS")).toBe("");
39
+ expect(estiloDeTexto("background", "var(--umds-primitive-red-pure)")).toBe("");
40
+ });
41
+
42
+ it("o span guarda só as declarações válidas; o resto do style sai", () => {
43
+ expect(sanitizarHtml('<span style="color: var(--umds-primitive-red-pure); background: red; font-size: 40px">a</span>')).toBe(
44
+ '<span style="color: var(--umds-primitive-red-pure);">a</span>',
45
+ );
46
+ expect(sanitizarHtml('<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm)">a</span>')).toBe(
47
+ '<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm);">a</span>',
48
+ );
49
+ expect(sanitizarHtml('<span style="color: red">a</span>')).toBe("<span>a</span>");
50
+ });
51
+
52
+ it("estilo só vale em span: em outras tags sai", () => {
53
+ expect(sanitizarHtml('<b style="color: var(--umds-primitive-red-pure)">a</b>')).toBe("<b>a</b>");
54
+ expect(sanitizarHtml('<p style="color: var(--umds-primitive-red-pure)">a</p>')).toBe("<p>a</p>");
55
+ });
56
+ });
57
+
58
+ describe("RichTextEditor: fonte, tamanho e cor do texto", () => {
59
+ it("a barra padrão não ganha as ferramentas novas", () => {
60
+ render(<RichTextEditor ariaLabel="x" />);
61
+ for (const nome of ["Fonte", "Tamanho da fonte", "Cor do texto"]) expect(screen.queryByRole("button", { name: nome })).toBeNull();
62
+ });
63
+
64
+ it("cada ferramenta abre um painel nomeado abaixo da barra; Fechar e Esc fecham", async () => {
65
+ const usuario = userEvent.setup();
66
+ abrir(["fontFamily", "fontSize", "textColor"]);
67
+ await usuario.click(screen.getByRole("button", { name: "Tamanho da fonte" }));
68
+ expect(screen.getByRole("group", { name: "Tamanho da fonte" })).toBeInTheDocument();
69
+ await usuario.click(screen.getByRole("button", { name: "Fechar" }));
70
+ expect(screen.queryByRole("group", { name: "Tamanho da fonte" })).toBeNull();
71
+ await usuario.click(screen.getByRole("button", { name: "Cor do texto" }));
72
+ expect(screen.getByRole("group", { name: "Cor do texto" })).toBeInTheDocument();
73
+ fireEvent.keyDown(screen.getByRole("group", { name: "Cor do texto" }), { key: "Escape" });
74
+ expect(screen.queryByRole("group", { name: "Cor do texto" })).toBeNull();
75
+ });
76
+
77
+ it("a fonte monoespaçada embrulha o texto selecionado num span com o token", async () => {
78
+ const usuario = userEvent.setup();
79
+ const aoMudar = vi.fn();
80
+ abrir(["fontFamily"], { onChange: aoMudar });
81
+ selecionarTexto();
82
+ await usuario.click(screen.getByRole("button", { name: "Fonte" }));
83
+ await usuario.click(screen.getByRole("combobox", { name: "Fonte" }));
84
+ await usuario.click(screen.getByRole("option", { name: "Monoespaçada" }));
85
+ expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-family: var(--umds-semantic-font-family-mono);">Parecer</span>');
86
+ expect(aoMudar).toHaveBeenCalled();
87
+ expect(screen.queryByRole("group", { name: "Fonte" })).toBeNull();
88
+ });
89
+
90
+ it("o tamanho usa os degraus da escala do DS", async () => {
91
+ const usuario = userEvent.setup();
92
+ abrir(["fontSize"]);
93
+ selecionarTexto();
94
+ await usuario.click(screen.getByRole("button", { name: "Tamanho da fonte" }));
95
+ await usuario.click(screen.getByRole("combobox", { name: "Tamanho da fonte" }));
96
+ const opcoes = screen.getAllByRole("option").map((o) => o.textContent);
97
+ expect(opcoes).toEqual(["12", "14", "16", "20", "24"]);
98
+ await usuario.click(screen.getByRole("option", { name: "20" }));
99
+ expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-size: var(--umds-primitive-font-size-sm);">Parecer</span>');
100
+ });
101
+
102
+ it("a cor vem da paleta do DS e vira var() do token", async () => {
103
+ const usuario = userEvent.setup();
104
+ abrir(["textColor"]);
105
+ selecionarTexto();
106
+ await usuario.click(screen.getByRole("button", { name: "Cor do texto" }));
107
+ await usuario.click(screen.getByRole("radio", { name: "Vermelho" }));
108
+ expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="color: var(--umds-primitive-red-pure);">Parecer</span>');
109
+ });
110
+
111
+ it("sem texto selecionado o painel pede a seleção e não aplica nada", async () => {
112
+ const usuario = userEvent.setup();
113
+ abrir(["textColor"]);
114
+ window.getSelection()!.removeAllRanges();
115
+ await usuario.click(screen.getByRole("button", { name: "Cor do texto" }));
116
+ await usuario.click(screen.getByRole("radio", { name: "Vermelho" }));
117
+ expect(screen.getByText("Selecione o texto antes de aplicar.")).toBeInTheDocument();
118
+ expect(execCommand).not.toHaveBeenCalledWith("insertHTML", expect.anything(), expect.anything());
119
+ expect(screen.getByRole("group", { name: "Cor do texto" })).toBeInTheDocument();
120
+ });
121
+
122
+ it("os textos vêm por labels", async () => {
123
+ const usuario = userEvent.setup();
124
+ abrir(["fontFamily", "textColor"], { labels: { fontFamily: "Font", textColor: "Text color", fontFamilyMono: "Monospaced", styleClose: "Close" } });
125
+ selecionarTexto();
126
+ await usuario.click(screen.getByRole("button", { name: "Font" }));
127
+ await usuario.click(screen.getByRole("combobox", { name: "Font" }));
128
+ expect(screen.getByRole("option", { name: "Monospaced" })).toBeInTheDocument();
129
+ await usuario.keyboard("{Escape}");
130
+ expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
131
+ });
132
+
133
+ it("o valor volta higienizado com o estilo por token", () => {
134
+ abrir(["textColor"], { defaultValue: '<p><span style="color: var(--umds-primitive-blue-pure); background: red">a</span></p>' });
135
+ expect(area().innerHTML).toBe('<p><span style="color: var(--umds-primitive-blue-pure);">a</span></p>');
136
+ });
137
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import SegmentedControl from "../components/SegmentedControl/SegmentedControl";
5
+
6
+ const FILA = [
7
+ { label: "Todas", value: "todas", count: 31 },
8
+ { label: "Minhas", value: "minhas", count: 0 },
9
+ { label: "Arquivadas", value: "arquivadas" },
10
+ { label: "Vazias", value: "vazias", count: "" },
11
+ ];
12
+
13
+ describe("SegmentedControl · count", () => {
14
+ it("desenha a pílula do contador depois do texto; 0 aparece, vazio e ausente não", () => {
15
+ const { container } = render(<SegmentedControl label="Fila" options={FILA} />);
16
+ const itens = [...container.querySelectorAll(".umds-segmented__item")];
17
+ expect(itens[0].querySelector(".umds-segmented__count")).toHaveTextContent("31");
18
+ expect(itens[1].querySelector(".umds-segmented__count")).toHaveTextContent("0");
19
+ expect(itens[2].querySelector(".umds-segmented__count")).toBeNull();
20
+ expect(itens[3].querySelector(".umds-segmented__count")).toBeNull();
21
+ const conteudo = itens[0].querySelector(".umds-segmented__conteudo") as HTMLElement;
22
+ expect(conteudo.lastElementChild).toHaveClass("umds-segmented__count");
23
+ });
24
+
25
+ it("o contador entra no nome acessível do segmento (o texto visível manda)", () => {
26
+ render(<SegmentedControl label="Fila" id="fila" options={FILA} />);
27
+ expect(screen.getByRole("radio", { name: /Todas\s*31/ })).toBeInTheDocument();
28
+ });
29
+
30
+ it("o contador acompanha a escolha e a escolha segue funcionando", async () => {
31
+ const usuario = userEvent.setup();
32
+ const { container } = render(<SegmentedControl label="Fila" options={FILA} defaultValue="todas" />);
33
+ expect(screen.getByRole("radio", { name: /Todas/ })).toBeChecked();
34
+ await usuario.click(screen.getByText("Minhas"));
35
+ expect(screen.getByRole("radio", { name: /Minhas/ })).toBeChecked();
36
+ expect(container.querySelectorAll(".umds-segmented__count")).toHaveLength(2);
37
+ });
38
+
39
+ it("sem count em nenhum segmento, nada muda", () => {
40
+ const { container } = render(<SegmentedControl label="x" options={[{ label: "25", value: "25" }]} />);
41
+ expect(container.querySelector(".umds-segmented__count")).toBeNull();
42
+ });
43
+ });
@@ -0,0 +1,180 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import { faFilter } from "@fortawesome/free-solid-svg-icons";
4
+ import Select from "../components/Select/Select";
5
+
6
+ const OPCOES = [
7
+ { label: "Urgente", value: "urgente" },
8
+ { label: "Revisão", value: "revisao" },
9
+ ];
10
+
11
+ afterEach(() => vi.restoreAllMocks());
12
+
13
+ describe("Select · open e onOpenChange", () => {
14
+ it("open controlado mostra a lista sem clique; fechado, não mostra", () => {
15
+ const { rerender } = render(<Select ariaLabel="x" options={OPCOES} open={false} />);
16
+ expect(screen.queryByRole("listbox")).toBeNull();
17
+ rerender(<Select ariaLabel="x" options={OPCOES} open />);
18
+ expect(screen.getByRole("listbox")).toBeInTheDocument();
19
+ expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "true");
20
+ });
21
+
22
+ it("controlado: o clique só avisa; quem abre é o pai", () => {
23
+ const aoAbrir = vi.fn();
24
+ const { rerender } = render(<Select ariaLabel="x" options={OPCOES} open={false} onOpenChange={aoAbrir} />);
25
+ fireEvent.click(screen.getByRole("combobox"));
26
+ expect(aoAbrir).toHaveBeenCalledWith(true);
27
+ expect(screen.queryByRole("listbox")).toBeNull();
28
+ rerender(<Select ariaLabel="x" options={OPCOES} open onOpenChange={aoAbrir} />);
29
+ expect(screen.getByRole("listbox")).toBeInTheDocument();
30
+ });
31
+
32
+ it("controlado aberto: escolher uma opção, Esc e clique fora pedem para fechar", () => {
33
+ const aoAbrir = vi.fn();
34
+ const { rerender } = render(<Select ariaLabel="x" options={OPCOES} open onOpenChange={aoAbrir} />);
35
+ fireEvent.click(screen.getAllByRole("option")[0]);
36
+ expect(aoAbrir).toHaveBeenLastCalledWith(false);
37
+
38
+ aoAbrir.mockClear();
39
+ rerender(<Select ariaLabel="x" options={OPCOES} open onOpenChange={aoAbrir} />);
40
+ fireEvent.keyDown(screen.getByRole("combobox"), { key: "Escape" });
41
+ expect(aoAbrir).toHaveBeenLastCalledWith(false);
42
+
43
+ aoAbrir.mockClear();
44
+ fireEvent.mouseDown(document.body);
45
+ expect(aoAbrir).toHaveBeenCalledWith(false);
46
+ });
47
+
48
+ it("o clique fora não avisa quando a lista está fechada", () => {
49
+ const aoAbrir = vi.fn();
50
+ render(<Select ariaLabel="x" options={OPCOES} open={false} onOpenChange={aoAbrir} />);
51
+ fireEvent.mouseDown(document.body);
52
+ expect(aoAbrir).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("clique fora vê o open de agora, não o do primeiro render", () => {
56
+ const aoAbrir = vi.fn();
57
+ const { rerender } = render(<Select ariaLabel="x" options={OPCOES} open={false} onOpenChange={aoAbrir} />);
58
+ rerender(<Select ariaLabel="x" options={OPCOES} open onOpenChange={aoAbrir} />);
59
+ fireEvent.mouseDown(document.body);
60
+ expect(aoAbrir).toHaveBeenCalledWith(false);
61
+ });
62
+
63
+ it("sem open, o Select abre e fecha sozinho e também avisa", () => {
64
+ const aoAbrir = vi.fn();
65
+ render(<Select ariaLabel="x" options={OPCOES} onOpenChange={aoAbrir} />);
66
+ fireEvent.click(screen.getByRole("combobox"));
67
+ expect(screen.getByRole("listbox")).toBeInTheDocument();
68
+ expect(aoAbrir).toHaveBeenLastCalledWith(true);
69
+ fireEvent.click(screen.getAllByRole("option")[1]);
70
+ expect(screen.queryByRole("listbox")).toBeNull();
71
+ expect(aoAbrir).toHaveBeenLastCalledWith(false);
72
+ });
73
+ });
74
+
75
+ describe("Select · gatilho com ícone, amostra e aria-required", () => {
76
+ it("triggerIcon e triggerSwatch aparecem no gatilho, decorativos", () => {
77
+ const { container } = render(
78
+ <Select ariaLabel="x" options={OPCOES} triggerIcon={faFilter} triggerSwatch="var(--umds-semantic-color-primary-pure)" />,
79
+ );
80
+ const icone = container.querySelector(".umds-select__trigger-icon") as HTMLElement;
81
+ expect(icone).toHaveAttribute("aria-hidden", "true");
82
+ expect(icone.querySelector("svg")).toHaveAttribute("data-icon", "filter");
83
+ const amostra = container.querySelector(".umds-select__swatch") as HTMLElement;
84
+ expect(amostra).toHaveAttribute("aria-hidden", "true");
85
+ expect(amostra.style.background).toContain("--umds-semantic-color-primary-pure");
86
+ expect(container.querySelector(".umds-select--has-trigger-icon")).not.toBeNull();
87
+ });
88
+
89
+ it("sem as props, não há ícone nem amostra", () => {
90
+ const { container } = render(<Select ariaLabel="x" options={OPCOES} />);
91
+ expect(container.querySelector(".umds-select__trigger-icon")).toBeNull();
92
+ expect(container.querySelector(".umds-select__swatch")).toBeNull();
93
+ });
94
+
95
+ it("required repassa aria-required ao combobox", () => {
96
+ const { rerender } = render(<Select ariaLabel="x" options={OPCOES} required />);
97
+ expect(screen.getByRole("combobox")).toHaveAttribute("aria-required", "true");
98
+ rerender(<Select ariaLabel="x" options={OPCOES} />);
99
+ expect(screen.getByRole("combobox")).not.toHaveAttribute("aria-required");
100
+ });
101
+ });
102
+
103
+ describe("Select · variantes de filtro", () => {
104
+ it("search e combo escrevem a classe do filtro e a própria", () => {
105
+ const { container, rerender } = render(<Select ariaLabel="x" options={OPCOES} variant="search" />);
106
+ expect(container.querySelector(".umds-select--filter.umds-select--filter-search")).not.toBeNull();
107
+ rerender(<Select ariaLabel="x" options={OPCOES} variant="combo" />);
108
+ expect(container.querySelector(".umds-select--filter.umds-select--filter-combo")).not.toBeNull();
109
+ expect(container.querySelector(".umds-select--filter-search")).toBeNull();
110
+ });
111
+
112
+ it("search foca a busca ao abrir, como o filter", async () => {
113
+ render(<Select ariaLabel="x" options={OPCOES} variant="search" searchable />);
114
+ fireEvent.click(screen.getByRole("combobox"));
115
+ await waitFor(() => expect(document.activeElement).toBe(screen.getByRole("textbox")));
116
+ });
117
+ });
118
+
119
+ describe("Select · por cima do diálogo", () => {
120
+ it("dentro de role=dialog a lista vira fixed, ancorada no gatilho", async () => {
121
+ const { container } = render(
122
+ <div role="dialog" aria-label="Filtros" style={{ overflow: "clip" }}>
123
+ <Select ariaLabel="x" options={OPCOES} />
124
+ </div>,
125
+ );
126
+ fireEvent.click(screen.getByRole("combobox"));
127
+ const lista = container.querySelector(".umds-select__dropdown") as HTMLElement;
128
+ expect(container.querySelector(".umds-select--floating")).not.toBeNull();
129
+ expect(lista.style.position).toBe("fixed");
130
+ await waitFor(() => expect(lista.style.visibility).toBe("visible"));
131
+ });
132
+
133
+ it("fora de diálogo a lista continua posicionada pelo CSS (sem fixed)", () => {
134
+ const { container } = render(<Select ariaLabel="x" options={OPCOES} />);
135
+ fireEvent.click(screen.getByRole("combobox"));
136
+ const lista = container.querySelector(".umds-select__dropdown") as HTMLElement;
137
+ expect(container.querySelector(".umds-select--floating")).toBeNull();
138
+ expect(lista.style.position).toBe("");
139
+ });
140
+
141
+ it("aria-modal também conta como diálogo", () => {
142
+ const { container } = render(
143
+ <div aria-modal="true">
144
+ <Select ariaLabel="x" options={OPCOES} />
145
+ </div>,
146
+ );
147
+ fireEvent.click(screen.getByRole("combobox"));
148
+ expect(container.querySelector(".umds-select--floating")).not.toBeNull();
149
+ });
150
+ });
151
+
152
+ describe("Select · placement auto mede o ancestral que rola", () => {
153
+ it("abre para cima e limita a altura da lista ao espaço do ancestral", () => {
154
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (this: HTMLElement) {
155
+ const rolante = this.getAttribute("data-rolante") !== null;
156
+ const [topo, base] = rolante ? [100, 300] : [200, 244];
157
+ return { top: topo, bottom: base, left: 0, right: 0, width: 0, height: base - topo, x: 0, y: topo, toJSON() {} } as any;
158
+ });
159
+ Object.defineProperty(window, "innerHeight", { value: 900, configurable: true });
160
+ const { container } = render(
161
+ <div data-rolante style={{ overflowY: "auto" }}>
162
+ <Select ariaLabel="x" options={OPCOES} placement="auto" />
163
+ </div>,
164
+ );
165
+ fireEvent.click(screen.getByRole("combobox"));
166
+ expect(container.querySelector(".umds-select--placement-top")).not.toBeNull();
167
+ expect((container.querySelector(".umds-select__dropdown") as HTMLElement).style.maxHeight).toBe("96px");
168
+ });
169
+
170
+ it("sem ancestral que corta, só a janela conta e a lista não ganha teto", () => {
171
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
172
+ () => ({ top: 20, bottom: 64, left: 0, right: 0, width: 0, height: 44, x: 0, y: 20, toJSON() {} }) as any,
173
+ );
174
+ Object.defineProperty(window, "innerHeight", { value: 900, configurable: true });
175
+ const { container } = render(<Select ariaLabel="x" options={OPCOES} placement="auto" />);
176
+ fireEvent.click(screen.getByRole("combobox"));
177
+ expect(container.querySelector(".umds-select--placement-top")).toBeNull();
178
+ expect((container.querySelector(".umds-select__dropdown") as HTMLElement).style.maxHeight).toBe("");
179
+ });
180
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import Select from "../components/Select/Select";
4
+
5
+ const MARCADORES = [
6
+ { label: "Urgente", value: "urgente", color: "var(--umds-semantic-color-feedback-negative-pure)" },
7
+ { label: "Revisão", value: "revisao", color: "var(--umds-semantic-color-primary-pure)" },
8
+ { label: "Sem marcador", value: "nenhum" },
9
+ ];
10
+
11
+ describe("Select filtro · tamanho e fundo", () => {
12
+ it("as variantes de filtro nascem no tamanho sm; size explícito manda; o padrão segue md", () => {
13
+ const { container, rerender } = render(<Select ariaLabel="x" options={MARCADORES} variant="filter" />);
14
+ const raiz = () => container.firstElementChild as HTMLElement;
15
+ expect(raiz()).toHaveClass("umds-select--sm");
16
+ rerender(<Select ariaLabel="x" options={MARCADORES} variant="search" />);
17
+ expect(raiz()).toHaveClass("umds-select--sm");
18
+ rerender(<Select ariaLabel="x" options={MARCADORES} variant="combo" />);
19
+ expect(raiz()).toHaveClass("umds-select--sm");
20
+ rerender(<Select ariaLabel="x" options={MARCADORES} variant="filter" size="md" />);
21
+ expect(raiz()).toHaveClass("umds-select--md");
22
+ rerender(<Select ariaLabel="x" options={MARCADORES} />);
23
+ expect(raiz()).toHaveClass("umds-select--md");
24
+ expect(raiz()).not.toHaveClass("umds-select--sm");
25
+ });
26
+
27
+ it("o azul-claro do gatilho só vem com valor escolhido (--has-value); sem valor, não", () => {
28
+ const { container, rerender } = render(<Select ariaLabel="x" options={MARCADORES} variant="filter" placeholder="Marcadores" />);
29
+ const raiz = () => container.firstElementChild as HTMLElement;
30
+ expect(raiz()).not.toHaveClass("umds-select--has-value");
31
+ rerender(<Select ariaLabel="x" options={MARCADORES} variant="filter" placeholder="Marcadores" value="urgente" />);
32
+ expect(raiz()).toHaveClass("umds-select--has-value");
33
+ rerender(<Select ariaLabel="x" options={MARCADORES} placeholder="Marcadores" value="urgente" />);
34
+ expect(raiz()).not.toHaveClass("umds-select--has-value");
35
+ });
36
+
37
+ it("o valor vazio do emptyOptionLabel volta ao estado sem valor", () => {
38
+ const { container } = render(
39
+ <Select ariaLabel="x" options={MARCADORES} variant="filter" placeholder="Marcadores" emptyOptionLabel="Todos" value="" />,
40
+ );
41
+ expect(container.firstElementChild).not.toHaveClass("umds-select--has-value");
42
+ });
43
+ });
44
+
45
+ describe("Select · cor por opção", () => {
46
+ it("a lista mostra uma amostra por opção com color; sem color, não", () => {
47
+ const { container } = render(<Select ariaLabel="x" options={MARCADORES} variant="filter" />);
48
+ fireEvent.click(screen.getByRole("combobox"));
49
+ const opcoes = [...container.querySelectorAll('[role="option"]')];
50
+ expect(opcoes[0].querySelector(".umds-select__swatch")).toHaveAttribute("aria-hidden", "true");
51
+ expect((opcoes[0].querySelector(".umds-select__swatch") as HTMLElement).style.background).toContain("--umds-semantic-color-feedback-negative-pure");
52
+ expect(opcoes[1].querySelector(".umds-select__swatch")).not.toBeNull();
53
+ expect(opcoes[2].querySelector(".umds-select__swatch")).toBeNull();
54
+ });
55
+
56
+ it("o gatilho ganha a cor da opção escolhida (seleção única)", () => {
57
+ const { container } = render(<Select ariaLabel="x" options={MARCADORES} variant="filter" placeholder="Marcadores" value="revisao" />);
58
+ const amostra = container.querySelector(".umds-select__trigger .umds-select__swatch") as HTMLElement;
59
+ expect(amostra.style.background).toContain("--umds-semantic-color-primary-pure");
60
+ });
61
+
62
+ it("opção escolhida sem color e estado sem valor: o gatilho não tem amostra", () => {
63
+ const { container, rerender } = render(<Select ariaLabel="x" options={MARCADORES} placeholder="Marcadores" value="nenhum" />);
64
+ expect(container.querySelector(".umds-select__trigger .umds-select__swatch")).toBeNull();
65
+ rerender(<Select ariaLabel="x" options={MARCADORES} placeholder="Marcadores" />);
66
+ expect(container.querySelector(".umds-select__trigger .umds-select__swatch")).toBeNull();
67
+ });
68
+
69
+ it("triggerSwatch explícito manda sobre a cor da opção", () => {
70
+ const { container } = render(
71
+ <Select ariaLabel="x" options={MARCADORES} value="urgente" triggerSwatch="var(--umds-semantic-color-feedback-positive-pure)" />,
72
+ );
73
+ const amostras = container.querySelectorAll(".umds-select__trigger .umds-select__swatch");
74
+ expect(amostras).toHaveLength(1);
75
+ expect((amostras[0] as HTMLElement).style.background).toContain("--umds-semantic-color-feedback-positive-pure");
76
+ });
77
+
78
+ it("na seleção múltipla o gatilho não pinta amostra (são várias), mas a lista mostra as cores", () => {
79
+ const { container } = render(<Select ariaLabel="x" options={MARCADORES} multiple value={["urgente", "revisao"]} />);
80
+ expect(container.querySelector(".umds-select__trigger .umds-select__swatch")).toBeNull();
81
+ fireEvent.click(screen.getByRole("combobox"));
82
+ expect(container.querySelectorAll('[role="option"] .umds-select__swatch')).toHaveLength(2);
83
+ });
84
+
85
+ it("a cor não vira texto nem entra no nome acessível da opção", () => {
86
+ render(<Select ariaLabel="x" options={MARCADORES} />);
87
+ fireEvent.click(screen.getByRole("combobox"));
88
+ expect(screen.getByRole("option", { name: "Urgente" })).toBeInTheDocument();
89
+ });
90
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import StatCard from "../components/StatCard/StatCard";
4
+
5
+ describe("StatCard · segunda medida e valueTone", () => {
6
+ it("secondaryValue e secondaryLabel vêm depois do corpo, e o número principal segue antes do rótulo", () => {
7
+ const { container } = render(
8
+ <StatCard value="22,31%" label="Eficiência" description="Meu Setor" secondaryValue="14,11%" secondaryLabel="Geral" labelPosition="top" />,
9
+ );
10
+ const filhos = [...(container.firstElementChild as HTMLElement).children].map((e) => e.className);
11
+ expect(filhos[filhos.length - 1]).toBe("umds-stat-card__secundaria");
12
+ const secundaria = container.querySelector(".umds-stat-card__secundaria") as HTMLElement;
13
+ expect(secundaria.querySelector(".umds-stat-card__valor-secundario")).toHaveTextContent("14,11%");
14
+ expect(secundaria.querySelector(".umds-stat-card__rotulo-secundario")).toHaveTextContent("Geral");
15
+ expect(container.firstElementChild).toHaveClass("umds-stat-card--com-secundaria");
16
+ const texto = (container.firstElementChild as HTMLElement).textContent as string;
17
+ expect(texto.indexOf("22,31%")).toBeLessThan(texto.indexOf("Eficiência"));
18
+ expect(texto.indexOf("Eficiência")).toBeLessThan(texto.indexOf("14,11%"));
19
+ });
20
+
21
+ it("secondaryValue 0 aparece; vazio e ausente não", () => {
22
+ const { container, rerender } = render(<StatCard value="5" label="x" secondaryValue={0} />);
23
+ expect(container.querySelector(".umds-stat-card__valor-secundario")).toHaveTextContent("0");
24
+ rerender(<StatCard value="5" label="x" secondaryValue="" />);
25
+ expect(container.querySelector(".umds-stat-card__secundaria")).toBeNull();
26
+ rerender(<StatCard value="5" label="x" />);
27
+ expect(container.querySelector(".umds-stat-card__secundaria")).toBeNull();
28
+ expect(container.firstElementChild).not.toHaveClass("umds-stat-card--com-secundaria");
29
+ });
30
+
31
+ it("secondaryValue sem secondaryLabel não desenha o rótulo", () => {
32
+ const { container } = render(<StatCard value="5" label="x" secondaryValue="9" />);
33
+ expect(container.querySelector(".umds-stat-card__rotulo-secundario")).toBeNull();
34
+ });
35
+
36
+ it("valueTone escreve a classe do tom; default e ausente não", () => {
37
+ const { container, rerender } = render(<StatCard value="5" label="x" valueTone="negative" />);
38
+ expect(container.firstElementChild).toHaveClass("umds-stat-card--valor-negative");
39
+ rerender(<StatCard value="5" label="x" valueTone="primary" />);
40
+ expect(container.firstElementChild).toHaveClass("umds-stat-card--valor-primary");
41
+ rerender(<StatCard value="5" label="x" valueTone="default" />);
42
+ expect(container.firstElementChild!.className).not.toMatch(/valor-/);
43
+ rerender(<StatCard value="5" label="x" />);
44
+ expect(container.firstElementChild!.className).not.toMatch(/valor-/);
45
+ });
46
+
47
+ it("valueTone não mexe na variant do ícone", () => {
48
+ const { container } = render(<StatCard value="5" label="x" variant="positive" valueTone="negative" />);
49
+ expect(container.firstElementChild).toHaveClass("umds-stat-card--positive", "umds-stat-card--valor-negative");
50
+ });
51
+ });
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { act, fireEvent, render, screen } from "@testing-library/react";
3
+ import Toast from "../components/Toast/Toast";
4
+ import ToastStack from "../components/Toast/ToastStack";
5
+ import { dismissAllToasts, toast } from "../utils/toast";
6
+
7
+ afterEach(() => {
8
+ act(() => dismissAllToasts());
9
+ });
10
+
11
+ describe("Toast · ação e detalhes", () => {
12
+ it("actionLabel com onAction desenha o botão; acionar chama onAction e fecha", () => {
13
+ const aoAgir = vi.fn();
14
+ const aoFechar = vi.fn();
15
+ render(<Toast open message="Despacho enviado" actionLabel="Desfazer" onAction={aoAgir} onClose={aoFechar} />);
16
+ fireEvent.click(screen.getByRole("button", { name: "Desfazer" }));
17
+ expect(aoAgir).toHaveBeenCalledTimes(1);
18
+ expect(aoFechar).toHaveBeenCalledTimes(1);
19
+ });
20
+
21
+ it("a ação roda antes de fechar", () => {
22
+ const ordem: string[] = [];
23
+ render(
24
+ <Toast
25
+ open
26
+ message="x"
27
+ actionLabel="Ver processo"
28
+ onAction={() => ordem.push("acao")}
29
+ onClose={() => ordem.push("fechar")}
30
+ />,
31
+ );
32
+ fireEvent.click(screen.getByRole("button", { name: "Ver processo" }));
33
+ expect(ordem).toEqual(["acao", "fechar"]);
34
+ });
35
+
36
+ it("actionLabel sem onAction, ou onAction sem actionLabel, não desenha botão de ação", () => {
37
+ const { rerender } = render(<Toast open message="x" actionLabel="Desfazer" />);
38
+ expect(screen.queryByRole("button", { name: "Desfazer" })).toBeNull();
39
+ rerender(<Toast open message="x" onAction={() => {}} />);
40
+ expect(document.querySelector(".umds-toast__action")).toBeNull();
41
+ });
42
+
43
+ it("details vira uma lista de linhas abaixo da descrição", () => {
44
+ render(<Toast open message="3 anexos não enviados" description="Corrija e envie de novo." details={["Anexo 1 acima de 10 MB", "Anexo 2 sem assunto"]} />);
45
+ const itens = screen.getAllByRole("listitem");
46
+ expect(itens.map((i) => i.textContent)).toEqual(["Anexo 1 acima de 10 MB", "Anexo 2 sem assunto"]);
47
+ });
48
+
49
+ it("sem details não há lista", () => {
50
+ render(<Toast open message="Salvo" />);
51
+ expect(document.querySelector(".umds-toast__details")).toBeNull();
52
+ });
53
+
54
+ it("o botão de fechar continua ao lado da ação", () => {
55
+ render(<Toast open message="x" actionLabel="Desfazer" onAction={() => {}} />);
56
+ expect(screen.getByRole("button", { name: "Fechar notificação" })).toBeInTheDocument();
57
+ });
58
+ });
59
+
60
+ describe("toast() imperativo com ação e detalhes", () => {
61
+ it("passa ação e detalhes ao ToastStack, e acionar a ação fecha o toast", () => {
62
+ const aoAgir = vi.fn();
63
+ render(<ToastStack />);
64
+ act(() => {
65
+ toast({ message: "Despacho enviado", details: ["SADM", "SOBR"], actionLabel: "Desfazer", onAction: aoAgir });
66
+ });
67
+ expect(screen.getByText("Despacho enviado")).toBeInTheDocument();
68
+ expect(screen.getAllByRole("listitem")).toHaveLength(2);
69
+ fireEvent.click(screen.getByRole("button", { name: "Desfazer" }));
70
+ expect(aoAgir).toHaveBeenCalledTimes(1);
71
+ expect(screen.queryByText("Despacho enviado")).toBeNull();
72
+ });
73
+
74
+ it("toast com ação não some sozinho (duration 0); sem ação continua em 5000", () => {
75
+ vi.useFakeTimers();
76
+ try {
77
+ render(<ToastStack />);
78
+ act(() => {
79
+ toast({ message: "Com ação", actionLabel: "Desfazer", onAction: () => {} });
80
+ toast({ message: "Sem ação" });
81
+ });
82
+ expect(screen.getByText("Com ação")).toBeInTheDocument();
83
+ expect(screen.getByText("Sem ação")).toBeInTheDocument();
84
+ act(() => {
85
+ vi.advanceTimersByTime(5100);
86
+ });
87
+ expect(screen.queryByText("Sem ação")).toBeNull();
88
+ expect(screen.getByText("Com ação")).toBeInTheDocument();
89
+ } finally {
90
+ vi.useRealTimers();
91
+ }
92
+ });
93
+
94
+ it("um duration explícito vale também com ação", () => {
95
+ vi.useFakeTimers();
96
+ try {
97
+ render(<ToastStack />);
98
+ act(() => {
99
+ toast({ message: "Curto", actionLabel: "Desfazer", onAction: () => {}, duration: 1000 });
100
+ });
101
+ act(() => {
102
+ vi.advanceTimersByTime(1100);
103
+ });
104
+ expect(screen.queryByText("Curto")).toBeNull();
105
+ } finally {
106
+ vi.useRealTimers();
107
+ }
108
+ });
109
+ });