From 3dc08a5d0f45994433d42411a1424a557a6d9408 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 08:57:08 -0400 Subject: [PATCH 1/9] Add initial rapid experiment details page and edit form - Fixes #2687 --- app/experimenter/static/rapid/.eslintrc.js | 4 + .../static/rapid/__tests__/.eslintrc | 3 +- .../static/rapid/__tests__/app.test.js | 27 --- .../static/rapid/__tests__/app.test.tsx | 71 +++++++ .../__tests__/experimentDetails.test.tsx | 58 ++++++ .../rapid/__tests__/experimentForm.test.js | 71 ------- .../rapid/__tests__/experimentForm.test.tsx | 180 ++++++++++++++++++ .../static/rapid/__tests__/utils.tsx | 49 +++++ .../static/rapid/components/App.tsx | 7 + .../experiments/ExperimentDetails.tsx | 82 ++++++++ .../rapid/components/forms/ExperimentForm.tsx | 34 +++- .../pages/ExperimentDetailsPage.tsx | 24 +++ .../components/pages/ExperimentFormPage.tsx | 27 +-- .../rapid/contexts/experiment/context.ts | 17 ++ .../static/rapid/contexts/experiment/hooks.ts | 19 ++ .../rapid/contexts/experiment/provider.tsx | 47 +++++ .../rapid/contexts/experiment/reducer.ts | 24 +++ .../{jest-env.setup.js => jest-env.setup.ts} | 0 app/experimenter/static/rapid/jest.config.js | 16 +- app/experimenter/static/rapid/jest.setup.js | 25 --- app/experimenter/static/rapid/jest.setup.ts | 2 + app/experimenter/static/rapid/package.json | 4 +- app/experimenter/static/rapid/tsconfig.json | 5 +- .../static/rapid/types/experiment.ts | 20 ++ .../static/rapid/webpack.config.js | 1 + app/yarn.lock | 105 ++++++++-- 26 files changed, 752 insertions(+), 170 deletions(-) delete mode 100644 app/experimenter/static/rapid/__tests__/app.test.js create mode 100644 app/experimenter/static/rapid/__tests__/app.test.tsx create mode 100644 app/experimenter/static/rapid/__tests__/experimentDetails.test.tsx delete mode 100644 app/experimenter/static/rapid/__tests__/experimentForm.test.js create mode 100644 app/experimenter/static/rapid/__tests__/experimentForm.test.tsx create mode 100644 app/experimenter/static/rapid/__tests__/utils.tsx create mode 100644 app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx create mode 100644 app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx create mode 100644 app/experimenter/static/rapid/contexts/experiment/context.ts create mode 100644 app/experimenter/static/rapid/contexts/experiment/hooks.ts create mode 100644 app/experimenter/static/rapid/contexts/experiment/provider.tsx create mode 100644 app/experimenter/static/rapid/contexts/experiment/reducer.ts rename app/experimenter/static/rapid/{jest-env.setup.js => jest-env.setup.ts} (100%) delete mode 100644 app/experimenter/static/rapid/jest.setup.js create mode 100644 app/experimenter/static/rapid/jest.setup.ts create mode 100644 app/experimenter/static/rapid/types/experiment.ts diff --git a/app/experimenter/static/rapid/.eslintrc.js b/app/experimenter/static/rapid/.eslintrc.js index deca6e3c92..efba2bc9b6 100644 --- a/app/experimenter/static/rapid/.eslintrc.js +++ b/app/experimenter/static/rapid/.eslintrc.js @@ -21,6 +21,10 @@ const baseSettings = { pattern: "experimenter-rapid/**", group: "parent", }, + { + pattern: "experimenter-types/**", + group: "parent", + }, ], pathGroupsExcludedImportTypes: ["builtin"], alphabetize: { diff --git a/app/experimenter/static/rapid/__tests__/.eslintrc b/app/experimenter/static/rapid/__tests__/.eslintrc index 31a7f88b92..5283a9bf79 100644 --- a/app/experimenter/static/rapid/__tests__/.eslintrc +++ b/app/experimenter/static/rapid/__tests__/.eslintrc @@ -4,6 +4,7 @@ }, "globals": { "fetchMock": "writable", - "renderWithRouter": "writable" + "renderWithRouter": "writable", + "wrapInExperimentProvider": "writeable" } } diff --git a/app/experimenter/static/rapid/__tests__/app.test.js b/app/experimenter/static/rapid/__tests__/app.test.js deleted file mode 100644 index d61ec85d1a..0000000000 --- a/app/experimenter/static/rapid/__tests__/app.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import React from "react"; - -import App from "experimenter-rapid/components/App"; - -afterEach(cleanup); - -describe("", () => { - it("root route shows link to create page", () => { - const { getByText } = renderWithRouter(); - expect(getByText("Create a new experiment")).toBeInTheDocument(); - }); - - it("unknown route shows 404 message", () => { - const { getByText } = renderWithRouter(, { - route: "/a/route/that/does/not/exist/", - }); - expect(getByText("404")).toBeInTheDocument(); - }); - - it("includes the experiment form page at `/new/`", () => { - const { getByText } = renderWithRouter(, { - route: "/new/", - }); - expect(getByText("Create a New A/A Experiment")).toBeInTheDocument(); - }); -}); diff --git a/app/experimenter/static/rapid/__tests__/app.test.tsx b/app/experimenter/static/rapid/__tests__/app.test.tsx new file mode 100644 index 0000000000..f0a1f95de2 --- /dev/null +++ b/app/experimenter/static/rapid/__tests__/app.test.tsx @@ -0,0 +1,71 @@ +import { cleanup, waitFor } from "@testing-library/react"; +import fetchMock from "jest-fetch-mock"; +import React from "react"; + +import App from "experimenter-rapid/components/App"; + +import { renderWithRouter } from "./utils"; + +afterEach(cleanup); + +describe("", () => { + it("root route shows link to create page", () => { + const { getByText } = renderWithRouter(); + expect(getByText("Create a new experiment")).toBeInTheDocument(); + }); + + it("unknown route shows 404 message", () => { + const { getByText } = renderWithRouter(, { + route: "/a/route/that/does/not/exist/", + }); + expect(getByText("404")).toBeInTheDocument(); + }); + + it("includes the experiment form page at `/new/`", () => { + const { getByText } = renderWithRouter(, { + route: "/new/", + }); + expect(getByText("Create a New A/A Experiment")).toBeInTheDocument(); + }); + + it("includes the experiment form page at `/:experimentSlug/edit/`", async () => { + fetchMock.mockOnce(async () => { + return JSON.stringify({ + slug: "test-slug", + name: "Test Name", + objectives: "Test objectives", + owner: "test@owner.com", + }); + }); + + const { getByText, getByLabelText } = renderWithRouter(, { + route: "/test-slug/edit/", + }); + expect(getByText("Create a New A/A Experiment")).toBeInTheDocument(); + + const nameField = getByLabelText("Public Name") as HTMLInputElement; + await waitFor(() => { + return expect(nameField.value).toEqual("Test Name"); + }); + }); + + it("includes the experiment details page at `/:experimentSlug/`", async () => { + fetchMock.mockOnce(async () => { + return JSON.stringify({ + slug: "test-slug", + name: "Test Name", + objectives: "Test objectives", + owner: "test@owner.com", + }); + }); + + const { getByText, getByDisplayValue } = renderWithRouter(, { + route: "/test-slug/", + }); + expect(getByText("Experiment Summary")).toBeInTheDocument(); + + await waitFor(() => { + return expect(getByDisplayValue("test@owner.com")).toBeInTheDocument(); + }); + }); +}); diff --git a/app/experimenter/static/rapid/__tests__/experimentDetails.test.tsx b/app/experimenter/static/rapid/__tests__/experimentDetails.test.tsx new file mode 100644 index 0000000000..d0a6665b11 --- /dev/null +++ b/app/experimenter/static/rapid/__tests__/experimentDetails.test.tsx @@ -0,0 +1,58 @@ +import { cleanup, fireEvent, waitFor } from "@testing-library/react"; +import fetchMock from "jest-fetch-mock"; +import React from "react"; + +import { + renderWithRouter, + wrapInExperimentProvider, +} from "experimenter-rapid/__tests__/utils"; +import ExperimentDetails from "experimenter-rapid/components/experiments/ExperimentDetails"; + +afterEach(async () => { + await cleanup(); + fetchMock.resetMocks(); +}); + +describe("", () => { + it("renders without issues", async () => { + const { getByDisplayValue } = renderWithRouter( + wrapInExperimentProvider(, { + initialState: { + slug: "test-slug", + name: "Test Name", + objectives: "Test objectives", + owner: "test@owner.com", + }, + }), + ); + + await waitFor(() => { + return expect(getByDisplayValue("test@owner.com")).toBeInTheDocument(); + }); + + expect(getByDisplayValue("Test Name")).toBeInTheDocument(); + expect(getByDisplayValue("Test objectives")).toBeInTheDocument(); + }); + + it("sends you to the edit page when the 'Back' button is clicked", async () => { + const { getByText, history } = renderWithRouter( + wrapInExperimentProvider(, { + initialState: { + slug: "test-slug", + name: "Test Name", + objectives: "Test objectives", + owner: "test@owner.com", + }, + }), + ); + + const historyPush = jest.spyOn(history, "push"); + + const backButton = getByText("Back"); + fireEvent.click(backButton); + + await waitFor(() => expect(historyPush).toHaveBeenCalledTimes(1)); + const lastEntry = history.entries.pop() || { pathname: "" }; + expect(lastEntry.pathname).toBe("/test-slug/edit/"); + }); +}); diff --git a/app/experimenter/static/rapid/__tests__/experimentForm.test.js b/app/experimenter/static/rapid/__tests__/experimentForm.test.js deleted file mode 100644 index 5729252465..0000000000 --- a/app/experimenter/static/rapid/__tests__/experimentForm.test.js +++ /dev/null @@ -1,71 +0,0 @@ -import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"; -import fetchMock from "jest-fetch-mock"; -import React from "react"; - -import ExperimentForm from "experimenter-rapid/components/forms/ExperimentForm"; - -afterEach(async () => { - await cleanup(); - fetchMock.resetMocks(); -}); - -describe("", () => { - it("renders without issues", () => { - const { getByText } = render(); - expect(getByText("Save")).toBeInTheDocument(); - }); - - it("makes the correct API call on save", async () => { - const { getByText, getByLabelText, history } = renderWithRouter( - , - ); - let formData; - fetchMock.mockOnce(async (req) => { - formData = JSON.parse(req.body); - return JSON.stringify({ slug: "test-slug" }); - }); - const historyPush = jest.spyOn(history, "push"); - - // Update the public name field - const nameField = getByLabelText("Public Name"); - fireEvent.change(nameField, { target: { value: "test name" } }); - - // Update the objectives field - const objectivesField = getByLabelText("Hypothesis"); - fireEvent.change(objectivesField, { target: { value: "test objective" } }); - - // Click the save button - fireEvent.click(getByText("Save")); - - // Ensure we redirect the user to the details page - await waitFor(() => expect(historyPush).toHaveBeenCalledTimes(1)); - expect(history.entries.pop().pathname).toBe("/test-slug/"); - - // Check the correct data was submitted - expect(formData).toEqual({ - name: "test name", - objectives: "test objective", - }); - }); - - it("makes the correct API call on save", async () => { - const { getByText } = renderWithRouter(); - let formData; - fetchMock.mockOnce(async (req) => { - formData = JSON.parse(req.body); - return { - status: 400, - body: JSON.stringify({ name: ["an error occurred"] }), - }; - }); - - // Click the save button - fireEvent.click(getByText("Save")); - - // Ensure the error message is shown - await waitFor(() => - expect(getByText("an error occurred")).toBeInTheDocument(), - ); - expect(formData).toEqual({ name: "", objectives: "" }); - }); -}); diff --git a/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx b/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx new file mode 100644 index 0000000000..bd5e487d6f --- /dev/null +++ b/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx @@ -0,0 +1,180 @@ +import { cleanup, fireEvent, waitFor } from "@testing-library/react"; +import fetchMock from "jest-fetch-mock"; +import React from "react"; + +import { + renderWithRouter, + wrapInExperimentProvider, +} from "experimenter-rapid/__tests__/utils"; +import ExperimentForm from "experimenter-rapid/components/forms/ExperimentForm"; + +afterEach(async () => { + await cleanup(); + fetchMock.resetMocks(); +}); + +describe("", () => { + it("renders without issues", () => { + const { getByText } = renderWithRouter( + wrapInExperimentProvider(), + ); + expect(getByText("Save")).toBeInTheDocument(); + }); + + it("is populated when data is available", async () => { + fetchMock.mockOnce(async () => { + return JSON.stringify({ + slug: "test-slug", + name: "Test Name", + objectives: "Test objectives", + owner: "test@owner.com", + }); + }); + + const { getByLabelText } = renderWithRouter( + wrapInExperimentProvider(), + { + route: "/test-slug/edit/", + matchRoutePath: "/:experimentSlug/edit/", + }, + ); + + const nameField = getByLabelText("Public Name") as HTMLInputElement; + await waitFor(() => { + return expect(nameField.value).toEqual("Test Name"); + }); + + const objectivesField = getByLabelText("Hypothesis") as HTMLInputElement; + expect(objectivesField.value).toEqual("Test objectives"); + }); + + it("makes the correct API call on save new", async () => { + const { getByText, getByLabelText, history } = renderWithRouter( + wrapInExperimentProvider(), + { + route: "/new/", + }, + ); + + let submitUrl; + let formData; + let requestMethod; + fetchMock.mockOnce(async (req) => { + if (req.body) { + formData = await req.json(); + } + + requestMethod = req.method; + submitUrl = req.url; + + return JSON.stringify({ slug: "test-slug" }); + }); + const historyPush = jest.spyOn(history, "push"); + + // Update the public name field + const nameField = getByLabelText("Public Name"); + fireEvent.change(nameField, { target: { value: "test name" } }); + + // Update the objectives field + const objectivesField = getByLabelText("Hypothesis"); + fireEvent.change(objectivesField, { target: { value: "test objective" } }); + + // Click the save button + fireEvent.click(getByText("Save")); + + // Ensure we redirect the user to the details page + await waitFor(() => expect(historyPush).toHaveBeenCalledTimes(1)); + const lastEntry = history.entries.pop() || { pathname: "" }; + expect(lastEntry.pathname).toBe("/test-slug/"); + + // Check the correct data was submitted + expect(submitUrl).toEqual("/api/v3/experiments/"); + expect(requestMethod).toEqual("POST"); + expect(formData).toEqual({ + name: "test name", + objectives: "test objective", + }); + }); + + it("makes the correct API call on save existing", async () => { + fetchMock.mockOnce(async () => { + return JSON.stringify({ + name: "Test Name", + objectives: "Test objectives", + }); + }); + + const { + getByText, + getByLabelText, + getByDisplayValue, + history, + } = renderWithRouter(wrapInExperimentProvider(), { + route: "/test-slug/edit/", + matchRoutePath: "/:experimentSlug/edit/", + }); + + await waitFor(() => + expect(getByDisplayValue("Test Name")).toBeInTheDocument(), + ); + + let submitUrl; + let formData; + let requestMethod; + fetchMock.mockOnce(async (req) => { + if (req.body) { + formData = await req.json(); + } + + requestMethod = req.method; + submitUrl = req.url; + + return JSON.stringify({ slug: "test-slug" }); + }); + const historyPush = jest.spyOn(history, "push"); + + // Update the public name field + const nameField = getByLabelText("Public Name"); + fireEvent.change(nameField, { target: { value: "foo" } }); + + // Click the save button + fireEvent.click(getByText("Save")); + + // Ensure we redirect the user to the details page + await waitFor(() => expect(historyPush).toHaveBeenCalledTimes(1)); + const lastEntry = history.entries.pop() || { pathname: "" }; + expect(lastEntry.pathname).toBe("/test-slug/"); + + // Check the correct data was submitted + expect(submitUrl).toEqual("/api/v3/experiments/test-slug/"); + expect(requestMethod).toEqual("PUT"); + expect(formData).toEqual({ + name: "foo", + objectives: "Test objectives", + }); + }); + + ["name", "objectives", "feature", "audience", "trigger", "version"].forEach( + (fieldName) => { + it(`shows the appropriate error message for '${fieldName}' on save`, async () => { + const { getByText } = renderWithRouter( + wrapInExperimentProvider(), + ); + fetchMock.mockOnce(async (req) => { + return { + status: 400, + body: JSON.stringify({ [fieldName]: ["an error occurred"] }), + }; + }); + + // Click the save button + fireEvent.click(getByText("Save")); + + // Ensure the error message is shown + await waitFor(() => + expect(getByText("an error occurred")).toBeInTheDocument(), + ); + }); + }, + ); +}); diff --git a/app/experimenter/static/rapid/__tests__/utils.tsx b/app/experimenter/static/rapid/__tests__/utils.tsx new file mode 100644 index 0000000000..3b40bb855a --- /dev/null +++ b/app/experimenter/static/rapid/__tests__/utils.tsx @@ -0,0 +1,49 @@ +import { render, RenderResult } from "@testing-library/react"; +import { createMemoryHistory, MemoryHistory } from "history"; +import React from "react"; +import { Route, Router, Switch } from "react-router-dom"; + +import { INITIAL_CONTEXT } from "experimenter-rapid/contexts/experiment/context"; +import ExperimentProvider from "experimenter-rapid/contexts/experiment/provider"; +import { ExperimentData } from "experimenter-types/experiment"; + +type RenderWithRouterResult = RenderResult & { history: MemoryHistory }; + +export function renderWithRouter( + ui: React.ReactElement, + { + route = "/", + matchRoutePath = route, + history = createMemoryHistory({ initialEntries: [route] }), + } = {}, +): RenderWithRouterResult { + const Wrapper = ({ children }) => ( + + + + {children} + + + + ); + return { + ...render(ui, { wrapper: Wrapper }), + // adding `history` to the returned utilities to allow us + // to reference it in our tests (just try to avoid using + // this to test implementation details). + history, + }; +} + +interface WrapInExperimentProviderOptions { + initialState?: ExperimentData; +} + +export function wrapInExperimentProvider( + ui: React.ReactElement, + { initialState } = {} as WrapInExperimentProviderOptions, +): React.ReactElement { + return ( + {ui} + ); +} diff --git a/app/experimenter/static/rapid/components/App.tsx b/app/experimenter/static/rapid/components/App.tsx index 1e74dc04bc..82de64155e 100644 --- a/app/experimenter/static/rapid/components/App.tsx +++ b/app/experimenter/static/rapid/components/App.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Switch, Link, Route } from "react-router-dom"; +import ExperimentDetailsPage from "experimenter-rapid/components/pages/ExperimentDetailsPage"; import ExperimentFormPage from "experimenter-rapid/components/pages/ExperimentFormPage"; const App: React.FC = () => { @@ -20,6 +21,12 @@ const App: React.FC = () => { + + + + + +
404
diff --git a/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx new file mode 100644 index 0000000000..040aebf0db --- /dev/null +++ b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { useHistory } from "react-router"; + +import { useExperimentState } from "experimenter-rapid/contexts/experiment/hooks"; + +const LabelledRow: React.FC<{ label: string; value?: string | undefined }> = ({ + children, + label, + value, +}) => { + return ( +
+ {label} + + + {children} + +
+ ); +}; + +const ExperimentDetails: React.FC = () => { + const data = useExperimentState(); + const history = useHistory(); + + const handleClickBack = () => { + history.push(`/${data.slug}/edit/`); + }; + + const handleClickRequestApproval = () => { + // No-op + }; + + return ( + <> + + +
+ Bugzilla ticket can be found here. +
+
+ + + + + + +

Results

+

+ The results will be available 7 days after the experiment is launched. + An email will be sent to you once we start recording data. +

+

+ The results can be found here: (link here) +

+ +
+ + + + + + + +
+ + ); +}; + +export default ExperimentDetails; diff --git a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx index 0479221649..d458f9ac8f 100644 --- a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx +++ b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx @@ -1,5 +1,13 @@ +import { Readable } from "stream"; + import React from "react"; -import { useHistory } from "react-router-dom"; +import { useHistory, useParams } from "react-router-dom"; + +import { + useExperimentDispatch, + useExperimentState, +} from "experimenter-rapid/contexts/experiment/hooks"; +import { ExperimentReducerActionType } from "experimenter-types/experiment"; import { XSelect } from "./XSelect"; @@ -22,10 +30,10 @@ const ErrorList: React.FC = ({ errors }) => { }; const ExperimentForm: React.FC = () => { - const [formData, setFormData] = React.useState({ - name: "", - objectives: "", - }); + const formData = useExperimentState(); + const dispatch = useExperimentDispatch(); + const { experimentSlug } = useParams(); + const [errors, setErrors] = React.useState<{ [name: string]: Array }>( {}, ); @@ -33,15 +41,21 @@ const ExperimentForm: React.FC = () => { const handleChange = (ev) => { const field = ev.target; - setFormData({ - ...formData, - [field.getAttribute("name")]: field.value, + dispatch({ + type: ExperimentReducerActionType.UPDATE_STATE, + state: { + ...formData, + [field.getAttribute("name")]: field.value, + }, }); }; const handleClickSave = async () => { - const response = await fetch("/api/v3/experiments/", { - method: "POST", + const url = experimentSlug + ? `/api/v3/experiments/${experimentSlug}/` + : "/api/v3/experiments/"; + const response = await fetch(url, { + method: experimentSlug ? "PUT" : "POST", headers: { "Content-Type": "application/json", }, diff --git a/app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx b/app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx new file mode 100644 index 0000000000..85ce73e85f --- /dev/null +++ b/app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import ExperimentDetails from "experimenter-rapid/components/experiments/ExperimentDetails"; +import { useExperimentState } from "experimenter-rapid/contexts/experiment/hooks"; +import ExperimentProvider from "experimenter-rapid/contexts/experiment/provider"; + +const ExperimentDetailsPage: React.FC = () => { + return ( + +
+
+
+

Experiment Summary

+ Draft +
+ + +
+
+
+ ); +}; + +export default ExperimentDetailsPage; diff --git a/app/experimenter/static/rapid/components/pages/ExperimentFormPage.tsx b/app/experimenter/static/rapid/components/pages/ExperimentFormPage.tsx index 340036ca2a..fb5b1a96f0 100644 --- a/app/experimenter/static/rapid/components/pages/ExperimentFormPage.tsx +++ b/app/experimenter/static/rapid/components/pages/ExperimentFormPage.tsx @@ -1,23 +1,26 @@ import React from "react"; import ExperimentForm from "experimenter-rapid/components/forms/ExperimentForm"; +import ExperimentProvider from "experimenter-rapid/contexts/experiment/provider"; const ExperimentFormPage: React.FC = () => { return ( -
-
-
-

Create a New A/A Experiment

- Draft + +
+
+
+

Create a New A/A Experiment

+ Draft +
+

+ Create and automatically launch an A/A CFR experiment. A/A + experiments measure the accuracy of the tool. +

-

- Create and automatically launch an A/A CFR experiment. A/A experiments - measure the accuracy of the tool. -

-
- -
+ +
+ ); }; diff --git a/app/experimenter/static/rapid/contexts/experiment/context.ts b/app/experimenter/static/rapid/contexts/experiment/context.ts new file mode 100644 index 0000000000..08499718ce --- /dev/null +++ b/app/experimenter/static/rapid/contexts/experiment/context.ts @@ -0,0 +1,17 @@ +import React from "react"; + +import { ExperimentContext } from "experimenter-types/experiment"; + +export const INITIAL_CONTEXT: ExperimentContext = { + state: { + name: "", + objectives: "", + }, + dispatch(action) { + /* istanbul ignore next */ + console.log(action); + }, +}; + +const context = React.createContext(INITIAL_CONTEXT); +export default context; diff --git a/app/experimenter/static/rapid/contexts/experiment/hooks.ts b/app/experimenter/static/rapid/contexts/experiment/hooks.ts new file mode 100644 index 0000000000..ed6cde23eb --- /dev/null +++ b/app/experimenter/static/rapid/contexts/experiment/hooks.ts @@ -0,0 +1,19 @@ +import React from "react"; + +import context from "experimenter-rapid/contexts/experiment/context"; +import { + ExperimentData, + ExperimentReducerAction, +} from "experimenter-types/experiment"; + +export function useExperimentState(): ExperimentData { + const { state } = React.useContext(context); + return state; +} + +export function useExperimentDispatch(): React.Dispatch< + ExperimentReducerAction +> { + const { dispatch } = React.useContext(context); + return dispatch; +} diff --git a/app/experimenter/static/rapid/contexts/experiment/provider.tsx b/app/experimenter/static/rapid/contexts/experiment/provider.tsx new file mode 100644 index 0000000000..3a39a87dc1 --- /dev/null +++ b/app/experimenter/static/rapid/contexts/experiment/provider.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { useLocation, useParams } from "react-router"; + +import context, { + INITIAL_CONTEXT, +} from "experimenter-rapid/contexts/experiment/context"; +import reducer from "experimenter-rapid/contexts/experiment/reducer"; +import { + ExperimentData, + ExperimentReducerActionType, +} from "experimenter-types/experiment"; + +const ExperimentProvider: React.FC<{ initialState?: ExperimentData }> = ({ + children, + initialState = INITIAL_CONTEXT.state, +}) => { + const [state, dispatch] = React.useReducer(reducer, initialState); + const { experimentSlug } = useParams(); + const { Provider } = context; + + React.useEffect(() => { + if (!experimentSlug) { + return; + } + + const fetchData = async () => { + const response = await fetch(`/api/v3/experiments/${experimentSlug}/`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + dispatch({ + type: ExperimentReducerActionType.UPDATE_STATE, + state: data, + }); + }; + + fetchData(); + }, [experimentSlug]); + + return {children}; +}; + +export default ExperimentProvider; diff --git a/app/experimenter/static/rapid/contexts/experiment/reducer.ts b/app/experimenter/static/rapid/contexts/experiment/reducer.ts new file mode 100644 index 0000000000..8fdc568f5f --- /dev/null +++ b/app/experimenter/static/rapid/contexts/experiment/reducer.ts @@ -0,0 +1,24 @@ +import produce from "immer"; + +import { + ExperimentData, + ExperimentReducerAction, + ExperimentReducerActionType, +} from "experimenter-types/experiment"; + +const reducer = produce( + (draft: ExperimentData, action: ExperimentReducerAction) => { + switch (action.type) { + case ExperimentReducerActionType.UPDATE_STATE: { + return action.state; + } + + /* istanbul ignore next */ + default: { + return draft; + } + } + }, +); + +export default reducer; diff --git a/app/experimenter/static/rapid/jest-env.setup.js b/app/experimenter/static/rapid/jest-env.setup.ts similarity index 100% rename from app/experimenter/static/rapid/jest-env.setup.js rename to app/experimenter/static/rapid/jest-env.setup.ts diff --git a/app/experimenter/static/rapid/jest.config.js b/app/experimenter/static/rapid/jest.config.js index 998994fe00..151523b999 100644 --- a/app/experimenter/static/rapid/jest.config.js +++ b/app/experimenter/static/rapid/jest.config.js @@ -1,18 +1,26 @@ /* eslint-env node */ module.exports = { globals: {}, + testMatch: ["**/__tests__/?(*.)test.ts?(x)"], transform: { - "^.+\\.[jt]sx?$": "babel-jest", + "^.+\\.jsx?$": "babel-jest", + "^.+\\.tsx?$": "ts-jest", }, moduleNameMapper: { "^experimenter-rapid/(.*)$": "/$1", + "^experimenter-types/(.*)$": "/types/$1", "\\.(less|css)$": "identity-obj-proxy", }, - setupFiles: ["/jest.setup.js"], - setupFilesAfterEnv: ["/jest-env.setup.js"], + setupFiles: ["/jest.setup.ts"], + setupFilesAfterEnv: ["/jest-env.setup.ts"], verbose: true, collectCoverage: true, - collectCoverageFrom: ["/**/*.{ts,tsx}", "!/index.tsx"], + collectCoverageFrom: [ + "/**/*.{ts,tsx}", + "!/__tests__/**/*.{ts,tsx}", + "!/types/**/*.{ts,tsx}", + "!/index.tsx", + ], coverageReporters: ["text"], coverageThreshold: { global: { diff --git a/app/experimenter/static/rapid/jest.setup.js b/app/experimenter/static/rapid/jest.setup.js deleted file mode 100644 index c47062cc38..0000000000 --- a/app/experimenter/static/rapid/jest.setup.js +++ /dev/null @@ -1,25 +0,0 @@ -import { render } from "@testing-library/react"; -import { createMemoryHistory } from "history"; -import React from "react"; -import { Router } from "react-router-dom"; - -require("jest-fetch-mock").enableMocks(); - -global.renderWithRouter = ( - ui, - { - route = "/", - history = createMemoryHistory({ initialEntries: [route] }), - } = {}, -) => { - const Wrapper = ({ children }) => ( - {children} - ); - return { - ...render(ui, { wrapper: Wrapper }), - // adding `history` to the returned utilities to allow us - // to reference it in our tests (just try to avoid using - // this to test implementation details). - history, - }; -}; diff --git a/app/experimenter/static/rapid/jest.setup.ts b/app/experimenter/static/rapid/jest.setup.ts new file mode 100644 index 0000000000..a09fbe7e90 --- /dev/null +++ b/app/experimenter/static/rapid/jest.setup.ts @@ -0,0 +1,2 @@ +import FetchMock from "jest-fetch-mock"; +FetchMock.enableMocks(); diff --git a/app/experimenter/static/rapid/package.json b/app/experimenter/static/rapid/package.json index 61657c7180..fcfe1955f2 100644 --- a/app/experimenter/static/rapid/package.json +++ b/app/experimenter/static/rapid/package.json @@ -4,6 +4,7 @@ "version": "1.0.0", "description": "New rapid-experimentation UI for Experimenter", "dependencies": { + "immer": "7.0.4", "react": "16.13.1", "react-dom": "16.13.1", "react-router-dom": "5.2.0", @@ -20,13 +21,13 @@ "@babel/preset-typescript": "7.10.1", "@testing-library/jest-dom": "5.10.1", "@testing-library/react": "10.3.0", + "@types/jest": "26.0.0", "@types/react": "16.9.35", "@types/react-router": "5.1.7", "@types/react-router-dom": "5.1.5", "@types/react-select": "3.0.13", "@typescript-eslint/eslint-plugin": "3.3.0", "@typescript-eslint/parser": "3.3.0", - "babel-jest": "26.0.1", "babel-loader": "8.1.0", "eslint": "7.3.1", "eslint-config-prettier": "6.11.0", @@ -38,6 +39,7 @@ "jest-fetch-mock": "3.0.3", "prettier": "2.0.5", "rimraf": "3.0.2", + "ts-jest": "26.1.1", "typescript": "3.9.5", "webpack": "4.43.0", "webpack-cli": "3.3.12" diff --git a/app/experimenter/static/rapid/tsconfig.json b/app/experimenter/static/rapid/tsconfig.json index ce087cacbe..ec0b63bed3 100644 --- a/app/experimenter/static/rapid/tsconfig.json +++ b/app/experimenter/static/rapid/tsconfig.json @@ -7,14 +7,15 @@ "esModuleInterop": true, "baseUrl": ".", "paths": { - "experimenter-rapid/*": ["./*"] + "experimenter-rapid/*": ["./*"], + "experimenter-types/*": ["./types/*"] }, "jsx": "react", "downlevelIteration": true, "noEmit": true, "strictNullChecks": true, "isolatedModules": true, - "typeRoots": ["./node_modules/@types", "./types"] + "typeRoots": ["../../../node_modules/@types", "./node_modules/@types", "./types"] }, "include": ["./**/*.ts", "./**/*.tsx"], "exclude": ["../../../node_modules", "./node_modules"] diff --git a/app/experimenter/static/rapid/types/experiment.ts b/app/experimenter/static/rapid/types/experiment.ts new file mode 100644 index 0000000000..fccf5b0dff --- /dev/null +++ b/app/experimenter/static/rapid/types/experiment.ts @@ -0,0 +1,20 @@ +export type ExperimentData = { + name: string; + objectives: string; + owner?: string; + slug?: string; +}; + +export enum ExperimentReducerActionType { + UPDATE_STATE = "UPDATE_STATE", +} + +export type ExperimentReducerAction = { + type: ExperimentReducerActionType.UPDATE_STATE; + state: ExperimentData; +}; + +export interface ExperimentContext { + state: ExperimentData; + dispatch: React.Dispatch; +} diff --git a/app/experimenter/static/rapid/webpack.config.js b/app/experimenter/static/rapid/webpack.config.js index 4972b661f9..e339f611a6 100644 --- a/app/experimenter/static/rapid/webpack.config.js +++ b/app/experimenter/static/rapid/webpack.config.js @@ -33,6 +33,7 @@ module.exports = (env, argv = {}) => { resolve: { alias: { "experimenter-rapid": path.resolve(__dirname), + "experimenter-types": path.resolve(__dirname, "types"), }, extensions: [".tsx", ".ts", ".js"], }, diff --git a/app/yarn.lock b/app/yarn.lock index 7af3be54af..06c48304c9 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2371,6 +2371,14 @@ jest-diff "^25.1.0" pretty-format "^25.1.0" +"@types/jest@26.0.0": + version "26.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.0.tgz#a6d7573dffa9c68cbbdf38f2e0de26f159e11134" + integrity sha512-/yeMsH9HQ1RLORlXAwoLXe8S98xxvhNtUz3yrgrwbaxYjT+6SFPZZRksmRKRA6L5vsUtSHeN71viDOTTyYAD+g== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.4": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -3381,6 +3389,13 @@ browserslist@^4.12.0: node-releases "^1.1.53" pkg-up "^2.0.0" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -3393,7 +3408,7 @@ buffer-equal@0.0.1: resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= -buffer-from@^1.0.0: +buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== @@ -5036,7 +5051,7 @@ fast-glob@^2.2.2: merge2 "^1.2.3" micromatch "^3.1.10" -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -5672,6 +5687,11 @@ ignore@^5.1.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== +immer@7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.4.tgz#f5d41ee61830f1620ac7f168b3b57078f0200d1a" + integrity sha512-HSBBQVdsJp6Bce6ggxwZiqWwPWMHvPCn3ZHzVgKZOAx7Ikr7U8s4x+Wgr5y5xyn3ZggWJYeWGnOEP3Z4g+r7RQ== + immutable@4.0.0-rc.12: version "4.0.0-rc.12" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.12.tgz#ca59a7e4c19ae8d9bf74a97bdf0f6e2f2a5d0217" @@ -6193,6 +6213,16 @@ jest-diff@^25.1.0, jest-diff@^25.2.6: jest-get-type "^25.2.6" pretty-format "^25.2.6" +jest-diff@^25.2.1: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-diff@^26.0.1: version "26.0.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.0.1.tgz#c44ab3cdd5977d466de69c46929e0e57f89aa1de" @@ -6651,6 +6681,13 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@2.x: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -6802,7 +6839,7 @@ lodash.clone@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= -lodash.memoize@^4.1.2: +lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= @@ -6865,6 +6902,11 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -6941,6 +6983,14 @@ merge2@^1.2.3: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== +micromatch@4.x, micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -6960,14 +7010,6 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -7062,6 +7104,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp@1.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" @@ -8201,7 +8248,7 @@ pretty-format@^25.1.0, pretty-format@^25.2.6: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^25.5.0: +pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== @@ -8965,16 +9012,16 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@7.x, semver@^7.2.1, semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + semver@^6.0.0, semver@^6.1.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== - send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -9752,6 +9799,22 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +ts-jest@26.1.1: + version "26.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.1.tgz#b98569b8a4d4025d966b3d40c81986dd1c510f8d" + integrity sha512-Lk/357quLg5jJFyBQLnSbhycnB3FPe+e9i7ahxokyXxAYoB0q1pPmqxxRPYr4smJic1Rjcf7MXDBhZWgxlli0A== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + micromatch "4.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "18.x" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -10365,6 +10428,14 @@ yaml@^1.7.2: dependencies: "@babel/runtime" "^7.8.7" +yargs-parser@18.x: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" From 6fdee51bae501598b991796471d013b3feae5775 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 09:03:36 -0400 Subject: [PATCH 2/9] Change XSelect tests to typescript --- .../static/rapid/__tests__/{xSelect.test.js => xSelect.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/experimenter/static/rapid/__tests__/{xSelect.test.js => xSelect.test.tsx} (100%) diff --git a/app/experimenter/static/rapid/__tests__/xSelect.test.js b/app/experimenter/static/rapid/__tests__/xSelect.test.tsx similarity index 100% rename from app/experimenter/static/rapid/__tests__/xSelect.test.js rename to app/experimenter/static/rapid/__tests__/xSelect.test.tsx From 4aa8b0a86d1f26a3595be027c4bde95602211710 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 09:05:46 -0400 Subject: [PATCH 3/9] Remove unneccessary globals from .eslintrc --- app/experimenter/static/rapid/__tests__/.eslintrc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/experimenter/static/rapid/__tests__/.eslintrc b/app/experimenter/static/rapid/__tests__/.eslintrc index 5283a9bf79..55f121d152 100644 --- a/app/experimenter/static/rapid/__tests__/.eslintrc +++ b/app/experimenter/static/rapid/__tests__/.eslintrc @@ -1,10 +1,5 @@ { "env": { "jest": true - }, - "globals": { - "fetchMock": "writable", - "renderWithRouter": "writable", - "wrapInExperimentProvider": "writeable" } } From 17c0d54b97018ad5f63e042e7151ab21a49da54e Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 09:08:40 -0400 Subject: [PATCH 4/9] Run yarn install with `--frozen-lockfile` option --- app/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Dockerfile b/app/Dockerfile index a184c7f4fb..47049b947f 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -31,7 +31,7 @@ COPY ./package.json /app/package.json COPY ./yarn.lock /app/yarn.lock COPY ./experimenter/static/core/package.json /app/experimenter/static/core/package.json COPY ./experimenter/static/rapid/package.json /app/experimenter/static/rapid/package.json -RUN yarn install +RUN yarn install --frozen-lockfile # Build assets COPY ./experimenter/static/ /app/experimenter/static/ From 8fd83f04bcf15aa6cd7203e77da5d801417d0027 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 09:14:29 -0400 Subject: [PATCH 5/9] Clean up eslint issues --- app/experimenter/static/rapid/__tests__/experimentForm.test.tsx | 2 +- app/experimenter/static/rapid/__tests__/utils.tsx | 1 - .../static/rapid/components/forms/ExperimentForm.tsx | 2 -- .../static/rapid/components/pages/ExperimentDetailsPage.tsx | 1 - app/experimenter/static/rapid/contexts/experiment/provider.tsx | 2 +- 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx b/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx index bd5e487d6f..69070864df 100644 --- a/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx +++ b/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx @@ -160,7 +160,7 @@ describe("", () => { const { getByText } = renderWithRouter( wrapInExperimentProvider(), ); - fetchMock.mockOnce(async (req) => { + fetchMock.mockOnce(async () => { return { status: 400, body: JSON.stringify({ [fieldName]: ["an error occurred"] }), diff --git a/app/experimenter/static/rapid/__tests__/utils.tsx b/app/experimenter/static/rapid/__tests__/utils.tsx index 3b40bb855a..5808fe9458 100644 --- a/app/experimenter/static/rapid/__tests__/utils.tsx +++ b/app/experimenter/static/rapid/__tests__/utils.tsx @@ -3,7 +3,6 @@ import { createMemoryHistory, MemoryHistory } from "history"; import React from "react"; import { Route, Router, Switch } from "react-router-dom"; -import { INITIAL_CONTEXT } from "experimenter-rapid/contexts/experiment/context"; import ExperimentProvider from "experimenter-rapid/contexts/experiment/provider"; import { ExperimentData } from "experimenter-types/experiment"; diff --git a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx index d458f9ac8f..956b9e4576 100644 --- a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx +++ b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx @@ -1,5 +1,3 @@ -import { Readable } from "stream"; - import React from "react"; import { useHistory, useParams } from "react-router-dom"; diff --git a/app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx b/app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx index 85ce73e85f..cb47be61bc 100644 --- a/app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx +++ b/app/experimenter/static/rapid/components/pages/ExperimentDetailsPage.tsx @@ -1,7 +1,6 @@ import React from "react"; import ExperimentDetails from "experimenter-rapid/components/experiments/ExperimentDetails"; -import { useExperimentState } from "experimenter-rapid/contexts/experiment/hooks"; import ExperimentProvider from "experimenter-rapid/contexts/experiment/provider"; const ExperimentDetailsPage: React.FC = () => { diff --git a/app/experimenter/static/rapid/contexts/experiment/provider.tsx b/app/experimenter/static/rapid/contexts/experiment/provider.tsx index 3a39a87dc1..75146bede1 100644 --- a/app/experimenter/static/rapid/contexts/experiment/provider.tsx +++ b/app/experimenter/static/rapid/contexts/experiment/provider.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useLocation, useParams } from "react-router"; +import { useParams } from "react-router"; import context, { INITIAL_CONTEXT, From 22e614b9b604826087818ac05d99a6e7269c0f53 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 14:48:51 -0400 Subject: [PATCH 6/9] Feedbacked --- .../experiments/ExperimentDetails.tsx | 17 +++++------------ .../rapid/components/forms/ExperimentForm.tsx | 7 ++++--- .../static/rapid/contexts/experiment/reducer.ts | 4 ++-- .../static/rapid/types/experiment.ts | 8 ++++---- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx index 040aebf0db..d060f6f5b6 100644 --- a/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx +++ b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx @@ -1,9 +1,10 @@ import React from "react"; import { useHistory } from "react-router"; +import { Link } from "react-router-dom"; import { useExperimentState } from "experimenter-rapid/contexts/experiment/hooks"; -const LabelledRow: React.FC<{ label: string; value?: string | undefined }> = ({ +const LabelledRow: React.FC<{ label: string; value?: string }> = ({ children, label, value, @@ -23,10 +24,6 @@ const ExperimentDetails: React.FC = () => { const data = useExperimentState(); const history = useHistory(); - const handleClickBack = () => { - history.push(`/${data.slug}/edit/`); - }; - const handleClickRequestApproval = () => { // No-op }; @@ -35,7 +32,7 @@ const ExperimentDetails: React.FC = () => { <> -
+
Bugzilla ticket can be found here.
@@ -56,13 +53,9 @@ const ExperimentDetails: React.FC = () => {
- + diff --git a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx index 956b9e4576..b667ef6006 100644 --- a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx +++ b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx @@ -61,6 +61,7 @@ const ExperimentForm: React.FC = () => { }); const responseData = await response.json(); if (!response.ok) { + console.log("whoops!"); setErrors(responseData); } else { setErrors({}); @@ -110,7 +111,7 @@ const ExperimentForm: React.FC = () => {

Select the user action or feature that you'd be measuring with @@ -119,10 +120,10 @@ const ExperimentForm: React.FC = () => { - +

diff --git a/app/experimenter/static/rapid/contexts/experiment/reducer.ts b/app/experimenter/static/rapid/contexts/experiment/reducer.ts index 8fdc568f5f..b6adda4e01 100644 --- a/app/experimenter/static/rapid/contexts/experiment/reducer.ts +++ b/app/experimenter/static/rapid/contexts/experiment/reducer.ts @@ -1,4 +1,4 @@ -import produce from "immer"; +import produce, { Draft } from "immer"; import { ExperimentData, @@ -7,7 +7,7 @@ import { } from "experimenter-types/experiment"; const reducer = produce( - (draft: ExperimentData, action: ExperimentReducerAction) => { + (draft: Draft, action: ExperimentReducerAction) => { switch (action.type) { case ExperimentReducerActionType.UPDATE_STATE: { return action.state; diff --git a/app/experimenter/static/rapid/types/experiment.ts b/app/experimenter/static/rapid/types/experiment.ts index fccf5b0dff..42ba6e7eca 100644 --- a/app/experimenter/static/rapid/types/experiment.ts +++ b/app/experimenter/static/rapid/types/experiment.ts @@ -1,18 +1,18 @@ -export type ExperimentData = { +export interface ExperimentData { name: string; objectives: string; owner?: string; slug?: string; -}; +} export enum ExperimentReducerActionType { UPDATE_STATE = "UPDATE_STATE", } -export type ExperimentReducerAction = { +export interface ExperimentReducerAction { type: ExperimentReducerActionType.UPDATE_STATE; state: ExperimentData; -}; +} export interface ExperimentContext { state: ExperimentData; From 6468e604e229ae7261a4d5e59180c277109c55f3 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 14:57:57 -0400 Subject: [PATCH 7/9] Remove unused variable --- .../static/rapid/components/experiments/ExperimentDetails.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx index d060f6f5b6..658c8de0db 100644 --- a/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx +++ b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useHistory } from "react-router"; import { Link } from "react-router-dom"; import { useExperimentState } from "experimenter-rapid/contexts/experiment/hooks"; @@ -22,7 +21,6 @@ const LabelledRow: React.FC<{ label: string; value?: string }> = ({ const ExperimentDetails: React.FC = () => { const data = useExperimentState(); - const history = useHistory(); const handleClickRequestApproval = () => { // No-op From d06a92f59e3a3e1cd2e4d244c50fba73a6f13481 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Fri, 26 Jun 2020 15:07:17 -0400 Subject: [PATCH 8/9] Fix tests --- app/experimenter/static/rapid/__tests__/experimentForm.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx b/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx index 69070864df..cdf8214128 100644 --- a/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx +++ b/app/experimenter/static/rapid/__tests__/experimentForm.test.tsx @@ -154,7 +154,7 @@ describe("", () => { }); }); - ["name", "objectives", "feature", "audience", "trigger", "version"].forEach( + ["name", "objectives", "features", "audience", "trigger", "version"].forEach( (fieldName) => { it(`shows the appropriate error message for '${fieldName}' on save`, async () => { const { getByText } = renderWithRouter( From 797581863cdbd598de4a4a0f14e245e9f57b7823 Mon Sep 17 00:00:00 2001 From: Rehan Dalal Date: Mon, 29 Jun 2020 15:33:20 -0400 Subject: [PATCH 9/9] More feedback --- .../static/rapid/components/experiments/ExperimentDetails.tsx | 4 +++- .../static/rapid/components/forms/ExperimentForm.tsx | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx index 658c8de0db..7ac5f8b640 100644 --- a/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx +++ b/app/experimenter/static/rapid/components/experiments/ExperimentDetails.tsx @@ -10,7 +10,9 @@ const LabelledRow: React.FC<{ label: string; value?: string }> = ({ }) => { return (
- {label} + {children} diff --git a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx index b667ef6006..3256eb4485 100644 --- a/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx +++ b/app/experimenter/static/rapid/components/forms/ExperimentForm.tsx @@ -61,7 +61,6 @@ const ExperimentForm: React.FC = () => { }); const responseData = await response.json(); if (!response.ok) { - console.log("whoops!"); setErrors(responseData); } else { setErrors({});