diff --git a/package.json b/package.json index c70c8df..060a3c6 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@mantine/prism": "^4.2.9", "@mantine/rte": "^4.2.9", "framer-motion": "^6.3.11", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-router-dom": "6.3.0", "styled-jsx": "^5.0.2", "tabler-icons-react": "^1.48.0", @@ -55,8 +55,8 @@ "@parcel/transformer-webmanifest": "2.3.2", "@types/jest": "^27.5.1", "@types/node": "^16.11.24", - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.11", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", "@types/react-router": "5.1.5", "@types/react-router-dom": "5.1.3", "@types/styled-jsx": "^3.4.4", diff --git a/src/demo/Sample.tsx b/src/demo/Sample.tsx new file mode 100644 index 0000000..df52946 --- /dev/null +++ b/src/demo/Sample.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; + +export const Sample = () => { + const [message, setMessage] = useState(""); + const [popup, setPopup] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const values = Object.fromEntries(formData); + + const response = await fetch("/create-user", { + method: "POST", + body: JSON.stringify(values), + }); + + const { message } = await response.json(); + setMessage(message); + }; + + const openMockManager = () => { + if (popup !== null && !popup.closed) { + popup?.focus(); + } else { + const newPopup = window.open("/mock-manager", "", "popup"); + setPopup(newPopup); + } + }; + + const closeMocker = () => { + if (popup !== null && !popup.closed) { + popup.postMessage({ source: "window-parent", content: "close" }, "*"); + setPopup(null); + console.log("Popup has been closed."); + } + }; + + return ( +
+
+ + + +
+
+

{message}

+
+
+ + +
+ ); +}; diff --git a/src/index.tsx b/src/index.tsx index b1ef1c0..f350d3b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,41 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import App from "./App"; - -ReactDOM.render( - - - , - document.getElementById("root") -); +import { createRoot } from "react-dom/client"; +import { Sample } from "./demo/Sample"; + +const container = document.getElementById("root"); +const root = createRoot(container!); + +if (process.env.NODE_ENV === "production") { + root.render(); +} else { + console.log("NODE_ENV:", process.env.NODE_ENV); + const { MockManager } = require("./views/MockManager"); + const { worker } = require("./mocks/browser"); + const { processChildIntent } = require("./utils/processChildIntent"); + const { processParentIntent } = require("./utils/processParentIntent"); + + window.addEventListener( + "message", + function (event) { + if (event.data.source === "window-child") + processChildIntent(worker, event.data); + }, + false + ); + + window.addEventListener( + "message", + function (event) { + if (event.data.source === "window-parent") + processParentIntent(event.data.content); + }, + false + ); + + worker.start(); + + if (window.location.pathname === "/mock-manager") { + root.render(); + } else { + root.render(); + } +} diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts new file mode 100644 index 0000000..74df1db --- /dev/null +++ b/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from "msw"; +import { handlers } from "./handlers"; + +export const worker = setupWorker(...handlers); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts new file mode 100644 index 0000000..b5bff06 --- /dev/null +++ b/src/mocks/handlers.ts @@ -0,0 +1,12 @@ +import { rest } from "msw"; + +export const mswRest = rest; + +export const handlers = [ + rest.post("/create-user", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ message: "This is the original response message" }) + ); + }), +]; diff --git a/src/utils/processChildIntent.ts b/src/utils/processChildIntent.ts new file mode 100644 index 0000000..bc87850 --- /dev/null +++ b/src/utils/processChildIntent.ts @@ -0,0 +1,12 @@ +import { rest, SetupWorkerApi } from "msw"; + +export const processChildIntent = (worker: SetupWorkerApi, data: any) => { + worker.use( + rest.post("/create-user", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ message: data.values.override_body }) + ); + }) + ); +}; diff --git a/src/utils/processParentIntent.ts b/src/utils/processParentIntent.ts new file mode 100644 index 0000000..4297c6f --- /dev/null +++ b/src/utils/processParentIntent.ts @@ -0,0 +1,6 @@ +export const processParentIntent = (data: any) => { + switch (data) { + case "close": + window.close(); + } +}; diff --git a/src/views/MockManager.tsx b/src/views/MockManager.tsx new file mode 100644 index 0000000..d2965ef --- /dev/null +++ b/src/views/MockManager.tsx @@ -0,0 +1,299 @@ +import { + Accordion, + Badge, + Box, + Button, + Code, + ColorScheme, + ColorSchemeProvider, + Divider, + Group, + JsonInput, + MantineProvider, + Paper, + Switch, + Text, + useMantineTheme, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { + NotificationsProvider, + useNotifications, +} from "@mantine/notifications"; +import { SetupWorkerApi } from "msw"; +import { FC, useState } from "react"; +import PageBody from "../components/PageBody"; +import PageHeader from "../components/PageHeader"; +import { DEFAULT_THEME } from "../constants"; +import { stripBasePath } from "../utils"; + +interface IProps { + worker: SetupWorkerApi; +} + +interface IFormValues { + override_body: string; + override_run_once: boolean; +} + +const MockManagerView: FC = ({ worker }) => { + const [enabled, setEnabled] = useState(true); + + const notifications = useNotifications(); + + const theme = useMantineTheme(); + + const form = useForm({ + initialValues: { + override_body: "", + override_run_once: false, + }, + }); + + const handleSubmit = (values: IFormValues) => { + if (!values.override_body) { + notifications.showNotification({ + title: "Missing input", + color: "red", + message: "Please provide a value.", + }); + + return; + } + + // if (!info?.path) { + // notifications.showNotification({ + // title: "Missing path", + // color: "red", + // message: "No path provided.", + // }); + + // return; + // } + + try { + // commit the override to `msw` + // onSubmit({ + // body: JSON.parse(body), + // once, + // path: info.path, + // method: info.method.toLowerCase(), + // }); + + // also save it to state for the UI to use it + // setRuntimeOverride(info.path, once, body); + + window.opener.postMessage({ source: "window-child", values }, "*"); + + notifications.showNotification({ + title: "Saved!", + color: "green", + message: "The override was correctly submitted.", + }); + } catch (err) { + notifications.showNotification({ + autoClose: false, + title: "Submission error", + color: "red", + message: `There was an error in submitting the form${ + (err as Error).message ? ": " + (err as Error).message : "" + }`, + }); + } + + form.reset(); + }; + + return ( +
+ + + Mock info:{" "} + ({ + fontSize: 20, + marginLeft: 10, + marginBottom: -3, + position: "relative", + top: -1, + })} + > + {stripBasePath("info?.path")} + + + } + /> + + + + ({ padding: t.spacing.lg })}> + + Method: + + + {"info?.method"} + + + + + + ({ padding: t.spacing.lg })}> + + Path: + + {"info?.path"} + + + + + + + + + + + Enter a value in the following field to override the mocked + response served by the service worker. The field accepts JSON, + and will validate & format your input. The override can run just + once, (then the previous response will be in effect), or + permanently. + + +
+ + + + + + + + +
+
+ + + {/* {itemOverrides?.length ? ( + <> + + {itemOverrides.map((o, i, arr) => { + const content = ( + + {o.body} + + ); + + if (o.once) { + return ( + + {content} + + ); + } + + return {content}; + })} + + + + + ({ flexBasis: "80%", paddingRight: 16 })} + > + Click to destroy all runtime overrides. +
+ Only the mocks provided to msw during + initialization will be active. +
+ + +
+
+ + ) : ( + "There are no overrides." + )} */} +
+
+
+
+ ); +}; + +export const MockManager: FC = ({ worker }) => { + const [colorScheme, setColorScheme] = useState(DEFAULT_THEME); + const toggleColorScheme = (value?: ColorScheme) => + setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")); + return ( + + + + + + + + ); +}; diff --git a/yarn.lock b/yarn.lock index 4b37120..82fe785 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2389,10 +2389,10 @@ dependencies: parchment "^1.1.2" -"@types/react-dom@^17.0.11": - version "17.0.11" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz" - integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== +"@types/react-dom@^18.0.9": + version "18.0.9" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.9.tgz#ffee5e4bfc2a2f8774b15496474f8e7fe8d0b504" + integrity sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg== dependencies: "@types/react" "*" @@ -2421,7 +2421,7 @@ "@types/history" "*" "@types/react" "*" -"@types/react@*", "@types/react@^17.0.39": +"@types/react@*": version "17.0.39" resolved "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz" integrity sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug== @@ -2430,6 +2430,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.0.25": + version "18.0.25" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44" + integrity sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/scheduler@*": version "0.16.2" resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" @@ -5425,14 +5434,13 @@ rc@^1.0.1, rc@^1.1.6: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.23.0" react-fast-compare@^3.0.1: version "3.2.0" @@ -5510,13 +5518,12 @@ react-transition-group@^4.4.2: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" readable-stream@^3.4.0: version "3.6.0" @@ -5691,13 +5698,12 @@ safe-buffer@^5.0.1, safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" semver@7.0.0: version "7.0.0"