Complete S01 Gmail matching and import workflow
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { ConfirmProvider } from "./confirm";
|
||||
import { PromptProvider } from "./prompt";
|
||||
import { ToastProvider } from "./toast";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
import JobDetailsDialog from "./components/JobDetailsDialog";
|
||||
import { api } from "./api";
|
||||
|
||||
jest.setTimeout(15000);
|
||||
|
||||
jest.mock("./api", () => ({
|
||||
api: {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
function renderDialog() {
|
||||
return render(
|
||||
<ToastProvider>
|
||||
<I18nProvider>
|
||||
<ConfirmProvider>
|
||||
<PromptProvider>
|
||||
<JobDetailsDialog open jobId={42} onClose={() => {}} initialTab={1} />
|
||||
</PromptProvider>
|
||||
</ConfirmProvider>
|
||||
</I18nProvider>
|
||||
</ToastProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("correspondence Gmail import", () => {
|
||||
let correspondenceMessages: any[];
|
||||
|
||||
beforeEach(() => {
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
});
|
||||
window.open = jest.fn();
|
||||
|
||||
correspondenceMessages = [];
|
||||
|
||||
mockedApi.get.mockImplementation((url: string, config?: any) => {
|
||||
if (url === "/jobapplications/42") {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
id: 42,
|
||||
jobTitle: "Backend Developer",
|
||||
status: "Applied",
|
||||
dateApplied: new Date().toISOString(),
|
||||
daysSince: 3,
|
||||
company: { name: "Acme", recruiterEmail: "maria@acme.test", recruiterName: "Maria Recruiter" },
|
||||
tailoredCvText: "",
|
||||
shortSummary: "summary",
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
if (url === "/auth/me") {
|
||||
return Promise.resolve({ data: { roles: [], profileCvText: "Master CV text" } } as any);
|
||||
}
|
||||
if (url === "/jobapplications/42/history") {
|
||||
return Promise.resolve({ data: [] } as any);
|
||||
}
|
||||
if (url === "/attachments/42") {
|
||||
return Promise.resolve({ data: [] } as any);
|
||||
}
|
||||
if (url === "/correspondence/42") {
|
||||
return Promise.resolve({ data: correspondenceMessages } as any);
|
||||
}
|
||||
if (url === "/gmail/status") {
|
||||
return Promise.resolve({ data: { connected: true, gmailAddress: "user@example.test", lastSyncedAt: new Date().toISOString() } } as any);
|
||||
}
|
||||
if (url === "/gmail/job-candidates") {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
jobApplicationId: 42,
|
||||
jobTitle: "Backend Developer",
|
||||
companyName: "Acme",
|
||||
recruiterName: "Maria Recruiter",
|
||||
recruiterEmail: "maria@acme.test",
|
||||
queries: [config?.params?.queryOverride || '"Acme" "Backend Developer" newer_than:365d'],
|
||||
threads: [
|
||||
{
|
||||
threadId: "thread-1",
|
||||
subject: "Backend Developer interview",
|
||||
score: 42,
|
||||
confidence: "high",
|
||||
hasImportedMessages: false,
|
||||
messageCount: 2,
|
||||
latestDate: new Date().toISOString(),
|
||||
matchReasons: [
|
||||
{ label: "company", value: "Acme" },
|
||||
{ label: "recruiterEmail", value: "maria@acme.test" },
|
||||
],
|
||||
messages: [
|
||||
{
|
||||
id: "msg-1",
|
||||
threadId: "thread-1",
|
||||
subject: "Backend Developer interview",
|
||||
from: "Maria Recruiter <maria@acme.test>",
|
||||
to: "user@example.test",
|
||||
date: new Date().toISOString(),
|
||||
snippet: "Acme wants to schedule a call.",
|
||||
score: 42,
|
||||
confidence: "high",
|
||||
alreadyImported: false,
|
||||
matchReasons: [
|
||||
{ label: "company", value: "Acme" },
|
||||
{ label: "jobTitle", value: "Developer" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "msg-2",
|
||||
threadId: "thread-1",
|
||||
subject: "Backend Developer follow-up",
|
||||
from: "user@example.test",
|
||||
to: "Maria Recruiter <maria@acme.test>",
|
||||
date: new Date().toISOString(),
|
||||
snippet: "Following up on the role.",
|
||||
score: 24,
|
||||
confidence: "medium",
|
||||
alreadyImported: false,
|
||||
matchReasons: [{ label: "recency", value: "45d" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
return Promise.resolve({ data: [] } as any);
|
||||
});
|
||||
|
||||
mockedApi.post.mockImplementation((url: string, body?: any) => {
|
||||
if (url === "/gmail/import") {
|
||||
correspondenceMessages = [
|
||||
{
|
||||
id: 700,
|
||||
jobApplicationId: 42,
|
||||
from: "Company",
|
||||
content: "Acme wants to schedule a call.",
|
||||
subject: "Backend Developer interview",
|
||||
channel: "Email",
|
||||
date: new Date().toISOString(),
|
||||
externalMessageId: body.messageId,
|
||||
externalThreadId: "thread-1",
|
||||
externalFrom: "Maria Recruiter <maria@acme.test>",
|
||||
externalTo: "user@example.test",
|
||||
},
|
||||
];
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
imported: 1,
|
||||
skipped: 0,
|
||||
messageId: body.messageId,
|
||||
threadId: "thread-1",
|
||||
message: correspondenceMessages[0],
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
if (url === "/gmail/import-thread") {
|
||||
return Promise.resolve({ data: { imported: 2, skipped: 0, threadId: "thread-1" } } as any);
|
||||
}
|
||||
if (url === "/correspondence") {
|
||||
return Promise.resolve({ data: {} } as any);
|
||||
}
|
||||
return Promise.resolve({ data: {} } as any);
|
||||
});
|
||||
|
||||
mockedApi.put.mockResolvedValue({ data: {} } as any);
|
||||
mockedApi.delete.mockResolvedValue({ data: {} } as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("shows ranked Gmail suggestions with reasons and refreshes correspondence after message import", async () => {
|
||||
renderDialog();
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
||||
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
||||
|
||||
expect((await screen.findAllByText(/backend developer interview/i)).length).toBeGreaterThan(0);
|
||||
expect((await screen.findAllByText(/high confidence · score 42/i)).length).toBeGreaterThan(0);
|
||||
expect((await screen.findAllByText(/company: acme/i)).length).toBeGreaterThan(0);
|
||||
expect(await screen.findByText(/recruiter email: maria@acme\.test/i)).toBeInTheDocument();
|
||||
|
||||
const importButtons = await screen.findAllByRole("button", { name: /^import email$/i });
|
||||
fireEvent.click(importButtons[importButtons.length - 1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.post).toHaveBeenCalledWith("/gmail/import", { jobApplicationId: 42, messageId: "msg-2" });
|
||||
});
|
||||
|
||||
expect(await screen.findByText(/thread thread-1/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/from maria recruiter <maria@acme\.test>/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/to user@example\.test/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("manual Gmail search override reloads job candidates with queryOverride", async () => {
|
||||
renderDialog();
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
||||
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
||||
|
||||
const search = await screen.findByLabelText(/search gmail/i);
|
||||
fireEvent.change(search, { target: { value: 'subject:"Acme" newer_than:30d' } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /^search$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.get).toHaveBeenCalledWith("/gmail/job-candidates", expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
jobApplicationId: 42,
|
||||
queryOverride: 'subject:"Acme" newer_than:30d',
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user