rsnext/test/development/client-dev-overlay/index.test.ts
Zack Tanner ee15b3be5d
fix: error overlay hijacking application focus (safari) (#53693)
### What?
When Safari is in the background and HMR triggers a full page reload,
Safari hijacks application focus.

### Why?
Having a `role="dialog"` is correctly prompting Safari to autofocus the
first focusable element (the close button). However, Safari's behavior
seems to also bring the application to the foreground when a background
focus event occurs.

### How?
This only adds the role when the document is focused. 

#### Before

https://github.com/vercel/next.js/assets/1939140/9d2cce52-c6ee-4d49-9262-03620efad86c

#### After

https://github.com/vercel/next.js/assets/1939140/dc7d337c-b621-49e9-9a17-03b5d8b5c3f4

Confirmed voiceover behavior still appears to be correct
<img width="1371" alt="CleanShot 2023-08-07 at 12 14 34@2x"
src="https://github.com/vercel/next.js/assets/1939140/e53acfbc-cf6b-4d74-8b83-cf98edb2c2ab">

slack x-ref:
https://vercel.slack.com/archives/C03KAR5DCKC/p1691264077313599

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-08-08 21:45:03 +02:00

83 lines
2.5 KiB
TypeScript

import { createNext, FileRef } from 'e2e-utils'
import webdriver from 'next-webdriver'
import { NextInstance } from 'test/lib/next-modes/base'
import { join } from 'path'
import { BrowserInterface } from 'test/lib/browsers/base'
import { check } from 'next-test-utils'
describe('client-dev-overlay', () => {
let next: NextInstance
let browser: BrowserInterface
beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'app/pages')),
},
})
})
beforeEach(async () => {
browser = await webdriver(next.url, '')
})
afterAll(() => next.destroy())
// The `BrowserInterface.hasElementByCssSelector` cannot be used for elements inside a shadow DOM.
function elementExistsInNextJSPortalShadowDOM(selector: string) {
return browser.eval(
`!!document.querySelector('nextjs-portal').shadowRoot.querySelector('${selector}')`
) as any
}
const selectors = {
fullScreenDialog: '[data-nextjs-dialog]',
toast: '[data-nextjs-toast]',
minimizeButton: '[data-nextjs-errors-dialog-left-right-close-button]',
hideButton: '[data-nextjs-toast-errors-hide-button]',
}
function getToast() {
return browser.elementByCss(selectors.toast)
}
function getMinimizeButton() {
return browser.elementByCss(selectors.minimizeButton)
}
function getHideButton() {
return browser.elementByCss(selectors.hideButton)
}
it('should be able to fullscreen the minimized overlay', async () => {
await getMinimizeButton().click()
await getToast().click()
await check(async () => {
return (await elementExistsInNextJSPortalShadowDOM(
selectors.fullScreenDialog
))
? 'success'
: 'missing'
}, 'success')
})
it('should be able to minimize the fullscreen overlay', async () => {
await getMinimizeButton().click()
expect(await elementExistsInNextJSPortalShadowDOM(selectors.toast)).toBe(
true
)
})
it('should be able to hide the minimized overlay', async () => {
await getMinimizeButton().click()
await getHideButton().click()
await check(async () => {
const exists = await elementExistsInNextJSPortalShadowDOM('div')
return exists ? 'found' : 'success'
}, 'success')
})
it('should have a role of "dialog" if the page is focused', async () => {
await check(async () => {
return (await elementExistsInNextJSPortalShadowDOM('[role="dialog"]'))
? 'exists'
: 'missing'
}, 'exists')
})
})