-
Notifications
You must be signed in to change notification settings - Fork 26.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Run link-ref tests in /app and /pages (#69564)
We need to handle ref cleanups in a way that is compatible with React 18 and 19. Forking these into -pages (18) and -app (19). Combining them into a single app lead to a different behavior with regards to preload.
- Loading branch information
Showing
17 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
53 changes: 53 additions & 0 deletions
53
test/integration/link-ref-app/app/child-ref-func-cleanup/page.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
'use client' | ||
import React from 'react' | ||
import Link from 'next/link' | ||
import { useCallback, useRef, useEffect, useState } from 'react' | ||
import { flushSync } from 'react-dom' | ||
|
||
export default function Page() { | ||
const [isVisible, setIsVisible] = useState(true) | ||
|
||
const statusRef = useRef({ wasInitialized: false, wasCleanedUp: false }) | ||
|
||
const refWithCleanup = useCallback((el) => { | ||
if (!el) { | ||
console.error( | ||
'callback refs that returned a cleanup should never be called with null' | ||
) | ||
return | ||
} | ||
|
||
statusRef.current.wasInitialized = true | ||
return () => { | ||
statusRef.current.wasCleanedUp = true | ||
} | ||
}, []) | ||
|
||
useEffect(() => { | ||
const timeout = setTimeout( | ||
() => { | ||
flushSync(() => { | ||
setIsVisible(false) | ||
}) | ||
if (!statusRef.current.wasInitialized) { | ||
console.error('callback ref was not initialized') | ||
} | ||
if (!statusRef.current.wasCleanedUp) { | ||
console.error('callback ref was not cleaned up') | ||
} | ||
}, | ||
100 // if we hide the Link too quickly, the prefetch won't fire, failing a test | ||
) | ||
return () => clearTimeout(timeout) | ||
}, []) | ||
|
||
if (!isVisible) { | ||
return null | ||
} | ||
|
||
return ( | ||
<Link href="/" ref={refWithCleanup}> | ||
Click me | ||
</Link> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
'use client' | ||
import React from 'react' | ||
import Link from 'next/link' | ||
|
||
export default () => { | ||
const myRef = React.createRef(null) | ||
|
||
React.useEffect(() => { | ||
if (!myRef.current) { | ||
console.error(`ref wasn't updated`) | ||
} | ||
}) | ||
|
||
return ( | ||
<Link | ||
href="/" | ||
ref={(el) => { | ||
myRef.current = el | ||
}} | ||
> | ||
Click me | ||
</Link> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
'use client' | ||
import React from 'react' | ||
import Link from 'next/link' | ||
|
||
export default () => { | ||
const myRef = React.createRef(null) | ||
|
||
React.useEffect(() => { | ||
if (!myRef.current) { | ||
console.error(`ref wasn't updated`) | ||
} | ||
}) | ||
|
||
return ( | ||
<Link href="/" ref={myRef}> | ||
Click me | ||
</Link> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
'use client' | ||
import React from 'react' | ||
import Link from 'next/link' | ||
|
||
class MyLink extends React.Component { | ||
render() { | ||
return <a {...this.props}>Click me</a> | ||
} | ||
} | ||
|
||
export default () => ( | ||
<Link href="/" passHref legacyBehavior> | ||
<MyLink /> | ||
</Link> | ||
) |
57 changes: 57 additions & 0 deletions
57
test/integration/link-ref-app/app/click-away-race-condition/page.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
'use client' | ||
import React, { useCallback, useEffect, useRef, useState } from 'react' | ||
import Link from 'next/link' | ||
|
||
const useClickAway = (ref, onClickAway) => { | ||
useEffect(() => { | ||
const handler = (event) => { | ||
const el = ref.current | ||
|
||
// when menu is open and clicked inside menu, A is expected to be false | ||
// when menu is open and clicked outside menu, A is expected to be true | ||
console.log('A', el && !el.contains(event.target)) | ||
|
||
el && !el.contains(event.target) && onClickAway(event) | ||
} | ||
|
||
let timeoutID = setTimeout(() => { | ||
timeoutID = null | ||
document.addEventListener('click', handler) | ||
}, 0) | ||
|
||
return () => { | ||
if (timeoutID != null) { | ||
clearTimeout(timeoutID) | ||
} | ||
document.removeEventListener('click', handler) | ||
} | ||
}, [onClickAway, ref]) | ||
} | ||
|
||
export default function App() { | ||
const [open, setOpen] = useState(false) | ||
|
||
const menuRef = useRef(null) | ||
|
||
const onClickAway = useCallback(() => { | ||
console.log('click away, open', open) | ||
if (open) { | ||
setOpen(false) | ||
} | ||
}, [open]) | ||
|
||
useClickAway(menuRef, onClickAway) | ||
|
||
return ( | ||
<div> | ||
<div id="click-me" onClick={() => setOpen(true)}> | ||
Open Menu | ||
</div> | ||
{open && ( | ||
<div ref={menuRef} id="the-menu"> | ||
<Link href="/">some link</Link> | ||
</div> | ||
)} | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
'use client' | ||
import React from 'react' | ||
import Link from 'next/link' | ||
|
||
const MyLink = React.forwardRef((props, ref) => ( | ||
<a {...props} ref={ref}> | ||
Click me | ||
</a> | ||
)) | ||
|
||
export default () => ( | ||
<Link href="/" passHref legacyBehavior> | ||
<MyLink /> | ||
</Link> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export default function RootLayout({ children }) { | ||
return ( | ||
<html> | ||
<body>{children}</body> | ||
</html> | ||
) | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/* eslint-env jest */ | ||
|
||
import { join } from 'path' | ||
import webdriver from 'next-webdriver' | ||
import { | ||
retry, | ||
findPort, | ||
launchApp, | ||
killApp, | ||
nextStart, | ||
nextBuild, | ||
waitFor, | ||
} from 'next-test-utils' | ||
|
||
let app | ||
let appPort | ||
const appDir = join(__dirname, '..') | ||
|
||
const noError = async (pathname) => { | ||
const browser = await webdriver(appPort, '/') | ||
await browser.eval(`(function() { | ||
window.caughtErrors = [] | ||
const origError = window.console.error | ||
window.console.error = function (format) { | ||
window.caughtErrors.push(format) | ||
origError(arguments) | ||
} | ||
window.next.router.replace('${pathname}') | ||
})()`) | ||
await waitFor(1000) | ||
const errors = await browser.eval(`window.caughtErrors`) | ||
expect(errors).toEqual([]) | ||
await browser.close() | ||
} | ||
|
||
const didPrefetch = async (pathname) => { | ||
const requests = [] | ||
const browser = await webdriver(appPort, pathname, { | ||
beforePageLoad(page) { | ||
page.on('request', async (req) => { | ||
const url = new URL(req.url()) | ||
const headers = await req.allHeaders() | ||
if (headers['next-router-prefetch']) { | ||
requests.push(url.pathname) | ||
} | ||
}) | ||
}, | ||
}) | ||
|
||
await browser.waitForIdleNetwork() | ||
|
||
await retry(async () => { | ||
expect(requests).toEqual( | ||
expect.arrayContaining([expect.stringContaining('/')]) | ||
) | ||
}) | ||
|
||
await browser.close() | ||
} | ||
|
||
function runCommonTests() { | ||
// See https://github.com/vercel/next.js/issues/18437 | ||
it('should not have a race condition with a click handler', async () => { | ||
const browser = await webdriver(appPort, '/click-away-race-condition') | ||
await browser.elementByCss('#click-me').click() | ||
await browser.waitForElementByCss('#the-menu') | ||
}) | ||
} | ||
|
||
describe('Invalid hrefs', () => { | ||
;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( | ||
'development mode', | ||
() => { | ||
beforeAll(async () => { | ||
appPort = await findPort() | ||
app = await launchApp(appDir, appPort) | ||
}) | ||
afterAll(() => killApp(app)) | ||
|
||
runCommonTests() | ||
|
||
it('should not show error for function component with forwardRef', async () => { | ||
await noError('/function') | ||
}) | ||
|
||
it('should not show error for class component as child of next/link', async () => { | ||
await noError('/class') | ||
}) | ||
|
||
it('should handle child ref with React.createRef', async () => { | ||
await noError('/child-ref') | ||
}) | ||
|
||
it('should handle child ref that is a function', async () => { | ||
await noError('/child-ref-func') | ||
}) | ||
|
||
it('should handle child ref that is a function that returns a cleanup function', async () => { | ||
await noError('/child-ref-func-cleanup') | ||
}) | ||
} | ||
) | ||
;(process.env.TURBOPACK_DEV ? describe.skip : describe)( | ||
'production mode', | ||
() => { | ||
beforeAll(async () => { | ||
await nextBuild(appDir) | ||
appPort = await findPort() | ||
app = await nextStart(appDir, appPort) | ||
}) | ||
afterAll(() => killApp(app)) | ||
|
||
runCommonTests() | ||
|
||
it('should preload with forwardRef', async () => { | ||
await didPrefetch('/function') | ||
}) | ||
|
||
it('should preload with child ref with React.createRef', async () => { | ||
await didPrefetch('/child-ref') | ||
}) | ||
|
||
it('should preload with child ref with function', async () => { | ||
await didPrefetch('/child-ref-func') | ||
}) | ||
|
||
it('should preload with child ref with function that returns a cleanup function', async () => { | ||
await didPrefetch('/child-ref-func-cleanup') | ||
}) | ||
} | ||
) | ||
}) |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default () => 'hi' |
File renamed without changes.