mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
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:
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user