Skip to content

Commit 6a5b6df

Browse files
committed
test: improved test reliability
1 parent bba1a78 commit 6a5b6df

File tree

3 files changed

+165
-35
lines changed

3 files changed

+165
-35
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { useState } from 'react'
5+
6+
export function LinkAccordion({ href }: { href: string }) {
7+
const [isOpen, setIsOpen] = useState(false)
8+
9+
return (
10+
<div
11+
data-testid="link-accordion"
12+
data-href={href}
13+
style={{
14+
border: '1px solid #e5e7eb',
15+
borderRadius: '12px',
16+
padding: '16px',
17+
margin: '12px 0',
18+
backgroundColor: 'white',
19+
boxShadow:
20+
'0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
21+
transition: 'all 0.2s ease',
22+
cursor: 'pointer',
23+
}}
24+
>
25+
<div
26+
style={{
27+
display: 'flex',
28+
justifyContent: 'space-between',
29+
alignItems: 'center',
30+
gap: '12px',
31+
}}
32+
>
33+
<div
34+
style={{
35+
fontFamily: 'monospace',
36+
fontSize: '14px',
37+
color: '#374151',
38+
fontWeight: '500',
39+
flex: 1,
40+
}}
41+
>
42+
{href}
43+
</div>
44+
{!isOpen && (
45+
<button
46+
onClick={() => setIsOpen(true)}
47+
style={{
48+
padding: '8px 16px',
49+
borderRadius: '8px',
50+
border: 'none',
51+
backgroundColor: '#3b82f6',
52+
color: 'white',
53+
fontSize: '14px',
54+
fontWeight: '600',
55+
cursor: 'pointer',
56+
transition: 'all 0.2s ease',
57+
display: 'flex',
58+
alignItems: 'center',
59+
gap: '6px',
60+
}}
61+
>
62+
<span></span>
63+
Open
64+
</button>
65+
)}
66+
</div>
67+
{isOpen && (
68+
<div
69+
style={{
70+
marginTop: '16px',
71+
paddingTop: '16px',
72+
borderTop: '1px solid #e5e7eb',
73+
}}
74+
>
75+
<Link
76+
href={href}
77+
style={{
78+
display: 'inline-block',
79+
padding: '10px 20px',
80+
backgroundColor: '#10b981',
81+
color: 'white',
82+
textDecoration: 'none',
83+
borderRadius: '8px',
84+
fontSize: '14px',
85+
fontWeight: '600',
86+
}}
87+
>
88+
Navigate to {href}
89+
</Link>
90+
</div>
91+
)}
92+
</div>
93+
)
94+
}

test/e2e/app-dir/interception-dynamic-segment/app/page.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Link from 'next/link'
1+
import { LinkAccordion } from './page.client'
22

