diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts new file mode 100644 index 00000000000..7ce334330d5 --- /dev/null +++ b/integration/hmr-test.ts @@ -0,0 +1,242 @@ +import { test, expect } from "@playwright/test"; +import execa from "execa"; +import fs from "node:fs"; +import path from "node:path"; +import type { Readable } from "node:stream"; + +import { createFixtureProject } from "./helpers/create-fixture"; + +let port = 3099; + +let fixture = { + future: { + unstable_dev: { + appServerPort: port, + }, + unstable_tailwind: true, + }, + files: { + "package.json": ` + { + "private": true, + "sideEffects": false, + "scripts": { + "dev:remix": "NODE_ENV=development node ../../../build/node_modules/@remix-run/dev/dist/cli.js dev", + "dev:app": "NODE_ENV=development nodemon --watch build/ ./server.js" + }, + "dependencies": { + "@remix-run/node": "0.0.0-local-version", + "@remix-run/react": "0.0.0-local-version", + "express": "0.0.0-local-version", + "nodemon": "0.0.0-local-version", + "react": "0.0.0-local-version", + "react-dom": "0.0.0-local-version", + "tailwindcss": "0.0.0-local-version" + }, + "devDependencies": { + "@remix-run/dev": "0.0.0-local-version", + "@types/react": "0.0.0-local-version", + "@types/react-dom": "0.0.0-local-version", + "typescript": "0.0.0-local-version" + }, + "engines": { + "node": ">=14" + } + } + `, + "server.js": ` + let path = require("path"); + let express = require("express"); + let { createRequestHandler } = require("@remix-run/express"); + + const app = express(); + app.use(express.static("public", { immutable: true, maxAge: "1y" })); + + const MODE = process.env.NODE_ENV; + const BUILD_DIR = path.join(process.cwd(), "build"); + + app.all( + "*", + createRequestHandler({ + build: require(BUILD_DIR), + mode: MODE, + }) + ); + + let port = ${port}; + app.listen(port, () => { + require(BUILD_DIR); + console.log('✅ app ready: http://localhost:' + port); + }); + `, + "tailwind.config.js": ` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ["./app/**/*.{ts,tsx,jsx,js}"], + theme: { + extend: {}, + }, + plugins: [], + }; + `, + "app/tailwind.css": ` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + "app/root.tsx": ` + import type { LinksFunction } from "@remix-run/node"; + import { Link, Links, Meta, Outlet, Scripts } from "@remix-run/react"; + + import styles from "./tailwind.css"; + + export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, + ]; + + export default function Root() { + return ( + + + + + + +
+ + +
+ + + + + ); + } + `, + "app/routes/index.tsx": ` + export default function Index() { + return ( +
+

Index

+ + +
+ ) + } + `, + "app/routes/a.tsx": ` + export default function A() { + return ( +
+

A

+ + +
+ ) + } + `, + "app/routes/b.tsx": ` + export default function B() { + return ( +
+

B

+ + +
+ ) + } + `, + }, +}; + +let sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +let wait = async ( + callback: () => boolean, + { timeoutMs = 1000, intervalMs = 250 } = {} +) => { + let start = Date.now(); + while (Date.now() - start <= timeoutMs) { + if (callback()) { + return; + } + await sleep(intervalMs); + } + throw Error(`wait: timeout ${timeoutMs}ms`); +}; + +let bufferize = (stream: Readable): (() => string) => { + let buffer = ""; + stream.on("data", (data) => (buffer += data.toString())); + return () => buffer; +}; + +test("blah", async ({ page }) => { + let projectDir = await createFixtureProject(fixture); + + let dev = execa("npm", ["run", "dev:remix"], { cwd: projectDir }); + let devStdout = bufferize(dev.stdout!); + await wait(() => /💿 Built in /.test(devStdout()), { timeoutMs: 3000 }); + + let app = execa("npm", ["run", "dev:app"], { cwd: projectDir }); + let appStdout = bufferize(app.stdout!); + await wait(() => /✅ app ready: /.test(appStdout())); + + try { + await page.goto(`http://localhost:${port}`); + + let input = page.getByLabel("Index Input"); + expect(input).toBeVisible(); + await input.type("asdfasdf"); + + let newIndex = ` + export default function Index() { + return ( +
+

Changed

+ + +
+ ) + } + `; + fs.writeFileSync( + path.join(projectDir, "app", "routes", "index.tsx"), + newIndex + ); + + await page.getByText("Changed").waitFor({ timeout: 1000 }); + let input2 = page.getByLabel("Persisted Input"); + expect(await input2.inputValue()).toBe("asdfasdf"); + } finally { + dev.kill(); + app.kill(); + } + + // expect home page + // setup stateful thing (e.g. text input) + // edit css + // expect style changes + // expect state remains + // edit markup + // expect markup changes + // expect state remains + // edit loader + // expect loader changes in markup + // expect state remains +}); + +// TEST undo flow +// 1. Go to route A +// 2. Go to route B +// 3. Modify code for route A +// 4. Navigate to route A +// 5. Nav to route B +// 6. Undo changes from (3) +// 7. Nav to route A