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 index c2d13f8bb..f1196aa0c 100644 --- 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 @@ -1,38 +1,34 @@ import { test, expect } from '@playwright/test'; /** - * Draft card edit mode tests — MSW mock mode. + * Extraction chip display tests — MSW mock mode. * - * DraftDiffCard has inline edit mode triggered by the "Edit value" button. - * All selectors confirmed from DraftDiffCard.tsx: + * After the assistant extracts attributes from a chat message, read-only + * ExtractionChips appear below the message thread. Each chip shows: + * - A label span (e.g. "Height", "Hair Color", "Languages") + * - A value span (e.g. "168", "blonde", "en, is") * - * 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 + * Chips update on every AI reply — only the last AI message with extractions + * is shown. Sending a second message replaces the previous chip set. * - * 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") + * MSW extraction for "I'm 5'6, have blonde hair and slim build, speak English and Icelandic": + * height_cm: 168 → chip "Height" / "168" + * hair_color: blonde → chip "Hair Color" / "blonde" + * languages: ['en','is'] → chip "Languages" / "en, is" * - * 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" + * MSW extraction for "I speak Spanish and German": + * languages: ['es','de'] → chip "Languages" / "es, de" */ 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 }) => { +test.describe('Profile assistant — extraction chip display', () => { + test('extraction chips show label and value for each extracted attribute', async ({ page }) => { await page.goto(EDITOR_URL); await page.waitForLoadState('networkidle'); - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + await page.waitForSelector('button:has-text("Essentials")', { timeout: 30_000 }); const fab = page.getByRole('button', { name: 'Open profile assistant' }); await fab.click(); @@ -45,29 +41,18 @@ test.describe('Profile assistant — draft card edit mode', () => { 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'); + // All three extracted attributes appear as chips + await expect(dialog.locator('text=Height').first()).toBeVisible({ timeout: 15_000 }); + await expect(dialog.locator('text=168').first()).toBeVisible({ timeout: 3_000 }); + await expect(dialog.locator('text=Hair Color').first()).toBeVisible({ timeout: 3_000 }); + await expect(dialog.locator('text=blonde').first()).toBeVisible({ timeout: 3_000 }); + await expect(dialog.locator('text=Languages').first()).toBeVisible({ timeout: 3_000 }); }); - test('Cancel edit reverts to normal card state', async ({ page }) => { + test('sending a second message replaces chips with latest extractions', async ({ page }) => { await page.goto(EDITOR_URL); await page.waitForLoadState('networkidle'); - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + await page.waitForSelector('button:has-text("Essentials")', { timeout: 30_000 }); const fab = page.getByRole('button', { name: 'Open profile assistant' }); await fab.click(); @@ -77,29 +62,28 @@ test.describe('Profile assistant — draft card edit mode', () => { 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"); + + // First message — height and hair extracted + await input.fill("I'm 5'6, have blonde hair"); + await input.press('Enter'); + await expect(dialog.locator('text=Height').first()).toBeVisible({ timeout: 15_000 }); + + // Second message — only languages extracted, replaces previous chip set + await input.fill('I speak Spanish and German'); await input.press('Enter'); - const firstDraftCard = page.locator('[aria-label*="Draft change for"]').first(); - await firstDraftCard.waitFor({ state: 'visible', timeout: 15_000 }); + // Languages chip with new values appears + await expect(dialog.locator('text=Languages').first()).toBeVisible({ timeout: 15_000 }); + await expect(dialog.locator('text=es, de').first()).toBeVisible({ timeout: 5_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'); + // Height chip from first message is gone (chips reflect only last AI extraction) + await expect(dialog.locator('text=168')).not.toBeVisible({ timeout: 3_000 }); }); - test('saving edited value confirms attribute and enables Preview Draft', async ({ page }) => { + test('Preview Draft auto-enables once extraction chips appear', async ({ page }) => { await page.goto(EDITOR_URL); await page.waitForLoadState('networkidle'); - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + await page.waitForSelector('button:has-text("Essentials")', { timeout: 30_000 }); const fab = page.getByRole('button', { name: 'Open profile assistant' }); await fab.click(); @@ -108,27 +92,16 @@ test.describe('Profile assistant — draft card edit mode', () => { 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 + // Preview Draft is disabled before any message const previewBtn = dialog.getByRole('button', { name: /preview draft/i }).first(); + await expect(previewBtn).toBeDisabled({ timeout: 3_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'); + + // Chips appear → fetchDraftPreview fires → hasDrafts=true → button enabled + await expect(dialog.locator('text=Height').first()).toBeVisible({ timeout: 15_000 }); await expect(previewBtn).not.toBeDisabled({ timeout: 8_000 }); }); }); diff --git a/features/profile/frontend-showcase/e2e/mock/assistant-editor.spec.ts b/features/profile/frontend-showcase/e2e/mock/assistant-editor.spec.ts index c649f8143..2c99b7668 100644 --- a/features/profile/frontend-showcase/e2e/mock/assistant-editor.spec.ts +++ b/features/profile/frontend-showcase/e2e/mock/assistant-editor.spec.ts @@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'; /** * Profile assistant tests on the Editor route — MSW mock mode. * - * IMPORTANT: These tests are serial — each test builds on state created by - * prior interactions with the same MSW session. + * IMPORTANT: These tests are serial — each test builds on state from the + * same session. Serial mode avoids MSW service-worker re-activation delays. * * On the editor route the AssistantProvider detects the "editor" context via * useLocation/useParams. The session creation MSW handler returns: @@ -13,17 +13,23 @@ import { test, expect } from '@playwright/test'; * - quick replies: "Physical traits", "Services", "Use template" * * When the user describes physical traits, the MSW /messages handler returns - * an AI reply with extracted attributes as draft cards: - * - generic regions with accessible name "Draft change for Height", etc. - * - "Confirm change" button inside each draft card - * - "Preview Draft" button in the assistant footer (disabled until confirmed) + * an AI reply with extracted attributes. These are rendered as read-only + * ExtractionChips (label + value spans) below the message thread. + * After sendMessage, fetchDraftPreview fires automatically → hasDrafts=true + * → "Preview Draft" button becomes enabled without any confirmation step. * - * Accessibility selectors confirmed from snapshot: + * Accessibility selectors: * dialog "Profile assistant" * textbox "Message input" - * generic "Draft change for Height" - * button "Confirm change" (text: "Confirm") - * button "Preview Draft" + * button "Preview Draft" (disabled until hasDrafts=true) + * + * Extraction chips are plain styled spans with no aria roles — use text + * content selectors to assert their presence. + * + * MSW extraction for "I'm 5'6, have blonde hair and slim build, speak English and Icelandic": + * height_cm: 168 → chip label "Height", chip value "168" + * hair_color: blonde → chip label "Hair Color", chip value "blonde" + * languages: ['en','is'] → chip label "Languages", chip value "en, is" */ test.describe.configure({ mode: 'serial' }); @@ -36,7 +42,7 @@ test.describe('Profile assistant — editor context', () => { await page.waitForLoadState('networkidle'); // Wait for profile editor to fully load (categories visible) - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + await page.waitForSelector('button:has-text("Essentials")', { timeout: 30_000 }); const fab = page.getByRole('button', { name: 'Open profile assistant' }); await expect(fab).toBeVisible({ timeout: 10_000 }); @@ -54,7 +60,7 @@ test.describe('Profile assistant — editor context', () => { await page.goto(EDITOR_URL); await page.waitForLoadState('networkidle'); - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + await page.waitForSelector('button:has-text("Essentials")', { timeout: 30_000 }); const fab = page.getByRole('button', { name: 'Open profile assistant' }); await fab.click(); @@ -68,113 +74,77 @@ test.describe('Profile assistant — editor context', () => { await expect(dialog.locator('button', { hasText: /use template/i })).toBeVisible({ timeout: 5_000 }); }); - test('describing physical traits extracts attributes and shows draft cards', async ({ page }) => { + test('describing physical traits shows extraction chips', async ({ page }) => { await page.goto(EDITOR_URL); await page.waitForLoadState('networkidle'); - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + 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 }); - - // Wait for welcome message before interacting await page.waitForSelector('[role="dialog"] p', { timeout: 8_000 }); - // Send a description that triggers attribute extraction 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'); - // MSW returns AI reply with draft cards — each has aria-label="Draft change for X" - const draftCard = page.locator('[aria-label*="Draft change for"]').first(); - await expect(draftCard).toBeVisible({ timeout: 15_000 }); + // MSW extracts height_cm → chip with label "Height" and value "168" + await expect(dialog.locator('text=Height').first()).toBeVisible({ timeout: 15_000 }); + await expect(dialog.locator('text=168').first()).toBeVisible({ timeout: 3_000 }); - // Confirm button is present on the first draft card - const confirmBtn = draftCard.getByRole('button', { name: /confirm/i }); - await expect(confirmBtn).toBeVisible({ timeout: 5_000 }); + // hair_color → chip with label "Hair Color" + await expect(dialog.locator('text=Hair Color').first()).toBeVisible({ timeout: 3_000 }); }); - test('confirming a draft attribute applies the change', async ({ page }) => { + test('extraction chips are read-only with no confirm or edit buttons', async ({ page }) => { await page.goto(EDITOR_URL); await page.waitForLoadState('networkidle'); - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + 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 - const firstDraftCard = page.locator('[aria-label*="Draft change for"]').first(); - await firstDraftCard.waitFor({ state: 'visible', timeout: 15_000 }); + // Wait for extraction chips to appear + await expect(dialog.locator('text=Height').first()).toBeVisible({ timeout: 15_000 }); - // Capture current draft card count - const initialDraftCount = await page.locator('[aria-label*="Draft change for"]').count(); - - // Click confirm on the first draft - const confirmBtn = firstDraftCard.getByRole('button', { name: /confirm/i }); - await confirmBtn.click(); - - // After confirming, either the card disappears or its state changes - // We wait for any change: either count decreases or the confirm button state changes - await page.waitForFunction( - (count: number) => { - const cards = document.querySelectorAll('[role="generic"][aria-label*="Draft change for"]'); - // Either fewer cards remain, or the confirm button is gone/disabled - return cards.length < count || !document.querySelector('button[aria-label*="Confirm"], button:has-text("Confirm")'); - }, - initialDraftCount, - { timeout: 8_000 }, - ).catch(() => { - // Acceptable: button may transition to "Confirmed" state rather than disappearing - }); - - // Verify the confirmation was accepted — at minimum the dialog is still open - await expect(dialog).toBeVisible(); + // Chips are read-only annotations — no confirm, edit, or cancel buttons + await expect(dialog.getByRole('button', { name: /confirm/i })).not.toBeVisible(); + await expect(dialog.getByRole('button', { name: /edit value/i })).not.toBeVisible(); + await expect(dialog.getByRole('button', { name: /cancel edit/i })).not.toBeVisible(); }); - test('Preview Draft button enables after at least one confirmation', async ({ page }) => { + test('Preview Draft enables automatically after extraction', async ({ page }) => { await page.goto(EDITOR_URL); await page.waitForLoadState('networkidle'); - await page.waitForSelector('button:has-text("Essentials")', { timeout: 15_000 }); + 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 to appear - const firstDraftCard = page.locator('[aria-label*="Draft change for"]').first(); - await firstDraftCard.waitFor({ state: 'visible', timeout: 15_000 }); - - // Confirm the first draft - const confirmBtn = firstDraftCard.getByRole('button', { name: /confirm/i }); - await confirmBtn.click(); - - // Preview Draft button should be enabled after confirmation triggers a preview fetch. - // After hasDrafts=true, TWO "Preview draft" buttons render (header icon + footer button). - // Use first() to avoid strict-mode multiple-match violation. + // sendMessage auto-calls fetchDraftPreview → hasDrafts=true → button enabled + // No confirmation step required. const previewBtn = dialog.getByRole('button', { name: /preview draft/i }).first(); - await expect(previewBtn).toBeVisible({ timeout: 8_000 }); - await expect(previewBtn).not.toBeDisabled({ timeout: 8_000 }); + await expect(previewBtn).not.toBeDisabled({ timeout: 15_000 }); }); });