diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f863e6244..69cf95aa5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -64,10 +64,20 @@ "github.remotehub", "circleci.circleci", "stylelint.vscode-stylelint", - "Orta.vscode-jest" + "Orta.vscode-jest", + "christian-kohler.path-intellisense", + "esbenp.prettier-vscode", + "avraammavridis.vsc-react-documentation", + "ofhumanbondage.react-proptypes-intellisense", + "syler.sass-indented", + "codezombiech.gitignore" ], "settings": { - "terminal.integrated.defaultProfile.linux": "zsh" + "terminal.integrated.defaultProfile.linux": "zsh", + "jest.autoRun": { + "watch": false, + "onSave": "test-src-file" + } } } }, diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..d7638731d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.md] +max_line_length = 0 +trim_trailing_whitespace = false + +[COMMIT_EDITMSG] +max_line_length = 0 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..530cfa5e8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,47 @@ +{ + "root": true, + "extends": [ + "react-app", + "react-app/jest", + "plugin:prettier/recommended" + ], + "plugins": [ + "prettier", + "react", + "jsx-a11y", + "jest" + ], + "settings": { + "react": { + "version": "18.1.0" + } + }, + "parser": "babel-eslint", + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "modules": true, + "jsx": true + } + }, + "env": { + "amd": true, + "browser": true, + "es6": true, + "jquery": false, + "node": true + }, + "rules": { + "prettier/prettier": [ + "error", + { + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "all", + "semi": true, + "bracketSpacing": true, + "arrowParens": "always" + } + ] + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index f479d28d5..ffa0376ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added ### Changed +- Move eslint config to `.eslintrc.json` with prettier support +- Add `.editorconfig` ### Fixed diff --git a/package.json b/package.json index d9707aa92..f15d75b8e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "codemirror": "^6.0.1", "container-query-polyfill": "^1.0.2", "date-fns": "^2.29.3", + "eslint-config-prettier": "^8.8.0", "file-saver": "^2.0.5", "fs-extra": "^9.0.1", "graphql": "^16.6.0", @@ -76,7 +77,9 @@ "build": "node scripts/build.js", "build:dev": "yarn run build-storybook && node scripts/build.js", "build-storybook": "cd ./storybook && yarn install && yarn run build-storybook -- -o ../public/storybook --loglevel warn", - "lint": "eslint src/. --ext .js", + "lint": "eslint src/**/*.{js,jsx,json}", + "lint:fix": "eslint --fix src/**/*.{js,jsx,json}", + "format": "prettier --write src/**/*.{js,jsx,css,md,json,scss} --config ./.prettierrc", "stylelint": "stylelint --syntax=scss src/**/*.scss", "test": "node scripts/test.js --transformIgnorePatterns 'node_modules/(?!three)/'", "test:integration": "cd e2e; docker compose up --exit-code-from cypress", @@ -86,12 +89,6 @@ "build:wc": "NODE_ENV=production BABEL_ENV=production webpack build -c ./webpack.component.config.js", "heroku-postbuild": "export PUBLIC_URL='' && yarn build && yarn build:wc" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, "browserslist": { "production": [ ">0.2%", @@ -135,6 +132,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.21.5", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "^3.9.2", @@ -160,6 +158,7 @@ "postcss-normalize": "8.0.1", "postcss-preset-env": "6.7.0", "postcss-safe-parser": "5.0.2", + "prettier": "^2.8.8", "react-dev-utils": "^11.0.3", "react-test-renderer": "^17.0.2", "redux-mock-store": "^1.5.4", diff --git a/src/App.test.js b/src/App.test.js index 9283d3432..2794faf73 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,21 +1,21 @@ -import App from './App'; -import { Provider } from 'react-redux' -import React from 'react'; -import { act, render, screen } from '@testing-library/react'; -import { Cookies, CookiesProvider } from 'react-cookie'; -import configureStore from 'redux-mock-store'; - -jest.mock('./utils/Notifications') -jest.mock('./components/Editor/EditorSlice', () => { - const actual = jest.requireActual('./components/Editor/EditorSlice') +import App from "./App"; +import { Provider } from "react-redux"; +import React from "react"; +import { act, render, screen } from "@testing-library/react"; +import { Cookies, CookiesProvider } from "react-cookie"; +import configureStore from "redux-mock-store"; + +jest.mock("./utils/Notifications"); +jest.mock("./components/Editor/EditorSlice", () => { + const actual = jest.requireActual("./components/Editor/EditorSlice"); return { ...actual, - saveProject: jest.fn() - } -}) + saveProject: jest.fn(), + }; +}); -describe('Browser prefers light mode', () => { - let store +describe("Browser prefers light mode", () => { + let store; let cookies; beforeEach(() => { @@ -28,49 +28,49 @@ describe('Browser prefers light mode', () => { addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), - }) + }); - cookies = new Cookies() - const middlewares = [] - const mockStore = configureStore(middlewares) + cookies = new Cookies(); + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: {}, - auth: {} - } + auth: {}, + }; store = mockStore(initialState); - }) + }); - test('Light mode class name added if no cookie', () => { + test("Light mode class name added if no cookie", () => { const appContainer = render( - - ) - expect(appContainer.container.querySelector('#app')).toHaveClass("--light") - }) + , + ); + expect(appContainer.container.querySelector("#app")).toHaveClass("--light"); + }); - test('Dark mode class name added if cookie specifies dark theme', () => { - cookies.set('theme', 'dark') + test("Dark mode class name added if cookie specifies dark theme", () => { + cookies.set("theme", "dark"); const appContainer = render( - - ) - expect(appContainer.container.querySelector('#app')).toHaveClass("--dark") - }) + , + ); + expect(appContainer.container.querySelector("#app")).toHaveClass("--dark"); + }); afterEach(() => { - act(() => cookies.remove('theme')) - }) -}) + act(() => cookies.remove("theme")); + }); +}); -describe('Browser prefers dark mode', () => { +describe("Browser prefers dark mode", () => { let cookies; - let store + let store; beforeEach(() => { window.matchMedia = (query) => ({ @@ -82,84 +82,84 @@ describe('Browser prefers dark mode', () => { addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), - }) + }); cookies = new Cookies(); - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: {}, - auth: {} - } + auth: {}, + }; store = mockStore(initialState); - }) + }); - test('Dark mode class name added if no cookie', () => { + test("Dark mode class name added if no cookie", () => { const appContainer = render( - - ) - expect(appContainer.container.querySelector('#app')).toHaveClass("--dark") - }) + , + ); + expect(appContainer.container.querySelector("#app")).toHaveClass("--dark"); + }); - test('Light mode class name added if cookie specifies light theme', () => { - cookies.set('theme', 'light') + test("Light mode class name added if cookie specifies light theme", () => { + cookies.set("theme", "light"); const appContainer = render( - - ) - expect(appContainer.container.querySelector('#app')).toHaveClass("--light") - }) + , + ); + expect(appContainer.container.querySelector("#app")).toHaveClass("--light"); + }); afterEach(() => { - act(() => cookies.remove('theme')) - }) -}) + act(() => cookies.remove("theme")); + }); +}); -describe('Beta banner', () => { - let cookies - let store +describe("Beta banner", () => { + let cookies; + let store; beforeEach(() => { - cookies = new Cookies() - const middlewares = [] - const mockStore = configureStore(middlewares) + cookies = new Cookies(); + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: {}, - auth: {} - } + auth: {}, + }; store = mockStore(initialState); - }) + }); - test('Renders beta banner if betaBannerDismissed cookie not set', () => { + test("Renders beta banner if betaBannerDismissed cookie not set", () => { render( - - ) - expect(screen.queryByText('betaBanner.message')).toBeInTheDocument() - }) + , + ); + expect(screen.queryByText("betaBanner.message")).toBeInTheDocument(); + }); - test('Does not render beta banner if betaBannerDismissed cookie is true', () => { - cookies.set('betaBannerDismissed', 'true') + test("Does not render beta banner if betaBannerDismissed cookie is true", () => { + cookies.set("betaBannerDismissed", "true"); render( - - ) - expect(screen.queryByText('betaBanner.message')).not.toBeInTheDocument() - }) + , + ); + expect(screen.queryByText("betaBanner.message")).not.toBeInTheDocument(); + }); afterEach(() => { - act(() => cookies.remove('betaBannerDismissed')) - }) -}) + act(() => cookies.remove("betaBannerDismissed")); + }); +}); diff --git a/src/Icons.js b/src/Icons.js index 7279eaf6c..d22681f41 100644 --- a/src/Icons.js +++ b/src/Icons.js @@ -432,7 +432,7 @@ export const RunIcon = () => { fill="none" xmlns="http://www.w3.org/2000/svg" > - + ); }; @@ -480,7 +480,7 @@ export const StopIcon = () => { fill="none" xmlns="http://www.w3.org/2000/svg" > - + ); }; diff --git a/src/app/ComponentStore.js b/src/app/ComponentStore.js index d7c8ed823..f6bd2325f 100644 --- a/src/app/ComponentStore.js +++ b/src/app/ComponentStore.js @@ -1,10 +1,10 @@ -import { configureStore } from '@reduxjs/toolkit' -import EditorReducer from '../components/Editor/EditorSlice' +import { configureStore } from "@reduxjs/toolkit"; +import EditorReducer from "../components/Editor/EditorSlice"; const ComponentStore = configureStore({ reducer: { editor: EditorReducer, }, -}) +}); export default ComponentStore; diff --git a/src/app/store.js b/src/app/store.js index d91ee01f4..55cffeec9 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,7 +1,7 @@ -import { configureStore } from '@reduxjs/toolkit' -import EditorReducer from '../components/Editor/EditorSlice' -import { reducer, loadUser } from 'redux-oidc' -import userManager from '../utils/userManager' +import { configureStore } from "@reduxjs/toolkit"; +import EditorReducer from "../components/Editor/EditorSlice"; +import { reducer, loadUser } from "redux-oidc"; +import userManager from "../utils/userManager"; const store = configureStore({ reducer: { @@ -11,11 +11,14 @@ const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { - ignoredActions: ['redux-oidc/USER_FOUND', 'redux-odic/SILENT_RENEW_ERROR'], - ignoredPaths: ['auth.user'], - }, - }), -}) + ignoredActions: [ + "redux-oidc/USER_FOUND", + "redux-odic/SILENT_RENEW_ERROR", + ], + ignoredPaths: ["auth.user"], + }, + }), +}); loadUser(store, userManager); diff --git a/src/components/AppRoutes.js b/src/components/AppRoutes.js index 2c41de761..dfa671a85 100644 --- a/src/components/AppRoutes.js +++ b/src/components/AppRoutes.js @@ -1,40 +1,40 @@ -import { React } from 'react' -import { Route, Routes, Navigate, useParams } from 'react-router-dom' +import { React } from "react"; +import { Route, Routes, Navigate, useParams } from "react-router-dom"; import * as Sentry from "@sentry/react"; -import ProjectComponentLoader from './Editor/ProjectComponentLoader/ProjectComponentLoader' -import ProjectIndex from './ProjectIndex/ProjectIndex' -import EmbeddedViewer from './EmbeddedViewer/EmbeddedViewer' -import Callback from './Callback' -import SilentRenew from './SilentRenew' -import LocaleLayout from './LocaleLayout/LocaleLayout'; +import ProjectComponentLoader from "./Editor/ProjectComponentLoader/ProjectComponentLoader"; +import ProjectIndex from "./ProjectIndex/ProjectIndex"; +import EmbeddedViewer from "./EmbeddedViewer/EmbeddedViewer"; +import Callback from "./Callback"; +import SilentRenew from "./SilentRenew"; +import LocaleLayout from "./LocaleLayout/LocaleLayout"; -const projectLinkRedirects = ['/null/projects/:identifier', '/projects/:identifier'] -const localeRedirects = ['/', '/projects'] +const projectLinkRedirects = [ + "/null/projects/:identifier", + "/projects/:identifier", +]; +const localeRedirects = ["/", "/projects"]; const ProjectsRedirect = () => { const { identifier } = useParams(); - return -} + return ; +}; -const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes) +const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); const AppRoutes = () => ( - } - /> + } /> - } - /> - }> + } /> + }> } /> } /> - } /> - } /> + } + /> + } /> ( {/* Redirects will be moved into a cloudflare worker. This is just interim */} - { projectLinkRedirects.map(link => { - return } /> - }) } - - { localeRedirects.map(link => { - return } /> - }) } + {projectLinkRedirects.map((link) => { + return } />; + })} + + {localeRedirects.map((link) => { + return ( + } + /> + ); + })} -) +); -export default AppRoutes +export default AppRoutes; diff --git a/src/components/AstroPiModel/AstroPiControls/AstroPiControls.js b/src/components/AstroPiModel/AstroPiControls/AstroPiControls.js index 02dff4224..3ca189fc4 100644 --- a/src/components/AstroPiModel/AstroPiControls/AstroPiControls.js +++ b/src/components/AstroPiModel/AstroPiControls/AstroPiControls.js @@ -1,34 +1,65 @@ -import React from 'react'; -import Input from './Input'; -import MotionInput from './MotionInput'; -import SliderInput from './SliderInput'; -import '../AstroPiModel.scss'; -import Stopwatch from './Stopwatch'; -import { HumidityIcon, PressureIcon, TemperatureIcon } from '../../../Icons'; -import { useTranslation } from 'react-i18next'; +import React from "react"; +import Input from "./Input"; +import MotionInput from "./MotionInput"; +import SliderInput from "./SliderInput"; +import "../AstroPiModel.scss"; +import Stopwatch from "./Stopwatch"; +import { HumidityIcon, PressureIcon, TemperatureIcon } from "../../../Icons"; +import { useTranslation } from "react-i18next"; const AstroPiControls = (props) => { - const {temperature, pressure, humidity, colour, motion} = props - const { t } = useTranslation() + const { temperature, pressure, humidity, colour, motion } = props; + const { t } = useTranslation(); return ( -
-

{t('output.senseHat.controls.name')}

+
+

+ {t("output.senseHat.controls.name")} +

-
- - - +
+ + +
- +
- +
- ) + ); }; -export default AstroPiControls +export default AstroPiControls; diff --git a/src/components/AstroPiModel/AstroPiControls/AstroPiControls.test.js b/src/components/AstroPiModel/AstroPiControls/AstroPiControls.test.js index 12c23ea6c..36a4e94c0 100644 --- a/src/components/AstroPiModel/AstroPiControls/AstroPiControls.test.js +++ b/src/components/AstroPiModel/AstroPiControls/AstroPiControls.test.js @@ -1,64 +1,73 @@ import React from "react"; -import { render } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; +import { render } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; import AstroPiControls from "./AstroPiControls"; -import Sk from 'skulpt'; +import Sk from "skulpt"; let container; let store; beforeEach(() => { - Sk.sense_hat={ + Sk.sense_hat = { mz_criteria: {}, rtimu: { - temperature: [0,0], - pressure: [0,0], - humidity: [0,0] - } - } - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - codeRunTriggered: false - }, - } - store = mockStore(initialState); - container = render() -}) + temperature: [0, 0], + pressure: [0, 0], + humidity: [0, 0], + }, + }; + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + codeRunTriggered: false, + }, + }; + store = mockStore(initialState); + container = render( + + + , + ); +}); test("Renders temperature with units", () => { - expect(container.queryByText("65°C")).not.toBeNull() -}) + expect(container.queryByText("65°C")).not.toBeNull(); +}); test("Renders pressure with units", () => { - expect(container.getByText("456hPa")).not.toBeNull() -}) + expect(container.getByText("456hPa")).not.toBeNull(); +}); test("Renders humidity with units", () => { - expect(container.getByText("37%")).not.toBeNull() -}) + expect(container.getByText("37%")).not.toBeNull(); +}); test("Renders temperature input with correct value", () => { - expect(container.queryByDisplayValue(65)).not.toBeNull() -}) + expect(container.queryByDisplayValue(65)).not.toBeNull(); +}); test("Renders pressure input with correct value", () => { - expect(container.queryByDisplayValue(456)).not.toBeNull() -}) + expect(container.queryByDisplayValue(456)).not.toBeNull(); +}); test("Renders humidity input with correct value", () => { - expect(container.queryByDisplayValue(37)).not.toBeNull() -}) + expect(container.queryByDisplayValue(37)).not.toBeNull(); +}); test("Renders motion input with correct value", () => { - expect(container.queryByLabelText('output.senseHat.controls.motion').checked).toBe(true) -}) + expect( + container.queryByLabelText("output.senseHat.controls.motion").checked, + ).toBe(true); +}); test("Renders colour input with correct value", () => { - expect(container.queryByDisplayValue("#e01010")).not.toBeNull() -}) - - - + expect(container.queryByDisplayValue("#e01010")).not.toBeNull(); +}); diff --git a/src/components/AstroPiModel/AstroPiControls/Input.js b/src/components/AstroPiModel/AstroPiControls/Input.js index f8b7ec9a2..39b448452 100644 --- a/src/components/AstroPiModel/AstroPiControls/Input.js +++ b/src/components/AstroPiModel/AstroPiControls/Input.js @@ -1,24 +1,34 @@ -import React from 'react'; -import { useEffect, useState } from 'react'; -import Sk from 'skulpt'; -import '../AstroPiModel.scss'; +import React from "react"; +import { useEffect, useState } from "react"; +import Sk from "skulpt"; +import "../AstroPiModel.scss"; const Input = (props) => { - const { name, label, type, defaultValue} = props; + const { name, label, type, defaultValue } = props; const [value, setValue] = useState(defaultValue); useEffect(() => { if (Sk.sense_hat) { - Sk.sense_hat[name] = value + Sk.sense_hat[name] = value; } - }, [name, value]) + }, [name, value]); return ( -
- - setValue(e.target.value)} /> +
+ + setValue(e.target.value)} + />
- ) + ); }; -export default Input +export default Input; diff --git a/src/components/AstroPiModel/AstroPiControls/Input.test.js b/src/components/AstroPiModel/AstroPiControls/Input.test.js index 01eec23e1..318d4d233 100644 --- a/src/components/AstroPiModel/AstroPiControls/Input.test.js +++ b/src/components/AstroPiModel/AstroPiControls/Input.test.js @@ -1,27 +1,35 @@ import React from "react"; -import { render } from "@testing-library/react" +import { render } from "@testing-library/react"; import Input from "./Input"; -import Sk from 'skulpt'; +import Sk from "skulpt"; let inputComponent; beforeEach(() => { - Sk.sense_hat={} - inputComponent = render() -}) + Sk.sense_hat = {}; + inputComponent = render( + , + ); +}); test("Renders as expected", () => { - expect(inputComponent.queryByText(/bar/)).not.toBeNull() -}) + expect(inputComponent.queryByText(/bar/)).not.toBeNull(); +}); test("Creates input of correct type", () => { - expect(inputComponent.queryByLabelText(/bar/)).toHaveAttribute("type", "color") -}) + expect(inputComponent.queryByLabelText(/bar/)).toHaveAttribute( + "type", + "color", + ); +}); test("Creates input with correct value", () => { - expect(inputComponent.queryByLabelText(/bar/)).toHaveAttribute("value", "#e01010") -}) + expect(inputComponent.queryByLabelText(/bar/)).toHaveAttribute( + "value", + "#e01010", + ); +}); test("Sets skulpt value correctly", () => { - expect(Sk.sense_hat.foo).toEqual("#e01010") -}) + expect(Sk.sense_hat.foo).toEqual("#e01010"); +}); diff --git a/src/components/AstroPiModel/AstroPiControls/MotionInput.js b/src/components/AstroPiModel/AstroPiControls/MotionInput.js index cddec8972..eb5e5ba3c 100644 --- a/src/components/AstroPiModel/AstroPiControls/MotionInput.js +++ b/src/components/AstroPiModel/AstroPiControls/MotionInput.js @@ -1,42 +1,60 @@ -import React from 'react'; -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import Toggle from 'react-toggle'; -import Sk from 'skulpt'; -import '../AstroPiModel.scss'; -import "react-toggle/style.css" -import { useTranslation } from 'react-i18next'; +import React from "react"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import Toggle from "react-toggle"; +import Sk from "skulpt"; +import "../AstroPiModel.scss"; +import "react-toggle/style.css"; +import { useTranslation } from "react-i18next"; const MotionInput = (props) => { - const {defaultValue} = props; + const { defaultValue } = props; const [value, setValue] = useState(defaultValue); - const codeRunTriggered = useSelector((state) => state.editor.codeRunTriggered); - const { t } = useTranslation() + const codeRunTriggered = useSelector( + (state) => state.editor.codeRunTriggered, + ); + const { t } = useTranslation(); useEffect(() => { if (!codeRunTriggered) { - Sk.sense_hat.start_motion_callback = () => {} - Sk.sense_hat.stop_motion_callback = () => {} + Sk.sense_hat.start_motion_callback = () => {}; + Sk.sense_hat.stop_motion_callback = () => {}; } - }, [codeRunTriggered]) + }, [codeRunTriggered]); useEffect(() => { if (Sk.sense_hat) { - Sk.sense_hat.motion = value + Sk.sense_hat.motion = value; } - value ? Sk.sense_hat.start_motion_callback() : Sk.sense_hat.stop_motion_callback() - }, [value]) + value + ? Sk.sense_hat.start_motion_callback() + : Sk.sense_hat.stop_motion_callback(); + }, [value]); return ( -
- -
- - setValue(e.target.checked)}/> - +
+ +
+ + setValue(e.target.checked)} + /> + +
-
- ) + ); }; -export default MotionInput +export default MotionInput; diff --git a/src/components/AstroPiModel/AstroPiControls/MotionInput.test.js b/src/components/AstroPiModel/AstroPiControls/MotionInput.test.js index c41cf64f6..61f154f13 100644 --- a/src/components/AstroPiModel/AstroPiControls/MotionInput.test.js +++ b/src/components/AstroPiModel/AstroPiControls/MotionInput.test.js @@ -1,115 +1,135 @@ import React from "react"; -import { render } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; +import { render } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; import MotionInput from "./MotionInput"; -import Sk from 'skulpt'; +import Sk from "skulpt"; let container; let store; -const start_motion_function = jest.fn() -const stop_motion_function = jest.fn() +const start_motion_function = jest.fn(); +const stop_motion_function = jest.fn(); describe("No motion and code running", () => { beforeEach(() => { - Sk.sense_hat={ + Sk.sense_hat = { start_motion_callback: start_motion_function, - stop_motion_callback: stop_motion_function - } - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - codeRunTriggered: true - }, - } - store = mockStore(initialState); - container = render() - }) - + stop_motion_callback: stop_motion_function, + }; + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + codeRunTriggered: true, + }, + }; + store = mockStore(initialState); + container = render( + + + , + ); + }); + test("Displays checkbox with correct value", () => { - expect(container.queryByLabelText('output.senseHat.controls.motion').checked).toBe(false) - }) - + expect( + container.queryByLabelText("output.senseHat.controls.motion").checked, + ).toBe(false); + }); + test("Stop motion function has been called", () => { - expect(stop_motion_function).toHaveBeenCalledTimes(1) - }) - + expect(stop_motion_function).toHaveBeenCalledTimes(1); + }); + test("Skulpt sense hat motion set to false", () => { - expect(Sk.sense_hat.motion).toBe(false) - }) -}) + expect(Sk.sense_hat.motion).toBe(false); + }); +}); describe("Motion and code running", () => { beforeEach(() => { - Sk.sense_hat={ + Sk.sense_hat = { start_motion_callback: start_motion_function, - stop_motion_callback: stop_motion_function - } - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - codeRunTriggered: true - }, - } - store = mockStore(initialState); - container = render() - }) - + stop_motion_callback: stop_motion_function, + }; + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + codeRunTriggered: true, + }, + }; + store = mockStore(initialState); + container = render( + + + , + ); + }); + test("Displays checkbox with correct value", () => { - expect(container.queryByLabelText('output.senseHat.controls.motion').checked).toBe(true) - }) - + expect( + container.queryByLabelText("output.senseHat.controls.motion").checked, + ).toBe(true); + }); + test("Stop motion function has been called", () => { - expect(start_motion_function).toHaveBeenCalledTimes(1) - }) + expect(start_motion_function).toHaveBeenCalledTimes(1); + }); test("Skulpt sense hat motion set to true", () => { - expect(Sk.sense_hat.motion).toBe(true) - }) -}) + expect(Sk.sense_hat.motion).toBe(true); + }); +}); describe("No motion and code not running", () => { beforeEach(() => { - Sk.sense_hat={ + Sk.sense_hat = { start_motion_callback: start_motion_function, - stop_motion_callback: stop_motion_function - } - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - codeRunTriggered: false - }, - } - store = mockStore(initialState); - container = render() - }) + stop_motion_callback: stop_motion_function, + }; + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + codeRunTriggered: false, + }, + }; + store = mockStore(initialState); + container = render( + + + , + ); + }); test("Stop motion function not called", () => { - expect(stop_motion_function).not.toHaveBeenCalled() - }) -}) + expect(stop_motion_function).not.toHaveBeenCalled(); + }); +}); describe("Motion and code not running", () => { beforeEach(() => { - Sk.sense_hat={ + Sk.sense_hat = { start_motion_callback: start_motion_function, - stop_motion_callback: stop_motion_function - } - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - codeRunTriggered: false - }, - } - store = mockStore(initialState); - container = render() - }) + stop_motion_callback: stop_motion_function, + }; + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + codeRunTriggered: false, + }, + }; + store = mockStore(initialState); + container = render( + + + , + ); + }); test("Start motion function not called", () => { - expect(start_motion_function).not.toHaveBeenCalled() - }) -}) + expect(start_motion_function).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/AstroPiModel/AstroPiControls/SliderInput.js b/src/components/AstroPiModel/AstroPiControls/SliderInput.js index 8444184d5..03277016f 100644 --- a/src/components/AstroPiModel/AstroPiControls/SliderInput.js +++ b/src/components/AstroPiModel/AstroPiControls/SliderInput.js @@ -1,28 +1,45 @@ -import React from 'react'; -import { useEffect, useState } from 'react'; -import '../AstroPiModel.scss'; -import Sk from 'skulpt'; +import React from "react"; +import { useEffect, useState } from "react"; +import "../AstroPiModel.scss"; +import Sk from "skulpt"; const SliderInput = (props) => { - const { name, label, unit, min, max, defaultValue, Icon} = props; + const { name, label, unit, min, max, defaultValue, Icon } = props; const [value, setValue] = useState(defaultValue); - + useEffect(() => { if (Sk.sense_hat) { - Sk.sense_hat.rtimu[name][1] = value+Math.random()-0.5 + Sk.sense_hat.rtimu[name][1] = value + Math.random() - 0.5; } - }, [name, value]) + }, [name, value]); return (
- - setValue(parseFloat(e.target.value))}/> + + setValue(parseFloat(e.target.value))} + />
{Icon ? : null} - {value}{unit} + + {value} + {unit} +
- ) + ); }; -export default SliderInput +export default SliderInput; diff --git a/src/components/AstroPiModel/AstroPiControls/SliderInput.test.js b/src/components/AstroPiModel/AstroPiControls/SliderInput.test.js index 7f1cd1163..3924bbf9e 100644 --- a/src/components/AstroPiModel/AstroPiControls/SliderInput.test.js +++ b/src/components/AstroPiModel/AstroPiControls/SliderInput.test.js @@ -1,29 +1,36 @@ import React from "react"; -import { render } from "@testing-library/react" +import { render } from "@testing-library/react"; import SliderInput from "./SliderInput"; -import Sk from 'skulpt'; +import Sk from "skulpt"; let container; -Sk.sense_hat={ +Sk.sense_hat = { rtimu: { - "foo": [0,0] - } -} + foo: [0, 0], + }, +}; beforeEach(() => { - container = render() - -}) + container = render( + , + ); +}); test("Renders with correct unit and value", () => { - expect(container.queryByText("1234bar")).not.toBeNull() -}) + expect(container.queryByText("1234bar")).not.toBeNull(); +}); test("Renders slider input with correct value", () => { - expect(container.queryByDisplayValue(1234)).not.toBeNull() -}) + expect(container.queryByDisplayValue(1234)).not.toBeNull(); +}); test("Correct skulpt value when renders", () => { - expect(Sk.sense_hat.rtimu["foo"][1]).toBeLessThan(1234.5) - expect(Sk.sense_hat.rtimu["foo"][1]).toBeGreaterThanOrEqual(1233.5) -}) + expect(Sk.sense_hat.rtimu["foo"][1]).toBeLessThan(1234.5); + expect(Sk.sense_hat.rtimu["foo"][1]).toBeGreaterThanOrEqual(1233.5); +}); diff --git a/src/components/AstroPiModel/AstroPiControls/Stopwatch.js b/src/components/AstroPiModel/AstroPiControls/Stopwatch.js index d4f99a8d9..c36191bb0 100644 --- a/src/components/AstroPiModel/AstroPiControls/Stopwatch.js +++ b/src/components/AstroPiModel/AstroPiControls/Stopwatch.js @@ -1,47 +1,63 @@ -import React, { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { useStopwatch } from 'react-timer-hook'; -import Sk from 'skulpt' +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { useStopwatch } from "react-timer-hook"; +import Sk from "skulpt"; const Stopwatch = () => { - const codeRunTriggered = useSelector((state) => state.editor.codeRunTriggered) - const { - seconds, - minutes, - isRunning, - pause, - reset - } = useStopwatch({ autoStart: false }) - const [hasLostFocus, setHasLostFocus] = useState(false) - const { t } = useTranslation() + const codeRunTriggered = useSelector( + (state) => state.editor.codeRunTriggered, + ); + const { seconds, minutes, isRunning, pause, reset } = useStopwatch({ + autoStart: false, + }); + const [hasLostFocus, setHasLostFocus] = useState(false); + const { t } = useTranslation(); useEffect(() => { - window.addEventListener('blur', () => { - setHasLostFocus(true) - }) - }, []) + window.addEventListener("blur", () => { + setHasLostFocus(true); + }); + }, []); useEffect(() => { if (codeRunTriggered && !isRunning) { - setHasLostFocus(false) - reset() + setHasLostFocus(false); + reset(); } - if (!codeRunTriggered && isRunning){ - pause() - Sk.sense_hat.mz_criteria.duration = hasLostFocus ? null : minutes * 60 + seconds + if (!codeRunTriggered && isRunning) { + pause(); + Sk.sense_hat.mz_criteria.duration = hasLostFocus + ? null + : minutes * 60 + seconds; } - }, [codeRunTriggered, hasLostFocus, minutes, seconds, isRunning, pause, reset]) - + }, [ + codeRunTriggered, + hasLostFocus, + minutes, + seconds, + isRunning, + pause, + reset, + ]); return ( -
- - - {String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')} +
+ + + {String(minutes).padStart(2, "0")}: + {String(seconds).padStart(2, "0")}
- ) -} + ); +}; -export default Stopwatch +export default Stopwatch; diff --git a/src/components/AstroPiModel/AstroPiControls/Stopwatch.test.js b/src/components/AstroPiModel/AstroPiControls/Stopwatch.test.js index af9ecf7d5..63cc0b13b 100644 --- a/src/components/AstroPiModel/AstroPiControls/Stopwatch.test.js +++ b/src/components/AstroPiModel/AstroPiControls/Stopwatch.test.js @@ -1,23 +1,27 @@ import React from "react"; -import { render } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import Sk from 'skulpt'; +import { render } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import Sk from "skulpt"; import Stopwatch from "./Stopwatch"; -const middlewares = [] -const mockStore = configureStore(middlewares) +const middlewares = []; +const mockStore = configureStore(middlewares); const initialState = { editor: { - codeRunTriggered: false + codeRunTriggered: false, }, -} +}; const store = mockStore(initialState); -Sk.sense_hat={ - mz_criteria: {} -} +Sk.sense_hat = { + mz_criteria: {}, +}; -test("Stopwatch renders in form mm:ss", () =>{ - const { getAllByText } = render() - expect(getAllByText("00")).toHaveLength(2) -}) +test("Stopwatch renders in form mm:ss", () => { + const { getAllByText } = render( + + + , + ); + expect(getAllByText("00")).toHaveLength(2); +}); diff --git a/src/components/AstroPiModel/AstroPiModel.js b/src/components/AstroPiModel/AstroPiModel.js index 956ca669f..f14de8729 100644 --- a/src/components/AstroPiModel/AstroPiModel.js +++ b/src/components/AstroPiModel/AstroPiModel.js @@ -1,77 +1,97 @@ -import React from 'react'; -import './AstroPiModel.scss'; -import Simulator from './Simulator'; -import Sk from 'skulpt'; -import AstroPiControls from './AstroPiControls/AstroPiControls'; -import OrientationPanel from './OrientationPanel/OrientationPanel'; -import { useEffect, useState } from 'react'; -import { resetModel, updateRTIMU } from '../../utils/Orientation'; -import { useSelector } from 'react-redux'; -import { defaultMZCriteria } from './DefaultMZCriteria'; +import React from "react"; +import "./AstroPiModel.scss"; +import Simulator from "./Simulator"; +import Sk from "skulpt"; +import AstroPiControls from "./AstroPiControls/AstroPiControls"; +import OrientationPanel from "./OrientationPanel/OrientationPanel"; +import { useEffect, useState } from "react"; +import { resetModel, updateRTIMU } from "../../utils/Orientation"; +import { useSelector } from "react-redux"; +import { defaultMZCriteria } from "./DefaultMZCriteria"; const AstroPiModel = () => { - const project = useSelector((state) => state.editor.project) - const [orientation, setOrientation] = useState([0,90,0]) + const project = useSelector((state) => state.editor.project); + const [orientation, setOrientation] = useState([0, 90, 0]); const resetOrientation = (e) => { - resetModel(e) - setOrientation([0,90,0]) - } + resetModel(e); + setOrientation([0, 90, 0]); + }; - const defaultPressure = 1013 - const defaultTemperature = 13 - const defaultHumidity = 45 + const defaultPressure = 1013; + const defaultTemperature = 13; + const defaultHumidity = 45; - if (!Sk.sense_hat){ + if (!Sk.sense_hat) { Sk.sense_hat = { colour: "#FF00A4", - gamma: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + gamma: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ], low_light: false, motion: false, - mz_criteria: {...defaultMZCriteria}, + mz_criteria: { ...defaultMZCriteria }, pixels: [], rtimu: { - pressure: [1, defaultPressure+Math.random()-0.5], /* isValid, pressure*/ - temperature: [1, defaultTemperature+Math.random()-0.5], /* isValid, temperature */ - humidity: [1, defaultHumidity+Math.random()-0.5], /* isValid, humidity */ - gyro: [0, 0, 0], /* all 3 gyro values */ - accel: [0, 0, 0], /* all 3 accel values */ - compass: [0, 0, 33], /* all compass values */ - raw_orientation: [0, 90, 0] + pressure: [ + 1, + defaultPressure + Math.random() - 0.5, + ] /* isValid, pressure*/, + temperature: [ + 1, + defaultTemperature + Math.random() - 0.5, + ] /* isValid, temperature */, + humidity: [ + 1, + defaultHumidity + Math.random() - 0.5, + ] /* isValid, humidity */, + gyro: [0, 0, 0] /* all 3 gyro values */, + accel: [0, 0, 0] /* all 3 accel values */, + compass: [0, 0, 33] /* all compass values */, + raw_orientation: [0, 90, 0], }, sensestick: { _eventQueue: [], off: () => {}, - once: () => {} + once: () => {}, }, start_motion_callback: () => {}, stop_motion_callback: () => {}, - } + }; for (var i = 0; i < 64; i++) { Sk.sense_hat.pixels.push([0, 0, 0]); } } useEffect(() => { - Sk.sense_hat.mz_criteria = {...defaultMZCriteria} + Sk.sense_hat.mz_criteria = { ...defaultMZCriteria }; }, [project]); useEffect(() => { - Sk.sense_hat.rtimu.raw_orientation = orientation - updateRTIMU() - }, [orientation]) + Sk.sense_hat.rtimu.raw_orientation = orientation; + updateRTIMU(); + }, [orientation]); - return ( -
-
- - -
+ return ( +
+
+ + +
- {/* */} - + {/* */} + +
+ ); +}; -
- ) - }; - - export default AstroPiModel; +export default AstroPiModel; diff --git a/src/components/AstroPiModel/AstroPiModel.test.js b/src/components/AstroPiModel/AstroPiModel.test.js index d3c36df7e..8ca5e791b 100644 --- a/src/components/AstroPiModel/AstroPiModel.test.js +++ b/src/components/AstroPiModel/AstroPiModel.test.js @@ -1,41 +1,46 @@ import React from "react"; -import { fireEvent, render } from "@testing-library/react" -import { Provider } from 'react-redux'; +import { fireEvent, render } from "@testing-library/react"; +import { Provider } from "react-redux"; import AstroPiModel from "./AstroPiModel"; -import configureStore from 'redux-mock-store'; -import Sk from 'skulpt'; +import configureStore from "redux-mock-store"; +import Sk from "skulpt"; let container; let store; beforeEach(() => { - Sk.sense_hat={ + Sk.sense_hat = { mz_criteria: {}, rtimu: { - temperature: [0,0], - pressure: [0,0], - humidity: [0,0] - } - } - window.mod={ - rotation: {} - } - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - codeRunTriggered: false - }, - } - store = mockStore(initialState); - container = render( ) -}) + temperature: [0, 0], + pressure: [0, 0], + humidity: [0, 0], + }, + }; + window.mod = { + rotation: {}, + }; + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + codeRunTriggered: false, + }, + }; + store = mockStore(initialState); + container = render( + + {" "} + {" "} + , + ); +}); test("Component renders", () => { - expect(container).not.toBeNull() -}) + expect(container).not.toBeNull(); +}); test("Update orientation function resets the model", () => { - fireEvent.click(container.getByRole("button")) - expect(window.mod.rotation).toStrictEqual({x: 0, y: 0, z:0}) -}) + fireEvent.click(container.getByRole("button")); + expect(window.mod.rotation).toStrictEqual({ x: 0, y: 0, z: 0 }); +}); diff --git a/src/components/AstroPiModel/DefaultMZCriteria.js b/src/components/AstroPiModel/DefaultMZCriteria.js index 96d355881..d8f0b47f4 100644 --- a/src/components/AstroPiModel/DefaultMZCriteria.js +++ b/src/components/AstroPiModel/DefaultMZCriteria.js @@ -5,5 +5,5 @@ export const defaultMZCriteria = { readHumidity: false, readPressure: false, readTemperature: false, - usedLEDs: false -} + usedLEDs: false, +}; diff --git a/src/components/AstroPiModel/FlightCase.js b/src/components/AstroPiModel/FlightCase.js index 160ce76aa..b229e2d69 100644 --- a/src/components/AstroPiModel/FlightCase.js +++ b/src/components/AstroPiModel/FlightCase.js @@ -1,73 +1,77 @@ -import React from 'react'; -import * as THREE from 'three'; -import { useGLTF } from '@react-three/drei'; +import React from "react"; +import * as THREE from "three"; +import { useGLTF } from "@react-three/drei"; import Sk from "skulpt"; -import { invalidate } from '@react-three/fiber'; +import { invalidate } from "@react-three/fiber"; const FlightCase = () => { + const { scene } = useGLTF( + `${process.env.PUBLIC_URL}/models/raspi-compressed.glb`, + ); + window.mod = scene; - const { scene } = useGLTF(`${process.env.PUBLIC_URL}/models/raspi-compressed.glb`) - window.mod = scene - - const blankLED = new THREE.MeshStandardMaterial({color: `rgb(0,0,0)`}) + const blankLED = new THREE.MeshStandardMaterial({ color: `rgb(0,0,0)` }); var leds = { - '0_0_0': blankLED - } + "0_0_0": blankLED, + }; - function setPixel(ledIndex,r,g,b) { + function setPixel(ledIndex, r, g, b) { var x = ledIndex % 8; var y = Math.floor(ledIndex / 8); - let ledMaterial + let ledMaterial; if (`${r}_${g}_${b}` in leds) { - ledMaterial = leds[`${r}_${g}_${b}`] + ledMaterial = leds[`${r}_${g}_${b}`]; } else { - ledMaterial = new THREE.MeshStandardMaterial({color: `rgb(${r},${g},${b})`}); - leds[`${r}_${g}_${b}`] = ledMaterial + ledMaterial = new THREE.MeshStandardMaterial({ + color: `rgb(${r},${g},${b})`, + }); + leds[`${r}_${g}_${b}`] = ledMaterial; } - var object = scene.getObjectByName(`circle${x}_${7-y}-1`); + var object = scene.getObjectByName(`circle${x}_${7 - y}-1`); - if(object != null){ + if (object != null) { object.material = ledMaterial; } } function setPixels(indexes, pix) { - if(indexes == null){ - indexes = Array.from(Array(8*8).keys()) + if (indexes == null) { + indexes = Array.from(Array(8 * 8).keys()); } var i = 0; - for (const ledIndex of indexes){ - setPixel(ledIndex, pix[i][0], pix[i][1], pix[i][2]) + for (const ledIndex of indexes) { + setPixel(ledIndex, pix[i][0], pix[i][1], pix[i][2]); i += 1; } } - Sk.sense_hat_emit = function(event, data) { - - if (event && event === 'setpixel') { + Sk.sense_hat_emit = function (event, data) { + if (event && event === "setpixel") { // change the led const ledIndex = data; - const ledData = Sk.sense_hat.pixels[ledIndex]; + const ledData = Sk.sense_hat.pixels[ledIndex]; // Convert LED-RGB to RGB565 // and then to RGB555 Sk.sense_hat.pixels[ledIndex] = [ ledData[0] & ~7, ledData[1] & ~3, - ledData[2] & ~7 + ledData[2] & ~7, ]; - setPixel(ledIndex, parseInt(ledData[0]*255), parseInt(ledData[1]*255), parseInt(ledData[2]*255)); - } - else if (event && event === 'setpixels') { + setPixel( + ledIndex, + parseInt(ledData[0] * 255), + parseInt(ledData[1] * 255), + parseInt(ledData[2] * 255), + ); + } else if (event && event === "setpixels") { setPixels(data, Sk.sense_hat.pixels); } - invalidate() - } + invalidate(); + }; - return ( - - ) -} + return ; +}; -export default FlightCase +export default FlightCase; diff --git a/src/components/AstroPiModel/FlightCase.test.js b/src/components/AstroPiModel/FlightCase.test.js index a7f87d15b..b0fbe1ef6 100644 --- a/src/components/AstroPiModel/FlightCase.test.js +++ b/src/components/AstroPiModel/FlightCase.test.js @@ -1,10 +1,8 @@ import React from "react"; -import ReactThreeTestRenderer from '@react-three/test-renderer'; +import ReactThreeTestRenderer from "@react-three/test-renderer"; import FlightCase from "./FlightCase"; test("3D model loads and renders", async () => { - await ReactThreeTestRenderer.create() -}) - - + await ReactThreeTestRenderer.create(); +}); diff --git a/src/components/AstroPiModel/Lighting.js b/src/components/AstroPiModel/Lighting.js index d67f72909..5876b5a3d 100644 --- a/src/components/AstroPiModel/Lighting.js +++ b/src/components/AstroPiModel/Lighting.js @@ -1,19 +1,44 @@ -import React from 'react'; +import React from "react"; const Lighting = () => { - - return( - <> + return ( + <> - - - - - - - + + + + + + + - ) -} + ); +}; -export default Lighting +export default Lighting; diff --git a/src/components/AstroPiModel/Lighting.test.js b/src/components/AstroPiModel/Lighting.test.js index 375ee5589..5de81ab5b 100644 --- a/src/components/AstroPiModel/Lighting.test.js +++ b/src/components/AstroPiModel/Lighting.test.js @@ -1,7 +1,7 @@ import React from "react"; -import ReactThreeTestRenderer from '@react-three/test-renderer'; +import ReactThreeTestRenderer from "@react-three/test-renderer"; import Lighting from "./Lighting"; test("Lighting component renders", async () => { - await ReactThreeTestRenderer.create() -}) + await ReactThreeTestRenderer.create(); +}); diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationPanel.js b/src/components/AstroPiModel/OrientationPanel/OrientationPanel.js index 2114b9fda..43bf4da74 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationPanel.js +++ b/src/components/AstroPiModel/OrientationPanel/OrientationPanel.js @@ -1,27 +1,35 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import '../AstroPiModel.scss'; -import OrientationReading from './OrientationReading'; -import OrientationResetButton from './OrientationResetButton'; +import React from "react"; +import { useTranslation } from "react-i18next"; +import "../AstroPiModel.scss"; +import OrientationReading from "./OrientationReading"; +import OrientationResetButton from "./OrientationResetButton"; const OrientationPanel = (props) => { - - const { t } = useTranslation() - const {orientation, resetOrientation} = props + const { t } = useTranslation(); + const { orientation, resetOrientation } = props; return (
- - - + + +
- +
- ) + ); }; -export default OrientationPanel +export default OrientationPanel; diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.js b/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.js index 0492762f1..04f5e5d24 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.js +++ b/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.js @@ -1,16 +1,21 @@ import React from "react"; -import { render } from "@testing-library/react" +import { render } from "@testing-library/react"; import OrientationPanel from "./OrientationPanel"; let panel; -const resetFunction = jest.fn() +const resetFunction = jest.fn(); beforeAll(() => { - panel = render() -}) + panel = render( + , + ); +}); test("Renders roll, pitch and yaw values", () => { - expect(panel.queryByText("output.senseHat.model.roll: 278")).not.toBeNull() - expect(panel.queryByText("output.senseHat.model.pitch: 84")).not.toBeNull() - expect(panel.queryByText("output.senseHat.model.yaw: 327")).not.toBeNull() -}) + expect(panel.queryByText("output.senseHat.model.roll: 278")).not.toBeNull(); + expect(panel.queryByText("output.senseHat.model.pitch: 84")).not.toBeNull(); + expect(panel.queryByText("output.senseHat.model.yaw: 327")).not.toBeNull(); +}); diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationReading.js b/src/components/AstroPiModel/OrientationPanel/OrientationReading.js index 8a70116a6..939402b0d 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationReading.js +++ b/src/components/AstroPiModel/OrientationPanel/OrientationReading.js @@ -1,14 +1,14 @@ -import React from 'react'; -import '../AstroPiModel.scss'; +import React from "react"; +import "../AstroPiModel.scss"; const OrientationReading = (props) => { - const {name, value} = props + const { name, value } = props; return ( - - {name}: {Math.round(value)} - - ) -} + + {name}: {Math.round(value)} + + ); +}; -export default OrientationReading +export default OrientationReading; diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationReading.test.js b/src/components/AstroPiModel/OrientationPanel/OrientationReading.test.js index 0e4d0ecd1..36acd69ae 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationReading.test.js +++ b/src/components/AstroPiModel/OrientationPanel/OrientationReading.test.js @@ -1,17 +1,17 @@ import React from "react"; -import { render } from "@testing-library/react" +import { render } from "@testing-library/react"; import OrientationReading from "./OrientationReading"; let reading; beforeEach(() => { - reading = render() -}) + reading = render(); +}); test("Renders name", () => { - expect(reading.queryByText(/foo/)).not.toBeNull() -}) + expect(reading.queryByText(/foo/)).not.toBeNull(); +}); test("Renders rounded value", () => { - expect(reading.queryByText(/124/)).not.toBeNull() -}) + expect(reading.queryByText(/124/)).not.toBeNull(); +}); diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.js b/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.js index 0da038da0..889eaf6fb 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.js +++ b/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.js @@ -1,16 +1,15 @@ -import React from 'react'; -import '../AstroPiModel.scss'; -import { ResetIcon } from '../../../Icons'; +import React from "react"; +import "../AstroPiModel.scss"; +import { ResetIcon } from "../../../Icons"; const OrientationResetButton = (props) => { - - const {resetOrientation} = props; + const { resetOrientation } = props; return ( - - ) -} + ); +}; -export default OrientationResetButton +export default OrientationResetButton; diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.js b/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.js index 260ae7562..0f9c37c86 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.js +++ b/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.js @@ -1,15 +1,17 @@ import React from "react"; -import { render, fireEvent } from "@testing-library/react" +import { render, fireEvent } from "@testing-library/react"; import OrientationResetButton from "./OrientationResetButton"; let resetButton; -const resetFunction = jest.fn() +const resetFunction = jest.fn(); beforeEach(() => { - resetButton = render() -}) + resetButton = render( + , + ); +}); test("Clicking button calls reset function", () => { - fireEvent.click(resetButton.getByRole("button")) - expect(resetFunction).toHaveBeenCalledTimes(1) -}) + fireEvent.click(resetButton.getByRole("button")); + expect(resetFunction).toHaveBeenCalledTimes(1); +}); diff --git a/src/components/AstroPiModel/Simulator.js b/src/components/AstroPiModel/Simulator.js index 7d008b6de..bd9d1d8fe 100644 --- a/src/components/AstroPiModel/Simulator.js +++ b/src/components/AstroPiModel/Simulator.js @@ -1,16 +1,16 @@ -import React from 'react'; -import * as THREE from 'three'; +import React from "react"; +import * as THREE from "three"; import { ResizeObserver } from "@juggle/resize-observer"; import { Canvas } from "@react-three/fiber"; import { OrbitControls, PerspectiveCamera } from "@react-three/drei"; -import Lighting from './Lighting'; -import { Suspense } from 'react'; +import Lighting from "./Lighting"; +import { Suspense } from "react"; -import { extractRollPitchYaw } from '../../utils/Orientation'; -import FlightCase from './FlightCase' -import './AstroPiModel.scss'; +import { extractRollPitchYaw } from "../../utils/Orientation"; +import FlightCase from "./FlightCase"; +import "./AstroPiModel.scss"; -var isDragging=false +var isDragging = false; var targetRotationX = 0.5; var targetRotationY = 0.2; var mouseX = 0; @@ -20,36 +20,39 @@ var mouseYOnMouseDown = 0; var windowHalfX = window.innerWidth / 2; var windowHalfY = window.innerHeight / 2; var slowingFactor = 0.25; -const rotationScaleFactor = 0.00025 +const rotationScaleFactor = 0.00025; const Simulator = (props) => { - const {updateOrientation} = props + const { updateOrientation } = props; const handleDragStart = (e) => { - isDragging=true + isDragging = true; mouseXOnMouseDown = e.clientX - windowHalfX; mouseYOnMouseDown = e.clientY - windowHalfY; - } + }; const handleDragStop = () => { - isDragging=false - } + isDragging = false; + }; const dragModel = (e) => { mouseX = e.clientX - windowHalfX; - targetRotationX = ( mouseX - mouseXOnMouseDown ) * rotationScaleFactor; + targetRotationX = (mouseX - mouseXOnMouseDown) * rotationScaleFactor; mouseY = e.clientY - windowHalfY; - targetRotationY = ( mouseY - mouseYOnMouseDown ) * rotationScaleFactor; - - if(isDragging) { - window.mod.rotateOnWorldAxis(new THREE.Vector3(0, 0, -1), targetRotationX); + targetRotationY = (mouseY - mouseYOnMouseDown) * rotationScaleFactor; + + if (isDragging) { + window.mod.rotateOnWorldAxis( + new THREE.Vector3(0, 0, -1), + targetRotationX, + ); window.mod.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), targetRotationY); updateOrientation( extractRollPitchYaw( - window.mod.rotation.x, - window.mod.rotation.y, - window.mod.rotation.z - ) - ) + window.mod.rotation.x, + window.mod.rotation.y, + window.mod.rotation.z, + ), + ); targetRotationY = targetRotationY * (1 - slowingFactor); targetRotationX = targetRotationX * (1 - slowingFactor); } @@ -57,21 +60,32 @@ const Simulator = (props) => { return ( - + - + - ) + ); }; -export default Simulator +export default Simulator; diff --git a/src/components/AstroPiModel/Simulator.test.js b/src/components/AstroPiModel/Simulator.test.js index 5480f4c12..d52708bd0 100644 --- a/src/components/AstroPiModel/Simulator.test.js +++ b/src/components/AstroPiModel/Simulator.test.js @@ -7,27 +7,27 @@ window.mod = { rotation: { x: 0, y: 0, - z: 0 - } -} + z: 0, + }, +}; test("Three canvas renders", () => { - render() -}) + render(); +}); test("Moving pointer over model does not change orientation", () => { - const updateOrientation = jest.fn() - const simulator = render() - const canvas = simulator.container.querySelector("canvas") - fireEvent.pointerMove(canvas) - expect(updateOrientation).not.toHaveBeenCalled() -}) + const updateOrientation = jest.fn(); + const simulator = render(); + const canvas = simulator.container.querySelector("canvas"); + fireEvent.pointerMove(canvas); + expect(updateOrientation).not.toHaveBeenCalled(); +}); test("Dragging model changes orientation", async () => { - const updateOrientation = jest.fn() - const simulator = render() - const canvas = simulator.container.querySelector("canvas") - fireEvent.pointerDown(canvas) - fireEvent.pointerMove(canvas) - expect(updateOrientation).toHaveBeenCalled() -}) + const updateOrientation = jest.fn(); + const simulator = render(); + const canvas = simulator.container.querySelector("canvas"); + fireEvent.pointerDown(canvas); + fireEvent.pointerMove(canvas); + expect(updateOrientation).toHaveBeenCalled(); +}); diff --git a/src/components/BetaBanner/BetaBanner.js b/src/components/BetaBanner/BetaBanner.js index 28ca17500..c4383b29f 100644 --- a/src/components/BetaBanner/BetaBanner.js +++ b/src/components/BetaBanner/BetaBanner.js @@ -6,41 +6,56 @@ import { CloseIcon } from "../../Icons"; import Button from "../Button/Button"; import { showBetaModal } from "../Editor/EditorSlice"; -import './BetaBanner.scss' +import "./BetaBanner.scss"; const BetaBanner = () => { - - const dispatch = useDispatch() - const { t } = useTranslation() - const [cookies, setCookie] = useCookies(['betaBannerDismissed']) + const dispatch = useDispatch(); + const { t } = useTranslation(); + const [cookies, setCookie] = useCookies(["betaBannerDismissed"]); const closeBanner = () => { - setCookie('betaBannerDismissed', 'true', { path: '/' }) - } - const showModal = () => { dispatch(showBetaModal()) } - const isShowing = !cookies.betaBannerDismissed + setCookie("betaBannerDismissed", "true", { path: "/" }); + }; + const showModal = () => { + dispatch(showBetaModal()); + }; + const isShowing = !cookies.betaBannerDismissed; const handleKeyDown = (e) => { - const enterKey = 13 - const spaceKey = 32 + const enterKey = 13; + const spaceKey = 32; if (e.keyCode === enterKey || e.keyCode === spaceKey) { e.preventDefault(); - showModal() + showModal(); } - } + }; - return ( - isShowing ? - (
- Beta - - {t('betaBanner.message')} - {t('betaBanner.modalLink')} + return isShowing ? ( +
+ Beta + + {t("betaBanner.message")} + + {t("betaBanner.modalLink")} -
) - : <> - ) -} +
+
+ ) : ( + <> + ); +}; -export default BetaBanner +export default BetaBanner; diff --git a/src/components/BetaBanner/BetaBanner.test.js b/src/components/BetaBanner/BetaBanner.test.js index ee10b7482..d5508bab8 100644 --- a/src/components/BetaBanner/BetaBanner.test.js +++ b/src/components/BetaBanner/BetaBanner.test.js @@ -1,45 +1,44 @@ import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import { Cookies, CookiesProvider } from 'react-cookie'; +import { fireEvent, render, screen } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import { Cookies, CookiesProvider } from "react-cookie"; import BetaBanner from "./BetaBanner"; -let cookies -let store +let cookies; +let store; beforeEach(() => { cookies = new Cookies(); - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = {} - store = mockStore(initialState); + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = {}; + store = mockStore(initialState); render( - - ) -}) + , + ); +}); -test('Banner shows', () => { - expect(screen.queryByText('betaBanner.message')).toBeInTheDocument() -}) +test("Banner shows", () => { + expect(screen.queryByText("betaBanner.message")).toBeInTheDocument(); +}); -test('Clicking close button sets cookie', () => { - const closeButton = screen.getAllByRole('button').pop() - fireEvent.click(closeButton) - expect(cookies.cookies.betaBannerDismissed).toBe('true') +test("Clicking close button sets cookie", () => { + const closeButton = screen.getAllByRole("button").pop(); + fireEvent.click(closeButton); + expect(cookies.cookies.betaBannerDismissed).toBe("true"); +}); -}) - -test('Clicking link dispatches modal action', () => { - const modalLink = screen.queryByText('betaBanner.modalLink') - fireEvent.click(modalLink) - expect(store.getActions()).toEqual([{type: 'editor/showBetaModal'}]) -}) +test("Clicking link dispatches modal action", () => { + const modalLink = screen.queryByText("betaBanner.modalLink"); + fireEvent.click(modalLink); + expect(store.getActions()).toEqual([{ type: "editor/showBetaModal" }]); +}); afterEach(() => { - cookies.remove('betaBannerDismissed') -}) + cookies.remove("betaBannerDismissed"); +}); diff --git a/src/components/Callback.js b/src/components/Callback.js index d50ab1f8d..590b72b08 100644 --- a/src/components/Callback.js +++ b/src/components/Callback.js @@ -1,30 +1,34 @@ import React from "react"; import { connect } from "react-redux"; import { CallbackComponent } from "redux-oidc"; -import { useNavigate } from 'react-router-dom' +import { useNavigate } from "react-router-dom"; import userManager from "../utils/userManager"; const Callback = () => { - let navigate = useNavigate() + let navigate = useNavigate(); - const previousRoute = localStorage.getItem('location') + const previousRoute = localStorage.getItem("location"); const onSuccess = () => { - localStorage.removeItem('location') - window.plausible('Login successful') - navigate(previousRoute) - } + localStorage.removeItem("location"); + window.plausible("Login successful"); + navigate(previousRoute); + }; const onError = (error) => { navigate(previousRoute); console.error(error); - } + }; return ( - onError(error)}> + onError(error)} + >
Redirecting...
); -} +}; export default connect()(Callback); diff --git a/src/components/Editor/DraggableTabs/DraggableTab.js b/src/components/Editor/DraggableTabs/DraggableTab.js index 785f2d1c9..4154400aa 100644 --- a/src/components/Editor/DraggableTabs/DraggableTab.js +++ b/src/components/Editor/DraggableTabs/DraggableTab.js @@ -4,41 +4,57 @@ import { useDispatch, useSelector } from "react-redux"; import { Tab } from "react-tabs"; import { setFocussedFileIndex } from "../EditorSlice"; -import './DraggableTabs.scss' +import "./DraggableTabs.scss"; -const DraggableTab = ({children, panelIndex, fileIndex, ...otherProps}) => { - const openFiles = useSelector((state) => state.editor.openFiles) - const openFilesCount = openFiles[panelIndex].length - const dispatch = useDispatch() +const DraggableTab = ({ children, panelIndex, fileIndex, ...otherProps }) => { + const openFiles = useSelector((state) => state.editor.openFiles); + const openFilesCount = openFiles[panelIndex].length; + const dispatch = useDispatch(); const switchToFileTab = (panelIndex, fileIndex) => { - dispatch(setFocussedFileIndex({panelIndex, fileIndex})) - } + dispatch(setFocussedFileIndex({ panelIndex, fileIndex })); + }; const onKeyPress = (e, panelIndex, fileIndex) => { - if (e.code === 'ArrowRight') { - switchToFileTab(panelIndex, (fileIndex + 1) % openFilesCount) - } else if (e.code === 'ArrowLeft') { - switchToFileTab(panelIndex, (fileIndex + openFilesCount - 1) % openFilesCount ) + if (e.code === "ArrowRight") { + switchToFileTab(panelIndex, (fileIndex + 1) % openFilesCount); + } else if (e.code === "ArrowLeft") { + switchToFileTab( + panelIndex, + (fileIndex + openFilesCount - 1) % openFilesCount, + ); } - } - + }; + return ( - - {({innerRef, draggableProps, dragHandleProps}) => ( -
- { - e.stopPropagation() - switchToFileTab(panelIndex, fileIndex) - }} onKeyDown={e => onKeyPress(e, panelIndex, fileIndex)} {...otherProps} > + + {({ innerRef, draggableProps, dragHandleProps }) => ( +
+ { + e.stopPropagation(); + switchToFileTab(panelIndex, fileIndex); + }} + onKeyDown={(e) => onKeyPress(e, panelIndex, fileIndex)} + {...otherProps} + > {children}
)}
- ) + ); }; -DraggableTab.tabsRole = 'Tab' +DraggableTab.tabsRole = "Tab"; -export default DraggableTab +export default DraggableTab; diff --git a/src/components/Editor/DraggableTabs/DraggableTab.test.js b/src/components/Editor/DraggableTabs/DraggableTab.test.js index ee9237624..487a81de8 100644 --- a/src/components/Editor/DraggableTabs/DraggableTab.test.js +++ b/src/components/Editor/DraggableTabs/DraggableTab.test.js @@ -1,5 +1,5 @@ import React from "react"; -import configureStore from 'redux-mock-store' +import configureStore from "redux-mock-store"; import { fireEvent, render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; import DraggableTab from "./DraggableTab"; @@ -7,53 +7,69 @@ import DroppableTabList from "./DroppableTabList"; import { DragDropContext } from "@hello-pangea/dnd"; import { setFocussedFileIndex } from "../EditorSlice"; -describe('when tab is in focus', () => { - let store +describe("when tab is in focus", () => { + let store; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - name: 'main', - extension: 'py', - content: 'print("hello")' + name: "main", + extension: "py", + content: 'print("hello")', }, { - name: 'a', - extension: 'py', - content: '# Your code here' - } - ] + name: "a", + extension: "py", + content: "# Your code here", + }, + ], }, - openFiles: [['a.py', 'main.py', 'b.py']], - focussedFileIndices: [1] + openFiles: [["a.py", "main.py", "b.py"]], + focussedFileIndices: [1], }, auth: { - user: null - } - } + user: null, + }, + }; store = mockStore(initialState); - render(main.py) - }) - test('pressing right arrow key moves focus to right', () => { - const tab = screen.queryByText('main.py') - fireEvent.keyDown(tab, {code: 'ArrowRight'}) - expect(store.getActions()).toEqual([setFocussedFileIndex({panelIndex: 0, fileIndex: 2})]) - }) + render( + + + + + main.py + + + + , + ); + }); + test("pressing right arrow key moves focus to right", () => { + const tab = screen.queryByText("main.py"); + fireEvent.keyDown(tab, { code: "ArrowRight" }); + expect(store.getActions()).toEqual([ + setFocussedFileIndex({ panelIndex: 0, fileIndex: 2 }), + ]); + }); - test('pressing left arrow key moves focus to left', () => { - const tab = screen.queryByText('main.py') - fireEvent.keyDown(tab, {code: 'ArrowLeft'}) - expect(store.getActions()).toEqual([setFocussedFileIndex({panelIndex: 0, fileIndex: 0})]) - }) + test("pressing left arrow key moves focus to left", () => { + const tab = screen.queryByText("main.py"); + fireEvent.keyDown(tab, { code: "ArrowLeft" }); + expect(store.getActions()).toEqual([ + setFocussedFileIndex({ panelIndex: 0, fileIndex: 0 }), + ]); + }); - test('clicking tab switches focus to it', () => { - const tab = screen.queryByText('main.py') - fireEvent.click(tab) - expect(store.getActions()).toEqual([setFocussedFileIndex({panelIndex: 0, fileIndex: 1})]) - }) -}) + test("clicking tab switches focus to it", () => { + const tab = screen.queryByText("main.py"); + fireEvent.click(tab); + expect(store.getActions()).toEqual([ + setFocussedFileIndex({ panelIndex: 0, fileIndex: 1 }), + ]); + }); +}); diff --git a/src/components/Editor/DraggableTabs/DroppableTabList.js b/src/components/Editor/DraggableTabs/DroppableTabList.js index 837bb59a2..0839e73f7 100644 --- a/src/components/Editor/DraggableTabs/DroppableTabList.js +++ b/src/components/Editor/DraggableTabs/DroppableTabList.js @@ -1,24 +1,28 @@ -import React from "react" -import { Droppable } from "@hello-pangea/dnd" -import { TabList } from "react-tabs" +import React from "react"; +import { Droppable } from "@hello-pangea/dnd"; +import { TabList } from "react-tabs"; -import './DraggableTabs.scss' +import "./DraggableTabs.scss"; -const DroppableTabList = ({children, index, ...otherProps}) => { +const DroppableTabList = ({ children, index, ...otherProps }) => { return ( - - - {({innerRef, droppableProps, placeholder}) => ( -
+ + + {({ innerRef, droppableProps, placeholder }) => ( +
{children} {placeholder}
)}
- ) -} + ); +}; -DroppableTabList.tabsRole = 'TabList' +DroppableTabList.tabsRole = "TabList"; -export default DroppableTabList +export default DroppableTabList; diff --git a/src/components/Editor/DraggableTabs/DroppableTabList.test.js b/src/components/Editor/DraggableTabs/DroppableTabList.test.js index c0e3aa39f..9e5b3024b 100644 --- a/src/components/Editor/DraggableTabs/DroppableTabList.test.js +++ b/src/components/Editor/DraggableTabs/DroppableTabList.test.js @@ -4,9 +4,13 @@ import { render, screen } from "@testing-library/react"; import { DragDropContext } from "@hello-pangea/dnd"; beforeEach(() => { - render(hello) -}) + render( + + hello + , + ); +}); -test('Renders', () => { - expect(screen.queryByText('hello')).toBeInTheDocument() -}) +test("Renders", () => { + expect(screen.queryByText("hello")).toBeInTheDocument(); +}); diff --git a/src/components/Editor/Editor.js b/src/components/Editor/Editor.js index da0b1b731..ecaeea871 100644 --- a/src/components/Editor/Editor.js +++ b/src/components/Editor/Editor.js @@ -1,17 +1,17 @@ // import './editor.css' -import React, { useRef, useEffect } from 'react'; -import { useSelector, useDispatch } from 'react-redux' -import { update } from './EditorSlice' +import React, { useRef, useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { update } from "./EditorSlice"; -import { EditorState, basicSetup } from '@codemirror/basic-setup'; -import { EditorView, keymap } from '@codemirror/view'; -import { defaultKeymap } from '@codemirror/commands'; -import { python } from '@codemirror/lang-python'; +import { EditorState, basicSetup } from "@codemirror/basic-setup"; +import { EditorView, keymap } from "@codemirror/view"; +import { defaultKeymap } from "@codemirror/commands"; +import { python } from "@codemirror/lang-python"; function Editor() { const editor = useRef(); - const code = useSelector((state) => state.editor.code) - const dispatch = useDispatch() + const code = useSelector((state) => state.editor.code); + const dispatch = useDispatch(); const onUpdate = EditorView.updateListener.of((v) => { dispatch(update(v.state.doc.toString())); @@ -20,12 +20,7 @@ function Editor() { useEffect(() => { const startState = EditorState.create({ doc: code, - extensions: [ - basicSetup, - keymap.of(defaultKeymap), - python(), - onUpdate, - ], + extensions: [basicSetup, keymap.of(defaultKeymap), python(), onUpdate], }); const view = new EditorView({ state: startState, parent: editor.current }); diff --git a/src/components/Editor/EditorInput/EditorInput.js b/src/components/Editor/EditorInput/EditorInput.js index e80aea9da..6e5eb3d1e 100644 --- a/src/components/Editor/EditorInput/EditorInput.js +++ b/src/components/Editor/EditorInput/EditorInput.js @@ -1,81 +1,111 @@ -import React, { createRef, useEffect, useRef, useState } from 'react' -import { DragDropContext } from '@hello-pangea/dnd' -import { useDispatch, useSelector } from 'react-redux' -import { TabPanel, Tabs } from 'react-tabs' -import classNames from 'classnames'; +import React, { createRef, useEffect, useRef, useState } from "react"; +import { DragDropContext } from "@hello-pangea/dnd"; +import { useDispatch, useSelector } from "react-redux"; +import { TabPanel, Tabs } from "react-tabs"; +import classNames from "classnames"; -import { closeFile, setFocussedFileIndex, setOpenFiles } from '../EditorSlice' -import Button from '../../Button/Button' -import { CloseIcon } from '../../../Icons' -import EditorPanel from '../EditorPanel/EditorPanel' -import DraggableTab from '../DraggableTabs/DraggableTab' -import DroppableTabList from '../DraggableTabs/DroppableTabList' -import RunBar from '../../RunButton/RunBar' +import { closeFile, setFocussedFileIndex, setOpenFiles } from "../EditorSlice"; +import Button from "../../Button/Button"; +import { CloseIcon } from "../../../Icons"; +import EditorPanel from "../EditorPanel/EditorPanel"; +import DraggableTab from "../DraggableTabs/DraggableTab"; +import DroppableTabList from "../DraggableTabs/DroppableTabList"; +import RunBar from "../../RunButton/RunBar"; -import './EditorInput.scss' +import "./EditorInput.scss"; const EditorInput = () => { - const project = useSelector((state) => state.editor.project) - const openFiles = useSelector((state) => state.editor.openFiles) - const focussedFileIndices = useSelector((state) => state.editor.focussedFileIndices) - const dispatch = useDispatch() + const project = useSelector((state) => state.editor.project); + const openFiles = useSelector((state) => state.editor.openFiles); + const focussedFileIndices = useSelector( + (state) => state.editor.focussedFileIndices, + ); + const dispatch = useDispatch(); const onDragStart = (input) => { - const { source } = input - dispatch(setFocussedFileIndex({panelIndex: parseInt(source.droppableId), fileIndex: source.index})) - } + const { source } = input; + dispatch( + setFocussedFileIndex({ + panelIndex: parseInt(source.droppableId), + fileIndex: source.index, + }), + ); + }; const onDragEnd = (result) => { - const { source, destination } = result - if (!destination) return + const { source, destination } = result; + if (!destination) return; - let openFilesData = [...openFiles] - let oldPane = [...openFilesData[source.droppableId]] - const [removed] = oldPane.splice(source.index, 1) - openFilesData[source.droppableId] = [...oldPane] + let openFilesData = [...openFiles]; + let oldPane = [...openFilesData[source.droppableId]]; + const [removed] = oldPane.splice(source.index, 1); + openFilesData[source.droppableId] = [...oldPane]; - let newPane = [...openFilesData[destination.droppableId]] - newPane.splice(destination.index, 0, removed) - openFilesData[destination.droppableId] = [...newPane] - dispatch(setOpenFiles(openFilesData)) - dispatch(setFocussedFileIndex({panelIndex: parseInt(destination.droppableId), fileIndex: destination.index})) + let newPane = [...openFilesData[destination.droppableId]]; + newPane.splice(destination.index, 0, removed); + openFilesData[destination.droppableId] = [...newPane]; + dispatch(setOpenFiles(openFilesData)); + dispatch( + setFocussedFileIndex({ + panelIndex: parseInt(destination.droppableId), + fileIndex: destination.index, + }), + ); if (destination.droppableId !== source.droppableId) { - dispatch(setFocussedFileIndex({panelIndex: parseInt(source.droppableId), fileIndex: Math.max(source.index - 1, 0)})) + dispatch( + setFocussedFileIndex({ + panelIndex: parseInt(source.droppableId), + fileIndex: Math.max(source.index - 1, 0), + }), + ); } - } + }; const closeFileTab = (e, fileName) => { - e.stopPropagation() - dispatch(closeFile(fileName)) - } + e.stopPropagation(); + dispatch(closeFile(fileName)); + }; - const [numberOfComponents, setNumberOfComponents] = useState(project.components.length) - let tabRefs = useRef(project.components.map(createRef)) + const [numberOfComponents, setNumberOfComponents] = useState( + project.components.length, + ); + let tabRefs = useRef(project.components.map(createRef)); useEffect(() => { - setNumberOfComponents(project.components.length) - Array(project.components.length).fill().forEach((_, i) => { - tabRefs.current[i] = tabRefs.current[i] || React.createRef(); - }) - }, [project]) + setNumberOfComponents(project.components.length); + Array(project.components.length) + .fill() + .forEach((_, i) => { + tabRefs.current[i] = tabRefs.current[i] || React.createRef(); + }); + }, [project]); useEffect(() => { focussedFileIndices.forEach((index, i) => { - const fileName = openFiles[i][index] - const componentIndex = project.components.findIndex(file => `${file.name}.${file.extension}`=== fileName) - const fileRef = tabRefs.current[componentIndex] + const fileName = openFiles[i][index]; + const componentIndex = project.components.findIndex( + (file) => `${file.name}.${file.extension}` === fileName, + ); + const fileRef = tabRefs.current[componentIndex]; if (fileRef && fileRef.current) { - fileRef.current.parentElement.scrollIntoView() + fileRef.current.parentElement.scrollIntoView(); } - }) - }, [focussedFileIndices, openFiles, numberOfComponents, project]) + }); + }, [focussedFileIndices, openFiles, numberOfComponents, project]); return ( - onDragStart(input)} onDragEnd={result => onDragEnd(result)}> -
+ onDragStart(input)} + onDragEnd={(result) => onDragEnd(result)} + > +
{openFiles.map((panel, panelIndex) => ( - {}}> -
+ {}} + > +
{panel.map((fileName, fileIndex) => ( { panelIndex={panelIndex} > `${file.name}.${file.extension}`===fileName)]} + className={classNames("react-tabs__tab-inner", { + "react-tabs__tab-inner--split": ![ + "main.py", + "index.html", + ].includes(fileName), + })} + ref={ + tabRefs.current[ + project.components.findIndex( + (file) => + `${file.name}.${file.extension}` === fileName, + ) + ] + } > {fileName} - {!["main.py", "index.html"].includes(fileName) ? -
{panel.map((fileName, i) => ( - + ))} @@ -110,6 +157,6 @@ const EditorInput = () => { ))}
- ) -} -export default EditorInput + ); +}; +export default EditorInput; diff --git a/src/components/Editor/EditorInput/EditorInput.test.js b/src/components/Editor/EditorInput/EditorInput.test.js index 110881f35..0b7d82c38 100644 --- a/src/components/Editor/EditorInput/EditorInput.test.js +++ b/src/components/Editor/EditorInput/EditorInput.test.js @@ -1,82 +1,111 @@ -import React from "react" -import configureStore from 'redux-mock-store' -import EditorInput from "./EditorInput" -import { fireEvent, render, screen, waitFor } from "@testing-library/react" -import { Provider } from "react-redux" -import { closeFile, setFocussedFileIndex, setOpenFiles } from "../EditorSlice" +import React from "react"; +import configureStore from "redux-mock-store"; +import EditorInput from "./EditorInput"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { Provider } from "react-redux"; +import { closeFile, setFocussedFileIndex, setOpenFiles } from "../EditorSlice"; -window.HTMLElement.prototype.scrollIntoView = jest.fn() +window.HTMLElement.prototype.scrollIntoView = jest.fn(); -describe('Tab interactions', () => { - let store +describe("Tab interactions", () => { + let store; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - name: 'main', - extension: 'py', - content: 'print("hello")' + name: "main", + extension: "py", + content: 'print("hello")', }, { - name: 'a', - extension: 'py', - content: '# Your code here' - } - ] + name: "a", + extension: "py", + content: "# Your code here", + }, + ], }, - openFiles: [['main.py', 'a.py']], - focussedFileIndices: [1] + openFiles: [["main.py", "a.py"]], + focussedFileIndices: [1], }, auth: { - user: null - } - } + user: null, + }, + }; store = mockStore(initialState); - render(
) - }) + render( + +
+ +
+
, + ); + }); test("Renders content of focussed file", () => { - expect(screen.queryByText('# Your code here')).toBeInTheDocument() - }) + expect(screen.queryByText("# Your code here")).toBeInTheDocument(); + }); test("Scrolls focussed file into view", () => { - expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled() - }) + expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled(); + }); - test('Clicking the file close button dispatches close action', () => { - const closeButton = screen.queryAllByRole('button')[2] - fireEvent.click(closeButton) - expect(store.getActions()).toEqual([closeFile('a.py')]) - }) + test("Clicking the file close button dispatches close action", () => { + const closeButton = screen.queryAllByRole("button")[2]; + fireEvent.click(closeButton); + expect(store.getActions()).toEqual([closeFile("a.py")]); + }); - test('Focusses tab when dragged', async () => { - const tab = screen.queryByText('main.py').parentElement.parentElement - fireEvent.keyDown(tab, {key: ' ', keyCode: 32, code: 'Space'}) - await waitFor(() => expect(store.getActions()).toEqual([setFocussedFileIndex({panelIndex: 0, fileIndex: 0})])) - }) + test("Focusses tab when dragged", async () => { + const tab = screen.queryByText("main.py").parentElement.parentElement; + fireEvent.keyDown(tab, { key: " ", keyCode: 32, code: "Space" }); + await waitFor(() => + expect(store.getActions()).toEqual([ + setFocussedFileIndex({ panelIndex: 0, fileIndex: 0 }), + ]), + ); + }); - test('moves tab correctly when dropped', async () => { - const tab = screen.queryByText('main.py').parentElement.parentElement - fireEvent.keyDown(tab, {key: ' ', keyCode: 32, code: 'Space'}) - fireEvent.keyDown(tab, {key: 'ArrowRight', keyCode: 39, code: 'ArrowRight'}) - fireEvent.keyDown(tab, {key: ' ', keyCode: 32, code: 'Space'}) + test("moves tab correctly when dropped", async () => { + const tab = screen.queryByText("main.py").parentElement.parentElement; + fireEvent.keyDown(tab, { key: " ", keyCode: 32, code: "Space" }); + fireEvent.keyDown(tab, { + key: "ArrowRight", + keyCode: 39, + code: "ArrowRight", + }); + fireEvent.keyDown(tab, { key: " ", keyCode: 32, code: "Space" }); - const moveTabAction = setOpenFiles([['a.py', 'main.py']]) - await waitFor(() => expect(store.getActions()).toEqual(expect.arrayContaining([moveTabAction]))) - }) + const moveTabAction = setOpenFiles([["a.py", "main.py"]]); + await waitFor(() => + expect(store.getActions()).toEqual( + expect.arrayContaining([moveTabAction]), + ), + ); + }); - test('focusses dropped tab', async () => { - const tab = screen.queryByText('main.py').parentElement.parentElement - fireEvent.keyDown(tab, {key: ' ', keyCode: 32, code: 'Space'}) - fireEvent.keyDown(tab, {key: 'ArrowRight', keyCode: 39, code: 'ArrowRight'}) - fireEvent.keyDown(tab, {key: ' ', keyCode: 32, code: 'Space'}) + test("focusses dropped tab", async () => { + const tab = screen.queryByText("main.py").parentElement.parentElement; + fireEvent.keyDown(tab, { key: " ", keyCode: 32, code: "Space" }); + fireEvent.keyDown(tab, { + key: "ArrowRight", + keyCode: 39, + code: "ArrowRight", + }); + fireEvent.keyDown(tab, { key: " ", keyCode: 32, code: "Space" }); - const switchFocusAction = setFocussedFileIndex({panelIndex: 0, fileIndex: 1}) - await waitFor(() => expect(store.getActions()).toEqual(expect.arrayContaining([switchFocusAction]))) - }) -}) + const switchFocusAction = setFocussedFileIndex({ + panelIndex: 0, + fileIndex: 1, + }); + await waitFor(() => + expect(store.getActions()).toEqual( + expect.arrayContaining([switchFocusAction]), + ), + ); + }); +}); diff --git a/src/components/Editor/EditorPanel/EditorPanel.js b/src/components/Editor/EditorPanel/EditorPanel.js index 0dadaa9c5..8f89e2e87 100644 --- a/src/components/Editor/EditorPanel/EditorPanel.js +++ b/src/components/Editor/EditorPanel/EditorPanel.js @@ -1,64 +1,74 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import './EditorPanel.scss' -import React, { useRef, useEffect, useContext } from 'react'; -import { useSelector, useDispatch } from 'react-redux' -import { updateProjectComponent } from '../EditorSlice' -import { useCookies } from 'react-cookie'; +import "./EditorPanel.scss"; +import React, { useRef, useEffect, useContext } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { updateProjectComponent } from "../EditorSlice"; +import { useCookies } from "react-cookie"; import { useTranslation } from "react-i18next"; -import { basicSetup } from 'codemirror' -import { EditorView, keymap } from '@codemirror/view' -import { EditorState } from '@codemirror/state' -import { defaultKeymap, indentWithTab } from '@codemirror/commands' -import { indentationMarkers } from '@replit/codemirror-indentation-markers' -import { indentUnit } from '@codemirror/language'; +import { basicSetup } from "codemirror"; +import { EditorView, keymap } from "@codemirror/view"; +import { EditorState } from "@codemirror/state"; +import { defaultKeymap, indentWithTab } from "@codemirror/commands"; +import { indentationMarkers } from "@replit/codemirror-indentation-markers"; +import { indentUnit } from "@codemirror/language"; -import { html } from '@codemirror/lang-html' -import { css } from '@codemirror/lang-css' -import { python } from '@codemirror/lang-python' +import { html } from "@codemirror/lang-html"; +import { css } from "@codemirror/lang-css"; +import { python } from "@codemirror/lang-python"; -import { editorLightTheme } from '../editorLightTheme' -import { editorDarkTheme } from '../editorDarkTheme' -import { SettingsContext } from '../../../settings'; +import { editorLightTheme } from "../editorLightTheme"; +import { editorDarkTheme } from "../editorDarkTheme"; +import { SettingsContext } from "../../../settings"; -const EditorPanel = ({ - extension = 'html', - fileName = 'index' -}) => { +const EditorPanel = ({ extension = "html", fileName = "index" }) => { const editor = useRef(); const project = useSelector((state) => state.editor.project); - const [cookies] = useCookies(['theme', 'fontSize']) + const [cookies] = useCookies(["theme", "fontSize"]); const dispatch = useDispatch(); const { t } = useTranslation(); - const settings = useContext(SettingsContext) + const settings = useContext(SettingsContext); const updateStoredProject = (content) => { - dispatch(updateProjectComponent({ extension: extension, name: fileName, code: content})); - } + dispatch( + updateProjectComponent({ + extension: extension, + name: fileName, + code: content, + }), + ); + }; - const label = EditorView.contentAttributes.of({ 'aria-label': t('editorPanel.ariaLabel') }); + const label = EditorView.contentAttributes.of({ + "aria-label": t("editorPanel.ariaLabel"), + }); const onUpdate = EditorView.updateListener.of((viewUpdate) => { - if(viewUpdate.docChanged) { + if (viewUpdate.docChanged) { updateStoredProject(viewUpdate.state.doc.toString()); } }); const getMode = () => { switch (extension) { - case 'html': + case "html": return html(); - case 'css': + case "css": return css(); - case 'py': + case "py": return python(); default: return html(); } - } - const isDarkMode = cookies.theme==="dark" || (!cookies.theme && window.matchMedia("(prefers-color-scheme:dark)").matches) - const editorTheme = isDarkMode ? editorDarkTheme : editorLightTheme + }; + const isDarkMode = + cookies.theme === "dark" || + (!cookies.theme && + window.matchMedia("(prefers-color-scheme:dark)").matches); + const editorTheme = isDarkMode ? editorDarkTheme : editorLightTheme; useEffect(() => { - const code = project.components.find(item => item.extension === extension && item.name === fileName).content; + const code = project.components.find( + (item) => item.extension === extension && item.name === fileName, + ).content; const mode = getMode(); const startState = EditorState.create({ doc: code, @@ -70,7 +80,7 @@ const EditorPanel = ({ onUpdate, editorTheme, indentationMarkers(), - indentUnit.of(' '), + indentUnit.of(" "), ], }); @@ -81,12 +91,13 @@ const EditorPanel = ({ }); // 'aria-hidden' to fix keyboard access accessibility error - view.scrollDOM.setAttribute('aria-hidden', 'true') + view.scrollDOM.setAttribute("aria-hidden", "true"); // Add alt text to hidden images to fix accessibility error - const hiddenImages = view.contentDOM.getElementsByClassName('cm-widgetBuffer'); + const hiddenImages = + view.contentDOM.getElementsByClassName("cm-widgetBuffer"); for (let img of hiddenImages) { - img.setAttribute('role', 'presentation') + img.setAttribute("role", "presentation"); } return () => { @@ -97,6 +108,6 @@ const EditorPanel = ({ return (
); -} +}; export default EditorPanel; diff --git a/src/components/Editor/EditorPanel/EditorPanel.test.js b/src/components/Editor/EditorPanel/EditorPanel.test.js index cc879ba17..c18c5c953 100644 --- a/src/components/Editor/EditorPanel/EditorPanel.test.js +++ b/src/components/Editor/EditorPanel/EditorPanel.test.js @@ -1,45 +1,44 @@ -import configureStore from 'redux-mock-store' -import { Provider } from "react-redux" -import { SettingsContext } from "../../../settings" -import { render } from '@testing-library/react' -import { axe, toHaveNoViolations }from 'jest-axe' -import EditorPanel from './EditorPanel' +import configureStore from "redux-mock-store"; +import { Provider } from "react-redux"; +import { SettingsContext } from "../../../settings"; +import { render } from "@testing-library/react"; +import { axe, toHaveNoViolations } from "jest-axe"; +import EditorPanel from "./EditorPanel"; -expect.extend(toHaveNoViolations) +expect.extend(toHaveNoViolations); -describe('When font size is set', () => { +describe("When font size is set", () => { + let editor; - let editor - beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { - components: [ - {name: 'main', extension: 'py', content: ''} - ] - } - } - } + components: [{ name: "main", extension: "py", content: "" }], + }, + }, + }; const store = mockStore(initialState); const editorContainer = render( - - + + - - ) - editor = editorContainer.container.querySelector('.editor') - }) + , + ); + editor = editorContainer.container.querySelector(".editor"); + }); - test('Font size class is set correctly', () => { - expect(editor).toHaveClass("editor--myFontSize") - }) + test("Font size class is set correctly", () => { + expect(editor).toHaveClass("editor--myFontSize"); + }); - test('Editor panel has no AXE violations', async () => { - const axeResults = await axe(editor) - expect(axeResults).toHaveNoViolations() - }) -}) + test("Editor panel has no AXE violations", async () => { + const axeResults = await axe(editor); + expect(axeResults).toHaveNoViolations(); + }); +}); diff --git a/src/components/Editor/EditorSlice.js b/src/components/Editor/EditorSlice.js index f6e19d4df..c9dbc4dac 100644 --- a/src/components/Editor/EditorSlice.js +++ b/src/components/Editor/EditorSlice.js @@ -13,7 +13,7 @@ export const syncProject = (actionName) => `editor/${actionName}Project`, async ( { project, identifier, locale, accessToken, autosave }, - { rejectWithValue } + { rejectWithValue }, ) => { let response; switch (actionName) { @@ -52,7 +52,7 @@ export const syncProject = (actionName) => return false; } }, - } + }, ); export const loadProjectList = createAsyncThunk( @@ -64,7 +64,7 @@ export const loadProjectList = createAsyncThunk( page, links: parseLinkHeader(response.headers.link), }; - } + }, ); export const EditorSlice = createSlice({ @@ -110,32 +110,42 @@ export const EditorSlice = createSlice({ }, reducers: { closeFile: (state, action) => { - const panelIndex = state.openFiles.map((fileNames) => fileNames.includes(action.payload)).indexOf(true) - const closedFileIndex = state.openFiles[panelIndex].indexOf(action.payload) - state.openFiles[panelIndex] = state.openFiles[panelIndex].filter(fileName => fileName !== action.payload) + const panelIndex = state.openFiles + .map((fileNames) => fileNames.includes(action.payload)) + .indexOf(true); + const closedFileIndex = state.openFiles[panelIndex].indexOf( + action.payload, + ); + state.openFiles[panelIndex] = state.openFiles[panelIndex].filter( + (fileName) => fileName !== action.payload, + ); if ( - state.focussedFileIndices[panelIndex] >= state.openFiles[panelIndex].length || + state.focussedFileIndices[panelIndex] >= + state.openFiles[panelIndex].length || closedFileIndex < state.focussedFileIndices[panelIndex] ) { - state.focussedFileIndices[panelIndex]-- + state.focussedFileIndices[panelIndex]--; } }, openFile: (state, action) => { - const firstPanelIndex = 0 + const firstPanelIndex = 0; if (!state.openFiles.flat().includes(action.payload)) { - state.openFiles[firstPanelIndex].push(action.payload) + state.openFiles[firstPanelIndex].push(action.payload); } - state.focussedFileIndices[firstPanelIndex] = state.openFiles[firstPanelIndex].indexOf(action.payload) + state.focussedFileIndices[firstPanelIndex] = state.openFiles[ + firstPanelIndex + ].indexOf(action.payload); }, setOpenFiles: (state, action) => { - state.openFiles = action.payload + state.openFiles = action.payload; }, addFilePanel: (state) => { - state.openFiles.push([]) - state.focussedFileIndices.push(0) + state.openFiles.push([]); + state.focussedFileIndices.push(0); }, setFocussedFileIndex: (state, action) => { - state.focussedFileIndices[action.payload.panelIndex] = action.payload.fileIndex + state.focussedFileIndices[action.payload.panelIndex] = + action.payload.fileIndex; }, updateImages: (state, action) => { if (!state.project.image_list) { @@ -168,13 +178,13 @@ export const EditorSlice = createSlice({ if (!state.project.image_list) { state.project.image_list = []; } - state.loading="success" + state.loading = "success"; if (state.openFiles.flat().length === 0) { - const firstPanelIndex = 0 + const firstPanelIndex = 0; if (state.project.project_type === "html") { - state.openFiles[firstPanelIndex].push("index.html") + state.openFiles[firstPanelIndex].push("index.html"); } else { - state.openFiles[firstPanelIndex].push("main.py") + state.openFiles[firstPanelIndex].push("main.py"); } } state.justLoaded = true; @@ -217,9 +227,11 @@ export const EditorSlice = createSlice({ state.project.components[key].name = name; state.project.components[key].extension = extension; if (state.openFiles.flat().includes(oldName)) { - const panelIndex = state.openFiles.map((fileNames) => fileNames.includes(oldName)).indexOf(true) - const fileIndex = state.openFiles[panelIndex].indexOf(oldName) - state.openFiles[panelIndex][fileIndex] = `${name}.${extension}` + const panelIndex = state.openFiles + .map((fileNames) => fileNames.includes(oldName)) + .indexOf(true); + const fileIndex = state.openFiles[panelIndex].indexOf(oldName); + state.openFiles[panelIndex][fileIndex] = `${name}.${extension}`; } state.saving = "idle"; }, @@ -352,7 +364,7 @@ export const EditorSlice = createSlice({ state.saving = "idle"; state.currentLoadingRequestId = undefined; if (state.openFiles.flat().length === 0) { - const firstPanelIndex = 0 + const firstPanelIndex = 0; if (state.project.project_type === "html") { state.openFiles[firstPanelIndex].push("index.html"); } else { diff --git a/src/components/Editor/EditorSlice.test.js b/src/components/Editor/EditorSlice.test.js index 4a7a6d040..a53c3b702 100644 --- a/src/components/Editor/EditorSlice.test.js +++ b/src/components/Editor/EditorSlice.test.js @@ -1,4 +1,10 @@ -import { createOrUpdateProject, createRemix, deleteProject, readProject, readProjectList } from '../../utils/apiCallHandler'; +import { + createOrUpdateProject, + createRemix, + deleteProject, + readProject, + readProjectList, +} from "../../utils/apiCallHandler"; import reducer, { syncProject, @@ -12,585 +18,677 @@ import reducer, { loadProjectList, } from "./EditorSlice"; -jest.mock('../../utils/apiCallHandler') +jest.mock("../../utils/apiCallHandler"); test("Action stopCodeRun sets codeRunStopped to true", () => { const previousState = { codeRunTriggered: true, - codeRunStopped: false + codeRunStopped: false, }; const expectedState = { codeRunTriggered: true, - codeRunStopped: true - } - expect(reducer(previousState, stopCodeRun())).toEqual(expectedState) -}) + codeRunStopped: true, + }; + expect(reducer(previousState, stopCodeRun())).toEqual(expectedState); +}); test("Showing rename modal sets file state and showing status", () => { const previousState = { renameFileModalShowing: false, modals: {}, - } + }; const expectedState = { renameFileModalShowing: true, modals: { renameFile: { - name: 'main', - ext: '.py', - fileKey: 0 - } + name: "main", + ext: ".py", + fileKey: 0, + }, }, - } - expect(reducer(previousState, showRenameFileModal({name: 'main', ext: '.py', fileKey: 0}))).toEqual(expectedState) -}) + }; + expect( + reducer( + previousState, + showRenameFileModal({ name: "main", ext: ".py", fileKey: 0 }), + ), + ).toEqual(expectedState); +}); test("closing rename modal updates showing status", () => { const previousState = { - nameError: 'some error', + nameError: "some error", renameFileModalShowing: true, modals: { renameFile: { - name: 'main', - ext: '.py', - fileKey: 0 - } - } - } + name: "main", + ext: ".py", + fileKey: 0, + }, + }, + }; const expectedState = { - nameError: '', + nameError: "", renameFileModalShowing: false, modals: { renameFile: { - name: 'main', - ext: '.py', - fileKey: 0 - } - } - } - expect(reducer(previousState, closeRenameFileModal())).toEqual(expectedState) -}) - -describe('When project has no identifier', () => { - const dispatch = jest.fn() + name: "main", + ext: ".py", + fileKey: 0, + }, + }, + }; + expect(reducer(previousState, closeRenameFileModal())).toEqual(expectedState); +}); + +describe("When project has no identifier", () => { + const dispatch = jest.fn(); const project = { - name: 'hello world', - project_type: 'python', + name: "hello world", + project_type: "python", components: [ { - name: 'main', - extension: 'py', - content: '# hello' - } - ] - } - const access_token = 'myToken' + name: "main", + extension: "py", + content: "# hello", + }, + ], + }; + const access_token = "myToken"; const initialState = { editor: { project: project, - saving: 'idle', + saving: "idle", }, auth: { - isLoadingUser: false - } - } + isLoadingUser: false, + }, + }; - let saveThunk - let saveAction + let saveThunk; + let saveAction; beforeEach(() => { - Date.now = jest.fn(() => 1669808953) - saveThunk= syncProject('save') - saveAction = saveThunk({ project, accessToken: access_token, autosave: false }) - }) - - test('Saving creates new project', async () => { - await saveAction(dispatch, () => initialState) - expect(createOrUpdateProject).toHaveBeenCalledWith(project, access_token) - }) - - test('Successfully creating project triggers fulfilled action', async () => { - createOrUpdateProject.mockImplementationOnce(() => Promise.resolve({ status: 200 })) - await saveAction(dispatch, () => initialState) - expect(dispatch.mock.calls[1][0].type).toBe('editor/saveProject/fulfilled') - }) - - test('The saveProject/fulfilled action sets saving to success and loaded to idle', async () => { - const returnedProject = {...project, identifier: 'auto-generated-identifier'} + Date.now = jest.fn(() => 1669808953); + saveThunk = syncProject("save"); + saveAction = saveThunk({ + project, + accessToken: access_token, + autosave: false, + }); + }); + + test("Saving creates new project", async () => { + await saveAction(dispatch, () => initialState); + expect(createOrUpdateProject).toHaveBeenCalledWith(project, access_token); + }); + + test("Successfully creating project triggers fulfilled action", async () => { + createOrUpdateProject.mockImplementationOnce(() => + Promise.resolve({ status: 200 }), + ); + await saveAction(dispatch, () => initialState); + expect(dispatch.mock.calls[1][0].type).toBe("editor/saveProject/fulfilled"); + }); + + test("The saveProject/fulfilled action sets saving to success and loaded to idle", async () => { + const returnedProject = { + ...project, + identifier: "auto-generated-identifier", + }; const expectedState = { project: returnedProject, - saving: 'success', + saving: "success", lastSavedTime: 1669808953, - loading: 'idle' - } + loading: "idle", + }; - expect(reducer(initialState.editor, saveThunk.fulfilled({ project: returnedProject }))).toEqual(expectedState) - }) + expect( + reducer( + initialState.editor, + saveThunk.fulfilled({ project: returnedProject }), + ), + ).toEqual(expectedState); + }); // TODO: Autosave state testing +}); -}) - -describe('When project has an identifier', () => { - const dispatch = jest.fn() +describe("When project has an identifier", () => { + const dispatch = jest.fn(); const project = { - name: 'hello world', - project_type: 'python', - identifier: 'my-project-identifier', + name: "hello world", + project_type: "python", + identifier: "my-project-identifier", components: [ { - name: 'main', - extension: 'py', - content: '# hello' - } + name: "main", + extension: "py", + content: "# hello", + }, ], - image_list: [] - } - const access_token = 'myToken' + image_list: [], + }; + const access_token = "myToken"; const initialState = { editor: { project: project, - saving: 'idle' + saving: "idle", }, auth: { - isLoadingUser: false - } - } + isLoadingUser: false, + }, + }; - let saveThunk - let saveAction + let saveThunk; + let saveAction; - let remixThunk - let remixAction + let remixThunk; + let remixAction; beforeEach(() => { - saveThunk= syncProject('save') - saveAction = saveThunk({ project, accessToken: access_token, autosave: false }) - remixThunk = syncProject('remix') - remixAction = remixThunk({ project, accessToken: access_token }) - }) - - test('Saving updates existing project', async () => { - await saveAction(dispatch, () => initialState) - expect(createOrUpdateProject).toHaveBeenCalledWith(project, access_token) - }) - - test('Successfully updating project triggers fulfilled action', async () => { - createOrUpdateProject.mockImplementationOnce(() => Promise.resolve({ status: 200 })) - await saveAction(dispatch, () => initialState) - expect(dispatch.mock.calls[1][0].type).toBe('editor/saveProject/fulfilled') - }) - - test('The saveProject/fulfilled action sets saving to success', async () => { + saveThunk = syncProject("save"); + saveAction = saveThunk({ + project, + accessToken: access_token, + autosave: false, + }); + remixThunk = syncProject("remix"); + remixAction = remixThunk({ project, accessToken: access_token }); + }); + + test("Saving updates existing project", async () => { + await saveAction(dispatch, () => initialState); + expect(createOrUpdateProject).toHaveBeenCalledWith(project, access_token); + }); + + test("Successfully updating project triggers fulfilled action", async () => { + createOrUpdateProject.mockImplementationOnce(() => + Promise.resolve({ status: 200 }), + ); + await saveAction(dispatch, () => initialState); + expect(dispatch.mock.calls[1][0].type).toBe("editor/saveProject/fulfilled"); + }); + + test("The saveProject/fulfilled action sets saving to success", async () => { const expectedState = { project: project, - saving: 'success' - } - - expect(reducer(initialState.editor, saveThunk.fulfilled({ project }))).toEqual(expectedState) - }) - - test('Remixing triggers createRemix API call', async () => { - await remixAction(dispatch, () => initialState) - expect(createRemix).toHaveBeenCalledWith(project, access_token) - }) - - test('Successfully remixing project triggers fulfilled action', async () => { - createRemix.mockImplementationOnce(() => Promise.resolve({ status: 200 })) - await remixAction(dispatch, () => initialState) - expect(dispatch.mock.calls[1][0].type).toBe('editor/remixProject/fulfilled') - }) - - test('The remixProject/fulfilled action sets saving, loading and lastSaveAutosave', async () => { + saving: "success", + }; + + expect( + reducer(initialState.editor, saveThunk.fulfilled({ project })), + ).toEqual(expectedState); + }); + + test("Remixing triggers createRemix API call", async () => { + await remixAction(dispatch, () => initialState); + expect(createRemix).toHaveBeenCalledWith(project, access_token); + }); + + test("Successfully remixing project triggers fulfilled action", async () => { + createRemix.mockImplementationOnce(() => Promise.resolve({ status: 200 })); + await remixAction(dispatch, () => initialState); + expect(dispatch.mock.calls[1][0].type).toBe( + "editor/remixProject/fulfilled", + ); + }); + + test("The remixProject/fulfilled action sets saving, loading and lastSaveAutosave", async () => { const expectedState = { project: project, - saving: 'success', - loading: 'idle', - lastSaveAutosave: false - } - - expect(reducer(initialState.editor, remixThunk.fulfilled({ project }))).toEqual(expectedState) - }) -}) - -describe('When renaming a project from the rename project modal', () => { - let project = { name: 'hello world' } - const access_token = 'myToken' + saving: "success", + loading: "idle", + lastSaveAutosave: false, + }; + + expect( + reducer(initialState.editor, remixThunk.fulfilled({ project })), + ).toEqual(expectedState); + }); +}); + +describe("When renaming a project from the rename project modal", () => { + let project = { name: "hello world" }; + const access_token = "myToken"; const initialState = { editor: { project: {}, - modals: {renameProject: project}, + modals: { renameProject: project }, renameProjectModalShowing: true, - projectListLoaded: 'success' + projectListLoaded: "success", }, - auth: {user: {access_token}} - } + auth: { user: { access_token } }, + }; - let saveThunk + let saveThunk; beforeEach(() => { - saveThunk= syncProject('save') - }) + saveThunk = syncProject("save"); + }); - test('The saveProject/fulfilled action closes rename project modal and reloads projects list', () => { + test("The saveProject/fulfilled action closes rename project modal and reloads projects list", () => { const expectedState = { project: {}, - saving: 'success', + saving: "success", modals: { renameProject: null }, renameProjectModalShowing: false, - projectListLoaded: 'idle' - } - expect(reducer(initialState.editor, saveThunk.fulfilled({ project }))).toEqual(expectedState) - }) -}) - -describe('When deleting a project', () => { - const dispatch = jest.fn() - let project = { identifier: 'my-amazing-project', name: 'hello world' } - const access_token = 'myToken' + projectListLoaded: "idle", + }; + expect( + reducer(initialState.editor, saveThunk.fulfilled({ project })), + ).toEqual(expectedState); + }); +}); + +describe("When deleting a project", () => { + const dispatch = jest.fn(); + let project = { identifier: "my-amazing-project", name: "hello world" }; + const access_token = "myToken"; const initialState = { editor: { project: {}, - modals: {deleteProject: project}, + modals: { deleteProject: project }, deleteProjectModalShowing: true, - projectListLoaded: 'success' + projectListLoaded: "success", }, - auth: {user: {access_token}} - } + auth: { user: { access_token } }, + }; - let deleteThunk - let deleteAction + let deleteThunk; + let deleteAction; beforeEach(() => { - deleteThunk = syncProject('delete') - deleteAction = deleteThunk({ identifier: project.identifier, accessToken: access_token }) - }) - - test('Deleting a project triggers deleteProject API call', async () => { - await deleteAction(dispatch, () => initialState) - expect(deleteProject).toHaveBeenCalledWith(project.identifier, access_token) - }) - - test('Successfully deleting project triggers fulfilled action', async () => { - deleteProject.mockImplementationOnce(() => Promise.resolve({ status: 200 })) - await deleteAction(dispatch, () => initialState) - expect(dispatch.mock.calls[1][0].type).toBe('editor/deleteProject/fulfilled') - }) - - test('The deleteProject/fulfilled action closes delete project modal and reloads projects list', () => { + deleteThunk = syncProject("delete"); + deleteAction = deleteThunk({ + identifier: project.identifier, + accessToken: access_token, + }); + }); + + test("Deleting a project triggers deleteProject API call", async () => { + await deleteAction(dispatch, () => initialState); + expect(deleteProject).toHaveBeenCalledWith( + project.identifier, + access_token, + ); + }); + + test("Successfully deleting project triggers fulfilled action", async () => { + deleteProject.mockImplementationOnce(() => + Promise.resolve({ status: 200 }), + ); + await deleteAction(dispatch, () => initialState); + expect(dispatch.mock.calls[1][0].type).toBe( + "editor/deleteProject/fulfilled", + ); + }); + + test("The deleteProject/fulfilled action closes delete project modal and reloads projects list", () => { const expectedState = { project: {}, modals: { deleteProject: null }, deleteProjectModalShowing: false, - projectListLoaded: 'idle' - } - expect(reducer(initialState.editor, deleteThunk.fulfilled({}))).toEqual(expectedState) - }) -}) - -const requestingAProject = function(project, projectFile) { - const dispatch = jest.fn() + projectListLoaded: "idle", + }; + expect(reducer(initialState.editor, deleteThunk.fulfilled({}))).toEqual( + expectedState, + ); + }); +}); + +const requestingAProject = function (project, projectFile) { + const dispatch = jest.fn(); const initialState = { editor: { project: {}, - loading: 'idle' + loading: "idle", }, auth: { - isLoadingUser: false - } - } + isLoadingUser: false, + }, + }; - let loadThunk - let loadAction + let loadThunk; + let loadAction; - let loadFulfilledAction - let loadRejectedAction + let loadFulfilledAction; + let loadRejectedAction; beforeEach(() => { - loadThunk = syncProject('load') - loadAction = loadThunk({ identifier: 'my-project-identifier', locale: 'ja-JP', accessToken: 'my_token' }) - - loadFulfilledAction = loadThunk.fulfilled({ project }) - loadFulfilledAction.meta.requestId='my_request_id' - loadRejectedAction = loadThunk.rejected() - loadRejectedAction.meta.requestId='my_request_id' - }) - - test('Reads project from database', async () => { - await loadAction(dispatch, () => initialState) - expect(readProject).toHaveBeenCalledWith('my-project-identifier', 'ja-JP', 'my_token') - }) - - test('If loading status pending, loading success updates status', () => { + loadThunk = syncProject("load"); + loadAction = loadThunk({ + identifier: "my-project-identifier", + locale: "ja-JP", + accessToken: "my_token", + }); + + loadFulfilledAction = loadThunk.fulfilled({ project }); + loadFulfilledAction.meta.requestId = "my_request_id"; + loadRejectedAction = loadThunk.rejected(); + loadRejectedAction.meta.requestId = "my_request_id"; + }); + + test("Reads project from database", async () => { + await loadAction(dispatch, () => initialState); + expect(readProject).toHaveBeenCalledWith( + "my-project-identifier", + "ja-JP", + "my_token", + ); + }); + + test("If loading status pending, loading success updates status", () => { const initialState = { openFiles: [[]], - loading: 'pending', - currentLoadingRequestId: 'my_request_id' - } + loading: "pending", + currentLoadingRequestId: "my_request_id", + }; const expectedState = { openFiles: [[projectFile]], - loading: 'success', + loading: "success", justLoaded: true, - saving: 'idle', + saving: "idle", project: project, currentLoadingRequestId: undefined, - } - expect(reducer(initialState, loadFulfilledAction)).toEqual(expectedState) - }) + }; + expect(reducer(initialState, loadFulfilledAction)).toEqual(expectedState); + }); - test('If not latest request, loading success does not update status', () => { + test("If not latest request, loading success does not update status", () => { const initialState = { - loading: 'pending', - currentLoadingRequestId: 'another_request_id' - } - expect(reducer(initialState, loadFulfilledAction)).toEqual(initialState) - }) + loading: "pending", + currentLoadingRequestId: "another_request_id", + }; + expect(reducer(initialState, loadFulfilledAction)).toEqual(initialState); + }); - test('If already rejected, loading success does not update status', () => { + test("If already rejected, loading success does not update status", () => { const initialState = { - loading: 'failed' - } - expect(reducer(initialState, syncProject('load').fulfilled())).toEqual(initialState) - }) - - test('If loading status pending, loading failure updates status', () => { + loading: "failed", + }; + expect(reducer(initialState, syncProject("load").fulfilled())).toEqual( + initialState, + ); + }); + + test("If loading status pending, loading failure updates status", () => { const initialState = { - loading: 'pending', - currentLoadingRequestId: 'my_request_id' - } + loading: "pending", + currentLoadingRequestId: "my_request_id", + }; const expectedState = { - loading: 'failed', - saving: 'idle', - currentLoadingRequestId: undefined - } - expect(reducer(initialState, loadRejectedAction)).toEqual(expectedState) - }) - - test('If not latest request, loading failure does not update status', () => { + loading: "failed", + saving: "idle", + currentLoadingRequestId: undefined, + }; + expect(reducer(initialState, loadRejectedAction)).toEqual(expectedState); + }); + + test("If not latest request, loading failure does not update status", () => { const initialState = { - loading: 'pending', - currentLoadingRequestId: 'another_request_id' - } - expect(reducer(initialState, loadRejectedAction)).toEqual(initialState) - }) + loading: "pending", + currentLoadingRequestId: "another_request_id", + }; + expect(reducer(initialState, loadRejectedAction)).toEqual(initialState); + }); - test('If already fulfilled, loading rejection does not update status', () => { + test("If already fulfilled, loading rejection does not update status", () => { const initialState = { - loading: 'success' - } - expect(reducer(initialState, loadThunk.rejected())).toEqual(initialState) - }) -} + loading: "success", + }; + expect(reducer(initialState, loadThunk.rejected())).toEqual(initialState); + }); +}; -describe('When requesting a python project', () => { +describe("When requesting a python project", () => { const project = { - name: 'hello world', - project_type: 'python', - identifier: 'my-project-identifier', + name: "hello world", + project_type: "python", + identifier: "my-project-identifier", components: [ { - name: 'main', - extension: 'py', - content: '# hello' - } + name: "main", + extension: "py", + content: "# hello", + }, ], - image_list: [] - } - requestingAProject(project, 'main.py') -}) + image_list: [], + }; + requestingAProject(project, "main.py"); +}); -describe('When requesting a HTML project', () => { +describe("When requesting a HTML project", () => { const project = { - name: 'hello html world', - project_type: 'html', - identifier: 'my-project-identifier', + name: "hello html world", + project_type: "html", + identifier: "my-project-identifier", components: [ { - name: 'index', - extension: 'html', - content: '# hello world' - } + name: "index", + extension: "html", + content: "# hello world", + }, ], - image_list: [] - } - requestingAProject(project, 'index.html') -}) - -describe('When requesting project list', () => { - const dispatch = jest.fn() - const projects = [ - { name: 'project1' }, - { name: 'project2' } - ] + image_list: [], + }; + requestingAProject(project, "index.html"); +}); + +describe("When requesting project list", () => { + const dispatch = jest.fn(); + const projects = [{ name: "project1" }, { name: "project2" }]; const initialState = { projectList: [], - projectListLoaded: 'pending', - projectIndexCurrentPage: 4 - } - let loadProjectListThunk + projectListLoaded: "pending", + projectIndexCurrentPage: 4, + }; + let loadProjectListThunk; beforeEach(() => { - loadProjectListThunk = loadProjectList({page: 12, accessToken: 'access_token'}) - }) - - test('Loading project list triggers loadProjectList API call', async () => { - await loadProjectListThunk(dispatch, () => initialState) - expect(readProjectList).toHaveBeenCalledWith(12, 'access_token') - }) - - test('Successfully loading project list triggers fulfilled action', async () => { - readProjectList.mockImplementationOnce(() => Promise.resolve({ status: 200, headers: {}})) - await loadProjectListThunk(dispatch, () => initialState) - expect(dispatch.mock.calls[1][0].type).toBe('editor/loadProjectList/fulfilled') - }) - - test('The loadProjectList/fulfilled action with projects returned sets the projectList and total pages', () => { + loadProjectListThunk = loadProjectList({ + page: 12, + accessToken: "access_token", + }); + }); + + test("Loading project list triggers loadProjectList API call", async () => { + await loadProjectListThunk(dispatch, () => initialState); + expect(readProjectList).toHaveBeenCalledWith(12, "access_token"); + }); + + test("Successfully loading project list triggers fulfilled action", async () => { + readProjectList.mockImplementationOnce(() => + Promise.resolve({ status: 200, headers: {} }), + ); + await loadProjectListThunk(dispatch, () => initialState); + expect(dispatch.mock.calls[1][0].type).toBe( + "editor/loadProjectList/fulfilled", + ); + }); + + test("The loadProjectList/fulfilled action with projects returned sets the projectList and total pages", () => { const expectedState = { projectList: projects, - projectListLoaded: 'success', + projectListLoaded: "success", projectIndexCurrentPage: 4, - projectIndexTotalPages: 12 - } - expect(reducer(initialState, loadProjectList.fulfilled({projects, page: 4, links: {last: {page: 12}}}))).toEqual(expectedState) - }) - - test('The loadProjectList/fulfilled action with no projects loads previous page', () => { + projectIndexTotalPages: 12, + }; + expect( + reducer( + initialState, + loadProjectList.fulfilled({ + projects, + page: 4, + links: { last: { page: 12 } }, + }), + ), + ).toEqual(expectedState); + }); + + test("The loadProjectList/fulfilled action with no projects loads previous page", () => { const expectedState = { projectList: [], - projectListLoaded: 'idle', - projectIndexCurrentPage: 3 - } - expect(reducer(initialState, loadProjectList.fulfilled({projects: [], page: 4}))).toEqual(expectedState) - }) - - test('The loadProjectList/fulfilled action with no projects on page 1 sets loading to success', () => { + projectListLoaded: "idle", + projectIndexCurrentPage: 3, + }; + expect( + reducer( + initialState, + loadProjectList.fulfilled({ projects: [], page: 4 }), + ), + ).toEqual(expectedState); + }); + + test("The loadProjectList/fulfilled action with no projects on page 1 sets loading to success", () => { const expectedState = { projectList: [], - projectListLoaded: 'success', + projectListLoaded: "success", projectIndexCurrentPage: 1, - projectIndexTotalPages: 1 - } - expect(reducer({...initialState, projectIndexCurrentPage: 1}, loadProjectList.fulfilled({projects: [], page: 1}))).toEqual(expectedState) - }) -}) - -describe('Opening files', () => { + projectIndexTotalPages: 1, + }; + expect( + reducer( + { ...initialState, projectIndexCurrentPage: 1 }, + loadProjectList.fulfilled({ projects: [], page: 1 }), + ), + ).toEqual(expectedState); + }); +}); + +describe("Opening files", () => { const initialState = { - openFiles: [['main.py', 'file1.py']], - focussedFileIndices: [0] - } + openFiles: [["main.py", "file1.py"]], + focussedFileIndices: [0], + }; - test('Opening unopened file adds it to openFiles and focusses that file', () => { + test("Opening unopened file adds it to openFiles and focusses that file", () => { const expectedState = { - openFiles: [['main.py', 'file1.py', 'file2.py']], - focussedFileIndices: [2] - } - expect(reducer(initialState, openFile('file2.py'))).toEqual(expectedState) - }) + openFiles: [["main.py", "file1.py", "file2.py"]], + focussedFileIndices: [2], + }; + expect(reducer(initialState, openFile("file2.py"))).toEqual(expectedState); + }); - test('Opening already open file focusses that file', () => { + test("Opening already open file focusses that file", () => { const expectedState = { - openFiles: [['main.py', 'file1.py']], - focussedFileIndices: [1] - } - expect(reducer(initialState, openFile('file1.py'))).toEqual(expectedState) - }) + openFiles: [["main.py", "file1.py"]], + focussedFileIndices: [1], + }; + expect(reducer(initialState, openFile("file1.py"))).toEqual(expectedState); + }); - test('Switching file focus', () => { + test("Switching file focus", () => { const expectedState = { - openFiles: [['main.py', 'file1.py']], - focussedFileIndices: [1] - } - expect(reducer(initialState, setFocussedFileIndex({panelIndex: 0, fileIndex: 1}))).toEqual(expectedState) - }) -}) - -describe('Closing files', () => { - test('Closing the last file when focussed transfers focus to the left', () => { + openFiles: [["main.py", "file1.py"]], + focussedFileIndices: [1], + }; + expect( + reducer( + initialState, + setFocussedFileIndex({ panelIndex: 0, fileIndex: 1 }), + ), + ).toEqual(expectedState); + }); +}); + +describe("Closing files", () => { + test("Closing the last file when focussed transfers focus to the left", () => { const initialState = { - openFiles: [['main.py', 'file1.py']], - focussedFileIndices: [1] - } + openFiles: [["main.py", "file1.py"]], + focussedFileIndices: [1], + }; const expectedState = { - openFiles: [['main.py']], - focussedFileIndices: [0] - } - expect(reducer(initialState, closeFile('file1.py'))).toEqual(expectedState) - }) + openFiles: [["main.py"]], + focussedFileIndices: [0], + }; + expect(reducer(initialState, closeFile("file1.py"))).toEqual(expectedState); + }); - test('Closing not the last file when focussed does not change focus', () => { + test("Closing not the last file when focussed does not change focus", () => { const initialState = { - openFiles: [['main.py', 'file1.py', 'file2.py']], - focussedFileIndices: [1] - } + openFiles: [["main.py", "file1.py", "file2.py"]], + focussedFileIndices: [1], + }; const expectedState = { - openFiles: [['main.py', 'file2.py']], - focussedFileIndices: [1] - } - expect(reducer(initialState, closeFile('file1.py'))).toEqual(expectedState) - }) + openFiles: [["main.py", "file2.py"]], + focussedFileIndices: [1], + }; + expect(reducer(initialState, closeFile("file1.py"))).toEqual(expectedState); + }); - test('Closing unfocussed file before file that is in focus keeps same file in focus', () => { + test("Closing unfocussed file before file that is in focus keeps same file in focus", () => { const initialState = { - openFiles: [['main.py', 'file1.py', 'file2.py', 'file3.py']], - focussedFileIndices: [2] - } + openFiles: [["main.py", "file1.py", "file2.py", "file3.py"]], + focussedFileIndices: [2], + }; const expectedState = { - openFiles: [['main.py', 'file2.py', 'file3.py']], - focussedFileIndices: [1] - } - expect(reducer(initialState, closeFile('file1.py'))).toEqual(expectedState) - }) + openFiles: [["main.py", "file2.py", "file3.py"]], + focussedFileIndices: [1], + }; + expect(reducer(initialState, closeFile("file1.py"))).toEqual(expectedState); + }); - test('Closing unfocussed file after file that is in focus keeps same file in focus', () => { + test("Closing unfocussed file after file that is in focus keeps same file in focus", () => { const initialState = { - openFiles: [['main.py', 'file1.py', 'file2.py', 'file3.py']], - focussedFileIndices: [1] - } + openFiles: [["main.py", "file1.py", "file2.py", "file3.py"]], + focussedFileIndices: [1], + }; const expectedState = { - openFiles: [['main.py', 'file1.py', 'file3.py']], - focussedFileIndices: [1] - } - expect(reducer(initialState, closeFile('file2.py'))).toEqual(expectedState) - }) -}) - -describe('Updating file name', () => { + openFiles: [["main.py", "file1.py", "file3.py"]], + focussedFileIndices: [1], + }; + expect(reducer(initialState, closeFile("file2.py"))).toEqual(expectedState); + }); +}); + +describe("Updating file name", () => { const initialState = { project: { components: [ - {name: 'file', extension: 'py' }, - {name: 'another_file', extension: 'py'} - ] + { name: "file", extension: "py" }, + { name: "another_file", extension: "py" }, + ], }, - openFiles: [['file.py']] - } + openFiles: [["file.py"]], + }; - test('If file is open updates name in project and openFiles and saves', () => { + test("If file is open updates name in project and openFiles and saves", () => { const expectedState = { project: { components: [ - {name: 'my_file', extension: 'py' }, - {name: 'another_file', extension: 'py'} - ] + { name: "my_file", extension: "py" }, + { name: "another_file", extension: "py" }, + ], }, - openFiles: [['my_file.py']], + openFiles: [["my_file.py"]], saving: "idle", - } - expect(reducer(initialState, updateComponentName({key: 0, name: 'my_file', extension: 'py'}))).toEqual(expectedState) - }) - - test('If file is closed updates name in project and saves', () => { + }; + expect( + reducer( + initialState, + updateComponentName({ key: 0, name: "my_file", extension: "py" }), + ), + ).toEqual(expectedState); + }); + + test("If file is closed updates name in project and saves", () => { const expectedState = { project: { components: [ - {name: 'file', extension: 'py' }, - {name: 'my_file', extension: 'py'} - ] + { name: "file", extension: "py" }, + { name: "my_file", extension: "py" }, + ], }, - openFiles: [['file.py']], + openFiles: [["file.py"]], saving: "idle", - } - expect(reducer(initialState, updateComponentName({key: 1, name: 'my_file', extension: 'py'}))).toEqual(expectedState) - }) -}) + }; + expect( + reducer( + initialState, + updateComponentName({ key: 1, name: "my_file", extension: "py" }), + ), + ).toEqual(expectedState); + }); +}); diff --git a/src/components/Editor/ErrorMessage/ErrorMessage.js b/src/components/Editor/ErrorMessage/ErrorMessage.js index f2efa986b..ee2d151c9 100644 --- a/src/components/Editor/ErrorMessage/ErrorMessage.js +++ b/src/components/Editor/ErrorMessage/ErrorMessage.js @@ -1,15 +1,15 @@ -import React, { useContext } from 'react' -import './ErrorMessage.scss' -import { useSelector } from 'react-redux' -import { SettingsContext } from '../../../settings'; +import React, { useContext } from "react"; +import "./ErrorMessage.scss"; +import { useSelector } from "react-redux"; +import { SettingsContext } from "../../../settings"; const ErrorMessage = () => { const error = useSelector((state) => state.editor.error); - const settings = useContext(SettingsContext) + const settings = useContext(SettingsContext); return error ? (
-

