diff --git a/nala/blocks/brand-concierge/TEST-PLAN.md b/nala/blocks/brand-concierge/TEST-PLAN.md new file mode 100644 index 0000000000..d86c34a9ee --- /dev/null +++ b/nala/blocks/brand-concierge/TEST-PLAN.md @@ -0,0 +1,276 @@ +# Brand Concierge (BC) — Test Plan + +## Overview + +Brand Concierge is an AI-powered chat experience on Adobe.com. Testing is split into: + +- **Automated tests** — deterministic page rendering, block variants, floating button behavior, aria/config checks. Lives in `brand-concierge.test.js`. +- **Manual tests** — AI-driven chat flows where the response is non-deterministic (product recommendations, advisor handoff, meeting booking, etc.). Executed before each release. + +--- + +## Environments + +| Env | URL | +|-----|-----| +| Live / Prod | `https://main--milo--adobecom.aem.live` | +| Stage | `https://stage--milo--adobecom.aem.live` | +| PR branch | `https://--milo--adobecom.aem.live` | + +Test content pages live under `/drafts/nala/blocks/brand-concierge/`. + +--- + +## Automated Test Coverage + +Run: `PR_BRANCH_LIVE_URL= npx playwright test nala/blocks/brand-concierge/brand-concierge.test.js --project=milo-live-chromium --grep='@regression'` + +| Tcid | Test | Focus | +|------|------|-------| +| 0 | `@brand-concierge default` | Default inline variant renders | +| 1 | `@brand-concierge hero` | Hero variant renders | +| 2 | `@brand-concierge 404` | 404 variant renders | +| 3 | `@brand-concierge floating button` | Inline + floating button, stays visible on scroll | +| 4 | `@brand-concierge floating button delay` | `floating-delay-*` class + scroll threshold | +| 5 | `@brand-concierge hero floating button` | Hero + floating button initially hidden, appears after scrolling past hero | +| 6 | `@brand-concierge floating button only` | Floating-only variant, no inline block content | +| 7 | `@brand-concierge floating anchor hide` | `floating-anchor-hide` class behavior | +| 8 | `@brand-concierge modal open close` | Floating button → chat modal opens → Escape closes → floating button reattaches | +| 9 | `@brand-concierge web client preload` | BC agent `main.js` script is injected during block init | +| 10 | `@brand-concierge webclient baseStage param` | `?webclient=baseStage` loads the base-stage script URL | +| 11 | `@brand-concierge hero floating aria-hidden` | Hero floating button starts with `aria-hidden="true"` | +| 12 | `@brand-concierge consent hide block` | Without C0002 consent, block gets `hide-block` class | + +--- + +## Manual Test Plan + +Run the below on a pre-release build. Open browser DevTools if a check references network or DOM state. + +### TC-M1: Product Recommendation Flow + +**URL:** `/drafts/nala/blocks/brand-concierge/brand-concierge` + +1. Open the default BC page. +2. Type: `I want to touch up and enhance my photos`, press Enter. +3. Modal should open with an assistant reply. + +**Expected:** +- Reply mentions a photo-related Adobe product (Photoshop / Lightroom). +- `Sources` section is expandable. +- Thumbs up / thumbs down feedback icons are visible. +- Product card (if rendered) is clickable and links to the correct product page. + +--- + +### TC-M2: Firefly Gallery Widget + +**URL:** `/drafts/nala/blocks/brand-concierge/brand-concierge` + +1. Ask: `Show me Firefly community creations`. +2. Wait for the assistant response. + +**Expected:** +- A Firefly gallery widget renders inside the chat. +- Gallery thumbnails are clickable. +- Clicking `More in Firefly gallery` navigates to `firefly.adobe.com`. +- On stage env, gallery widget should point to `firefly-stage.corp.adobe.com`. + +--- + +### TC-M3: Advisor Connection (Sales Handoff) + +**URL:** `/drafts/nala/blocks/brand-concierge/brand-concierge` (or `business.adobe.com` equivalent) + +1. Ask: `I want to talk to sales about Firefly Services`. +2. Reply to the follow-up question (e.g. `I want to know more about pricing`). + +**Expected:** +- System prompts: "We'd be happy to connect you to an advisor. First, can you give us a better idea of what you'd like to discuss?" +- Then: "Connecting you to an advisor who can help." +- Chat transitions to advisor mode: + - Banner: "You are now connected to …" + - Input placeholder changes to `Type a response` + - `End connection` button appears. +- Clicking `End connection` returns to AI mode. + +--- + +### TC-M4: Meeting Booking & Calendar + +1. Trigger the book-a-meeting intent (advisor flow can lead into it). +2. Meeting form renders. + +**Expected:** +- Form title is empty (behavior-driven) and subtitle reads: + `To connect you with the right person for a demo, I'll need a few details…` +- Text/email/phone/number fields lay out 2 per row. +- Dropdowns and checkboxes lay out 1 per row. +- Submit button label: `Schedule a meeting` (left-aligned). +- After submitting contact info, calendar widget appears. +- Calendar subtitle: `Thanks for providing your contact information! Please select a meeting date and time.` +- Confirm button: `Schedule a meeting`. + +--- + +### TC-M5: Sign-in / SUSI Light + +**Pre-requisite:** Logged out. + +1. Trigger a CTA that requires auth (varies by intent — e.g. saving a project). +2. SUSI modal opens. + +**Expected:** +- Modal title: `Sign in or create an account`. +- Options visible: Continue with Google / Facebook / Apple / Email. +- On stage env, the SUSI script loads from `auth-light.identity-stage.adobe.com`. +- Successful login: + - SUSI modal closes. + - Chat remains open with message history intact. + - `window.adobeIMS.isSignedInUser()` returns true (check in DevTools). +- Cancelling: + - Modal closes without side effects. + +--- + +### TC-M6: Navigation Persistence (Floating Entry Point) + +1. Open a page with BC floating button (e.g. `bc-floating-button`). +2. Open the chat, send a message. +3. Click a link to a different Adobe page (Products / Solutions / etc.). +4. Observe the new page. + +**Expected:** +- Floating entry point appears on the new page. +- Clicking it reopens the chat with prior history preserved (if backed by persistence). +- `aria-hidden` is not present while visible. + +--- + +### TC-M7: Comparison Tables + +1. Ask: `Compare Photoshop vs Lightroom`. + +**Expected:** +- Comparison table renders inside the chat with feature rows for both products. +- Table is scrollable on mobile. +- No layout breakage on small widths. + +--- + +### TC-M8: Citations / Sources + +1. Ask any factual product question. +2. Expand the `Sources` section below the reply. + +**Expected:** +- Citation numbers (`[1]`, `[2]`…) appear inline and inside Sources. +- Clicking a citation link opens the correct Adobe.com page in a new tab. +- No broken links. + +--- + +### TC-M9: Mobile Keyboard White Area (MWPW regression) + +**Pre-requisite:** Mobile device or browser devtools mobile emulation. + +1. Open BC on mobile, open chat modal. +2. Tap the input — keyboard slides up. +3. Type something, then dismiss keyboard (tap outside or close button). + +**Expected:** +- No white empty area below chat content when keyboard is dismissed. +- Modal resizes correctly; input stays anchored to the bottom. +- Rotating the device recalculates height correctly. + +--- + +### TC-M10: Chat Input Max-Width 800px + +**Pre-requisite:** Desktop ≥ 1440px wide. + +1. Open any BC variant and open the chat modal. +2. Measure the chat input section width (DevTools → inspect `.input-container`). + +**Expected:** +- Effective max-width is ≤ 800px even on very wide viewports. +- Input stays centered within the modal. + +--- + +### TC-M11: Modal Height on Orientation Change + +**Pre-requisite:** Mobile or tablet. + +1. Open the chat modal in portrait. +2. Rotate to landscape. +3. Rotate back to portrait. + +**Expected:** +- Modal height adjusts to viewport after rotation (no overflow, no clipping). +- Input + submit button remain visible. + +--- + +### TC-M12: Error / Loading States (mock network slow) + +1. In DevTools → Network, set throttling to `Slow 3G`. +2. Submit a chat question. + +**Expected:** +- Loading dots indicator appears. +- If request times out or fails, error message is friendly: + `I'm sorry, I'm having trouble connecting…` or `I'm sorry, something went wrong…`. +- After failure, user can retry without reloading the page. + +--- + +### TC-M13: Feedback Submission + +1. Send any question, wait for a reply. +2. Click thumbs up OR thumbs down. +3. Feedback dialog appears. + +**Expected:** +- Positive dialog: `Your feedback is appreciated` + `What went well? Select all that apply.` +- Negative dialog: same title + `What went wrong?` +- Options include `Other`, free-form notes field is optional. +- After submit: toast `Thank you for the feedback.` appears. +- Cancel closes the dialog without submitting. + +--- + +### TC-M14: Web Client Switching (stage QA only) + +URL patterns (append to any BC page): + +| Param | Expected source | +|-------|-----------------| +| `?webclient=prod` | `adobe-brand-concierge-acom…` on `experience.adobe.net` | +| `?webclient=stage` | `adobe-brand-concierge-acom…` on `experience-stage.adobe.net` | +| `?webclient=baseProd` | `experience-platform-brand-concierge…` on `experience.adobe.net` | +| `?webclient=baseStage` | `experience-platform-brand-concierge…` on `experience-stage.adobe.net` | + +**Expected:** +- DevTools → Network → the corresponding `main.js` is requested. +- Console logs `prod` / `stage` / `baseProd` / `baseStage` with the URL. + +--- + +## Release Checklist + +Before tagging a BC release, confirm: + +- [ ] Automated suite passes: 13/13 `@regression` +- [ ] Manual TC-M1 through TC-M14 pass on stage +- [ ] Accessibility: no new axe violations (check console on each test page) +- [ ] Mobile smoke: TC-M9, TC-M11 on real device (iOS Safari + Android Chrome) +- [ ] Sign-in: TC-M5 verified with a real IMS account on stage +- [ ] Advisor flow: TC-M3 verified with QA advisor account + +--- + +## Known Issues + +- **MWPW-190449** — color-contrast axe violation on `.bc-floating-input` for the `bc-floating-anchor-hide` variant. Automated a11y skip in place for tcid 7. +- **`tab-index` attribute typo** in `brand-concierge.js` floating-button hide state (should be `tabindex`). Minor a11y gap; tracked separately. diff --git a/nala/blocks/brand-concierge/brand-concierge.page.js b/nala/blocks/brand-concierge/brand-concierge.page.js index 42aaeb8d5d..a5ab4cdaad 100644 --- a/nala/blocks/brand-concierge/brand-concierge.page.js +++ b/nala/blocks/brand-concierge/brand-concierge.page.js @@ -31,5 +31,16 @@ export default class BrandConciergeBlock { // Page-level content elements this.pageHeadings = this.page.locator('h1, h2, h3'); this.pageBody = this.page.locator('p'); + + // Modal close/dismiss elements + this.modalCloseButton = this.page.locator('#brand-concierge-modal .dialog-close'); + this.modalCurtain = this.page.locator('.modal-curtain'); + + // Web client script loader (used for pre-load + ?webclient= tests) + // Matches base/prod/stage URLs for BC agent main.js + this.webClientScript = this.page.locator( + 'script[src*="adobe-brand-concierge-acom-brand-concierge-web-agent/static-assets/main.js"], ' + + 'script[src*="experience-platform-brand-concierge-web-agent/static-assets/main.js"]', + ); } } diff --git a/nala/blocks/brand-concierge/brand-concierge.spec.js b/nala/blocks/brand-concierge/brand-concierge.spec.js index 36500e24f1..d17d6b6e94 100644 --- a/nala/blocks/brand-concierge/brand-concierge.spec.js +++ b/nala/blocks/brand-concierge/brand-concierge.spec.js @@ -103,5 +103,43 @@ module.exports = { delayClass: 'floating-delay-100', }, }, + { + tcid: '8', + name: '@brand-concierge modal open close', + path: '/drafts/nala/blocks/brand-concierge/bc-floating-button', + tags: '@brand-concierge @brand-concierge-modal @regression @milo', + data: {}, + }, + { + tcid: '9', + name: '@brand-concierge web client preload', + path: '/drafts/nala/blocks/brand-concierge/brand-concierge', + tags: '@brand-concierge @brand-concierge-preload @regression @milo', + data: { scriptPattern: 'brand-concierge-web-agent/static-assets/main.js' }, + }, + { + tcid: '10', + name: '@brand-concierge webclient baseStage param', + path: '/drafts/nala/blocks/brand-concierge/brand-concierge?webclient=baseStage', + tags: '@brand-concierge @brand-concierge-webclient @regression @milo', + data: { + scriptPattern: 'experience-platform-brand-concierge-web-agent/static-assets/main.js', + envPattern: 'experience-stage.adobe.net', + }, + }, + { + tcid: '11', + name: '@brand-concierge hero floating aria-hidden', + path: '/drafts/nala/blocks/brand-concierge/bc-hero-floating-button', + tags: '@brand-concierge @brand-concierge-aria-hidden @regression @milo', + data: {}, + }, + { + tcid: '12', + name: '@brand-concierge consent hide block', + path: '/drafts/nala/blocks/brand-concierge/brand-concierge', + tags: '@brand-concierge @brand-concierge-consent @regression @milo', + data: {}, + }, ], }; diff --git a/nala/blocks/brand-concierge/brand-concierge.test.js b/nala/blocks/brand-concierge/brand-concierge.test.js index a38b15a371..755a3c1ae9 100644 --- a/nala/blocks/brand-concierge/brand-concierge.test.js +++ b/nala/blocks/brand-concierge/brand-concierge.test.js @@ -357,4 +357,160 @@ test.describe('Milo Brand Concierge Block test suite', () => { // Skipping a11y test: known color-contrast violation on bc-floating-input (MWPW-190449) }, ); + + // Test 8: Brand Concierge modal open/close via floating button + test( + `[Test Id - ${features[8].tcid}] ${features[8].name},${features[8].tags}`, + async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[8].path}${miloLibs}`); + + await test.step('step-1: Go to Brand Concierge floating button page', async () => { + await page.goto(`${baseURL}${features[8].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[8].path}${miloLibs}`); + }); + + await test.step('step-2: Verify modal is not present initially', async () => { + await expect(bc.modal).toHaveCount(0); + }); + + await test.step('step-3: Click floating button opens modal with mount + close button', async () => { + await expect(bc.floatingButton).toBeVisible(); + await bc.floatingButtonContainer.click(); + + await expect(bc.modal).toBeVisible({ timeout: 10000 }); + await expect(bc.modalMount).toBeVisible({ timeout: 10000 }); + await expect(bc.modalCloseButton).toBeVisible(); + }); + + await test.step('step-4: Press Escape dismisses modal', async () => { + await page.keyboard.press('Escape'); + await expect(bc.modal).not.toBeVisible({ timeout: 5000 }); + }); + + await test.step('step-5: Floating button is still attached after modal close', async () => { + await expect(bc.floatingButton).toBeAttached(); + }); + + await test.step('step-6: Re-open modal via click and close via close button', async () => { + await bc.floatingButtonContainer.click(); + await expect(bc.modal).toBeVisible({ timeout: 10000 }); + await bc.modalCloseButton.click(); + await expect(bc.modal).not.toBeVisible({ timeout: 5000 }); + }); + }, + ); + + // Test 9: Brand Concierge web client pre-load + test( + `[Test Id - ${features[9].tcid}] ${features[9].name},${features[9].tags}`, + async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[9].path}${miloLibs}`); + const { data } = features[9]; + + await test.step('step-1: Go to Brand Concierge default page', async () => { + await page.goto(`${baseURL}${features[9].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('step-2: Verify BC block is initialized', async () => { + await expect(bc.block).toBeVisible(); + }); + + await test.step('step-3: Verify web client main.js script is pre-loaded into the DOM', async () => { + // Script is injected as the final step of init(), before any modal opens. + await expect(bc.webClientScript.first()).toBeAttached({ timeout: 10000 }); + + const scriptSrc = await bc.webClientScript.first().getAttribute('src'); + expect(scriptSrc).toContain(data.scriptPattern); + }); + }, + ); + + // Test 10: Brand Concierge ?webclient=baseStage URL parameter + test( + `[Test Id - ${features[10].tcid}] ${features[10].name},${features[10].tags}`, + async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[10].path}${miloLibs}`); + const { data } = features[10]; + + await test.step('step-1: Navigate with ?webclient=baseStage query', async () => { + await page.goto(`${baseURL}${features[10].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(bc.block).toBeVisible(); + }); + + await test.step('step-2: Verify baseStage web agent script is loaded', async () => { + // baseStage uses experience-platform-brand-concierge-web-agent on experience-stage.adobe.net + await expect(bc.webClientScript.first()).toBeAttached({ timeout: 10000 }); + + const scripts = await page.locator('script').evaluateAll( + (nodes) => nodes.map((n) => n.getAttribute('src')).filter(Boolean), + ); + const matched = scripts.find((src) => src.includes(data.scriptPattern) + && src.includes(data.envPattern)); + expect(matched, `Expected script src containing both "${data.scriptPattern}" and "${data.envPattern}"`).toBeTruthy(); + }); + }, + ); + + // Test 11: Hero floating button has aria-hidden when initially hidden + test( + `[Test Id - ${features[11].tcid}] ${features[11].name},${features[11].tags}`, + async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[11].path}${miloLibs}`); + + await test.step('step-1: Go to Hero floating button page', async () => { + await page.goto(`${baseURL}${features[11].path}${miloLibs}`); + await page.waitForLoadState('networkidle'); + }); + + await test.step('step-2: Verify floating button is in hidden state', async () => { + await expect(bc.floatingButtonHidden).toBeAttached({ timeout: 10000 }); + }); + + await test.step('step-3: Verify floating button container has aria-hidden=true', async () => { + await expect(bc.floatingButtonContainer).toHaveAttribute('aria-hidden', 'true'); + }); + + await test.step('step-4: After scrolling past hero, aria-hidden is removed', async () => { + await page.evaluate(() => { + const el = document.querySelector('.brand-concierge.hero'); + if (el) window.scrollTo(0, el.scrollHeight + 1); + }); + await page.waitForTimeout(1000); + + await expect(bc.floatingButton).not.toHaveClass(/floating-hidden/, { timeout: 10000 }); + const ariaHidden = await bc.floatingButtonContainer.getAttribute('aria-hidden'); + expect(ariaHidden).toBeNull(); + }); + }, + ); + + // Test 12: Consent — block is hidden when C0002 cookie group is not granted + test( + `[Test Id - ${features[12].tcid}] ${features[12].name},${features[12].tags}`, + async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[12].path}${miloLibs}`); + + await test.step('step-1: Stub window.adobePrivacy before navigation (no C0002 consent)', async () => { + await page.addInitScript(() => { + // Provide a fake adobePrivacy that reports no C0002 consent. + Object.defineProperty(window, 'adobePrivacy', { + configurable: true, + value: { activeCookieGroups: () => ['C0001'] }, + }); + }); + }); + + await test.step('step-2: Go to Brand Concierge page', async () => { + await page.goto(`${baseURL}${features[12].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('step-3: Verify block has hide-block class', async () => { + await expect(bc.block).toHaveClass(/hide-block/, { timeout: 10000 }); + }); + }, + ); });