IKHSAN
ENID Baca dalam Bahasa Indonesia

opendesk

Public
Branch
main
Latest commit
08d9f3328 October 2026

people.spec.ts

Open raw
import { expect, test, uid, usePersona } from "../../helpers/fixtures";
import { personas } from "../../helpers/personas";
import {
  createThrowawayAgent,
  openSettings,
} from "../../helpers/settings";
import type { Locator, Page } from "@playwright/test";
import type { Api } from "../../helpers/api";

usePersona("admin");

test("every settings tab opens without errors", async ({ page }) => {
  const errors: string[] = [];
  page.on("pageerror", (error) => errors.push(error.message));
  page.on("response", (response) => {
    if (response.url().includes("/api/") && response.status() >= 500) {
      errors.push(`${response.status()} ${response.url()}`);
    }
  });
  await page.goto("/opendesk/tickets");
  const dialog = await openSettings(page, "Preferences");
  const tabs = dialog.getByRole("navigation").getByRole("button");
  expect(await tabs.count()).toBeGreaterThan(10);

  for (const tab of await tabs.all()) {
    await tab.click();
    await expect(dialog.getByRole("heading", { level: 1 }).nth(1)).toBeVisible();
  }
  expect(errors).toEqual([]);
});

test("inviting an agent shows it pending, and accepting creates the agent", async ({ page, api }) => {
  const [cancelled, accepted] = [`e2e-cancel-${uid()}@example.com`, `e2e-invite-${uid()}@example.com`];
  await page.goto("/opendesk/tickets");
  const dialog = await openSettings(page, "Invite Agents");

  await dialog.getByRole("textbox", { name: /Invite by email/ }).fill(`${cancelled}, ${accepted}`);
  // The textarea's v-model is debounced by 100ms.
  await page.waitForTimeout(300);
  await dialog.getByRole("button", { name: "Send Invites" }).click();
  const pending = dialog.getByRole("listitem");
  await expect(pending.filter({ hasText: cancelled })).toBeVisible();
  await expect(pending.filter({ hasText: accepted })).toBeVisible();

  await pending.filter({ hasText: cancelled }).getByRole("button").click();
  await expect(pending.filter({ hasText: cancelled })).toHaveCount(0);
  expect(await invitationStatus(api, cancelled)).toBe("Cancelled");

  const key = await invitationKey(api, accepted);
  const response = await page.request.get(
    `/api/method/frappe.core.api.user_invitation.accept_invitation?key=${key}`,
    { maxRedirects: 0 }
  );
  expect(response.status()).toBeLessThan(400);
  expect(await invitationStatus(api, accepted)).toBe("Accepted");
  expect(await api.exists("OD Agent", { user: accepted })).toBeTruthy();
});

test("inviting an agent ticks the invite onboarding step", async ({ page, api }) => {
  const steps = [{ name: "invite_agents", completed: false }];
  await api.call("frappe.onboarding.update_user_onboarding_status", {
    steps: JSON.stringify(steps),
    appName: "opendesk",
  });
  await page.addInitScript(() => localStorage.clear());
  await page.goto("/opendesk/tickets");
  const dialog = await openSettings(page, "Invite Agents");
  await dialog.getByRole("textbox", { name: /Invite by email/ }).fill(`e2e-step-${uid()}@example.com`);
  await page.waitForTimeout(300);
  await dialog.getByRole("button", { name: "Send Invites" }).click();
  await expect
    .poll(async () => {
      const status = await api.call("frappe.onboarding.get_onboarding_status");
      return status.opendesk_onboarding_status?.find((step) => step.name === "invite_agents")?.completed;
    })
    .toBe(true);
});

