fix: label remaining website and picture inputs (#3424)

* fix: connect remaining website and picture labels to inputs

* test(builder): use realistic website input events
This commit is contained in:
Amruth Pillai
2026-09-05 08:51:22 -07:00
committed by GitHub
parent 0878b256a9
commit 5a6f5d4d68
5 changed files with 129 additions and 46 deletions
+7 -22
View File
@@ -1,6 +1,5 @@
// @vitest-environment happy-dom
import type { Website } from "@reactive-resume/schema/resume/data";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeAll, describe, expect, it, vi } from "vitest";
@@ -8,25 +7,6 @@ import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { useAppForm } from "./tanstack-form";
vi.mock("@/components/input/url-input", () => ({
URLInput: ({
value,
onChange,
hideLabelButton,
}: {
value: Website;
onChange: (value: Website) => void;
hideLabelButton?: boolean;
}) => (
<input
aria-label="Website value"
data-hide-label-button={hideLabelButton}
value={value.url}
onChange={(event) => onChange({ ...value, url: event.target.value })}
/>
),
}));
vi.mock("@/components/input/rich-input", () => ({
RichInput: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => (
<textarea aria-label="Description value" value={value} onChange={(event) => onChange(event.target.value)} />
@@ -73,9 +53,14 @@ describe("registered resume fields", () => {
expect(screen.getByText("Description")).toBeInTheDocument();
expect(container.querySelector(".website-field")).toBeInTheDocument();
expect(container.querySelector(".description-field")).toBeInTheDocument();
expect(screen.getByLabelText("Website value")).toHaveAttribute("data-hide-label-button", "true");
const website = screen.getByRole("textbox", { name: "Website" });
expect(website).toHaveValue("example.com");
expect(screen.getByLabelText("Website")).toBe(website);
expect(container.querySelector(".website-field button")).not.toBeInTheDocument();
await user.click(screen.getByText("Website", { selector: "label" }));
expect(website).toHaveFocus();
await user.clear(screen.getByLabelText("Website value"));
await user.clear(website);
await user.clear(screen.getByLabelText("Description value"));
expect(screen.getByText("Website is required")).toBeInTheDocument();
+8 -4
View File
@@ -124,10 +124,14 @@ function WebsiteField({ label, formItemClassName, hideLabelButton }: WebsiteFiel
return (
<FormItem hasError={hasError} className={formItemClassName}>
<FormLabel>{label}</FormLabel>
<URLInput
value={field.state.value}
onChange={(value) => field.handleChange(value)}
hideLabelButton={hideLabelButton}
<FormControl
render={
<URLInput
value={field.state.value}
onChange={(value) => field.handleChange(value)}
hideLabelButton={hideLabelButton}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
@@ -180,12 +180,16 @@ function BasicsSectionForm() {
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
name={field.name}
value={field.state.value}
onChange={(value) => {
field.handleChange(value);
}}
<FormControl
render={
<URLInput
name={field.name}
value={field.state.value}
onChange={(value) => {
field.handleChange(value);
}}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
@@ -0,0 +1,86 @@
// @vitest-environment happy-dom
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { ReactNode } from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { BasicsSectionBuilder } from "./basics";
import { PictureSectionBuilder } from "./picture";
const state = vi.hoisted(() => ({ data: {} as ResumeData, update: vi.fn() }));
vi.mock("@/features/resume/builder/draft", () => ({
useCurrentBuilderResumeSelector: (selector: (resume: { data: ResumeData }) => unknown) => selector(state),
useUpdateResumeData: () => state.update,
}));
vi.mock("@/libs/tanstack-form", async () => {
const { useForm } = await import("@tanstack/react-form");
return { useAppForm: useForm };
});
vi.mock("@/libs/orpc/client", () => ({
orpc: {
storage: {
uploadFile: { mutationOptions: () => ({ mutationFn: vi.fn() }) },
deleteFile: { mutationOptions: () => ({ mutationFn: vi.fn() }) },
},
},
}));
vi.mock("../shared/section-base", () => ({
SectionBase: ({ children }: { children: ReactNode }) => children,
}));
vi.mock("./custom-fields", () => ({ CustomFieldsSection: () => null }));
vi.mock("@/components/input/color-picker", () => ({ ColorPicker: () => null }));
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
beforeEach(() => {
state.data = structuredClone(defaultResumeData);
state.update.mockReset();
state.update.mockImplementation((update: (draft: ResumeData) => void) => update(state.data));
});
function renderSection(children: ReactNode) {
return render(
<QueryClientProvider client={new QueryClient()}>
<I18nProvider i18n={i18n}>{children}</I18nProvider>
</QueryClientProvider>,
);
}
describe("builder field labels", () => {
it("names and focuses the Website input while preserving URL edits", async () => {
const user = userEvent.setup();
renderSection(<BasicsSectionBuilder />);
const input = screen.getByRole("textbox", { name: "Website" });
expect(screen.getByLabelText("Website")).toBe(input);
await user.click(screen.getByText("Website", { selector: "label" }));
expect(input).toHaveFocus();
await user.type(input, "example.com/profile");
await waitFor(() => expect(state.data.basics.website.url).toBe("https://example.com/profile"));
});
it("names and focuses Picture Size while preserving numeric edits", async () => {
const user = userEvent.setup();
renderSection(<PictureSectionBuilder />);
const input = screen.getByRole("spinbutton", { name: "Size" });
expect(screen.getByLabelText("Size")).toBe(input);
await user.click(screen.getByText("Size", { selector: "label" }));
expect(input).toHaveFocus();
fireEvent.change(input, { target: { value: "144" } });
await waitFor(() => expect(state.data.picture.size).toBe(144));
});
});
@@ -158,20 +158,24 @@ function PictureGeometryFields({ form, onAutoSave }: PictureGeometryFieldsProps)
<Trans>Size</Trans>
</FormLabel>
<InputGroup>
<InputGroupInput
name={field.name}
value={field.state.value}
type="number"
min={32}
max={512}
step={1}
onBlur={field.handleBlur}
onChange={(e) => {
const value = e.target.value;
if (value === "") field.handleChange("" as unknown as number);
else field.handleChange(Number(value));
onAutoSave();
}}
<FormControl
render={
<InputGroupInput
name={field.name}
value={field.state.value}
type="number"
min={32}
max={512}
step={1}
onBlur={field.handleBlur}
onChange={(e) => {
const value = e.target.value;
if (value === "") field.handleChange("" as unknown as number);
else field.handleChange(Number(value));
onAutoSave();
}}
/>
}
/>
<InputGroupAddon align="inline-end">