Files
kandev/apps/web/components/settings/editor-form.test.tsx
T
2026-07-19 14:21:36 +01:00

121 lines
4.0 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { EditorForm, defaultFormState } from "./editor-form";
import { SettingsPageTemplate } from "./settings-page-template";
import { SettingsSaveProvider, useSettingsSaveContributor } from "./settings-save-provider";
afterEach(cleanup);
const COMMAND_PLACEHOLDER = "code --goto {file}:{line}";
const DIRTY_ATTRIBUTE_VALUE = "true";
const DIRTY_ATTRIBUTE = "data-settings-dirty";
function renderEditorForm(props: Partial<React.ComponentProps<typeof EditorForm>> = {}) {
const onSave = props.onSave ?? vi.fn().mockResolvedValue(undefined);
render(
<SettingsSaveProvider>
<EditorForm
title="New custom editor"
initialState={defaultFormState()}
onCancel={vi.fn()}
onSave={onSave}
submitLabel="Add editor"
isSaving={false}
coordinatedSaveId="custom-editor:new"
dirtyWhenMounted
{...props}
/>
</SettingsSaveProvider>,
);
return { onSave };
}
function DirtySiblingContributor() {
useSettingsSaveContributor({
id: "sibling",
revision: 1,
isDirty: true,
save: vi.fn(),
discard: vi.fn(),
});
return null;
}
describe("EditorForm coordinated save", () => {
it("treats a new inline editor as dirty and saves it through the route action", async () => {
const { onSave } = renderEditorForm();
const form = screen.getByText("New custom editor").parentElement;
expect(form?.getAttribute(DIRTY_ATTRIBUTE)).toBe(DIRTY_ATTRIBUTE_VALUE);
expect(screen.queryByRole("button", { name: "Add editor" })).toBeNull();
expect(screen.getByRole("button", { name: "Save changes" }).hasAttribute("disabled")).toBe(
true,
);
fireEvent.change(screen.getByPlaceholderText(COMMAND_PLACEHOLDER), {
target: { value: COMMAND_PLACEHOLDER },
});
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
expect(onSave).toHaveBeenCalledWith(
expect.objectContaining({
name: "code --goto {file}:{line}",
command: COMMAND_PLACEHOLDER,
}),
);
});
it("marks the owning page card dirty when an existing editor changes", async () => {
const initialState = {
...defaultFormState(),
name: "VS Code",
command: "code {file}",
};
render(
<SettingsSaveProvider>
<SettingsPageTemplate title="Editors" isDirty={false} saveStatus="idle" onSave={vi.fn()}>
<EditorForm
title="Edit VS Code"
initialState={initialState}
onCancel={vi.fn()}
onSave={vi.fn()}
submitLabel="Save editor"
isSaving={false}
coordinatedSaveId="custom-editor:editor-1"
/>
</SettingsPageTemplate>
</SettingsSaveProvider>,
);
const pageCard = document.querySelector('[data-settings-dirty-level="card"]');
expect(pageCard?.getAttribute(DIRTY_ATTRIBUTE)).toBe("false");
fireEvent.change(screen.getByPlaceholderText(COMMAND_PLACEHOLDER), {
target: { value: "cursor {file}" },
});
await waitFor(() =>
expect(pageCard?.getAttribute(DIRTY_ATTRIBUTE)).toBe(DIRTY_ATTRIBUTE_VALUE),
);
expect(screen.getByText("Edit VS Code").parentElement?.getAttribute(DIRTY_ATTRIBUTE)).toBe(
DIRTY_ATTRIBUTE_VALUE,
);
});
it("does not mark a page card dirty for a contributor outside its scope", async () => {
render(
<SettingsSaveProvider>
<DirtySiblingContributor />
<SettingsPageTemplate title="Editors" isDirty={false} saveStatus="idle" onSave={vi.fn()}>
<div>Editor settings</div>
</SettingsPageTemplate>
</SettingsSaveProvider>,
);
const pageCard = document.querySelector('[data-settings-dirty-level="card"]');
await waitFor(() => expect(screen.getByRole("button", { name: "Save changes" })).toBeTruthy());
expect(pageCard?.getAttribute(DIRTY_ATTRIBUTE)).toBe("false");
});
});