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
34 changes: 34 additions & 0 deletions __tests__/components/DensitySelector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react-native";

import { DensitySelector } from "~/components/common/DensitySelector";
import { useUIStore } from "~/stores/ui-store";

describe("DensitySelector", () => {
beforeEach(() => {
// Reset the shared store to the default before each case.
useUIStore.setState({ density: "comfortable" });
});

it("renders both density options", () => {
const { getByText } = render(<DensitySelector />);
expect(getByText("Comfortable")).toBeTruthy();
expect(getByText("Compact")).toBeTruthy();
});

it("switches the global density to compact on press", () => {
const { getByLabelText } = render(<DensitySelector />);
expect(useUIStore.getState().density).toBe("comfortable");

fireEvent.press(getByLabelText("Compact"));
expect(useUIStore.getState().density).toBe("compact");
});

it("toggles back to comfortable", () => {
useUIStore.setState({ density: "compact" });
const { getByLabelText } = render(<DensitySelector />);

fireEvent.press(getByLabelText("Comfortable"));
expect(useUIStore.getState().density).toBe("comfortable");
});
});
72 changes: 72 additions & 0 deletions __tests__/components/DetailInlineSelectEdit.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react-native";

import { DetailViewRenderer } from "~/components/renderers/DetailViewRenderer";
import type { FieldDefinition, FormViewMeta } from "~/components/renderers/types";

/**
* Inline select/status editing on the detail screen. The editable badge is an
* internal component (`EditableSelectField`), so it's exercised through
* `DetailViewRenderer` with an `onFieldEdit` handler.
*/
describe("DetailViewRenderer — inline select edit", () => {
const record = { id: "r1", status: "todo" };
const fields: FieldDefinition[] = [
{
name: "status",
label: "Status",
type: "select",
options: [
{ value: "todo", label: "To Do" },
{ value: "in_progress", label: "In Progress" },
{ value: "done", label: "Done" },
],
},
];
// A curated view guarantees the field renders regardless of value.
const view: FormViewMeta = { sections: [{ fields: ["status"] }] };

it("stays read-only (no edit affordance) without an onFieldEdit handler", () => {
const { getByText, queryByLabelText } = render(
<DetailViewRenderer record={record} fields={fields} view={view} />,
);
// The current value still shows as its option label…
expect(getByText("To Do")).toBeTruthy();
// …but there is no tappable edit control.
expect(queryByLabelText("Edit Status")).toBeNull();
});

it("opens the picker and persists a new value via onFieldEdit", () => {
const onFieldEdit = jest.fn().mockResolvedValue(undefined);
const { getByLabelText } = render(
<DetailViewRenderer
record={record}
fields={fields}
view={view}
onFieldEdit={onFieldEdit}
/>,
);

fireEvent.press(getByLabelText("Edit Status"));
fireEvent.press(getByLabelText("In Progress"));

expect(onFieldEdit).toHaveBeenCalledWith("status", "in_progress");
});

it("does not persist when the current value is re-selected", () => {
const onFieldEdit = jest.fn();
const { getByLabelText } = render(
<DetailViewRenderer
record={record}
fields={fields}
view={view}
onFieldEdit={onFieldEdit}
/>,
);

fireEvent.press(getByLabelText("Edit Status"));
fireEvent.press(getByLabelText("To Do")); // already the current value

expect(onFieldEdit).not.toHaveBeenCalled();
});
});
79 changes: 79 additions & 0 deletions __tests__/components/QuickCreateSheet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react-native";

import "~/lib/i18n"; // initialize i18next so t() resolves (the component chain doesn't)
import { QuickCreateSheet } from "~/components/home/QuickCreateSheet";
import type { AppMeta } from "~/hooks/useApps";

// The global expo-router mock hands back a fresh `push` per call, so override it
// here with a shared spy we can assert on.
const mockPush = jest.fn();
jest.mock("expo-router", () => ({
useRouter: () => ({ push: mockPush, replace: jest.fn(), back: jest.fn() }),
}));

const apps: AppMeta[] = [
{
name: "todo_app",
label: "Todo",
navigation: [
{
id: "g1",
type: "group",
label: "Tasks",
children: [
{ id: "n1", type: "object", label: "All Tasks", objectName: "todo_task" },
],
},
// Non-object leaves are ignored by quick-create.
{ id: "n2", type: "dashboard", label: "Dashboard", dashboardName: "d1" },
],
},
{
name: "crm",
label: "CRM",
navigation: [
{ id: "n3", type: "object", label: "Contacts", objectName: "contact" },
// Same app/object surfaced under a second view — must dedupe.
{ id: "n4", type: "object", label: "Contacts (Kanban)", objectName: "contact" },
],
},
];

describe("QuickCreateSheet", () => {
beforeEach(() => mockPush.mockClear());

it("lists creatable objects flattened from the apps' navigation, deduped", () => {
const { getByText, queryByText, queryAllByText } = render(
<QuickCreateSheet open onOpenChange={jest.fn()} apps={apps} />,
);

// Object leaves (incl. those nested in a group) surface.
expect(getByText("All Tasks")).toBeTruthy();
expect(getByText("Contacts")).toBeTruthy();
// The dashboard leaf is not a creatable object.
expect(queryByText("Dashboard")).toBeNull();
// The second view over `contact` is deduped away.
expect(queryByText("Contacts (Kanban)")).toBeNull();
expect(queryAllByText("Contacts")).toHaveLength(1);
});

it("opens the object's create form and closes the sheet on press", () => {
const onOpenChange = jest.fn();
const { getByLabelText } = render(
<QuickCreateSheet open onOpenChange={onOpenChange} apps={apps} />,
);

fireEvent.press(getByLabelText("All Tasks"));

expect(onOpenChange).toHaveBeenCalledWith(false);
expect(mockPush).toHaveBeenCalledWith("/(app)/todo_app/todo_task/new");
});

it("shows an empty hint when no app publishes a creatable object", () => {
const { getByText } = render(
<QuickCreateSheet open onOpenChange={jest.fn()} apps={[]} />,
);
expect(getByText("No objects available to create.")).toBeTruthy();
});
});
Loading