hello from pages/blog/[slug]
+ > + ) +} diff --git a/test/e2e/root-dir/app/pages/index.js b/test/e2e/root-dir/app/pages/index.js new file mode 100644 index 0000000000000..8846b6ec63e5d --- /dev/null +++ b/test/e2e/root-dir/app/pages/index.js @@ -0,0 +1,7 @@ +export default function Page(props) { + return ( + <> +hello from pages/index
+ > + ) +} diff --git a/test/e2e/root-dir/app/public/hello.txt b/test/e2e/root-dir/app/public/hello.txt new file mode 100644 index 0000000000000..95d09f2b10159 --- /dev/null +++ b/test/e2e/root-dir/app/public/hello.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/test/e2e/root-dir/app/root.server.js b/test/e2e/root-dir/app/root.server.js new file mode 100644 index 0000000000000..d270310372cf6 --- /dev/null +++ b/test/e2e/root-dir/app/root.server.js @@ -0,0 +1,11 @@ +export default function Root({ headChildren, bodyChildren }) { + return ( + + + {headChildren} +hello from root/client-component-route. count: {count}
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/client-nested.client.js b/test/e2e/root-dir/app/root/client-nested.client.js new file mode 100644 index 0000000000000..6f835e03f4c60 --- /dev/null +++ b/test/e2e/root-dir/app/root/client-nested.client.js @@ -0,0 +1,15 @@ +import { useState, useEffect } from 'react' + +export default function ClientNestedLayout({ children }) { + const [count, setCount] = useState(0) + useEffect(() => { + setCount(1) + }, []) + return ( + <> +hello from root/client-nested
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/conditional/[slug].server.js b/test/e2e/root-dir/app/root/conditional/[slug].server.js new file mode 100644 index 0000000000000..14dae08e2713a --- /dev/null +++ b/test/e2e/root-dir/app/root/conditional/[slug].server.js @@ -0,0 +1,27 @@ +export async function getServerSideProps({ params }) { + if (params.slug === 'nonexistent') { + return { + notFound: true, + } + } + return { + props: { + isUser: params.slug === 'tim', + isBoth: params.slug === 'both', + }, + } +} + +export default function UserOrTeam({ isUser, isBoth, user, team }) { + return ( + <> + {isUser && !isBoth ? user : team} + {isBoth ? ( + <> + {user} + {team} + > + ) : null} + > + ) +} diff --git a/test/e2e/root-dir/app/root/conditional/[slug]@team/index.js b/test/e2e/root-dir/app/root/conditional/[slug]@team/index.js new file mode 100644 index 0000000000000..02119380f3382 --- /dev/null +++ b/test/e2e/root-dir/app/root/conditional/[slug]@team/index.js @@ -0,0 +1,7 @@ +export default function TeamHomePage(props) { + return ( + <> +hello from team homepage
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/conditional/[slug]@team/members.js b/test/e2e/root-dir/app/root/conditional/[slug]@team/members.js new file mode 100644 index 0000000000000..2c3ba112beade --- /dev/null +++ b/test/e2e/root-dir/app/root/conditional/[slug]@team/members.js @@ -0,0 +1,7 @@ +export default function TeamMembersPage(props) { + return ( + <> +hello from team/members
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/conditional/[slug]@user/index.js b/test/e2e/root-dir/app/root/conditional/[slug]@user/index.js new file mode 100644 index 0000000000000..81100777dae29 --- /dev/null +++ b/test/e2e/root-dir/app/root/conditional/[slug]@user/index.js @@ -0,0 +1,7 @@ +export default function UserHomePage(props) { + return ( + <> +hello from user homepage
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/conditional/[slug]@user/teams.js b/test/e2e/root-dir/app/root/conditional/[slug]@user/teams.js new file mode 100644 index 0000000000000..294c3bf316dad --- /dev/null +++ b/test/e2e/root-dir/app/root/conditional/[slug]@user/teams.js @@ -0,0 +1,7 @@ +export default function UserTeamsPage(props) { + return ( + <> +hello from user/teams
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/dashboard+changelog.server.js b/test/e2e/root-dir/app/root/dashboard+changelog.server.js new file mode 100644 index 0000000000000..a9a3c0e759c45 --- /dev/null +++ b/test/e2e/root-dir/app/root/dashboard+changelog.server.js @@ -0,0 +1,7 @@ +export default function ChangelogPage(props) { + return ( + <> +hello from root/dashboard/changelog
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/dashboard+rootonly/hello.server.js b/test/e2e/root-dir/app/root/dashboard+rootonly/hello.server.js new file mode 100644 index 0000000000000..fd76d51734650 --- /dev/null +++ b/test/e2e/root-dir/app/root/dashboard+rootonly/hello.server.js @@ -0,0 +1,7 @@ +export default function HelloPage(props) { + return ( + <> +hello from root/dashboard/rootonly/hello
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/dashboard.server.js b/test/e2e/root-dir/app/root/dashboard.server.js new file mode 100644 index 0000000000000..2c16fe844ef2e --- /dev/null +++ b/test/e2e/root-dir/app/root/dashboard.server.js @@ -0,0 +1,8 @@ +export default function DashboardLayout({ children }) { + return ( + <> +hello from root/dashboard/deployments/[id]. ID is: {props.id}
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/dashboard/deployments/info.server.js b/test/e2e/root-dir/app/root/dashboard/deployments/info.server.js new file mode 100644 index 0000000000000..e11cc49991086 --- /dev/null +++ b/test/e2e/root-dir/app/root/dashboard/deployments/info.server.js @@ -0,0 +1,7 @@ +export default function DeploymentsInfoPage(props) { + return ( + <> +hello from root/dashboard/deployments/info
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/dashboard/index.server.js b/test/e2e/root-dir/app/root/dashboard/index.server.js new file mode 100644 index 0000000000000..f80ed6fe91206 --- /dev/null +++ b/test/e2e/root-dir/app/root/dashboard/index.server.js @@ -0,0 +1,7 @@ +export default function DashboardPage(props) { + return ( + <> +hello from root/dashboard
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/dashboard/integrations/index.server.js b/test/e2e/root-dir/app/root/dashboard/integrations/index.server.js new file mode 100644 index 0000000000000..0300726704aad --- /dev/null +++ b/test/e2e/root-dir/app/root/dashboard/integrations/index.server.js @@ -0,0 +1,7 @@ +export default function IntegrationsPage(props) { + return ( + <> +hello from root/dashboard/integrations
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/partial-match-[id].server.js b/test/e2e/root-dir/app/root/partial-match-[id].server.js new file mode 100644 index 0000000000000..c14c8105e909f --- /dev/null +++ b/test/e2e/root-dir/app/root/partial-match-[id].server.js @@ -0,0 +1,15 @@ +export async function getServerSideProps({ params }) { + return { + props: { + id: params.id, + }, + } +} + +export default function DeploymentsPage(props) { + return ( + <> +hello from root/partial-match-[id]. ID is: {props.id}
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/shared-component-route.js b/test/e2e/root-dir/app/root/shared-component-route.js new file mode 100644 index 0000000000000..a1d4062296914 --- /dev/null +++ b/test/e2e/root-dir/app/root/shared-component-route.js @@ -0,0 +1,7 @@ +export default function SharedComponentRoute() { + return ( + <> +hello from root/shared-component-route
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/should-not-serve-client.client.js b/test/e2e/root-dir/app/root/should-not-serve-client.client.js new file mode 100644 index 0000000000000..8e0300fed5d8c --- /dev/null +++ b/test/e2e/root-dir/app/root/should-not-serve-client.client.js @@ -0,0 +1,7 @@ +export default function ShouldNotServeClientDotJs(props) { + return ( + <> +hello from root/should-not-serve-client
+ > + ) +} diff --git a/test/e2e/root-dir/app/root/should-not-serve-server.server.js b/test/e2e/root-dir/app/root/should-not-serve-server.server.js new file mode 100644 index 0000000000000..eb0db64e1151e --- /dev/null +++ b/test/e2e/root-dir/app/root/should-not-serve-server.server.js @@ -0,0 +1,7 @@ +export default function ShouldNotServeServerDotJs(props) { + return ( + <> +hello from root/should-not-serve-server
+ > + ) +} diff --git a/test/e2e/root-dir/index.test.ts b/test/e2e/root-dir/index.test.ts new file mode 100644 index 0000000000000..2f9b1b9dc91de --- /dev/null +++ b/test/e2e/root-dir/index.test.ts @@ -0,0 +1,263 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' +import path from 'path' +import cheerio from 'cheerio' +import webdriver from 'next-webdriver' + +// TODO: implementation +describe.skip('root dir', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + public: new FileRef(path.join(__dirname, 'app/public')), + pages: new FileRef(path.join(__dirname, 'app/pages')), + root: new FileRef(path.join(__dirname, 'app/root')), + 'root.server.js': new FileRef( + path.join(__dirname, 'app/root.server.js') + ), + 'next.config.js': new FileRef( + path.join(__dirname, 'app/next.config.js') + ), + }, + dependencies: { + react: '18.0.0-rc.2', + 'react-dom': '18.0.0-rc.2', + }, + }) + }) + afterAll(() => next.destroy()) + + it('should serve from pages', async () => { + const html = await renderViaHTTP(next.url, '/') + expect(html).toContain('hello from pages/index') + }) + + it('should serve dynamic route from pages', async () => { + const html = await renderViaHTTP(next.url, '/blog/first') + expect(html).toContain('hello from pages/blog/[slug]') + }) + + it('should serve from public', async () => { + const html = await renderViaHTTP(next.url, '/hello.txt') + expect(html).toContain('hello world') + }) + + it('should serve from root', async () => { + const html = await renderViaHTTP(next.url, '/dashboard') + expect(html).toContain('hello from root/dashboard') + }) + + it('should include layouts when no direct parent layout', async () => { + const html = await renderViaHTTP(next.url, '/dashboard/integrations') + const $ = cheerio.load(html) + // Should not be nested in dashboard + expect($('h1').text()).toBe('Dashboard') + // Should include the page text + expect($('p').text()).toBe('hello from root/dashboard/integrations') + }) + + // TODO: why is this routable but /should-not-serve-server.server.js + it('should not include parent when not in parent directory with route in directory', async () => { + const html = await renderViaHTTP(next.url, '/dashboard/rootonly/hello') + const $ = cheerio.load(html) + + // Should be nested in /root.js + expect($('html').hasClass('this-is-the-document-html')).toBeTruthy() + expect($('body').hasClass('this-is-the-document-body')).toBeTruthy() + + // Should not be nested in dashboard + expect($('h1').text()).toBeFalsy() + + // Should render the page text + expect($('p').text()).toBe('hello from root/dashboard/rootonly/hello') + }) + + it('should include parent document when no direct parent layout', async () => { + const html = await renderViaHTTP(next.url, '/dashboard/integrations') + const $ = cheerio.load(html) + + // Root has to provide it's own document + expect($('html').hasClass('this-is-the-document-html')).toBeTruthy() + expect($('body').hasClass('this-is-the-document-body')).toBeTruthy() + }) + + it('should not include parent when not in parent directory', async () => { + const html = await renderViaHTTP(next.url, '/dashboard/changelog') + const $ = cheerio.load(html) + // Should not be nested in dashboard + expect($('h1').text()).toBeFalsy() + // Should include the page text + expect($('p').text()).toBe('hello from root/dashboard/changelog') + }) + + it('should serve nested parent', async () => { + const html = await renderViaHTTP(next.url, '/dashboard/deployments/123') + const $ = cheerio.load(html) + // Should be nested in dashboard + expect($('h1').text()).toBe('Dashboard') + // Should be nested in deployments + expect($('h2').text()).toBe('Deployments hello') + }) + + it('should serve dynamic parameter', async () => { + const html = await renderViaHTTP(next.url, '/dashboard/deployments/123') + const $ = cheerio.load(html) + // Should include the page text with the parameter + expect($('p').text()).toBe( + 'hello from root/dashboard/deployments/[id]. ID is: 123' + ) + }) + + it('should include document html and body', async () => { + const html = await renderViaHTTP(next.url, '/dashboard') + const $ = cheerio.load(html) + + expect($('html').hasClass('this-is-the-document-html')).toBeTruthy() + expect($('body').hasClass('this-is-the-document-body')).toBeTruthy() + }) + + it('should not serve when layout is provided but no folder index', async () => { + const res = await fetchViaHTTP(next.url, '/dashboard/deployments') + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + // TODO: do we want to make this only work for /root or is it allowed + // to work for /pages as well? + it('should match partial parameters', async () => { + const html = await renderViaHTTP(next.url, '/partial-match-123') + expect(html).toContain('hello from root/partial-match-[id]. ID is: 123') + }) + + describe('parallel routes', () => { + describe('conditional routes', () => { + it('should serve user page', async () => { + const html = await renderViaHTTP(next.url, '/conditional/tim') + expect(html).toContain('hello from user homepage') + }) + + it('should serve user teams page', async () => { + const html = await renderViaHTTP(next.url, '/conditional/tim/teams') + expect(html).toContain('hello from user/teams') + }) + + it('should not serve teams page to user', async () => { + const html = await renderViaHTTP(next.url, '/conditional/tim/members') + expect(html).not.toContain('hello from team/members') + }) + + it('should serve team page', async () => { + const html = await renderViaHTTP(next.url, '/conditional/vercel') + expect(html).toContain('hello from team homepage') + }) + + it('should serve team members page', async () => { + const html = await renderViaHTTP( + next.url, + '/conditional/vercel/members' + ) + expect(html).toContain('hello from team/members') + }) + + it('should provide both matches if both paths match', async () => { + const html = await renderViaHTTP(next.url, '/conditional/both') + expect(html).toContain('hello from team homepage') + expect(html).toContain('hello from user homepage') + }) + + it('should 404 based on getServerSideProps', async () => { + const res = await fetchViaHTTP(next.url, '/conditional/nonexistent') + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + }) + }) + + describe('server components', () => { + // TODO: why is this not servable but /dashboard+rootonly/hello.server.js + // should be? Seems like they both either should be servable or not + it('should not serve .server.js as a path', async () => { + // Without .server.js should serve + const html = await renderViaHTTP(next.url, '/should-not-serve-server') + expect(html).toContain('hello from root/should-not-serve-server') + + // Should not serve `.server` + const res = await fetchViaHTTP( + next.url, + '/should-not-serve-server.server' + ) + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + + // Should not serve `.server.js` + const res2 = await fetchViaHTTP( + next.url, + '/should-not-serve-server.server.js' + ) + expect(res2.status).toBe(404) + expect(await res2.text()).toContain('This page could not be found') + }) + + it('should not serve .client.js as a path', async () => { + // Without .client.js should serve + const html = await renderViaHTTP(next.url, '/should-not-serve-client') + expect(html).toContain('hello from root/should-not-serve-client') + + // Should not serve `.client` + const res = await fetchViaHTTP( + next.url, + '/should-not-serve-client.client' + ) + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + + // Should not serve `.client.js` + const res2 = await fetchViaHTTP( + next.url, + '/should-not-serve-client.client.js' + ) + expect(res2.status).toBe(404) + expect(await res2.text()).toContain('This page could not be found') + }) + + it('should serve shared component', async () => { + // Without .client.js should serve + const html = await renderViaHTTP(next.url, '/shared-component-route') + expect(html).toContain('hello from root/shared-component-route') + }) + + it('should serve client component', async () => { + const html = await renderViaHTTP(next.url, '/client-component-route') + expect(html).toContain('hello from root/client-component-route. count: 0') + + const browser = await webdriver(next.url, '/client-component-route') + // After hydration count should be 1 + expect(await browser.elementByCss('p').text()).toBe( + 'hello from root/client-component-route. count: 1' + ) + }) + + it('should include client component layout with server component route', async () => { + const html = await renderViaHTTP(next.url, '/client-nested') + const $ = cheerio.load(html) + // Should not be nested in dashboard + expect($('h1').text()).toBe('Client Nested. Count: 0') + // Should include the page text + expect($('p').text()).toBe('hello from root/client-nested') + + const browser = await webdriver(next.url, '/client-nested') + // After hydration count should be 1 + expect(await browser.elementByCss('h1').text()).toBe( + 'Client Nested. Count: 0' + ) + + // After hydration count should be 1 + expect(await browser.elementByCss('h1').text()).toBe( + 'hello from root/client-nested' + ) + }) + }) +})