diff --git a/test/basic.test.ts b/test/basic.test.ts index ae7c9c56..36b00125 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -44,6 +44,7 @@ testDevAndProd("basic", ({ 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=content]")).toBe( prettyHtml(`

Hello from Vinxi

`), diff --git a/test/fs-router.test.ts b/test/fs-router.test.ts index 6b927395..d3afed7e 100644 --- a/test/fs-router.test.ts +++ b/test/fs-router.test.ts @@ -60,6 +60,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,7 +77,7 @@ testDevAndProd("fs-router", ({ createFixture }) => { await app.clickElement("a[href='/hello']"); await app.waitForURL("/hello"); - await new Promise((r) => setTimeout(r, 1000)); + await app.isReady(); expect(await app.getHtml("[data-test-id=title]")).toBe( prettyHtml(`

Hello from Vinxi

`), @@ -87,7 +88,7 @@ testDevAndProd("fs-router", ({ createFixture }) => { await app.clickElement("a[href='/']"); await app.waitForURL("/"); - await new Promise((r) => setTimeout(r, 2000)); + await app.isReady(); expect(await app.getHtml("[data-test-id=title]")).toBe( prettyHtml(`

Vinxi Home

`), diff --git a/test/helpers/create-fixture.ts b/test/helpers/create-fixture.ts index 7146b5d4..5e7d2b23 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, 2000)); } } + cache.clear(); // await fse.remove(projectDir); // projectDir = await createFixtureProject(init); }, @@ -157,6 +159,7 @@ export async function createDevFixture(init: FixtureInit) { } await fse.writeFile(path.join(projectDir, filename), content); + await new Promise((r) => setTimeout(r, 2000)); }, createServer: async () => { return { diff --git a/test/helpers/playwright-fixture.ts b/test/helpers/playwright-fixture.ts index d6bbc95a..428beae5 100644 --- a/test/helpers/playwright-fixture.ts +++ b/test/helpers/playwright-fixture.ts @@ -190,6 +190,36 @@ 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 () { + let DEBUG = !!process.env.DEBUG; + + const readyCheck = async (timeout?: number) => { + await this.page.waitForLoadState("networkidle"); + await this.page.waitForLoadState("load"); + await this.page.locator('[data-ready]').waitFor({ state: "attached", timeout }); + } + + for(let i = 1; i <= 3; i++) { + try { + await readyCheck(20 * i * 1000) + // Give React Suspense some extra time to remove its magical display: hidden + await new Promise(res => setTimeout(res, 2000)); + return true; + } catch(err) { + if (DEBUG) { + console.log('Something went wrong during the page hydration, reloading the page') + } + await this.page.reload() + } + } + + throw new Error('Page hydration failed'); + } } export async function getHtml(page: Page, selector?: string) { diff --git a/test/hmr.test.ts b/test/hmr.test.ts index 45429deb..7aa12919 100644 --- a/test/hmr.test.ts +++ b/test/hmr.test.ts @@ -74,8 +74,6 @@ 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"); @@ -87,6 +85,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

`), @@ -118,8 +117,6 @@ test.describe("hmr", () => { }`, ); - await new Promise((r) => setTimeout(r, 1000)); - expect(await app.getHtml("[data-test-id=button]")).toBe( prettyHtml(``), ); @@ -138,8 +135,6 @@ test.describe("hmr", () => { }`, ); - await new Promise((r) => setTimeout(r, 1000)); - res = await fixture.requestDocument("/api/hello"); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("text/html"); @@ -152,8 +147,6 @@ test.describe("hmr", () => { }`, ); - await new Promise((r) => setTimeout(r, 2000)); - res = await fixture.requestDocument("/api/new"); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("text/html"); diff --git a/test/playwright.config.ts b/test/playwright.config.ts index d29cb353..c1432b75 100644 --- a/test/playwright.config.ts +++ b/test/playwright.config.ts @@ -5,12 +5,11 @@ 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 }, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? "github" : [["html", { open: process.env.TEST_REPORT ? "always" : "none" }]], diff --git a/test/rsc.test.ts b/test/rsc.test.ts index b15c53bd..dc42d84b 100644 --- a/test/rsc.test.ts +++ b/test/rsc.test.ts @@ -36,6 +36,7 @@ test.describe("rsc", () => { test("spa", 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(`

Hello from Vinxi

`), diff --git a/test/srv-fn.test.ts b/test/srv-fn.test.ts index 862ed6c0..d362e8c1 100644 --- a/test/srv-fn.test.ts +++ b/test/srv-fn.test.ts @@ -32,6 +32,7 @@ testDevAndProd("srv-fn", ({ createFixture }) => { test("spa", async ({ page }) => { let app = new PlaywrightFixture(appFixture, page); await app.goto("/", true); + await app.isReady(); let responses = app.collectResponses(); await app.clickElement("[data-test-id=button]"); diff --git a/test/templates/react-rsc/app/Counter.tsx b/test/templates/react-rsc/app/Counter.tsx index 84657b21..be79514e 100644 --- a/test/templates/react-rsc/app/Counter.tsx +++ b/test/templates/react-rsc/app/Counter.tsx @@ -1,8 +1,14 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export function Counter({ onChange }) { + useEffect(() => { + document.documentElement.dataset.ready = ""; + return () => { + document.documentElement.dataset.ready = null; + } + }, []); const [count, setCount] = useState(0); return (