mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-26 09:54:43 +10:00
📦 v5.0.15 - https://docs.rxresu.me/changelog
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it } from "vite-plus/test";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
import { LinkedTitle } from "./linked-title";
|
||||
|
||||
describe("LinkedTitle", () => {
|
||||
describe("renders as link when showLinkInTitle and website.url are provided", () => {
|
||||
it("renders an anchor tag", () => {
|
||||
render(
|
||||
<LinkedTitle title="Acme Corp" website={{ url: "https://acme.com", label: "Acme" }} showLinkInTitle={true} />,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "Acme Corp" });
|
||||
expect(link).toBeDefined();
|
||||
expect(link.getAttribute("href")).toBe("https://acme.com");
|
||||
});
|
||||
|
||||
it("sets target=_blank and rel=noopener on the link", () => {
|
||||
render(
|
||||
<LinkedTitle title="Company" website={{ url: "https://example.com", label: "" }} showLinkInTitle={true} />,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link");
|
||||
expect(link.getAttribute("target")).toBe("_blank");
|
||||
expect(link.getAttribute("rel")).toBe("noopener");
|
||||
});
|
||||
|
||||
it("wraps the title in a strong tag inside the link", () => {
|
||||
render(
|
||||
<LinkedTitle title="Bold Title" website={{ url: "https://example.com", label: "" }} showLinkInTitle={true} />,
|
||||
);
|
||||
|
||||
const strong = screen.getByText("Bold Title");
|
||||
expect(strong.tagName).toBe("STRONG");
|
||||
});
|
||||
});
|
||||
|
||||
describe("renders as plain strong tag otherwise", () => {
|
||||
it("renders strong when showLinkInTitle is false", () => {
|
||||
render(
|
||||
<LinkedTitle title="Plain Title" website={{ url: "https://example.com", label: "" }} showLinkInTitle={false} />,
|
||||
);
|
||||
|
||||
const strong = screen.getByText("Plain Title");
|
||||
expect(strong.tagName).toBe("STRONG");
|
||||
expect(screen.queryByRole("link")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders strong when showLinkInTitle is undefined", () => {
|
||||
render(<LinkedTitle title="No Link" />);
|
||||
|
||||
expect(screen.getByText("No Link").tagName).toBe("STRONG");
|
||||
expect(screen.queryByRole("link")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders strong when website is undefined", () => {
|
||||
render(<LinkedTitle title="No Website" showLinkInTitle={true} />);
|
||||
|
||||
expect(screen.getByText("No Website").tagName).toBe("STRONG");
|
||||
expect(screen.queryByRole("link")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders strong when website.url is empty", () => {
|
||||
render(<LinkedTitle title="Empty URL" website={{ url: "", label: "" }} showLinkInTitle={true} />);
|
||||
|
||||
expect(screen.getByText("Empty URL").tagName).toBe("STRONG");
|
||||
expect(screen.queryByRole("link")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("className prop", () => {
|
||||
it("applies className to the link element", () => {
|
||||
render(
|
||||
<LinkedTitle
|
||||
title="Styled"
|
||||
website={{ url: "https://x.com", label: "" }}
|
||||
showLinkInTitle={true}
|
||||
className="custom-class"
|
||||
/>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link");
|
||||
expect(link.className).toContain("custom-class");
|
||||
});
|
||||
|
||||
it("applies className to the strong element", () => {
|
||||
render(<LinkedTitle title="Styled Strong" className="my-class" />);
|
||||
|
||||
const strong = screen.getByText("Styled Strong");
|
||||
expect(strong.className).toContain("my-class");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
|
||||
|
||||
// Mock dependencies before importing the store
|
||||
vi.mock("@lingui/core/macro", () => ({
|
||||
t: (strings: TemplateStringsArray) => strings[0],
|
||||
}));
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
error: vi.fn(() => "toast-id"),
|
||||
dismiss: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/integrations/orpc/client", () => ({
|
||||
orpc: {
|
||||
resume: {
|
||||
update: {
|
||||
call: vi.fn(() => Promise.resolve()),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { defaultResumeData } from "@/schema/resume/data";
|
||||
|
||||
import { useResumeStore } from "./resume";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeResume(overrides?: Record<string, unknown>) {
|
||||
return {
|
||||
id: "test-resume-id",
|
||||
name: "Test Resume",
|
||||
slug: "test-resume",
|
||||
tags: ["test"],
|
||||
isLocked: false,
|
||||
data: structuredClone(defaultResumeData),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
// Reset store between tests
|
||||
useResumeStore.getState().initialize(null);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// initialize
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useResumeStore — initialize", () => {
|
||||
it("sets resume and isReady to true", () => {
|
||||
const resume = makeResume();
|
||||
useResumeStore.getState().initialize(resume);
|
||||
|
||||
const state = useResumeStore.getState();
|
||||
expect(state.isReady).toBe(true);
|
||||
expect(state.resume.id).toBe("test-resume-id");
|
||||
expect(state.resume.name).toBe("Test Resume");
|
||||
});
|
||||
|
||||
it("sets isReady to false when initialized with null", () => {
|
||||
useResumeStore.getState().initialize(null);
|
||||
|
||||
const state = useResumeStore.getState();
|
||||
expect(state.isReady).toBe(false);
|
||||
});
|
||||
|
||||
it("replaces previous resume data", () => {
|
||||
useResumeStore.getState().initialize(makeResume({ name: "First" }));
|
||||
expect(useResumeStore.getState().resume.name).toBe("First");
|
||||
|
||||
useResumeStore.getState().initialize(makeResume({ name: "Second" }));
|
||||
expect(useResumeStore.getState().resume.name).toBe("Second");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// updateResumeData
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useResumeStore — updateResumeData", () => {
|
||||
it("updates resume data via immer draft", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.name = "Jane Doe";
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.basics.name).toBe("Jane Doe");
|
||||
});
|
||||
|
||||
it("can update nested fields", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.website.url = "https://example.com";
|
||||
draft.basics.email = "jane@example.com";
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.basics.website.url).toBe("https://example.com");
|
||||
expect(useResumeStore.getState().resume.data.basics.email).toBe("jane@example.com");
|
||||
});
|
||||
|
||||
it("can add items to sections", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.sections.skills.items.push({
|
||||
id: "s1",
|
||||
hidden: false,
|
||||
icon: "",
|
||||
name: "TypeScript",
|
||||
proficiency: "Advanced",
|
||||
level: 4,
|
||||
keywords: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.sections.skills.items).toHaveLength(1);
|
||||
expect(useResumeStore.getState().resume.data.sections.skills.items[0].name).toBe("TypeScript");
|
||||
});
|
||||
|
||||
it("does not update when resume is not initialized", () => {
|
||||
// Store starts with null resume
|
||||
useResumeStore.getState().initialize(null);
|
||||
|
||||
// Should not throw
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.name = "Should not apply";
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks updates when resume is locked", async () => {
|
||||
const { toast } = await import("sonner");
|
||||
useResumeStore.getState().initialize(makeResume({ isLocked: true }));
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.name = "Should not apply";
|
||||
});
|
||||
|
||||
// Name should remain unchanged
|
||||
expect(useResumeStore.getState().resume.data.basics.name).toBe("");
|
||||
// Should show error toast
|
||||
expect(toast.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not block updates on unlocked resume", () => {
|
||||
useResumeStore.getState().initialize(makeResume({ isLocked: false }));
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.name = "Updated";
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.basics.name).toBe("Updated");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Multiple updates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useResumeStore — multiple updates", () => {
|
||||
it("applies sequential updates correctly", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.name = "First Update";
|
||||
});
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.headline = "Developer";
|
||||
});
|
||||
|
||||
const data = useResumeStore.getState().resume.data;
|
||||
expect(data.basics.name).toBe("First Update");
|
||||
expect(data.basics.headline).toBe("Developer");
|
||||
});
|
||||
|
||||
it("preserves unmodified sections across updates", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.basics.name = "Jane";
|
||||
});
|
||||
|
||||
// Metadata and other sections should be untouched
|
||||
expect(useResumeStore.getState().resume.data.metadata.template).toBe("onyx");
|
||||
expect(useResumeStore.getState().resume.data.sections.skills.items).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Temporal store (undo/redo)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useResumeStore — temporal", () => {
|
||||
it("has a temporal store accessible", () => {
|
||||
expect(useResumeStore.temporal).toBeDefined();
|
||||
expect(useResumeStore.temporal.getState).toBeDefined();
|
||||
});
|
||||
|
||||
it("temporal store has undo/redo actions", () => {
|
||||
const temporal = useResumeStore.temporal.getState();
|
||||
expect(typeof temporal.undo).toBe("function");
|
||||
expect(typeof temporal.redo).toBe("function");
|
||||
expect(typeof temporal.clear).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Edge cases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useResumeStore — edge cases", () => {
|
||||
it("can update metadata fields", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.metadata.template = "pikachu";
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.metadata.template).toBe("pikachu");
|
||||
});
|
||||
|
||||
it("can update picture settings", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.picture.url = "https://example.com/photo.jpg";
|
||||
draft.picture.size = 120;
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.picture.url).toBe("https://example.com/photo.jpg");
|
||||
expect(useResumeStore.getState().resume.data.picture.size).toBe(120);
|
||||
});
|
||||
|
||||
it("can remove all items from a section", () => {
|
||||
const resume = makeResume();
|
||||
resume.data.sections.skills.items = [
|
||||
{ id: "s1", hidden: false, icon: "", name: "JS", proficiency: "", level: 0, keywords: [] },
|
||||
];
|
||||
useResumeStore.getState().initialize(resume);
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.sections.skills.items = [];
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.sections.skills.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can update custom sections", () => {
|
||||
useResumeStore.getState().initialize(makeResume());
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.customSections.push({
|
||||
id: "custom-1",
|
||||
type: "experience",
|
||||
title: "Freelance",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
items: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(useResumeStore.getState().resume.data.customSections).toHaveLength(1);
|
||||
expect(useResumeStore.getState().resume.data.customSections[0].title).toBe("Freelance");
|
||||
});
|
||||
});
|
||||
@@ -32,12 +32,37 @@ type ResumeStore = ResumeStoreState & ResumeStoreActions;
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
const _syncResume = (resume: Resume) => {
|
||||
void orpc.resume.update.call({ id: resume.id, data: resume.data }, { signal });
|
||||
let syncErrorToastId: string | number | undefined;
|
||||
|
||||
const _syncResume = async (resume: Resume) => {
|
||||
try {
|
||||
await orpc.resume.update.call({ id: resume.id, data: resume.data }, { signal });
|
||||
|
||||
// Dismiss error toast on successful sync
|
||||
if (syncErrorToastId !== undefined) {
|
||||
toast.dismiss(syncErrorToastId);
|
||||
syncErrorToastId = undefined;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// Ignore aborted requests (e.g. page navigation)
|
||||
if (error instanceof DOMException && error.name === "AbortError") return;
|
||||
|
||||
syncErrorToastId = toast.error(
|
||||
t`Your latest changes could not be saved. Please make sure you are connected to the internet and try again.`,
|
||||
{ id: syncErrorToastId, duration: Infinity },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const syncResume = debounce(_syncResume, 500, { signal });
|
||||
|
||||
// Flush pending sync before the page unloads to prevent data loss
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("beforeunload", () => {
|
||||
syncResume.flush();
|
||||
});
|
||||
}
|
||||
|
||||
let errorToastId: string | number | undefined;
|
||||
|
||||
type PartializedState = { resume: Resume | null };
|
||||
|
||||
Reference in New Issue
Block a user