mirror of
https://github.com/mosh-hamedani/helpdesk.git
synced 2026-05-21 11:58:19 +02:00
239 lines
7.2 KiB
TypeScript
239 lines
7.2 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { loginAsAdmin } from "../fixtures/auth";
|
|
import type { InboundEmailInput } from "core/schemas/tickets.ts";
|
|
|
|
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
|
|
const API_BASE_URL = process.env.BETTER_AUTH_URL!;
|
|
|
|
/**
|
|
* Creates a ticket via the inbound email webhook and returns the ticket object.
|
|
*/
|
|
async function createTicketViaWebhook(
|
|
request: Parameters<Parameters<typeof test>[1]>[0]["request"],
|
|
payload: InboundEmailInput
|
|
) {
|
|
const response = await request.post(
|
|
`${API_BASE_URL}/api/webhooks/inbound-email`,
|
|
{
|
|
headers: { "x-webhook-secret": WEBHOOK_SECRET },
|
|
data: payload,
|
|
}
|
|
);
|
|
expect(response.status()).toBe(201);
|
|
const body = await response.json();
|
|
return body.ticket as {
|
|
id: number;
|
|
subject: string;
|
|
senderName: string;
|
|
senderEmail: string;
|
|
body: string;
|
|
bodyHtml: string | null;
|
|
status: string;
|
|
category: string | null;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds a unique inbound-email payload so tests don't collide with each other.
|
|
*/
|
|
function createTestPayload(
|
|
uniqueId: string,
|
|
overrides?: Partial<InboundEmailInput>
|
|
): InboundEmailInput {
|
|
return {
|
|
from: `sender-${uniqueId}@example.com`,
|
|
fromName: `Test Sender ${uniqueId}`,
|
|
subject: `Test Subject ${uniqueId}`,
|
|
body: `This is the body of ticket ${uniqueId}`,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test.describe("Ticket Detail Page", () => {
|
|
test("should redirect unauthenticated user to login", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const uniqueId = `nav-unauth-${Date.now()}`;
|
|
const ticket = await createTicketViaWebhook(
|
|
request,
|
|
createTestPayload(uniqueId)
|
|
);
|
|
|
|
await page.goto(`/tickets/${ticket.id}`);
|
|
|
|
await expect(page).toHaveURL("/login");
|
|
});
|
|
|
|
test("should persist ticket updates after page reload", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const uniqueId = `persist-updates-${Date.now()}`;
|
|
const ticket = await createTicketViaWebhook(
|
|
request,
|
|
createTestPayload(uniqueId)
|
|
);
|
|
|
|
await loginAsAdmin(page);
|
|
await page.goto(`/tickets/${ticket.id}`);
|
|
|
|
// Update status
|
|
const statusPatch = page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes(`/api/tickets/${ticket.id}`) &&
|
|
resp.request().method() === "PATCH" &&
|
|
resp.status() === 200
|
|
);
|
|
await page.getByRole("combobox").filter({ hasText: "Open" }).click();
|
|
await page.getByRole("option", { name: /^Resolved$/ }).click();
|
|
await statusPatch;
|
|
|
|
// Update category
|
|
const categoryPatch = page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes(`/api/tickets/${ticket.id}`) &&
|
|
resp.request().method() === "PATCH" &&
|
|
resp.status() === 200
|
|
);
|
|
await page.getByRole("combobox").filter({ hasText: "None" }).click();
|
|
await page.getByRole("option", { name: /^Technical$/ }).click();
|
|
await categoryPatch;
|
|
|
|
// Update assignment
|
|
const assignPatch = page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes(`/api/tickets/${ticket.id}`) &&
|
|
resp.request().method() === "PATCH" &&
|
|
resp.status() === 200
|
|
);
|
|
await page
|
|
.getByRole("combobox")
|
|
.filter({ hasText: "Unassigned" })
|
|
.click();
|
|
await page.getByRole("option", { name: /^Admin$/ }).click();
|
|
await assignPatch;
|
|
|
|
// Reload and verify all changes persisted
|
|
await page.reload();
|
|
|
|
await expect(
|
|
page.getByRole("combobox").filter({ hasText: "Resolved" })
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("combobox").filter({ hasText: "Technical" })
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("combobox").filter({ hasText: "Admin" })
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("should persist replies after page reload", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const uniqueId = `persist-reply-${Date.now()}`;
|
|
const ticket = await createTicketViaWebhook(
|
|
request,
|
|
createTestPayload(uniqueId)
|
|
);
|
|
|
|
await loginAsAdmin(page);
|
|
await page.goto(`/tickets/${ticket.id}`);
|
|
|
|
const replyText = `Persisted reply for ${uniqueId}`;
|
|
|
|
const postPromise = page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes(`/api/tickets/${ticket.id}/replies`) &&
|
|
resp.request().method() === "POST" &&
|
|
resp.status() === 201
|
|
);
|
|
|
|
await page.getByPlaceholder(/type your reply/i).fill(replyText);
|
|
await page.getByRole("button", { name: /send reply/i }).click();
|
|
await postPromise;
|
|
|
|
await page.reload();
|
|
|
|
await expect(page.getByText(replyText)).toBeVisible();
|
|
});
|
|
|
|
test("should complete full agent workflow: navigate, view, update, reply, and return to list", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const uniqueId = `workflow-${Date.now()}`;
|
|
const payload = createTestPayload(uniqueId, {
|
|
body: `Customer question for ${uniqueId}`,
|
|
});
|
|
const ticket = await createTicketViaWebhook(request, payload);
|
|
|
|
await loginAsAdmin(page);
|
|
|
|
// Navigate from list to detail
|
|
await page.goto("/tickets");
|
|
const subjectLink = page.getByRole("link", {
|
|
name: ticket.subject,
|
|
exact: true,
|
|
});
|
|
await expect(subjectLink).toBeVisible();
|
|
await subjectLink.click();
|
|
await expect(page).toHaveURL(`/tickets/${ticket.id}`);
|
|
|
|
// Verify ticket details
|
|
await expect(
|
|
page.getByRole("heading", { name: ticket.subject })
|
|
).toBeVisible();
|
|
const fromRow = page.locator("div").filter({ hasText: /^From:\s/ });
|
|
await expect(fromRow.first()).toContainText(ticket.senderName);
|
|
await expect(fromRow.first()).toContainText(ticket.senderEmail);
|
|
await expect(page.getByText(ticket.body, { exact: true })).toBeVisible();
|
|
|
|
// Update status
|
|
const statusPatch = page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes(`/api/tickets/${ticket.id}`) &&
|
|
resp.request().method() === "PATCH" &&
|
|
resp.status() === 200
|
|
);
|
|
await page.getByRole("combobox").filter({ hasText: "Open" }).click();
|
|
await page.getByRole("option", { name: /^Resolved$/ }).click();
|
|
await statusPatch;
|
|
await expect(
|
|
page.getByRole("combobox").filter({ hasText: "Resolved" })
|
|
).toBeVisible();
|
|
|
|
// Update category
|
|
const categoryPatch = page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes(`/api/tickets/${ticket.id}`) &&
|
|
resp.request().method() === "PATCH" &&
|
|
resp.status() === 200
|
|
);
|
|
await page.getByRole("combobox").filter({ hasText: "None" }).click();
|
|
await page.getByRole("option", { name: /^Technical$/ }).click();
|
|
await categoryPatch;
|
|
await expect(
|
|
page.getByRole("combobox").filter({ hasText: "Technical" })
|
|
).toBeVisible();
|
|
|
|
// Add a reply
|
|
const replyText = `Agent resolution note for ${uniqueId}`;
|
|
const postPromise = page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes(`/api/tickets/${ticket.id}/replies`) &&
|
|
resp.request().method() === "POST" &&
|
|
resp.status() === 201
|
|
);
|
|
await page.getByPlaceholder(/type your reply/i).fill(replyText);
|
|
await page.getByRole("button", { name: /send reply/i }).click();
|
|
await postPromise;
|
|
await expect(page.getByText(replyText)).toBeVisible();
|
|
|
|
// Navigate back to list
|
|
await page.getByRole("link", { name: /back to tickets/i }).click();
|
|
await expect(page).toHaveURL("/tickets");
|
|
});
|
|
});
|