diff --git a/features/profile/frontend-showcase/e2e/mock/assistant-draft-modal.spec.ts b/features/profile/frontend-showcase/e2e/mock/assistant-draft-modal.spec.ts new file mode 100644 index 000000000..ee4557312 --- /dev/null +++ b/features/profile/frontend-showcase/e2e/mock/assistant-draft-modal.spec.ts @@ -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 { + 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 }); + }); +}); diff --git a/features/profile/frontend-showcase/e2e/mock/assistant-editor-edit-mode.spec.ts b/features/profile/frontend-showcase/e2e/mock/assistant-editor-edit-mode.spec.ts new file mode 100644 index 000000000..c2d13f8bb --- /dev/null +++ b/features/profile/frontend-showcase/e2e/mock/assistant-editor-edit-mode.spec.ts @@ -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 }); + }); +}); diff --git a/features/profile/frontend-showcase/e2e/mock/assistant-manage.spec.ts b/features/profile/frontend-showcase/e2e/mock/assistant-manage.spec.ts new file mode 100644 index 000000000..3fc3f2f9c --- /dev/null +++ b/features/profile/frontend-showcase/e2e/mock/assistant-manage.spec.ts @@ -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 }); + }); +});