{ error }

+

{error}

) : null; }; diff --git a/src/components/Editor/ErrorMessage/ErrorMessage.test.js b/src/components/Editor/ErrorMessage/ErrorMessage.test.js index c9444f7b7..e45d7ba60 100644 --- a/src/components/Editor/ErrorMessage/ErrorMessage.test.js +++ b/src/components/Editor/ErrorMessage/ErrorMessage.test.js @@ -1,35 +1,36 @@ -import configureStore from 'redux-mock-store' -import { Provider } from "react-redux" -import { SettingsContext } from "../../../settings" -import ErrorMessage from "./ErrorMessage" -import { render, screen } from '@testing-library/react' +import configureStore from "redux-mock-store"; +import { Provider } from "react-redux"; +import { SettingsContext } from "../../../settings"; +import ErrorMessage from "./ErrorMessage"; +import { render, screen } from "@testing-library/react"; -describe('When error is set', () => { - +describe("When error is set", () => { beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { - error: 'Oops' - } - } + error: "Oops", + }, + }; const store = mockStore(initialState); render( - + - - ) - }) + , + ); + }); - test('Error message displays', () => { - expect(screen.queryByText('Oops')).toBeInTheDocument() - }) + test("Error message displays", () => { + expect(screen.queryByText("Oops")).toBeInTheDocument(); + }); - test('Font size class is set correctly', () => { - const errorMessage = screen.queryByText('Oops').parentElement - expect(errorMessage).toHaveClass("error-message--myFontSize") - }) -}) + test("Font size class is set correctly", () => { + const errorMessage = screen.queryByText("Oops").parentElement; + expect(errorMessage).toHaveClass("error-message--myFontSize"); + }); +}); diff --git a/src/components/Editor/ErrorMessage/NameErrorMessage.js b/src/components/Editor/ErrorMessage/NameErrorMessage.js index e8de91a99..78663e0f1 100644 --- a/src/components/Editor/ErrorMessage/NameErrorMessage.js +++ b/src/components/Editor/ErrorMessage/NameErrorMessage.js @@ -1,12 +1,12 @@ -import './ErrorMessage.scss' -import { useSelector } from 'react-redux' +import "./ErrorMessage.scss"; +import { useSelector } from "react-redux"; const NameErrorMessage = () => { const error = useSelector((state) => state.editor.nameError); return error ? ( -
-

{ error }

+
+

{error}

) : null; }; diff --git a/src/components/Editor/FontSizeSelector/FontSizeSelector.js b/src/components/Editor/FontSizeSelector/FontSizeSelector.js index d7858df12..af2e63632 100644 --- a/src/components/Editor/FontSizeSelector/FontSizeSelector.js +++ b/src/components/Editor/FontSizeSelector/FontSizeSelector.js @@ -2,46 +2,67 @@ import React from "react"; import { useCookies } from "react-cookie"; import { useTranslation } from "react-i18next"; import { FontIcon } from "../../../Icons"; -import './FontSizeSelector.scss' +import "./FontSizeSelector.scss"; -const COOKIE_PATHS = ['/', '/projects', '/python'] +const COOKIE_PATHS = ["/", "/projects", "/python"]; const FontSizeSelector = () => { - const [ cookies , setCookie, removeCookie] = useCookies(['fontSize']) - const fontSize = cookies.fontSize || "small" - const { t } = useTranslation() + const [cookies, setCookie, removeCookie] = useCookies(["fontSize"]); + const fontSize = cookies.fontSize || "small"; + const { t } = useTranslation(); const setFontSize = (fontSize) => { if (cookies.fontSize) { COOKIE_PATHS.forEach((path) => { - removeCookie('fontSize', {path}) - }) + removeCookie("fontSize", { path }); + }); } - setCookie('fontSize', fontSize, { path: '/' }) - } + setCookie("fontSize", fontSize, { path: "/" }); + }; return ( -
-
setFontSize('small')}> - -