33
export default function Page() {
44
return (
@@ -8,17 +8,15 @@ export default function Page() {
88

99
<div>
1010
<h3>Test Case 1a: Simple page (no parallel routes)</h3>
11-
<Link href="/simple-page" style={{ color: 'blue', fontSize: '16px' }}>
12-
→ /simple-page
13-
</Link>
11+
<LinkAccordion href="/simple-page" />
1412
<p>
1513
Interception route with just a page.tsx, no parallel routes at all.
1614
</p>
1715
</div>
1816

1917
<div>
2018
<h3>Test Case 1b: Has page.tsx</h3>
21-
<Link href="/has-page">→ /has-page</Link>
19+
<LinkAccordion href="/has-page" />
2220
<p>
2321
Interception route has page.tsx at root level. No children slot
2422
exists.
@@ -27,15 +25,13 @@ export default function Page() {
2725

2826
<div>
2927
<h3>Test Case 2: No parallel routes</h3>
30-
<Link href="/no-parallel-routes/deeper">
31-
→ /no-parallel-routes/deeper
32-
</Link>
28+
<LinkAccordion href="/no-parallel-routes/deeper" />
3329
<p>No @parallel routes exist, so no implicit layout created.</p>
3430
</div>
3531

3632
<div>
3733
<h3>Test Case 3: Has both @sidebar AND page.tsx</h3>
38-
<Link href="/has-both">→ /has-both</Link>
34+
<LinkAccordion href="/has-both" />
3935
<p>
4036
Has @sidebar parallel route BUT page.tsx fills the children slot.
4137
</p>
@@ -47,14 +43,14 @@ export default function Page() {
4743

4844
<div>
4945
<h3>Test Case 4a: Has @sidebar but NO page.tsx (implicit layout)</h3>
50-
<Link href="/test-nested">→ /test-nested</Link>
46+
<LinkAccordion href="/test-nested" />
5147
<p>Has @sidebar (creates implicit layout) but NO page.tsx.</p>
5248
<p>✓ Auto-uses null default (no explicit files needed)</p>
5349
</div>
5450

5551
<div>
5652
<h3>Test Case 4b: Has explicit layout.tsx but NO parallel routes</h3>
57-
<Link href="/explicit-layout/deeper">→ /explicit-layout/deeper</Link>
53+
<LinkAccordion href="/explicit-layout/deeper" />
5854
<p>
5955
Has explicit layout.tsx with children slot, but NO parallel routes
6056
like @sidebar.
@@ -69,18 +65,16 @@ export default function Page() {
6965
<h2>Original Tests</h2>
7066
<ul>
7167
<li>
72-
<Link href="/foo/1">/foo/1</Link>
68+
<LinkAccordion href="/foo/1" />
7369
</li>
7470
<li>
75-
<Link href="/bar/1">/bar/1</Link>
71+
<LinkAccordion href="/bar/1" />
7672
</li>
7773
<li>
78-
<Link href="/test-nested/deeper">/test-nested/deeper</Link>
74+
<LinkAccordion href="/test-nested/deeper" />
7975
</li>
8076
<li>
81-
<Link href="/generate-static-params/foo">
82-
/generate-static-params/foo
83-
</Link>
77+
<LinkAccordion href="/generate-static-params/foo" />
8478
</li>
8579
</ul>
8680
</section>

test/e2e/app-dir/interception-dynamic-segment/interception-dynamic-segment.test.ts

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,50 @@
11
import { nextTestSetup } from 'e2e-utils'
22
import { retry } from 'next-test-utils'
3+
import { Playwright } from 'next-webdriver'
34
import { createRouterAct } from 'router-act'
45

56
describe('interception-dynamic-segment', () => {
67
const { next, isNextStart, isNextDev } = nextTestSetup({
78
files: __dirname,
89
})
910

11+
/**
12+
* Returns true if the given href should already be opened. This allows us to
13+
* condition on whether to expect any additional network requests.
14+
*/
15+
async function isAccordionClosed(
16+
browser: Playwright,
17+
href: string
18+
): Promise<boolean> {
19+
const selector = `[data-testid="link-accordion"][data-href="${href}"]`
20+
21+
// Check if the button is already open
22+
return await browser.hasElementByCss(`${selector} button`)
23+
}
24+
25+
/**
26+
* Helper to navigate via the LinkAccordion component.
27+
* Scrolls to the accordion, opens it, and clicks the link.
28+
*/
29+
async function navigate(browser: Playwright, href: string) {
30+
const selector = `[data-testid="link-accordion"][data-href="${href}"]`
31+
32+
// Find and scroll to accordion
33+
const accordion = await browser.elementByCss(selector)
34+
await accordion.scrollIntoViewIfNeeded()
35+
36+
// Click the "Open" button, it may already be open, so we don't need to
37+
// click it again.
38+
if (await isAccordionClosed(browser, href)) {
39+
const button = await browser.elementByCss(`${selector} button`)
40+
await button.click()
41+
}
42+
43+
// Click the actual link
44+
const link = await browser.elementByCss(`${selector} a`)
45+
await link.click()
46+
}
47+
1048
/**
1149
* Create a browser with router act that will FAIL if any 404s occur during navigation.
1250
* This is critical because if a 404 occurs, the client will perform MPA navigation
@@ -27,7 +65,7 @@ describe('interception-dynamic-segment', () => {
2765
it('should work when interception route is paired with a dynamic segment', async () => {
2866
const browser = await next.browser('/')
2967

30-
await browser.elementByCss('[href="/foo/1"]').click()
68+
await navigate(browser, '/foo/1')
3169
await browser.waitForIdleNetwork()
3270

3371
await retry(async () => {
@@ -52,7 +90,7 @@ describe('interception-dynamic-segment', () => {
5290
const browser = await next.browser('/')
5391

5492
// Navigate with interception
55-
await browser.elementByCss('[href="/foo/1"]').click()
93+
await navigate(browser, '/foo/1')
5694
await browser.waitForIdleNetwork()
5795

5896
await retry(async () => {
@@ -82,7 +120,7 @@ describe('interception-dynamic-segment', () => {
82120
const browser = await next.browser('/')
83121

84122
for (let i = 0; i < 2; i++) {
85-
await browser.elementByCss('[href="/foo/1"]').click()
123+
await navigate(browser, '/foo/1')
86124
await browser.waitForIdleNetwork()
87125

88126
await retry(async () => {
@@ -126,7 +164,7 @@ describe('interception-dynamic-segment', () => {
126164
const { act, browser } = await createBrowserWithRouterAct('/')
127165

128166
await act(async () => {
129-
await browser.elementByCss('[href="/foo/1"]').click()
167+
await navigate(browser, '/foo/1')
130168
})
131169

132170
await retry(async () => {
@@ -145,7 +183,7 @@ describe('interception-dynamic-segment', () => {
145183
const { act, browser } = await createBrowserWithRouterAct('/')
146184

147185
await act(async () => {
148-
await browser.elementByCss('[href="/simple-page"]').click()
186+
await navigate(browser, '/simple-page')
149187
})
150188

151189
await retry(async () => {
@@ -165,7 +203,7 @@ describe('interception-dynamic-segment', () => {
165203
const { act, browser } = await createBrowserWithRouterAct('/')
166204

167205
await act(async () => {
168-
await browser.elementByCss('[href="/has-page"]').click()
206+
await navigate(browser, '/has-page')
169207
})
170208

171209
await retry(async () => {
@@ -185,9 +223,7 @@ describe('interception-dynamic-segment', () => {
185223
const { act, browser } = await createBrowserWithRouterAct('/')
186224

187225
await act(async () => {
188-
await browser
189-
.elementByCss('[href="/no-parallel-routes/deeper"]')
190-
.click()
226+
await navigate(browser, '/no-parallel-routes/deeper')
191227
})
192228

193229
await retry(async () => {
@@ -207,7 +243,7 @@ describe('interception-dynamic-segment', () => {
207243
const { act, browser } = await createBrowserWithRouterAct('/')
208244

209245
await act(async () => {
210-
await browser.elementByCss('[href="/has-both"]').click()
246+
await navigate(browser, '/has-both')
211247
})
212248

213249
await retry(async () => {
@@ -235,7 +271,7 @@ describe('interception-dynamic-segment', () => {
235271
const { act, browser } = await createBrowserWithRouterAct('/')
236272

237273
await act(async () => {
238-
await browser.elementByCss('[href="/test-nested"]').click()
274+
await navigate(browser, '/test-nested')
239275
})
240276

241277
await retry(async () => {
@@ -260,7 +296,7 @@ describe('interception-dynamic-segment', () => {
260296

261297
// Navigate directly to the deeper page from home
262298
await act(async () => {
263-
await browser.elementByCss('[href="/test-nested/deeper"]').click()
299+
await navigate(browser, '/test-nested/deeper')
264300
})
265301

266302
await retry(async () => {
@@ -290,7 +326,7 @@ describe('interception-dynamic-segment', () => {
290326
const { act, browser } = await createBrowserWithRouterAct('/')
291327

292328
await act(async () => {
293-
await browser.elementByCss('[href="/explicit-layout/deeper"]').click()
329+
await navigate(browser, '/explicit-layout/deeper')
294330
})
295331

296332
await retry(async () => {
@@ -310,12 +346,18 @@ describe('interception-dynamic-segment', () => {
310346
const { act, browser } = await createBrowserWithRouterAct('/')
311347

312348
for (let i = 0; i < 3; i++) {
349+
const isAccordionOpen = i > 0
350+
351+
await expect(
352+
isAccordionClosed(browser, '/test-nested')
353+
).resolves.toBe(!isAccordionOpen)
354+
313355
// Forward navigation: triggers RSC request (validates no 404)
314356
await act(
315357
async () => {
316-
await browser.elementByCss('[href="/test-nested"]').click()
358+
await navigate(browser, '/test-nested')
317359
},
318-
i > 0 ? 'no-requests' : undefined
360+
!isAccordionOpen ? undefined : 'no-requests'
319361
)
320362

321363
await retry(async () => {
@@ -343,7 +385,7 @@ describe('interception-dynamic-segment', () => {
343385

344386
// First interception
345387
await act(async () => {
346-
await browser.elementByCss('[href="/test-nested"]').click()
388+
await navigate(browser, '/test-nested')
347389
})
348390

349391
await retry(async () => {
@@ -353,8 +395,8 @@ describe('interception-dynamic-segment', () => {
353395

354396
// Second interception
355397
await act(async () => {
356-
await browser.elementByCss('[href="/has-both"]').click()
357-
}, 'no-requests')
398+
await navigate(browser, '/has-both')
399+
})
358400

359401
await retry(async () => {
360402
const modalContent = await browser.elementByCss('#modal').text()

0 commit comments

Comments
 (0)