Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"rules": {
"explicit-function-return-type": "off",
"no-explicit-any": "off",
"no-unsafe-assignment": "off"
"no-unsafe-assignment": "off",
"no-empty-function": "off"
Comment on lines +24 to +25
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling typescript/no-empty-function for the whole frontend makes it easy to miss real bugs (empty handlers in production code). If this is only needed for Storybook mocks, prefer file-level/inline disables in storybook/ instead of turning the rule off globally.

Suggested change
"no-unsafe-assignment": "off",
"no-empty-function": "off"
"no-unsafe-assignment": "off"

Copilot uses AI. Check for mistakes.
}
},
{
Expand Down
82 changes: 0 additions & 82 deletions frontend/__tests__/components/ui/ValidatedInput.spec.tsx

This file was deleted.

91 changes: 91 additions & 0 deletions frontend/__tests__/components/ui/form/Checkbox.spec.tsx
Original file line number Diff line number Diff line change
@@ -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");
});
});
88 changes: 88 additions & 0 deletions frontend/__tests__/components/ui/form/FieldIndicator.spec.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
118 changes: 118 additions & 0 deletions frontend/__tests__/components/ui/form/InputField.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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("renders input with field name as id", () => {
const field = makeField("email");
render(() => <InputField field={() => field} />);

const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("id", "email");
expect(input).toHaveAttribute("name", "email");
});

it("uses field name as default placeholder", () => {
const field = makeField("username");
render(() => <InputField field={() => field} />);

expect(screen.getByPlaceholderText("username")).toBeInTheDocument();
});

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();
});
});
Loading
Loading