Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 193 additions & 2 deletions packages/react-router/tests/Scripts.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,59 @@
import { describe, expect, test } from 'vitest'
import { act, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, test } from 'vitest'
import {
act,
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react'
import { createPortal } from 'react-dom'
import ReactDOMServer from 'react-dom/server'

import {
HeadContent,
Link,
Outlet,
RouterProvider,
createBrowserHistory,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'
import { Scripts } from '../src/Scripts'
import type { Manifest } from '@tanstack/router-core'

const createTestManifest = (routeId: string) =>
({
routes: {
[routeId]: {
assets: [
{
tag: 'link',
attrs: {
rel: 'stylesheet',
href: '/main.css',
},
},
],
},
},
}) satisfies Manifest

const browserHistories: Array<ReturnType<typeof createBrowserHistory>> = []

const createTestBrowserHistory = () => {
const history = createBrowserHistory()
browserHistories.push(history)
return history
}

afterEach(() => {
cleanup()
browserHistories.splice(0).forEach((history) => history.destroy())
window.history.replaceState(null, 'root', '/')
})

describe('ssr scripts', () => {
test('it works', async () => {
Expand Down Expand Up @@ -327,6 +369,155 @@ describe('ssr HeadContent', () => {
`<title>Index</title><meta name="image" content="image.jpg"/><meta property="og:description" content="Root description"/><meta name="description" content="Index"/><meta name="last-modified" content="2021-10-10"/><meta property="og:image" content="index-image.jpg"/>`,
)
})

test('keeps manifest stylesheet links mounted when history state changes', async () => {
const history = createTestBrowserHistory()

const rootRoute = createRootRoute({
component: () => {
return (
<>
{createPortal(<HeadContent />, document.head)}
<button
onClick={() => {
window.history.replaceState(
{ slideId: 'slide-2' },
'',
window.location.href,
)
}}
>
Replace state
</button>
<Outlet />
</>
)
},
})

const indexRoute = createRoute({
path: '/',
getParentRoute: () => rootRoute,
component: () => <div>Index</div>,
})

const router = createRouter({
history,
routeTree: rootRoute.addChildren([indexRoute]),
})

router.ssr = {
manifest: createTestManifest(rootRoute.id),
}

await router.load()

await act(() => render(<RouterProvider router={router} />))

const getStylesheetLink = () =>
Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find(
(link) => link.getAttribute('href') === '/main.css',
)

await waitFor(() => {
expect(getStylesheetLink()).toBeInstanceOf(HTMLLinkElement)
})

const initialLink = getStylesheetLink()
expect(initialLink).toBeInstanceOf(HTMLLinkElement)

fireEvent.click(screen.getByRole('button', { name: 'Replace state' }))

await waitFor(() => {
expect(router.state.location.state).toMatchObject({
slideId: 'slide-2',
})
})

expect(getStylesheetLink()).toBe(initialLink)
expect(
Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(
(link) => link.getAttribute('href') === '/main.css',
),
).toHaveLength(1)
})

test('keeps manifest stylesheet links mounted across repeated Link navigations', async () => {
const history = createTestBrowserHistory()

const rootRoute = createRootRoute({
component: () => {
return (
<>
{createPortal(<HeadContent />, document.head)}
<Outlet />
</>
)
},
})

const indexRoute = createRoute({
path: '/',
getParentRoute: () => rootRoute,
component: () => <Link to="/about">Go to about page</Link>,
})

const aboutRoute = createRoute({
path: '/about',
getParentRoute: () => rootRoute,
component: () => <Link to="/">Back to home</Link>,
})

const router = createRouter({
history,
routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
})

router.ssr = {
manifest: createTestManifest(rootRoute.id),
}

await router.load()

await act(() => render(<RouterProvider router={router} />))

const getStylesheetLink = () =>
Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find(
(link) => link.getAttribute('href') === '/main.css',
)

await waitFor(() => {
expect(getStylesheetLink()).toBeInstanceOf(HTMLLinkElement)
})

const initialLink = getStylesheetLink()
expect(initialLink).toBeInstanceOf(HTMLLinkElement)

for (let i = 0; i < 5; i++) {
fireEvent.click(screen.getByRole('link', { name: 'Go to about page' }))

await waitFor(() => {
expect(router.state.location.pathname).toBe('/about')
})

await screen.findByRole('link', { name: 'Back to home' })

fireEvent.click(screen.getByRole('link', { name: 'Back to home' }))

await waitFor(() => {
expect(router.state.location.pathname).toBe('/')
})

await screen.findByRole('link', { name: 'Go to about page' })
}

expect(getStylesheetLink()).toBe(initialLink)
expect(
Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(
(link) => link.getAttribute('href') === '/main.css',
),
).toHaveLength(1)
})
})

describe('data script rendering', () => {
Expand Down
11 changes: 8 additions & 3 deletions packages/solid-router/src/headContentUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Solid from 'solid-js'
import { escapeHtml } from '@tanstack/router-core'
import { escapeHtml, replaceEqualDeep } from '@tanstack/router-core'
import { useRouter } from './useRouter'
import type { RouterManagedTag } from '@tanstack/router-core'

Expand Down Expand Up @@ -179,8 +179,8 @@ export const useTags = () => {
})),
)

return () =>
uniqBy(
return Solid.createMemo((prev: Array<RouterManagedTag> | undefined) => {
const next = uniqBy(
[
...meta(),
...preloadLinks(),
Expand All @@ -192,6 +192,11 @@ export const useTags = () => {
return JSON.stringify(d)
},
)
if (prev === undefined) {
return next
}
return replaceEqualDeep(prev, next)
})
}

export function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
Expand Down
Loading
Loading