diff --git a/packages/login/next.config.js b/packages/login/next.config.js index d909856a7d..f01221c3c0 100644 --- a/packages/login/next.config.js +++ b/packages/login/next.config.js @@ -26,12 +26,23 @@ /** @type {import('next').NextConfig} */ -const path = require("path"); const pkg = require("./package.json"); +const IS_TEST = process.env.TEST; + +const output = IS_TEST ? {} : { output: "standalone" }; +const imgGenerator = IS_TEST + ? {} + : { + generator: { + emit: false, + filename: "static/chunks/[path][name][ext]?[hash]", + }, + }; + const nextConfig = { basePath: "/login", - output: "standalone", + ...output, compiler: { styledComponents: true, }, @@ -74,10 +85,7 @@ module.exports = { // Reapply the existing rule, but only for svg imports ending in ?url { type: "asset/resource", - generator: { - emit: false, - filename: "static/chunks/[path][name][ext]?[hash]", - }, + ...imgGenerator, test: /\.(svg|png|jpe?g|gif|ico|woff2)$/i, resourceQuery: /url/, // *.svg?url }, diff --git a/packages/login/package.json b/packages/login/package.json index 6616cbedac..db160af124 100644 --- a/packages/login/package.json +++ b/packages/login/package.json @@ -11,10 +11,9 @@ "lint": "next lint", "clean": "shx rm -rf .next", "deploy": "shx --silent mkdir -p ../../../publish/web/login && shx --silent mkdir -p ../../../publish/web/login/.next && shx --silent mkdir -p ../../../publish/web/login/node_modules && shx --silent mkdir -p ../../../publish/web/login/.next/static && shx cp -r .next/standalone/node_modules/* ../../../publish/web/login/node_modules && shx cp -r .next/static/* ../../../publish/web/login/.next/static && shx cp -r .next/standalone/packages/login/.next/* ../../../publish/web/login/.next && shx cp -f server.prod.js ../../../publish/web/login/server.js", - "test:build": "node ./scripts/buildTranslations.js && TEST=true next build", - "test:start": "PORT=5111 NODE_ENV=development TEST=true node server.js", - "test:e2e": "npx playwright test", - "test:e2e:ui": "npx playwright test --ui" + "test:build": "node ./scripts/buildTranslations.js && TEST=true API_HOST=https://api.example.com next build", + "test:start": "PORT=5111 API_HOST=https://api.example.com NODE_ENV=production TEST=true node server.js", + "test:e2e": "npx playwright test" }, "dependencies": { "@hcaptcha/react-hcaptcha": "^1.10.1", diff --git a/packages/login/playwright.config.ts b/packages/login/playwright.config.ts index d9ff55cf94..9b3dd30de5 100644 --- a/packages/login/playwright.config.ts +++ b/packages/login/playwright.config.ts @@ -1,5 +1,9 @@ import { defineConfig, devices } from "@playwright/test"; +import { BASE_URL } from "@docspace/shared/__mocks__/e2e"; + +const PORT = 5111; + /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -25,7 +29,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://127.0.0.1:5111", + baseURL: `${BASE_URL}:${PORT}`, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -72,7 +76,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { command: "yarn test:start", - port: 5111, + port: PORT, timeout: 1000 * 60 * 5, }, }); diff --git a/packages/login/public/mockServiceWorker.js b/packages/login/public/mockServiceWorker.js new file mode 100644 index 0000000000..9148ff615b --- /dev/null +++ b/packages/login/public/mockServiceWorker.js @@ -0,0 +1,287 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (2.0.14). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = "c5f7f8e188b673ea4e677df7ea3c5a39"; +const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); +const activeClientIds = new Set(); + +self.addEventListener("install", function () { + self.skipWaiting(); +}); + +self.addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", async function (event) { + const clientId = event.source.id; + + if (!clientId || !self.clients) { + return; + } + + const client = await self.clients.get(clientId); + + if (!client) { + return; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + switch (event.data) { + case "KEEPALIVE_REQUEST": { + sendToClient(client, { + type: "KEEPALIVE_RESPONSE", + }); + break; + } + + case "INTEGRITY_CHECK_REQUEST": { + sendToClient(client, { + type: "INTEGRITY_CHECK_RESPONSE", + payload: INTEGRITY_CHECKSUM, + }); + break; + } + + case "MOCK_ACTIVATE": { + activeClientIds.add(clientId); + + sendToClient(client, { + type: "MOCKING_ENABLED", + payload: true, + }); + break; + } + + case "MOCK_DEACTIVATE": { + activeClientIds.delete(clientId); + break; + } + + case "CLIENT_CLOSED": { + activeClientIds.delete(clientId); + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId; + }); + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister(); + } + + break; + } + } +}); + +self.addEventListener("fetch", function (event) { + const { request } = event; + + // Bypass navigation requests. + if (request.mode === "navigate") { + return; + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === "only-if-cached" && request.mode !== "same-origin") { + return; + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return; + } + + // Generate unique request ID. + const requestId = crypto.randomUUID(); + event.respondWith(handleRequest(event, requestId)); +}); + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event); + const response = await getResponse(event, client, requestId); + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + (async function () { + const responseClone = response.clone(); + + sendToClient( + client, + { + type: "RESPONSE", + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body] + ); + })(); + } + + return response; +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId); + + if (client?.frameType === "top-level") { + return client; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === "visible"; + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id); + }); +} + +async function getResponse(event, client, requestId) { + const { request } = event; + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone(); + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()); + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers["x-msw-intention"]; + + return fetch(requestClone, { headers }); + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough(); + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough(); + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + const mswIntention = request.headers.get("x-msw-intention"); + if (["bypass", "passthrough"].includes(mswIntention)) { + return passthrough(); + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer(); + const clientMessage = await sendToClient( + client, + { + type: "REQUEST", + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer] + ); + + switch (clientMessage.type) { + case "MOCK_RESPONSE": { + return respondWithMock(clientMessage.data); + } + + case "MOCK_NOT_FOUND": { + return passthrough(); + } + } + + return passthrough(); +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error); + } + + resolve(event.data); + }; + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)) + ); + }); +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error(); + } + + const mockedResponse = new Response(response.body, response); + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }); + + return mockedResponse; +} diff --git a/packages/login/server.js b/packages/login/server.js index 8da98ba8a0..f71c747697 100644 --- a/packages/login/server.js +++ b/packages/login/server.js @@ -47,13 +47,13 @@ app.prepare().then(() => { await handle(req, res, parsedUrl); } catch (err) { console.error("Error occurred handling", req.url, err); - res.statusCode = 500; - res.end("internal server error"); + // res.statusCode = 500; + // res.end("internal server error"); } }) .once("error", (err) => { console.error(err); - process.exit(1); + // process.exit(1); }) .listen(port, () => { console.log(`Server is listening on port ${port}`); diff --git a/packages/login/src/app/layout.tsx b/packages/login/src/app/layout.tsx index a5cce15095..ff7035c4d8 100644 --- a/packages/login/src/app/layout.tsx +++ b/packages/login/src/app/layout.tsx @@ -40,6 +40,16 @@ import { } from "@/utils/actions"; import "../styles/globals.scss"; +import { MSWProvider } from "@docspace/shared/__mocks__/e2e"; + +if (process.env.NEXT_RUNTIME === "nodejs") { + console.log("SERVER LISTEN"); + + const { server } = require("../mocks/node"); + server.listen(); + + Reflect.set(fetch, "__FOO", "YES"); +} export default async function RootLayout({ children, @@ -124,20 +134,22 @@ export default async function RootLayout({
-