/** * E2E helpers for AssistantSettings pages. */ import type { Page } from '@playwright/test'; import { expect } from '../fixtures'; import { goToAssistantSettings as goToAssistantSettingsRoute } from './navigation'; const ASSISTANT_EDITOR = '[data-testid="assistant-editor-page"], [data-testid="assistant-edit-drawer"]'; function waitForAssistantDetailResponse(page: Page, assistantId: string): Promise { return page .waitForResponse( (response) => { const url = new URL(response.url()); return ( response.request().method() === 'GET' && decodeURIComponent(url.pathname).endsWith(`/api/assistants/${assistantId}`) ); }, { timeout: 5_000 } ) .catch(() => null); } // ── Navigation ────────────────────────────────────────────────────────────── /** Navigate to the assistant settings page via UI clicks. */ export async function goToAssistantSettings(page: Page): Promise { await goToAssistantSettingsRoute(page); } /** Open the assistant editor surface by clicking on an assistant card. */ export async function openAssistantEditor(page: Page, assistant_id: string): Promise { const card = page.locator(`[data-testid="assistant-card-${assistant_id}"]`); const detailResponse = waitForAssistantDetailResponse(page, assistant_id); await expect(card).toBeVisible({ timeout: 8_000 }); const editButton = page.locator(`[data-testid="btn-edit-${assistant_id}"]`).first(); if (await editButton.isVisible().catch(() => false)) { await editButton.click(); } else { await card.click(); } await page.locator(ASSISTANT_EDITOR).waitFor({ state: 'visible', timeout: 5_000 }); await detailResponse; await page.locator('[data-testid="input-assistant-name"]').waitFor({ state: 'visible', timeout: 5_000 }); } /** Click the Create Assistant button. */ export async function clickCreateAssistant(page: Page): Promise { const editor = page.locator(ASSISTANT_EDITOR); if (await editor.isVisible().catch(() => false)) { await closeAssistantEditor(page); await editor.waitFor({ state: 'hidden', timeout: 3_000 }).catch(() => {}); } await page.locator('[data-testid="btn-create-assistant"]').click(); await editor.waitFor({ state: 'visible', timeout: 5_000 }); } // ── CRUD helpers ──────────────────────────────────────────────────────────── /** Fill the assistant name input. */ export async function fillAssistantName(page: Page, name: string): Promise { const input = page.locator('[data-testid="input-assistant-name"]'); await input.clear(); await input.fill(name); } /** Fill the assistant description input. */ export async function fillAssistantDescription(page: Page, description: string): Promise { const input = page.locator('[data-testid="input-assistant-desc"]'); await input.clear(); await input.fill(description); } /** Click the Save/Create button in the assistant editor. */ export async function saveAssistant(page: Page): Promise { for (let attempt = 0; attempt < 3; attempt++) { const button = page.locator('[data-testid="btn-save-assistant"]:visible').last(); await button.waitFor({ state: 'visible', timeout: 5_000 }); try { await button.click(); return; } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!message.includes('detached from the DOM') || attempt === 2) { throw error; } } } } /** Click the Delete button in the assistant editor, then confirm. */ export async function deleteAssistant(page: Page): Promise { await page.locator('[data-testid="btn-delete-assistant"]').click(); // Wait for confirm modal (uses wrapClassName='delete-assistant-modal') const confirmBtn = page.locator('.delete-assistant-modal .arco-btn-status-danger'); await confirmBtn.waitFor({ state: 'visible', timeout: 5_000 }); await confirmBtn.click(); } /** Click the Duplicate link for an assistant. */ export async function duplicateAssistant(page: Page, assistant_id: string): Promise { const dupBtn = page.locator(`[data-testid="btn-duplicate-${assistant_id}"]`); const detailResponse = waitForAssistantDetailResponse(page, assistant_id); await dupBtn.click(); await page.locator(ASSISTANT_EDITOR).waitFor({ state: 'visible', timeout: 5_000 }); await detailResponse; await page.locator('[data-testid="input-assistant-name"]').waitFor({ state: 'visible', timeout: 5_000 }); } /** Toggle the enabled/disabled switch for an assistant. */ export async function toggleAssistantEnabled(page: Page, assistant_id: string): Promise { const sw = page.locator(`[data-testid="switch-enabled-${assistant_id}"]`); const checkedBefore = await sw.getAttribute('aria-checked').catch(() => null); await sw.click(); if (checkedBefore !== null) { await expect .poll(async () => sw.getAttribute('aria-checked').catch(() => null), { timeout: 5_000, }) .not.toBe(checkedBefore); } } // ── Search & Filter ───────────────────────────────────────────────────────── /** Expand search and type a query. */ export async function searchAssistants(page: Page, query: string): Promise { const searchToggle = page.locator('[data-testid="btn-search-toggle"]'); const searchInput = page.locator('[data-testid="input-search-assistant"]'); // If search input not visible, toggle it open if (!(await searchInput.isVisible().catch(() => false))) { await searchToggle.click(); await searchInput.waitFor({ state: 'visible', timeout: 3_000 }); } await searchInput.clear(); await searchInput.fill(query); } /** Clear search by clicking the toggle button (closes search). */ export async function clearSearch(page: Page): Promise { const searchToggle = page.locator('[data-testid="btn-search-toggle"]'); await searchToggle.click(); } /** * Tab text mapping: supports both English and Chinese labels. * The actual text depends on the app's i18n locale. */ const TAB_TEXT_MAP: Record = { All: /^(All|全部)$/i, System: /^(System|系统)$/i, Custom: /^(Custom|自定义)$/i, }; /** Select a filter tab by logical name (All / System / Custom). */ export async function selectFilterTab(page: Page, tabText: string): Promise { const pattern = TAB_TEXT_MAP[tabText] ?? new RegExp(tabText, 'i'); const tab = page.locator('.assistant-filter-tabs .arco-tabs-header-title').filter({ hasText: pattern }); await tab.first().click(); } // ── Assertions ────────────────────────────────────────────────────────────── /** Get all visible assistant card IDs. */ export async function getVisibleAssistantIds(page: Page): Promise { const cards = page.locator('[data-testid^="assistant-card-"]'); const count = await cards.count(); const ids: string[] = []; for (let i = 0; i < count; i++) { const testid = await cards.nth(i).getAttribute('data-testid'); if (testid) ids.push(testid.replace('assistant-card-', '')); } return ids; } /** Get all visible assistant names from cards. */ export async function getVisibleAssistantNames(page: Page): Promise { const cards = page.locator('[data-testid^="assistant-card-"]'); const count = await cards.count(); const names: string[] = []; for (let i = 0; i < count; i++) { const text = await cards.nth(i).locator('.font-medium.text-t-primary span.truncate').first().textContent(); if (text) names.push(text.trim()); } return names; } /** Check if the assistant editor surface is visible. */ export async function isAssistantEditorVisible(page: Page): Promise { return page .locator(ASSISTANT_EDITOR) .isVisible() .catch(() => false); } /** Wait for the assistant editor surface to close (max 5s). */ export async function waitForAssistantEditorClose(page: Page): Promise { await expect(page.locator(ASSISTANT_EDITOR)).not.toBeVisible({ timeout: 5_000 }); } /** Force-close the editor via full-page controls or legacy drawer dismissal. */ export async function closeAssistantEditor(page: Page): Promise { const editor = page.locator(ASSISTANT_EDITOR); if (!(await editor.isVisible().catch(() => false))) return; const backButton = page.locator('[data-testid="btn-back-assistant-editor"]'); if (await backButton.isVisible().catch(() => false)) { await Promise.all([ editor.waitFor({ state: 'hidden', timeout: 5_000 }).catch(() => {}), backButton.click({ force: true }).catch(() => {}), ]); return; } const cancelButton = page.locator('[data-testid="btn-cancel-assistant-editor"]'); if (await cancelButton.isVisible().catch(() => false)) { await Promise.all([ editor.waitFor({ state: 'hidden', timeout: 5_000 }).catch(() => {}), cancelButton.click({ force: true }).catch(() => {}), ]); return; } const drawerWrapper = page.locator('.arco-drawer-wrapper'); if (!(await drawerWrapper.isVisible().catch(() => false))) return; const mask = page.locator('.arco-drawer-mask'); if (await mask.isVisible().catch(() => false)) { await mask.click({ force: true }); } else { await page.locator('body').press('Escape'); } await page .locator('.arco-drawer-wrapper') .waitFor({ state: 'hidden', timeout: 5_000 }) .catch(() => {}); }