diff --git a/package.json b/package.json index d9a1d56..775221a 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,10 @@ "esbuild", "msw" ] + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..34057e8 --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,307 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.7.3' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +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: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + 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 (activeClientIds.has(event.clientId)) { + return client + } + + 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() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + 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() + } + + // 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 'PASSTHROUGH': { + 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/src/App.tsx b/src/App.tsx index 0e78e84..351f119 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,8 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { PeopleView } from "./views/people/people.view"; +const queryClient = new QueryClient(); + export function App() { - return ; + return ; } diff --git a/src/constants/environments.ts b/src/constants/environments.ts deleted file mode 100644 index fc4a0d0..0000000 --- a/src/constants/environments.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const PRODUCTION = "PRODUCTION"; -export const STAGING = "STAGING"; diff --git a/src/hooks/useNetworkingParams.ts b/src/hooks/useNetworkingParams.ts index 7999e74..b0d3d1e 100644 --- a/src/hooks/useNetworkingParams.ts +++ b/src/hooks/useNetworkingParams.ts @@ -1,15 +1,8 @@ import { useMemo } from "react"; -import { PRODUCTION, STAGING } from "../constants/environments"; - -export const getBaseUrl = (env: ImportMetaEnv) => { - if (env.ENVIRONMENT === STAGING) { - return `https://${env.API_URL.STAGING}`; - } - if (env.ENVIRONMENT === PRODUCTION) { - return `https://${env.API_URL.PROD}`; - } - return `https://${env.API_URL.DEV}`; +export const getBaseUrl = (_env: ImportMetaEnv) => { + // You can return different base URLs depending on your environmental vars + return `https://example.com`; }; export const useNetworkingParams = () => { @@ -20,5 +13,5 @@ export const useNetworkingParams = () => { return getBaseUrl(env); }, [env]); - return { baseUrl, env: env?.ENVIRONMENT } as const; + return { baseUrl } as const; }; diff --git a/src/main.tsx b/src/main.tsx index a1b5c62..b2a0945 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,8 +2,18 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { App } from "./App.tsx"; -createRoot(document.getElementById("root")!).render( - - - , -); + +async function enableMocking() { + // You can add a check here for `dev` mode to conditionally enable this + + const { worker } = await import('./utils/testing-utils/server') + return worker.start() +} + +enableMocking().then(() => { + createRoot(document.getElementById("root")!).render( + + + , + ); +}) diff --git a/src/services/people.ts b/src/services/people.ts index d8052c6..223e354 100644 --- a/src/services/people.ts +++ b/src/services/people.ts @@ -28,7 +28,7 @@ async function makeNetworkRequest({ method: Method; relativeUrl: string; }): Promise { - return await fetch(relativeUrl + baseUrl, { + return await fetch(baseUrl + relativeUrl, { method: method, body: JSON.stringify(body), headers: { @@ -76,7 +76,7 @@ export async function createPersonHobbies({ person_id, }); const response = await makeNetworkRequest({ - method: "GET", + method: "POST", baseUrl: baseUrl, relativeUrl: pathUrl, signal, diff --git a/src/types/environmental-vars.d.ts b/src/types/environmental-vars.d.ts deleted file mode 100644 index 7483b83..0000000 --- a/src/types/environmental-vars.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface ImportMeta { - env: { - ENVIRONMENT?: string; - API_URL: { - PROD: string; - DEV: string; - STAGING: string; - }; - }; -} diff --git a/src/utils/testing-utils/people-mocks.ts b/src/utils/testing-utils/people-mocks.ts index 2dd4a9a..c97c9c9 100644 --- a/src/utils/testing-utils/people-mocks.ts +++ b/src/utils/testing-utils/people-mocks.ts @@ -6,7 +6,7 @@ import { import { returnJSON } from "./json"; // Should be replaced with an env var of some kind -const baseUrl = "exmaple.com"; +const baseUrl = "example.com"; export const getAbsolutePeoplePath = (path: string) => { return `https://${baseUrl}${path}`; diff --git a/src/utils/testing-utils/server.ts b/src/utils/testing-utils/server.ts index e7049dd..27653fd 100644 --- a/src/utils/testing-utils/server.ts +++ b/src/utils/testing-utils/server.ts @@ -1,23 +1,28 @@ import { http, HttpResponse } from "msw"; -import { setupServer } from "msw/node"; +import { setupWorker } from 'msw/browser' import { peopleHandlers, getAbsolutePeoplePath } from "./people-mocks"; import { returnJSON } from "./json"; -export const server = setupServer(...peopleHandlers); +/** + * This only works like this if you're using Vitest browser mode. + * + * If you're using JSDom, you'll need to import from `msw/node` and mock the FE differently + */ +export const worker = setupWorker(...peopleHandlers) export function mockEndpointV5( path: string, body: object, type: "get" | "post" | "put" | "delete" = "get", ) { - server.use(http[type](getAbsolutePeoplePath(path), returnJSON(body))); + worker.use(http[type](getAbsolutePeoplePath(path), returnJSON(body))); } export function mockEndpointWithPromiseV5( path: string, bodyPromise: Promise, ) { - server.use( + worker.use( http.get(getAbsolutePeoplePath(path), async () => { const body = await bodyPromise; return HttpResponse.json(body); diff --git a/src/views/people/people.logic.ts b/src/views/people/people.logic.ts index 80d12d8..3fae3e0 100644 --- a/src/views/people/people.logic.ts +++ b/src/views/people/people.logic.ts @@ -4,7 +4,7 @@ import { createPersonHobbies as _createPersonHobbies, HobbyCreatePayload, } from "../../services/people"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; interface UsePeopleLogicProps { person_id: string; @@ -40,6 +40,11 @@ export function usePeopleLogic({ person_id }: UsePeopleLogicProps) { }); }; + useEffect(() => { + if (!personHobbiesError) return; + console.error(personHobbiesError); + }, [personHobbiesError]) + return { personHobbies, createPersonHobbies,