@1doc/1ds-react 2.9.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +653 -0
- package/dist/components/Accordion/Accordion.css +13 -0
- package/dist/components/Accordion/Accordion.d.ts +9 -0
- package/dist/components/Accordion/Accordion.js +18 -5
- package/dist/components/AppSubheader/AppSubheader.css +10 -1
- package/dist/components/AttachmentCard/AttachmentCard.css +10 -8
- package/dist/components/AttachmentCard/AttachmentCard.d.ts +3 -0
- package/dist/components/AttachmentCard/AttachmentCard.js +8 -1
- package/dist/components/Badge/Badge.css +27 -5
- package/dist/components/Badge/Badge.d.ts +20 -4
- package/dist/components/Badge/Badge.js +6 -0
- package/dist/components/Button/Button.css +5 -0
- package/dist/components/Button/Button.d.ts +3 -0
- package/dist/components/Button/Button.js +1 -0
- package/dist/components/Calendar/Calendar.css +10 -0
- package/dist/components/Calendar/Calendar.d.ts +10 -6
- package/dist/components/Calendar/Calendar.js +6 -1
- package/dist/components/Coachmark/Coachmark.css +10 -0
- package/dist/components/Coachmark/Coachmark.d.ts +14 -1
- package/dist/components/Coachmark/Coachmark.js +51 -5
- package/dist/components/ColorPicker/ColorPicker.css +8 -1
- package/dist/components/ColorPicker/ColorPicker.d.ts +2 -2
- package/dist/components/DataTable/DataTable.css +216 -0
- package/dist/components/DataTable/DataTable.d.ts +70 -2
- package/dist/components/DataTable/DataTable.js +293 -6
- package/dist/components/DescriptionList/DescriptionList.css +14 -0
- package/dist/components/DescriptionList/DescriptionList.d.ts +3 -0
- package/dist/components/DescriptionList/DescriptionList.js +1 -0
- package/dist/components/EmptyState/EmptyState.d.ts +4 -2
- package/dist/components/EmptyState/EmptyState.js +2 -2
- package/dist/components/Input/Input.css +30 -0
- package/dist/components/Input/Input.d.ts +10 -0
- package/dist/components/Input/Input.js +3 -1
- package/dist/components/List/ListItem.d.ts +14 -2
- package/dist/components/List/ListItem.js +3 -2
- package/dist/components/Modal/Modal.css +19 -0
- package/dist/components/Modal/Modal.d.ts +7 -0
- package/dist/components/Modal/Modal.js +1 -0
- package/dist/components/RichTextEditor/RichTextEditor.css +47 -0
- package/dist/components/RichTextEditor/RichTextEditor.d.ts +22 -8
- package/dist/components/RichTextEditor/RichTextEditor.js +460 -3
- package/dist/components/SegmentedControl/SegmentedControl.css +70 -0
- package/dist/components/SegmentedControl/SegmentedControl.d.ts +5 -0
- package/dist/components/SegmentedControl/SegmentedControl.js +5 -1
- package/dist/components/Select/Select.css +122 -2
- package/dist/components/Select/Select.d.ts +33 -4
- package/dist/components/Select/Select.js +256 -35
- package/dist/components/StatCard/StatCard.css +73 -0
- package/dist/components/StatCard/StatCard.d.ts +12 -0
- package/dist/components/StatCard/StatCard.js +11 -1
- package/dist/components/Tabs/Tabs.css +11 -0
- package/dist/components/Tabs/Tabs.d.ts +7 -0
- package/dist/components/Tabs/Tabs.js +7 -1
- package/dist/components/Tag/Tag.css +3 -1
- package/dist/components/Toast/Toast.css +32 -0
- package/dist/components/Toast/Toast.d.ts +18 -0
- package/dist/components/Toast/Toast.js +14 -1
- package/dist/components/Toast/ToastStack.js +8 -1
- package/dist/teste/AccordionControlado.test.d.ts +1 -0
- package/dist/teste/AccordionControlado.test.js +73 -0
- package/dist/teste/AccordionTituloNeutro.test.d.ts +1 -0
- package/dist/teste/AccordionTituloNeutro.test.js +23 -0
- package/dist/teste/AppSubheaderRegiao.test.d.ts +1 -0
- package/dist/teste/AppSubheaderRegiao.test.js +13 -0
- package/dist/teste/AttachmentCardLista.test.d.ts +1 -0
- package/dist/teste/AttachmentCardLista.test.js +49 -0
- package/dist/teste/AttachmentCardSm.test.d.ts +1 -0
- package/dist/teste/AttachmentCardSm.test.js +26 -0
- package/dist/teste/BadgeAttachedTomEPadding.test.d.ts +1 -0
- package/dist/teste/BadgeAttachedTomEPadding.test.js +43 -0
- package/dist/teste/BadgeNeutroColado.test.d.ts +1 -0
- package/dist/teste/BadgeNeutroColado.test.js +11 -0
- package/dist/teste/BadgeTag.test.js +13 -0
- package/dist/teste/ButtonDensidade.test.d.ts +1 -0
- package/dist/teste/ButtonDensidade.test.js +28 -0
- package/dist/teste/CalendarCabecalho.test.d.ts +1 -0
- package/dist/teste/CalendarCabecalho.test.js +11 -0
- package/dist/teste/CalendarCompacto.test.d.ts +1 -0
- package/dist/teste/CalendarCompacto.test.js +29 -0
- package/dist/teste/CoachmarkSemVeu.test.d.ts +1 -0
- package/dist/teste/CoachmarkSemVeu.test.js +85 -0
- package/dist/teste/CoachmarkUmPasso.test.d.ts +1 -0
- package/dist/teste/CoachmarkUmPasso.test.js +34 -0
- package/dist/teste/ColorPickerXs.test.d.ts +1 -0
- package/dist/teste/ColorPickerXs.test.js +21 -0
- package/dist/teste/DataTableArvoreEVariantes.test.d.ts +1 -0
- package/dist/teste/DataTableArvoreEVariantes.test.js +190 -0
- package/dist/teste/DataTableCompactaETom.test.d.ts +1 -0
- package/dist/teste/DataTableCompactaETom.test.js +69 -0
- package/dist/teste/DescriptionListCompacta.test.d.ts +1 -0
- package/dist/teste/DescriptionListCompacta.test.js +22 -0
- package/dist/teste/EmptyStateTituloRico.test.d.ts +1 -0
- package/dist/teste/EmptyStateTituloRico.test.js +23 -0
- package/dist/teste/InputAgrupado.test.d.ts +1 -0
- package/dist/teste/InputAgrupado.test.js +24 -0
- package/dist/teste/InputGhost.test.d.ts +1 -0
- package/dist/teste/InputGhost.test.js +32 -0
- package/dist/teste/ListItemTooltipStandalone.test.d.ts +1 -0
- package/dist/teste/ListItemTooltipStandalone.test.js +39 -0
- package/dist/teste/ModalProporcoes1Doc.test.d.ts +1 -0
- package/dist/teste/ModalProporcoes1Doc.test.js +32 -0
- package/dist/teste/RichTextEditorAlinhaImagemTabela.test.d.ts +1 -0
- package/dist/teste/RichTextEditorAlinhaImagemTabela.test.js +212 -0
- package/dist/teste/RichTextEditorEstiloDeTexto.test.d.ts +1 -0
- package/dist/teste/RichTextEditorEstiloDeTexto.test.js +120 -0
- package/dist/teste/SegmentedControlContador.test.d.ts +1 -0
- package/dist/teste/SegmentedControlContador.test.js +39 -0
- package/dist/teste/SegmentedControlSuperficie.test.d.ts +1 -0
- package/dist/teste/SegmentedControlSuperficie.test.js +39 -0
- package/dist/teste/SelectAbertoControladoEFlutuante.test.d.ts +1 -0
- package/dist/teste/SelectAbertoControladoEFlutuante.test.js +145 -0
- package/dist/teste/SelectFiltroCorETamanho.test.d.ts +1 -0
- package/dist/teste/SelectFiltroCorETamanho.test.js +77 -0
- package/dist/teste/SelectItemComEscopo.test.d.ts +1 -0
- package/dist/teste/SelectItemComEscopo.test.js +54 -0
- package/dist/teste/SelectListaFiltro.test.d.ts +1 -0
- package/dist/teste/SelectListaFiltro.test.js +102 -0
- package/dist/teste/StatCardSecundariaETom.test.d.ts +1 -0
- package/dist/teste/StatCardSecundariaETom.test.js +45 -0
- package/dist/teste/StatCardTamanhoSm.test.d.ts +1 -0
- package/dist/teste/StatCardTamanhoSm.test.js +32 -0
- package/dist/teste/TabsCompacta.test.d.ts +1 -0
- package/dist/teste/TabsCompacta.test.js +35 -0
- package/dist/teste/TagPositivoTingido.test.d.ts +1 -0
- package/dist/teste/TagPositivoTingido.test.js +54 -0
- package/dist/teste/ToastAcaoEDetalhes.test.d.ts +1 -0
- package/dist/teste/ToastAcaoEDetalhes.test.js +94 -0
- package/dist/teste/lerArquivo.d.ts +1 -0
- package/dist/teste/lerArquivo.js +12 -0
- package/dist/utils/html-seguro.d.ts +19 -1
- package/dist/utils/html-seguro.js +113 -2
- package/dist/utils/toast.d.ts +10 -1
- package/dist/utils/toast.js +2 -1
- package/package.json +3 -3
- package/src/teste/AccordionControlado.test.tsx +87 -0
- package/src/teste/AccordionTituloNeutro.test.tsx +25 -0
- package/src/teste/AppSubheaderRegiao.test.tsx +17 -0
- package/src/teste/AttachmentCardLista.test.tsx +53 -0
- package/src/teste/AttachmentCardSm.test.tsx +29 -0
- package/src/teste/BadgeAttachedTomEPadding.test.tsx +60 -0
- package/src/teste/BadgeNeutroColado.test.tsx +15 -0
- package/src/teste/BadgeTag.test.tsx +21 -0
- package/src/teste/ButtonDensidade.test.tsx +40 -0
- package/src/teste/CalendarCabecalho.test.tsx +11 -0
- package/src/teste/CalendarCompacto.test.tsx +37 -0
- package/src/teste/CoachmarkSemVeu.test.tsx +101 -0
- package/src/teste/CoachmarkUmPasso.test.tsx +45 -0
- package/src/teste/ColorPickerXs.test.tsx +22 -0
- package/src/teste/DataTableArvoreEVariantes.test.tsx +240 -0
- package/src/teste/DataTableCompactaETom.test.tsx +85 -0
- package/src/teste/DescriptionListCompacta.test.tsx +24 -0
- package/src/teste/EmptyStateTituloRico.test.tsx +34 -0
- package/src/teste/InputAgrupado.test.tsx +26 -0
- package/src/teste/InputGhost.test.tsx +42 -0
- package/src/teste/ListItemTooltipStandalone.test.tsx +64 -0
- package/src/teste/ModalProporcoes1Doc.test.tsx +36 -0
- package/src/teste/RichTextEditorAlinhaImagemTabela.test.tsx +238 -0
- package/src/teste/RichTextEditorEstiloDeTexto.test.tsx +137 -0
- package/src/teste/SegmentedControlContador.test.tsx +43 -0
- package/src/teste/SegmentedControlSuperficie.test.tsx +52 -0
- package/src/teste/SelectAbertoControladoEFlutuante.test.tsx +180 -0
- package/src/teste/SelectFiltroCorETamanho.test.tsx +90 -0
- package/src/teste/SelectItemComEscopo.test.tsx +69 -0
- package/src/teste/SelectListaFiltro.test.tsx +120 -0
- package/src/teste/StatCardSecundariaETom.test.tsx +51 -0
- package/src/teste/StatCardTamanhoSm.test.tsx +42 -0
- package/src/teste/TabsCompacta.test.tsx +45 -0
- package/src/teste/TagPositivoTingido.test.tsx +60 -0
- package/src/teste/ToastAcaoEDetalhes.test.tsx +109 -0
- package/src/teste/lerArquivo.ts +13 -0
- package/src/utils/html-seguro.ts +105 -2
- package/src/utils/toast.ts +12 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import RichTextEditor from "../components/RichTextEditor/RichTextEditor";
|
|
6
|
+
import { htmlDaTabela, htmlVazio, imagemSegura, sanitizarHtml } from "../utils/html-seguro";
|
|
7
|
+
let execCommand;
|
|
8
|
+
let estados;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
estados = {};
|
|
11
|
+
execCommand = vi.fn(() => true);
|
|
12
|
+
document.execCommand = execCommand;
|
|
13
|
+
document.queryCommandState = (comando) => !!estados[comando];
|
|
14
|
+
});
|
|
15
|
+
const area = () => screen.getByRole("textbox");
|
|
16
|
+
const PNG = "data:image/png;base64,iVBORw0KGgo=";
|
|
17
|
+
describe("html-seguro: alinhamento, imagem e tabela", () => {
|
|
18
|
+
it("text-align fica nos blocos, em style ou em align; o resto do style sai", () => {
|
|
19
|
+
expect(sanitizarHtml('<p style="text-align: center; color: red">a</p>')).toBe('<p style="text-align: center;">a</p>');
|
|
20
|
+
expect(sanitizarHtml('<div align="right">a</div>')).toBe('<div style="text-align: right;">a</div>');
|
|
21
|
+
expect(sanitizarHtml('<p style="text-align: expression(alert(1))">a</p>')).toBe("<p>a</p>");
|
|
22
|
+
expect(sanitizarHtml('<li style="text-align:justify">a</li>')).toBe('<li style="text-align: justify;">a</li>');
|
|
23
|
+
expect(sanitizarHtml('<b style="text-align:center">a</b>')).toBe("<b>a</b>");
|
|
24
|
+
});
|
|
25
|
+
it("imagem: http(s), caminho e data:image em PNG, JPEG, GIF e WebP ficam; o resto some inteiro", () => {
|
|
26
|
+
expect(sanitizarHtml('<img src="https://a.com/x.png" alt="Brasão" onerror="x()">')).toBe('<img src="https://a.com/x.png" alt="Brasão">');
|
|
27
|
+
expect(sanitizarHtml('<img src="/img/x.png">')).toBe('<img src="/img/x.png" alt="">');
|
|
28
|
+
expect(sanitizarHtml(`<img src="${PNG}" alt="a">`)).toBe(`<img src="${PNG}" alt="a">`);
|
|
29
|
+
for (const mau of ["javascript:alert(1)", "x", "data:image/svg+xml;base64,PHN2Zz4=", "data:text/html;base64,PGI+", "//evil.com/x.png", "ftp://a/x.png"]) {
|
|
30
|
+
expect(sanitizarHtml(`<p>a<img src="${mau}">b</p>`)).toBe("<p>ab</p>");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it("imagemSegura devolve vazio para o perigoso", () => {
|
|
34
|
+
expect(imagemSegura("java\nscript:alert(1)")).toBe("");
|
|
35
|
+
expect(imagemSegura("https://a.com/x.png")).toBe("https://a.com/x.png");
|
|
36
|
+
expect(imagemSegura("")).toBe("");
|
|
37
|
+
});
|
|
38
|
+
it("tabela simples: tags e colspan/rowspan ficam; atributos soltos saem", () => {
|
|
39
|
+
const html = '<table class="x" onclick="a()"><thead><tr><th colspan="2">T</th></tr></thead><tbody><tr><td rowspan="3" style="color:red">a</td><td colspan="x">b</td></tr></tbody></table>';
|
|
40
|
+
expect(sanitizarHtml(html)).toBe('<table><thead><tr><th colspan="2">T</th></tr></thead><tbody><tr><td rowspan="3">a</td><td>b</td></tr></tbody></table>');
|
|
41
|
+
});
|
|
42
|
+
it("htmlVazio: imagem e tabela contam como conteúdo", () => {
|
|
43
|
+
expect(htmlVazio("<img src='/a.png'>")).toBe(false);
|
|
44
|
+
expect(htmlVazio(htmlDaTabela(1, 1))).toBe(false);
|
|
45
|
+
expect(htmlVazio("<p><br></p>")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it("htmlDaTabela faz linhas × colunas de células com quebra", () => {
|
|
48
|
+
const celulas = (htmlDaTabela(2, 3).match(/<td><br><\/td>/g) || []).length;
|
|
49
|
+
expect(celulas).toBe(6);
|
|
50
|
+
expect((htmlDaTabela(2, 3).match(/<tr>/g) || []).length).toBe(2);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("RichTextEditor: alinhamento", () => {
|
|
54
|
+
it("as quatro ferramentas de alinhamento pedem o comando e seguem o cursor", async () => {
|
|
55
|
+
const usuario = userEvent.setup();
|
|
56
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["alignLeft", "alignCenter", "alignRight", "alignJustify"], defaultValue: "<p>x</p>" }));
|
|
57
|
+
const esperado = {
|
|
58
|
+
"Alinhar à esquerda": "justifyLeft",
|
|
59
|
+
Centralizar: "justifyCenter",
|
|
60
|
+
"Alinhar à direita": "justifyRight",
|
|
61
|
+
Justificar: "justifyFull",
|
|
62
|
+
};
|
|
63
|
+
for (const [nome, comando] of Object.entries(esperado)) {
|
|
64
|
+
execCommand.mockClear();
|
|
65
|
+
await usuario.click(screen.getByRole("button", { name: nome }));
|
|
66
|
+
expect(execCommand).toHaveBeenCalledWith(comando, false, undefined);
|
|
67
|
+
}
|
|
68
|
+
estados.justifyCenter = true;
|
|
69
|
+
const faixa = document.createRange();
|
|
70
|
+
faixa.selectNodeContents(area());
|
|
71
|
+
window.getSelection().removeAllRanges();
|
|
72
|
+
window.getSelection().addRange(faixa);
|
|
73
|
+
fireEvent(document, new Event("selectionchange"));
|
|
74
|
+
expect(screen.getByRole("button", { name: "Centralizar" })).toHaveAttribute("aria-pressed", "true");
|
|
75
|
+
expect(screen.getByRole("button", { name: "Justificar" })).toHaveAttribute("aria-pressed", "false");
|
|
76
|
+
});
|
|
77
|
+
it("a barra padrão não ganha as ferramentas novas", () => {
|
|
78
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x" }));
|
|
79
|
+
for (const nome of ["Centralizar", "Imagem", "Tabela"])
|
|
80
|
+
expect(screen.queryByRole("button", { name: nome })).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("RichTextEditor: imagem", () => {
|
|
84
|
+
const abrirImagem = async (props = {}, botao = "Imagem") => {
|
|
85
|
+
const usuario = userEvent.setup();
|
|
86
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["image"], defaultValue: "<p>texto</p>", ...props }));
|
|
87
|
+
await usuario.click(screen.getByRole("button", { name: botao }));
|
|
88
|
+
return usuario;
|
|
89
|
+
};
|
|
90
|
+
it("o botão abre o painel com foco no endereço; Esc fecha", async () => {
|
|
91
|
+
const usuario = await abrirImagem();
|
|
92
|
+
expect(screen.getByLabelText("Endereço da imagem")).toHaveFocus();
|
|
93
|
+
expect(screen.getByLabelText("Texto alternativo")).toBeInTheDocument();
|
|
94
|
+
await usuario.keyboard("{Escape}");
|
|
95
|
+
expect(screen.queryByLabelText("Endereço da imagem")).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
it("endereço e texto alternativo inserem o img com alt", async () => {
|
|
98
|
+
const aoMudar = vi.fn();
|
|
99
|
+
const usuario = await abrirImagem({ onChange: aoMudar });
|
|
100
|
+
await usuario.type(screen.getByLabelText("Endereço da imagem"), "exemplo.com.br/brasao.png");
|
|
101
|
+
await usuario.type(screen.getByLabelText("Texto alternativo"), "Brasão da prefeitura");
|
|
102
|
+
await usuario.click(screen.getByRole("button", { name: "Inserir imagem" }));
|
|
103
|
+
expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<img src="https://exemplo.com.br/brasao.png" alt="Brasão da prefeitura">');
|
|
104
|
+
expect(screen.queryByLabelText("Endereço da imagem")).toBeNull();
|
|
105
|
+
expect(aoMudar).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
it("endereço perigoso mostra o erro e o painel continua aberto", async () => {
|
|
108
|
+
const usuario = await abrirImagem();
|
|
109
|
+
await usuario.type(screen.getByLabelText("Endereço da imagem"), "javascript:alert(1)");
|
|
110
|
+
await usuario.click(screen.getByRole("button", { name: "Inserir imagem" }));
|
|
111
|
+
expect(screen.getByText("Use um endereço http ou https, ou escolha um arquivo.")).toBeInTheDocument();
|
|
112
|
+
expect(execCommand).not.toHaveBeenCalledWith("insertHTML", expect.anything(), expect.anything());
|
|
113
|
+
expect(screen.getByLabelText("Endereço da imagem")).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
it("arquivo vira data URL e entra no img", async () => {
|
|
116
|
+
const usuario = await abrirImagem();
|
|
117
|
+
const entrada = document.querySelector('input[type="file"]');
|
|
118
|
+
const arquivo = new File([new Uint8Array([137, 80, 78, 71])], "brasao.png", { type: "image/png" });
|
|
119
|
+
fireEvent.change(entrada, { target: { files: [arquivo] } });
|
|
120
|
+
await waitFor(() => expect(screen.getByText("brasao.png")).toBeInTheDocument());
|
|
121
|
+
await usuario.click(screen.getByRole("button", { name: "Inserir imagem" }));
|
|
122
|
+
const chamada = execCommand.mock.calls.find((c) => c[0] === "insertHTML");
|
|
123
|
+
expect(chamada[2]).toMatch(/^<img src="data:image\/png;base64,[A-Za-z0-9+/=]+" alt="">$/);
|
|
124
|
+
});
|
|
125
|
+
it("arquivo que não é PNG, JPEG, GIF ou WebP é recusado; acima de 2 MB também", async () => {
|
|
126
|
+
await abrirImagem();
|
|
127
|
+
const entrada = document.querySelector('input[type="file"]');
|
|
128
|
+
fireEvent.change(entrada, { target: { files: [new File(["x"], "a.svg", { type: "image/svg+xml" })] } });
|
|
129
|
+
expect(await screen.findByText("Use uma imagem PNG, JPEG, GIF ou WebP.")).toBeInTheDocument();
|
|
130
|
+
const grande = new File(["x"], "g.png", { type: "image/png" });
|
|
131
|
+
Object.defineProperty(grande, "size", { value: 3 * 1024 * 1024 });
|
|
132
|
+
fireEvent.change(entrada, { target: { files: [grande] } });
|
|
133
|
+
expect(await screen.findByText("A imagem passa de 2 MB.")).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
it("os textos do painel vêm por labels", async () => {
|
|
136
|
+
await abrirImagem({ labels: { image: "Image", imageUrl: "Image address", imageAlt: "Alt text", imageApply: "Insert image", imageCancel: "Cancel", imageFile: "Choose file" } }, "Image");
|
|
137
|
+
expect(screen.getByLabelText("Image address")).toBeInTheDocument();
|
|
138
|
+
expect(screen.getByLabelText("Alt text")).toBeInTheDocument();
|
|
139
|
+
expect(screen.getByRole("button", { name: "Insert image" })).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByRole("button", { name: "Choose file" })).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("RichTextEditor: tabela", () => {
|
|
144
|
+
it("o painel insere linhas × colunas; o padrão é 3 × 3", async () => {
|
|
145
|
+
const usuario = userEvent.setup();
|
|
146
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["table"] }));
|
|
147
|
+
await usuario.click(screen.getByRole("button", { name: "Tabela" }));
|
|
148
|
+
expect(screen.getByLabelText("Linhas")).toHaveFocus();
|
|
149
|
+
await usuario.clear(screen.getByLabelText("Linhas"));
|
|
150
|
+
await usuario.type(screen.getByLabelText("Linhas"), "2");
|
|
151
|
+
await usuario.click(screen.getByRole("button", { name: "Inserir tabela" }));
|
|
152
|
+
const chamada = execCommand.mock.calls.find((c) => c[0] === "insertHTML");
|
|
153
|
+
expect(chamada[2]).toBe(htmlDaTabela(2, 3));
|
|
154
|
+
});
|
|
155
|
+
it("fora de 1 a 20 linhas e 1 a 10 colunas mostra o erro e não insere", async () => {
|
|
156
|
+
const usuario = userEvent.setup();
|
|
157
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["table"] }));
|
|
158
|
+
await usuario.click(screen.getByRole("button", { name: "Tabela" }));
|
|
159
|
+
await usuario.clear(screen.getByLabelText("Colunas"));
|
|
160
|
+
await usuario.type(screen.getByLabelText("Colunas"), "11");
|
|
161
|
+
await usuario.click(screen.getByRole("button", { name: "Inserir tabela" }));
|
|
162
|
+
expect(screen.getByText("Use de 1 a 20 linhas e de 1 a 10 colunas.")).toBeInTheDocument();
|
|
163
|
+
expect(execCommand).not.toHaveBeenCalledWith("insertHTML", expect.anything(), expect.anything());
|
|
164
|
+
});
|
|
165
|
+
const TABELA = "<table><tbody><tr><td>a</td><td>b</td></tr><tr><td>c</td><td>d</td></tr></tbody></table>";
|
|
166
|
+
const cursorNaCelula = (indice) => {
|
|
167
|
+
const celula = area().querySelectorAll("td")[indice];
|
|
168
|
+
const faixa = document.createRange();
|
|
169
|
+
faixa.selectNodeContents(celula);
|
|
170
|
+
window.getSelection().removeAllRanges();
|
|
171
|
+
window.getSelection().addRange(faixa);
|
|
172
|
+
fireEvent(document, new Event("selectionchange"));
|
|
173
|
+
};
|
|
174
|
+
it("com o cursor numa célula aparece a barra; sem tabela na toolbar, não aparece", async () => {
|
|
175
|
+
const { rerender } = render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["table"], defaultValue: TABELA }));
|
|
176
|
+
expect(screen.queryByRole("group", { name: "Tabela" })).toBeNull();
|
|
177
|
+
cursorNaCelula(0);
|
|
178
|
+
expect(await screen.findByRole("group", { name: "Tabela" })).toBeInTheDocument();
|
|
179
|
+
rerender(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["bold"], defaultValue: TABELA }));
|
|
180
|
+
expect(screen.queryByRole("group", { name: "Tabela" })).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
it("adiciona e remove linha e coluna na célula do cursor e avisa onChange", async () => {
|
|
183
|
+
const usuario = userEvent.setup();
|
|
184
|
+
const aoMudar = vi.fn();
|
|
185
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["table"], defaultValue: TABELA, onChange: aoMudar }));
|
|
186
|
+
cursorNaCelula(0);
|
|
187
|
+
await usuario.click(await screen.findByRole("button", { name: "Adicionar coluna" }));
|
|
188
|
+
expect(area().querySelectorAll("tr")[0].cells.length).toBe(3);
|
|
189
|
+
expect(area().querySelectorAll("tr")[1].cells.length).toBe(3);
|
|
190
|
+
expect(aoMudar).toHaveBeenCalled();
|
|
191
|
+
cursorNaCelula(0);
|
|
192
|
+
await usuario.click(await screen.findByRole("button", { name: "Adicionar linha" }));
|
|
193
|
+
expect(area().querySelectorAll("tr")).toHaveLength(3);
|
|
194
|
+
cursorNaCelula(0);
|
|
195
|
+
await usuario.click(await screen.findByRole("button", { name: "Remover linha" }));
|
|
196
|
+
expect(area().querySelectorAll("tr")).toHaveLength(2);
|
|
197
|
+
cursorNaCelula(0);
|
|
198
|
+
await usuario.click(await screen.findByRole("button", { name: "Remover coluna" }));
|
|
199
|
+
expect(area().querySelectorAll("tr")[0].cells.length).toBe(2);
|
|
200
|
+
cursorNaCelula(0);
|
|
201
|
+
await usuario.click(await screen.findByRole("button", { name: "Remover tabela" }));
|
|
202
|
+
expect(area().querySelector("table")).toBeNull();
|
|
203
|
+
expect(screen.queryByRole("group", { name: "Tabela" })).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
it("remover a única linha ou coluna remove a tabela", async () => {
|
|
206
|
+
const usuario = userEvent.setup();
|
|
207
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: ["table"], defaultValue: "<table><tbody><tr><td>a</td></tr></tbody></table>" }));
|
|
208
|
+
cursorNaCelula(0);
|
|
209
|
+
await usuario.click(await screen.findByRole("button", { name: "Remover linha" }));
|
|
210
|
+
expect(area().querySelector("table")).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import RichTextEditor from "../components/RichTextEditor/RichTextEditor";
|
|
6
|
+
import { estiloDeTexto, sanitizarHtml } from "../utils/html-seguro";
|
|
7
|
+
let execCommand;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
execCommand = vi.fn(() => true);
|
|
10
|
+
document.execCommand = execCommand;
|
|
11
|
+
document.queryCommandState = () => false;
|
|
12
|
+
});
|
|
13
|
+
const area = () => screen.getByRole("textbox");
|
|
14
|
+
const selecionarTexto = () => {
|
|
15
|
+
const alvo = area().querySelector("p").firstChild;
|
|
16
|
+
const faixa = document.createRange();
|
|
17
|
+
faixa.setStart(alvo, 0);
|
|
18
|
+
faixa.setEnd(alvo, alvo.length);
|
|
19
|
+
window.getSelection().removeAllRanges();
|
|
20
|
+
window.getSelection().addRange(faixa);
|
|
21
|
+
};
|
|
22
|
+
const abrir = (toolbar, props = {}) => render(_jsx(RichTextEditor, { ariaLabel: "x", toolbar: toolbar, defaultValue: "<p>Parecer</p>", ...props }));
|
|
23
|
+
describe("html-seguro: estilo de texto por token", () => {
|
|
24
|
+
it("estiloDeTexto só aceita token do DS", () => {
|
|
25
|
+
expect(estiloDeTexto("color", "var(--umds-primitive-red-pure)")).toBe("color: var(--umds-primitive-red-pure);");
|
|
26
|
+
expect(estiloDeTexto("color", "var(--umds-semantic-color-feedback-negative-pure)")).toBe("color: var(--umds-semantic-color-feedback-negative-pure);");
|
|
27
|
+
expect(estiloDeTexto("font-size", "var(--umds-primitive-font-size-xs)")).toBe("font-size: var(--umds-primitive-font-size-xs);");
|
|
28
|
+
expect(estiloDeTexto("font-family", "var(--umds-semantic-font-family-mono)")).toBe("font-family: var(--umds-semantic-font-family-mono);");
|
|
29
|
+
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)"]) {
|
|
30
|
+
expect(estiloDeTexto("color", mau)).toBe("");
|
|
31
|
+
}
|
|
32
|
+
expect(estiloDeTexto("font-size", "12px")).toBe("");
|
|
33
|
+
expect(estiloDeTexto("font-size", "var(--umds-primitive-red-pure)")).toBe("");
|
|
34
|
+
expect(estiloDeTexto("font-family", "Comic Sans MS")).toBe("");
|
|
35
|
+
expect(estiloDeTexto("background", "var(--umds-primitive-red-pure)")).toBe("");
|
|
36
|
+
});
|
|
37
|
+
it("o span guarda só as declarações válidas; o resto do style sai", () => {
|
|
38
|
+
expect(sanitizarHtml('<span style="color: var(--umds-primitive-red-pure); background: red; font-size: 40px">a</span>')).toBe('<span style="color: var(--umds-primitive-red-pure);">a</span>');
|
|
39
|
+
expect(sanitizarHtml('<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm)">a</span>')).toBe('<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm);">a</span>');
|
|
40
|
+
expect(sanitizarHtml('<span style="color: red">a</span>')).toBe("<span>a</span>");
|
|
41
|
+
});
|
|
42
|
+
it("estilo só vale em span: em outras tags sai", () => {
|
|
43
|
+
expect(sanitizarHtml('<b style="color: var(--umds-primitive-red-pure)">a</b>')).toBe("<b>a</b>");
|
|
44
|
+
expect(sanitizarHtml('<p style="color: var(--umds-primitive-red-pure)">a</p>')).toBe("<p>a</p>");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe("RichTextEditor: fonte, tamanho e cor do texto", () => {
|
|
48
|
+
it("a barra padrão não ganha as ferramentas novas", () => {
|
|
49
|
+
render(_jsx(RichTextEditor, { ariaLabel: "x" }));
|
|
50
|
+
for (const nome of ["Fonte", "Tamanho da fonte", "Cor do texto"])
|
|
51
|
+
expect(screen.queryByRole("button", { name: nome })).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it("cada ferramenta abre um painel nomeado abaixo da barra; Fechar e Esc fecham", async () => {
|
|
54
|
+
const usuario = userEvent.setup();
|
|
55
|
+
abrir(["fontFamily", "fontSize", "textColor"]);
|
|
56
|
+
await usuario.click(screen.getByRole("button", { name: "Tamanho da fonte" }));
|
|
57
|
+
expect(screen.getByRole("group", { name: "Tamanho da fonte" })).toBeInTheDocument();
|
|
58
|
+
await usuario.click(screen.getByRole("button", { name: "Fechar" }));
|
|
59
|
+
expect(screen.queryByRole("group", { name: "Tamanho da fonte" })).toBeNull();
|
|
60
|
+
await usuario.click(screen.getByRole("button", { name: "Cor do texto" }));
|
|
61
|
+
expect(screen.getByRole("group", { name: "Cor do texto" })).toBeInTheDocument();
|
|
62
|
+
fireEvent.keyDown(screen.getByRole("group", { name: "Cor do texto" }), { key: "Escape" });
|
|
63
|
+
expect(screen.queryByRole("group", { name: "Cor do texto" })).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it("a fonte monoespaçada embrulha o texto selecionado num span com o token", async () => {
|
|
66
|
+
const usuario = userEvent.setup();
|
|
67
|
+
const aoMudar = vi.fn();
|
|
68
|
+
abrir(["fontFamily"], { onChange: aoMudar });
|
|
69
|
+
selecionarTexto();
|
|
70
|
+
await usuario.click(screen.getByRole("button", { name: "Fonte" }));
|
|
71
|
+
await usuario.click(screen.getByRole("combobox", { name: "Fonte" }));
|
|
72
|
+
await usuario.click(screen.getByRole("option", { name: "Monoespaçada" }));
|
|
73
|
+
expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-family: var(--umds-semantic-font-family-mono);">Parecer</span>');
|
|
74
|
+
expect(aoMudar).toHaveBeenCalled();
|
|
75
|
+
expect(screen.queryByRole("group", { name: "Fonte" })).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
it("o tamanho usa os degraus da escala do DS", async () => {
|
|
78
|
+
const usuario = userEvent.setup();
|
|
79
|
+
abrir(["fontSize"]);
|
|
80
|
+
selecionarTexto();
|
|
81
|
+
await usuario.click(screen.getByRole("button", { name: "Tamanho da fonte" }));
|
|
82
|
+
await usuario.click(screen.getByRole("combobox", { name: "Tamanho da fonte" }));
|
|
83
|
+
const opcoes = screen.getAllByRole("option").map((o) => o.textContent);
|
|
84
|
+
expect(opcoes).toEqual(["12", "14", "16", "20", "24"]);
|
|
85
|
+
await usuario.click(screen.getByRole("option", { name: "20" }));
|
|
86
|
+
expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-size: var(--umds-primitive-font-size-sm);">Parecer</span>');
|
|
87
|
+
});
|
|
88
|
+
it("a cor vem da paleta do DS e vira var() do token", async () => {
|
|
89
|
+
const usuario = userEvent.setup();
|
|
90
|
+
abrir(["textColor"]);
|
|
91
|
+
selecionarTexto();
|
|
92
|
+
await usuario.click(screen.getByRole("button", { name: "Cor do texto" }));
|
|
93
|
+
await usuario.click(screen.getByRole("radio", { name: "Vermelho" }));
|
|
94
|
+
expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="color: var(--umds-primitive-red-pure);">Parecer</span>');
|
|
95
|
+
});
|
|
96
|
+
it("sem texto selecionado o painel pede a seleção e não aplica nada", async () => {
|
|
97
|
+
const usuario = userEvent.setup();
|
|
98
|
+
abrir(["textColor"]);
|
|
99
|
+
window.getSelection().removeAllRanges();
|
|
100
|
+
await usuario.click(screen.getByRole("button", { name: "Cor do texto" }));
|
|
101
|
+
await usuario.click(screen.getByRole("radio", { name: "Vermelho" }));
|
|
102
|
+
expect(screen.getByText("Selecione o texto antes de aplicar.")).toBeInTheDocument();
|
|
103
|
+
expect(execCommand).not.toHaveBeenCalledWith("insertHTML", expect.anything(), expect.anything());
|
|
104
|
+
expect(screen.getByRole("group", { name: "Cor do texto" })).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
it("os textos vêm por labels", async () => {
|
|
107
|
+
const usuario = userEvent.setup();
|
|
108
|
+
abrir(["fontFamily", "textColor"], { labels: { fontFamily: "Font", textColor: "Text color", fontFamilyMono: "Monospaced", styleClose: "Close" } });
|
|
109
|
+
selecionarTexto();
|
|
110
|
+
await usuario.click(screen.getByRole("button", { name: "Font" }));
|
|
111
|
+
await usuario.click(screen.getByRole("combobox", { name: "Font" }));
|
|
112
|
+
expect(screen.getByRole("option", { name: "Monospaced" })).toBeInTheDocument();
|
|
113
|
+
await usuario.keyboard("{Escape}");
|
|
114
|
+
expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
it("o valor volta higienizado com o estilo por token", () => {
|
|
117
|
+
abrir(["textColor"], { defaultValue: '<p><span style="color: var(--umds-primitive-blue-pure); background: red">a</span></p>' });
|
|
118
|
+
expect(area().innerHTML).toBe('<p><span style="color: var(--umds-primitive-blue-pure);">a</span></p>');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import SegmentedControl from "../components/SegmentedControl/SegmentedControl";
|
|
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
|
+
describe("SegmentedControl · count", () => {
|
|
13
|
+
it("desenha a pílula do contador depois do texto; 0 aparece, vazio e ausente não", () => {
|
|
14
|
+
const { container } = render(_jsx(SegmentedControl, { label: "Fila", options: FILA }));
|
|
15
|
+
const itens = [...container.querySelectorAll(".umds-segmented__item")];
|
|
16
|
+
expect(itens[0].querySelector(".umds-segmented__count")).toHaveTextContent("31");
|
|
17
|
+
expect(itens[1].querySelector(".umds-segmented__count")).toHaveTextContent("0");
|
|
18
|
+
expect(itens[2].querySelector(".umds-segmented__count")).toBeNull();
|
|
19
|
+
expect(itens[3].querySelector(".umds-segmented__count")).toBeNull();
|
|
20
|
+
const conteudo = itens[0].querySelector(".umds-segmented__conteudo");
|
|
21
|
+
expect(conteudo.lastElementChild).toHaveClass("umds-segmented__count");
|
|
22
|
+
});
|
|
23
|
+
it("o contador entra no nome acessível do segmento (o texto visível manda)", () => {
|
|
24
|
+
render(_jsx(SegmentedControl, { label: "Fila", id: "fila", options: FILA }));
|
|
25
|
+
expect(screen.getByRole("radio", { name: /Todas\s*31/ })).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
it("o contador acompanha a escolha e a escolha segue funcionando", async () => {
|
|
28
|
+
const usuario = userEvent.setup();
|
|
29
|
+
const { container } = render(_jsx(SegmentedControl, { label: "Fila", options: FILA, defaultValue: "todas" }));
|
|
30
|
+
expect(screen.getByRole("radio", { name: /Todas/ })).toBeChecked();
|
|
31
|
+
await usuario.click(screen.getByText("Minhas"));
|
|
32
|
+
expect(screen.getByRole("radio", { name: /Minhas/ })).toBeChecked();
|
|
33
|
+
expect(container.querySelectorAll(".umds-segmented__count")).toHaveLength(2);
|
|
34
|
+
});
|
|
35
|
+
it("sem count em nenhum segmento, nada muda", () => {
|
|
36
|
+
const { container } = render(_jsx(SegmentedControl, { label: "x", options: [{ label: "25", value: "25" }] }));
|
|
37
|
+
expect(container.querySelector(".umds-segmented__count")).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import SegmentedControl from "../components/SegmentedControl/SegmentedControl";
|
|
4
|
+
import { render } from "@testing-library/react";
|
|
5
|
+
import { lerArquivo } from "./lerArquivo";
|
|
6
|
+
const cssSegmentedControl = await lerArquivo("../core/src/components/SegmentedControl/SegmentedControl.css");
|
|
7
|
+
const CSS = { SegmentedControl: cssSegmentedControl };
|
|
8
|
+
const css = (componente) => CSS[componente].replace(/\r\n/g, "\n");
|
|
9
|
+
const bloco = (texto, seletor) => {
|
|
10
|
+
const inicio = texto.indexOf(seletor + " {");
|
|
11
|
+
expect(inicio, seletor).toBeGreaterThanOrEqual(0);
|
|
12
|
+
return texto.slice(inicio, texto.indexOf("}", inicio));
|
|
13
|
+
};
|
|
14
|
+
describe("SegmentedControl · variant surface", () => {
|
|
15
|
+
const OPCOES = [
|
|
16
|
+
{ label: "Individual", value: "individual", count: 12 },
|
|
17
|
+
{ label: "Em lote", value: "lote" },
|
|
18
|
+
];
|
|
19
|
+
it("surface escreve a classe; sem a prop e com default, não", () => {
|
|
20
|
+
const { container } = render(_jsxs(_Fragment, { children: [_jsx(SegmentedControl, { options: OPCOES, variant: "surface", defaultValue: "individual" }), _jsx(SegmentedControl, { options: OPCOES, defaultValue: "individual" }), _jsx(SegmentedControl, { options: OPCOES, variant: "default", defaultValue: "individual" })] }));
|
|
21
|
+
expect(container.children[0]).toHaveClass("umds-segmented--surface");
|
|
22
|
+
expect(container.children[1]).not.toHaveClass("umds-segmented--surface");
|
|
23
|
+
expect(container.children[2]).not.toHaveClass("umds-segmented--surface");
|
|
24
|
+
});
|
|
25
|
+
it("CSS: o escolhido é branco, em negrito e com sombra; o trilho é cinza claro; o azul cheio do padrão segue", () => {
|
|
26
|
+
const t = css("SegmentedControl");
|
|
27
|
+
const escolhido = bloco(t, ".umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item,\n.umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item:hover");
|
|
28
|
+
expect(escolhido).toMatch(/background-color:\s*var\(--umds-semantic-color-background-white\)/);
|
|
29
|
+
expect(escolhido).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-bold\)/);
|
|
30
|
+
expect(escolhido).toMatch(/box-shadow:\s*var\(--umds-semantic-shadow-md\)/);
|
|
31
|
+
expect(bloco(t, ".umds-segmented--surface .umds-segmented__track")).toMatch(/background-color:\s*var\(--umds-semantic-color-neutral-light-medium\)/);
|
|
32
|
+
expect(t).toMatch(/\.umds-segmented__input:checked \+ \.umds-segmented__item,\n\.umds-segmented__input:checked \+ \.umds-segmented__item:hover \{\n\s*background-color:\s*var\(--umds-semantic-color-primary-pure\)/);
|
|
33
|
+
});
|
|
34
|
+
it("o contador do escolhido fica cinza sobre o branco e o dos outros, branco sobre o trilho", () => {
|
|
35
|
+
const t = css("SegmentedControl");
|
|
36
|
+
expect(bloco(t, ".umds-segmented--surface .umds-segmented__count")).toMatch(/background-color:\s*var\(--umds-semantic-color-background-white\)/);
|
|
37
|
+
expect(bloco(t, ".umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item .umds-segmented__count")).toMatch(/background-color:\s*var\(--umds-semantic-color-neutral-light-medium\)/);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
|
5
|
+
import Select from "../components/Select/Select";
|
|
6
|
+
const OPCOES = [
|
|
7
|
+
{ label: "Urgente", value: "urgente" },
|
|
8
|
+
{ label: "Revisão", value: "revisao" },
|
|
9
|
+
];
|
|
10
|
+
afterEach(() => vi.restoreAllMocks());
|
|
11
|
+
describe("Select · open e onOpenChange", () => {
|
|
12
|
+
it("open controlado mostra a lista sem clique; fechado, não mostra", () => {
|
|
13
|
+
const { rerender } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: false }));
|
|
14
|
+
expect(screen.queryByRole("listbox")).toBeNull();
|
|
15
|
+
rerender(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: true }));
|
|
16
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "true");
|
|
18
|
+
});
|
|
19
|
+
it("controlado: o clique só avisa; quem abre é o pai", () => {
|
|
20
|
+
const aoAbrir = vi.fn();
|
|
21
|
+
const { rerender } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: false, onOpenChange: aoAbrir }));
|
|
22
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
23
|
+
expect(aoAbrir).toHaveBeenCalledWith(true);
|
|
24
|
+
expect(screen.queryByRole("listbox")).toBeNull();
|
|
25
|
+
rerender(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: true, onOpenChange: aoAbrir }));
|
|
26
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it("controlado aberto: escolher uma opção, Esc e clique fora pedem para fechar", () => {
|
|
29
|
+
const aoAbrir = vi.fn();
|
|
30
|
+
const { rerender } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: true, onOpenChange: aoAbrir }));
|
|
31
|
+
fireEvent.click(screen.getAllByRole("option")[0]);
|
|
32
|
+
expect(aoAbrir).toHaveBeenLastCalledWith(false);
|
|
33
|
+
aoAbrir.mockClear();
|
|
34
|
+
rerender(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: true, onOpenChange: aoAbrir }));
|
|
35
|
+
fireEvent.keyDown(screen.getByRole("combobox"), { key: "Escape" });
|
|
36
|
+
expect(aoAbrir).toHaveBeenLastCalledWith(false);
|
|
37
|
+
aoAbrir.mockClear();
|
|
38
|
+
fireEvent.mouseDown(document.body);
|
|
39
|
+
expect(aoAbrir).toHaveBeenCalledWith(false);
|
|
40
|
+
});
|
|
41
|
+
it("o clique fora não avisa quando a lista está fechada", () => {
|
|
42
|
+
const aoAbrir = vi.fn();
|
|
43
|
+
render(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: false, onOpenChange: aoAbrir }));
|
|
44
|
+
fireEvent.mouseDown(document.body);
|
|
45
|
+
expect(aoAbrir).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
it("clique fora vê o open de agora, não o do primeiro render", () => {
|
|
48
|
+
const aoAbrir = vi.fn();
|
|
49
|
+
const { rerender } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: false, onOpenChange: aoAbrir }));
|
|
50
|
+
rerender(_jsx(Select, { ariaLabel: "x", options: OPCOES, open: true, onOpenChange: aoAbrir }));
|
|
51
|
+
fireEvent.mouseDown(document.body);
|
|
52
|
+
expect(aoAbrir).toHaveBeenCalledWith(false);
|
|
53
|
+
});
|
|
54
|
+
it("sem open, o Select abre e fecha sozinho e também avisa", () => {
|
|
55
|
+
const aoAbrir = vi.fn();
|
|
56
|
+
render(_jsx(Select, { ariaLabel: "x", options: OPCOES, onOpenChange: aoAbrir }));
|
|
57
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
58
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
59
|
+
expect(aoAbrir).toHaveBeenLastCalledWith(true);
|
|
60
|
+
fireEvent.click(screen.getAllByRole("option")[1]);
|
|
61
|
+
expect(screen.queryByRole("listbox")).toBeNull();
|
|
62
|
+
expect(aoAbrir).toHaveBeenLastCalledWith(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("Select · gatilho com ícone, amostra e aria-required", () => {
|
|
66
|
+
it("triggerIcon e triggerSwatch aparecem no gatilho, decorativos", () => {
|
|
67
|
+
const { container } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, triggerIcon: faFilter, triggerSwatch: "var(--umds-semantic-color-primary-pure)" }));
|
|
68
|
+
const icone = container.querySelector(".umds-select__trigger-icon");
|
|
69
|
+
expect(icone).toHaveAttribute("aria-hidden", "true");
|
|
70
|
+
expect(icone.querySelector("svg")).toHaveAttribute("data-icon", "filter");
|
|
71
|
+
const amostra = container.querySelector(".umds-select__swatch");
|
|
72
|
+
expect(amostra).toHaveAttribute("aria-hidden", "true");
|
|
73
|
+
expect(amostra.style.background).toContain("--umds-semantic-color-primary-pure");
|
|
74
|
+
expect(container.querySelector(".umds-select--has-trigger-icon")).not.toBeNull();
|
|
75
|
+
});
|
|
76
|
+
it("sem as props, não há ícone nem amostra", () => {
|
|
77
|
+
const { container } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES }));
|
|
78
|
+
expect(container.querySelector(".umds-select__trigger-icon")).toBeNull();
|
|
79
|
+
expect(container.querySelector(".umds-select__swatch")).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
it("required repassa aria-required ao combobox", () => {
|
|
82
|
+
const { rerender } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, required: true }));
|
|
83
|
+
expect(screen.getByRole("combobox")).toHaveAttribute("aria-required", "true");
|
|
84
|
+
rerender(_jsx(Select, { ariaLabel: "x", options: OPCOES }));
|
|
85
|
+
expect(screen.getByRole("combobox")).not.toHaveAttribute("aria-required");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("Select · variantes de filtro", () => {
|
|
89
|
+
it("search e combo escrevem a classe do filtro e a própria", () => {
|
|
90
|
+
const { container, rerender } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, variant: "search" }));
|
|
91
|
+
expect(container.querySelector(".umds-select--filter.umds-select--filter-search")).not.toBeNull();
|
|
92
|
+
rerender(_jsx(Select, { ariaLabel: "x", options: OPCOES, variant: "combo" }));
|
|
93
|
+
expect(container.querySelector(".umds-select--filter.umds-select--filter-combo")).not.toBeNull();
|
|
94
|
+
expect(container.querySelector(".umds-select--filter-search")).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it("search foca a busca ao abrir, como o filter", async () => {
|
|
97
|
+
render(_jsx(Select, { ariaLabel: "x", options: OPCOES, variant: "search", searchable: true }));
|
|
98
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
99
|
+
await waitFor(() => expect(document.activeElement).toBe(screen.getByRole("textbox")));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("Select · por cima do diálogo", () => {
|
|
103
|
+
it("dentro de role=dialog a lista vira fixed, ancorada no gatilho", async () => {
|
|
104
|
+
const { container } = render(_jsx("div", { role: "dialog", "aria-label": "Filtros", style: { overflow: "clip" }, children: _jsx(Select, { ariaLabel: "x", options: OPCOES }) }));
|
|
105
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
106
|
+
const lista = container.querySelector(".umds-select__dropdown");
|
|
107
|
+
expect(container.querySelector(".umds-select--floating")).not.toBeNull();
|
|
108
|
+
expect(lista.style.position).toBe("fixed");
|
|
109
|
+
await waitFor(() => expect(lista.style.visibility).toBe("visible"));
|
|
110
|
+
});
|
|
111
|
+
it("fora de diálogo a lista continua posicionada pelo CSS (sem fixed)", () => {
|
|
112
|
+
const { container } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES }));
|
|
113
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
114
|
+
const lista = container.querySelector(".umds-select__dropdown");
|
|
115
|
+
expect(container.querySelector(".umds-select--floating")).toBeNull();
|
|
116
|
+
expect(lista.style.position).toBe("");
|
|
117
|
+
});
|
|
118
|
+
it("aria-modal também conta como diálogo", () => {
|
|
119
|
+
const { container } = render(_jsx("div", { "aria-modal": "true", children: _jsx(Select, { ariaLabel: "x", options: OPCOES }) }));
|
|
120
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
121
|
+
expect(container.querySelector(".umds-select--floating")).not.toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe("Select · placement auto mede o ancestral que rola", () => {
|
|
125
|
+
it("abre para cima e limita a altura da lista ao espaço do ancestral", () => {
|
|
126
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function () {
|
|
127
|
+
const rolante = this.getAttribute("data-rolante") !== null;
|
|
128
|
+
const [topo, base] = rolante ? [100, 300] : [200, 244];
|
|
129
|
+
return { top: topo, bottom: base, left: 0, right: 0, width: 0, height: base - topo, x: 0, y: topo, toJSON() { } };
|
|
130
|
+
});
|
|
131
|
+
Object.defineProperty(window, "innerHeight", { value: 900, configurable: true });
|
|
132
|
+
const { container } = render(_jsx("div", { "data-rolante": true, style: { overflowY: "auto" }, children: _jsx(Select, { ariaLabel: "x", options: OPCOES, placement: "auto" }) }));
|
|
133
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
134
|
+
expect(container.querySelector(".umds-select--placement-top")).not.toBeNull();
|
|
135
|
+
expect(container.querySelector(".umds-select__dropdown").style.maxHeight).toBe("96px");
|
|
136
|
+
});
|
|
137
|
+
it("sem ancestral que corta, só a janela conta e a lista não ganha teto", () => {
|
|
138
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(() => ({ top: 20, bottom: 64, left: 0, right: 0, width: 0, height: 44, x: 0, y: 20, toJSON() { } }));
|
|
139
|
+
Object.defineProperty(window, "innerHeight", { value: 900, configurable: true });
|
|
140
|
+
const { container } = render(_jsx(Select, { ariaLabel: "x", options: OPCOES, placement: "auto" }));
|
|
141
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
142
|
+
expect(container.querySelector(".umds-select--placement-top")).toBeNull();
|
|
143
|
+
expect(container.querySelector(".umds-select__dropdown").style.maxHeight).toBe("");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|