IKHSAN
ENID Baca dalam Bahasa Indonesia

opendesk

Public
Branch
main
Latest commit
08d9f3328 October 2026

rules.spec.ts

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

const RULE = "Assignment Rule";
const DEPENDENCY = "Field Dependency-ticket_type-priority";
const DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];

usePersona("admin");

test.describe("field dependencies", () => {
  test.afterEach(async ({ api }) => {
    if (await api.exists("OD Form Script", { name: DEPENDENCY })) await api.delete("OD Form Script", DEPENDENCY);
  });

  test("the builder maps values per parent, then edits, disables and deletes the dependency", async ({ page, api }) => {
    await page.goto("/opendesk/tickets");
    const dialog = await openSettings(page, "Field Dependencies");
    await dialog.getByRole("button", { name: "New" }).click();
    await pickOption(page, dialog.getByRole("combobox").first(), "Ticket Type");
    await pickOption(page, dialog.getByRole("combobox").nth(1), "Priority");

    // Search narrows the child list, and Select All only takes the filtered values.
    await parentValue(dialog, "Bug").click();
    await dialog.getByPlaceholder("Search Bug values").fill("Urg");
    await dialog.getByText("Select All", { exact: true }).click();
    await expect(dialog.getByText("1 value selected")).toBeVisible();
    await dialog.getByPlaceholder("Search Bug values").fill("");
    await expect(childValue(dialog, "Low").getByRole("checkbox")).not.toBeChecked();

    await parentValue(dialog, "Question").click();
    await childValue(dialog, "Low").click();
    await childValue(dialog, "Medium").click();
    await expect(dialog.getByText("2 values selected")).toBeVisible();
    await expect(parentValue(dialog, "Bug")).toContainText("1");

    await turnOffCriteria(dialog);
    await dialog.getByRole("button", { name: "Save", exact: true }).click();
    await expect.poll(() => mapping(api)).toEqual({ Bug: ["Urgent"], Question: ["Low", "Medium"] });

    const row = dialog.locator("div.grid-cols-11").filter({ hasText: "Ticket Type → Priority" });
    await row.getByText("Ticket Type → Priority").click();
    await expect(dialog.getByRole("combobox").first()).toBeDisabled();
    await parentValue(dialog, "Question").click();
    await childValue(dialog, "Medium").click();
    await dialog.getByRole("button", { name: "Save", exact: true }).click();
    await expect.poll(() => mapping(api)).toEqual({ Bug: ["Urgent"], Question: ["Low"] });

    // The back button carries the dependency's label.
    await dialog.getByRole("button", { name: /Ticket Type → Priority/ }).first().click();
    await row.getByRole("switch").click();
    await expect.poll(async () => (await api.get("OD Form Script", DEPENDENCY)).enabled).toBe(0);

    await row.getByRole("button").last().click();
    await page.getByRole("menuitem", { name: "Delete" }).click();
    await page.getByRole("menuitem", { name: "Confirm Delete" }).click();
    await expect.poll(() => api.exists("OD Form Script", { name: DEPENDENCY })).toBeFalsy();
  });

  test("going back with unsaved choices asks before discarding them", async ({ page, api }) => {
    await page.goto("/opendesk/tickets");
    const dialog = await openSettings(page, "Field Dependencies");
    await dialog.getByRole("button", { name: "New" }).click();
    await pickOption(page, dialog.getByRole("combobox").first(), "Ticket Type");
    await dialog.getByRole("button", { name: "New Field Dependency" }).click();
    const confirm = page.getByRole("dialog", { name: "Unsaved changes" });
    await confirm.getByRole("button", { name: "Confirm" }).click();
    await expect(dialog.getByRole("button", { name: "New", exact: true })).toBeVisible();
    expect(await api.exists("OD Form Script", { name: DEPENDENCY })).toBeFalsy();
  });
});

