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:
parent
79ae69e79d
commit
d51f97527e
3 changed files with 405 additions and 0 deletions
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue