.gitignore and v0.2.0

This commit is contained in:
2026-08-12 03:44:07 -06:00
parent 26851fa750
commit 8f009bb1ed
22 changed files with 1175 additions and 25 deletions
+123
View File
@@ -0,0 +1,123 @@
import { fireEvent, render } from "@testing-library/vue";
import { describe, expect, it } from "vitest";
import DsLogicTree from "../src/components/data/DsLogicTree.vue";
import DsSortableList from "../src/components/data/DsSortableList.vue";
import type { DsLogicGroup, DsSortableItem } from "../src/types";
const sortableItems: DsSortableItem[] = [
{ id: "one", label: "Primero" },
{ id: "two", label: "Segundo" },
{ id: "three", label: "Tercero" },
];
const emptyTree: DsLogicGroup = {
id: "root",
type: "group",
operator: "and",
children: [],
};
const fields = [
{ label: "Estado", value: "status" },
{ label: "Plan", value: "plan" },
];
describe("data tools", () => {
it("reorders items with drag-and-drop and emits the movement", async () => {
const view = render(DsSortableList, {
props: { modelValue: sortableItems, label: "Prioridades" },
});
const items = view.getAllByRole("listitem");
await fireEvent.dragStart(items[0] as HTMLElement);
await fireEvent.dragOver(items[2] as HTMLElement);
await fireEvent.drop(items[2] as HTMLElement);
const updates = view.emitted()["update:modelValue"] as
unknown[][] | undefined;
const reordered = updates?.at(-1)?.[0] as DsSortableItem[] | undefined;
expect(reordered?.map((item) => item.id)).toEqual(["two", "three", "one"]);
expect(view.emitted().reorder?.at(-1)).toEqual([
expect.objectContaining({ from: 0, to: 2 }),
]);
});
it("offers buttons as an accessible alternative to dragging", async () => {
const view = render(DsSortableList, {
props: { modelValue: sortableItems, label: "Prioridades" },
});
await fireEvent.click(
view.getByRole("button", { name: "Mover abajo: Primero" }),
);
const updates = view.emitted()["update:modelValue"] as
unknown[][] | undefined;
const reordered = updates?.at(-1)?.[0] as DsSortableItem[] | undefined;
expect(reordered?.map((item) => item.id)).toEqual(["two", "one", "three"]);
expect(view.getByRole("status").textContent).toContain(
"Primero, movido a la posición 2 de 3.",
);
});
it("adds a serializable condition to a logic tree", async () => {
const view = render(DsLogicTree, {
props: { modelValue: emptyTree, fields },
});
await fireEvent.click(
view.getByRole("button", { name: "Añadir condición" }),
);
const updates = view.emitted()["update:modelValue"] as
unknown[][] | undefined;
const tree = updates?.at(-1)?.[0] as DsLogicGroup | undefined;
expect(tree?.children).toHaveLength(1);
expect(tree?.children[0]).toEqual(
expect.objectContaining({
type: "condition",
field: "status",
comparison: "equals",
value: "",
}),
);
expect(view.emitted().change?.at(-1)).toEqual([tree]);
});
it("updates operators and removes nested rules", async () => {
const tree: DsLogicGroup = {
...emptyTree,
children: [
{
id: "status",
type: "condition",
field: "status",
comparison: "equals",
value: "active",
},
],
};
const operatorView = render(DsLogicTree, {
props: { modelValue: tree, fields },
});
await fireEvent.update(
operatorView.getByLabelText("Operador lógico"),
"or",
);
const operatorUpdates = operatorView.emitted()["update:modelValue"] as
unknown[][] | undefined;
expect(
(operatorUpdates?.at(-1)?.[0] as DsLogicGroup | undefined)?.operator,
).toBe("or");
operatorView.unmount();
const removeView = render(DsLogicTree, {
props: { modelValue: tree, fields },
});
await fireEvent.click(removeView.getByRole("button", { name: "Eliminar" }));
const removeUpdates = removeView.emitted()["update:modelValue"] as
unknown[][] | undefined;
const updated = removeUpdates?.at(-1)?.[0] as DsLogicGroup | undefined;
expect(updated?.children).toEqual([]);
});
});
+41
View File
@@ -1,6 +1,8 @@
import { fireEvent, render } from "@testing-library/vue";
import { h } from "vue";
import { describe, expect, it } from "vitest";
import DsCheckbox from "../src/components/forms/DsCheckbox.vue";
import DsRadioGroup from "../src/components/forms/DsRadioGroup.vue";
import DsTextInput from "../src/components/forms/DsTextInput.vue";
describe("form controls", () => {
@@ -34,4 +36,43 @@ describe("form controls", () => {
true,
);
});
it("associates checkbox descriptions and errors at the same time", () => {
const view = render(DsCheckbox, {
props: {
label: "Acepto",
description: "Lee las condiciones.",
error: "Debes aceptar.",
},
});
const checkbox = view.getByRole("checkbox");
const describedBy = checkbox.getAttribute("aria-describedby") ?? "";
expect(describedBy).toContain("description");
expect(describedBy).toContain("error");
expect(checkbox.getAttribute("aria-invalid")).toBe("true");
});
it("gives radio groups unique descriptions and exposes invalid state", () => {
const props = {
legend: "Plan",
name: "plan",
options: [{ label: "Profesional", value: "pro" }],
description: "Elige un plan.",
error: "Selecciona una opción.",
};
const view = render({
setup: () => () =>
h("div", [h(DsRadioGroup, props), h(DsRadioGroup, props)]),
});
const groups = view.getAllByRole("group");
const describedBy = groups.map((group) =>
group.getAttribute("aria-describedby"),
);
expect(describedBy[0]).not.toBe(describedBy[1]);
expect(describedBy[0]).toContain("description");
expect(describedBy[0]).toContain("error");
expect(view.getAllByRole("radio")[0]?.getAttribute("aria-invalid")).toBe(
"true",
);
});
});
+2
View File
@@ -7,6 +7,8 @@ describe("public API", () => {
expect(designSystem.DsTextInput).toBeDefined();
expect(designSystem.DsTabs).toBeDefined();
expect(designSystem.DsDialog).toBeDefined();
expect(designSystem.DsSortableList).toBeDefined();
expect(designSystem.DsLogicTree).toBeDefined();
expect(designSystem.DesignSystem).toBeDefined();
});
});
+23
View File
@@ -1,5 +1,6 @@
import { fireEvent, render } from "@testing-library/vue";
import { describe, expect, it } from "vitest";
import { h } from "vue";
import DsTabs from "../src/components/navigation/DsTabs.vue";
describe("DsTabs", () => {
@@ -29,4 +30,26 @@ describe("DsTabs", () => {
});
expect(view.emitted()["update:modelValue"]?.at(-1)).toEqual(["billing"]);
});
it("keeps tab and panel relationships unique across instances", () => {
const props = {
label: "Cuenta",
modelValue: "profile",
items: [{ id: "profile", label: "Perfil" }],
};
const view = render({
setup: () => () =>
h("div", [
h(DsTabs, props, { profile: () => "Perfil" }),
h(DsTabs, props, { profile: () => "Perfil" }),
]),
});
const [firstTab, secondTab] = view.getAllByRole("tab");
const [firstPanel, secondPanel] = view.getAllByRole("tabpanel");
expect(firstTab?.id).not.toBe(secondTab?.id);
expect(firstPanel?.id).not.toBe(secondPanel?.id);
expect(firstTab?.getAttribute("aria-controls")).toBe(firstPanel?.id);
expect(firstPanel?.getAttribute("aria-labelledby")).toBe(firstTab?.id);
});
});