diff --git a/apps/web/e2e/smoke.spec.ts b/apps/web/e2e/smoke.spec.ts new file mode 100644 index 0000000..db01fea --- /dev/null +++ b/apps/web/e2e/smoke.spec.ts @@ -0,0 +1,48 @@ +/** + * Smoke test: verify the app loads without React errors. + * + * This test ensures that the Vite build resolves a single React + * instance (no duplicate React versions from pnpm transitive deps). + */ +import { test, expect } from "@playwright/test"; + +test("app loads without React hook errors", async ({ page }) => { + const consoleErrors: string[] = []; + const pageErrors: string[] = []; + + page.on("console", (msg) => { + if (msg.type() === "error") consoleErrors.push(msg.text()); + }); + page.on("pageerror", (err) => { + pageErrors.push(err.message); + }); + + await page.goto("/app", { waitUntil: "domcontentloaded", timeout: 10000 }); + await page.waitForTimeout(5000); + + // The critical check: no "Invalid hook call" or related React errors + const hookErrors = pageErrors.filter((e) => + e.includes("Invalid hook call") || + e.includes("useMemo") || + e.includes("useRef") || + e.includes("useContext") || + e.includes("more than one copy of React") + ); + + expect(hookErrors).toEqual([]); + + // Root element must exist + await expect(page.locator("#root")).toBeAttached(); + + // Should show either login form or app content (not a blank/dark screen) + const bodyText = await page.evaluate(() => document.body?.innerText || ""); + const hasContent = + bodyText.includes("Sign in") || + bodyText.includes("Anmelden") || + bodyText.includes("EvOo") || + bodyText.includes("Projekte") || + bodyText.includes("Heute") || + bodyText.includes("Team"); + + expect(hasContent).toBe(true); +}); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 33255f1..dccd5f2 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -226,6 +226,11 @@ function attachNovncProxy(server: ViteDevServer | PreviewServer, secret: string, } export default defineConfig(({ mode }) => { + const rootCwd = path.resolve(import.meta.dirname, "../.."); + const reactPath = path.resolve(rootCwd, "node_modules/react"); + const reactDomPath = path.resolve(rootCwd, "node_modules/react-dom"); + const reactIndex = path.resolve(reactPath, "index.js"); + const reactDomIndex = path.resolve(reactDomPath, "index.js"); const rootEnv = loadEnv(mode, path.resolve(import.meta.dirname, "../.."), ""); const api = process.env.API_PROXY_TARGET ?? rootEnv.API_PROXY_TARGET ?? "http://127.0.0.1:3100"; const previewHost = process.env.RAKAZO_HOST ?? rootEnv.RAKAZO_HOST ?? "localhost"; @@ -298,6 +303,14 @@ export default defineConfig(({ mode }) => { }, }, ], + resolve: { + alias: { + "react": reactPath, + "react-dom": reactDomPath, + "react/jsx-runtime": reactPath, + "react/jsx-dev-runtime": reactPath, + }, + }, server: { host: "0.0.0.0", port: webPort, diff --git a/packages/ui-web/package.json b/packages/ui-web/package.json index 0f684ad..44c7c9e 100644 --- a/packages/ui-web/package.json +++ b/packages/ui-web/package.json @@ -32,7 +32,7 @@ "vitest": "4.1.11" }, "peerDependencies": { - "react": "19.2.3", + "react": ">=16.8.0", "react-dom": "19.2.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a32807d..92c6b05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6498,6 +6498,10 @@ packages: resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} engines: {node: '>=14.14'} + fs-extra@11.4.1: + resolution: {integrity: sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -11433,7 +11437,7 @@ snapshots: dependencies: cross-dirname: 0.1.0 debug: 4.4.3 - fs-extra: 11.4.0 + fs-extra: 11.4.1 minimist: 1.2.8 postject: 1.0.0-alpha.6 transitivePeerDependencies: @@ -16663,6 +16667,13 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fs-extra@11.4.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + optional: true + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11