@@ -7,17 +7,15 @@
* The tests run at both desktop (1280× 720) and tablet (768× 1024) viewports
* as required by the acceptance criteria.
*/
import { test , expect } from '@playwright/test' ;
import { test , expect , type Page } from '@playwright/test' ;
/**
* Find a document edit URL to use as the test page.
* Falls back to /documents/new if no existing document is found.
*/
async function getDocumentEditUrl (
page : Parameters < typeof test > [ 1 ] extends ( args : { page : infer P } ) = > unknown ? P : never
) : Promise < string > {
async function getDocumentEditUrl ( page : Page ) : Promise < string > {
await page . goto ( '/' ) ;
await page . waitForSelector ( '[data-hydrated] ' ) ;
await page . waitForLoadState ( 'networkidle ' ) ;
const firstDocLink = page . locator ( 'a[href^="/documents/"]' ) . first ( ) ;
const href = await firstDocLink . getAttribute ( 'href' ) . catch ( ( ) = > null ) ;
if ( href ) {
@@ -26,6 +24,11 @@ async function getDocumentEditUrl(
return '/documents/new' ;
}
/** Wait for the listbox to become visible after triggering a search. */
async function waitForListbox ( page : Page ) : Promise < void > {
await page . waitForSelector ( '[role="listbox"]' , { state : 'visible' , timeout : 2000 } ) ;
}
test . describe ( 'PersonTypeahead — dropdown visibility (desktop)' , ( ) = > {
test . use ( { viewport : { width : 1280 , height : 720 } } ) ;
@@ -42,23 +45,20 @@ test.describe('PersonTypeahead — dropdown visibility (desktop)', () => {
await senderInput . click ( ) ;
await senderInput . fill ( 'a' ) ;
// Wait for the dropdown to appear
await page . waitForTimeout ( 400 ) ; // debounce is 300ms
// Wait for the dropdown to appear (handles debounce automatically)
await waitForListbox ( page ) ;
// If there are results, verify the first item is visible (not occluded)
const dropdown = page . locator ( '[role="listbox"]' ) . first ( ) ;
const hasResults = await dropdown . count ( ) . then ( ( n ) = > n > 0 ) ;
await expect ( dropdown ) . toBeVisible ( ) ;
if ( hasResults ) {
const firstOption = dropdown . locator ( '[role="o ption"]' ) . first ( ) ;
await expect ( firstOption ) . toBeVisible ( ) ;
const firstOption = dropdown . locator ( '[role="option"]' ) . first ( ) ;
await expect ( firstO ption) . toBeVisible ( ) ;
// Verify the bounding box is within the viewport (not clipped)
const box = await firstOption . boundingBox ( ) ;
expect ( box ) . not . toBeNull ( ) ;
expect ( box ! . y ) . toBeGreaterThan ( 0 ) ;
expect ( box ! . y + box ! . height ) . toBeLessThan ( 720 ) ;
}
// Verify the bounding box is within the viewport (not clipped)
const box = await firstOption . boundingBox ( ) ;
expect ( box ) . not . toBeNull ( ) ;
expect ( box ! . y ) . toBeGreaterThan ( 0 ) ;
expect ( box ! . y + box ! . height ) . toBeLessThan ( 720 ) ;
await page . screenshot ( { path : 'test-results/e2e/person-typeahead-dropdown-desktop.png' } ) ;
} ) ;
@@ -78,18 +78,16 @@ test.describe('PersonTypeahead — dropdown visibility (desktop)', () => {
await senderInput . click ( ) ;
await senderInput . fill ( 'a' ) ;
await page . waitForTimeout ( 400 ) ;
await waitForListbox ( page ) ;
const dropdown = page . locator ( '[role="listbox"]' ) . first ( ) ;
const hasDropdown = ( await dropdown . count ( ) ) > 0 ;
await expect ( dropdown ) . toBeVisible ( ) ;
if ( hasDropdown ) {
const dropdownBox = await dropdown . boundingBox ( ) ;
expect ( dropdownBox ) . not . toBeNull ( ) ;
const dropdownBox = await dropdown . boundingBox ( ) ;
expect ( dropdownBox ) . not . toBeNull ( ) ;
// Dropdown must appear below the input, not on top or clipped behind it
expect ( dropdownBox ! . y ) . toBeGreaterThanOrEqual ( inputBox ! . y + inputBox ! . height - 5 ) ;
}
// Dropdown must appear below the input, not on top or clipped behind it
expect ( dropdownBox ! . y ) . toBeGreaterThanOrEqual ( inputBox ! . y + inputBox ! . height - 5 ) ;
await page . screenshot ( { path : 'test-results/e2e/person-typeahead-dropdown-position.png' } ) ;
} ) ;
@@ -108,20 +106,18 @@ test.describe('PersonTypeahead — dropdown visibility (tablet)', () => {
await senderInput . click ( ) ;
await senderInput . fill ( 'a' ) ;
await page . waitForTimeout ( 400 ) ;
await waitForListbox ( page ) ;
const dropdown = page . locator ( '[role="listbox"]' ) . first ( ) ;
const hasResults = ( await dropdown . count ( ) ) > 0 ;
await expect ( dropdown ) . toBeVisible ( ) ;
if ( hasResults ) {
const firstOption = dropdown . locator ( '[role="o ption"]' ) . first ( ) ;
await expect ( firstOption ) . toBeVisible ( ) ;
const firstOption = dropdown . locator ( '[role="option"]' ) . first ( ) ;
await expect ( firstO ption) . toBeVisible ( ) ;
const box = await firstOption . boundingBox ( ) ;
expect ( box ) . not . toBeNull ( ) ;
expect ( box ! . y ) . toBeGreaterThan ( 0 ) ;
expect ( box ! . y + box ! . height ) . toBeLessThan ( 1024 ) ;
}
const box = await firstOption . boundingBox ( ) ;
expect ( box ) . not . toBeNull ( ) ;
expect ( box ! . y ) . toBeGreaterThan ( 0 ) ;
expect ( box ! . y + box ! . height ) . toBeLessThan ( 1024 ) ;
await page . screenshot ( { path : 'test-results/e2e/person-typeahead-dropdown-tablet.png' } ) ;
} ) ;
@@ -138,19 +134,14 @@ test.describe('PersonTypeahead — keyboard navigation', () => {
const senderInput = page . locator ( '#senderId-search' ) ;
await senderInput . click ( ) ;
await senderInput . fill ( 'a' ) ;
await page . waitForTimeout ( 400 ) ;
await waitForListbox ( page ) ;
const dropdown = page . locator ( '[role="listbox"]' ) . first ( ) ;
const hasDropdown = ( await dropdown . count ( ) ) > 0 ;
await senderInput . press ( 'ArrowDown' ) ;
// First option should now be the active descendant
const activeDescendant = await senderInput . getAttribute ( 'aria-activedescendant' ) ;
expect ( activeDescendant ) . toBeTruthy ( ) ;
if ( hasDropdown ) {
await senderInput . press ( 'ArrowDown' ) ;
// First option should now be the active descendant
const activeDescendant = await senderInput . getAttribute ( 'aria-activedescendant' ) ;
expect ( activeDescendant ) . toBeTruthy ( ) ;
await page . screenshot ( { path : 'test-results/e2e/person-typeahead-keyboard-nav.png' } ) ;
}
await page . screenshot ( { path : 'test-results/e2e/person-typeahead-keyboard-nav.png' } ) ;
} ) ;
test ( 'Escape key closes the dropdown' , async ( { page } ) = > {
@@ -161,17 +152,12 @@ test.describe('PersonTypeahead — keyboard navigation', () => {
const senderInput = page . locator ( '#senderId-search' ) ;
await senderInput . click ( ) ;
await senderInput . fill ( 'a' ) ;
await page . waitForTimeout ( 400 ) ;
await waitForListbox ( page ) ;
const dropdown = page . locator ( '[role="listbox"]' ) . first ( ) ;
const hasDropdown = ( await dropdown . count ( ) ) > 0 ;
if ( hasDropdown ) {
await expect ( dropdown ) . toBeVisible ( ) ;
await senderInput . press ( 'Escape' ) ;
await page . waitForTimeout ( 100 ) ;
await expect ( dropdown ) . not . toBeVisible ( ) ;
}
await expect ( dropdown ) . toBeVisible ( ) ;
await senderInput . press ( 'Escape' ) ;
await expect ( dropdown ) . not . toBeVisible ( ) ;
} ) ;
test ( 'aria-expanded is true when dropdown is open' , async ( { page } ) = > {
@@ -187,15 +173,10 @@ test.describe('PersonTypeahead — keyboard navigation', () => {
await senderInput . click ( ) ;
await senderInput . fill ( 'a' ) ;
await page . waitForTimeout ( 400 ) ;
await waitForListbox ( page ) ;
const dropdown = page . locator ( '[role="listbox"]' ) . first ( ) ;
const hasDropdown = ( await dropdown . count ( ) ) > 0 ;
if ( hasDropdown ) {
const expanded = await senderInput . getAttribute ( 'aria-expanded' ) ;
expect ( expanded ) . toBe ( 'true' ) ;
}
const expanded = await senderInput . getAttribute ( 'aria-expanded' ) ;
expect ( expanded ) . toBe ( 'true' ) ;
} ) ;
} ) ;
@@ -210,17 +191,12 @@ test.describe('PersonTypeahead — click-outside dismiss (fixed position)', () =
const senderInput = page . locator ( '#senderId-search' ) ;
await senderInput . click ( ) ;
await senderInput . fill ( 'a' ) ;
await page . waitForTimeout ( 400 ) ;
await waitForListbox ( page ) ;
const dropdown = page . locator ( '[role="listbox"]' ) . first ( ) ;
const hasDropdown = ( await dropdown . count ( ) ) > 0 ;
if ( hasDropdown ) {
await expect ( dropdown ) . toBeVisible ( ) ;
// Click somewhere else on the page
await page . click ( 'body' , { position : { x : 10 , y : 10 } } ) ;
await page . waitForTimeout ( 100 ) ;
await expect ( dropdown ) . not . toBeVisible ( ) ;
}
await expect ( dropdown ) . toBeVisible ( ) ;
// Click somewhere else on the page
await page . click ( 'body' , { position : { x : 10 , y : 10 } } ) ;
await expect ( dropdown ) . not . toBeVisible( ) ;
} ) ;
} ) ;