From b4bddb8bace345f9690f6eb326ed529d404d0b75 Mon Sep 17 00:00:00 2001 From: Katja Lutz Date: Tue, 25 Jun 2024 17:00:32 +0200 Subject: [PATCH] fix: wait for hydration via react effect --- test/basic.test.ts | 20 ++++++++++++-------- test/fs-router.test.ts | 20 ++++++++++++++------ test/helpers/create-fixture.ts | 2 ++ test/helpers/playwright-fixture.ts | 8 ++++++++ test/hmr.test.ts | 19 ++++++++++++------- test/playwright.config.ts | 2 +- test/templates/react-ssr-fs/app/app.tsx | 16 ++++++++++++++-- test/templates/react/app/root.tsx | 17 ++++++++++++++--- 8 files changed, 77 insertions(+), 27 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index ae7c9c56..5ba6d816 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -32,10 +32,12 @@ testDevAndProd("basic", ({ createFixture }) => { }); }); - test("ssr", async () => { - let res = await fixture.requestDocument("/"); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("text/html"); + test("ssr", async ({ page }) => { + const app = new PlaywrightFixture(appFixture, page); + const res = await app.goto("/", true); + expect(res.status()).toBe(200); + const headers = await res.allHeaders(); + expect(headers["content-type"]).toBe("text/html"); expect(selectHtml(await res.text(), "[data-test-id=content]")).toBe( prettyHtml(`

Hello from Vinxi

`), ); @@ -59,10 +61,12 @@ testDevAndProd("basic", ({ createFixture }) => { ); }); - test("api", async () => { - let res = await fixture.requestDocument("/api/hello"); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("text/html"); + test("api", async ({ page }) => { + const app = new PlaywrightFixture(appFixture, page); + const res = await app.goto("/api/hello", true); + expect(res.status()).toBe(200); + const headers = await res.allHeaders(); + expect(headers["content-type"]).toBe("text/html"); expect(await res.text()).toBe("Hello world"); }); }); diff --git a/test/fs-router.test.ts b/test/fs-router.test.ts index 6b927395..a8bb982d 100644 --- a/test/fs-router.test.ts +++ b/test/fs-router.test.ts @@ -34,9 +34,12 @@ testDevAndProd("fs-router", ({ createFixture }) => { }); test("ssr", async ({ page }) => { - let res = await fixture.requestDocument("/"); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("text/html"); + let app = new PlaywrightFixture(appFixture, page); + let res = await app.goto("/", true); + let headers = await res.allHeaders(); + + expect(res.status()).toBe(200); + expect(headers["content-type"]).toBe("text/html"); let html = await res.text(); expect(selectHtml(html, "[data-test-id=title]")).toBe( prettyHtml(`

Vinxi Home

`), @@ -45,9 +48,11 @@ testDevAndProd("fs-router", ({ createFixture }) => { prettyHtml(``), ); - res = await fixture.requestDocument("/hello"); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("text/html"); + res = await app.goto("/hello", true); + headers = await res.allHeaders(); + + expect(res.status()).toBe(200); + expect(headers["content-type"]).toBe("text/html"); html = await res.text(); expect(selectHtml(html, "[data-test-id=title]")).toBe( prettyHtml(`

Hello from Vinxi

`), @@ -60,6 +65,7 @@ testDevAndProd("fs-router", ({ createFixture }) => { test("hydrates", async ({ page }) => { let app = new PlaywrightFixture(appFixture, page); await app.goto("/", true); + await app.isReady(); expect(await app.getHtml("[data-test-id=title]")).toBe( prettyHtml(`

Vinxi Home

`), @@ -76,6 +82,7 @@ testDevAndProd("fs-router", ({ createFixture }) => { await app.clickElement("a[href='/hello']"); await app.waitForURL("/hello"); + await app.isReady(); await new Promise((r) => setTimeout(r, 1000)); expect(await app.getHtml("[data-test-id=title]")).toBe( @@ -87,6 +94,7 @@ testDevAndProd("fs-router", ({ createFixture }) => { await app.clickElement("a[href='/']"); await app.waitForURL("/"); + await app.isReady(); await new Promise((r) => setTimeout(r, 2000)); expect(await app.getHtml("[data-test-id=title]")).toBe( diff --git a/test/helpers/create-fixture.ts b/test/helpers/create-fixture.ts index 7146b5d4..7e527b6e 100644 --- a/test/helpers/create-fixture.ts +++ b/test/helpers/create-fixture.ts @@ -133,8 +133,10 @@ export async function createDevFixture(init: FixtureInit) { await fse.remove(path.join(projectDir, filename)); } else { await fse.writeFile(path.join(projectDir, filename), prevValue); + await new Promise((r) => setTimeout(r, 1000)); } } + cache.clear(); // await fse.remove(projectDir); // projectDir = await createFixtureProject(init); }, diff --git a/test/helpers/playwright-fixture.ts b/test/helpers/playwright-fixture.ts index d6bbc95a..d3d3038b 100644 --- a/test/helpers/playwright-fixture.ts +++ b/test/helpers/playwright-fixture.ts @@ -190,6 +190,14 @@ export class PlaywrightFixture { cp.exec(`open ${this.app.serverUrl}${href}`); return new Promise(res => setTimeout(res, ms)); } + + /** + * Checks if the page is hydrated via a data-ready attribute, + * that is being set with useEffect. + */ + async isReady () { + return this.page.locator('[data-ready]').waitFor({ state: "attached" }); + } } export async function getHtml(page: Page, selector?: string) { diff --git a/test/hmr.test.ts b/test/hmr.test.ts index 45429deb..67412938 100644 --- a/test/hmr.test.ts +++ b/test/hmr.test.ts @@ -44,10 +44,13 @@ test.describe("hmr", () => { await fixture.reset(); }); - test("hmr ssr", async () => { - let res = await fixture.requestDocument("/"); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("text/html"); + test("hmr ssr", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + let res = await app.goto("/", true); + let headers = await res.allHeaders(); + + expect(res.status()).toBe(200); + expect(headers["content-type"]).toBe("text/html"); expect(await selectHtml(await res.text(), "[data-test-id=content]")).toBe( prettyHtml(`

Hello from Vinxi

`), ); @@ -76,9 +79,10 @@ test.describe("hmr", () => { await new Promise((r) => setTimeout(r, 1000)); - res = await fixture.requestDocument("/"); - expect(res.status).toBe(200); - expect(res.headers.get("Content-Type")).toBe("text/html"); + res = await app.goto("/", true); + headers = await res.allHeaders(); + expect(res.status()).toBe(200); + expect(headers["content-type"]).toBe("text/html"); expect(selectHtml(await res.text(), "[data-test-id=content]")).toBe( prettyHtml(`

Hello from Vinxi too

`), ); @@ -87,6 +91,7 @@ test.describe("hmr", () => { test("client hmr", async ({ page }) => { let app = new PlaywrightFixture(appFixture, page); await app.goto("/", true); + await app.isReady(); expect(await app.getHtml("[data-test-id=content]")).toBe( prettyHtml(`

Hello from Vinxi

`), diff --git a/test/playwright.config.ts b/test/playwright.config.ts index d29cb353..2072d617 100644 --- a/test/playwright.config.ts +++ b/test/playwright.config.ts @@ -5,7 +5,7 @@ const config: PlaywrightTestConfig = { testDir: ".", testMatch: ["**/*.test.ts"], fullyParallel: false, - timeout: process.env.CI ? 120_000 : 30_000, // 2 minutes in CI, 30 seconds locally + timeout: process.env.CI ? 360_000 : 30_000, // 5 minutes in CI, 30 seconds locally expect: { timeout: 5_000, // 5 second retries for assertions }, diff --git a/test/templates/react-ssr-fs/app/app.tsx b/test/templates/react-ssr-fs/app/app.tsx index 0bf0c666..e0e92483 100644 --- a/test/templates/react-ssr-fs/app/app.tsx +++ b/test/templates/react-ssr-fs/app/app.tsx @@ -1,12 +1,24 @@ -import React from "react"; +import React, { useEffect, useRef } from "react"; import { Counter } from "./Counter"; import "./style.css"; +const useReadyRef = () => { + const ref = useRef(); + useEffect(() => { + ref.current.dataset.ready = ""; + return () => { + ref.current.dataset.ready = null; + } + }, []); + return ref; +} + export default function App({ assets, children }) { const [count, setCount] = React.useState(0); + const ref = useReadyRef(); return ( - + {assets} diff --git a/test/templates/react/app/root.tsx b/test/templates/react/app/root.tsx index 908eb70e..d4afbadb 100644 --- a/test/templates/react/app/root.tsx +++ b/test/templates/react/app/root.tsx @@ -1,10 +1,21 @@ -import React from "react"; - +import React, { useEffect, useRef } from "react"; import { Counter } from "./Counter"; +const useReadyRef = () => { + const ref = useRef(); + useEffect(() => { + ref.current.dataset.ready = ""; + return () => { + ref.current.dataset.ready = null; + } + }, []); + return ref; +} + export default function App({ assets }) { + const ref = useReadyRef(); return ( - + {assets}