589 lines
25 KiB
TypeScript
589 lines
25 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { act, 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 Correspondence from "./components/Correspondence";
|
|
import { api } from "./api";
|
|
import { translations } from "./i18n/translations";
|
|
|
|
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() } },
|
|
},
|
|
getApiErrorMessage: (_error: unknown, fallback: string) => fallback,
|
|
}));
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
const originalWindowOpen = window.open;
|
|
const gmailCallbackOrigin = "http://localhost:3000";
|
|
const gmailAuthorizationUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=test-client&redirect_uri=${encodeURIComponent(`${gmailCallbackOrigin}/api/gmail/oauth/callback`)}&response_type=code&state=opaque-test-state`;
|
|
|
|
function dialog(jobId = 42) {
|
|
return (
|
|
<ToastProvider>
|
|
<I18nProvider>
|
|
<ConfirmProvider>
|
|
<PromptProvider>
|
|
<Correspondence
|
|
jobId={jobId}
|
|
jobContext={{ jobTitle: "Backend Developer", companyName: "Acme", recruiterEmail: "maria@acme.test" }}
|
|
/>
|
|
</PromptProvider>
|
|
</ConfirmProvider>
|
|
</I18nProvider>
|
|
</ToastProvider>
|
|
);
|
|
}
|
|
|
|
function renderDialog() {
|
|
return render(dialog());
|
|
}
|
|
|
|
async function oauthMessage(data: unknown, source: Window | null, origin = gmailCallbackOrigin) {
|
|
await act(async () => {
|
|
window.dispatchEvent(new MessageEvent("message", { data, source, origin }));
|
|
});
|
|
}
|
|
|
|
describe("correspondence Gmail import", () => {
|
|
let correspondenceMessages: any[];
|
|
let gmailConnected: boolean;
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
Object.assign(navigator, {
|
|
clipboard: {
|
|
writeText: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
});
|
|
window.open = jest.fn();
|
|
|
|
correspondenceMessages = [];
|
|
gmailConnected = true;
|
|
|
|
mockedApi.get.mockImplementation((url: string, config?: any) => {
|
|
if (url === "/jobapplications/choices") {
|
|
return Promise.resolve({
|
|
data: [
|
|
{
|
|
id: 42,
|
|
jobTitle: "Backend Developer",
|
|
companyName: "Acme",
|
|
},
|
|
{
|
|
id: 77,
|
|
jobTitle: "Platform Engineer",
|
|
companyName: "Beta",
|
|
},
|
|
],
|
|
} as 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: gmailConnected, gmailAddress: "user@example.test", lastSyncedAt: new Date().toISOString(), lastSyncAttemptedAt: new Date().toISOString(), lastSyncMode: "list-messages", lastSyncSource: "custom-query", lastSyncStatus: "error", lastSyncError: "Token refresh failed" } } as any);
|
|
}
|
|
if (url === "/gmail/connect-url") {
|
|
return Promise.resolve({ data: { url: gmailAuthorizationUrl } } 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/relink-thread") {
|
|
correspondenceMessages = [];
|
|
return Promise.resolve({ data: { threadId: body.threadId, jobApplicationId: body.jobApplicationId, imported: 1, skipped: 0, unlinkedMessages: 1 } } as any);
|
|
}
|
|
if (url === "/gmail/unlink-thread") {
|
|
const removed = correspondenceMessages.filter((message) => message.externalThreadId === body.threadId).length;
|
|
correspondenceMessages = correspondenceMessages.filter((message) => message.externalThreadId !== body.threadId);
|
|
return Promise.resolve({ data: { threadId: body.threadId, jobApplicationId: body.jobApplicationId, removedMessages: removed, decision: body.nextDecision || 'review' } } as any);
|
|
}
|
|
if (url === "/gmail/refresh-linked-threads") {
|
|
const hasReply = correspondenceMessages.some((message) => message.externalMessageId === "msg-2");
|
|
if (!hasReply && correspondenceMessages.some((message) => message.externalThreadId === "thread-1")) {
|
|
correspondenceMessages = [
|
|
...correspondenceMessages,
|
|
{
|
|
id: 701,
|
|
jobApplicationId: 42,
|
|
from: "Me",
|
|
content: "Following up on the role.",
|
|
subject: "Backend Developer follow-up",
|
|
channel: "Email",
|
|
date: new Date().toISOString(),
|
|
externalMessageId: "msg-2",
|
|
externalThreadId: "thread-1",
|
|
externalFrom: "user@example.test",
|
|
externalTo: "Maria Recruiter <maria@acme.test>",
|
|
},
|
|
];
|
|
return Promise.resolve({
|
|
data: {
|
|
jobApplicationId: body.jobApplicationId,
|
|
threadsChecked: 1,
|
|
imported: 1,
|
|
skipped: 1,
|
|
hasLinkedThreads: true,
|
|
refreshedAt: new Date().toISOString(),
|
|
threads: [
|
|
{
|
|
threadId: "thread-1",
|
|
imported: 1,
|
|
skipped: 1,
|
|
totalMessages: 2,
|
|
status: "imported-new-messages",
|
|
latestMessageDate: new Date().toISOString(),
|
|
},
|
|
],
|
|
},
|
|
} as any);
|
|
}
|
|
|
|
return Promise.resolve({
|
|
data: {
|
|
jobApplicationId: body.jobApplicationId,
|
|
threadsChecked: correspondenceMessages.some((message) => message.externalThreadId === "thread-1") ? 1 : 0,
|
|
imported: 0,
|
|
skipped: correspondenceMessages.some((message) => message.externalThreadId === "thread-1") ? correspondenceMessages.filter((message) => message.externalThreadId === "thread-1").length : 0,
|
|
hasLinkedThreads: correspondenceMessages.some((message) => message.externalThreadId === "thread-1"),
|
|
refreshedAt: new Date().toISOString(),
|
|
threads: correspondenceMessages.some((message) => message.externalThreadId === "thread-1")
|
|
? [{ threadId: "thread-1", imported: 0, skipped: correspondenceMessages.filter((message) => message.externalThreadId === "thread-1").length, totalMessages: correspondenceMessages.filter((message) => message.externalThreadId === "thread-1").length, status: "already-current", latestMessageDate: new Date().toISOString() }]
|
|
: [],
|
|
},
|
|
} as 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(() => {
|
|
window.open = originalWindowOpen;
|
|
window.localStorage.clear();
|
|
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("automatically refreshes already-linked Gmail threads without manual re-import", async () => {
|
|
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: "msg-1",
|
|
externalThreadId: "thread-1",
|
|
externalFrom: "Maria Recruiter <maria@acme.test>",
|
|
externalTo: "user@example.test",
|
|
},
|
|
];
|
|
|
|
renderDialog();
|
|
|
|
await waitFor(() => {
|
|
expect(mockedApi.post).toHaveBeenCalledWith("/gmail/refresh-linked-threads", { jobApplicationId: 42 });
|
|
});
|
|
|
|
expect(await screen.findByText(/backend developer follow-up/i)).toBeInTheDocument();
|
|
expect(await screen.findByText(/following up on the role\./i)).toBeInTheDocument();
|
|
expect((await screen.findAllByText(/thread thread-1/i)).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("lets the user unlink a linked Gmail thread", async () => {
|
|
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: "msg-1",
|
|
externalThreadId: "thread-1",
|
|
externalFrom: "Maria Recruiter <maria@acme.test>",
|
|
externalTo: "user@example.test",
|
|
},
|
|
];
|
|
|
|
renderDialog();
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: /manage thread-1/i }));
|
|
fireEvent.click(await screen.findByRole("button", { name: /unlink from this job/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(mockedApi.post).toHaveBeenCalledWith("/gmail/unlink-thread", expect.objectContaining({
|
|
jobApplicationId: 42,
|
|
threadId: "thread-1",
|
|
nextDecision: "review",
|
|
}));
|
|
});
|
|
|
|
expect(await screen.findByText(/no messages yet/i)).toBeInTheDocument();
|
|
});
|
|
|
|
test("lets the user move a linked Gmail thread to another job", async () => {
|
|
correspondenceMessages = [
|
|
{
|
|
id: 702,
|
|
jobApplicationId: 42,
|
|
from: "Company",
|
|
content: "Second import.",
|
|
subject: "Backend Developer interview",
|
|
channel: "Email",
|
|
date: new Date().toISOString(),
|
|
externalMessageId: "msg-1",
|
|
externalThreadId: "thread-1",
|
|
externalFrom: "Maria Recruiter <maria@acme.test>",
|
|
externalTo: "user@example.test",
|
|
},
|
|
];
|
|
|
|
renderDialog();
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: /manage thread-1/i }));
|
|
fireEvent.mouseDown(await screen.findByRole("combobox", { name: /move to job/i }));
|
|
fireEvent.click(await screen.findByRole("option", { name: /beta • platform engineer/i }));
|
|
fireEvent.click(screen.getByRole("button", { name: /move thread/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(mockedApi.post).toHaveBeenCalledWith("/gmail/relink-thread", expect.objectContaining({
|
|
jobApplicationId: 77,
|
|
threadId: "thread-1",
|
|
removeFromOtherJobs: true,
|
|
}));
|
|
});
|
|
});
|
|
|
|
test("shows Gmail sync state diagnostics alongside linked thread continuity", async () => {
|
|
renderDialog();
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
|
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
|
|
|
expect(await screen.findByText(/sync checked/i)).toBeInTheDocument();
|
|
expect((await screen.findAllByText(/token refresh failed/i)).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
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',
|
|
}),
|
|
}));
|
|
});
|
|
});
|
|
|
|
test("ignores an unsolicited Gmail success without status, candidate reads or feedback", async () => {
|
|
gmailConnected = false;
|
|
renderDialog();
|
|
await screen.findByText(/no messages yet/i);
|
|
const initialReads = [...mockedApi.get.mock.calls];
|
|
|
|
await oauthMessage({ source: "jobtracker-gmail-oauth", status: "connected", message: "Forged Gmail success" }, window);
|
|
|
|
expect(mockedApi.get.mock.calls).toEqual(initialReads);
|
|
expect(screen.queryByRole("alert", { hidden: true })).not.toBeInTheDocument();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
expect(window.open).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("accepts only the pending Gmail popup and callback origin, including a just-closed popup, once", async () => {
|
|
gmailConnected = false;
|
|
const popup = { closed: false, focus: jest.fn() } as unknown as Window;
|
|
(window.open as jest.Mock).mockReturnValue(popup);
|
|
renderDialog();
|
|
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
|
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
|
fireEvent.click(await screen.findByRole("button", { name: /^connect gmail$/i }));
|
|
await waitFor(() => expect(window.open).toHaveBeenCalledWith(gmailAuthorizationUrl, "jobtracker-gmail-connect", "width=620,height=760,resizable=yes,scrollbars=yes"));
|
|
const pendingReads = [...mockedApi.get.mock.calls];
|
|
const connected = { source: "jobtracker-gmail-oauth", status: "connected" };
|
|
|
|
await oauthMessage(connected, popup, "https://forged.example.test");
|
|
await oauthMessage(connected, window);
|
|
await oauthMessage(connected, null);
|
|
await oauthMessage({ source: "jobtracker-microsoft-oauth", status: "connected" }, popup);
|
|
await oauthMessage({ source: "jobtracker-gmail-oauth", status: "pending" }, popup);
|
|
await oauthMessage({ source: "jobtracker-gmail-oauth", status: "connected", message: 42 }, popup);
|
|
await oauthMessage(null, popup);
|
|
|
|
expect(mockedApi.get.mock.calls).toEqual(pendingReads);
|
|
expect(screen.queryByRole("alert", { hidden: true })).not.toBeInTheDocument();
|
|
gmailConnected = true;
|
|
Object.assign(popup, { closed: true });
|
|
await oauthMessage(connected, popup);
|
|
|
|
expect(await screen.findByText("Google account linked.")).toBeInTheDocument();
|
|
await screen.findByLabelText(/search gmail/i);
|
|
expect(mockedApi.get.mock.calls.filter(([url]) => url === "/gmail/status")).toHaveLength(2);
|
|
expect(mockedApi.get).toHaveBeenCalledWith("/gmail/job-candidates", expect.objectContaining({ params: expect.objectContaining({ jobApplicationId: 42 }) }));
|
|
const acceptedReads = [...mockedApi.get.mock.calls];
|
|
|
|
await oauthMessage({ ...connected, message: "Duplicate terminal callback" }, popup);
|
|
|
|
expect(mockedApi.get.mock.calls).toEqual(acceptedReads);
|
|
expect(screen.queryByText("Duplicate terminal callback")).not.toBeInTheDocument();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("keeps a pending Gmail popup across locale changes and reports terminal error in the current locale once", async () => {
|
|
gmailConnected = false;
|
|
const popup = { closed: false, focus: jest.fn() } as unknown as Window;
|
|
(window.open as jest.Mock).mockReturnValue(popup);
|
|
renderDialog();
|
|
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
|
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
|
fireEvent.click(await screen.findByRole("button", { name: /^connect gmail$/i }));
|
|
await waitFor(() => expect(window.open).toHaveBeenCalledTimes(1));
|
|
await act(async () => {
|
|
window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" }));
|
|
});
|
|
await screen.findByRole("button", { name: /^koble til gmail$/i });
|
|
const pendingReads = [...mockedApi.get.mock.calls];
|
|
|
|
await oauthMessage({ source: "jobtracker-gmail-oauth", status: "error" }, popup);
|
|
|
|
expect(await screen.findByText("Google-autentisering mislyktes.")).toBeInTheDocument();
|
|
expect(screen.queryByText("Google authentication failed.")).not.toBeInTheDocument();
|
|
expect(mockedApi.get.mock.calls).toEqual(pendingReads);
|
|
await oauthMessage({ source: "jobtracker-gmail-oauth", status: "connected", message: "Error callback already consumed" }, popup);
|
|
expect(mockedApi.get.mock.calls).toEqual(pendingReads);
|
|
expect(screen.queryByText("Error callback already consumed")).not.toBeInTheDocument();
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("does not open a Gmail popup when its connect URL resolves after unmount", async () => {
|
|
gmailConnected = false;
|
|
let resolveConnect!: (value: { data: { url: string } }) => void;
|
|
const connect = new Promise<any>((resolve) => { resolveConnect = resolve; });
|
|
const get = mockedApi.get.getMockImplementation()!;
|
|
mockedApi.get.mockImplementation((url: string, config?: any) => url === "/gmail/connect-url" ? connect : get(url, config));
|
|
const view = renderDialog();
|
|
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
|
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
|
fireEvent.click(await screen.findByRole("button", { name: /^connect gmail$/i }));
|
|
expect(mockedApi.get).toHaveBeenCalledWith("/gmail/connect-url");
|
|
view.unmount();
|
|
const disposedReads = [...mockedApi.get.mock.calls];
|
|
|
|
await act(async () => { resolveConnect({ data: { url: gmailAuthorizationUrl } }); });
|
|
|
|
expect(window.open).not.toHaveBeenCalled();
|
|
expect(mockedApi.get.mock.calls).toEqual(disposedReads);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("invalidates the pending Gmail callback when the correspondence job changes", async () => {
|
|
gmailConnected = false;
|
|
const popup = { closed: false, focus: jest.fn() } as unknown as Window;
|
|
(window.open as jest.Mock).mockReturnValue(popup);
|
|
const view = renderDialog();
|
|
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
|
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
|
fireEvent.click(await screen.findByRole("button", { name: /^connect gmail$/i }));
|
|
await waitFor(() => expect(window.open).toHaveBeenCalledTimes(1));
|
|
|
|
view.rerender(dialog(43));
|
|
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith("/correspondence/43"));
|
|
const currentJobReads = [...mockedApi.get.mock.calls];
|
|
await oauthMessage({ source: "jobtracker-gmail-oauth", status: "connected", message: "Previous job Gmail callback" }, popup);
|
|
|
|
expect(mockedApi.get.mock.calls).toEqual(currentJobReads);
|
|
expect(screen.queryByText("Previous job Gmail callback")).not.toBeInTheDocument();
|
|
expect(mockedApi.get.mock.calls.filter(([url]) => url === "/gmail/job-candidates")).toHaveLength(0);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test.each(["blocked", "rejected"] as const)("delayed %s Gmail startup feedback follows a locale change while the connect URL is pending", async outcome => {
|
|
gmailConnected = false;
|
|
let resolveConnect!: (value: any) => void;
|
|
let rejectConnect!: (cause: unknown) => void;
|
|
const pending = new Promise<any>((resolve, reject) => { resolveConnect = resolve; rejectConnect = reject; });
|
|
const get = mockedApi.get.getMockImplementation()!;
|
|
mockedApi.get.mockImplementation((url: string, config?: any) => url === "/gmail/connect-url" ? pending : get(url, config));
|
|
(window.open as jest.Mock).mockReturnValue(null);
|
|
renderDialog();
|
|
fireEvent.click(await screen.findByRole("button", { name: /import email/i }));
|
|
fireEvent.click(await screen.findByRole("tab", { name: /^google$/i }));
|
|
fireEvent.click(await screen.findByRole("button", { name: /^connect gmail$/i }));
|
|
expect(mockedApi.get).toHaveBeenCalledWith("/gmail/connect-url");
|
|
await act(async () => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" })); });
|
|
await screen.findByRole("button", { name: /^koble til gmail$/i });
|
|
const reads = [...mockedApi.get.mock.calls];
|
|
await act(async () => {
|
|
if (outcome === "blocked") resolveConnect({ data: { url: gmailAuthorizationUrl } });
|
|
else rejectConnect(new Error("offline"));
|
|
});
|
|
const key = outcome === "blocked" ? "correspondenceBlockedPopup" : "correspondenceStartGmailFailed";
|
|
expect(await screen.findByText(translations.nb[key])).toBeVisible();
|
|
expect(screen.queryByText(translations.en[key])).not.toBeInTheDocument();
|
|
expect(mockedApi.get.mock.calls).toEqual(reads); expect(window.open).toHaveBeenCalledTimes(outcome === "blocked" ? 1 : 0);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
});
|