IKHSAN
EN Read in EnglishID

opendesk

Publik
Branch
main
Commit terakhir
08d9f3328 Oktober 2026

customers.spec.ts

Buka raw
import type { Page } from "@playwright/test";
import { expectDenied, type Api } from "../../helpers/api";
import {
  createContact,
  createContactWithUser,
  createCustomer,
  customerUrl,
  pageMenu,
} from "../../helpers/directory";
import { expect, test, uid, usePersona } from "../../helpers/fixtures";

usePersona("manager");

test("create a customer with a primary contact, promote a member, list its tickets", async ({
  page,
  api,
}) => {
  const id = uid();
  const name = `E2E Customer ${id}`;
  await page.goto("/opendesk/customers");
  await page.getByRole("button", { name: "Create" }).click();
  const dialog = page.getByRole("dialog", { name: "Create Customer" });
  await dialog.getByRole("textbox", { name: "Name (required)" }).fill(name);
  await dialog.getByRole("textbox", { name: "First Name", exact: true }).fill("Omar");
  await dialog.getByRole("textbox", { name: "Email", exact: true }).fill(`e2e-primary-${id}@example.com`);
  await dialog.getByRole("button", { name: "Create" }).click();
  await expect(page).toHaveURL(new RegExp(customerUrl(name)));

  const member = await createContact(api, "Member");
  const doc = await api.get("OD Customer", name);
  await api.update("OD Customer", name, {
    contacts: [...doc.contacts, { contact_name: member.name }],
  });
  const subject = `E2E customer ticket ${id}`;
  await api.insert("OD Ticket", { subject, description: subject, raised_by: member.email, customer: name });

  await page.reload();
  await expect(page.getByText(subject)).toBeVisible();
  await page.getByRole("tab", { name: /Contacts/ }).click();
  await openCardMenu(page, member.name);
  await page.getByRole("menuitem", { name: "Role" }).hover();
  await page.getByRole("menuitem", { name: "Customer Manager" }).click();
  await confirm(page);

  await expect.poll(async () => (await memberRow(api, name, member.name))?.is_manager).toBe(1);
});

test("a manager edits a customer's domain, then renames it", async ({ page, api }) => {
  const name = await createCustomer(api);
  const renamed = `${name} Renamed`;
  await page.goto(customerUrl(name));

  await page.getByRole("button", { name: "Edit" }).click();
  await page.getByRole("dialog").getByRole("textbox", { name: "Domain" }).fill("e2e.example.com");
  await page.getByRole("dialog").getByRole("button", { name: "Save" }).click();
  await expect(page.getByText("e2e.example.com")).toBeVisible();
  expect((await api.get("OD Customer", name)).domain).toBe("e2e.example.com");

  await page.getByRole("button", { name: "Edit" }).click();
  await page.getByRole("dialog").getByRole("textbox", { name: /^Name/ }).fill(renamed);
  await page.getByRole("dialog").getByRole("button", { name: "Save" }).click();
  await expect(page).toHaveURL(new RegExp(customerUrl(renamed)));
  expect(await api.exists("OD Customer", { name: renamed })).toBe(renamed);
  expect(await api.exists("OD Customer", { name })).toBeUndefined();
});

test("inviting adds an existing user at once and keeps a new email pending until revoked", async ({
  page,
  api,
}) => {
  const existing = await createContactWithUser(api, "Uma");
  const newcomer = `e2e-newcomer-${uid()}@example.com`;
  const name = await createCustomer(api);

  await page.goto(`${customerUrl(name)}#contacts`);
  await page.getByRole("button", { name: "Invite" }).click();
  const dialog = page.getByRole("dialog", { name: "Invite Contact" });
  const input = dialog.getByPlaceholder("Enter email address");
  for (const email of [existing.email, newcomer]) {
    await input.fill(email);
    await input.press("Enter");
  }
  // Escape closes the open suggestions without blurring, so the picker's delayed refocus can't reopen them
  await input.press("Escape");
  await expect(page.getByRole("listbox")).toHaveCount(0);
  await dialog.getByRole("button", { name: "Invite", exact: true }).click();

  await expect(card(page, existing.name)).toBeVisible();
  expect(await memberRow(api, name, existing.name)).toBeTruthy();
  const pending = dialog.locator("div.justify-between").filter({ hasText: newcomer });
  await expect(pending).toBeVisible();
  expect(await invitationStatus(api, newcomer)).toBe("Pending");

  await pending.getByRole("button").click();
  await pending.getByRole("button", { name: "Confirm" }).click();
  await expect(pending).toHaveCount(0);
  expect(await invitationStatus(api, newcomer)).toBe("Cancelled");
});