{t('header.settingsMenu.textSizeOptions.small')}

+

{t("header.settingsMenu.textSizeOptions.small")}

-
setFontSize('medium')}> - -

{t('header.settingsMenu.textSizeOptions.medium')}

+

{t("header.settingsMenu.textSizeOptions.medium")}

-
setFontSize('large')}> - -

{t('header.settingsMenu.textSizeOptions.large')}

+

{t("header.settingsMenu.textSizeOptions.large")}

- ) -} + ); +}; -export default FontSizeSelector +export default FontSizeSelector; diff --git a/src/components/Editor/FontSizeSelector/FontSizeSelector.test.js b/src/components/Editor/FontSizeSelector/FontSizeSelector.test.js index e7472ac1e..fc1db32c1 100644 --- a/src/components/Editor/FontSizeSelector/FontSizeSelector.test.js +++ b/src/components/Editor/FontSizeSelector/FontSizeSelector.test.js @@ -1,7 +1,7 @@ import React from "react"; -import { render, fireEvent } from "@testing-library/react" +import { render, fireEvent } from "@testing-library/react"; import FontSizeSelector from "./FontSizeSelector"; -import { Cookies, CookiesProvider } from 'react-cookie'; +import { Cookies, CookiesProvider } from "react-cookie"; describe("When font size cookie unset", () => { let cookies; @@ -12,33 +12,39 @@ describe("When font size cookie unset", () => { fontSelector = render( - - ) - }) + , + ); + }); - test('Cookie remains unset after render', () => { - expect(cookies.cookies.fontSize).toBeUndefined() - }) + test("Cookie remains unset after render", () => { + expect(cookies.cookies.fontSize).toBeUndefined(); + }); - test('Sets cookie to large when first button clicked', async () => { - const largeButton = fontSelector.getByText('header.settingsMenu.textSizeOptions.large').parentElement - fireEvent.click(largeButton) - expect(cookies.cookies.fontSize).toBe("large") - }) + test("Sets cookie to large when first button clicked", async () => { + const largeButton = fontSelector.getByText( + "header.settingsMenu.textSizeOptions.large", + ).parentElement; + fireEvent.click(largeButton); + expect(cookies.cookies.fontSize).toBe("large"); + }); - test('Sets cookie to medium when second button clicked', async () => { - const mediumButton = fontSelector.getByText('header.settingsMenu.textSizeOptions.medium').parentElement - fireEvent.click(mediumButton) - expect(cookies.cookies.fontSize).toBe("medium") - }) + test("Sets cookie to medium when second button clicked", async () => { + const mediumButton = fontSelector.getByText( + "header.settingsMenu.textSizeOptions.medium", + ).parentElement; + fireEvent.click(mediumButton); + expect(cookies.cookies.fontSize).toBe("medium"); + }); - test('Sets cookie to small when third button clicked', async () => { - const smallButton = fontSelector.getByText('header.settingsMenu.textSizeOptions.small').parentElement - fireEvent.click(smallButton) - expect(cookies.cookies.fontSize).toBe("small") - }) + test("Sets cookie to small when third button clicked", async () => { + const smallButton = fontSelector.getByText( + "header.settingsMenu.textSizeOptions.small", + ).parentElement; + fireEvent.click(smallButton); + expect(cookies.cookies.fontSize).toBe("small"); + }); afterEach(() => { - cookies.remove("theme") - }) -}) + cookies.remove("theme"); + }); +}); diff --git a/src/components/Editor/Hooks/useEmbeddedMode.js b/src/components/Editor/Hooks/useEmbeddedMode.js index 3bdbf4809..e3432577b 100644 --- a/src/components/Editor/Hooks/useEmbeddedMode.js +++ b/src/components/Editor/Hooks/useEmbeddedMode.js @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { useEffect } from 'react'; -import { useDispatch } from 'react-redux' -import { setEmbedded } from '../EditorSlice' +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { setEmbedded } from "../EditorSlice"; export const useEmbeddedMode = (embed = false) => { const dispatch = useDispatch(); diff --git a/src/components/Editor/Hooks/useProject.js b/src/components/Editor/Hooks/useProject.js index 6ac28205f..602d054cc 100644 --- a/src/components/Editor/Hooks/useProject.js +++ b/src/components/Editor/Hooks/useProject.js @@ -9,7 +9,7 @@ export const useProject = (projectIdentifier = null, accessToken = null) => { const getCachedProject = (id) => JSON.parse(localStorage.getItem(id || "project")); const [cachedProject, setCachedProject] = useState( - getCachedProject(projectIdentifier) + getCachedProject(projectIdentifier), ); const { i18n } = useTranslation(); const dispatch = useDispatch(); @@ -40,7 +40,7 @@ export const useProject = (projectIdentifier = null, accessToken = null) => { identifier: projectIdentifier, locale: i18n.language, accessToken, - }) + }), ); return; } diff --git a/src/components/Editor/Hooks/useProject.test.js b/src/components/Editor/Hooks/useProject.test.js index 55ca440a8..d369457e1 100644 --- a/src/components/Editor/Hooks/useProject.test.js +++ b/src/components/Editor/Hooks/useProject.test.js @@ -55,7 +55,7 @@ test("If cached project does not match identifer does not use cached project", a localStorage.setItem("project", JSON.stringify(cachedProject)); renderHook(() => useProject("my-favourite-project")); await waitFor(() => - expect(setProject).not.toHaveBeenCalledWith(cachedProject) + expect(setProject).not.toHaveBeenCalledWith(cachedProject), ); }); @@ -68,7 +68,7 @@ test("If cached project does not match identifier loads correct uncached project identifier: project1.identifier, locale: "ja-JP", accessToken, - }) + }), ); }); @@ -80,7 +80,7 @@ test("If no cached project loads uncached project", async () => { identifier: "hello-world-project", locale: "ja-JP", accessToken, - }) + }), ); }); diff --git a/src/components/Editor/Hooks/useRequiresUser.js b/src/components/Editor/Hooks/useRequiresUser.js index ca7f99e97..088f7a672 100644 --- a/src/components/Editor/Hooks/useRequiresUser.js +++ b/src/components/Editor/Hooks/useRequiresUser.js @@ -1,18 +1,17 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom' +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; export const useRequiresUser = (isLoading, user) => { - const navigate = useNavigate() + const navigate = useNavigate(); useEffect(() => { - if(isLoading) { + if (isLoading) { return; } - if(!isLoading && !user) { - navigate('/') + if (!isLoading && !user) { + navigate("/"); } }, [isLoading, user]); }; - diff --git a/src/components/Editor/ImageUploadButton/ImageUploadButton.js b/src/components/Editor/ImageUploadButton/ImageUploadButton.js index ea5384d34..f3b69479f 100644 --- a/src/components/Editor/ImageUploadButton/ImageUploadButton.js +++ b/src/components/Editor/ImageUploadButton/ImageUploadButton.js @@ -1,139 +1,161 @@ -import './ImageUploadButton.css'; +import "./ImageUploadButton.css"; -import { useCallback, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux'; -import Dropzone from 'react-dropzone'; -import Modal from 'react-modal'; +import { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import Dropzone from "react-dropzone"; +import Modal from "react-modal"; -import { updateImages, setNameError } from '../EditorSlice'; -import Button from '../../Button/Button' -import NameErrorMessage from '../ErrorMessage/NameErrorMessage'; -import store from '../../../app/store'; -import { uploadImages } from '../../../utils/apiCallHandler'; +import { updateImages, setNameError } from "../EditorSlice"; +import Button from "../../Button/Button"; +import NameErrorMessage from "../ErrorMessage/NameErrorMessage"; +import store from "../../../app/store"; +import { uploadImages } from "../../../utils/apiCallHandler"; const allowedExtensions = { - "python": [ - "jpg", - "jpeg", - "png", - "gif" - ] -} + python: ["jpg", "jpeg", "png", "gif"], +}; const allowedExtensionsString = (projectType) => { const extensionsList = allowedExtensions[projectType]; if (extensionsList.length === 1) { - return `'.${extensionsList[0]}'` + return `'.${extensionsList[0]}'`; } else { - return `'.` + extensionsList.slice(0, -1).join(`', '.`) + `' or '.` + extensionsList[extensionsList.length - 1] + `'`; + return ( + `'.` + + extensionsList.slice(0, -1).join(`', '.`) + + `' or '.` + + extensionsList[extensionsList.length - 1] + + `'` + ); } -} +}; const ImageUploadButton = () => { const [modalIsOpen, setIsOpen] = useState(false); const [files, setFiles] = useState([]); const dispatch = useDispatch(); const projectType = useSelector((state) => state.editor.project.project_type); - const projectIdentifier = useSelector((state) => state.editor.project.identifier); + const projectIdentifier = useSelector( + (state) => state.editor.project.identifier, + ); const projectImages = useSelector((state) => state.editor.project.image_list); - const imageNames = projectImages.map(image => `${image.filename}`); + const imageNames = projectImages.map((image) => `${image.filename}`); const user = useSelector((state) => state.auth.user); const closeModal = () => { - setFiles([]) + setFiles([]); setIsOpen(false); - } + }; const showModal = () => { dispatch(setNameError("")); - setIsOpen(true) + setIsOpen(true); }; const saveImages = async () => { files.every((file) => { - const fileName = file.name - const extension = fileName.split('.').slice(1).join('.').toLowerCase(); - if (imageNames.includes(fileName) || files.filter(file => file.name === fileName).length > 1) { + const fileName = file.name; + const extension = fileName.split(".").slice(1).join(".").toLowerCase(); + if ( + imageNames.includes(fileName) || + files.filter((file) => file.name === fileName).length > 1 + ) { dispatch(setNameError("Image names must be unique.")); - return false - } - else if (isValidFileName(fileName, files)) { - return true + return false; + } else if (isValidFileName(fileName, files)) { + return true; } else if (!allowedExtensions[projectType].includes(extension)) { - dispatch(setNameError(`Image names must end in ${allowedExtensionsString(projectType)}.`)); - return false + dispatch( + setNameError( + `Image names must end in ${allowedExtensionsString(projectType)}.`, + ), + ); + return false; } else { dispatch(setNameError("Error")); - return false + return false; } - }) - if (store.getState().editor.nameError === '') { - const response = await uploadImages(projectIdentifier, user.access_token, files) - dispatch(updateImages(response.data.image_list)) - closeModal() + }); + if (store.getState().editor.nameError === "") { + const response = await uploadImages( + projectIdentifier, + user.access_token, + files, + ); + dispatch(updateImages(response.data.image_list)); + closeModal(); } - } + }; const isValidFileName = (fileName, files) => { - const extension = fileName.split('.').slice(1).join('.').toLowerCase() - if (allowedExtensions[projectType].includes(extension) && !imageNames.includes(fileName) && files.filter(file => file.name === fileName).length === 1) { + const extension = fileName.split(".").slice(1).join(".").toLowerCase(); + if ( + allowedExtensions[projectType].includes(extension) && + !imageNames.includes(fileName) && + files.filter((file) => file.name === fileName).length === 1 + ) { return true; } else { return false; } - } + }; const customStyles = { content: { - top: '50%', - left: '50%', - right: 'auto', - bottom: 'auto', - marginRight: '-50%', - transform: 'translate(-50%, -50%)', + top: "50%", + left: "50%", + right: "auto", + bottom: "auto", + marginRight: "-50%", + transform: "translate(-50%, -50%)", }, overlay: { - zIndex: 1000 - } + zIndex: 1000, + }, }; return ( <> -
- ); -} +}; export default ImageUploadButton; diff --git a/src/components/Editor/ImageUploadButton/ImageUploadButton.test.js b/src/components/Editor/ImageUploadButton/ImageUploadButton.test.js index b121206b8..df575e530 100644 --- a/src/components/Editor/ImageUploadButton/ImageUploadButton.test.js +++ b/src/components/Editor/ImageUploadButton/ImageUploadButton.test.js @@ -1,7 +1,7 @@ import React from "react"; -import { fireEvent, render } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; +import { fireEvent, render } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; import ImageUploadButton from "./ImageUploadButton"; @@ -11,47 +11,53 @@ describe("When user logged in and owns project", () => { let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { name: "main", - extension: "py" - } + extension: "py", + }, ], image_list: [], project_type: "python", - user_id: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf" + user_id: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", }, nameError: "", }, auth: { user: { profile: { - user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf" - } - } - } - } + user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf", + }, + }, + }, + }; store = mockStore(initialState); - ({ queryByText } = render(
)) + ({ queryByText } = render( + +
+ +
+
, + )); button = queryByText(/Upload Image/); - }) + }); test("Modal opens when Image Upload button clicked", () => { - fireEvent.click(button) - const dropzone = queryByText(/Drag and drop/) - expect(dropzone).not.toBeNull() - }) + fireEvent.click(button); + const dropzone = queryByText(/Drag and drop/); + expect(dropzone).not.toBeNull(); + }); test("Modal closes when cancel button clicked", () => { - fireEvent.click(button) - const cancelButton = queryByText(/Cancel/) - fireEvent.click(cancelButton) - const dropzone = queryByText(/Drag and drop/) - expect(dropzone).toBeNull() - }) -}) + fireEvent.click(button); + const cancelButton = queryByText(/Cancel/); + fireEvent.click(cancelButton); + const dropzone = queryByText(/Drag and drop/); + expect(dropzone).toBeNull(); + }); +}); diff --git a/src/components/Editor/NewComponentButton/NewComponentButton.js b/src/components/Editor/NewComponentButton/NewComponentButton.js index 84c6de76f..c9e440fed 100644 --- a/src/components/Editor/NewComponentButton/NewComponentButton.js +++ b/src/components/Editor/NewComponentButton/NewComponentButton.js @@ -1,30 +1,30 @@ -import './NewComponentButton.scss'; +import "./NewComponentButton.scss"; -import {React} from 'react' -import { useDispatch } from 'react-redux'; +import { React } from "react"; +import { useDispatch } from "react-redux"; -import { showNewFileModal } from '../EditorSlice'; -import Button from '../../Button/Button' -import { PlusIcon } from '../../../Icons'; -import { useTranslation } from 'react-i18next'; +import { showNewFileModal } from "../EditorSlice"; +import Button from "../../Button/Button"; +import { PlusIcon } from "../../../Icons"; +import { useTranslation } from "react-i18next"; const NewComponentButton = () => { - const { t } = useTranslation() - const dispatch = useDispatch(); + const { t } = useTranslation(); + const dispatch = useDispatch(); - const openNewFileModal = () => { - dispatch(showNewFileModal()) - } + const openNewFileModal = () => { + dispatch(showNewFileModal()); + }; - return ( -
- ) -} + ); +}; -export default OutputViewToggle +export default OutputViewToggle; diff --git a/src/components/Editor/Runners/PythonRunner/OutputViewToggle.test.js b/src/components/Editor/Runners/PythonRunner/OutputViewToggle.test.js index 176e0f189..94c90811b 100644 --- a/src/components/Editor/Runners/PythonRunner/OutputViewToggle.test.js +++ b/src/components/Editor/Runners/PythonRunner/OutputViewToggle.test.js @@ -1,151 +1,203 @@ import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; +import { fireEvent, render, screen } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; import OutputViewToggle from "./OutputViewToggle"; import { setIsSplitView } from "../../EditorSlice"; -describe('When in tabbed view', () => { +describe("When in tabbed view", () => { beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { isSplitView: false, - } - } + }, + }; const store = mockStore(initialState); - render(); - }) - - test('Tabbed view button is active', () => { - expect(screen.getAllByRole('button')[0]).toHaveClass('output-view-toggle__button--tabbed output-view-toggle__button--active') - }) - - test('Split view button is not active', () => { - expect(screen.getAllByRole('button')[1]).toHaveClass('output-view-toggle__button--split') - expect(screen.getAllByRole('button')[1]).not.toHaveClass('output-view-toggle__button--active') - }) -}) - -describe('When in split view', () => { + render( + + + , + ); + }); + + test("Tabbed view button is active", () => { + expect(screen.getAllByRole("button")[0]).toHaveClass( + "output-view-toggle__button--tabbed output-view-toggle__button--active", + ); + }); + + test("Split view button is not active", () => { + expect(screen.getAllByRole("button")[1]).toHaveClass( + "output-view-toggle__button--split", + ); + expect(screen.getAllByRole("button")[1]).not.toHaveClass( + "output-view-toggle__button--active", + ); + }); +}); + +describe("When in split view", () => { beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { isSplitView: true, - } - } - const store = mockStore(initialState); - render(); - }) - - test('Split view button is active', () => { - expect(screen.getAllByRole('button')[1]).toHaveClass('output-view-toggle__button--split output-view-toggle__button--active') - }) - - test('Tabbed view button is not active', () => { - expect(screen.getAllByRole('button')[0]).toHaveClass('output-view-toggle__button--tabbed') - expect(screen.getAllByRole('button')[0]).not.toHaveClass('output-view-toggle__button--active') - }) -}) - -test('Clicking tabbed view icon switches to tabbed view', () => { - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: {} - } - const store = mockStore(initialState); - render(); - fireEvent.click(screen.getAllByRole('button')[0]) - const expectedActions = [setIsSplitView(false)] - expect(store.getActions()).toEqual(expectedActions); -}) - -test('Clicking split view icon switches to tabbed view', () => { - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: {} - } + }, + }; const store = mockStore(initialState); - render(); - fireEvent.click(screen.getAllByRole('button')[1]) - const expectedActions = [setIsSplitView(true)] - expect(store.getActions()).toEqual(expectedActions); -}) - -describe('When in a code run is triggered', () => { + render( + + + , + ); + }); + + test("Split view button is active", () => { + expect(screen.getAllByRole("button")[1]).toHaveClass( + "output-view-toggle__button--split output-view-toggle__button--active", + ); + }); + + test("Tabbed view button is not active", () => { + expect(screen.getAllByRole("button")[0]).toHaveClass( + "output-view-toggle__button--tabbed", + ); + expect(screen.getAllByRole("button")[0]).not.toHaveClass( + "output-view-toggle__button--active", + ); + }); +}); + +test("Clicking tabbed view icon switches to tabbed view", () => { + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: {}, + }; + const store = mockStore(initialState); + render( + + + , + ); + fireEvent.click(screen.getAllByRole("button")[0]); + const expectedActions = [setIsSplitView(false)]; + expect(store.getActions()).toEqual(expectedActions); +}); + +test("Clicking split view icon switches to tabbed view", () => { + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: {}, + }; + const store = mockStore(initialState); + render( + + + , + ); + fireEvent.click(screen.getAllByRole("button")[1]); + const expectedActions = [setIsSplitView(true)]; + expect(store.getActions()).toEqual(expectedActions); +}); + +describe("When in a code run is triggered", () => { beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { codeRunTriggered: true, - } - } + }, + }; const store = mockStore(initialState); - render(); - }) - - test('Tabbed view button is disabled', () => { - expect(screen.getAllByRole('button')[0]).toHaveClass('output-view-toggle__button--tabbed') - expect(screen.getAllByRole('button')[0]).toBeDisabled() - }) - - test('Split view button is disabled', () => { - expect(screen.getAllByRole('button')[1]).toHaveClass('output-view-toggle__button--split') - expect(screen.getAllByRole('button')[1]).toBeDisabled() - }) -}) - -describe('When in a draw run is triggered', () => { + render( + + + , + ); + }); + + test("Tabbed view button is disabled", () => { + expect(screen.getAllByRole("button")[0]).toHaveClass( + "output-view-toggle__button--tabbed", + ); + expect(screen.getAllByRole("button")[0]).toBeDisabled(); + }); + + test("Split view button is disabled", () => { + expect(screen.getAllByRole("button")[1]).toHaveClass( + "output-view-toggle__button--split", + ); + expect(screen.getAllByRole("button")[1]).toBeDisabled(); + }); +}); + +describe("When in a draw run is triggered", () => { beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { drawTriggered: true, - } - } + }, + }; const store = mockStore(initialState); - render(); - }) - - test('Tabbed view button is disabled', () => { - expect(screen.getAllByRole('button')[0]).toHaveClass('output-view-toggle__button--tabbed') - expect(screen.getAllByRole('button')[0]).toBeDisabled() - }) - - test('Split view button is disabled', () => { - expect(screen.getAllByRole('button')[1]).toHaveClass('output-view-toggle__button--split') - expect(screen.getAllByRole('button')[1]).toBeDisabled() - }) -}) - -describe('When in neither a code run nor a draw run is triggered', () => { + render( + + + , + ); + }); + + test("Tabbed view button is disabled", () => { + expect(screen.getAllByRole("button")[0]).toHaveClass( + "output-view-toggle__button--tabbed", + ); + expect(screen.getAllByRole("button")[0]).toBeDisabled(); + }); + + test("Split view button is disabled", () => { + expect(screen.getAllByRole("button")[1]).toHaveClass( + "output-view-toggle__button--split", + ); + expect(screen.getAllByRole("button")[1]).toBeDisabled(); + }); +}); + +describe("When in neither a code run nor a draw run is triggered", () => { beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { - codeRunTriggered:false, + codeRunTriggered: false, drawTriggered: false, - } - } + }, + }; const store = mockStore(initialState); - render(); - }) - - test('Tabbed view button is enabled', () => { - expect(screen.getAllByRole('button')[0]).toHaveClass('output-view-toggle__button--tabbed') - expect(screen.getAllByRole('button')[0]).not.toBeDisabled() - }) - - test('Split view button is enabled', () => { - expect(screen.getAllByRole('button')[1]).toHaveClass('output-view-toggle__button--split') - expect(screen.getAllByRole('button')[1]).not.toBeDisabled() - }) -}) + render( + + + , + ); + }); + + test("Tabbed view button is enabled", () => { + expect(screen.getAllByRole("button")[0]).toHaveClass( + "output-view-toggle__button--tabbed", + ); + expect(screen.getAllByRole("button")[0]).not.toBeDisabled(); + }); + + test("Split view button is enabled", () => { + expect(screen.getAllByRole("button")[1]).toHaveClass( + "output-view-toggle__button--split", + ); + expect(screen.getAllByRole("button")[1]).not.toBeDisabled(); + }); +}); diff --git a/src/components/Editor/Runners/PythonRunner/PythonRunner.js b/src/components/Editor/Runners/PythonRunner/PythonRunner.js index 500d46631..4e965fdb8 100644 --- a/src/components/Editor/Runners/PythonRunner/PythonRunner.js +++ b/src/components/Editor/Runners/PythonRunner/PythonRunner.js @@ -1,74 +1,84 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import './PythonRunner.scss'; -import React, { useContext, useEffect, useRef, useState } from 'react'; +import "./PythonRunner.scss"; +import React, { useContext, useEffect, useRef, useState } from "react"; import * as Sentry from "@sentry/browser"; -import { useSelector, useDispatch } from 'react-redux' -import { useTranslation } from 'react-i18next'; -import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; -import Sk from "skulpt" -import { setError, codeRunHandled, stopDraw, setSenseHatEnabled, triggerDraw } from '../../EditorSlice' -import ErrorMessage from '../../ErrorMessage/ErrorMessage' - -import store from '../../../../app/store' -import VisualOutputPane from './VisualOutputPane'; -import OutputViewToggle from './OutputViewToggle'; -import { SettingsContext } from '../../../../settings'; +import { useSelector, useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import Sk from "skulpt"; +import { + setError, + codeRunHandled, + stopDraw, + setSenseHatEnabled, + triggerDraw, +} from "../../EditorSlice"; +import ErrorMessage from "../../ErrorMessage/ErrorMessage"; + +import store from "../../../../app/store"; +import VisualOutputPane from "./VisualOutputPane"; +import OutputViewToggle from "./OutputViewToggle"; +import { SettingsContext } from "../../../../settings"; const externalLibraries = { "./pygal/__init__.js": { path: `${process.env.PUBLIC_URL}/shims/pygal/pygal.js`, dependencies: [ - 'https://cdnjs.cloudflare.com/ajax/libs/highcharts/6.0.2/highcharts.js', - 'https://cdnjs.cloudflare.com/ajax/libs/highcharts/6.0.2/js/highcharts-more.js' + "https://cdnjs.cloudflare.com/ajax/libs/highcharts/6.0.2/highcharts.js", + "https://cdnjs.cloudflare.com/ajax/libs/highcharts/6.0.2/js/highcharts-more.js", ], }, "./py5/__init__.js": { path: `${process.env.PUBLIC_URL}/shims/processing/py5/py5-shim.js`, - dependencies: [ - 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js' - ] + dependencies: ["https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"], }, "./py5_imported/__init__.js": { path: `${process.env.PUBLIC_URL}/shims/processing/py5_imported_mode/py5_imported.js`, }, "./py5_imported_mode.py": { - path: `${process.env.PUBLIC_URL}/shims/processing/py5_imported_mode/py5_imported_mode.py` + path: `${process.env.PUBLIC_URL}/shims/processing/py5_imported_mode/py5_imported_mode.py`, }, "./p5/__init__.js": { path: `${process.env.PUBLIC_URL}/shims/processing/p5/p5-shim.js`, - dependencies: [ - 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js' - ] + dependencies: ["https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"], }, "./_internal_sense_hat/__init__.js": { - path: `${process.env.PUBLIC_URL}/shims/sense_hat/_internal_sense_hat.js` + path: `${process.env.PUBLIC_URL}/shims/sense_hat/_internal_sense_hat.js`, }, "./sense_hat.py": { - path: `${process.env.PUBLIC_URL}/shims/sense_hat/sense_hat_blob.py` - } + path: `${process.env.PUBLIC_URL}/shims/sense_hat/sense_hat_blob.py`, + }, }; const PythonRunner = () => { const projectCode = useSelector((state) => state.editor.project.components); const isSplitView = useSelector((state) => state.editor.isSplitView); const isEmbedded = useSelector((state) => state.editor.isEmbedded); - const codeRunTriggered = useSelector((state) => state.editor.codeRunTriggered); + const codeRunTriggered = useSelector( + (state) => state.editor.codeRunTriggered, + ); const codeRunStopped = useSelector((state) => state.editor.codeRunStopped); const drawTriggered = useSelector((state) => state.editor.drawTriggered); - const senseHatAlwaysEnabled = useSelector((state) => state.editor.senseHatAlwaysEnabled); + const senseHatAlwaysEnabled = useSelector( + (state) => state.editor.senseHatAlwaysEnabled, + ); const output = useRef(); const dispatch = useDispatch(); - const { t } = useTranslation() - const settings = useContext(SettingsContext) + const { t } = useTranslation(); + const settings = useContext(SettingsContext); - const queryParams = new URLSearchParams(window.location.search) - const [hasVisualOutput, setHasVisualOutput] = useState(queryParams.get('show_visual_tab') === 'true' || senseHatAlwaysEnabled) + const queryParams = new URLSearchParams(window.location.search); + const [hasVisualOutput, setHasVisualOutput] = useState( + queryParams.get("show_visual_tab") === "true" || senseHatAlwaysEnabled, + ); const getInput = () => { - const pageInput = document.getElementById("input") - const webComponentInput = document.querySelector('editor-wc') ? document.querySelector('editor-wc').shadowRoot.getElementById("input") : null; - return pageInput || webComponentInput - } + const pageInput = document.getElementById("input"); + const webComponentInput = document.querySelector("editor-wc") + ? document.querySelector("editor-wc").shadowRoot.getElementById("input") + : null; + return pageInput || webComponentInput; + }; useEffect(() => { if (codeRunTriggered) { @@ -78,63 +88,62 @@ const PythonRunner = () => { useEffect(() => { if (codeRunStopped && getInput()) { - const input = getInput() - input.removeAttribute("id") - input.removeAttribute("contentEditable") - dispatch(setError(t('output.errors.interrupted'))); - dispatch(codeRunHandled()) + const input = getInput(); + input.removeAttribute("id"); + input.removeAttribute("contentEditable"); + dispatch(setError(t("output.errors.interrupted"))); + dispatch(codeRunHandled()); } }, [codeRunStopped]); useEffect(() => { if (!codeRunTriggered && !drawTriggered) { if (getInput()) { - const input = getInput() - input.removeAttribute("id") - input.removeAttribute("contentEditable") + const input = getInput(); + input.removeAttribute("id"); + input.removeAttribute("contentEditable"); } } - }, - [drawTriggered, codeRunTriggered] - ) + }, [drawTriggered, codeRunTriggered]); - const visualLibraries =[ + const visualLibraries = [ "./pygal/__init__.js", "./py5/__init__.js", "./py5_imported/__init__.js", "./p5/__init__.js", "./_internal_sense_hat/__init__.js", - "src/builtin/turtle/__init__.js" - ] + "src/builtin/turtle/__init__.js", + ]; const outf = (text) => { if (text !== "") { const node = output.current; - const div = document.createElement("span") - div.classList.add('pythonrunner-console-output-line') + const div = document.createElement("span"); + div.classList.add("pythonrunner-console-output-line"); div.innerHTML = new Option(text).innerHTML; node.appendChild(div); node.scrollTop = node.scrollHeight; } - } + }; const builtinRead = (x) => { - - if (x==="./_internal_sense_hat/__init__.js") { - dispatch(setSenseHatEnabled(true)) + if (x === "./_internal_sense_hat/__init__.js") { + dispatch(setSenseHatEnabled(true)); } - if(x === "./p5/__init__.js" || x === "./py5/__init__.js") { - dispatch(triggerDraw()) + if (x === "./p5/__init__.js" || x === "./py5/__init__.js") { + dispatch(triggerDraw()); } - + // TODO: Handle pre-importing py5_imported when refactored py5 shim imported if (visualLibraries.includes(x)) { - setHasVisualOutput(true) + setHasVisualOutput(true); } - let localProjectFiles = projectCode.filter((component) => component.name !== 'main').map((component) => `./${component.name}.py`); + let localProjectFiles = projectCode + .filter((component) => component.name !== "main") + .map((component) => `./${component.name}.py`); if (localProjectFiles.includes(x)) { let filename = x.slice(2, -3); @@ -144,57 +153,78 @@ const PythonRunner = () => { } } - if (Sk.builtinFiles !== undefined && Sk.builtinFiles["files"][x] !== undefined) { + if ( + Sk.builtinFiles !== undefined && + Sk.builtinFiles["files"][x] !== undefined + ) { return Sk.builtinFiles["files"][x]; } if (externalLibraries[x]) { var externalLibraryInfo = externalLibraries[x]; - return externalLibraries[x].code || Sk.misceval.promiseToSuspension( - fetch(externalLibraryInfo.path).then((response) => response.text()).then((code) => { - if (!code) { - throw new Sk.builtin.ImportError("Failed to load remote module"); - } - externalLibraries[x].code = code - var promise; - - function mapUrlToPromise(path) { - // If the script is already in the DOM don't add it again. - const existingScriptElement = document.querySelector(`script[src="${path}"]`) - if(!existingScriptElement) { - return new Promise(function (resolve, _reject) { - let scriptElement = document.createElement("script"); - scriptElement.type = "text/javascript"; - scriptElement.src = path; - scriptElement.async = true - scriptElement.onload = function () { - resolve(true); + return ( + externalLibraries[x].code || + Sk.misceval.promiseToSuspension( + fetch(externalLibraryInfo.path) + .then((response) => response.text()) + .then((code) => { + if (!code) { + throw new Sk.builtin.ImportError( + "Failed to load remote module", + ); + } + externalLibraries[x].code = code; + var promise; + + function mapUrlToPromise(path) { + // If the script is already in the DOM don't add it again. + const existingScriptElement = document.querySelector( + `script[src="${path}"]`, + ); + if (!existingScriptElement) { + return new Promise(function (resolve, _reject) { + let scriptElement = document.createElement("script"); + scriptElement.type = "text/javascript"; + scriptElement.src = path; + scriptElement.async = true; + scriptElement.onload = function () { + resolve(true); + }; + + document.body.appendChild(scriptElement); + }); } - - document.body.appendChild(scriptElement); - }); - } - } - if (externalLibraryInfo.loadDepsSynchronously) { - promise = (externalLibraryInfo.dependencies || []).reduce((p, url) => { - return p.then(() => mapUrlToPromise(url)); - }, Promise.resolve()); // initial - } else { - promise = Promise.all((externalLibraryInfo.dependencies || []).map(mapUrlToPromise)); - } - - return promise.then(function () { - return code; - }).catch(function () { - throw new Sk.builtin.ImportError("Failed to load dependencies required"); - }); - }) + } + if (externalLibraryInfo.loadDepsSynchronously) { + promise = (externalLibraryInfo.dependencies || []).reduce( + (p, url) => { + return p.then(() => mapUrlToPromise(url)); + }, + Promise.resolve(), + ); // initial + } else { + promise = Promise.all( + (externalLibraryInfo.dependencies || []).map(mapUrlToPromise), + ); + } + + return promise + .then(function () { + return code; + }) + .catch(function () { + throw new Sk.builtin.ImportError( + "Failed to load dependencies required", + ); + }); + }), + ) ); } throw new Error("File not found: '" + x + "'"); - } + }; const inputSpan = () => { const span = document.createElement("span"); @@ -202,76 +232,81 @@ const PythonRunner = () => { span.setAttribute("spellCheck", "false"); span.setAttribute("class", "pythonrunner-input"); span.setAttribute("contentEditable", "true"); - return span - } + return span; + }; const inf = function () { if (Sk.sense_hat) { - Sk.sense_hat.mz_criteria.noInputEvents = false + Sk.sense_hat.mz_criteria.noInputEvents = false; } const outputPane = output.current; outputPane.appendChild(inputSpan()); - const input = getInput() + const input = getInput(); input.focus(); return new Promise(function (resolve, reject) { input.addEventListener("keydown", function removeInput(e) { if (e.key === "Enter") { - input.removeEventListener(e.type, removeInput) + input.removeEventListener(e.type, removeInput); // resolve the promise with the value of the input field const answer = input.innerText; input.removeAttribute("id"); input.removeAttribute("contentEditable"); - input.innerText=answer+'\n'; + input.innerText = answer + "\n"; document.addEventListener("keyup", function storeInput(e) { if (e.key === "Enter") { document.removeEventListener(e.type, storeInput); resolve(answer); } - }) + }); } - }) - }) - } + }); + }); + }; const handleError = (err) => { - let errorMessage - if (err.message === t('output.errors.interrupted')) { - errorMessage = err.message + let errorMessage; + if (err.message === t("output.errors.interrupted")) { + errorMessage = err.message; } else { - const errorDetails = (err.tp$str && err.tp$str().v).replace(/\[(.*?)\]/, "").replace(/\.$/, '') - const errorType = err.tp$name || err.constructor.name - const lineNumber = err.traceback[0].lineno - const fileName = err.traceback[0].filename.replace(/^\.\//, '') - - Sentry.captureMessage(`${errorType}: ${errorDetails}`) + const errorDetails = (err.tp$str && err.tp$str().v) + .replace(/\[(.*?)\]/, "") + .replace(/\.$/, ""); + const errorType = err.tp$name || err.constructor.name; + const lineNumber = err.traceback[0].lineno; + const fileName = err.traceback[0].filename.replace(/^\.\//, ""); + + Sentry.captureMessage(`${errorType}: ${errorDetails}`); errorMessage = `${errorType}: ${errorDetails} on line ${lineNumber} of ${fileName}`; } dispatch(setError(errorMessage)); dispatch(stopDraw()); if (getInput()) { - const input = getInput() - input.removeAttribute("id") - input.removeAttribute("contentEditable") + const input = getInput(); + input.removeAttribute("id"); + input.removeAttribute("contentEditable"); } - } + }; const runCode = () => { // clear previous output dispatch(setError("")); - output.current.innerHTML = ''; - dispatch(setSenseHatEnabled(false)) + output.current.innerHTML = ""; + dispatch(setSenseHatEnabled(false)); var prog = projectCode[0].content; - if (prog.includes(`# ${t('input.comment.py5')}`)) { - prog = prog.replace(`# ${t('input.comment.py5')}`,'from py5_imported_mode import *') + if (prog.includes(`# ${t("input.comment.py5")}`)) { + prog = prog.replace( + `# ${t("input.comment.py5")}`, + "from py5_imported_mode import *", + ); - if (! prog.match(/(run_sketch)/)) { - prog = prog.concat('\nrun_sketch()') + if (!prog.match(/(run_sketch)/)) { + prog = prog.concat("\nrun_sketch()"); } } @@ -281,25 +316,25 @@ const PythonRunner = () => { read: builtinRead, debugging: true, inputTakesPrompt: true, - uncaughtException: handleError + uncaughtException: handleError, }); - var myPromise = Sk.misceval.asyncToPromise(() => - Sk.importMainWithBody("main", false, prog, true), { - "*": () => { - if (store.getState().editor.codeRunStopped) { - throw new Error(t('output.errors.interrupted')); - } + var myPromise = Sk.misceval + .asyncToPromise(() => Sk.importMainWithBody("main", false, prog, true), { + "*": () => { + if (store.getState().editor.codeRunStopped) { + throw new Error(t("output.errors.interrupted")); } }, - ).catch(err => { - handleError(err) - }).finally(()=>{ - dispatch(codeRunHandled()); - }); - myPromise.then(function (_mod) { - }) - } + }) + .catch((err) => { + handleError(err); + }) + .finally(() => { + dispatch(codeRunHandled()); + }); + myPromise.then(function (_mod) {}); + }; function shiftFocusToInput(e) { if (document.getSelection().toString().length > 0) { @@ -308,82 +343,100 @@ const PythonRunner = () => { const inputBox = getInput(); if (inputBox && e.target !== inputBox) { - const input = getInput() + const input = getInput(); const selection = window.getSelection(); selection.removeAllRanges(); - if (input.innerText && input.innerText.length > 0){ + if (input.innerText && input.innerText.length > 0) { const range = document.createRange(); range.setStart(input, 1); range.collapse(true); selection.addRange(range); } - input.focus() + input.focus(); } } return (
- { isSplitView ? + {isSplitView ? ( <> - {hasVisualOutput ?
+ {hasVisualOutput ? ( +
+ +
+ + + + {t("output.visualOutput")} + + + + {!isEmbedded ? : null} +
+ + + +
+
+ ) : null} +
-
+
- {t('output.visualOutput')} + + {t("output.textOutput")} + - {!isEmbedded ? : null } -
- - - - -
: null} -
- -
- - - {t('output.textOutput')} - - - { hasVisualOutput || isEmbedded ? null : } + {hasVisualOutput || isEmbedded ? null : }
-

+                

               
- - : - -
- - {hasVisualOutput ? - - {t('output.visualOutput')} - : null - } - - {t('output.textOutput')} - - - {!isEmbedded ? : null } -
- - {hasVisualOutput ? - - - : null - } - -

-        
-
- } + + ) : ( + +
+ + {hasVisualOutput ? ( + + + {t("output.visualOutput")} + + + ) : null} + + + {t("output.textOutput")} + + + + {!isEmbedded ? : null} +
+ + {hasVisualOutput ? ( + + + + ) : null} + +

+          
+
+ )}
); }; diff --git a/src/components/Editor/Runners/PythonRunner/PythonRunner.test.js b/src/components/Editor/Runners/PythonRunner/PythonRunner.test.js index 00cb5ef50..bb013ca5c 100644 --- a/src/components/Editor/Runners/PythonRunner/PythonRunner.test.js +++ b/src/components/Editor/Runners/PythonRunner/PythonRunner.test.js @@ -1,7 +1,7 @@ import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; +import { fireEvent, render, screen } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; import PythonRunner from "./PythonRunner"; import { codeRunHandled, setError, triggerDraw } from "../../EditorSlice"; @@ -12,585 +12,679 @@ describe("Testing basic input span functionality", () => { let store; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "input()" - } + content: "input()", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - } - } + }, + }; store = mockStore(initialState); - render(); + render( + + + , + ); input = document.getElementById("input"); - }) + }); test("Input function in code makes editable input box appear", () => { expect(input).toHaveAttribute("contentEditable", "true"); - }) + }); test("Input box has focus when it appears", () => { expect(input).toHaveFocus(); - }) + }); test("Clicking output pane transfers focus to input", () => { - const outputPane = document.getElementsByClassName("pythonrunner-console")[0] + const outputPane = document.getElementsByClassName( + "pythonrunner-console", + )[0]; fireEvent.click(outputPane); expect(input).toHaveFocus(); - }) + }); test("Pressing enter stops the input box being editable", () => { - const inputText = 'hello world'; + const inputText = "hello world"; input.innerText = inputText; - fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 }) + fireEvent.keyDown(input, { key: "Enter", code: "Enter", charCode: 13 }); expect(input).not.toHaveAttribute("contentEditable", "true"); - expect(input.innerText).toBe(inputText + '\n'); - }) -}) + expect(input.innerText).toBe(inputText + "\n"); + }); +}); test("Input box not there when input function not called", () => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "print('Hello')" - } + content: "print('Hello')", + }, ], - image_list: [] + image_list: [], }, - codeRunTriggered: true - } - } + codeRunTriggered: true, + }, + }; const store = mockStore(initialState); - render(); - expect(document.getElementById("input")).toBeNull() - -}) + render( + + + , + ); + expect(document.getElementById("input")).toBeNull(); +}); describe("Testing stopping the code run with input", () => { let store; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "input()" - } + content: "input()", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - codeRunStopped: true - } - } + codeRunStopped: true, + }, + }; store = mockStore(initialState); - render(); - }) + render( + + + , + ); + }); test("Disables input span", () => { expect(document.getElementById("input")).toBeNull(); - }) + }); test("Sets interruption error", () => { - expect(store.getActions()).toEqual(expect.arrayContaining([setError('output.errors.interrupted')])) - }) + expect(store.getActions()).toEqual( + expect.arrayContaining([setError("output.errors.interrupted")]), + ); + }); test("Handles code run", () => { - expect(store.getActions()).toEqual(expect.arrayContaining([codeRunHandled()])) - }) -}) + expect(store.getActions()).toEqual( + expect.arrayContaining([codeRunHandled()]), + ); + }); +}); -describe('When in split view, no visual libraries used and code run', () => { +describe("When in split view, no visual libraries used and code run", () => { let store; let queryByText; - + beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "print('hello world')" - } + content: "print('hello world')", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: true - } - } + isSplitView: true, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is not shown', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).not.toBeInTheDocument() - }) -}) - -describe('When in split view, py5 imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is not shown", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).not.toBeInTheDocument(); + }); +}); + +describe("When in split view, py5 imported and code run", () => { let store; let queryByText; - + beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import py5" - } + content: "import py5", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: true - } - } + isSplitView: true, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is shown', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) - - test('Draw is triggered', () => { - expect(store.getActions()).toEqual(expect.arrayContaining([triggerDraw()])) - }) -}) - -describe('When in split view, py5_imported imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is shown", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); + + test("Draw is triggered", () => { + expect(store.getActions()).toEqual(expect.arrayContaining([triggerDraw()])); + }); +}); + +describe("When in split view, py5_imported imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import py5_imported" - } + content: "import py5_imported", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: true - } - } + isSplitView: true, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is shown', async () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) -}) - -describe('When in split view, pygal imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is shown", async () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); + +describe("When in split view, pygal imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import pygal" - } + content: "import pygal", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: true - } - } + isSplitView: true, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is shown', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) -}) - -describe('When in split view, turtle imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is shown", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); + +describe("When in split view, turtle imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import turtle" - } + content: "import turtle", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: true - } - } + isSplitView: true, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is shown', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) -}) - -describe('When in split view, sense_hat imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is shown", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); + +describe("When in split view, sense_hat imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import _internal_sense_hat" - } + content: "import _internal_sense_hat", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: true - } - } + isSplitView: true, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is shown', async () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() }) -}) - -describe('When in tabbed view, no visual libraries used and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is shown", async () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); + +describe("When in tabbed view, no visual libraries used and code run", () => { let store; let queryByText; - + beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "print('hello world')" - } + content: "print('hello world')", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: false - } - } + isSplitView: false, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is not shown', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).not.toBeInTheDocument() - }) -}) - -describe('When in tabbed view, py5 imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is not shown", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).not.toBeInTheDocument(); + }); +}); + +describe("When in tabbed view, py5 imported and code run", () => { let store; let queryByText; - + beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import py5" - } + content: "import py5", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: false - } - } + isSplitView: false, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is not hidden', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) - - test('Draw is triggered', () => { - expect(store.getActions()).toEqual(expect.arrayContaining([triggerDraw()])) - }) -}) - -describe('When in tabbed view, py5_imported imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is not hidden", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); + + test("Draw is triggered", () => { + expect(store.getActions()).toEqual(expect.arrayContaining([triggerDraw()])); + }); +}); + +describe("When in tabbed view, py5_imported imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import py5_imported" - } + content: "import py5_imported", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: false - } - } + isSplitView: false, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is not hidden', async () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) -}) - -describe('When in tabbed view, pygal imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is not hidden", async () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); + +describe("When in tabbed view, pygal imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import pygal" - } + content: "import pygal", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: false - } - } + isSplitView: false, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is not hidden', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) -}) - -describe('When in tabbed view, turtle imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is not hidden", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); + +describe("When in tabbed view, turtle imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import turtle" - } + content: "import turtle", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: false - } - } + isSplitView: false, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) - - test('Visual tab is not hidden', () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) -}) - -describe('When in tabbed view, sense_hat imported and code run', () => { + ({ queryByText } = render( + + + , + )); + }); + + test("Visual tab is not hidden", () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); + +describe("When in tabbed view, sense_hat imported and code run", () => { let store; let queryByText; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import _internal_sense_hat" - } + content: "import _internal_sense_hat", + }, ], - image_list: [] + image_list: [], }, codeRunTriggered: true, - isSplitView: false - } - } + isSplitView: false, + }, + }; store = mockStore(initialState); - ({queryByText} = render()); - }) + ({ queryByText } = render( + + + , + )); + }); - test('Visual tab is not hidden', async () => { - const visualTab = queryByText('output.visualOutput') - expect(visualTab).toBeInTheDocument() - }) -}) + test("Visual tab is not hidden", async () => { + const visualTab = queryByText("output.visualOutput"); + expect(visualTab).toBeInTheDocument(); + }); +}); test("When embedded in split view with visual output does not render output view toggle", () => { - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - project: { - components: [ - { - content: "import p5" - } - ], - }, - codeRUnTriggered: true, - isSplitView: true, - isEmbedded: true - } - } - const store = mockStore(initialState); - render() - expect(screen.queryByRole('button')).not.toBeInTheDocument() -}) + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: { + components: [ + { + content: "import p5", + }, + ], + }, + codeRUnTriggered: true, + isSplitView: true, + isEmbedded: true, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); +}); test("When embedded in split view with no visual output does not render output view toggle", () => { - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - project: {}, - senseHatAlwaysEnabled: false, - isSplitView: true, - isEmbedded: true - } - } - const store = mockStore(initialState); - render() - expect(screen.queryByRole('button')).not.toBeInTheDocument() -}) + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: {}, + senseHatAlwaysEnabled: false, + isSplitView: true, + isEmbedded: true, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); +}); test("When embedded in tabbed view does not render output view toggle", () => { - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - project: {}, - isSplitView: false, - isEmbedded: true - } - } - const store = mockStore(initialState); - render() - expect(screen.queryByRole('button')).not.toBeInTheDocument() -}) - -test('Tabbed view has text and visual tabs with same parent element', () => { - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - project: {}, - senseHatAlwaysEnabled: true, - isSplitView: false - } - } - const store = mockStore(initialState); - render() - expect(screen.getByText('output.visualOutput').parentElement.parentElement).toEqual(screen.getByText('output.textOutput').parentElement.parentElement) -}) - -test('Split view has text and visual tabs with different parent elements', () => { - const middlewares = [] - const mockStore = configureStore(middlewares) - const initialState = { - editor: { - project: {}, - senseHatAlwaysEnabled: true, - isSplitView: true - } - } - const store = mockStore(initialState); - render() - expect(screen.getByText('output.visualOutput').parentElement).not.toEqual(screen.getByText('output.textOutput').parentElement) -}) + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: {}, + isSplitView: false, + isEmbedded: true, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); +}); + +test("Tabbed view has text and visual tabs with same parent element", () => { + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: {}, + senseHatAlwaysEnabled: true, + isSplitView: false, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); + expect( + screen.getByText("output.visualOutput").parentElement.parentElement, + ).toEqual(screen.getByText("output.textOutput").parentElement.parentElement); +}); + +test("Split view has text and visual tabs with different parent elements", () => { + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: {}, + senseHatAlwaysEnabled: true, + isSplitView: true, + }, + }; + const store = mockStore(initialState); + render( + + + , + ); + expect(screen.getByText("output.visualOutput").parentElement).not.toEqual( + screen.getByText("output.textOutput").parentElement, + ); +}); + +describe("When font size is set", () => { + let runnerContainer; -describe('When font size is set', () => { - let runnerContainer - beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { - project: {} - } - } + project: {}, + }, + }; const store = mockStore(initialState); runnerContainer = render( - + - - ) - }) - - test('Font size class is set correctly', () => { - const runnerConsole = runnerContainer.container.querySelector('.pythonrunner-console') - expect(runnerConsole).toHaveClass("pythonrunner-console--myFontSize") - }) -}) + , + ); + }); + + test("Font size class is set correctly", () => { + const runnerConsole = runnerContainer.container.querySelector( + ".pythonrunner-console", + ); + expect(runnerConsole).toHaveClass("pythonrunner-console--myFontSize"); + }); +}); diff --git a/src/components/Editor/Runners/PythonRunner/VisualOutputPane.js b/src/components/Editor/Runners/PythonRunner/VisualOutputPane.js index 923e4593a..8eb56f6ee 100644 --- a/src/components/Editor/Runners/PythonRunner/VisualOutputPane.js +++ b/src/components/Editor/Runners/PythonRunner/VisualOutputPane.js @@ -2,15 +2,18 @@ import React, { useEffect } from "react"; import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; -import Sk from "skulpt" +import Sk from "skulpt"; import AstroPiModel from "../../../AstroPiModel/AstroPiModel"; import { codeRunHandled, setError } from "../../EditorSlice"; const VisualOutputPane = () => { - - const codeRunTriggered = useSelector((state) => state.editor.codeRunTriggered) - const drawTriggered = useSelector((state) => state.editor.drawTriggered) - const senseHatAlwaysEnabled = useSelector((state) => state.editor.senseHatAlwaysEnabled); + const codeRunTriggered = useSelector( + (state) => state.editor.codeRunTriggered, + ); + const drawTriggered = useSelector((state) => state.editor.drawTriggered); + const senseHatAlwaysEnabled = useSelector( + (state) => state.editor.senseHatAlwaysEnabled, + ); const senseHatEnabled = useSelector((state) => state.editor.senseHatEnabled); const projectImages = useSelector((state) => state.editor.project.image_list); const error = useSelector((state) => state.editor.error); @@ -19,56 +22,67 @@ const VisualOutputPane = () => { const pygalOutput = useRef(); const p5Output = useRef(); const dispatch = useDispatch(); - const { t } = useTranslation() + const { t } = useTranslation(); useEffect(() => { if (codeRunTriggered) { - outputCanvas.current.innerHTML = ''; - pygalOutput.current.innerHTML = ''; - p5Output.current.innerHTML = ''; + outputCanvas.current.innerHTML = ""; + pygalOutput.current.innerHTML = ""; + p5Output.current.innerHTML = ""; - Sk.py5 = {} + Sk.py5 = {}; Sk.py5.sketch = "p5Sketch"; Sk.py5.assets = projectImages; - Sk.p5 = {} + Sk.p5 = {}; Sk.p5.sketch = "p5Sketch"; Sk.p5.assets = projectImages; - + (Sk.pygal || (Sk.pygal = {})).outputCanvas = pygalOutput.current; - - (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'outputCanvas'; - Sk.TurtleGraphics.assets = Object.assign({}, ...projectImages.map((image) => ({[`${image.name}.${image.extension}`]: image.url}))) - + + (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = "outputCanvas"; + Sk.TurtleGraphics.assets = Object.assign( + {}, + ...projectImages.map((image) => ({ + [`${image.name}.${image.extension}`]: image.url, + })), + ); } - }, [codeRunTriggered, projectImages]) + }, [codeRunTriggered, projectImages]); useEffect(() => { - if (!drawTriggered && p5Output.current && p5Output.current.innerHTML !== '') { + if ( + !drawTriggered && + p5Output.current && + p5Output.current.innerHTML !== "" + ) { if (Sk.py5.stop) { - Sk.py5.stop() + Sk.py5.stop(); } else { - Sk.p5.stop() + Sk.p5.stop(); } - - if(error === ''){ - dispatch(setError(t('output.errors.interrupted'))) + + if (error === "") { + dispatch(setError(t("output.errors.interrupted"))); } - dispatch(codeRunHandled()) + dispatch(codeRunHandled()); } - - }, [drawTriggered, dispatch, t, error]) + }, [drawTriggered, dispatch, t, error]); return ( -
-
-
+
+
+
-
+
- {senseHatEnabled || senseHatAlwaysEnabled ?:null} + {senseHatEnabled || senseHatAlwaysEnabled ? : null}
- ) -} + ); +}; -export default VisualOutputPane +export default VisualOutputPane; diff --git a/src/components/Editor/Runners/PythonRunner/VisualOutputPane.test.js b/src/components/Editor/Runners/PythonRunner/VisualOutputPane.test.js index 0b06504b1..a3d38c9f4 100644 --- a/src/components/Editor/Runners/PythonRunner/VisualOutputPane.test.js +++ b/src/components/Editor/Runners/PythonRunner/VisualOutputPane.test.js @@ -1,100 +1,111 @@ import React from "react"; -import { render } from "@testing-library/react" -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; +import { render } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; import VisualOutputPane from "./VisualOutputPane"; -import Sk from 'skulpt'; +import Sk from "skulpt"; describe("When Sense Hat library used", () => { let canvas; let store; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "import _internal_sense_hat" - } + content: "import _internal_sense_hat", + }, ], - image_list: [] + image_list: [], }, - codeRunTriggered: true - } - } + codeRunTriggered: true, + }, + }; store = mockStore(initialState); - render(); - canvas = document.getElementsByClassName("sense-hat")[0] - }) + render( + + + , + ); + canvas = document.getElementsByClassName("sense-hat")[0]; + }); test("Astro Pi component appears", () => { - expect(canvas).not.toBeNull() - }) -}) + expect(canvas).not.toBeNull(); + }); +}); describe("When Sense Hat library not used", () => { let canvas; let store; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [ { - content: "print('Hello world')" - } + content: "print('Hello world')", + }, ], - image_list: [] + image_list: [], }, - codeRunTriggered: true - } - } + codeRunTriggered: true, + }, + }; store = mockStore(initialState); - render(); - canvas = document.getElementsByClassName("sense-hat")[0] - }) + render( + + + , + ); + canvas = document.getElementsByClassName("sense-hat")[0]; + }); test("Astro Pi component does not appear", () => { - expect(canvas).not.toBeDefined() - }) -}) - -describe("When code run is triggered",() => { + expect(canvas).not.toBeDefined(); + }); +}); +describe("When code run is triggered", () => { let store; beforeEach(() => { - const middlewares = [] - const mockStore = configureStore(middlewares) + const middlewares = []; + const mockStore = configureStore(middlewares); const initialState = { editor: { project: { components: [], - image_list: [] + image_list: [], }, - codeRunTriggered: true - } - } + codeRunTriggered: true, + }, + }; store = mockStore(initialState); - render(); - }) + render( + + + , + ); + }); - test('Sets up p5 canvas', () => { - expect(Sk.py5.sketch).not.toBeNull() - }) + test("Sets up p5 canvas", () => { + expect(Sk.py5.sketch).not.toBeNull(); + }); - test('Sets up pygal canvas', () => { - expect(Sk.pygal.outputCanvas).not.toBeNull() - }) + test("Sets up pygal canvas", () => { + expect(Sk.pygal.outputCanvas).not.toBeNull(); + }); - test('Sets up turtle canvas', () => { - expect(Sk.TurtleGraphics.target).not.toBeNull() - }) -}) + test("Sets up turtle canvas", () => { + expect(Sk.TurtleGraphics.target).not.toBeNull(); + }); +}); diff --git a/src/components/Editor/Runners/RunnerFactory.js b/src/components/Editor/Runners/RunnerFactory.js index 0d66ce9f5..12f6aee4a 100644 --- a/src/components/Editor/Runners/RunnerFactory.js +++ b/src/components/Editor/Runners/RunnerFactory.js @@ -1,21 +1,18 @@ -import React from 'react'; -import PythonRunner from './PythonRunner/PythonRunner'; -import HtmlRunner from './HtmlRunner/HtmlRunner'; +import React from "react"; +import PythonRunner from "./PythonRunner/PythonRunner"; +import HtmlRunner from "./HtmlRunner/HtmlRunner"; const RunnerFactory = ({ projectType }) => { const Runner = () => { - if (projectType === 'html') { + if (projectType === "html") { return HtmlRunner; } return PythonRunner; - } + }; const Selected = Runner(); - return ( - - ) -} + return ; +}; export default RunnerFactory; - diff --git a/src/components/Editor/editorDarkTheme.js b/src/components/Editor/editorDarkTheme.js index bf4907d53..74b0e1c94 100644 --- a/src/components/Editor/editorDarkTheme.js +++ b/src/components/Editor/editorDarkTheme.js @@ -1,40 +1,43 @@ -import { EditorView } from '@codemirror/view'; -import '../../font-weight.scss'; -import '../../font-size.scss'; -import '../../line-height.scss'; +import { EditorView } from "@codemirror/view"; +import "../../font-weight.scss"; +import "../../font-size.scss"; +import "../../line-height.scss"; -export const editorDarkTheme = EditorView.theme({ - ".cm-gutters": { - "background-color": "#2A2B32", - "color": "white", - "border": "none" +export const editorDarkTheme = EditorView.theme( + { + ".cm-gutters": { + "background-color": "#2A2B32", + color: "white", + border: "none", + }, + ".cm-activeLine": { + "background-color": "inherit", + }, + ".cm-activeLineGutter": { + "background-color": "inherit", + color: "inherit", + }, + "&.cm-focused .cm-selectionBackground, ::selection": { + background: "#144866", + }, + "&.cm-focused .cm-cursor": { + borderLeftColor: "white", + }, + ".cm-line .cm-indentation-marker": { + background: "none", + "border-left": "solid grey", + "&.active": { + background: "none", + "border-left": "solid lightgrey", + }, + }, + ".ͼb": { color: "#FF00A4" }, + ".ͼc": { color: "#1498D0" }, + ".ͼd": { color: "#1498D0" }, + ".ͼe": { color: "#6CE68D" }, + ".ͼg": { color: "#1498D0" }, + ".ͼj": { color: "#1498D0" }, + ".ͼm": { color: "#C1C1C1" }, }, - ".cm-activeLine": { - "background-color": "inherit", - }, - ".cm-activeLineGutter": { - "background-color": "inherit", - "color": "inherit" - }, - "&.cm-focused .cm-selectionBackground, ::selection": { - "background": "#144866" - }, - "&.cm-focused .cm-cursor": { - borderLeftColor: "white" - }, - ".cm-line .cm-indentation-marker": { - 'background': 'none', - 'border-left': 'solid grey', - "&.active": { - 'background': 'none', - 'border-left': 'solid lightgrey', - } - }, - ".ͼb": {color: "#FF00A4"}, - ".ͼc": {color: "#1498D0"}, - ".ͼd": {color: "#1498D0"}, - ".ͼe": {color: "#6CE68D"}, - ".ͼg": {color: "#1498D0"}, - ".ͼj": {color: "#1498D0"}, - ".ͼm": {color: "#C1C1C1"}, -}, {dark: true}) + { dark: true }, +); diff --git a/src/components/Editor/editorLightTheme.js b/src/components/Editor/editorLightTheme.js index 25d6b3c42..bdaf6b39e 100644 --- a/src/components/Editor/editorLightTheme.js +++ b/src/components/Editor/editorLightTheme.js @@ -1,24 +1,27 @@ -import { EditorView } from '@codemirror/view'; +import { EditorView } from "@codemirror/view"; -export const editorLightTheme = EditorView.theme({ - ".cm-activeLine": { - "background-color": "inherit", +export const editorLightTheme = EditorView.theme( + { + ".cm-activeLine": { + "background-color": "inherit", + }, + ".cm-activeLineGutter": { + "background-color": "inherit", + color: "inherit", + }, + ".cm-gutters": { + border: "none", + color: "black", + "background-color": "white", + }, + ".cm-line .cm-indentation-marker": { + background: "none", + "border-left": "solid lightgrey", + "&.active": { + background: "none", + "border-left": "solid grey", + }, + }, }, - ".cm-activeLineGutter": { - "background-color": "inherit", - "color": "inherit" - }, - ".cm-gutters": { - "border": "none", - "color": "black", - "background-color": "white" - }, - ".cm-line .cm-indentation-marker": { - 'background': 'none', - 'border-left': 'solid lightgrey', - "&.active": { - 'background': 'none', - 'border-left': 'solid grey', - } - }, -}, {dark: false}) + { dark: false }, +); diff --git a/src/components/EmbeddedViewer/EmbeddedControls/EmbeddedControls.js b/src/components/EmbeddedViewer/EmbeddedControls/EmbeddedControls.js index 574b5d1b4..78ba5105a 100644 --- a/src/components/EmbeddedViewer/EmbeddedControls/EmbeddedControls.js +++ b/src/components/EmbeddedViewer/EmbeddedControls/EmbeddedControls.js @@ -1,12 +1,12 @@ -import './EmbeddedControls.css'; -import RunnerControls from '../../RunButton/RunnerControls'; +import "./EmbeddedControls.css"; +import RunnerControls from "../../RunButton/RunnerControls"; const EmbeddedControls = () => { - return ( -
- -
- ) -} + return ( +
+ +
+ ); +}; export default EmbeddedControls; diff --git a/src/components/EmbeddedViewer/EmbeddedViewer.test.js b/src/components/EmbeddedViewer/EmbeddedViewer.test.js index 3747b57cb..c1a3f5ca2 100644 --- a/src/components/EmbeddedViewer/EmbeddedViewer.test.js +++ b/src/components/EmbeddedViewer/EmbeddedViewer.test.js @@ -1,33 +1,32 @@ -import React from 'react'; -import EmbeddedViewer from './EmbeddedViewer'; +import React from "react"; +import EmbeddedViewer from "./EmbeddedViewer"; -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import { render } from '@testing-library/react'; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import { render } from "@testing-library/react"; -let store +let store; beforeEach(() => { const middlewares = []; const mockStore = configureStore(middlewares); const initialState = { - editor: { - loading: 'success', - project: { - components: [] - } - } + editor: { + loading: "success", + project: { + components: [], + }, + }, }; store = mockStore(initialState); +}); -}) - -test('Renders without crashing', () => { +test("Renders without crashing", () => { const { asFragment } = render( - + , ); expect(asFragment()).toMatchSnapshot(); }); diff --git a/src/components/ExternalFiles/ExternalFiles.js b/src/components/ExternalFiles/ExternalFiles.js index fc3fd36d5..6c6683988 100644 --- a/src/components/ExternalFiles/ExternalFiles.js +++ b/src/components/ExternalFiles/ExternalFiles.js @@ -1,20 +1,23 @@ -import React from 'react'; -import { useSelector } from 'react-redux' +import React from "react"; +import { useSelector } from "react-redux"; const ExternalFiles = () => { const project = useSelector((state) => state.editor.project); return ( - - ) + + ); }; -export default ExternalFiles +export default ExternalFiles; diff --git a/src/components/ExternalFiles/ExternalFiles.test.js b/src/components/ExternalFiles/ExternalFiles.test.js index 3c182a1b7..a05e55d91 100644 --- a/src/components/ExternalFiles/ExternalFiles.test.js +++ b/src/components/ExternalFiles/ExternalFiles.test.js @@ -1,28 +1,32 @@ import React from "react"; import { render } from "@testing-library/react"; -import { Provider } from 'react-redux'; +import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import ExternalFiles from "./ExternalFiles"; -const middlewares = [] -const mockStore = configureStore(middlewares) +const middlewares = []; +const mockStore = configureStore(middlewares); test("External files component contains text from file with filename id", () => { - const initialState = { - editor: { - project: { - components: [ - { - name: "hello", - extension: "txt", - content: "hello world!" - } - ] - } - } - } - const store = mockStore(initialState) - const {getByText} = render(); - const fileContent = getByText("hello world!") - expect(fileContent).toHaveAttribute('id', 'hello.txt') -}) + const initialState = { + editor: { + project: { + components: [ + { + name: "hello", + extension: "txt", + content: "hello world!", + }, + ], + }, + }, + }; + const store = mockStore(initialState); + const { getByText } = render( + + + , + ); + const fileContent = getByText("hello world!"); + expect(fileContent).toHaveAttribute("id", "hello.txt"); +}); diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js index 3d29b0ba6..6a844badb 100644 --- a/src/components/Footer/Footer.js +++ b/src/components/Footer/Footer.js @@ -1,20 +1,42 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import './Footer.scss'; +import "./Footer.scss"; const Footer = () => { - const { t } = useTranslation() + const { t } = useTranslation(); return ( -