Files
next.js/test/e2e/app-dir/app-new-scroll-handler-focus/app-new-scroll-handler-focus.test.ts
JosephandGitHub b59fae0429 [fragment-scroll] Stop blurring on navigations (#96113)
Dynamic pages (all pages in dev), blur and select the body as
`activeElement`.

- [x] e2e for https://github.com/vercel/next.js/issues/96050
- [ ] attempt a fix that prevents blur
2026-07-29 19:13:35 -04:00

111 lines
3.7 KiB
TypeScript

import { isNextDev, nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
// Typing into an input that pushes `?q=` per keystroke should keep focus. On a
// dynamic page focus was lost after the first character; a static page is fine.
describe('app-new-scroll-handler-focus', () => {
const { next } = nextTestSetup({
files: __dirname,
})
// Use the page keyboard, not element.type() (which re-focuses and hides the
// bug). If focus is lost, the second key never lands and the URL stays `q=a`.
async function typeTwoKeysBothMustLand(path: string, testId: string) {
const browser = await next.browser(path)
const activeTestId = () =>
browser.eval(() => document.activeElement?.getAttribute('data-testid'))
// Focus explicitly — autoFocus timing isn't reliable across bundlers.
await browser.elementByCss(`[data-testid="${testId}"]`).click()
await retry(async () => {
expect(await activeTestId()).toBe(testId)
})
await browser.keydown('a')
await browser.keyup('a')
await retry(async () => {
expect(await browser.url()).toContain('q=a')
})
await browser.keydown('b')
await browser.keyup('b')
await retry(async () => {
expect(await browser.url()).toContain('q=ab')
})
await retry(async () => {
expect(await activeTestId()).toBe(testId)
})
}
it('dynamic page: keeps focus across search-param keystrokes (all modes)', async () => {
await typeTwoKeysBothMustLand('/', 'search-input')
})
// Static pages are immune; only assertable outside dev (dev renders dynamically).
if (!isNextDev) {
it('static page: unaffected by the new scroll handler (start/deploy only)', async () => {
await typeTwoKeysBothMustLand('/static', 'static-search-input')
})
}
// The navigation should still scroll to top while keeping focus.
it('dynamic page: still scrolls to top on a search-param nav, focus preserved', async () => {
const browser = await next.browser('/')
await browser.elementByCss('[data-testid="search-input"]').click()
await retry(async () => {
expect(
await browser.eval(() =>
document.activeElement?.getAttribute('data-testid')
)
).toBe('search-input')
})
await browser.eval(() => window.scrollTo(0, 800))
await retry(async () => {
expect(await browser.eval(() => window.scrollY)).toBeGreaterThan(400)
})
await browser.keydown('a')
await browser.keyup('a')
await retry(async () => {
expect(await browser.url()).toContain('q=a')
})
await retry(async () => {
expect(await browser.eval(() => window.scrollY)).toBe(0)
})
expect(
await browser.eval(() =>
document.activeElement?.getAttribute('data-testid')
)
).toBe('search-input')
})
// Focus is left alone, so a route+search change keeps focus on an input that
// survives in a parent layout.
it('route + search change keeps focus (all modes)', async () => {
const browser = await next.browser('/combo')
const activeTestId = () =>
browser.eval(() => document.activeElement?.getAttribute('data-testid'))
await browser.elementByCss('[data-testid="combo-input"]').click()
await retry(async () => {
expect(await activeTestId()).toBe('combo-input')
})
await browser.keydown('a')
await browser.keyup('a')
await retry(async () => {
expect(await browser.url()).toContain('/combo/next?q=a')
})
// The layout input survives the navigation and keeps focus.
expect(
await browser.eval(
() => !!document.querySelector('[data-testid="combo-input"]')
)
).toBe(true)
expect(await activeTestId()).toBe('combo-input')
})
})