This commit is contained in:
91
frontend/__tests__/components/ui/form/Checkbox.spec.tsx
Normal file
91
frontend/__tests__/components/ui/form/Checkbox.spec.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { render, screen, fireEvent } from "@solidjs/testing-library";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { Checkbox } from "../../../../src/ts/components/ui/form/Checkbox";
|
||||
|
||||
function makeField(name: string, checked = false) {
|
||||
return {
|
||||
name,
|
||||
state: { value: checked },
|
||||
handleBlur: vi.fn(),
|
||||
handleChange: vi.fn(),
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("Checkbox", () => {
|
||||
it("renders with label text", () => {
|
||||
const field = makeField("agree");
|
||||
render(() => <Checkbox field={() => field} label="I agree" />);
|
||||
|
||||
expect(screen.getByText("I agree")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders checkbox with field name", () => {
|
||||
const field = makeField("terms");
|
||||
render(() => <Checkbox field={() => field} />);
|
||||
|
||||
const input = screen.getByRole("checkbox", { hidden: true });
|
||||
expect(input).toHaveAttribute("id", "terms");
|
||||
expect(input).toHaveAttribute("name", "terms");
|
||||
});
|
||||
|
||||
it("reflects checked state", () => {
|
||||
const field = makeField("opt", true);
|
||||
render(() => <Checkbox field={() => field} />);
|
||||
|
||||
const input = screen.getByRole("checkbox", { hidden: true });
|
||||
expect(input).toBeChecked();
|
||||
});
|
||||
|
||||
it("reflects unchecked state", () => {
|
||||
const field = makeField("opt", false);
|
||||
render(() => <Checkbox field={() => field} />);
|
||||
|
||||
const input = screen.getByRole("checkbox", { hidden: true });
|
||||
expect(input).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("calls handleChange on change", async () => {
|
||||
const field = makeField("opt");
|
||||
render(() => <Checkbox field={() => field} />);
|
||||
|
||||
const input = screen.getByRole("checkbox", { hidden: true });
|
||||
await fireEvent.change(input, { target: { checked: true } });
|
||||
expect(field.handleChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("calls handleBlur on blur", async () => {
|
||||
const field = makeField("opt");
|
||||
render(() => <Checkbox field={() => field} />);
|
||||
|
||||
const input = screen.getByRole("checkbox", { hidden: true });
|
||||
await fireEvent.blur(input);
|
||||
expect(field.handleBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders disabled checkbox", () => {
|
||||
const field = makeField("opt");
|
||||
render(() => <Checkbox field={() => field} disabled />);
|
||||
|
||||
const input = screen.getByRole("checkbox", { hidden: true });
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows check icon styling when checked", () => {
|
||||
const field = makeField("opt", true);
|
||||
const { container } = render(() => <Checkbox field={() => field} />);
|
||||
|
||||
const icon = container.querySelector(".fa-check");
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(icon).toHaveClass("text-main");
|
||||
});
|
||||
|
||||
it("shows transparent icon styling when unchecked", () => {
|
||||
const field = makeField("opt", false);
|
||||
const { container } = render(() => <Checkbox field={() => field} />);
|
||||
|
||||
const icon = container.querySelector(".fa-check");
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(icon).toHaveClass("text-transparent");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { render } from "@solidjs/testing-library";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { FieldIndicator } from "../../../../src/ts/components/ui/form/FieldIndicator";
|
||||
|
||||
function makeField(overrides: {
|
||||
isValidating?: boolean;
|
||||
isTouched?: boolean;
|
||||
isValid?: boolean;
|
||||
isDefaultValue?: boolean;
|
||||
errors?: string[];
|
||||
hasWarning?: boolean;
|
||||
warnings?: string[];
|
||||
}) {
|
||||
return {
|
||||
state: {
|
||||
meta: {
|
||||
isValidating: overrides.isValidating ?? false,
|
||||
isTouched: overrides.isTouched ?? false,
|
||||
isValid: overrides.isValid ?? true,
|
||||
isDefaultValue: overrides.isDefaultValue ?? true,
|
||||
errors: overrides.errors ?? [],
|
||||
},
|
||||
},
|
||||
getMeta: () => ({
|
||||
hasWarning: overrides.hasWarning ?? false,
|
||||
warnings: overrides.warnings ?? [],
|
||||
}),
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("FieldIndicator", () => {
|
||||
it("shows loading spinner when validating", () => {
|
||||
const { container } = render(() => (
|
||||
<FieldIndicator field={makeField({ isValidating: true })} />
|
||||
));
|
||||
expect(container.querySelector(".fa-circle-notch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows error icon when touched and invalid", () => {
|
||||
const { container } = render(() => (
|
||||
<FieldIndicator
|
||||
field={makeField({
|
||||
isTouched: true,
|
||||
isValid: false,
|
||||
errors: ["bad value"],
|
||||
})}
|
||||
/>
|
||||
));
|
||||
expect(container.querySelector(".fa-times")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows warning icon when has warning", () => {
|
||||
const { container } = render(() => (
|
||||
<FieldIndicator
|
||||
field={makeField({
|
||||
hasWarning: true,
|
||||
warnings: ["weak"],
|
||||
})}
|
||||
/>
|
||||
));
|
||||
expect(
|
||||
container.querySelector(".fa-exclamation-triangle"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows success check when touched, valid, and not default", () => {
|
||||
const { container } = render(() => (
|
||||
<FieldIndicator
|
||||
field={makeField({
|
||||
isTouched: true,
|
||||
isValid: true,
|
||||
isDefaultValue: false,
|
||||
})}
|
||||
/>
|
||||
));
|
||||
expect(container.querySelector(".fa-check")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows nothing when untouched and not validating", () => {
|
||||
const { container } = render(() => (
|
||||
<FieldIndicator field={makeField({})} />
|
||||
));
|
||||
expect(container.querySelector(".fa-times")).not.toBeInTheDocument();
|
||||
expect(container.querySelector(".fa-check")).not.toBeInTheDocument();
|
||||
expect(container.querySelector(".fa-circle-notch")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
102
frontend/__tests__/components/ui/form/InputField.spec.tsx
Normal file
102
frontend/__tests__/components/ui/form/InputField.spec.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { render, screen, fireEvent } from "@solidjs/testing-library";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { InputField } from "../../../../src/ts/components/ui/form/InputField";
|
||||
|
||||
function makeField(name: string, value = "") {
|
||||
return {
|
||||
name,
|
||||
state: {
|
||||
value,
|
||||
meta: {
|
||||
isValidating: false,
|
||||
isTouched: false,
|
||||
isValid: true,
|
||||
isDefaultValue: true,
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
handleBlur: vi.fn(),
|
||||
handleChange: vi.fn(),
|
||||
getMeta: () => ({ hasWarning: false, warnings: [] }),
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("InputField", () => {
|
||||
it("uses custom placeholder when provided", () => {
|
||||
const field = makeField("email");
|
||||
render(() => <InputField field={() => field} placeholder="Enter email" />);
|
||||
|
||||
expect(screen.getByPlaceholderText("Enter email")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("defaults to text type", () => {
|
||||
const field = makeField("name");
|
||||
render(() => <InputField field={() => field} />);
|
||||
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("type", "text");
|
||||
});
|
||||
|
||||
it("uses custom type", () => {
|
||||
const field = makeField("password");
|
||||
const { container } = render(() => (
|
||||
<InputField field={() => field} type="password" />
|
||||
));
|
||||
|
||||
expect(container.querySelector("input")).toHaveAttribute(
|
||||
"type",
|
||||
"password",
|
||||
);
|
||||
});
|
||||
|
||||
it("calls handleChange on input", async () => {
|
||||
const field = makeField("name");
|
||||
render(() => <InputField field={() => field} />);
|
||||
|
||||
await fireEvent.input(screen.getByRole("textbox"), {
|
||||
target: { value: "test" },
|
||||
});
|
||||
expect(field.handleChange).toHaveBeenCalledWith("test");
|
||||
});
|
||||
|
||||
it("calls handleBlur on blur", async () => {
|
||||
const field = makeField("name");
|
||||
render(() => <InputField field={() => field} />);
|
||||
|
||||
await fireEvent.blur(screen.getByRole("textbox"));
|
||||
expect(field.handleBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onFocus callback", async () => {
|
||||
const field = makeField("name");
|
||||
const onFocus = vi.fn();
|
||||
render(() => <InputField field={() => field} onFocus={onFocus} />);
|
||||
|
||||
await fireEvent.focus(screen.getByRole("textbox"));
|
||||
expect(onFocus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders disabled input", () => {
|
||||
const field = makeField("name");
|
||||
render(() => <InputField field={() => field} disabled />);
|
||||
|
||||
expect(screen.getByRole("textbox")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows FieldIndicator when showIndicator is true", () => {
|
||||
const field = makeField("name");
|
||||
field.state.meta.isValidating = true;
|
||||
const { container } = render(() => (
|
||||
<InputField field={() => field} showIndicator />
|
||||
));
|
||||
|
||||
expect(container.querySelector(".fa-circle-notch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides FieldIndicator by default", () => {
|
||||
const field = makeField("name");
|
||||
const { container } = render(() => <InputField field={() => field} />);
|
||||
|
||||
expect(container.querySelector(".fa-circle-notch")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
57
frontend/__tests__/components/ui/form/LabeledField.spec.tsx
Normal file
57
frontend/__tests__/components/ui/form/LabeledField.spec.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { render, screen } from "@solidjs/testing-library";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { LabeledField } from "../../../../src/ts/components/ui/form/LabeledField";
|
||||
|
||||
describe("LabeledField", () => {
|
||||
it("renders label text correctly", () => {
|
||||
render(() => (
|
||||
<LabeledField label="test label">
|
||||
<input />
|
||||
</LabeledField>
|
||||
));
|
||||
|
||||
expect(screen.getByText("test label")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders children correctly", () => {
|
||||
render(() => (
|
||||
<LabeledField label="test">
|
||||
<div data-testid="child">child content</div>
|
||||
</LabeledField>
|
||||
));
|
||||
|
||||
expect(screen.getByTestId("child")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders subtext when provided", () => {
|
||||
render(() => (
|
||||
<LabeledField label="test" subLabel="helper text">
|
||||
<input />
|
||||
</LabeledField>
|
||||
));
|
||||
|
||||
expect(screen.getByText("helper text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("links label to input when id is provided", () => {
|
||||
const { container } = render(() => (
|
||||
<LabeledField label="test" id="test-id">
|
||||
<input id="test-id" />
|
||||
</LabeledField>
|
||||
));
|
||||
|
||||
const label = container.querySelector("label");
|
||||
expect(label).toHaveAttribute("for", "test-id");
|
||||
});
|
||||
|
||||
it("applies custom class to wrapper", () => {
|
||||
const { container } = render(() => (
|
||||
<LabeledField label="test" class="custom-wrapper-class">
|
||||
<input />
|
||||
</LabeledField>
|
||||
));
|
||||
|
||||
expect(container.firstChild).toHaveClass("custom-wrapper-class");
|
||||
});
|
||||
});
|
||||
74
frontend/__tests__/components/ui/form/SubmitButton.spec.tsx
Normal file
74
frontend/__tests__/components/ui/form/SubmitButton.spec.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { render, screen } from "@solidjs/testing-library";
|
||||
import { JSXElement } from "solid-js";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { SubmitButton } from "../../../../src/ts/components/ui/form/SubmitButton";
|
||||
|
||||
type FormState = {
|
||||
canSubmit: boolean;
|
||||
isSubmitting: boolean;
|
||||
isValid: boolean;
|
||||
isDirty: boolean;
|
||||
};
|
||||
|
||||
function makeForm(state: Partial<FormState> = {}) {
|
||||
const fullState: FormState = {
|
||||
canSubmit: true,
|
||||
isSubmitting: false,
|
||||
isValid: true,
|
||||
isDirty: true,
|
||||
...state,
|
||||
};
|
||||
|
||||
return {
|
||||
Subscribe: (props: {
|
||||
selector: (state: FormState) => FormState;
|
||||
children: (state: () => FormState) => JSXElement;
|
||||
}) => props.children(() => props.selector(fullState)),
|
||||
};
|
||||
}
|
||||
|
||||
describe("SubmitButton", () => {
|
||||
it("renders enabled when form is dirty, valid, and can submit", () => {
|
||||
render(() => <SubmitButton form={makeForm()} text="Save" />);
|
||||
expect(screen.getByRole("button", { name: "Save" })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders as submit type", () => {
|
||||
render(() => <SubmitButton form={makeForm()} text="Save" />);
|
||||
expect(screen.getByRole("button")).toHaveAttribute("type", "submit");
|
||||
});
|
||||
|
||||
it("disables when form is not dirty", () => {
|
||||
render(() => (
|
||||
<SubmitButton form={makeForm({ isDirty: false })} text="Save" />
|
||||
));
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables when form cannot submit", () => {
|
||||
render(() => (
|
||||
<SubmitButton form={makeForm({ canSubmit: false })} text="Save" />
|
||||
));
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables when form is submitting", () => {
|
||||
render(() => (
|
||||
<SubmitButton form={makeForm({ isSubmitting: true })} text="Save" />
|
||||
));
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables when form is not valid", () => {
|
||||
render(() => (
|
||||
<SubmitButton form={makeForm({ isValid: false })} text="Save" />
|
||||
));
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables when disabled prop is true even if form is ready", () => {
|
||||
render(() => <SubmitButton form={makeForm()} text="Save" disabled />);
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
133
frontend/__tests__/components/ui/form/utils.spec.ts
Normal file
133
frontend/__tests__/components/ui/form/utils.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
fromSchema,
|
||||
handleResult,
|
||||
allFieldsMandatory,
|
||||
fieldMandatory,
|
||||
type ValidationResult,
|
||||
} from "../../../../src/ts/components/ui/form/utils";
|
||||
|
||||
describe("fromSchema", () => {
|
||||
const schema = z.string().min(3, "too short").max(10, "too long");
|
||||
const validate = fromSchema(schema);
|
||||
|
||||
it("returns undefined for valid value", () => {
|
||||
expect(validate({ value: "hello" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns error messages for invalid value", () => {
|
||||
expect(validate({ value: "ab" })).toEqual(["too short"]);
|
||||
});
|
||||
|
||||
it("returns multiple error messages", () => {
|
||||
const numSchema = z.number().min(5, "too small").max(3, "too big");
|
||||
const v = fromSchema(numSchema);
|
||||
const result = v({ value: 4 });
|
||||
// 4 fails min(5) but passes max(3)? Actually 4 > 3, so both fail
|
||||
// number 4: min(5) fails, max(3) fails
|
||||
expect(result).toEqual(["too small", "too big"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleResult", () => {
|
||||
const mockSetMeta = vi.fn();
|
||||
|
||||
function makeField() {
|
||||
mockSetMeta.mockClear();
|
||||
return { setMeta: mockSetMeta } as any;
|
||||
}
|
||||
|
||||
it("returns undefined for undefined results", () => {
|
||||
expect(handleResult(makeField(), undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for empty results", () => {
|
||||
expect(handleResult(makeField(), [])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns error messages and ignores warnings", () => {
|
||||
const results: ValidationResult[] = [
|
||||
{ type: "error", message: "bad email" },
|
||||
{ type: "error", message: "too short" },
|
||||
];
|
||||
expect(handleResult(makeField(), results)).toEqual([
|
||||
"bad email",
|
||||
"too short",
|
||||
]);
|
||||
});
|
||||
|
||||
it("sets warning meta on field", () => {
|
||||
const results: ValidationResult[] = [
|
||||
{ type: "warning", message: "weak password" },
|
||||
];
|
||||
const field = makeField();
|
||||
const result = handleResult(field, results);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockSetMeta).toHaveBeenCalledOnce();
|
||||
|
||||
const updater = mockSetMeta.mock.calls[0]![0];
|
||||
const newMeta = updater({ existing: true });
|
||||
expect(newMeta).toEqual({
|
||||
existing: true,
|
||||
hasWarning: true,
|
||||
warnings: ["weak password"],
|
||||
});
|
||||
});
|
||||
|
||||
it("handles both errors and warnings", () => {
|
||||
const results: ValidationResult[] = [
|
||||
{ type: "warning", message: "not recommended" },
|
||||
{ type: "error", message: "invalid" },
|
||||
];
|
||||
const field = makeField();
|
||||
const result = handleResult(field, results);
|
||||
|
||||
expect(mockSetMeta).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual(["invalid"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("allFieldsMandatory", () => {
|
||||
const validate = allFieldsMandatory<{ a: string; b: string }>();
|
||||
|
||||
it("returns undefined when all fields have values", () => {
|
||||
expect(validate({ value: { a: "x", b: "y" } })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns error when a field is empty string", () => {
|
||||
expect(validate({ value: { a: "x", b: "" } })).toBe(
|
||||
"all fields are mandatory",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns error when a field is undefined", () => {
|
||||
expect(validate({ value: { a: "x", b: undefined } as any })).toBe(
|
||||
"all fields are mandatory",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fieldMandatory", () => {
|
||||
it("returns undefined for non-empty value", () => {
|
||||
const validate = fieldMandatory();
|
||||
expect(validate({ value: "hello" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns default message for empty string", () => {
|
||||
const validate = fieldMandatory();
|
||||
expect(validate({ value: "" })).toBe("mandatory");
|
||||
});
|
||||
|
||||
it("returns default message for undefined", () => {
|
||||
const validate = fieldMandatory();
|
||||
expect(validate({ value: undefined })).toBe("mandatory");
|
||||
});
|
||||
|
||||
it("returns custom message", () => {
|
||||
const validate = fieldMandatory("required field");
|
||||
expect(validate({ value: "" })).toBe("required field");
|
||||
});
|
||||
});
|
||||
158
frontend/__tests__/components/ui/table/DataTable.spec.tsx
Normal file
158
frontend/__tests__/components/ui/table/DataTable.spec.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { render, screen, fireEvent } from "@solidjs/testing-library";
|
||||
import { createSignal } from "solid-js";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { DataTable } from "../../../../src/ts/components/ui/table/DataTable";
|
||||
|
||||
const [localStorage, setLocalStorage] = createSignal([]);
|
||||
vi.mock("../../../../src/ts/hooks/useLocalStorage", () => {
|
||||
return {
|
||||
useLocalStorage: () => {
|
||||
return [localStorage, setLocalStorage] as const;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const bpSignal = createSignal({
|
||||
xxs: true,
|
||||
sm: true,
|
||||
md: true,
|
||||
});
|
||||
|
||||
vi.mock("../../../../src/ts/states/breakpoints", () => ({
|
||||
bp: () => bpSignal[0](),
|
||||
}));
|
||||
|
||||
type Person = {
|
||||
name: string;
|
||||
age: number;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
id: "name",
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
cell: (info: any) => info.getValue(),
|
||||
meta: { maxBreakpoint: "sm" },
|
||||
},
|
||||
{
|
||||
id: "age",
|
||||
accessorKey: "age",
|
||||
header: "Age",
|
||||
cell: (info: any) => info.getValue(),
|
||||
meta: { breakpoint: "sm" },
|
||||
},
|
||||
];
|
||||
|
||||
const data: Person[] = [
|
||||
{ name: "Alice", age: 30 },
|
||||
{ name: "Bob", age: 20 },
|
||||
];
|
||||
|
||||
describe("DataTable", () => {
|
||||
beforeEach(() => {
|
||||
bpSignal[1]({
|
||||
xxs: true,
|
||||
sm: true,
|
||||
md: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders table headers and rows", () => {
|
||||
render(() => <DataTable id="people" columns={columns} data={data} />);
|
||||
|
||||
expect(screen.getByText("Name")).toBeInTheDocument();
|
||||
expect(screen.getByText("Age")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Alice")).toBeInTheDocument();
|
||||
expect(screen.getByText("Bob")).toBeInTheDocument();
|
||||
expect(screen.getByText("30")).toBeInTheDocument();
|
||||
expect(screen.getByText("20")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders fallback when there is no data", () => {
|
||||
render(() => (
|
||||
<DataTable
|
||||
id="empty"
|
||||
columns={columns}
|
||||
data={[]}
|
||||
fallback={<div>No data</div>}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText("No data")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sorts rows when clicking a sortable header", async () => {
|
||||
render(() => <DataTable id="sorting" columns={columns} data={data} />);
|
||||
|
||||
const ageHeaderButton = screen.getByRole("button", { name: "Age" });
|
||||
const ageHeaderCell = ageHeaderButton.closest("th");
|
||||
|
||||
// Initial
|
||||
expect(ageHeaderCell).toHaveAttribute("aria-sort", "none");
|
||||
expect(ageHeaderCell?.querySelector("i")).toHaveClass("fa-fw");
|
||||
|
||||
// Descending
|
||||
await fireEvent.click(ageHeaderButton);
|
||||
expect(ageHeaderCell).toHaveAttribute("aria-sort", "descending");
|
||||
expect(ageHeaderCell?.querySelector("i")).toHaveClass(
|
||||
"fa-sort-down",
|
||||
"fas",
|
||||
"fa-fw",
|
||||
);
|
||||
expect(localStorage()).toEqual([
|
||||
{
|
||||
desc: true,
|
||||
id: "age",
|
||||
},
|
||||
]);
|
||||
|
||||
let rows = screen.getAllByRole("row");
|
||||
expect(rows[1]).toHaveTextContent("Alice"); // age 30
|
||||
expect(rows[2]).toHaveTextContent("Bob"); // age 20
|
||||
|
||||
// Ascending
|
||||
await fireEvent.click(ageHeaderButton);
|
||||
expect(ageHeaderCell).toHaveAttribute("aria-sort", "ascending");
|
||||
expect(ageHeaderCell?.querySelector("i")).toHaveClass(
|
||||
"fa-sort-up",
|
||||
"fas",
|
||||
"fa-fw",
|
||||
);
|
||||
expect(localStorage()).toEqual([
|
||||
{
|
||||
desc: false,
|
||||
id: "age",
|
||||
},
|
||||
]);
|
||||
|
||||
rows = screen.getAllByRole("row");
|
||||
expect(rows[1]).toHaveTextContent("Bob");
|
||||
expect(rows[2]).toHaveTextContent("Alice");
|
||||
|
||||
//back to initial
|
||||
await fireEvent.click(ageHeaderButton);
|
||||
expect(ageHeaderCell).toHaveAttribute("aria-sort", "none");
|
||||
expect(localStorage()).toEqual([]);
|
||||
});
|
||||
|
||||
it("hides columns based on breakpoint visibility", () => {
|
||||
bpSignal[1]({
|
||||
xxs: true,
|
||||
sm: false,
|
||||
md: false,
|
||||
});
|
||||
|
||||
render(() => <DataTable id="breakpoints" columns={columns} data={data} />);
|
||||
const nameHeader = screen.getByRole("button", {
|
||||
name: "Name",
|
||||
}).parentElement;
|
||||
const ageHeader = screen.getByRole("button", { name: "Age" }).parentElement;
|
||||
|
||||
expect(nameHeader).not.toHaveClass("hidden");
|
||||
expect(nameHeader).toHaveClass("sm:hidden");
|
||||
expect(ageHeader).toHaveClass("hidden sm:table-cell");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user