test("a disabled agent disappears from the assignee list", async ({ page, api }) => {
  const email = `e2e-disable-${uid()}@example.com`;
  const agent = await createThrowawayAgent(api, email);
  const ticket = await api.insert("OD Ticket", { subject: `Disable check ${uid()}`, description: "x" });

  await page.goto(`/opendesk/tickets/${ticket.name}`);
  await expect(await assigneeOption(page, agent.agent_name)).toBeVisible();

  const dialog = await openSettings(page, "Agents");
  await dialog.getByRole("textbox", { name: "Search" }).fill(email);
  const row = dialog.locator("div.group").filter({ hasText: email });
  await row.getByRole("button").last().click();
  await page.getByRole("menuitem", { name: "Disable Agent" }).click();
  await expect.poll(async () => (await api.get("OD Agent", email)).is_active).toBe(0);

  await page.goto(`/opendesk/tickets/${ticket.name}`);
  await expect(await assigneeOption(page, agent.agent_name)).toHaveCount(0);
});

test("teams can be created, staffed, renamed, disabled and deleted", async ({ page, api }) => {
  const name = `E2E Team ${uid()}`;
  const renamed = `${name} Renamed`;
  await page.goto("/opendesk/tickets");
  const dialog = await openSettings(page, "Teams");

  await dialog.getByRole("button", { name: "New" }).click();
  await dialog.getByRole("textbox", { name: /Team Name/ }).fill(name);
  await pickAgent(page, dialog, "Bela E2E");
  await dialog.getByRole("button", { name: "Save", exact: true }).click();
  await expect.poll(() => teamUsers(api, name)).toEqual([personas.agent2.email]);

  await pickAgent(page, dialog, "Arjun E2E");
  await dialog.getByRole("button", { name: "Add Member" }).click();
  await expect
    .poll(() => teamUsers(api, name))
    .toEqual([personas.agent2.email, personas.agent.email].sort());

  await teamMenu(page, dialog, "Rename");
  const rename = page.getByRole("dialog", { name: "Rename team" });
  await rename.getByRole("textbox", { name: "Title" }).fill(renamed);
  await rename.getByRole("button", { name: "Confirm" }).click();
  await expect.poll(() => api.exists("OD Team", { name: renamed })).toBeTruthy();

  await dialog.getByText(renamed, { exact: true }).click();
  await dialog.getByRole("switch").click();
  await expect.poll(async () => (await api.get("OD Team", renamed)).disabled).toBe(1);

  await teamMenu(page, dialog, "Delete");
  await page.getByRole("menuitem", { name: "Confirm Delete" }).click();
  await expect.poll(() => api.exists("OD Team", { name: renamed })).toBeFalsy();
});

async function pickAgent(page: Page, dialog: Locator, agentName: string) {
  await dialog.getByPlaceholder("Type agent name or email").fill(agentName);
  await page.getByRole("option", { name: agentName }).click();
}

async function teamMenu(page: Page, dialog: Locator, item: string) {
  const header = dialog.getByText("Enabled", { exact: true }).locator("xpath=../..");
  await header.getByRole("button").click();
  await page.getByRole("menuitem", { name: item }).click();
}

async function teamUsers(api: Api, name: string) {
  if (!(await api.exists("OD Team", { name }))) return [];
  const team = await api.get("OD Team", name);
  return team.users.map((row) => row.user).sort();
}

async function assigneeOption(page: Page, agentName: string) {
  const field = page
    .getByText("Assignee", { exact: true })
    .locator("xpath=ancestor::div[.//button][1]");
  await field.getByRole("button").click();
  const search = page.getByRole("textbox", { name: "Search agents..." });
  await search.fill(agentName);
  return page.getByRole("dialog").getByRole("button", { name: agentName });
}

async function invitationStatus(api: Api, email: string) {
  const [invitation] = await api.list("User Invitation", {
    fields: ["status"],
    filters: { email },
  });
  return invitation?.status;
}

// The raw key only exists in the invitation email, so read it from the queue.
async function invitationKey(api: Api, email: string) {
  const [mail] = await api.list("Email Queue", {
    fields: ["message"],
    filters: [["Email Queue Recipient", "recipient", "=", email]],
  });
  const body = mail.message.replace(/=\r?\n/g, "").replace(/=3D/g, "=");
  return body.match(/accept_invitation\?key=(\w+)/)?.[1];
}
bytes
7,576
lines
175
Type
ts
Latest commit
28 October 2026