test("contact cards set the primary contact, revoke manager access and remove a contact", async ({
  page,
  api,
}) => {
  const lead = await createContact(api, "Lead");
  const deputy = await createContact(api, "Deputy");
  const name = await createCustomer(api, [{ name: lead.name, isManager: true }, { name: deputy.name }]);
  await page.goto(`${customerUrl(name)}#contacts`);

  await openCardMenu(page, deputy.name);
  await page.getByRole("menuitem", { name: "Set as Primary" }).click();
  await confirm(page);
  await expect(card(page, deputy.name).getByLabel("Primary")).toBeVisible();
  expect((await api.get("OD Customer", name)).primary_contact).toBe(deputy.name);

  await openCardMenu(page, lead.name);
  await page.getByRole("menuitem", { name: "Role" }).hover();
  await page.getByRole("menuitem", { name: "Customer", exact: true }).click();
  await confirm(page);
  await expect.poll(async () => (await memberRow(api, name, lead.name))?.is_manager).toBe(0);

  await openCardMenu(page, lead.name);
  await page.getByRole("menuitem", { name: "Remove Contact" }).click();
  await confirm(page);
  await expect(card(page, lead.name)).toHaveCount(0);
  expect(await memberRow(api, name, lead.name)).toBeUndefined();
});

test("deleting a customer keeps its tickets and clears their customer", async ({ page, api }) => {
  const member = await createContact(api, "Keeper");
  const name = await createCustomer(api, [{ name: member.name }]);
  const ticket = await api.insert("OD Ticket", {
    subject: `E2E kept ${uid()}`,
    raised_by: member.email,
    customer: name,
  });
  await page.goto(customerUrl(name));

  await pageMenu(page).click();
  await page.getByRole("menuitem", { name: "Delete" }).click();
  const dialog = page.getByRole("dialog", { name: "Delete Customer" });
  await expect(dialog.getByRole("checkbox", { name: "Delete 1 ticket(s)" })).not.toBeChecked();
  await dialog.getByRole("button", { name: "Delete" }).click();

  await expect(page).toHaveURL(/\/opendesk\/customers$/);
  expect(await api.exists("OD Customer", { name })).toBeUndefined();
  expect((await api.get("OD Ticket", ticket.name)).customer).toBeFalsy();
});

test.describe("agent", () => {
  usePersona("agent");

  test("an agent reads customers but gets none of the manager actions", async ({ page, api, apiAs }) => {
    const member = await createContact(api, "Viewer");
    const name = await createCustomer(api, [{ name: member.name }]);

    await page.goto(`${customerUrl(name)}#contacts`);
    await expect(card(page, member.name)).toBeVisible();
    await expect(page.getByRole("button", { name: "Edit" })).toHaveCount(0);
    await expect(page.getByRole("button", { name: "Invite" })).toHaveCount(0);
    await expect(card(page, member.name).getByRole("button")).toHaveCount(0);

    const agent = await apiAs("agent");
    await expectDenied(
      agent.raw("frappe.client.set_value", { doctype: "OD Customer", name, fieldname: "domain", value: "x.io" })
    );
    await expectDenied(agent.raw("opendesk.api.customer.create_customer", { customer: { customer_name: `${name} 2` } }));
  });
});

function card(page: Page, contact: string) {
  return page.locator("div.rounded-5").filter({ hasText: contact });
}

async function openCardMenu(page: Page, contact: string) {
  await card(page, contact).getByRole("button").click();
}

function confirm(page: Page) {
  return page.getByRole("dialog").getByRole("button", { name: "Confirm" }).click();
}

async function memberRow(api: Api, customer: string, contact: string) {
  const doc = await api.get("OD Customer", customer);
  return doc.contacts.find((row: { contact_name: string }) => row.contact_name === contact);
}

async function invitationStatus(api: Api, email: string) {
  const [invite] = await api.list("User Invitation", { fields: ["status"], filters: { email } });
  return invite?.status;
}
byte
8.264
baris
191
Jenis
ts
Commit terakhir
28 Oktober 2026