mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-04 02:33:47 +10:00
test: remove redundant cases and repeated setup
This commit is contained in:
@@ -4,21 +4,12 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { DatesField, readTypedDate } from "./dates-field";
|
||||
import { DatesField } from "./dates-field";
|
||||
|
||||
beforeAll(() => {
|
||||
i18n.loadAndActivate({ locale: "en-US", messages: {} });
|
||||
});
|
||||
|
||||
describe("readTypedDate", () => {
|
||||
it.each([
|
||||
["", null],
|
||||
["soon", undefined],
|
||||
])("reads %j as %j", (text, expected) => {
|
||||
expect(readTypedDate(text, "en-US")).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DatesField", () => {
|
||||
const renderField = (dates: ResumeDates, onChange = vi.fn()) => {
|
||||
render(
|
||||
@@ -29,12 +20,20 @@ describe("DatesField", () => {
|
||||
return onChange;
|
||||
};
|
||||
|
||||
it("saves a typed date and clears the review note", () => {
|
||||
it("saves and clears readable dates without committing invalid input or the review note", () => {
|
||||
const onChange = renderField({ start: "2016-06", end: "2018", present: false, raw: "Summer 2016 - 2018" });
|
||||
expect(screen.getByText(/We read "Summer 2016 - 2018"/)).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Start" }), { target: { value: "Jul 2016" } });
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith({ start: "2016-07", end: "2018", present: false });
|
||||
|
||||
onChange.mockClear();
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Start" }), { target: { value: "" } });
|
||||
expect(onChange).toHaveBeenCalledWith({ start: null, end: "2018", present: false });
|
||||
|
||||
onChange.mockClear();
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Start" }), { target: { value: "soon" } });
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { cn } from "@reactive-resume/utils/style";
|
||||
type DateSettings = { locale: string; format?: DateFormat | undefined };
|
||||
|
||||
/** What someone typed as a date: a year-month, null when cleared, or undefined when it can't be read. */
|
||||
export function readTypedDate(text: string, locale: string): YearMonth | null | undefined {
|
||||
function readTypedDate(text: string, locale: string): YearMonth | null | undefined {
|
||||
const value = text.trim();
|
||||
if (!value) return null;
|
||||
const reading = readSingleDate(value, locale);
|
||||
|
||||
@@ -478,174 +478,6 @@ describe("agentService.messages.send", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("merges an answered ask-user-question tool result into the existing assistant message", async () => {
|
||||
const activeThread = buildActiveThread();
|
||||
const userMessage = {
|
||||
id: "message-user-1",
|
||||
userId: "user-1",
|
||||
threadId: "thread-1",
|
||||
role: "user",
|
||||
status: "completed",
|
||||
sequence: 0,
|
||||
uiMessage: {
|
||||
id: "ui-user-1",
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: "Change the name" }],
|
||||
},
|
||||
};
|
||||
const unansweredAssistantMessage = {
|
||||
id: "message-assistant-1",
|
||||
userId: "user-1",
|
||||
threadId: "thread-1",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
sequence: 1,
|
||||
uiMessage: {
|
||||
id: "ui-assistant-1",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{
|
||||
type: "tool-ask_user_question",
|
||||
toolCallId: "call-1",
|
||||
state: "input-available",
|
||||
input: {
|
||||
question: "How broadly should I rename?",
|
||||
choices: ["Only change the main resume header name"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const answeredAssistantMessage = {
|
||||
...unansweredAssistantMessage,
|
||||
uiMessage: {
|
||||
...unansweredAssistantMessage.uiMessage,
|
||||
parts: [
|
||||
{
|
||||
type: "tool-ask_user_question",
|
||||
toolCallId: "call-1",
|
||||
state: "output-available",
|
||||
input: {
|
||||
question: "How broadly should I rename?",
|
||||
choices: ["Only change the main resume header name"],
|
||||
},
|
||||
output: "Only change the main resume header name",
|
||||
callProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
|
||||
resultProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const answeredAssistantModelInput = {
|
||||
...answeredAssistantMessage.uiMessage,
|
||||
parts: [
|
||||
{
|
||||
type: "tool-ask_user_question",
|
||||
toolCallId: "call-1",
|
||||
state: "output-available",
|
||||
input: {
|
||||
question: "How broadly should I rename?",
|
||||
choices: ["Only change the main resume header name"],
|
||||
},
|
||||
output: "Only change the main resume header name",
|
||||
},
|
||||
],
|
||||
};
|
||||
const updateSets: unknown[] = [];
|
||||
|
||||
dbMock.select
|
||||
.mockImplementationOnce(() => selectLimitResult([activeThread]))
|
||||
.mockImplementationOnce(() => selectOrderByResult([userMessage, unansweredAssistantMessage]))
|
||||
.mockImplementationOnce(() => selectOrderByResult([userMessage, answeredAssistantMessage]));
|
||||
|
||||
dbMock.update.mockImplementation(() => ({
|
||||
set: vi.fn((value) => {
|
||||
updateSets.push(value);
|
||||
return { where: vi.fn(async () => undefined) };
|
||||
}),
|
||||
}));
|
||||
|
||||
claimActiveAgentRunMock.mockResolvedValue(true);
|
||||
aiProvidersServiceMock.getRunnableById.mockResolvedValue({
|
||||
id: "provider-1",
|
||||
provider: "openai",
|
||||
model: "gpt-5",
|
||||
apiKey: "secret",
|
||||
baseURL: null,
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }] = await Promise.all([
|
||||
import("ai"),
|
||||
import("./streams"),
|
||||
]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([
|
||||
{ role: "user", content: [{ type: "text", text: "Change the name" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool-call", toolCallId: "call-1", toolName: "ask_user_question", input: {} }],
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "ask_user_question",
|
||||
output: "Only change the main resume header name",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as never);
|
||||
class MockToolLoopAgent {
|
||||
stream = vi.fn(async () => ({ toUIMessageStream: vi.fn(() => new ReadableStream()) }));
|
||||
}
|
||||
vi.mocked(ToolLoopAgent).mockImplementation(MockToolLoopAgent as never);
|
||||
vi.mocked(agentStreamLifecycle.create).mockResolvedValue(new ReadableStream());
|
||||
vi.mocked(streamToEventIterator).mockReturnValue("iterator" as never);
|
||||
|
||||
const { agentService } = await import("./service");
|
||||
|
||||
await agentService.messages.send({
|
||||
threadId: "thread-1",
|
||||
userId: "user-1",
|
||||
message: {
|
||||
id: "ui-assistant-1",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{
|
||||
type: "tool-ask_user_question",
|
||||
toolCallId: "call-1",
|
||||
state: "output-available",
|
||||
input: {
|
||||
question: "How broadly should I rename?",
|
||||
choices: ["Only change the main resume header name"],
|
||||
},
|
||||
output: "Only change the main resume header name",
|
||||
},
|
||||
],
|
||||
// biome-ignore lint/suspicious/noExplicitAny: minimal fixture for unit test
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(dbMock.insert).not.toHaveBeenCalled();
|
||||
expect(updateSets).toContainEqual(
|
||||
expect.objectContaining({
|
||||
uiMessage: expect.objectContaining({
|
||||
parts: [
|
||||
expect.objectContaining({
|
||||
type: "tool-ask_user_question",
|
||||
toolCallId: "call-1",
|
||||
state: "output-available",
|
||||
output: "Only change the main resume header name",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(convertToModelMessages).toHaveBeenCalledWith([userMessage.uiMessage, answeredAssistantModelInput]);
|
||||
});
|
||||
|
||||
// Regression (defect 8): a question continuation streams into the SAME uiMessage id; onFinish
|
||||
// must upsert the existing assistant row instead of inserting a duplicate row.
|
||||
it("continues the existing assistant row on a question continuation instead of inserting a duplicate", async () => {
|
||||
@@ -673,27 +505,41 @@ describe("agentService.messages.send", () => {
|
||||
parts: [{ type: "tool-ask_user_question", toolCallId: "call-1", state: "input-available", input: question }],
|
||||
},
|
||||
};
|
||||
const answeredAssistantModelInput = {
|
||||
...unansweredAssistantMessage.uiMessage,
|
||||
parts: [
|
||||
{
|
||||
type: "tool-ask_user_question",
|
||||
toolCallId: "call-1",
|
||||
state: "output-available",
|
||||
input: question,
|
||||
output: "Only the header",
|
||||
},
|
||||
],
|
||||
};
|
||||
const answeredAssistantMessage = {
|
||||
...unansweredAssistantMessage,
|
||||
uiMessage: {
|
||||
...unansweredAssistantMessage.uiMessage,
|
||||
parts: [
|
||||
{
|
||||
type: "tool-ask_user_question",
|
||||
toolCallId: "call-1",
|
||||
state: "output-available",
|
||||
input: question,
|
||||
output: "Only the header",
|
||||
},
|
||||
],
|
||||
...answeredAssistantModelInput,
|
||||
parts: answeredAssistantModelInput.parts.map((part) => ({
|
||||
...part,
|
||||
callProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
|
||||
resultProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
|
||||
})),
|
||||
},
|
||||
};
|
||||
const updateSets: unknown[] = [];
|
||||
|
||||
dbMock.select
|
||||
.mockImplementationOnce(() => selectLimitResult([activeThread]))
|
||||
.mockImplementationOnce(() => selectOrderByResult([userMessage, unansweredAssistantMessage]))
|
||||
.mockImplementationOnce(() => selectOrderByResult([userMessage, answeredAssistantMessage]));
|
||||
dbMock.update.mockImplementation(() => ({ set: vi.fn(() => ({ where: vi.fn(async () => undefined) })) }));
|
||||
dbMock.update.mockImplementation(() => ({
|
||||
set: vi.fn((value) => {
|
||||
updateSets.push(value);
|
||||
return { where: vi.fn(async () => undefined) };
|
||||
}),
|
||||
}));
|
||||
|
||||
claimActiveAgentRunMock.mockResolvedValue(true);
|
||||
aiProvidersServiceMock.getRunnableById.mockResolvedValue({
|
||||
@@ -735,9 +581,12 @@ describe("agentService.messages.send", () => {
|
||||
threadId: "thread-1",
|
||||
userId: "user-1",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: minimal fixture for unit test
|
||||
message: answeredAssistantMessage.uiMessage as any,
|
||||
message: answeredAssistantModelInput as any,
|
||||
});
|
||||
|
||||
expect(updateSets).toContainEqual(expect.objectContaining({ uiMessage: answeredAssistantModelInput }));
|
||||
expect(convertToModelMessages).toHaveBeenCalledWith([userMessage.uiMessage, answeredAssistantModelInput]);
|
||||
|
||||
const onFinish = uiStreamOptions?.onFinish as (event: Record<string, unknown>) => Promise<void>;
|
||||
const continuedMessage = {
|
||||
...answeredAssistantMessage.uiMessage,
|
||||
|
||||
@@ -36,10 +36,6 @@ AWS Solutions Architect Amazon 2021
|
||||
describe("parseResumeText", () => {
|
||||
const data = parseResumeText(SAMPLE);
|
||||
|
||||
it("always returns schema-valid resume data", () => {
|
||||
expect(() => resumeDataSchema.parse(data)).not.toThrow();
|
||||
});
|
||||
|
||||
it("reads the contact block", () => {
|
||||
expect(data.basics).toMatchObject({
|
||||
name: "Ada Lovelace",
|
||||
@@ -149,18 +145,6 @@ describe("parseResumeText edge cases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseResumeText review findings", () => {
|
||||
it("keeps a section whose heading is the first one in the document", () => {
|
||||
const data = parseResumeText(
|
||||
"Ada Lovelace\nada@example.com\n\nCAREER HIGHLIGHTS\nShipped the difference engine\nMentored the team\n",
|
||||
);
|
||||
|
||||
expect(data.customSections).toHaveLength(1);
|
||||
expect(data.customSections[0]).toMatchObject({ title: "CAREER HIGHLIGHTS" });
|
||||
expect(JSON.stringify(data)).toContain("Shipped the difference engine");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseResumeText multi-line entry preambles", () => {
|
||||
it("keeps an uppercase company followed by a separate role line as one entry", () => {
|
||||
const data = parseResumeText(
|
||||
|
||||
@@ -63,7 +63,9 @@ const finalTextStyle = async (template: Template, text: string, rule = "") => {
|
||||
const element = createElement(ResumeDocument, { data, template }) as unknown as Parameters<typeof pdf>[0];
|
||||
const instance = pdf(element);
|
||||
await expect.poll(() => instance.container.document).not.toBeNull();
|
||||
return mergedStyle(findText(instance.container.document as HostNode, text));
|
||||
const node = findText(instance.container.document as HostNode, text);
|
||||
expect(node).toBeDefined();
|
||||
return mergedStyle(node);
|
||||
};
|
||||
|
||||
describe("PDF semantic base and reset fidelity", () => {
|
||||
|
||||
@@ -23,18 +23,8 @@ type RasterLoadingTask = {
|
||||
destroy(): Promise<void>;
|
||||
};
|
||||
|
||||
export type RasterizePdfDependencies = {
|
||||
loadDocument?: (bytes: Uint8Array) => RasterLoadingTask;
|
||||
};
|
||||
|
||||
const loadPdfDocument = (bytes: Uint8Array): RasterLoadingTask =>
|
||||
getDocument({ data: bytes }) as unknown as RasterLoadingTask;
|
||||
|
||||
export async function rasterizePdf(
|
||||
bytes: Uint8Array,
|
||||
dependencies: RasterizePdfDependencies = {},
|
||||
): Promise<readonly RasterizedPdfPage[]> {
|
||||
const loadingTask = (dependencies.loadDocument ?? loadPdfDocument)(bytes);
|
||||
export async function rasterizePdf(bytes: Uint8Array): Promise<readonly RasterizedPdfPage[]> {
|
||||
const loadingTask = getDocument({ data: bytes }) as unknown as RasterLoadingTask;
|
||||
const pages: RasterizedPdfPage[] = [];
|
||||
|
||||
try {
|
||||
|
||||
@@ -95,7 +95,7 @@ const renderGengar = async (data: ResumeData, template: Template = "gengar") =>
|
||||
const page = await pdf.getPage(index);
|
||||
pages.push((await page.getTextContent()).items.flatMap((item) => ("str" in item ? [item] : [])));
|
||||
}
|
||||
return { bytes, pages, raster: await rasterizePdf(bytes.slice()) };
|
||||
return { bytes, pages };
|
||||
} finally {
|
||||
await loading.destroy();
|
||||
}
|
||||
@@ -141,7 +141,8 @@ describe("Gengar skill rating placement (#2611)", () => {
|
||||
const data = gengarFixture({ design: "rectangle" });
|
||||
const result = await renderGengar(data);
|
||||
const text = result.pages.flat().map((item) => item.str);
|
||||
const raster = required(result.raster[0]);
|
||||
const rasters = await rasterizePdf(result.bytes.slice());
|
||||
const raster = required(rasters[0]);
|
||||
const rows = Array.from({ length: raster.height }, (_, y) => {
|
||||
let pixels = 0;
|
||||
for (let x = 0; x < raster.width; x++) {
|
||||
@@ -159,6 +160,6 @@ describe("Gengar skill rating placement (#2611)", () => {
|
||||
expect(ratingY).toBeLessThan(nameY);
|
||||
expect(ratingY).toBeGreaterThan(proficiencyY);
|
||||
expect(text.join(" ")).toContain("Engineering");
|
||||
expect(result.raster.length).toBe(1);
|
||||
expect(rasters).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getSectionItemRows } from "./columns";
|
||||
|
||||
describe("getSectionItemRows", () => {
|
||||
it("groups items into rows of N columns", () => {
|
||||
expect(getSectionItemRows([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
|
||||
});
|
||||
});
|
||||
@@ -68,9 +68,9 @@ async function picturePixels(template: "onyx" | "ditto" | "glalie", borderWidth:
|
||||
}
|
||||
|
||||
describe("picture border visibility (#3017)", () => {
|
||||
it.each(["onyx"] as const)("draws the border around an opaque picture (%s)", async (template) => {
|
||||
const plain = await picturePixels(template, 0);
|
||||
const bordered = await picturePixels(template, 10);
|
||||
it("draws a border and a soft centered shadow without moving the photo", async () => {
|
||||
const plain = await picturePixels("onyx", 0);
|
||||
const bordered = await picturePixels("onyx", 10);
|
||||
expect(plain.borderPixels).toBe(0);
|
||||
expect(bordered.borderPixels).toBeGreaterThan(1000);
|
||||
expect(bordered.imagePixels).toBeGreaterThan(1000);
|
||||
@@ -78,10 +78,7 @@ describe("picture border visibility (#3017)", () => {
|
||||
// Rasterized border/image edges can differ by one antialiased pixel.
|
||||
for (const edge of ["left", "right", "top", "bottom"] as const)
|
||||
expect(Math.abs(bordered.bounds[edge] - plain.bounds[edge])).toBeLessThanOrEqual(1);
|
||||
});
|
||||
it.each(["onyx"] as const)("draws a soft centered shadow without moving the photo (%s)", async (template) => {
|
||||
const plain = await picturePixels(template, 0);
|
||||
const shadow = await picturePixels(template, 0, 10);
|
||||
const shadow = await picturePixels("onyx", 0, 10);
|
||||
expect(plain.shadowPixels).toBe(0);
|
||||
expect(shadow.shadowPixels).toBeGreaterThan(100);
|
||||
expect(shadow.bounds).toEqual(plain.bounds);
|
||||
|
||||
@@ -11,7 +11,8 @@ const renderHeading = async (css: string, hideSectionIcons = false) => {
|
||||
data.metadata.typography.body.fontFamily = "Helvetica";
|
||||
data.metadata.typography.heading.fontFamily = "Helvetica";
|
||||
data.metadata.page.hideSectionIcons = hideSectionIcons;
|
||||
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
|
||||
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills", "summary"], sidebar: [] }];
|
||||
data.summary.content = "<p>Summary body</p>";
|
||||
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: `@version 1; ${css}` } };
|
||||
data.sections.skills.items = [
|
||||
{ id: "skill", hidden: false, name: "Skill", proficiency: "", level: 0, keywords: [], icon: "", iconColor: "" },
|
||||
@@ -53,12 +54,12 @@ const renderHeading = async (css: string, hideSectionIcons = false) => {
|
||||
};
|
||||
|
||||
describe("Semantic section heading colors (#3348)", () => {
|
||||
it.each([false])("colors heading text with hideSectionIcons=%s", async (hidden) => {
|
||||
const { text } = await renderHeading("section-heading { color: #1234ef; }", hidden);
|
||||
it("applies heading color and text styles with section icons visible", async () => {
|
||||
const { text, sizes } = await renderHeading(
|
||||
"section-heading { color: #1234ef; } section[type='skills'] section-heading { font-size: 21pt; text-transform: lowercase; }",
|
||||
);
|
||||
expect(text).toContainEqual({ value: "heading", fill: "#1234ef" });
|
||||
expect(text).toContainEqual({ value: "HEADING", fill: "#1234ef" });
|
||||
});
|
||||
it.each([false])("gives heading text its text styles with hideSectionIcons=%s", async (hidden) => {
|
||||
const { sizes } = await renderHeading("section-heading { font-size: 21pt; text-transform: lowercase; }", hidden);
|
||||
expect(sizes).toContainEqual({ value: "heading", size: 21 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +92,8 @@ describe("skill rating alignment (#3343)", () => {
|
||||
expect(rows.map((row) => row.circles)).toEqual([10, 5]);
|
||||
expect(text).toContain("Keyword11");
|
||||
expect(text).toContain("Skill 2");
|
||||
expect(text.indexOf("Skill 0")).toBeLessThan(text.indexOf("Skill 1"));
|
||||
expect(text.indexOf("Skill 1")).toBeLessThan(text.indexOf("Skill 2"));
|
||||
});
|
||||
it("aligns language ratings in multi-column rows with unequal fluency text", async () => {
|
||||
const {
|
||||
|
||||
@@ -67,8 +67,6 @@ export type HarvestOptions = {
|
||||
operatorBudgetPerPageMs?: number;
|
||||
onProgress?: (progress: HarvestProgress) => void;
|
||||
signal?: { aborted: boolean };
|
||||
/** Injected for tests; defaults to `Date.now`. */
|
||||
monotonicNow?: () => number;
|
||||
};
|
||||
|
||||
export const HARVEST_DEFAULTS = {
|
||||
@@ -253,7 +251,6 @@ async function withTimeout<T>(work: Promise<T>, budgetMs: number): Promise<T | n
|
||||
* short, `operators` stays null and the report says which checks it could not run.
|
||||
*/
|
||||
export async function harvestPdfDocument(document: PdfDocumentLike, options: HarvestOptions): Promise<RawExtraction> {
|
||||
const now = options.monotonicNow ?? Date.now;
|
||||
const maxPages = options.maxPages ?? HARVEST_DEFAULTS.maxPages;
|
||||
const totalBudgetMs = options.operatorBudgetMs ?? HARVEST_DEFAULTS.operatorBudgetMs;
|
||||
const perPageBudgetMs = options.operatorBudgetPerPageMs ?? HARVEST_DEFAULTS.operatorBudgetPerPageMs;
|
||||
@@ -280,7 +277,7 @@ export async function harvestPdfDocument(document: PdfDocumentLike, options: Har
|
||||
const fontRefs = new Set<string>();
|
||||
const fonts = new Map<string, RawFont>();
|
||||
|
||||
const operatorStartedAt = now();
|
||||
const operatorStartedAt = Date.now();
|
||||
let operatorsAttempted = false;
|
||||
let operatorsSucceeded = false;
|
||||
|
||||
@@ -311,7 +308,7 @@ export async function harvestPdfDocument(document: PdfDocumentLike, options: Har
|
||||
links.push(...toLinkAnnotations(annotations, pageNumber));
|
||||
|
||||
let operators: PageOperatorSummary | null = null;
|
||||
const elapsed = now() - operatorStartedAt;
|
||||
const elapsed = Date.now() - operatorStartedAt;
|
||||
const remaining = Math.min(perPageBudgetMs, totalBudgetMs - elapsed);
|
||||
|
||||
if (remaining > 0) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
baseSectionSchema,
|
||||
customFieldSchema,
|
||||
customSectionSchema,
|
||||
experienceItemSchema,
|
||||
pageSchema,
|
||||
parseResumeData,
|
||||
pictureSchema,
|
||||
@@ -89,23 +88,6 @@ describe("customFieldSchema", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("experienceItemSchema", () => {
|
||||
it("defaults roles to [] via .catch when missing", () => {
|
||||
const result = experienceItemSchema.safeParse({
|
||||
id: "x",
|
||||
hidden: false,
|
||||
company: "Acme",
|
||||
position: "",
|
||||
location: "",
|
||||
period: "",
|
||||
website: { url: "", label: "", inlineLink: false },
|
||||
description: "",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.roles).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pageSchema", () => {
|
||||
it("defaults hideLinkUnderline to false when missing", () => {
|
||||
const { hideLinkUnderline: _, ...pageWithout } = defaultResumeData.metadata.page;
|
||||
|
||||
@@ -2,7 +2,9 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
import { createSampleResumeFromDashboard, openDownloadDialog, openSidebarSection } from "../fixtures/resume";
|
||||
import { expect, test } from "../fixtures/test";
|
||||
|
||||
test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => {
|
||||
test("round-trips a JSON backup and splits a legacy embedded letter on import", async ({
|
||||
authPage: page,
|
||||
}, testInfo) => {
|
||||
await createSampleResumeFromDashboard(page, testInfo);
|
||||
|
||||
const sheet = await openDownloadDialog(page);
|
||||
@@ -14,7 +16,11 @@ test("exports and imports a resume JSON backup", async ({ authPage: page }, test
|
||||
|
||||
const downloadPath = testInfo.outputPath(download.suggestedFilename());
|
||||
await download.saveAs(downloadPath);
|
||||
const exportedData = JSON.parse(await readFile(downloadPath, "utf-8")) as { basics: { name: string } };
|
||||
const exportedData = JSON.parse(await readFile(downloadPath, "utf-8")) as {
|
||||
basics: { name: string };
|
||||
customSections: unknown[];
|
||||
metadata: { layout: { pages: { fullWidth: boolean; main: string[]; sidebar: string[] }[] } };
|
||||
};
|
||||
// A name only this file carries, so a builder that opens anything but the imported resume fails.
|
||||
exportedData.basics.name = `Imported ${Date.now()}`;
|
||||
await writeFile(downloadPath, JSON.stringify(exportedData));
|
||||
@@ -29,24 +35,9 @@ test("exports and imports a resume JSON backup", async ({ authPage: page }, test
|
||||
await page.waitForURL(/\/builder\/.+/);
|
||||
await openSidebarSection(page, "Basics");
|
||||
await expect(page.getByRole("textbox", { name: "Full name", exact: true })).toHaveValue(exportedData.basics.name);
|
||||
});
|
||||
|
||||
test("imports a resume file that carries a cover letter as a resume and a letter of its own", async ({
|
||||
authPage: page,
|
||||
}, testInfo) => {
|
||||
await createSampleResumeFromDashboard(page, testInfo);
|
||||
const sheet = await openDownloadDialog(page);
|
||||
await sheet.getByRole("radio", { name: /^JSON/ }).click();
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await sheet.getByRole("button", { name: "Download JSON" }).click();
|
||||
const exported = await downloadPromise;
|
||||
const data = JSON.parse(await readFile((await exported.path()) as string, "utf-8")) as {
|
||||
customSections: unknown[];
|
||||
metadata: { layout: { pages: { fullWidth: boolean; main: string[]; sidebar: string[] }[] } };
|
||||
};
|
||||
|
||||
// A file from an older version, with the letter inside the resume.
|
||||
data.customSections.push({
|
||||
exportedData.customSections.push({
|
||||
id: "old-letter",
|
||||
type: "cover-letter",
|
||||
title: "Letter to Globex",
|
||||
@@ -57,9 +48,9 @@ test("imports a resume file that carries a cover letter as a resume and a letter
|
||||
startOnNewPage: false,
|
||||
items: [{ id: "old-letter-item", hidden: false, recipient: "<p>Globex</p>", content: "<p>Dear Globex team,</p>" }],
|
||||
});
|
||||
data.metadata.layout.pages.push({ fullWidth: true, main: ["old-letter"], sidebar: [] });
|
||||
exportedData.metadata.layout.pages.push({ fullWidth: true, main: ["old-letter"], sidebar: [] });
|
||||
const path = testInfo.outputPath("with-letter.json");
|
||||
await writeFile(path, JSON.stringify(data));
|
||||
await writeFile(path, JSON.stringify(exportedData));
|
||||
|
||||
await page.goto("/dashboard");
|
||||
await page.getByRole("button", { name: "New", exact: true }).click();
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { expect, test } from "../fixtures/test";
|
||||
|
||||
for (const accept of [false, true]) {
|
||||
test(`requires explicit OAuth consent before ${accept ? "allowing" : "denying"} access`, async ({
|
||||
authPage: page,
|
||||
baseURL,
|
||||
}) => {
|
||||
const origin = new URL(baseURL ?? "http://localhost:3000").origin;
|
||||
const metadata = await page.request.get("/.well-known/oauth-protected-resource");
|
||||
expect(metadata.status()).toBe(200);
|
||||
const advertisedResource = (await metadata.json()).resource;
|
||||
expect(advertisedResource).toBe(origin);
|
||||
const resource = `${origin}/mcp`;
|
||||
const callback = "http://127.0.0.1:33921/callback";
|
||||
const registration = await page.request.post("/api/auth/oauth2/register", {
|
||||
headers: { origin },
|
||||
data: { client_name: "Consent test client", redirect_uris: [callback] },
|
||||
});
|
||||
expect(registration.status(), await registration.text()).toBe(201);
|
||||
const client = await registration.json();
|
||||
const verifier = randomBytes(32).toString("base64url");
|
||||
const query = new URLSearchParams({
|
||||
client_id: client.client_id,
|
||||
redirect_uri: callback,
|
||||
response_type: "code",
|
||||
scope: "openid profile offline_access",
|
||||
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
|
||||
code_challenge_method: "S256",
|
||||
resource,
|
||||
state: "browser-consent-state",
|
||||
});
|
||||
query.append("resource", origin);
|
||||
await page.route(`${callback}**`, (route) => route.fulfill({ body: "Client callback" }));
|
||||
test("requires explicit OAuth consent before denying or allowing access", async ({ authPage: page, baseURL }) => {
|
||||
const origin = new URL(baseURL ?? "http://localhost:3000").origin;
|
||||
const metadata = await page.request.get("/.well-known/oauth-protected-resource");
|
||||
expect(metadata.status()).toBe(200);
|
||||
const advertisedResource = (await metadata.json()).resource;
|
||||
expect(advertisedResource).toBe(origin);
|
||||
const resource = `${origin}/mcp`;
|
||||
const callback = "http://127.0.0.1:33921/callback";
|
||||
const registration = await page.request.post("/api/auth/oauth2/register", {
|
||||
headers: { origin },
|
||||
data: { client_name: "Consent test client", redirect_uris: [callback] },
|
||||
});
|
||||
expect(registration.status(), await registration.text()).toBe(201);
|
||||
const client = await registration.json();
|
||||
const verifier = randomBytes(32).toString("base64url");
|
||||
const query = new URLSearchParams({
|
||||
client_id: client.client_id,
|
||||
redirect_uri: callback,
|
||||
response_type: "code",
|
||||
scope: "openid profile offline_access",
|
||||
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
|
||||
code_challenge_method: "S256",
|
||||
resource,
|
||||
state: "browser-consent-state",
|
||||
});
|
||||
query.append("resource", origin);
|
||||
await page.route(`${callback}**`, (route) => route.fulfill({ body: "Client callback" }));
|
||||
for (const accept of [false, true]) {
|
||||
await page.goto(`/api/auth/oauth2/authorize?${query}`);
|
||||
await expect(page.getByRole("heading", { name: "Connect an application" })).toBeVisible();
|
||||
await expect(page.getByText("Consent test client", { exact: true })).toBeVisible();
|
||||
@@ -49,7 +46,7 @@ for (const accept of [false, true]) {
|
||||
expect(target.searchParams.has("code")).toBe(false);
|
||||
const after = await page.request.get(`${origin}/api/auth/oauth2/get-consents`);
|
||||
expect(await after.json()).toEqual([]);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
expect(target.searchParams.get("code")).toBeTruthy();
|
||||
const token = await page.request.post(`${origin}/api/auth/oauth2/token`, {
|
||||
@@ -89,5 +86,5 @@ for (const accept of [false, true]) {
|
||||
data: initializePayload,
|
||||
});
|
||||
expect(wrongAudience.status()).toBe(401);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user