chore(profile): 🔧 update E2E test mocks and scenarios to improve accuracy for assistant draft modal interactions, editor edit mode validation, and management action edge cases

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-23 12:48:13 -08:00
parent 79ae69e79d
commit d51f97527e
3 changed files with 405 additions and 0 deletions

View file

@ -0,0 +1,167 @@
import { test, expect, type Page } from '@playwright/test';
/**
* Draft Preview Modal tests MSW mock mode.
*
* The DraftPreviewModal is triggered by clicking "Preview Draft" in the
* ChatPopover footer (enabled after at least one attribute is confirmed).
*
* Component accessibility (from DraftPreviewModal.tsx):
* role="dialog" aria-label="Draft preview"
* button "Close preview" (X icon, aria-label)
* button "Select all" (text content)
* button "Deselect all" (text content)
* span "N selected" (SelectedCount)
* button "Save Selected (N)" (text content, disabled when 0 selected)
* button "Save All" (text content, disabled when 0 items)
* button "Back to Chat" (text content)
*
* MSW /preview handler returns MOCK_DRAFT_ITEMS 5 items:
* age, height, languages, services, incall
*
* ChatFAB wires:
* onSaveAll publishAll() + setShowPreview(false)
* onSaveSelected publishSelected(codes) + setShowPreview(false)
* onBackToChat setShowPreview(false)
* onClose setShowPreview(false)
*/
const EDITOR_URL = '/providers/valeria-reykjavik/edit';
async function openDraftModal(page: Page): Promise<void> {
await page.goto(EDITOR_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('button:has-text("Essentials")', { timeout: 30_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await dialog.waitFor({ state: 'visible', timeout: 5_000 });
await page.waitForSelector('[role="dialog"] p', { timeout: 8_000 });
const input = page.getByRole('textbox', { name: /message input/i });
await input.fill("I'm 5'6, have blonde hair and slim build, speak English and Icelandic");
await input.press('Enter');
// Wait for draft cards (MSW extracts height, hair_color, languages)
const firstDraftCard = page.locator('[aria-label*="Draft change for"]').first();
await firstDraftCard.waitFor({ state: 'visible', timeout: 15_000 });
// Confirm first draft → fetchDraftPreview auto-fires → hasDrafts becomes true
await firstDraftCard.getByRole('button', { name: /confirm/i }).click();
// Preview Draft button becomes enabled
const previewBtn = dialog.getByRole('button', { name: /preview draft/i }).first();
await expect(previewBtn).not.toBeDisabled({ timeout: 8_000 });
// Open the modal
await previewBtn.click();
}
test.describe('Draft Preview Modal', () => {
test('opens after confirming a draft attribute', async ({ page }) => {
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
});
test('displays pending changes count from MSW preview data', async ({ page }) => {
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
// MOCK_DRAFT_ITEMS has 5 items — summary shows "N pending changes"
await expect(modal.locator('text=/\\d+ pending change/i')).toBeVisible({ timeout: 5_000 });
});
test('Select all enables Save Selected with full count', async ({ page }) => {
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
// Initially nothing selected — Save Selected disabled
await expect(modal.locator('text=0 selected')).toBeVisible({ timeout: 5_000 });
await expect(modal.getByRole('button', { name: /save selected/i })).toBeDisabled({ timeout: 3_000 });
// Select all → count updates, Save Selected becomes enabled
// exact: true prevents matching 'Deselect all' (which contains 'select all' as substring)
await modal.getByRole('button', { name: 'Select all', exact: true }).click();
await expect(modal.locator('text=/[1-9]\\d* selected/')).toBeVisible({ timeout: 3_000 });
await expect(modal.getByRole('button', { name: /save selected/i })).not.toBeDisabled({ timeout: 3_000 });
});
test('Deselect all resets selection and disables Save Selected', async ({ page }) => {
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
// Select then deselect
await modal.getByRole('button', { name: 'Select all', exact: true }).click();
await expect(modal.locator('text=/[1-9]\\d* selected/')).toBeVisible({ timeout: 3_000 });
await modal.getByRole('button', { name: 'Deselect all' }).click();
await expect(modal.locator('text=0 selected')).toBeVisible({ timeout: 3_000 });
await expect(modal.getByRole('button', { name: /save selected/i })).toBeDisabled({ timeout: 3_000 });
});
test('Back to Chat closes modal and keeps chat dialog open', async ({ page }) => {
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
// force: true because the modal footer may be below the visible viewport
await modal.getByRole('button', { name: 'Back to Chat' }).click({ force: true });
await expect(modal).not.toBeVisible({ timeout: 3_000 });
await expect(page.getByRole('dialog', { name: 'Profile assistant' })).toBeVisible({ timeout: 3_000 });
});
test('Close preview button dismisses the modal', async ({ page }) => {
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
await modal.getByRole('button', { name: 'Close preview' }).click();
await expect(modal).not.toBeVisible({ timeout: 3_000 });
});
test('Save All publishes drafts and closes modal', async ({ page }) => {
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
// force: true because the modal footer may be below the visible viewport
await modal.getByRole('button', { name: 'Save All' }).click({ force: true });
// ChatFAB calls publishAll() then setShowPreview(false)
await expect(modal).not.toBeVisible({ timeout: 5_000 });
});
test('Save Selected publishes selected drafts and closes modal', async ({ page }) => {
test.setTimeout(60_000);
await openDraftModal(page);
const modal = page.getByRole('dialog', { name: 'Draft preview' });
await expect(modal).toBeVisible({ timeout: 5_000 });
// Select all first to enable button
// exact: true prevents matching 'Deselect all' (which contains 'select all' as substring)
await modal.getByRole('button', { name: 'Select all', exact: true }).click();
const saveSelectedBtn = modal.getByRole('button', { name: /save selected/i });
await expect(saveSelectedBtn).not.toBeDisabled({ timeout: 3_000 });
// dispatchEvent bypasses viewport/scroll constraints for fixed-position modal footer
await saveSelectedBtn.dispatchEvent('click');
// ChatFAB calls publishSelected(codes) then setShowPreview(false)
await expect(modal).not.toBeVisible({ timeout: 5_000 });
});
});

View file

@ -0,0 +1,134 @@
import { test, expect } from '@playwright/test';
/**
* Draft card edit mode tests MSW mock mode.
*
* DraftDiffCard has inline edit mode triggered by the "Edit value" button.
* All selectors confirmed from DraftDiffCard.tsx:
*
* button aria-label="Edit value" toggles edit mode on
* button aria-label="Cancel edit" toggles edit mode off
* button aria-label="Confirm change" text "Confirm" (normal) / "Save" (edit mode)
* input aria-label="Edit value for {label}" text input in edit mode
*
* When edit mode is active:
* - The inline EditInput appears
* - Confirm button text changes to "Save"
* - Edit button text changes to "Cancel" (aria-label: "Cancel edit")
*
* Clicking Confirm/Save calls onConfirm(code, editValue) which triggers
* AssistantProvider.confirmAttribute POST /confirm + GET /preview.
*
* Test message: "I'm 5'6, have blonde hair and slim build, speak English and Icelandic"
* MSW extracts: height (168cm), hair_color (blonde), languages (['en'])
* First card aria-label: "Draft change for Height"
*/
test.describe.configure({ mode: 'serial' });
const EDITOR_URL = '/providers/valeria-reykjavik/edit';
test.describe('Profile assistant — draft card edit mode', () => {
test('Edit button toggles inline input visible', async ({ page }) => {
await page.goto(EDITOR_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await dialog.waitFor({ state: 'visible', timeout: 5_000 });
await page.waitForSelector('[role="dialog"] p', { timeout: 8_000 });
const input = page.getByRole('textbox', { name: /message input/i });
await input.fill("I'm 5'6, have blonde hair and slim build, speak English and Icelandic");
await input.press('Enter');
const firstDraftCard = page.locator('[aria-label*="Draft change for"]').first();
await firstDraftCard.waitFor({ state: 'visible', timeout: 15_000 });
// Click "Edit value"
const editBtn = firstDraftCard.getByRole('button', { name: 'Edit value' });
await expect(editBtn).toBeVisible({ timeout: 5_000 });
await editBtn.click();
// Inline input appears
const editInput = firstDraftCard.locator('[aria-label^="Edit value for"]');
await expect(editInput).toBeVisible({ timeout: 3_000 });
// Edit button switches to "Cancel edit"
await expect(firstDraftCard.getByRole('button', { name: 'Cancel edit' })).toBeVisible({ timeout: 3_000 });
// Confirm button text is now "Save"
await expect(firstDraftCard.getByRole('button', { name: 'Confirm change' })).toContainText('Save');
});
test('Cancel edit reverts to normal card state', async ({ page }) => {
await page.goto(EDITOR_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await dialog.waitFor({ state: 'visible', timeout: 5_000 });
await page.waitForSelector('[role="dialog"] p', { timeout: 8_000 });
const input = page.getByRole('textbox', { name: /message input/i });
await input.fill("I'm 5'6, have blonde hair and slim build, speak English and Icelandic");
await input.press('Enter');
const firstDraftCard = page.locator('[aria-label*="Draft change for"]').first();
await firstDraftCard.waitFor({ state: 'visible', timeout: 15_000 });
// Enter edit mode
await firstDraftCard.getByRole('button', { name: 'Edit value' }).click();
await expect(firstDraftCard.locator('[aria-label^="Edit value for"]')).toBeVisible({ timeout: 3_000 });
// Cancel edit
await firstDraftCard.getByRole('button', { name: 'Cancel edit' }).click();
// Input gone, normal state restored
await expect(firstDraftCard.locator('[aria-label^="Edit value for"]')).not.toBeVisible({ timeout: 3_000 });
await expect(firstDraftCard.getByRole('button', { name: 'Edit value' })).toBeVisible({ timeout: 3_000 });
await expect(firstDraftCard.getByRole('button', { name: 'Confirm change' })).toContainText('Confirm');
});
test('saving edited value confirms attribute and enables Preview Draft', async ({ page }) => {
await page.goto(EDITOR_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await dialog.waitFor({ state: 'visible', timeout: 5_000 });
await page.waitForSelector('[role="dialog"] p', { timeout: 8_000 });
const input = page.getByRole('textbox', { name: /message input/i });
await input.fill("I'm 5'6, have blonde hair and slim build, speak English and Icelandic");
await input.press('Enter');
const firstDraftCard = page.locator('[aria-label*="Draft change for"]').first();
await firstDraftCard.waitFor({ state: 'visible', timeout: 15_000 });
// Enter edit mode and override the extracted value
await firstDraftCard.getByRole('button', { name: 'Edit value' }).click();
const editInput = firstDraftCard.locator('[aria-label^="Edit value for"]');
await editInput.clear();
await editInput.fill('175');
// Save via Confirm button (shows "Save" text in edit mode)
await firstDraftCard.getByRole('button', { name: 'Confirm change' }).click();
// Card exits edit mode — Edit value button returns
await expect(firstDraftCard.getByRole('button', { name: 'Edit value' })).toBeVisible({ timeout: 5_000 });
// confirmAttribute + fetchDraftPreview fire → hasDrafts = true → Preview Draft enabled
const previewBtn = dialog.getByRole('button', { name: /preview draft/i }).first();
await expect(previewBtn).not.toBeDisabled({ timeout: 8_000 });
});
});

View file

@ -0,0 +1,104 @@
import { test, expect } from '@playwright/test';
/**
* Profile assistant tests on the Manage route MSW mock mode.
*
* On the manage route the AssistantProvider detects the "manage" context.
* The session creation MSW handler returns:
* - context: "manage"
* - welcome message mentioning profile management
* - quick replies: "Create profile", "Templates", "Duplicate"
*
* Keyword triggers in simulateManageReply:
* - "create" / "new profile" "Let's create a new profile" reply
* quick replies: "Use a template", "Start from scratch"
* - "template" template options reply
* quick replies: "Escort Basic", "Escort Premium", "Cam Model"
* - "duplicate" / "copy" duplicate profile reply
*
* Accessibility selectors confirmed from component source:
* button "Open profile assistant" (FAB)
* dialog "Profile assistant" (ChatPopover)
* textbox "Message input"
*/
const MANAGE_URL = '/manage';
test.describe('Profile assistant — manage context', () => {
test('FAB opens with manage welcome message on manage page', async ({ page }) => {
await page.goto(MANAGE_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('text=Manage Profiles', { timeout: 15_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await expect(fab).toBeVisible({ timeout: 10_000 });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await expect(dialog).toBeVisible({ timeout: 5_000 });
// Manage context welcome message mentions profile management
await expect(
dialog.locator('p, [role="paragraph"]', { hasText: /manage|create.*profile|template/i }).first(),
).toBeVisible({ timeout: 8_000 });
});
test('manage quick replies include Create profile, Templates and Duplicate', async ({ page }) => {
await page.goto(MANAGE_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('text=Manage Profiles', { timeout: 15_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await dialog.waitFor({ state: 'visible', timeout: 5_000 });
await expect(dialog.locator('button', { hasText: /create profile/i })).toBeVisible({ timeout: 8_000 });
await expect(dialog.locator('button', { hasText: /templates/i })).toBeVisible({ timeout: 5_000 });
await expect(dialog.locator('button', { hasText: /duplicate/i })).toBeVisible({ timeout: 5_000 });
});
test('sending create message triggers new profile reply with template quick replies', async ({ page }) => {
await page.goto(MANAGE_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('text=Manage Profiles', { timeout: 15_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await dialog.waitFor({ state: 'visible', timeout: 5_000 });
await page.waitForSelector('[role="dialog"] p', { timeout: 8_000 });
const input = page.getByRole('textbox', { name: /message input/i });
await input.fill('I want to create a new profile');
await input.press('Enter');
// MSW simulateManageReply returns quick replies: "Use a template" + "Start from scratch"
await expect(dialog.locator('button', { hasText: /use a template/i })).toBeVisible({ timeout: 10_000 });
await expect(dialog.locator('button', { hasText: /start from scratch/i })).toBeVisible({ timeout: 5_000 });
});
test('sending template message returns template options as quick replies', async ({ page }) => {
await page.goto(MANAGE_URL);
await page.waitForLoadState('networkidle');
await page.waitForSelector('text=Manage Profiles', { timeout: 15_000 });
const fab = page.getByRole('button', { name: 'Open profile assistant' });
await fab.click();
const dialog = page.getByRole('dialog', { name: 'Profile assistant' });
await dialog.waitFor({ state: 'visible', timeout: 5_000 });
await page.waitForSelector('[role="dialog"] p', { timeout: 8_000 });
const input = page.getByRole('textbox', { name: /message input/i });
await input.fill('show me templates');
await input.press('Enter');
// MSW returns template quick reply options: "Escort Basic", "Escort Premium", "Cam Model"
await expect(dialog.locator('button', { hasText: /escort basic/i })).toBeVisible({ timeout: 10_000 });
await expect(dialog.locator('button', { hasText: /escort premium/i })).toBeVisible({ timeout: 5_000 });
await expect(dialog.locator('button', { hasText: /cam model/i })).toBeVisible({ timeout: 5_000 });
});
});