diff --git a/e2e/solid-start/selective-ssr/package.json b/e2e/solid-start/selective-ssr/package.json index 886d94534bf..9435bf931be 100644 --- a/e2e/solid-start/selective-ssr/package.json +++ b/e2e/solid-start/selective-ssr/package.json @@ -9,7 +9,8 @@ "build": "vite build && tsc --noEmit", "preview": "vite preview", "start": "pnpx srvx --prod -s ../client dist/server/server.js", - "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" + "test:e2e": "rm -rf port*.txt; playwright test --project=chromium", + "test:e2e:dev": "rm -rf port*.txt; playwright test --config=playwright.dev.config.ts --project=chromium" }, "dependencies": { "@tanstack/solid-router": "workspace:^", diff --git a/e2e/solid-start/selective-ssr/playwright.dev.config.ts b/e2e/solid-start/selective-ssr/playwright.dev.config.ts new file mode 100644 index 00000000000..653df5e9980 --- /dev/null +++ b/e2e/solid-start/selective-ssr/playwright.dev.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(`${packageJson.name}_dev`) +const baseURL = `http://localhost:${PORT}` + +export default defineConfig({ + testDir: './tests', + workers: 1, + reporter: [['line']], + use: { + baseURL, + }, + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm exec vite dev --host 127.0.0.1 --port ${PORT}`, + url: baseURL, + reuseExistingServer: false, + stdout: 'pipe', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/selective-ssr/src/routeTree.gen.ts b/e2e/solid-start/selective-ssr/src/routeTree.gen.ts index 423535df711..4299ea185c0 100644 --- a/e2e/solid-start/selective-ssr/src/routeTree.gen.ts +++ b/e2e/solid-start/selective-ssr/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as PostsRouteImport } from './routes/posts' +import { Route as MreDataOnlyRouteImport } from './routes/mre-data-only' import { Route as IndexRouteImport } from './routes/index' import { Route as PostsPostIdRouteImport } from './routes/posts.$postId' @@ -18,6 +19,11 @@ const PostsRoute = PostsRouteImport.update({ path: '/posts', getParentRoute: () => rootRouteImport, } as any) +const MreDataOnlyRoute = MreDataOnlyRouteImport.update({ + id: '/mre-data-only', + path: '/mre-data-only', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -31,30 +37,34 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/mre-data-only': typeof MreDataOnlyRoute '/posts': typeof PostsRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/mre-data-only': typeof MreDataOnlyRoute '/posts': typeof PostsRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/mre-data-only': typeof MreDataOnlyRoute '/posts': typeof PostsRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/posts' | '/posts/$postId' + fullPaths: '/' | '/mre-data-only' | '/posts' | '/posts/$postId' fileRoutesByTo: FileRoutesByTo - to: '/' | '/posts' | '/posts/$postId' - id: '__root__' | '/' | '/posts' | '/posts/$postId' + to: '/' | '/mre-data-only' | '/posts' | '/posts/$postId' + id: '__root__' | '/' | '/mre-data-only' | '/posts' | '/posts/$postId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + MreDataOnlyRoute: typeof MreDataOnlyRoute PostsRoute: typeof PostsRouteWithChildren } @@ -67,6 +77,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof PostsRouteImport parentRoute: typeof rootRouteImport } + '/mre-data-only': { + id: '/mre-data-only' + path: '/mre-data-only' + fullPath: '/mre-data-only' + preLoaderRoute: typeof MreDataOnlyRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -96,6 +113,7 @@ const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + MreDataOnlyRoute: MreDataOnlyRoute, PostsRoute: PostsRouteWithChildren, } export const routeTree = rootRouteImport diff --git a/e2e/solid-start/selective-ssr/src/routes/index.tsx b/e2e/solid-start/selective-ssr/src/routes/index.tsx index c0da00c2aa6..858d8aebc27 100644 --- a/e2e/solid-start/selective-ssr/src/routes/index.tsx +++ b/e2e/solid-start/selective-ssr/src/routes/index.tsx @@ -174,6 +174,12 @@ function Home() {
test count: {links.length}
+
+ + MRE data-only + pendingComponent + +
+
{links}
) diff --git a/e2e/solid-start/selective-ssr/src/routes/mre-data-only.tsx b/e2e/solid-start/selective-ssr/src/routes/mre-data-only.tsx new file mode 100644 index 00000000000..aa328a14f19 --- /dev/null +++ b/e2e/solid-start/selective-ssr/src/routes/mre-data-only.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/mre-data-only')({ + ssr: 'data-only', + loader: async () => { + await new Promise((resolve) => setTimeout(resolve, 900)) + return { loadedAt: new Date().toISOString() } + }, + pendingComponent: () =>
PENDING
, + component: MreDataOnlyRoute, +}) + +function MreDataOnlyRoute() { + const data = Route.useLoaderData() + + return ( +
+

OK — loader finished

+
+        {JSON.stringify(data(), null, 2)}
+      
+
+ ) +} diff --git a/e2e/solid-start/selective-ssr/tests/pending-component-hydration.spec.ts b/e2e/solid-start/selective-ssr/tests/pending-component-hydration.spec.ts new file mode 100644 index 00000000000..b455a0b7f31 --- /dev/null +++ b/e2e/solid-start/selective-ssr/tests/pending-component-hydration.spec.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' + +test.describe('pending component hydration', () => { + test('data-only route hydrates from pending element to loaded state on first load', async ({ + page, + }) => { + const browserErrors: string[] = [] + + page.on('pageerror', (error) => { + browserErrors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error' || message.type() === 'warning') { + browserErrors.push(message.text()) + } + }) + + await page.goto('/mre-data-only') + + await expect(page.getByTestId('mre-data-only-ready-label')).toHaveText( + 'OK — loader finished', + { timeout: 5_000 }, + ) + + expect(browserErrors).not.toContainEqual( + expect.stringContaining('template is not a function'), + ) + expect(browserErrors).not.toContainEqual( + expect.stringContaining('Hydration Mismatch'), + ) + }) +})