From 496ae046afc69018df1c13151d340a55a8e71233 Mon Sep 17 00:00:00 2001 From: loiswells97 Date: Tue, 31 Oct 2023 11:14:49 +0000 Subject: [PATCH 01/13] Create draft PR for #727 From 58a83d29fc873ab14ec5458321d7e42874b73176 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Tue, 31 Oct 2023 16:55:49 +0000 Subject: [PATCH 02/13] auth with web component and load/save projects --- .env.webcomponent.example | 4 +- .../EmbeddedViewer/EmbeddedViewer.jsx | 5 +- src/components/SaveButton/SaveButton.jsx | 25 +----- src/containers/ProjectComponentLoader.jsx | 66 +------------- src/containers/WebComponentLoader.jsx | 43 +++++++-- src/hooks/useProject.js | 17 +++- src/hooks/useProjectPersistence.js | 89 +++++++++++++++++++ src/redux/EditorSlice.js | 6 ++ src/web-component.js | 8 +- 9 files changed, 164 insertions(+), 99 deletions(-) create mode 100644 src/hooks/useProjectPersistence.js diff --git a/.env.webcomponent.example b/.env.webcomponent.example index d1f1f62e1..917be7570 100644 --- a/.env.webcomponent.example +++ b/.env.webcomponent.example @@ -1,4 +1,6 @@ -# NB This is the URL of react-ui, rather than the web component +REACT_APP_AUTHENTICATION_CLIENT_ID='editor-dev' +REACT_APP_AUTHENTICATION_URL='http://localhost:9001' REACT_APP_SENTRY_DSN='' REACT_APP_SENTRY_ENV='local' +# NB This is the URL of react-ui, rather than the web component PUBLIC_URL=http://localhost:3000 diff --git a/src/components/EmbeddedViewer/EmbeddedViewer.jsx b/src/components/EmbeddedViewer/EmbeddedViewer.jsx index 0a37bfa0f..4975b31ef 100644 --- a/src/components/EmbeddedViewer/EmbeddedViewer.jsx +++ b/src/components/EmbeddedViewer/EmbeddedViewer.jsx @@ -6,7 +6,7 @@ import { useSelector } from "react-redux"; import { useProject } from "../../hooks/useProject"; import { useEmbeddedMode } from "../../hooks/useEmbeddedMode"; import Output from "../Editor/Output/Output"; -import { useParams } from "react-router-dom"; +import { useParams, useSearchParams } from "react-router-dom"; import NotFoundModalEmbedded from "../Modals/NotFoundModalEmbedded"; import AccessDeniedNoAuthModalEmbedded from "../Modals/AccessDeniedNoAuthModalEmbedded"; @@ -20,11 +20,14 @@ const EmbeddedViewer = () => { ); const { identifier } = useParams(); const user = useSelector((state) => state.auth.user) || {}; + const [searchParams] = useSearchParams(); + const isBrowserPreview = searchParams.get("browserPreview") !== "true"; useProject({ projectIdentifier: identifier, accessToken: user.access_token, isEmbedded: true, + isBrowserPreview, }); useEmbeddedMode(true); diff --git a/src/components/SaveButton/SaveButton.jsx b/src/components/SaveButton/SaveButton.jsx index 83c3a63e0..b4e94457d 100644 --- a/src/components/SaveButton/SaveButton.jsx +++ b/src/components/SaveButton/SaveButton.jsx @@ -1,36 +1,19 @@ +import React from "react"; import { useSelector, useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import DesignSystemButton from "../DesignSystemButton/DesignSystemButton"; import SaveIcon from "../../assets/icons/save.svg"; -import { syncProject, showLoginToSaveModal } from "../../redux/EditorSlice"; -import { isOwner } from "../../utils/projectHelpers"; +import { triggerSave } from "../../redux/EditorSlice"; const SaveButton = ({ className, type = "secondary" }) => { const dispatch = useDispatch(); const { t } = useTranslation(); - const user = useSelector((state) => state.auth.user); - const project = useSelector((state) => state.editor.project); const loading = useSelector((state) => state.editor.loading); const onClickSave = async () => { - window.plausible("Save button"); - - if (isOwner(user, project)) { - dispatch( - syncProject("save")({ - project, - accessToken: user.access_token, - autosave: false, - }), - ); - } else if (user && project.identifier) { - dispatch( - syncProject("remix")({ project, accessToken: user.access_token }), - ); - } else { - dispatch(showLoginToSaveModal()); - } + // window.plausible("Save button"); + dispatch(triggerSave()); }; return ( diff --git a/src/containers/ProjectComponentLoader.jsx b/src/containers/ProjectComponentLoader.jsx index 999df3f32..a79b96424 100644 --- a/src/containers/ProjectComponentLoader.jsx +++ b/src/containers/ProjectComponentLoader.jsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { useProject } from "../hooks/useProject"; import { useEmbeddedMode } from "../hooks/useEmbeddedMode"; import { useMediaQuery } from "react-responsive"; @@ -8,14 +8,6 @@ import { useTranslation } from "react-i18next"; import { MOBILE_MEDIA_QUERY } from "../utils/mediaQueryBreakpoints"; -import { - expireJustLoaded, - setHasShownSavePrompt, - syncProject, -} from "../redux/EditorSlice"; -import { isOwner } from "../utils/projectHelpers"; -import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; - import Project from "../components/Editor/Project/Project"; import MobileProject from "../components/Mobile/MobileProject/MobileProject"; import NewFileModal from "../components/Modals/NewFileModal"; @@ -23,6 +15,7 @@ import NotFoundModal from "../components/Modals/NotFoundModal"; import AccessDeniedNoAuthModal from "../components/Modals/AccessDeniedNoAuthModal"; import AccessDeniedWithAuthModal from "../components/Modals/AccessDeniedWithAuthModal"; import RenameFileModal from "../components/Modals/RenameFileModal"; +import { useProjectPersistence } from "../hooks/useProjectPersistence"; const ProjectComponentLoader = (props) => { const loading = useSelector((state) => state.editor.loading); @@ -31,10 +24,6 @@ const ProjectComponentLoader = (props) => { const user = useSelector((state) => state.auth.user); const accessToken = user ? user.access_token : null; const project = useSelector((state) => state.editor.project); - const justLoaded = useSelector((state) => state.editor.justLoaded); - const hasShownSavePrompt = useSelector( - (state) => state.editor.hasShownSavePrompt, - ); const modals = useSelector((state) => state.editor.modals); const newFileModalShowing = useSelector( @@ -54,12 +43,12 @@ const ProjectComponentLoader = (props) => { ); const navigate = useNavigate(); - const dispatch = useDispatch(); const { t, i18n } = useTranslation(); const isMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY }); useEmbeddedMode(embedded); useProject({ projectIdentifier: identifier, accessToken: accessToken }); + useProjectPersistence({ user }); useEffect(() => { if (loading === "idle" && project.identifier) { @@ -70,55 +59,6 @@ const ProjectComponentLoader = (props) => { } }, [loading, project, i18n.language, navigate]); - useEffect(() => { - if (user && localStorage.getItem("awaitingSave")) { - if (isOwner(user, project)) { - dispatch( - syncProject("save")({ - project, - accessToken: user.access_token, - autosave: false, - }), - ); - } else if (user && project.identifier) { - dispatch( - syncProject("remix")({ project, accessToken: user.access_token }), - ); - } - localStorage.removeItem("awaitingSave"); - return; - } - let debouncer = setTimeout(() => { - if (isOwner(user, project) && project.identifier) { - if (justLoaded) { - dispatch(expireJustLoaded()); - } - dispatch( - syncProject("save")({ - project, - accessToken: user.access_token, - autosave: true, - }), - ); - } else { - if (justLoaded) { - dispatch(expireJustLoaded()); - } else { - localStorage.setItem( - project.identifier || "project", - JSON.stringify(project), - ); - if (!hasShownSavePrompt) { - user ? showSavePrompt() : showLoginPrompt(); - dispatch(setHasShownSavePrompt()); - } - } - } - }, 2000); - - return () => clearTimeout(debouncer); - }, [dispatch, project, user, hasShownSavePrompt, justLoaded]); - return ( <> {loading === "success" ? ( diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index ccb14d4eb..e8e3c4e0a 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -1,24 +1,49 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { setProject, setSenseHatAlwaysEnabled } from "../redux/EditorSlice"; +import { setSenseHatAlwaysEnabled } from "../redux/EditorSlice"; import WebComponentProject from "../components/WebComponentProject/WebComponentProject"; import { useTranslation } from "react-i18next"; import { setInstructions } from "../redux/InstructionsSlice"; +import { useProject } from "../hooks/useProject"; +import { useProjectPersistence } from "../hooks/useProjectPersistence"; const WebComponentLoader = (props) => { const loading = useSelector((state) => state.editor.loading); - const { code, senseHatAlwaysEnabled = false, instructions } = props; + const { + authClient, + identifier, + code, + senseHatAlwaysEnabled = false, + instructions, + } = props; const dispatch = useDispatch(); const { t } = useTranslation(); + const [projectIdentifier, setProjectIdentifier] = useState(identifier); + const project = useSelector((state) => state.editor.project); + const user = JSON.parse( + localStorage.getItem( + `oidc.user:${process.env.REACT_APP_AUTHENTICATION_URL}:${authClient}`, + ), + ); + + useEffect(() => { + if (loading === "idle" && project.identifier) { + setProjectIdentifier(project.identifier); + } + }, [loading, project]); + + useProject({ + projectIdentifier: projectIdentifier, + code: code, + accessToken: user && user.access_token, + }); + useProjectPersistence({ + user: user, + }); useEffect(() => { - const proj = { - type: "python", - components: [{ name: "main", extension: "py", content: code }], - }; dispatch(setSenseHatAlwaysEnabled(senseHatAlwaysEnabled)); - dispatch(setProject(proj)); - }, [code, senseHatAlwaysEnabled, dispatch]); + }, [senseHatAlwaysEnabled, dispatch]); useEffect(() => { if (instructions) { diff --git a/src/hooks/useProject.js b/src/hooks/useProject.js index e36178cb2..a51cc9ef4 100644 --- a/src/hooks/useProject.js +++ b/src/hooks/useProject.js @@ -4,16 +4,16 @@ import { useDispatch } from "react-redux"; import { syncProject, setProject } from "../redux/EditorSlice"; import { defaultPythonProject } from "../utils/defaultProjects"; import { useTranslation } from "react-i18next"; -import { useSearchParams } from "react-router-dom"; export const useProject = ({ projectIdentifier = null, + code = null, accessToken = null, isEmbedded = false, + isBrowserPreview = false, }) => { - const [searchParams] = useSearchParams(); const getCachedProject = (id) => - isEmbedded && searchParams.get("browserPreview") !== "true" + isEmbedded && !isBrowserPreview ? null : JSON.parse(localStorage.getItem(id || "project")); const [cachedProject, setCachedProject] = useState( @@ -52,6 +52,17 @@ export const useProject = ({ ); return; } + + if (code) { + const project = { + name: "Blank project", + type: "python", + components: [{ name: "main", extension: "py", content: code }], + }; + dispatch(setProject(project)); + return; + } + const data = defaultPythonProject; dispatch(setProject(data)); }, [projectIdentifier, cachedProject, i18n.language, accessToken]); diff --git a/src/hooks/useProjectPersistence.js b/src/hooks/useProjectPersistence.js new file mode 100644 index 000000000..cff7a4f78 --- /dev/null +++ b/src/hooks/useProjectPersistence.js @@ -0,0 +1,89 @@ +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { isOwner } from "../utils/projectHelpers"; +import { + expireJustLoaded, + setHasShownSavePrompt, + showLoginToSaveModal, + syncProject, +} from "../redux/EditorSlice"; +import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; + +export const useProjectPersistence = ({ user }) => { + const dispatch = useDispatch(); + const project = useSelector((state) => state.editor.project); + const justLoaded = useSelector((state) => state.editor.justLoaded); + const hasShownSavePrompt = useSelector( + (state) => state.editor.hasShownSavePrompt, + ); + const saveTriggered = useSelector((state) => state.editor.saveTriggered); + + useEffect(() => { + if (saveTriggered) { + if (isOwner(user, project)) { + dispatch( + syncProject("save")({ + project, + accessToken: user.access_token, + autosave: false, + }), + ); + } else if (user && project.identifier) { + dispatch( + syncProject("remix")({ project, accessToken: user.access_token }), + ); + } else { + dispatch(showLoginToSaveModal()); + } + } + }, [saveTriggered, project, user, dispatch]); + + useEffect(() => { + if (user && localStorage.getItem("awaitingSave")) { + if (isOwner(user, project)) { + dispatch( + syncProject("save")({ + project, + accessToken: user.access_token, + autosave: false, + }), + ); + } else if (user && project.identifier) { + dispatch( + syncProject("remix")({ project, accessToken: user.access_token }), + ); + } + localStorage.removeItem("awaitingSave"); + return; + } + let debouncer = setTimeout(() => { + if (isOwner(user, project) && project.identifier) { + if (justLoaded) { + dispatch(expireJustLoaded()); + } + dispatch( + syncProject("save")({ + project, + accessToken: user.access_token, + autosave: true, + }), + ); + } else { + if (justLoaded) { + dispatch(expireJustLoaded()); + } else { + localStorage.setItem( + project.identifier || "project", + JSON.stringify(project), + ); + if (!hasShownSavePrompt) { + user ? showSavePrompt() : showLoginPrompt(); + dispatch(setHasShownSavePrompt()); + } + } + } + }, 2000); + + return () => clearTimeout(debouncer); + }, [dispatch, project, user, hasShownSavePrompt, justLoaded]); +}; diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index 25481dcd1..952900310 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -76,6 +76,7 @@ export const EditorSlice = createSlice({ name: "editor", initialState: { project: {}, + saveTriggered: false, saving: "idle", loading: "idle", justLoaded: false, @@ -208,6 +209,9 @@ export const EditorSlice = createSlice({ triggerDraw: (state) => { state.drawTriggered = true; }, + triggerSave: (state) => { + state.saveTriggered = true; + }, updateProjectComponent: (state, action) => { const extension = action.payload.extension; const fileName = action.payload.name; @@ -338,6 +342,7 @@ export const EditorSlice = createSlice({ extraReducers: (builder) => { builder.addCase("editor/saveProject/pending", (state) => { state.saving = "pending"; + state.saveTriggered = false; }); builder.addCase("editor/saveProject/fulfilled", (state, action) => { localStorage.removeItem(state.project.identifier || "project"); @@ -417,6 +422,7 @@ export const { stopDraw, triggerCodeRun, triggerDraw, + triggerSave, updateComponentName, updateImages, updateProjectComponent, diff --git a/src/web-component.js b/src/web-component.js index c45552f8a..f371fc433 100644 --- a/src/web-component.js +++ b/src/web-component.js @@ -35,7 +35,13 @@ class WebComponent extends HTMLElement { } static get observedAttributes() { - return ["code", "sense_hat_always_enabled", "instructions"]; + return [ + "auth_client", + "identifier", + "code", + "sense_hat_always_enabled", + "instructions", + ]; } attributeChangedCallback(name, _oldVal, newVal) { From 2137d735976ed8fd75441854a2050ebc068143ab Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Wed, 1 Nov 2023 11:48:05 +0000 Subject: [PATCH 03/13] refactoring and fixing broken tests --- .env.webcomponent.example | 1 - docker-compose.yml | 4 +- .../EmbeddedViewer/EmbeddedViewer.jsx | 2 +- .../EmbeddedViewer/EmbeddedViewer.test.js | 34 ++ src/components/SaveButton/SaveButton.jsx | 2 +- src/components/SaveButton/SaveButton.test.js | 393 ++++++++++-------- src/containers/WebComponentLoader.jsx | 10 +- src/containers/WebComponentLoader.test.js | 30 +- src/hooks/useProject.test.js | 20 +- src/web-component.js | 2 +- 10 files changed, 286 insertions(+), 212 deletions(-) diff --git a/.env.webcomponent.example b/.env.webcomponent.example index 917be7570..84460ebf7 100644 --- a/.env.webcomponent.example +++ b/.env.webcomponent.example @@ -1,4 +1,3 @@ -REACT_APP_AUTHENTICATION_CLIENT_ID='editor-dev' REACT_APP_AUTHENTICATION_URL='http://localhost:9001' REACT_APP_SENTRY_DSN='' REACT_APP_SENTRY_ENV='local' diff --git a/docker-compose.yml b/docker-compose.yml index b0f9148a5..b9f23e235 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,13 +17,13 @@ services: <<: *x-app command: yarn start ports: - - "3000:3000" + - "3010:3000" container_name: react-ui react-ui-wc: <<: *x-app command: yarn start:wc ports: - - "3001:3001" + - "3011:3001" container_name: react-ui-wc secrets: npmrc: diff --git a/src/components/EmbeddedViewer/EmbeddedViewer.jsx b/src/components/EmbeddedViewer/EmbeddedViewer.jsx index 4975b31ef..fb20fdf88 100644 --- a/src/components/EmbeddedViewer/EmbeddedViewer.jsx +++ b/src/components/EmbeddedViewer/EmbeddedViewer.jsx @@ -21,7 +21,7 @@ const EmbeddedViewer = () => { const { identifier } = useParams(); const user = useSelector((state) => state.auth.user) || {}; const [searchParams] = useSearchParams(); - const isBrowserPreview = searchParams.get("browserPreview") !== "true"; + const isBrowserPreview = searchParams.get("browserPreview") === "true"; useProject({ projectIdentifier: identifier, diff --git a/src/components/EmbeddedViewer/EmbeddedViewer.test.js b/src/components/EmbeddedViewer/EmbeddedViewer.test.js index 6183c2491..90ce8c168 100644 --- a/src/components/EmbeddedViewer/EmbeddedViewer.test.js +++ b/src/components/EmbeddedViewer/EmbeddedViewer.test.js @@ -6,11 +6,18 @@ import configureStore from "redux-mock-store"; import { render, screen } from "@testing-library/react"; import { useProject } from "../../hooks/useProject"; +let mockBrowserPreview = false; + jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), useParams: () => ({ identifier: "my-amazing-project", }), + useSearchParams: () => [ + { + get: (key) => (key === "browserPreview" ? mockBrowserPreview : null), + }, + ], })); jest.mock("../../hooks/useProject", () => ({ @@ -86,6 +93,33 @@ test("Loads project with correct params", () => { projectIdentifier: "my-amazing-project", accessToken: "my_token", isEmbedded: true, + isBrowserPreview: false, + }); +}); + +test("Loads project with correct params if browser preview", () => { + initialState = { + ...initialState, + editor: { + ...initialState.editor, + loading: "success", + }, + }; + + const mockStore = configureStore([]); + store = mockStore(initialState); + mockBrowserPreview = "true"; + + render( + + + , + ); + expect(useProject).toHaveBeenCalledWith({ + projectIdentifier: "my-amazing-project", + accessToken: "my_token", + isEmbedded: true, + isBrowserPreview: true, }); }); diff --git a/src/components/SaveButton/SaveButton.jsx b/src/components/SaveButton/SaveButton.jsx index b4e94457d..e5e01b993 100644 --- a/src/components/SaveButton/SaveButton.jsx +++ b/src/components/SaveButton/SaveButton.jsx @@ -12,7 +12,7 @@ const SaveButton = ({ className, type = "secondary" }) => { const loading = useSelector((state) => state.editor.loading); const onClickSave = async () => { - // window.plausible("Save button"); + window.plausible("Save button"); dispatch(triggerSave()); }; diff --git a/src/components/SaveButton/SaveButton.test.js b/src/components/SaveButton/SaveButton.test.js index 9dfd6d37e..c288bc988 100644 --- a/src/components/SaveButton/SaveButton.test.js +++ b/src/components/SaveButton/SaveButton.test.js @@ -1,224 +1,257 @@ import React from "react"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; -import { syncProject, showLoginToSaveModal } from "../../redux/EditorSlice"; -import { MemoryRouter } from "react-router-dom"; +import { triggerSave } from "../../redux/EditorSlice"; import SaveButton from "./SaveButton"; -jest.mock("axios"); - -jest.mock("react-router-dom", () => ({ - ...jest.requireActual("react-router-dom"), - useNavigate: () => jest.fn(), -})); - -jest.mock("../../redux/EditorSlice", () => ({ - ...jest.requireActual("../../redux/EditorSlice"), - syncProject: jest.fn((_) => jest.fn()), -})); - -const project = { - name: "Hello world", - identifier: "hello-world-project", - components: [], - image_list: [], - user_id: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", -}; -const user = { - access_token: "39a09671-be55-4847-baf5-8919a0c24a25", - profile: { - user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", - }, -}; - -describe("When logged in and user owns project", () => { - let store; - let saveButton; +// jest.mock("axios"); - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project: project, - loading: "success", - }, - auth: { - user: user, - }, - }; - store = mockStore(initialState); - render( - - - - - , - ); - saveButton = screen.queryByText("header.save"); - }); +// jest.mock("react-router-dom", () => ({ +// ...jest.requireActual("react-router-dom"), +// useNavigate: () => jest.fn(), +// })); - test("Clicking save dispatches saveProject with correct parameters", async () => { - const saveAction = { type: "SAVE_PROJECT" }; - const saveProject = jest.fn(() => saveAction); - syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); - fireEvent.click(saveButton); - await waitFor(() => - expect(saveProject).toHaveBeenCalledWith({ - project, - accessToken: user.access_token, - autosave: false, - }), - ); - expect(store.getActions()[0]).toEqual(saveAction); - }); -}); +// jest.mock("../../redux/EditorSlice", () => ({ +// ...jest.requireActual("../../redux/EditorSlice"), +// syncProject: jest.fn((_) => jest.fn()), +// })); -describe("When logged in and no project identifier", () => { +describe("When project is loaded", () => { let store; - const project_without_id = { ...project, identifier: null }; beforeEach(() => { const middlewares = []; const mockStore = configureStore(middlewares); const initialState = { editor: { - project: project_without_id, loading: "success", }, - auth: { - user: user, - }, }; store = mockStore(initialState); render( - - - + , ); }); - test("Clicking save dispatches saveProject with correct parameters", async () => { - const saveAction = { type: "SAVE_PROJECT" }; - const saveProject = jest.fn(() => saveAction); - syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); - const saveButton = screen.getByText("header.save"); + test("Save button renders", () => { + expect(screen.queryByText("header.save")).toBeInTheDocument(); + }); + + test("Clicking save dispatches trigger save action", () => { + const saveButton = screen.queryByText("header.save"); fireEvent.click(saveButton); - await waitFor(() => - expect(saveProject).toHaveBeenCalledWith({ - project: project_without_id, - accessToken: user.access_token, - autosave: false, - }), - ); - expect(store.getActions()[0]).toEqual(saveAction); + expect(store.getActions()).toEqual([triggerSave()]); }); }); -describe("When logged in and user does not own project", () => { - const another_project = { - ...project, - user_id: "5254370e-26d2-4c8a-9526-8dbafea43aa9", - }; - let store; - +describe("When project is not loaded", () => { beforeEach(() => { const middlewares = []; const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project: another_project, - loading: "success", - }, - auth: { - user: user, - }, - }; - store = mockStore(initialState); + const store = mockStore({ + editor: {}, + }); render( - - - + , ); }); - - test("Clicking save dispatches remixProject with correct parameters", async () => { - const remixAction = { type: "REMIX_PROJECT" }; - const remixProject = jest.fn(() => remixAction); - syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); - const saveButton = screen.getByText("header.save"); - fireEvent.click(saveButton); - await waitFor(() => - expect(remixProject).toHaveBeenCalledWith({ - project: another_project, - accessToken: user.access_token, - }), - ); - expect(store.getActions()[0]).toEqual(remixAction); + test("Does not render save button", () => { + expect(screen.queryByText("header.save")).not.toBeInTheDocument(); }); }); -describe("When not logged in", () => { - let store; +// describe("When logged in and user owns project", () => { +// let store; +// let saveButton; - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project: project, - loading: "success", - }, - auth: { - user: null, - }, - }; - store = mockStore(initialState); - render( - - - - - , - ); - }); +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project: project, +// loading: "success", +// }, +// auth: { +// user: user, +// }, +// }; +// store = mockStore(initialState); +// render( +// +// +// +// +// , +// ); +// saveButton = screen.queryByText("header.save"); +// }); - test("Clicking save opens login to save modal", () => { - const saveButton = screen.getByText("header.save"); - fireEvent.click(saveButton); - expect(store.getActions()).toEqual([showLoginToSaveModal()]); - }); -}); +// test("Clicking save dispatches saveProject with correct parameters", async () => { +// const saveAction = { type: "SAVE_PROJECT" }; +// const saveProject = jest.fn(() => saveAction); +// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); +// fireEvent.click(saveButton); +// await waitFor(() => +// expect(saveProject).toHaveBeenCalledWith({ +// project, +// accessToken: user.access_token, +// autosave: false, +// }), +// ); +// expect(store.getActions()[0]).toEqual(saveAction); +// }); +// }); -describe("When no project loaded", () => { - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project: {}, - loading: "idle", - }, - auth: { - user: user, - }, - }; - const store = mockStore(initialState); - render( - - - - - , - ); - }); +// describe("When logged in and no project identifier", () => { +// let store; +// const project_without_id = { ...project, identifier: null }; - test("No save button", () => { - expect(screen.queryByText("header.save")).not.toBeInTheDocument(); - }); -}); +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project: project_without_id, +// loading: "success", +// }, +// auth: { +// user: user, +// }, +// }; +// store = mockStore(initialState); +// render( +// +// +// +// +// , +// ); +// }); + +// test("Clicking save dispatches saveProject with correct parameters", async () => { +// const saveAction = { type: "SAVE_PROJECT" }; +// const saveProject = jest.fn(() => saveAction); +// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); +// const saveButton = screen.getByText("header.save"); +// fireEvent.click(saveButton); +// await waitFor(() => +// expect(saveProject).toHaveBeenCalledWith({ +// project: project_without_id, +// accessToken: user.access_token, +// autosave: false, +// }), +// ); +// expect(store.getActions()[0]).toEqual(saveAction); +// }); +// }); + +// describe("When logged in and user does not own project", () => { +// const another_project = { +// ...project, +// user_id: "5254370e-26d2-4c8a-9526-8dbafea43aa9", +// }; +// let store; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project: another_project, +// loading: "success", +// }, +// auth: { +// user: user, +// }, +// }; +// store = mockStore(initialState); +// render( +// +// +// +// +// , +// ); +// }); + +// test("Clicking save dispatches remixProject with correct parameters", async () => { +// const remixAction = { type: "REMIX_PROJECT" }; +// const remixProject = jest.fn(() => remixAction); +// syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); +// const saveButton = screen.getByText("header.save"); +// fireEvent.click(saveButton); +// await waitFor(() => +// expect(remixProject).toHaveBeenCalledWith({ +// project: another_project, +// accessToken: user.access_token, +// }), +// ); +// expect(store.getActions()[0]).toEqual(remixAction); +// }); +// }); + +// describe("When not logged in", () => { +// let store; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project: project, +// loading: "success", +// }, +// auth: { +// user: null, +// }, +// }; +// store = mockStore(initialState); +// render( +// +// +// +// +// , +// ); +// }); + +// test("Clicking save opens login to save modal", () => { +// const saveButton = screen.getByText("header.save"); +// fireEvent.click(saveButton); +// expect(store.getActions()).toEqual([showLoginToSaveModal()]); +// }); +// }); + +// describe("When no project loaded", () => { +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project: {}, +// loading: "idle", +// }, +// auth: { +// user: user, +// }, +// }; +// const store = mockStore(initialState); +// render( +// +// +// +// +// , +// ); +// }); + +// test("No save button", () => { +// expect(screen.queryByText("header.save")).not.toBeInTheDocument(); +// }); +// }); diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index e8e3c4e0a..b34f2be8c 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -10,7 +10,7 @@ import { useProjectPersistence } from "../hooks/useProjectPersistence"; const WebComponentLoader = (props) => { const loading = useSelector((state) => state.editor.loading); const { - authClient, + authKey, identifier, code, senseHatAlwaysEnabled = false, @@ -20,11 +20,7 @@ const WebComponentLoader = (props) => { const { t } = useTranslation(); const [projectIdentifier, setProjectIdentifier] = useState(identifier); const project = useSelector((state) => state.editor.project); - const user = JSON.parse( - localStorage.getItem( - `oidc.user:${process.env.REACT_APP_AUTHENTICATION_URL}:${authClient}`, - ), - ); + const user = JSON.parse(localStorage.getItem(authKey)); useEffect(() => { if (loading === "idle" && project.identifier) { @@ -34,7 +30,7 @@ const WebComponentLoader = (props) => { useProject({ projectIdentifier: projectIdentifier, - code: code, + code, accessToken: user && user.access_token, }); useProjectPersistence({ diff --git a/src/containers/WebComponentLoader.test.js b/src/containers/WebComponentLoader.test.js index 560b23dee..64f41c8c0 100644 --- a/src/containers/WebComponentLoader.test.js +++ b/src/containers/WebComponentLoader.test.js @@ -3,15 +3,23 @@ import React from "react"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import WebComponentLoader from "./WebComponentLoader"; -import { setProject, setSenseHatAlwaysEnabled } from "../redux/EditorSlice"; +import { setSenseHatAlwaysEnabled } from "../redux/EditorSlice"; import { setInstructions } from "../redux/InstructionsSlice"; +import { useProject } from "../hooks/useProject"; + +jest.mock("../hooks/useProject", () => ({ + useProject: jest.fn(), +})); let store; const code = "print('This project is amazing')"; +const identifier = "My amazing project"; const steps = [{ quiz: false, title: "Step 1", content: "Do something" }]; const instructions = { currentStepPosition: 3, project: { steps: steps } }; +const authKey = "my_key"; beforeEach(() => { + localStorage.setItem(authKey, JSON.stringify({ access_token: "my_token" })); const middlewares = []; const mockStore = configureStore(middlewares); const initialState = { @@ -32,21 +40,21 @@ beforeEach(() => { , ); }); -test("Sets project with code from attribute", () => { - const project = { - type: "python", - components: [{ name: "main", extension: "py", content: code }], - }; - expect(store.getActions()).toEqual( - expect.arrayContaining([setProject(project)]), - ); +test("Calls useProject hook with correct attribute", () => { + expect(useProject).toHaveBeenCalledWith({ + projectIdentifier: identifier, + code, + accessToken: "my_token", + }); }); test("Enables the SenseHat", () => { @@ -60,3 +68,7 @@ test("Sets the instructions", () => { expect.arrayContaining([setInstructions(instructions)]), ); }); + +afterEach(() => { + localStorage.clear(); +}); diff --git a/src/hooks/useProject.test.js b/src/hooks/useProject.test.js index 4a4724d12..9f3c9593c 100644 --- a/src/hooks/useProject.test.js +++ b/src/hooks/useProject.test.js @@ -4,21 +4,21 @@ import { syncProject, setProject } from "../redux/EditorSlice"; import { waitFor } from "@testing-library/react"; import { defaultPythonProject } from "../utils/defaultProjects"; -let mockBrowserPreview = "false"; +// let mockBrowserPreview = "false"; jest.mock("react-redux", () => ({ ...jest.requireActual("react-redux"), useDispatch: () => jest.fn(), })); -jest.mock("react-router-dom", () => ({ - ...jest.requireActual("react-router-dom"), - useSearchParams: () => [ - { - get: (key) => (key === "browserPreview" ? mockBrowserPreview : null), - }, - ], -})); +// jest.mock("react-router-dom", () => ({ +// ...jest.requireActual("react-router-dom"), +// useSearchParams: () => [ +// { +// get: (key) => (key === "browserPreview" ? mockBrowserPreview : null), +// }, +// ], +// })); const loadProject = jest.fn(); @@ -125,13 +125,13 @@ test("If embedded and cached project, loads from server", async () => { }); test("If new tab browser preview, uses cached changes", () => { - mockBrowserPreview = "true"; localStorage.setItem("hello-world-project", JSON.stringify(cachedProject)); renderHook(() => useProject({ projectIdentifier: "hello-world-project", accessToken, isEmbedded: true, + isBrowserPreview: true, }), ); expect(setProject).toHaveBeenCalledWith(cachedProject); diff --git a/src/web-component.js b/src/web-component.js index f371fc433..ed7a349ad 100644 --- a/src/web-component.js +++ b/src/web-component.js @@ -36,7 +36,7 @@ class WebComponent extends HTMLElement { static get observedAttributes() { return [ - "auth_client", + "auth_key", "identifier", "code", "sense_hat_always_enabled", From 0bc6a81ec1e85073afbd92831a0a0cd81c0bdfa6 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Wed, 1 Nov 2023 12:13:32 +0000 Subject: [PATCH 04/13] revert ports --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b9f23e235..b0f9148a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,13 +17,13 @@ services: <<: *x-app command: yarn start ports: - - "3010:3000" + - "3000:3000" container_name: react-ui react-ui-wc: <<: *x-app command: yarn start:wc ports: - - "3011:3001" + - "3001:3001" container_name: react-ui-wc secrets: npmrc: From 79a8aa22d301cfe14fe49fb1066a10cd65548913 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Wed, 1 Nov 2023 14:26:26 +0000 Subject: [PATCH 05/13] small tweaks and testing --- docker-compose.yml | 4 +- src/containers/ProjectComponentLoader.test.js | 1070 +++++++++-------- src/containers/WebComponentLoader.jsx | 1 + src/containers/WebComponentLoader.test.js | 14 +- src/hooks/useProject.test.js | 27 +- src/redux/EditorSlice.js | 1 + 6 files changed, 594 insertions(+), 523 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b0f9148a5..b9f23e235 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,13 +17,13 @@ services: <<: *x-app command: yarn start ports: - - "3000:3000" + - "3010:3000" container_name: react-ui react-ui-wc: <<: *x-app command: yarn start:wc ports: - - "3001:3001" + - "3011:3001" container_name: react-ui-wc secrets: npmrc: diff --git a/src/containers/ProjectComponentLoader.test.js b/src/containers/ProjectComponentLoader.test.js index 45c93674f..7346bd5e7 100644 --- a/src/containers/ProjectComponentLoader.test.js +++ b/src/containers/ProjectComponentLoader.test.js @@ -10,32 +10,37 @@ import { MOBILE_BREAKPOINT } from "../utils/mediaQueryBreakpoints"; import { setProject, - expireJustLoaded, - setHasShownSavePrompt, - syncProject, + // expireJustLoaded, + // setHasShownSavePrompt, + // syncProject, } from "../redux/EditorSlice"; -import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; +// import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; +import { useProjectPersistence } from "../hooks/useProjectPersistence"; -jest.mock("axios"); +// jest.mock("axios"); -jest.mock("react-router-dom", () => ({ - ...jest.requireActual("react-router-dom"), - useNavigate: () => jest.fn(), -})); +// jest.mock("react-router-dom", () => ({ +// ...jest.requireActual("react-router-dom"), +// useNavigate: () => jest.fn(), +// })); jest.mock("react-responsive", () => ({ ...jest.requireActual("react-responsive"), useMediaQuery: ({ query }) => mockMediaQuery(query), })); -jest.mock("../redux/EditorSlice", () => ({ - ...jest.requireActual("../redux/EditorSlice"), - syncProject: jest.fn((_) => jest.fn()), -})); +// jest.mock("../redux/EditorSlice", () => ({ +// ...jest.requireActual("../redux/EditorSlice"), +// syncProject: jest.fn((_) => jest.fn()), +// })); -jest.mock("../utils/Notifications"); +// jest.mock("../utils/Notifications"); -jest.useFakeTimers(); +jest.mock("../hooks/useProjectPersistence", () => ({ + useProjectPersistence: jest.fn(), +})); + +// jest.useFakeTimers(); let mockMediaQuery = (query) => { return matchMedia(query).matches; @@ -43,33 +48,40 @@ let mockMediaQuery = (query) => { window.HTMLElement.prototype.scrollIntoView = jest.fn(); -const user1 = { +const user = { access_token: "myAccessToken", profile: { user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", }, }; -const user2 = { - access_token: "myAccessToken", - profile: { - user: "cd8a5b3d-f7bb-425e-908f-1386decd6bb1", - }, -}; - -const project = { - name: "hello world", - project_type: "python", - identifier: "hello-world-project", - components: [ - { - name: "main", - extension: "py", - content: "# hello", - }, - ], - user_id: user1.profile.user, -}; +// const user1 = { +// access_token: "myAccessToken", +// profile: { +// user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", +// }, +// }; + +// const user2 = { +// access_token: "myAccessToken", +// profile: { +// user: "cd8a5b3d-f7bb-425e-908f-1386decd6bb1", +// }, +// }; + +// const project = { +// name: "hello world", +// project_type: "python", +// identifier: "hello-world-project", +// components: [ +// { +// name: "main", +// extension: "py", +// content: "# hello", +// }, +// ], +// user_id: user1.profile.user, +// }; test("Renders loading message if loading is pending", () => { const middlewares = []; @@ -138,6 +150,48 @@ test("Does not render loading message if loading is success", () => { expect(screen.queryByText("project.loading")).not.toBeInTheDocument(); }); +test("Calls useProjectPersistence with user when logged in", () => { + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: {}, + }, + auth: { user }, + }; + const store = mockStore(initialState); + render( + + +
+ +
+
, + ); + expect(useProjectPersistence).toHaveBeenCalledWith({ user }); +}); + +test("Calls useProjectPersistence without user when logged in", () => { + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: {}, + }, + auth: {}, + }; + const store = mockStore(initialState); + render( + + +
+ +
+
, + ); + expect(useProjectPersistence).toHaveBeenCalledWith({}); +}); + describe("When on mobile", () => { let mockStore; @@ -204,475 +258,475 @@ describe("When on mobile", () => { }); }); -describe("When not logged in and just loaded", () => { - let mockedStore; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - justLoaded: true, - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: {}, - }; - mockedStore = mockStore(initialState); - render( - - -
- -
-
-
, - ); - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Expires justLoaded", async () => { - const expectedActions = [ - setProject(defaultPythonProject), - expireJustLoaded(), - ]; - await waitFor( - () => expect(mockedStore.getActions()).toEqual(expectedActions), - { timeout: 2100 }, - ); - }); -}); - -describe("When not logged in and not just loaded", () => { - let mockedStore; - let expectedActions; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - justLoaded: false, - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: {}, - }; - mockedStore = mockStore(initialState); - render( - - -
- -
-
-
, - ); - expectedActions = [setProject(defaultPythonProject)]; - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Login prompt shown", async () => { - await waitFor(() => expect(showLoginPrompt).toHaveBeenCalled(), { - timeout: 2100, - }); - }); - - test("Dispatches save prompt shown action", async () => { - expectedActions.push(setHasShownSavePrompt()); - await waitFor( - () => expect(mockedStore.getActions()).toEqual(expectedActions), - { timeout: 2100 }, - ); - }); - - test("Project saved in localStorage", async () => { - await waitFor( - () => - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ), - { timeout: 2100 }, - ); - }); -}); - -describe("When not logged in and has been prompted to login to save", () => { - let mockedStore; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - justLoaded: false, - hasShownSavePrompt: true, - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: {}, - }; - mockedStore = mockStore(initialState); - render( - - -
- -
-
-
, - ); - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Login prompt shown", async () => { - jest.runAllTimers(); - await waitFor(() => expect(showLoginPrompt).not.toHaveBeenCalled(), { - timeout: 2100, - }); - }); - - test("Project saved in localStorage", async () => { - await waitFor( - () => - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ), - { timeout: 2100 }, - ); - }); -}); - -describe("When logged in and user does not own project and just loaded", () => { - let mockedStore; - let expectedActions; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - justLoaded: true, - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: { - user: user2, - }, - }; - mockedStore = mockStore(initialState); - render( - - -
- -
-
-
, - ); - expectedActions = [setProject(defaultPythonProject)]; - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Expires justLoaded", async () => { - expectedActions.push(expireJustLoaded()); - await waitFor( - () => expect(mockedStore.getActions()).toEqual(expectedActions), - { timeout: 2100 }, - ); - }); -}); - -describe("When logged in and user does not own project and not just loaded", () => { - let mockedStore; - let expectedActions; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - justLoaded: false, - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: { - user: user2, - }, - }; - mockedStore = mockStore(initialState); - render( - - -
- -
-
-
, - ); - expectedActions = [setProject(defaultPythonProject)]; - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Save prompt shown", async () => { - await waitFor(() => expect(showSavePrompt).toHaveBeenCalled(), { - timeout: 2100, - }); - }); - - test("Dispatches save prompt shown action", async () => { - expectedActions.push(setHasShownSavePrompt()); - await waitFor( - () => expect(mockedStore.getActions()).toEqual(expectedActions), - { timeout: 2100 }, - ); - }); - - test("Project saved in localStorage", async () => { - await waitFor( - () => - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ), - { timeout: 2100 }, - ); - }); -}); - -describe("When logged in and user does not own project and prompted to save", () => { - let mockedStore; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - justLoaded: false, - hasShownSavePrompt: true, - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: { - user: user2, - }, - }; - mockedStore = mockStore(initialState); - render( - - -
- -
-
-
, - ); - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Save prompt not shown again", async () => { - jest.runAllTimers(); - await waitFor(() => expect(showSavePrompt).not.toHaveBeenCalled(), { - timeout: 2100, - }); - }); - - test("Project saved in localStorage", async () => { - await waitFor( - () => - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ), - { timeout: 2100 }, - ); - }); -}); - -describe("When logged in and user does not own project and awaiting save", () => { - let mockedStore; - let remixProject; - let remixAction; - let expectedActions; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: { - user: user2, - }, - }; - mockedStore = mockStore(initialState); - localStorage.setItem("awaitingSave", "true"); - remixAction = { type: "REMIX_PROJECT" }; - remixProject = jest.fn(() => remixAction); - syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); - render( - - -
- -
-
-
, - ); - expectedActions = [setProject(defaultPythonProject)]; - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Project remixed and saved to database", async () => { - expectedActions.push(remixAction); - await waitFor( - () => - expect(remixProject).toHaveBeenCalledWith({ - project, - accessToken: user2.access_token, - }), - { timeout: 2100 }, - ); - expect(mockedStore.getActions()).toEqual(expectedActions); - }); -}); - -describe("When logged in and project has no identifier and awaiting save", () => { - let mockedStore; - let saveProject; - let saveAction; - let expectedActions; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project: { ...project, identifier: null }, - loading: "success", - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: { - user: user2, - }, - }; - mockedStore = mockStore(initialState); - localStorage.setItem("awaitingSave", "true"); - saveAction = { type: "SAVE_PROJECT" }; - saveProject = jest.fn(() => saveAction); - syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); - render( - - -
- -
-
-
, - ); - expectedActions = [setProject(defaultPythonProject)]; - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("Project saved to database", async () => { - expectedActions.push(saveAction); - await waitFor( - () => - expect(saveProject).toHaveBeenCalledWith({ - project: { ...project, identifier: null }, - accessToken: user2.access_token, - autosave: false, - }), - { timeout: 2100 }, - ); - expect(mockedStore.getActions()).toEqual(expectedActions); - }); -}); - -describe("When logged in and user owns project", () => { - let mockedStore; - let expectedActions; - - beforeEach(() => { - const middlewares = []; - const mockStore = configureStore(middlewares); - const initialState = { - editor: { - project, - loading: "success", - openFiles: [[]], - focussedFileIndices: [0], - }, - auth: { - user: user1, - }, - }; - mockedStore = mockStore(initialState); - render( - - -
- -
-
-
, - ); - expectedActions = [setProject(defaultPythonProject)]; - }); - - test("Project autosaved to database", async () => { - const saveAction = { type: "SAVE_PROJECT" }; - const saveProject = jest.fn(() => saveAction); - expectedActions.push(saveAction); - syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); - await waitFor( - () => - expect(saveProject).toHaveBeenCalledWith({ - project, - accessToken: user1.access_token, - autosave: true, - }), - { timeout: 2100 }, - ); - expect(mockedStore.getActions()).toEqual(expectedActions); - }); -}); +// describe("When not logged in and just loaded", () => { +// let mockedStore; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: {}, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Expires justLoaded", async () => { +// const expectedActions = [ +// setProject(defaultPythonProject), +// expireJustLoaded(), +// ]; +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When not logged in and not just loaded", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: {}, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Login prompt shown", async () => { +// await waitFor(() => expect(showLoginPrompt).toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Dispatches save prompt shown action", async () => { +// expectedActions.push(setHasShownSavePrompt()); +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When not logged in and has been prompted to login to save", () => { +// let mockedStore; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// hasShownSavePrompt: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: {}, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Login prompt shown", async () => { +// jest.runAllTimers(); +// await waitFor(() => expect(showLoginPrompt).not.toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and just loaded", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Expires justLoaded", async () => { +// expectedActions.push(expireJustLoaded()); +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and not just loaded", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Save prompt shown", async () => { +// await waitFor(() => expect(showSavePrompt).toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Dispatches save prompt shown action", async () => { +// expectedActions.push(setHasShownSavePrompt()); +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and prompted to save", () => { +// let mockedStore; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// hasShownSavePrompt: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Save prompt not shown again", async () => { +// jest.runAllTimers(); +// await waitFor(() => expect(showSavePrompt).not.toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and awaiting save", () => { +// let mockedStore; +// let remixProject; +// let remixAction; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// localStorage.setItem("awaitingSave", "true"); +// remixAction = { type: "REMIX_PROJECT" }; +// remixProject = jest.fn(() => remixAction); +// syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Project remixed and saved to database", async () => { +// expectedActions.push(remixAction); +// await waitFor( +// () => +// expect(remixProject).toHaveBeenCalledWith({ +// project, +// accessToken: user2.access_token, +// }), +// { timeout: 2100 }, +// ); +// expect(mockedStore.getActions()).toEqual(expectedActions); +// }); +// }); + +// describe("When logged in and project has no identifier and awaiting save", () => { +// let mockedStore; +// let saveProject; +// let saveAction; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project: { ...project, identifier: null }, +// loading: "success", +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// localStorage.setItem("awaitingSave", "true"); +// saveAction = { type: "SAVE_PROJECT" }; +// saveProject = jest.fn(() => saveAction); +// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Project saved to database", async () => { +// expectedActions.push(saveAction); +// await waitFor( +// () => +// expect(saveProject).toHaveBeenCalledWith({ +// project: { ...project, identifier: null }, +// accessToken: user2.access_token, +// autosave: false, +// }), +// { timeout: 2100 }, +// ); +// expect(mockedStore.getActions()).toEqual(expectedActions); +// }); +// }); + +// describe("When logged in and user owns project", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user1, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// test("Project autosaved to database", async () => { +// const saveAction = { type: "SAVE_PROJECT" }; +// const saveProject = jest.fn(() => saveAction); +// expectedActions.push(saveAction); +// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); +// await waitFor( +// () => +// expect(saveProject).toHaveBeenCalledWith({ +// project, +// accessToken: user1.access_token, +// autosave: true, +// }), +// { timeout: 2100 }, +// ); +// expect(mockedStore.getActions()).toEqual(expectedActions); +// }); +// }); diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index b34f2be8c..4778ae5be 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -33,6 +33,7 @@ const WebComponentLoader = (props) => { code, accessToken: user && user.access_token, }); + useProjectPersistence({ user: user, }); diff --git a/src/containers/WebComponentLoader.test.js b/src/containers/WebComponentLoader.test.js index 64f41c8c0..65fc6a8c0 100644 --- a/src/containers/WebComponentLoader.test.js +++ b/src/containers/WebComponentLoader.test.js @@ -6,20 +6,26 @@ import WebComponentLoader from "./WebComponentLoader"; import { setSenseHatAlwaysEnabled } from "../redux/EditorSlice"; import { setInstructions } from "../redux/InstructionsSlice"; import { useProject } from "../hooks/useProject"; +import { useProjectPersistence } from "../hooks/useProjectPersistence"; jest.mock("../hooks/useProject", () => ({ useProject: jest.fn(), })); +jest.mock("../hooks/useProjectPersistence", () => ({ + useProjectPersistence: jest.fn(), +})); + let store; const code = "print('This project is amazing')"; const identifier = "My amazing project"; const steps = [{ quiz: false, title: "Step 1", content: "Do something" }]; const instructions = { currentStepPosition: 3, project: { steps: steps } }; const authKey = "my_key"; +const user = { access_token: "my_token" }; beforeEach(() => { - localStorage.setItem(authKey, JSON.stringify({ access_token: "my_token" })); + localStorage.setItem(authKey, JSON.stringify(user)); const middlewares = []; const mockStore = configureStore(middlewares); const initialState = { @@ -49,7 +55,7 @@ beforeEach(() => { ); }); -test("Calls useProject hook with correct attribute", () => { +test("Calls useProject hook with correct attributes", () => { expect(useProject).toHaveBeenCalledWith({ projectIdentifier: identifier, code, @@ -57,6 +63,10 @@ test("Calls useProject hook with correct attribute", () => { }); }); +test("Calls useProjectPersistence hook with correct attribute", () => { + expect(useProjectPersistence).toHaveBeenCalledWith({ user }); +}); + test("Enables the SenseHat", () => { expect(store.getActions()).toEqual( expect.arrayContaining([setSenseHatAlwaysEnabled(true)]), diff --git a/src/hooks/useProject.test.js b/src/hooks/useProject.test.js index 9f3c9593c..d7e8384f1 100644 --- a/src/hooks/useProject.test.js +++ b/src/hooks/useProject.test.js @@ -4,22 +4,11 @@ import { syncProject, setProject } from "../redux/EditorSlice"; import { waitFor } from "@testing-library/react"; import { defaultPythonProject } from "../utils/defaultProjects"; -// let mockBrowserPreview = "false"; - jest.mock("react-redux", () => ({ ...jest.requireActual("react-redux"), useDispatch: () => jest.fn(), })); -// jest.mock("react-router-dom", () => ({ -// ...jest.requireActual("react-router-dom"), -// useSearchParams: () => [ -// { -// get: (key) => (key === "browserPreview" ? mockBrowserPreview : null), -// }, -// ], -// })); - const loadProject = jest.fn(); jest.mock("../redux/EditorSlice"); @@ -137,6 +126,22 @@ test("If new tab browser preview, uses cached changes", () => { expect(setProject).toHaveBeenCalledWith(cachedProject); }); +test("If no identifier or cached project, uses code attribute", () => { + const code = "print('hello world')"; + const expectedProject = { + name: "Blank project", + type: "python", + components: [{ name: "main", extension: "py", content: code }], + }; + renderHook(() => + useProject({ + code, + accessToken, + }), + ); + expect(setProject).toHaveBeenCalledWith(expectedProject); +}); + afterEach(() => { localStorage.clear(); }); diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index 952900310..cdb745fd4 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -287,6 +287,7 @@ export const EditorSlice = createSlice({ }, closeLoginToSaveModal: (state) => { state.loginToSaveModalShowing = false; + state.saveTriggered = false; }, closeNotFoundModal: (state) => { state.notFoundModalShowing = false; From fd883fa093a0d8920b7d2dd667fe1609ed1320d4 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Fri, 3 Nov 2023 14:03:50 +0000 Subject: [PATCH 06/13] put user data in the store --- src/app/WebComponentStore.js | 28 ++ src/containers/WebComponentLoader.jsx | 9 + src/hooks/useProjectPersistence.test.js | 472 ++++++++++++++++++ src/redux/WebComponentAuthSlice.js | 11 + .../reducers/webComponentAuthReducers.js | 9 + src/web-component.js | 3 +- 6 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 src/app/WebComponentStore.js create mode 100644 src/hooks/useProjectPersistence.test.js create mode 100644 src/redux/WebComponentAuthSlice.js create mode 100644 src/redux/reducers/webComponentAuthReducers.js diff --git a/src/app/WebComponentStore.js b/src/app/WebComponentStore.js new file mode 100644 index 000000000..02d751177 --- /dev/null +++ b/src/app/WebComponentStore.js @@ -0,0 +1,28 @@ +import { configureStore } from "@reduxjs/toolkit"; +import EditorReducer from "../redux/EditorSlice"; +import InstructionsReducer from "../redux/InstructionsSlice"; +import WebComponentAuthReducer from "../redux/WebComponentAuthSlice"; +// import { reducer, loadUser } from "redux-oidc"; +// import userManager from "../utils/userManager"; + +const store = configureStore({ + reducer: { + editor: EditorReducer, + instructions: InstructionsReducer, + auth: WebComponentAuthReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [ + "redux-oidc/USER_FOUND", + "redux-odic/SILENT_RENEW_ERROR", + ], + ignoredPaths: ["auth.user"], + }, + }), +}); + +// loadUser(store, userManager); + +export default store; diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index 4778ae5be..68cba7d6a 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { setInstructions } from "../redux/InstructionsSlice"; import { useProject } from "../hooks/useProject"; import { useProjectPersistence } from "../hooks/useProjectPersistence"; +import { removeUser, setUser } from "../redux/WebComponentAuthSlice"; const WebComponentLoader = (props) => { const loading = useSelector((state) => state.editor.loading); @@ -22,6 +23,14 @@ const WebComponentLoader = (props) => { const project = useSelector((state) => state.editor.project); const user = JSON.parse(localStorage.getItem(authKey)); + useEffect(() => { + if (user) { + dispatch(setUser(user)); + } else { + dispatch(removeUser()); + } + }, [user, dispatch]); + useEffect(() => { if (loading === "idle" && project.identifier) { setProjectIdentifier(project.identifier); diff --git a/src/hooks/useProjectPersistence.test.js b/src/hooks/useProjectPersistence.test.js new file mode 100644 index 000000000..13577eea8 --- /dev/null +++ b/src/hooks/useProjectPersistence.test.js @@ -0,0 +1,472 @@ +// describe("When not logged in and just loaded", () => { +// let mockedStore; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: {}, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Expires justLoaded", async () => { +// const expectedActions = [ +// setProject(defaultPythonProject), +// expireJustLoaded(), +// ]; +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When not logged in and not just loaded", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: {}, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Login prompt shown", async () => { +// await waitFor(() => expect(showLoginPrompt).toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Dispatches save prompt shown action", async () => { +// expectedActions.push(setHasShownSavePrompt()); +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When not logged in and has been prompted to login to save", () => { +// let mockedStore; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// hasShownSavePrompt: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: {}, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Login prompt shown", async () => { +// jest.runAllTimers(); +// await waitFor(() => expect(showLoginPrompt).not.toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and just loaded", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Expires justLoaded", async () => { +// expectedActions.push(expireJustLoaded()); +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and not just loaded", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Save prompt shown", async () => { +// await waitFor(() => expect(showSavePrompt).toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Dispatches save prompt shown action", async () => { +// expectedActions.push(setHasShownSavePrompt()); +// await waitFor( +// () => expect(mockedStore.getActions()).toEqual(expectedActions), +// { timeout: 2100 }, +// ); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and prompted to save", () => { +// let mockedStore; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// justLoaded: false, +// hasShownSavePrompt: true, +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Save prompt not shown again", async () => { +// jest.runAllTimers(); +// await waitFor(() => expect(showSavePrompt).not.toHaveBeenCalled(), { +// timeout: 2100, +// }); +// }); + +// test("Project saved in localStorage", async () => { +// await waitFor( +// () => +// expect(localStorage.getItem("hello-world-project")).toEqual( +// JSON.stringify(project), +// ), +// { timeout: 2100 }, +// ); +// }); +// }); + +// describe("When logged in and user does not own project and awaiting save", () => { +// let mockedStore; +// let remixProject; +// let remixAction; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// localStorage.setItem("awaitingSave", "true"); +// remixAction = { type: "REMIX_PROJECT" }; +// remixProject = jest.fn(() => remixAction); +// syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Project remixed and saved to database", async () => { +// expectedActions.push(remixAction); +// await waitFor( +// () => +// expect(remixProject).toHaveBeenCalledWith({ +// project, +// accessToken: user2.access_token, +// }), +// { timeout: 2100 }, +// ); +// expect(mockedStore.getActions()).toEqual(expectedActions); +// }); +// }); + +// describe("When logged in and project has no identifier and awaiting save", () => { +// let mockedStore; +// let saveProject; +// let saveAction; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project: { ...project, identifier: null }, +// loading: "success", +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user2, +// }, +// }; +// mockedStore = mockStore(initialState); +// localStorage.setItem("awaitingSave", "true"); +// saveAction = { type: "SAVE_PROJECT" }; +// saveProject = jest.fn(() => saveAction); +// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// afterEach(() => { +// localStorage.clear(); +// }); + +// test("Project saved to database", async () => { +// expectedActions.push(saveAction); +// await waitFor( +// () => +// expect(saveProject).toHaveBeenCalledWith({ +// project: { ...project, identifier: null }, +// accessToken: user2.access_token, +// autosave: false, +// }), +// { timeout: 2100 }, +// ); +// expect(mockedStore.getActions()).toEqual(expectedActions); +// }); +// }); + +// describe("When logged in and user owns project", () => { +// let mockedStore; +// let expectedActions; + +// beforeEach(() => { +// const middlewares = []; +// const mockStore = configureStore(middlewares); +// const initialState = { +// editor: { +// project, +// loading: "success", +// openFiles: [[]], +// focussedFileIndices: [0], +// }, +// auth: { +// user: user1, +// }, +// }; +// mockedStore = mockStore(initialState); +// render( +// +// +//
+// +//
+//
+//
, +// ); +// expectedActions = [setProject(defaultPythonProject)]; +// }); + +// test("Project autosaved to database", async () => { +// const saveAction = { type: "SAVE_PROJECT" }; +// const saveProject = jest.fn(() => saveAction); +// expectedActions.push(saveAction); +// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); +// await waitFor( +// () => +// expect(saveProject).toHaveBeenCalledWith({ +// project, +// accessToken: user1.access_token, +// autosave: true, +// }), +// { timeout: 2100 }, +// ); +// expect(mockedStore.getActions()).toEqual(expectedActions); +// }); +// }); diff --git a/src/redux/WebComponentAuthSlice.js b/src/redux/WebComponentAuthSlice.js new file mode 100644 index 000000000..43579db30 --- /dev/null +++ b/src/redux/WebComponentAuthSlice.js @@ -0,0 +1,11 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { reducers } from "./reducers/webComponentAuthReducers"; + +export const WebComponentAuthSlice = createSlice({ + name: "auth", + initialState: {}, + reducers, +}); + +export const { setUser, removeUser } = WebComponentAuthSlice.actions; +export default WebComponentAuthSlice.reducer; diff --git a/src/redux/reducers/webComponentAuthReducers.js b/src/redux/reducers/webComponentAuthReducers.js new file mode 100644 index 000000000..0005d2e2d --- /dev/null +++ b/src/redux/reducers/webComponentAuthReducers.js @@ -0,0 +1,9 @@ +export const setUser = (state, action) => { + state.user = action.payload; +}; + +export const removeUser = (state) => { + state.user = null; +}; + +export const reducers = { setUser, removeUser }; diff --git a/src/web-component.js b/src/web-component.js index ed7a349ad..926800bcb 100644 --- a/src/web-component.js +++ b/src/web-component.js @@ -4,7 +4,8 @@ import * as ReactDOMClient from "react-dom/client"; import * as Sentry from "@sentry/react"; import { BrowserTracing } from "@sentry/tracing"; import WebComponentLoader from "./containers/WebComponentLoader"; -import store from "./app/store"; +import store from "./app/WebComponentStore"; +// import store from "./app/store"; import { Provider } from "react-redux"; import "./utils/i18n"; import camelCase from "camelcase"; From 4935cc07616f1548cd5956dea793e164838fb4a7 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Fri, 3 Nov 2023 17:03:22 +0000 Subject: [PATCH 07/13] slight refactor and sorting some tests --- src/containers/ProjectComponentLoader.jsx | 13 +- src/containers/WebComponentLoader.jsx | 11 +- src/hooks/useProjectPersistence.js | 20 +- src/hooks/useProjectPersistence.test.js | 508 +++++++++------------- 4 files changed, 231 insertions(+), 321 deletions(-) diff --git a/src/containers/ProjectComponentLoader.jsx b/src/containers/ProjectComponentLoader.jsx index a79b96424..6b3db0e5c 100644 --- a/src/containers/ProjectComponentLoader.jsx +++ b/src/containers/ProjectComponentLoader.jsx @@ -24,6 +24,11 @@ const ProjectComponentLoader = (props) => { const user = useSelector((state) => state.auth.user); const accessToken = user ? user.access_token : null; const project = useSelector((state) => state.editor.project); + const justLoaded = useSelector((state) => state.editor.justLoaded); + const hasShownSavePrompt = useSelector( + (state) => state.editor.hasShownSavePrompt, + ); + const saveTriggered = useSelector((state) => state.editor.saveTriggered); const modals = useSelector((state) => state.editor.modals); const newFileModalShowing = useSelector( @@ -48,7 +53,13 @@ const ProjectComponentLoader = (props) => { useEmbeddedMode(embedded); useProject({ projectIdentifier: identifier, accessToken: accessToken }); - useProjectPersistence({ user }); + useProjectPersistence({ + user, + project, + justLoaded, + hasShownSavePrompt, + saveTriggered, + }); useEffect(() => { if (loading === "idle" && project.identifier) { diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index 68cba7d6a..09cddd157 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -22,6 +22,11 @@ const WebComponentLoader = (props) => { const [projectIdentifier, setProjectIdentifier] = useState(identifier); const project = useSelector((state) => state.editor.project); const user = JSON.parse(localStorage.getItem(authKey)); + const justLoaded = useSelector((state) => state.editor.justLoaded); + const hasShownSavePrompt = useSelector( + (state) => state.editor.hasShownSavePrompt, + ); + const saveTriggered = useSelector((state) => state.editor.saveTriggered); useEffect(() => { if (user) { @@ -44,7 +49,11 @@ const WebComponentLoader = (props) => { }); useProjectPersistence({ - user: user, + user, + project, + justLoaded, + hasShownSavePrompt, + saveTriggered, }); useEffect(() => { diff --git a/src/hooks/useProjectPersistence.js b/src/hooks/useProjectPersistence.js index cff7a4f78..5389803d7 100644 --- a/src/hooks/useProjectPersistence.js +++ b/src/hooks/useProjectPersistence.js @@ -9,14 +9,20 @@ import { } from "../redux/EditorSlice"; import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; -export const useProjectPersistence = ({ user }) => { +export const useProjectPersistence = ({ + user, + project, + justLoaded, + hasShownSavePrompt, + saveTriggered, +}) => { const dispatch = useDispatch(); - const project = useSelector((state) => state.editor.project); - const justLoaded = useSelector((state) => state.editor.justLoaded); - const hasShownSavePrompt = useSelector( - (state) => state.editor.hasShownSavePrompt, - ); - const saveTriggered = useSelector((state) => state.editor.saveTriggered); + // const project = useSelector((state) => state.editor.project); + // const justLoaded = useSelector((state) => state.editor.justLoaded); + // const hasShownSavePrompt = useSelector( + // (state) => state.editor.hasShownSavePrompt, + // ); + // const saveTriggered = useSelector((state) => state.editor.saveTriggered); useEffect(() => { if (saveTriggered) { diff --git a/src/hooks/useProjectPersistence.test.js b/src/hooks/useProjectPersistence.test.js index 13577eea8..ed5398325 100644 --- a/src/hooks/useProjectPersistence.test.js +++ b/src/hooks/useProjectPersistence.test.js @@ -1,315 +1,199 @@ -// describe("When not logged in and just loaded", () => { -// let mockedStore; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: {}, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Expires justLoaded", async () => { -// const expectedActions = [ -// setProject(defaultPythonProject), -// expireJustLoaded(), -// ]; -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When not logged in and not just loaded", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: {}, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Login prompt shown", async () => { -// await waitFor(() => expect(showLoginPrompt).toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Dispatches save prompt shown action", async () => { -// expectedActions.push(setHasShownSavePrompt()); -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When not logged in and has been prompted to login to save", () => { -// let mockedStore; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// hasShownSavePrompt: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: {}, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Login prompt shown", async () => { -// jest.runAllTimers(); -// await waitFor(() => expect(showLoginPrompt).not.toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When logged in and user does not own project and just loaded", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Expires justLoaded", async () => { -// expectedActions.push(expireJustLoaded()); -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When logged in and user does not own project and not just loaded", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Save prompt shown", async () => { -// await waitFor(() => expect(showSavePrompt).toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Dispatches save prompt shown action", async () => { -// expectedActions.push(setHasShownSavePrompt()); -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When logged in and user does not own project and prompted to save", () => { -// let mockedStore; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// hasShownSavePrompt: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Save prompt not shown again", async () => { -// jest.runAllTimers(); -// await waitFor(() => expect(showSavePrompt).not.toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); +import React from "react"; +import { render, renderHook, waitFor } from "@testing-library/react"; +import configureStore from "redux-mock-store"; +import { useProjectPersistence } from "./useProjectPersistence"; +import { Provider } from "react-redux"; +import { + expireJustLoaded, + setHasShownSavePrompt, + setProject, +} from "../redux/EditorSlice"; +import { defaultPythonProject } from "../utils/defaultProjects"; +import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; + +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: () => jest.fn(), +})); + +jest.mock("../redux/EditorSlice", () => ({ + ...jest.requireActual("../redux/EditorSlice"), + syncProject: jest.fn((_) => jest.fn()), + expireJustLoaded: jest.fn(), + setHasShownSavePrompt: jest.fn(), +})); + +jest.mock("../utils/Notifications", () => ({ + ...jest.requireActual("../utils/Notifications"), + showLoginPrompt: jest.fn(), + showSavePrompt: jest.fn(), +})); + +jest.useFakeTimers(); + +const user1 = { + access_token: "myAccessToken", + profile: { + user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", + }, +}; + +const user2 = { + access_token: "myAccessToken", + profile: { + user: "cd8a5b3d-f7bb-425e-908f-1386decd6bb1", + }, +}; + +const project = { + name: "hello world", + project_type: "python", + identifier: "hello-world-project", + components: [ + { + name: "main", + extension: "py", + content: "# hello", + }, + ], + user_id: user1.profile.user, +}; + +afterEach(() => { + localStorage.clear(); +}); + +describe("When not logged in and just loaded", () => { + beforeEach(() => { + renderHook(() => useProjectPersistence({ user: null, justLoaded: true })); + jest.runAllTimers(); + }); + + test("Expires justLoaded", async () => { + expect(expireJustLoaded).toHaveBeenCalled(); + }); +}); + +describe("When not logged in and not just loaded", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: null, + project: project, + justLoaded: false, + }), + ); + jest.runAllTimers(); + }); + + test("Login prompt shown", () => { + expect(showLoginPrompt).toHaveBeenCalled(); + }); + + test("Dispatches save prompt shown action", () => { + expect(setHasShownSavePrompt).toHaveBeenCalled(); + }); + + test("Project saved in localStorage", () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); +}); + +describe("When not logged in and has been prompted to login to save", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: null, + project: project, + justLoaded: false, + hasShownSavePrompt: true, + }), + ); + jest.runAllTimers(); + }); + + test("Login prompt shown", () => { + expect(showLoginPrompt).not.toHaveBeenCalled(); + }); + + test("Project saved in localStorage", () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); +}); + +describe("When logged in and user does not own project and just loaded", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + justLoaded: true, + }), + ); + jest.runAllTimers(); + }); + + test("Expires justLoaded", async () => { + expect(expireJustLoaded).toHaveBeenCalled(); + }); +}); + +describe("When logged in and user does not own project and not just loaded", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + justLoaded: false, + }), + ); + jest.runAllTimers(); + }); + + test("Save prompt shown", async () => { + expect(showSavePrompt).toHaveBeenCalled(); + }); + + test("Dispatches save prompt shown action", async () => { + expect(setHasShownSavePrompt).toHaveBeenCalled(); + }); + + test("Project saved in localStorage", async () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); +}); + +describe("When logged in and user does not own project and prompted to save", () => { + let mockedStore; + + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + justLoaded: false, + hasShownSavePrompt: true, + }), + ); + jest.runAllTimers(); + }); + + test("Save prompt not shown again", async () => { + expect(showSavePrompt).not.toHaveBeenCalled(); + }); + + test("Project saved in localStorage", async () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); +}); // describe("When logged in and user does not own project and awaiting save", () => { // let mockedStore; From d192d93159efeb28b000dfa80020bcd46db7b32a Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Mon, 6 Nov 2023 12:08:41 +0000 Subject: [PATCH 08/13] lots of testing --- src/components/SaveButton/SaveButton.test.js | 203 ------- src/containers/ProjectComponentLoader.test.js | 554 +---------------- src/containers/WebComponentLoader.test.js | 7 +- src/hooks/useProjectPersistence.js | 8 +- src/hooks/useProjectPersistence.test.js | 562 +++++++++--------- .../reducers/webComponentAuthReducers.test.js | 20 + 6 files changed, 322 insertions(+), 1032 deletions(-) create mode 100644 src/redux/reducers/webComponentAuthReducers.test.js diff --git a/src/components/SaveButton/SaveButton.test.js b/src/components/SaveButton/SaveButton.test.js index c288bc988..51d14bee4 100644 --- a/src/components/SaveButton/SaveButton.test.js +++ b/src/components/SaveButton/SaveButton.test.js @@ -5,18 +5,6 @@ import configureStore from "redux-mock-store"; import { triggerSave } from "../../redux/EditorSlice"; import SaveButton from "./SaveButton"; -// jest.mock("axios"); - -// jest.mock("react-router-dom", () => ({ -// ...jest.requireActual("react-router-dom"), -// useNavigate: () => jest.fn(), -// })); - -// jest.mock("../../redux/EditorSlice", () => ({ -// ...jest.requireActual("../../redux/EditorSlice"), -// syncProject: jest.fn((_) => jest.fn()), -// })); - describe("When project is loaded", () => { let store; @@ -64,194 +52,3 @@ describe("When project is not loaded", () => { expect(screen.queryByText("header.save")).not.toBeInTheDocument(); }); }); - -// describe("When logged in and user owns project", () => { -// let store; -// let saveButton; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project: project, -// loading: "success", -// }, -// auth: { -// user: user, -// }, -// }; -// store = mockStore(initialState); -// render( -// -// -// -// -// , -// ); -// saveButton = screen.queryByText("header.save"); -// }); - -// test("Clicking save dispatches saveProject with correct parameters", async () => { -// const saveAction = { type: "SAVE_PROJECT" }; -// const saveProject = jest.fn(() => saveAction); -// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); -// fireEvent.click(saveButton); -// await waitFor(() => -// expect(saveProject).toHaveBeenCalledWith({ -// project, -// accessToken: user.access_token, -// autosave: false, -// }), -// ); -// expect(store.getActions()[0]).toEqual(saveAction); -// }); -// }); - -// describe("When logged in and no project identifier", () => { -// let store; -// const project_without_id = { ...project, identifier: null }; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project: project_without_id, -// loading: "success", -// }, -// auth: { -// user: user, -// }, -// }; -// store = mockStore(initialState); -// render( -// -// -// -// -// , -// ); -// }); - -// test("Clicking save dispatches saveProject with correct parameters", async () => { -// const saveAction = { type: "SAVE_PROJECT" }; -// const saveProject = jest.fn(() => saveAction); -// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); -// const saveButton = screen.getByText("header.save"); -// fireEvent.click(saveButton); -// await waitFor(() => -// expect(saveProject).toHaveBeenCalledWith({ -// project: project_without_id, -// accessToken: user.access_token, -// autosave: false, -// }), -// ); -// expect(store.getActions()[0]).toEqual(saveAction); -// }); -// }); - -// describe("When logged in and user does not own project", () => { -// const another_project = { -// ...project, -// user_id: "5254370e-26d2-4c8a-9526-8dbafea43aa9", -// }; -// let store; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project: another_project, -// loading: "success", -// }, -// auth: { -// user: user, -// }, -// }; -// store = mockStore(initialState); -// render( -// -// -// -// -// , -// ); -// }); - -// test("Clicking save dispatches remixProject with correct parameters", async () => { -// const remixAction = { type: "REMIX_PROJECT" }; -// const remixProject = jest.fn(() => remixAction); -// syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); -// const saveButton = screen.getByText("header.save"); -// fireEvent.click(saveButton); -// await waitFor(() => -// expect(remixProject).toHaveBeenCalledWith({ -// project: another_project, -// accessToken: user.access_token, -// }), -// ); -// expect(store.getActions()[0]).toEqual(remixAction); -// }); -// }); - -// describe("When not logged in", () => { -// let store; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project: project, -// loading: "success", -// }, -// auth: { -// user: null, -// }, -// }; -// store = mockStore(initialState); -// render( -// -// -// -// -// , -// ); -// }); - -// test("Clicking save opens login to save modal", () => { -// const saveButton = screen.getByText("header.save"); -// fireEvent.click(saveButton); -// expect(store.getActions()).toEqual([showLoginToSaveModal()]); -// }); -// }); - -// describe("When no project loaded", () => { -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project: {}, -// loading: "idle", -// }, -// auth: { -// user: user, -// }, -// }; -// const store = mockStore(initialState); -// render( -// -// -// -// -// , -// ); -// }); - -// test("No save button", () => { -// expect(screen.queryByText("header.save")).not.toBeInTheDocument(); -// }); -// }); diff --git a/src/containers/ProjectComponentLoader.test.js b/src/containers/ProjectComponentLoader.test.js index 7346bd5e7..e79dd48f8 100644 --- a/src/containers/ProjectComponentLoader.test.js +++ b/src/containers/ProjectComponentLoader.test.js @@ -1,5 +1,5 @@ import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import { MemoryRouter } from "react-router-dom"; @@ -8,40 +8,18 @@ import { defaultPythonProject } from "../utils/defaultProjects"; import { matchMedia, setMedia } from "mock-match-media"; import { MOBILE_BREAKPOINT } from "../utils/mediaQueryBreakpoints"; -import { - setProject, - // expireJustLoaded, - // setHasShownSavePrompt, - // syncProject, -} from "../redux/EditorSlice"; -// import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; +import { setProject } from "../redux/EditorSlice"; import { useProjectPersistence } from "../hooks/useProjectPersistence"; -// jest.mock("axios"); - -// jest.mock("react-router-dom", () => ({ -// ...jest.requireActual("react-router-dom"), -// useNavigate: () => jest.fn(), -// })); +jest.mock("../hooks/useProjectPersistence", () => ({ + useProjectPersistence: jest.fn(), +})); jest.mock("react-responsive", () => ({ ...jest.requireActual("react-responsive"), useMediaQuery: ({ query }) => mockMediaQuery(query), })); -// jest.mock("../redux/EditorSlice", () => ({ -// ...jest.requireActual("../redux/EditorSlice"), -// syncProject: jest.fn((_) => jest.fn()), -// })); - -// jest.mock("../utils/Notifications"); - -jest.mock("../hooks/useProjectPersistence", () => ({ - useProjectPersistence: jest.fn(), -})); - -// jest.useFakeTimers(); - let mockMediaQuery = (query) => { return matchMedia(query).matches; }; @@ -55,34 +33,6 @@ const user = { }, }; -// const user1 = { -// access_token: "myAccessToken", -// profile: { -// user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", -// }, -// }; - -// const user2 = { -// access_token: "myAccessToken", -// profile: { -// user: "cd8a5b3d-f7bb-425e-908f-1386decd6bb1", -// }, -// }; - -// const project = { -// name: "hello world", -// project_type: "python", -// identifier: "hello-world-project", -// components: [ -// { -// name: "main", -// extension: "py", -// content: "# hello", -// }, -// ], -// user_id: user1.profile.user, -// }; - test("Renders loading message if loading is pending", () => { const middlewares = []; const mockStore = configureStore(middlewares); @@ -156,6 +106,9 @@ test("Calls useProjectPersistence with user when logged in", () => { const initialState = { editor: { project: {}, + hasShownSavePrompt: true, + justLoaded: false, + saveTriggered: false, }, auth: { user }, }; @@ -168,7 +121,13 @@ test("Calls useProjectPersistence with user when logged in", () => { , ); - expect(useProjectPersistence).toHaveBeenCalledWith({ user }); + expect(useProjectPersistence).toHaveBeenCalledWith({ + user, + project: {}, + hasShownSavePrompt: true, + justLoaded: false, + saveTriggered: false, + }); }); test("Calls useProjectPersistence without user when logged in", () => { @@ -177,6 +136,9 @@ test("Calls useProjectPersistence without user when logged in", () => { const initialState = { editor: { project: {}, + hasShownSavePrompt: true, + justLoaded: false, + saveTriggered: false, }, auth: {}, }; @@ -189,7 +151,12 @@ test("Calls useProjectPersistence without user when logged in", () => { , ); - expect(useProjectPersistence).toHaveBeenCalledWith({}); + expect(useProjectPersistence).toHaveBeenCalledWith({ + project: {}, + hasShownSavePrompt: true, + justLoaded: false, + saveTriggered: false, + }); }); describe("When on mobile", () => { @@ -257,476 +224,3 @@ describe("When on mobile", () => { expect(screen.queryByText("mobile.preview")).toBeInTheDocument(); }); }); - -// describe("When not logged in and just loaded", () => { -// let mockedStore; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: {}, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Expires justLoaded", async () => { -// const expectedActions = [ -// setProject(defaultPythonProject), -// expireJustLoaded(), -// ]; -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When not logged in and not just loaded", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: {}, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Login prompt shown", async () => { -// await waitFor(() => expect(showLoginPrompt).toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Dispatches save prompt shown action", async () => { -// expectedActions.push(setHasShownSavePrompt()); -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When not logged in and has been prompted to login to save", () => { -// let mockedStore; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// hasShownSavePrompt: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: {}, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Login prompt shown", async () => { -// jest.runAllTimers(); -// await waitFor(() => expect(showLoginPrompt).not.toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When logged in and user does not own project and just loaded", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Expires justLoaded", async () => { -// expectedActions.push(expireJustLoaded()); -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When logged in and user does not own project and not just loaded", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Save prompt shown", async () => { -// await waitFor(() => expect(showSavePrompt).toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Dispatches save prompt shown action", async () => { -// expectedActions.push(setHasShownSavePrompt()); -// await waitFor( -// () => expect(mockedStore.getActions()).toEqual(expectedActions), -// { timeout: 2100 }, -// ); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When logged in and user does not own project and prompted to save", () => { -// let mockedStore; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// justLoaded: false, -// hasShownSavePrompt: true, -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Save prompt not shown again", async () => { -// jest.runAllTimers(); -// await waitFor(() => expect(showSavePrompt).not.toHaveBeenCalled(), { -// timeout: 2100, -// }); -// }); - -// test("Project saved in localStorage", async () => { -// await waitFor( -// () => -// expect(localStorage.getItem("hello-world-project")).toEqual( -// JSON.stringify(project), -// ), -// { timeout: 2100 }, -// ); -// }); -// }); - -// describe("When logged in and user does not own project and awaiting save", () => { -// let mockedStore; -// let remixProject; -// let remixAction; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// localStorage.setItem("awaitingSave", "true"); -// remixAction = { type: "REMIX_PROJECT" }; -// remixProject = jest.fn(() => remixAction); -// syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Project remixed and saved to database", async () => { -// expectedActions.push(remixAction); -// await waitFor( -// () => -// expect(remixProject).toHaveBeenCalledWith({ -// project, -// accessToken: user2.access_token, -// }), -// { timeout: 2100 }, -// ); -// expect(mockedStore.getActions()).toEqual(expectedActions); -// }); -// }); - -// describe("When logged in and project has no identifier and awaiting save", () => { -// let mockedStore; -// let saveProject; -// let saveAction; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project: { ...project, identifier: null }, -// loading: "success", -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// localStorage.setItem("awaitingSave", "true"); -// saveAction = { type: "SAVE_PROJECT" }; -// saveProject = jest.fn(() => saveAction); -// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Project saved to database", async () => { -// expectedActions.push(saveAction); -// await waitFor( -// () => -// expect(saveProject).toHaveBeenCalledWith({ -// project: { ...project, identifier: null }, -// accessToken: user2.access_token, -// autosave: false, -// }), -// { timeout: 2100 }, -// ); -// expect(mockedStore.getActions()).toEqual(expectedActions); -// }); -// }); - -// describe("When logged in and user owns project", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user1, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// test("Project autosaved to database", async () => { -// const saveAction = { type: "SAVE_PROJECT" }; -// const saveProject = jest.fn(() => saveAction); -// expectedActions.push(saveAction); -// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); -// await waitFor( -// () => -// expect(saveProject).toHaveBeenCalledWith({ -// project, -// accessToken: user1.access_token, -// autosave: true, -// }), -// { timeout: 2100 }, -// ); -// expect(mockedStore.getActions()).toEqual(expectedActions); -// }); -// }); diff --git a/src/containers/WebComponentLoader.test.js b/src/containers/WebComponentLoader.test.js index 65fc6a8c0..bc6e23866 100644 --- a/src/containers/WebComponentLoader.test.js +++ b/src/containers/WebComponentLoader.test.js @@ -63,8 +63,11 @@ test("Calls useProject hook with correct attributes", () => { }); }); -test("Calls useProjectPersistence hook with correct attribute", () => { - expect(useProjectPersistence).toHaveBeenCalledWith({ user }); +test("Calls useProjectPersistence hook with correct attributes", () => { + expect(useProjectPersistence).toHaveBeenCalledWith({ + user, + project: { components: [] }, + }); }); test("Enables the SenseHat", () => { diff --git a/src/hooks/useProjectPersistence.js b/src/hooks/useProjectPersistence.js index 5389803d7..c26a3136d 100644 --- a/src/hooks/useProjectPersistence.js +++ b/src/hooks/useProjectPersistence.js @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { isOwner } from "../utils/projectHelpers"; import { expireJustLoaded, @@ -17,12 +17,6 @@ export const useProjectPersistence = ({ saveTriggered, }) => { const dispatch = useDispatch(); - // const project = useSelector((state) => state.editor.project); - // const justLoaded = useSelector((state) => state.editor.justLoaded); - // const hasShownSavePrompt = useSelector( - // (state) => state.editor.hasShownSavePrompt, - // ); - // const saveTriggered = useSelector((state) => state.editor.saveTriggered); useEffect(() => { if (saveTriggered) { diff --git a/src/hooks/useProjectPersistence.test.js b/src/hooks/useProjectPersistence.test.js index ed5398325..b56b74b26 100644 --- a/src/hooks/useProjectPersistence.test.js +++ b/src/hooks/useProjectPersistence.test.js @@ -1,14 +1,11 @@ -import React from "react"; -import { render, renderHook, waitFor } from "@testing-library/react"; -import configureStore from "redux-mock-store"; +import { renderHook } from "@testing-library/react"; import { useProjectPersistence } from "./useProjectPersistence"; -import { Provider } from "react-redux"; import { expireJustLoaded, setHasShownSavePrompt, - setProject, + showLoginToSaveModal, + syncProject, } from "../redux/EditorSlice"; -import { defaultPythonProject } from "../utils/defaultProjects"; import { showLoginPrompt, showSavePrompt } from "../utils/Notifications"; jest.mock("react-redux", () => ({ @@ -21,25 +18,27 @@ jest.mock("../redux/EditorSlice", () => ({ syncProject: jest.fn((_) => jest.fn()), expireJustLoaded: jest.fn(), setHasShownSavePrompt: jest.fn(), + showLoginToSaveModal: jest.fn(), })); -jest.mock("../utils/Notifications", () => ({ - ...jest.requireActual("../utils/Notifications"), - showLoginPrompt: jest.fn(), - showSavePrompt: jest.fn(), -})); +jest.mock("../utils/Notifications"); + +const remixAction = { type: "REMIX_PROJECT" }; +const remixProject = jest.fn(() => remixAction); +const saveAction = { type: "SAVE_PROJECT" }; +const saveProject = jest.fn(() => saveAction); jest.useFakeTimers(); const user1 = { - access_token: "myAccessToken", + access_token: "myAccessToken1", profile: { user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", }, }; const user2 = { - access_token: "myAccessToken", + access_token: "myAccessToken2", profile: { user: "cd8a5b3d-f7bb-425e-908f-1386decd6bb1", }, @@ -63,294 +62,277 @@ afterEach(() => { localStorage.clear(); }); -describe("When not logged in and just loaded", () => { - beforeEach(() => { - renderHook(() => useProjectPersistence({ user: null, justLoaded: true })); - jest.runAllTimers(); - }); - - test("Expires justLoaded", async () => { - expect(expireJustLoaded).toHaveBeenCalled(); - }); -}); - -describe("When not logged in and not just loaded", () => { - beforeEach(() => { - renderHook(() => - useProjectPersistence({ - user: null, - project: project, - justLoaded: false, - }), - ); - jest.runAllTimers(); - }); - - test("Login prompt shown", () => { - expect(showLoginPrompt).toHaveBeenCalled(); - }); - - test("Dispatches save prompt shown action", () => { - expect(setHasShownSavePrompt).toHaveBeenCalled(); - }); - - test("Project saved in localStorage", () => { - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ); - }); -}); - -describe("When not logged in and has been prompted to login to save", () => { - beforeEach(() => { - renderHook(() => - useProjectPersistence({ - user: null, - project: project, - justLoaded: false, - hasShownSavePrompt: true, - }), - ); - jest.runAllTimers(); - }); +describe("When not logged in", () => { + describe("When just loaded", () => { + beforeEach(() => { + renderHook(() => useProjectPersistence({ user: null, justLoaded: true })); + jest.runAllTimers(); + }); - test("Login prompt shown", () => { - expect(showLoginPrompt).not.toHaveBeenCalled(); + test("Expires justLoaded", () => { + expect(expireJustLoaded).toHaveBeenCalled(); + }); }); - test("Project saved in localStorage", () => { - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ); + describe("When not just loaded", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: null, + project: project, + justLoaded: false, + }), + ); + jest.runAllTimers(); + }); + + test("Login prompt shown", () => { + expect(showLoginPrompt).toHaveBeenCalled(); + }); + + test("Dispatches save prompt shown action", () => { + expect(setHasShownSavePrompt).toHaveBeenCalled(); + }); + + test("Project saved in localStorage", () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); }); -}); -describe("When logged in and user does not own project and just loaded", () => { - beforeEach(() => { - renderHook(() => - useProjectPersistence({ - user: user2, - project: project, - justLoaded: true, - }), - ); - jest.runAllTimers(); + describe("When has been prompted to login to save", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: null, + project: project, + justLoaded: false, + hasShownSavePrompt: true, + }), + ); + jest.runAllTimers(); + }); + + test("Login prompt shown", () => { + expect(showLoginPrompt).not.toHaveBeenCalled(); + }); + + test("Project saved in localStorage", () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); }); - test("Expires justLoaded", async () => { - expect(expireJustLoaded).toHaveBeenCalled(); + describe("When save has been triggered", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: null, + project: project, + saveTriggered: true, + }), + ); + jest.runAllTimers(); + }); + + test("Login to save modal is shown", () => { + expect(showLoginToSaveModal).toHaveBeenCalled(); + }); }); }); -describe("When logged in and user does not own project and not just loaded", () => { - beforeEach(() => { - renderHook(() => - useProjectPersistence({ - user: user2, - project: project, - justLoaded: false, - }), - ); - jest.runAllTimers(); - }); - - test("Save prompt shown", async () => { - expect(showSavePrompt).toHaveBeenCalled(); - }); - - test("Dispatches save prompt shown action", async () => { - expect(setHasShownSavePrompt).toHaveBeenCalled(); +describe("When logged in", () => { + describe("When user does not own project", () => { + describe("When just loaded", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + justLoaded: true, + }), + ); + jest.runAllTimers(); + }); + + test("Expires justLoaded", () => { + expect(expireJustLoaded).toHaveBeenCalled(); + }); + }); + + describe("When not just loaded", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + justLoaded: false, + }), + ); + jest.runAllTimers(); + }); + + test("Save prompt shown", () => { + expect(showSavePrompt).toHaveBeenCalled(); + }); + + test("Dispatches save prompt shown action", () => { + expect(setHasShownSavePrompt).toHaveBeenCalled(); + }); + + test("Project saved in localStorage", () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); + }); + + describe("When user has been prompted to save", () => { + beforeEach(() => { + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + justLoaded: false, + hasShownSavePrompt: true, + }), + ); + jest.runAllTimers(); + }); + + test("Save prompt not shown again", () => { + expect(showSavePrompt).not.toHaveBeenCalled(); + }); + + test("Project saved in localStorage", () => { + expect(localStorage.getItem("hello-world-project")).toEqual( + JSON.stringify(project), + ); + }); + }); + + describe("When project has identifier and awaiting save", () => { + beforeEach(() => { + localStorage.setItem("awaitingSave", "true"); + syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + }), + ); + jest.runAllTimers(); + }); + + test("Project remixed and saved to database", () => { + expect(remixProject).toHaveBeenCalledWith({ + project, + accessToken: user2.access_token, + }); + }); + }); + + describe("When project has identifier and save triggered", () => { + beforeEach(() => { + syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); + renderHook(() => + useProjectPersistence({ + user: user2, + project: project, + saveTriggered: true, + }), + ); + jest.runAllTimers(); + }); + test("Clicking save dispatches remixProject with correct parameters", async () => { + expect(remixProject).toHaveBeenCalledWith({ + project: project, + accessToken: user2.access_token, + }); + }); + }); + + describe("When project has no identifier and awaiting save", () => { + beforeEach(() => { + localStorage.setItem("awaitingSave", "true"); + syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); + renderHook(() => + useProjectPersistence({ + user: user2, + project: { ...project, identifier: null }, + }), + ); + jest.runAllTimers(); + }); + + test("Project saved to database", () => { + expect(saveProject).toHaveBeenCalledWith({ + project: { ...project, identifier: null }, + accessToken: user2.access_token, + autosave: false, + }); + }); + }); + + describe("When project has no identifier and save triggered", () => { + beforeEach(() => { + syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); + renderHook(() => + useProjectPersistence({ + user: user2, + project: { ...project, identifier: null }, + saveTriggered: true, + }), + ); + jest.runAllTimers(); + }); + test("Save project is called with the correct parameters", () => { + expect(saveProject).toHaveBeenCalledWith({ + project: { ...project, identifier: null }, + accessToken: user2.access_token, + autosave: false, + }); + }); + }); }); - test("Project saved in localStorage", async () => { - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ); + describe("When user owns project", () => { + beforeEach(() => { + syncProject.mockImplementation(jest.fn((_) => saveProject)); + }); + + test("Project autosaved to database if save not triggered", async () => { + renderHook(() => + useProjectPersistence({ + user: user1, + project: project, + saveTriggered: false, + }), + ); + jest.runAllTimers(); + expect(saveProject).toHaveBeenCalledWith({ + project, + accessToken: user1.access_token, + autosave: true, + }); + }); + + test("Saves project to database if save triggered", async () => { + renderHook(() => + useProjectPersistence({ + user: user1, + project: project, + saveTriggered: true, + }), + ); + jest.runAllTimers(); + expect(saveProject).toHaveBeenCalledWith({ + project, + accessToken: user1.access_token, + autosave: false, + }); + }); }); }); - -describe("When logged in and user does not own project and prompted to save", () => { - let mockedStore; - - beforeEach(() => { - renderHook(() => - useProjectPersistence({ - user: user2, - project: project, - justLoaded: false, - hasShownSavePrompt: true, - }), - ); - jest.runAllTimers(); - }); - - test("Save prompt not shown again", async () => { - expect(showSavePrompt).not.toHaveBeenCalled(); - }); - - test("Project saved in localStorage", async () => { - expect(localStorage.getItem("hello-world-project")).toEqual( - JSON.stringify(project), - ); - }); -}); - -// describe("When logged in and user does not own project and awaiting save", () => { -// let mockedStore; -// let remixProject; -// let remixAction; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// localStorage.setItem("awaitingSave", "true"); -// remixAction = { type: "REMIX_PROJECT" }; -// remixProject = jest.fn(() => remixAction); -// syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Project remixed and saved to database", async () => { -// expectedActions.push(remixAction); -// await waitFor( -// () => -// expect(remixProject).toHaveBeenCalledWith({ -// project, -// accessToken: user2.access_token, -// }), -// { timeout: 2100 }, -// ); -// expect(mockedStore.getActions()).toEqual(expectedActions); -// }); -// }); - -// describe("When logged in and project has no identifier and awaiting save", () => { -// let mockedStore; -// let saveProject; -// let saveAction; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project: { ...project, identifier: null }, -// loading: "success", -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user2, -// }, -// }; -// mockedStore = mockStore(initialState); -// localStorage.setItem("awaitingSave", "true"); -// saveAction = { type: "SAVE_PROJECT" }; -// saveProject = jest.fn(() => saveAction); -// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// afterEach(() => { -// localStorage.clear(); -// }); - -// test("Project saved to database", async () => { -// expectedActions.push(saveAction); -// await waitFor( -// () => -// expect(saveProject).toHaveBeenCalledWith({ -// project: { ...project, identifier: null }, -// accessToken: user2.access_token, -// autosave: false, -// }), -// { timeout: 2100 }, -// ); -// expect(mockedStore.getActions()).toEqual(expectedActions); -// }); -// }); - -// describe("When logged in and user owns project", () => { -// let mockedStore; -// let expectedActions; - -// beforeEach(() => { -// const middlewares = []; -// const mockStore = configureStore(middlewares); -// const initialState = { -// editor: { -// project, -// loading: "success", -// openFiles: [[]], -// focussedFileIndices: [0], -// }, -// auth: { -// user: user1, -// }, -// }; -// mockedStore = mockStore(initialState); -// render( -// -// -//
-// -//
-//
-//
, -// ); -// expectedActions = [setProject(defaultPythonProject)]; -// }); - -// test("Project autosaved to database", async () => { -// const saveAction = { type: "SAVE_PROJECT" }; -// const saveProject = jest.fn(() => saveAction); -// expectedActions.push(saveAction); -// syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); -// await waitFor( -// () => -// expect(saveProject).toHaveBeenCalledWith({ -// project, -// accessToken: user1.access_token, -// autosave: true, -// }), -// { timeout: 2100 }, -// ); -// expect(mockedStore.getActions()).toEqual(expectedActions); -// }); -// }); diff --git a/src/redux/reducers/webComponentAuthReducers.test.js b/src/redux/reducers/webComponentAuthReducers.test.js new file mode 100644 index 000000000..3a06726a7 --- /dev/null +++ b/src/redux/reducers/webComponentAuthReducers.test.js @@ -0,0 +1,20 @@ +import { setUser, removeUser } from "./webComponentAuthReducers"; + +const user = { + access_token: "my_token", +}; + +test("Sets user correctly", () => { + let state = {}; + const expectedState = { user }; + setUser(state, { payload: user }); + expect(state).toEqual(expectedState); +}); + +test("Removes user correctly", () => { + let state = { user }; + const expectedState = { user: null }; + + removeUser(state); + expect(state).toEqual(expectedState); +}); From 404b8aad5f485a9046a18ca41ca5892db6679ceb Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Mon, 6 Nov 2023 12:10:52 +0000 Subject: [PATCH 09/13] changelog and reverting docker compose --- CHANGELOG.md | 2 ++ docker-compose.yml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c178b93..f1e2e86a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Web component tests (#709, #710) - `instructions` attribute for the web component (#712) - Instructions slice to store data passed from the Projects site (#712) +- Adding auth to the web component (#728) +- Allow web component to load, save and remix projects (#728) ### Changed diff --git a/docker-compose.yml b/docker-compose.yml index b9f23e235..b0f9148a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,13 +17,13 @@ services: <<: *x-app command: yarn start ports: - - "3010:3000" + - "3000:3000" container_name: react-ui react-ui-wc: <<: *x-app command: yarn start:wc ports: - - "3011:3001" + - "3001:3001" container_name: react-ui-wc secrets: npmrc: From 5ca51407e3a89ea3deb6fe3baafb5751f95ccb4b Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Mon, 6 Nov 2023 12:13:18 +0000 Subject: [PATCH 10/13] tidying --- src/app/WebComponentStore.js | 4 ---- src/web-component.js | 1 - 2 files changed, 5 deletions(-) diff --git a/src/app/WebComponentStore.js b/src/app/WebComponentStore.js index 02d751177..bf2bde912 100644 --- a/src/app/WebComponentStore.js +++ b/src/app/WebComponentStore.js @@ -2,8 +2,6 @@ import { configureStore } from "@reduxjs/toolkit"; import EditorReducer from "../redux/EditorSlice"; import InstructionsReducer from "../redux/InstructionsSlice"; import WebComponentAuthReducer from "../redux/WebComponentAuthSlice"; -// import { reducer, loadUser } from "redux-oidc"; -// import userManager from "../utils/userManager"; const store = configureStore({ reducer: { @@ -23,6 +21,4 @@ const store = configureStore({ }), }); -// loadUser(store, userManager); - export default store; diff --git a/src/web-component.js b/src/web-component.js index 926800bcb..0c351aadd 100644 --- a/src/web-component.js +++ b/src/web-component.js @@ -5,7 +5,6 @@ import * as Sentry from "@sentry/react"; import { BrowserTracing } from "@sentry/tracing"; import WebComponentLoader from "./containers/WebComponentLoader"; import store from "./app/WebComponentStore"; -// import store from "./app/store"; import { Provider } from "react-redux"; import "./utils/i18n"; import camelCase from "camelcase"; From 3f8a4dabe4c802512c518dd134eb10b1f6954bd6 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Fri, 10 Nov 2023 12:57:07 +0000 Subject: [PATCH 11/13] trying to fix weird network error --- src/components/Modals/RenameProjectModal.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Modals/RenameProjectModal.test.js b/src/components/Modals/RenameProjectModal.test.js index 86bc364c3..87ab62401 100644 --- a/src/components/Modals/RenameProjectModal.test.js +++ b/src/components/Modals/RenameProjectModal.test.js @@ -10,6 +10,7 @@ import { } from "./RenameProjectModal"; import { showRenamedMessage } from "../../utils/Notifications"; +jest.mock("axios"); jest.mock("../../utils/Notifications"); describe("RenameProjectModal", () => { From 54410510af098e4375d693a86565a8597da703a8 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Fri, 10 Nov 2023 13:09:03 +0000 Subject: [PATCH 12/13] remove axios mock --- src/components/Modals/RenameProjectModal.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Modals/RenameProjectModal.test.js b/src/components/Modals/RenameProjectModal.test.js index 87ab62401..86bc364c3 100644 --- a/src/components/Modals/RenameProjectModal.test.js +++ b/src/components/Modals/RenameProjectModal.test.js @@ -10,7 +10,6 @@ import { } from "./RenameProjectModal"; import { showRenamedMessage } from "../../utils/Notifications"; -jest.mock("axios"); jest.mock("../../utils/Notifications"); describe("RenameProjectModal", () => { From 946f981e8bc733f4624e58feff5c6ec5b58bd62e Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 10 Nov 2023 13:13:12 +0000 Subject: [PATCH 13/13] added workerThreads to CI test command --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d9295dd8b..c972ff7d4 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -55,7 +55,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run tests - run: yarn run test --coverage --maxWorkers=4 --reporters=default --reporters=jest-junit --reporters=jest-github-actions-reporter + run: yarn run test --coverage --maxWorkers=4 --workerThreads=true --reporters=default --reporters=jest-junit --reporters=jest-github-actions-reporter env: JEST_JUNIT_OUTPUT_DIR: ./coverage/