test.describe("assignment rules", () => {
  let rule: string;

  test.beforeEach(async ({ api }) => {
    rule = `E2E Rule ${uid()}`;
    // The condition names a team that never exists, so the rule never grabs real tickets.
    await api.insert(RULE, {
      name: rule,
      assignment_rule_name: rule,
      document_type: "OD Ticket",
      description: "Route nothing",
      rule: "Round Robin",
      priority: 0,
      assign_condition: `agent_group == "${rule}"`,
      users: [{ user: personas.agent2.email }],
      assignment_days: DAYS.map((day) => ({ day })),
    });
  });

  test.afterEach(async ({ api }) => {
    for (const name of [rule, `${rule} (Copy)`, `${rule} Renamed`]) {
      if (await api.exists(RULE, { name })) await api.delete(RULE, name);
    }
  });

  test("the list changes priority, disables, duplicates and deletes a rule", async ({ page, api }) => {
    await page.goto("/opendesk/tickets");
    const dialog = await openSettings(page, "Assignment Rules");
    const row = ruleRow(dialog, rule);

    await row.locator("select").selectOption({ label: "High" });
    await expect.poll(async () => (await api.get(RULE, rule)).priority).toBe(4);
    await row.getByRole("switch").click();
    await expect.poll(async () => (await api.get(RULE, rule)).disabled).toBe(1);

    await row.getByRole("button").last().click();
    await page.getByRole("menuitem", { name: "Duplicate" }).click();
    const duplicate = page.getByRole("dialog", { name: "Duplicate Assignment Rule" });
    await expect(duplicate.getByRole("textbox")).toHaveValue(`${rule} (Copy)`);
    await duplicate.getByRole("button", { name: "Duplicate" }).click();
    await expect.poll(() => api.exists(RULE, { name: `${rule} (Copy)` })).toBeTruthy();
    expect((await api.get(RULE, `${rule} (Copy)`)).users.map((u: { user: string }) => u.user)).toEqual([
      personas.agent2.email,
    ]);

    await dialog.getByRole("button", { name: `${rule} (Copy)` }).first().click();
    await ruleRow(dialog, `${rule} (Copy)`).getByRole("button").last().click();
    await page.getByRole("menuitem", { name: "Delete" }).click();
    await page.getByRole("menuitem", { name: "Confirm Delete" }).click();
    await expect.poll(() => api.exists(RULE, { name: `${rule} (Copy)` })).toBeFalsy();
    expect(await api.exists(RULE, { name: rule })).toBeTruthy();
  });

  test("editing a rule with an old condition renames it and keeps that condition", async ({ page, api }) => {
    await page.goto("/opendesk/tickets");
    const dialog = await openSettings(page, "Assignment Rules");
    await ruleRow(dialog, rule).getByText(rule, { exact: true }).click();
    await expect(dialog.getByText("Old Condition").first()).toBeVisible();

    await dialog.getByRole("textbox", { name: /^Name/ }).fill(`${rule} Renamed`);
    await dialog.getByRole("textbox", { name: /^Description/ }).fill("Still routes nothing");
    await dialog.getByText("Low", { exact: true }).click();
    await page.getByText("Medium-High", { exact: true }).click();
    await dialog.getByRole("button", { name: "Save", exact: true }).click();

    await expect.poll(() => api.exists(RULE, { name: `${rule} Renamed` })).toBeTruthy();
    const saved = await api.get(RULE, `${rule} Renamed`);
    expect(saved).toMatchObject({ description: "Still routes nothing", priority: 3 });
    expect(saved.assign_condition).toBe(`agent_group == "${rule}"`);
  });

  test("saving without a name is refused", async ({ page }) => {
    await page.goto("/opendesk/tickets");
    const dialog = await openSettings(page, "Assignment Rules");
    await ruleRow(dialog, rule).getByText(rule, { exact: true }).click();
    await dialog.getByRole("textbox", { name: /^Name/ }).fill("");
    await dialog.getByRole("button", { name: "Save", exact: true }).click();
    await expect(page.getByText("Invalid fields, check if all are filled in and values are correct.")).toBeVisible();
  });
});

async function pickOption(page: Page, combobox: Locator, option: string) {
  await combobox.click();
  await page.getByRole("option", { name: option, exact: true }).click();
}

function parentValue(dialog: Locator, value: string) {
  return dialog.getByRole("listitem").filter({ hasText: new RegExp(`^${value}\\d*$`) }).first();
}

function childValue(dialog: Locator, value: string) {
  return dialog.getByRole("listitem").filter({ hasText: new RegExp(`^${value}$`) }).last();
}

// Leave the visibility and mandatory rules off so OD Ticket's schema stays untouched.
async function turnOffCriteria(dialog: Locator) {
  for (const rule of [/^Show Priority if/, /^Make Priority mandatory if/]) {
    await dialog.getByText(rule).locator("xpath=ancestor::div[.//button[@role='switch']][1]").getByRole("switch").click();
  }
}

async function mapping(api: Api) {
  if (!(await api.exists("OD Form Script", { name: DEPENDENCY }))) return null;
  const dependency = await api.call("opendesk.api.settings.field_dependency.get_field_dependency", {
    name: DEPENDENCY,
  });
  return JSON.parse(dependency.parent_child_mapping);
}

function ruleRow(dialog: Locator, name: string) {
  return dialog.locator("div.grid-cols-12").filter({ has: dialog.page().getByText(name, { exact: true }) });
}
bytes
9,180
lines
187
Type
ts
Latest commit
28 October 2026