diff --git a/jest.config.js b/jest.config.js index b9141a13..9a7c9017 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,5 +5,9 @@ module.exports = { }, setupFilesAfterEnv: ["src/setupTests.ts"], snapshotSerializers: ["enzyme-to-json/serializer"], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$": + "identity-obj-proxy" + }, testEnvironment: "jsdom" }; diff --git a/package.json b/package.json index 9ae41361..0187b49c 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "graphql-tools": "^4.0.5", "html-webpack-plugin": "^3.2.0", "husky": "^3.0.4", + "identity-obj-proxy": "^3.0.0", "jest": "^24.9.0", "jest-styled-components": "^6.3.4", "lint-staged": "^9.2.4", @@ -120,6 +121,7 @@ "terser-webpack-plugin": "^1.4.1", "ts-jest": "^24.1.0", "typescript": "^3.5.2", + "url-loader": "^2.3.0", "urql": "^1.4.1", "webpack": "^4.35.0", "webpack-cli": "^3.3.5" diff --git a/src/definitions.d.ts b/src/definitions.d.ts index f331aa5b..37cc11e0 100644 --- a/src/definitions.d.ts +++ b/src/definitions.d.ts @@ -1,2 +1,8 @@ declare module "*.svg"; declare module "nanoid"; + +declare namespace NodeJS { + interface Global { + matchMedia: jest.Mock; + } +} diff --git a/src/panel/App.test.tsx b/src/panel/App.test.tsx new file mode 100644 index 00000000..3257fad4 --- /dev/null +++ b/src/panel/App.test.tsx @@ -0,0 +1,47 @@ +jest.mock("./context/Devtools.tsx", () => { + return { + ...jest.requireActual("./context/Devtools.tsx"), + useDevtoolsContext: jest.fn() + }; +}); +import React from "react"; +import { shallow } from "enzyme"; +import { mocked } from "ts-jest/utils"; +import { App, AppRoutes } from "./App"; +import { useDevtoolsContext } from "./context"; + +describe("App", () => { + describe("on mount", () => { + it("matches snapshot", () => { + expect(shallow()).toMatchSnapshot(); + }); + }); +}); + +describe("App routes", () => { + describe("on mount", () => { + describe("on connected", () => { + beforeEach(() => { + mocked(useDevtoolsContext).mockReturnValue({ + clientConnected: true + } as any); + }); + + it("matches snapshot", () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + + describe("on disconnected", () => { + beforeEach(() => { + mocked(useDevtoolsContext).mockReturnValue({ + clientConnected: false + } as any); + }); + + it("matches snapshot", () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/panel/App.tsx b/src/panel/App.tsx index b3bb1e83..dbc8dfa4 100644 --- a/src/panel/App.tsx +++ b/src/panel/App.tsx @@ -1,9 +1,10 @@ import "./App.css"; -import React from "react"; +import React, { FC } from "react"; import { HashRouter, Route, Redirect } from "react-router-dom"; import { ThemeProvider } from "styled-components"; import { Events } from "./events"; import { Explorer } from "./explorer"; +import { Disconnected } from "./disconnected"; import { Navigation } from "./Navigation"; import { Request } from "./request/Request"; import { theme } from "./theme"; @@ -12,27 +13,40 @@ import { DevtoolsProvider, EventsProvider, RequestProvider, - ExplorerContextProvider + ExplorerContextProvider, + useDevtoolsContext } from "./context"; export const App = () => { return ( - - - - - - - - - - - - - } /> - - - - + + + + + + ); +}; + +export const AppRoutes: FC = () => { + const { clientConnected } = useDevtoolsContext(); + + if (!clientConnected) { + return ; + } + + return ( + + + + + + + + + + + } /> + + ); }; diff --git a/src/panel/__snapshots__/App.test.tsx.snap b/src/panel/__snapshots__/App.test.tsx.snap new file mode 100644 index 00000000..d18f0899 --- /dev/null +++ b/src/panel/__snapshots__/App.test.tsx.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`App on mount matches snapshot 1`] = ` + + + + + +`; + +exports[`App routes on mount on connected matches snapshot 1`] = ` + + + + + + + + + + + + + +`; + +exports[`App routes on mount on disconnected matches snapshot 1`] = ``; diff --git a/src/panel/context/Devtools.tsx b/src/panel/context/Devtools.tsx index 61706864..2ede5f31 100644 --- a/src/panel/context/Devtools.tsx +++ b/src/panel/context/Devtools.tsx @@ -5,7 +5,8 @@ import React, { FC, useRef, useCallback, - useState + useState, + useContext } from "react"; import { DevtoolsPanelConnectionName, PanelOutgoingMessage } from "../../types"; @@ -19,6 +20,8 @@ interface DevtoolsContextType { export const DevtoolsContext = createContext(null as any); +export const useDevtoolsContext = () => useContext(DevtoolsContext); + export const DevtoolsProvider: FC = ({ children }) => { const [clientConnected, setClientConnected] = useState(false); const connection = useRef( diff --git a/src/panel/context/Events.tsx b/src/panel/context/Events.tsx index c4cbe22d..26dfae4f 100644 --- a/src/panel/context/Events.tsx +++ b/src/panel/context/Events.tsx @@ -76,13 +76,6 @@ export const EventsProvider: FC = ({ children }) => { /** Handle incoming events */ useEffect(() => { return addMessageHandler(msg => { - // When using an effect on client connection true --> false - // something in the lifecycle seems to get lost... That's why - // I opted for this solution. - if (msg.type === "disconnect") { - setRawEvents(() => []); - } - if (!["operation", "response", "error"].includes(msg.type)) { return; } diff --git a/src/panel/context/Request.tsx b/src/panel/context/Request.tsx index 7b136410..6003461e 100644 --- a/src/panel/context/Request.tsx +++ b/src/panel/context/Request.tsx @@ -26,41 +26,43 @@ export const RequestContext = createContext(null as any); export const RequestProvider: FC = ({ children }) => { const { sendMessage, addMessageHandler } = useContext(DevtoolsContext); - const [fetching, setFetching] = useState(false); + const [state, setState] = useState<{ + fetching: boolean; + response?: object; + error?: object; + }>({ fetching: false, response: undefined, error: undefined }); const [query, setQuery] = useState(); - const [response, setResponse] = useState(); - const [error, setError] = useState(); const [schema, setSchema] = useState(); const execute = useCallback(() => { - setFetching(true); - setResponse(undefined); - setError(undefined); + setState({ + fetching: true, + response: undefined, + error: undefined + }); sendMessage({ type: "request", query: query || "" }); }, [query, sendMessage]); // Listen for response for devtools useEffect(() => { return addMessageHandler(e => { - if ( - !fetching || - e.type === "operation" || - e.type === "init" || - e.type === "disconnect" || - (e.data.operation.context.meta as any).source !== "Devtools" - ) { - return; - } - - if (e.data.error !== undefined) { - setError(e.data.error); - } else { - setResponse(e.data.data); - } + setState(s => { + if ( + !s.fetching || + (e.type !== "response" && e.type !== "error") || + (e.data.operation.context.meta as any).source !== "Devtools" + ) { + return s; + } - setFetching(false); + return { + fetching: false, + error: e.data.error, + response: e.data.data + }; + }); }); - }, [fetching, addMessageHandler]); + }, [addMessageHandler]); // Get schema useEffect(() => { @@ -88,13 +90,11 @@ export const RequestProvider: FC = ({ children }) => { () => ({ query, setQuery, - fetching, - response, - error, + ...state, execute, schema }), - [query, fetching, response, error, execute, schema] + [query, state, execute, schema] ); return ; diff --git a/src/panel/disconnected/Disconnected.test.tsx b/src/panel/disconnected/Disconnected.test.tsx new file mode 100644 index 00000000..c26afa76 --- /dev/null +++ b/src/panel/disconnected/Disconnected.test.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { Disconnected } from "./Disconnected"; + +describe("on mount", () => { + it("matches snapshot", () => { + expect(shallow()).toMatchSnapshot(); + }); +}); diff --git a/src/panel/disconnected/Disconnected.tsx b/src/panel/disconnected/Disconnected.tsx new file mode 100644 index 00000000..b243ac73 --- /dev/null +++ b/src/panel/disconnected/Disconnected.tsx @@ -0,0 +1,44 @@ +import React, { FC } from "react"; +import styled, { createGlobalStyle } from "styled-components"; +import image from "../../assets/icon.svg"; + +export const Disconnected: FC = () => ( + <> + + + +
Waiting for exchange
+ Make sure {"you're"} using the Urql Devtools exchange! +
+ +); + +const GlobalStyle = createGlobalStyle` + body { + margin: 0; + } +`; + +const Container = styled.div` + width: 100%; + height: 100%; + background: ${p => p.theme.dark["0"]}; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const Header = styled.h1` + color: ${p => p.theme.grey["+2"]}; + font-weight: 400; + margin: 0; +`; + +const Hint = styled.p` + color: ${p => p.theme.grey["-1"]}; +`; + +const Logo = styled.img` + width: 150px; +`; diff --git a/src/panel/disconnected/__snapshots__/Disconnected.test.tsx.snap b/src/panel/disconnected/__snapshots__/Disconnected.test.tsx.snap new file mode 100644 index 00000000..a7f4eb67 --- /dev/null +++ b/src/panel/disconnected/__snapshots__/Disconnected.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`on mount matches snapshot 1`] = ` + + + + +
+ Waiting for exchange +
+ + Make sure + you're + using the Urql Devtools exchange! + +
+
+`; diff --git a/src/panel/disconnected/index.ts b/src/panel/disconnected/index.ts new file mode 100644 index 00000000..d441ab2f --- /dev/null +++ b/src/panel/disconnected/index.ts @@ -0,0 +1 @@ +export * from "./Disconnected"; diff --git a/src/panel/request/Query.tsx b/src/panel/request/Query.tsx index dc68880a..a70021e7 100644 --- a/src/panel/request/Query.tsx +++ b/src/panel/request/Query.tsx @@ -13,11 +13,6 @@ import React, { useContext, useEffect, useState } from "react"; import styled from "styled-components"; import { RequestContext } from "../context"; -type CodemirrorEventHandler = ( - ed: CodeMirror.Editor, - ev: T -) => void; - /** Query editor * Inspired by Graphiql's query editor - https://github.com/graphql/graphiql/blob/master/packages/graphiql/src/components/QueryEditor.js */ diff --git a/src/panel/request/Response.tsx b/src/panel/request/Response.tsx index 24c76c6c..f7fc5a79 100644 --- a/src/panel/request/Response.tsx +++ b/src/panel/request/Response.tsx @@ -5,7 +5,7 @@ import { Pane } from "../components/Pane"; import { JsonCode } from "../components"; export const Response = () => { - const { response, error } = useContext(RequestContext); + const { fetching, response, error } = useContext(RequestContext); const className = error !== undefined ? "error" : response !== undefined ? "success" : ""; @@ -14,7 +14,9 @@ export const Response = () => { Response - + ); diff --git a/src/setupTests.ts b/src/setupTests.ts index 8005cecf..681c967f 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -3,3 +3,5 @@ import { configure } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; configure({ adapter: new Adapter() }); + +global.matchMedia = jest.fn(); diff --git a/webpack.config.js b/webpack.config.js index 4d828984..ac576152 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -54,6 +54,10 @@ module.exports = { { test: /\.css$/, use: ["style-loader", "css-loader"] + }, + { + test: /\.svg$/, + use: ["url-loader"] } ] }, diff --git a/yarn.lock b/yarn.lock index 9334061c..a8b98c19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4150,6 +4150,11 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" +harmony-reflect@^1.4.6: + version "1.6.1" + resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9" + integrity sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA== + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -4368,6 +4373,13 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" +identity-obj-proxy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= + dependencies: + harmony-reflect "^1.4.6" + ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -5816,6 +5828,11 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.42.0" +mime@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -7494,7 +7511,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.0: +schema-utils@^2.0.0, schema-utils@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.5.0.tgz#8f254f618d402cc80257486213c8970edfd7c22f" integrity sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ== @@ -8418,6 +8435,15 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-loader@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.3.0.tgz#e0e2ef658f003efb8ca41b0f3ffbf76bab88658b" + integrity sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog== + dependencies: + loader-utils "^1.2.3" + mime "^2.4.4" + schema-utils "^2.5.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"