From 188beada9a827df75bccdbe0674ebb34ac064a0d Mon Sep 17 00:00:00 2001 From: cocomarine Date: Thu, 28 May 2026 14:48:13 +0100 Subject: [PATCH 01/33] install pfem --- package.json | 1 + yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/package.json b/package.json index 03a21c018..f01088541 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@juggle/resize-observer": "^3.3.1", "@lezer/highlight": "^1.0.0", "@raspberrypifoundation/design-system-react": "^2.7.0", + "@raspberrypifoundation/python-friendly-error-messages": "0.1.6", "@react-three/drei": "9.114.3", "@react-three/fiber": "^8.0.13", "@reduxjs/toolkit": "^1.6.2", diff --git a/yarn.lock b/yarn.lock index befb7af43..e9082dbc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4423,6 +4423,7 @@ __metadata: "@lezer/highlight": "npm:^1.0.0" "@pmmmwh/react-refresh-webpack-plugin": "npm:0.4.3" "@raspberrypifoundation/design-system-react": "npm:^2.7.0" + "@raspberrypifoundation/python-friendly-error-messages": "npm:0.1.6" "@react-three/drei": "npm:9.114.3" "@react-three/fiber": "npm:^8.0.13" "@react-three/test-renderer": "npm:8.2.1" @@ -4577,6 +4578,13 @@ __metadata: languageName: unknown linkType: soft +"@raspberrypifoundation/python-friendly-error-messages@npm:0.1.6": + version: 0.1.6 + resolution: "@raspberrypifoundation/python-friendly-error-messages@npm:0.1.6" + checksum: 10/d453c0328a38b9f284009892d10f0c12b344fbd676b17cdfe3f832514d88d293600a648ae960fa38bfb6de1b303d177d2c20d704733ad34b02e16a2dac293b25 + languageName: node + linkType: hard + "@react-spring/animated@npm:~9.6.1": version: 9.6.1 resolution: "@react-spring/animated@npm:9.6.1" From e8ed800ec492e07a435c89e0fabcda0cb6a3b64d Mon Sep 17 00:00:00 2001 From: cocomarine Date: Thu, 28 May 2026 14:51:31 +0100 Subject: [PATCH 02/33] update webpack config to disable ESM strict resolution for pfem --- webpack.config.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/webpack.config.js b/webpack.config.js index 17ab34a2a..4741c5e4a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -67,6 +67,12 @@ const moduleRules = [ exclude: /node_modules/, use: ["babel-loader"], }, + { + test: /\.js$/, + include: + /node_modules\/@raspberrypifoundation\/python-friendly-error-messages/, + resolve: { fullySpecified: false }, + }, { test: /\.css$/, use: ["css-loader"], @@ -237,6 +243,10 @@ const mainConfig = { patterns: [ { from: "public", to: "" }, { from: "src/projects", to: "projects" }, + { + from: "node_modules/@raspberrypifoundation/python-friendly-error-messages/copydecks", + to: "python-error-copydecks", + }, ], }), ], From cfa91e62e5056d6ac0b6e299db18ed632044edc2 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Thu, 28 May 2026 15:58:12 +0100 Subject: [PATCH 03/33] add pfem to skulpt runner WIP pyodide runner --- .../SkulptRunner/SkulptRunner.jsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx index fb4158c53..c9513ec86 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx @@ -16,6 +16,12 @@ import { triggerDraw, setLoadedRunner, } from "../../../../../redux/EditorSlice"; +import { + loadCopydeckFor, + registerAdapter, + skulptAdapter, + friendlyExplain, +} from "@raspberrypifoundation/python-friendly-error-messages"; import ErrorMessage from "../../../ErrorMessage/ErrorMessage"; import ApiCallHandler from "../../../../../utils/apiCallHandler"; import store from "../../../../../redux/stores/WebComponentStore"; @@ -163,6 +169,13 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => { }; }; + useEffect(() => { + loadCopydeckFor(navigator.language, { + base: `${process.env.PUBLIC_URL}/python-error-copydecks/`, + }); + registerAdapter("skulpt", skulptAdapter); + }, []); + useEffect(() => { if (!codeRunTriggered) { setCodeHasVisualOutput( @@ -424,6 +437,18 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => { description: errorDescription, message: errorMessage, }; + + const inputCode = + projectCode?.find((c) => c.name === "main" && c.extension === "py") + ?.content ?? ""; + const friendlyError = friendlyExplain({ + error: errorMessage, + code: inputCode, + runtime: "skulpt", + }); + if (friendlyError?.html) { + errorMessage = friendlyError.html; + } } dispatch(setError(errorMessage)); From 088617ad1161b1b02b756fa6c17a4b6942ea11fb Mon Sep 17 00:00:00 2001 From: cocomarine Date: Mon, 1 Jun 2026 08:50:10 +0100 Subject: [PATCH 04/33] fix pyodide error --- .../PyodideRunner/PyodideRunner.jsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx index b88d5c0e5..264cd05a5 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx @@ -11,6 +11,12 @@ import { updateProjectComponent, addProjectComponent, } from "../../../../../redux/EditorSlice"; +import { + loadCopydeckFor, + registerAdapter, + pyodideAdapter, + friendlyExplain, +} from "@raspberrypifoundation/python-friendly-error-messages"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import { useMediaQuery } from "react-responsive"; import { MOBILE_MEDIA_QUERY } from "../../../../../utils/mediaQueryBreakpoints"; @@ -36,6 +42,13 @@ const getWorkerURL = (url) => { const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => { const [pyodideWorker, setPyodideWorker] = useState(null); + useEffect(() => { + loadCopydeckFor(navigator.language, { + base: `${process.env.PUBLIC_URL}/python-error-copydecks/`, + }); + registerAdapter("pyodide", pyodideAdapter); + }, []); + useEffect(() => { if (active) { const workerUrl = getWorkerURL( @@ -207,6 +220,19 @@ const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => { reactAppApiEndpoint, }); createError(projectIdentifier, userId, { errorType: type, errorMessage }); + + const inputCode = + projectCode?.find((c) => c.name === "main" && c.extension === "py") + ?.content ?? ""; + + const friendlyError = friendlyExplain({ + error: errorMessage, + code: inputCode, + runtime: "pyodide", + }); + if (friendlyError?.html) { + errorMessage = friendlyError.html; + } } dispatch(setError(errorMessage)); From 3ae80e6bbfc675bd7ca21d50fa244dfff3760b53 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Mon, 1 Jun 2026 11:02:16 +0100 Subject: [PATCH 05/33] add friendly error enabled attribute --- .../PyodideRunner/PyodideRunner.jsx | 40 +++++++++++-------- .../Runners/PythonRunner/PythonRunner.jsx | 5 +++ .../SkulptRunner/SkulptRunner.jsx | 38 +++++++++++------- src/containers/WebComponentLoader.jsx | 6 +++ src/redux/EditorSlice.js | 5 +++ src/web-component.js | 2 + 6 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx index 264cd05a5..d36d19b8d 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx @@ -39,14 +39,20 @@ const getWorkerURL = (url) => { return URL.createObjectURL(blob); }; -const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => { +const PyodideRunner = ({ + active, + outputPanels = ["text", "visual"], + friendlyErrorsEnabled = false, +}) => { const [pyodideWorker, setPyodideWorker] = useState(null); useEffect(() => { - loadCopydeckFor(navigator.language, { - base: `${process.env.PUBLIC_URL}/python-error-copydecks/`, - }); - registerAdapter("pyodide", pyodideAdapter); + if (friendlyErrorsEnabled) { + loadCopydeckFor(navigator.language, { + base: `${process.env.PUBLIC_URL}/python-error-copydecks/`, + }); + registerAdapter("pyodide", pyodideAdapter); + } }, []); useEffect(() => { @@ -221,17 +227,19 @@ const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => { }); createError(projectIdentifier, userId, { errorType: type, errorMessage }); - const inputCode = - projectCode?.find((c) => c.name === "main" && c.extension === "py") - ?.content ?? ""; - - const friendlyError = friendlyExplain({ - error: errorMessage, - code: inputCode, - runtime: "pyodide", - }); - if (friendlyError?.html) { - errorMessage = friendlyError.html; + if (friendlyErrorsEnabled) { + const inputCode = + projectCode?.find((c) => c.name === "main" && c.extension === "py") + ?.content ?? ""; + + const friendlyError = friendlyExplain({ + error: errorMessage, + code: inputCode, + runtime: "pyodide", + }); + if (friendlyError?.html) { + errorMessage = friendlyError.html; + } } } diff --git a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx index 60ede272e..2829c2352 100644 --- a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx @@ -27,6 +27,9 @@ const PythonRunner = ({ outputPanels = ["text", "visual"] }) => { const senseHatAlwaysEnabled = useSelector( (state) => state.editor.senseHatAlwaysEnabled, ); + const friendlyErrorsEnabled = useSelector( + (state) => state.editor.friendlyErrorsEnabled, + ); const [usePyodide, setUsePyodide] = useState(null); const [skulptFallback, setSkulptFallback] = useState(false); const { t } = useTranslation(); @@ -74,10 +77,12 @@ const PythonRunner = ({ outputPanels = ["text", "visual"] }) => { ); diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx index c9513ec86..cb45f051f 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx @@ -72,7 +72,11 @@ const VISUAL_LIBRARIES = [ "turtle", ]; -const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => { +const SkulptRunner = ({ + active, + outputPanels = ["text", "visual"], + friendlyErrorsEnabled = false, +}) => { const loadedRunner = useSelector((state) => state.editor.loadedRunner); const projectCode = useSelector((state) => state.editor.project.components); const mainComponent = projectCode?.find( @@ -170,10 +174,12 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => { }; useEffect(() => { - loadCopydeckFor(navigator.language, { - base: `${process.env.PUBLIC_URL}/python-error-copydecks/`, - }); - registerAdapter("skulpt", skulptAdapter); + if (friendlyErrorsEnabled) { + loadCopydeckFor(navigator.language, { + base: `${process.env.PUBLIC_URL}/python-error-copydecks/`, + }); + registerAdapter("skulpt", skulptAdapter); + } }, []); useEffect(() => { @@ -438,16 +444,18 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => { message: errorMessage, }; - const inputCode = - projectCode?.find((c) => c.name === "main" && c.extension === "py") - ?.content ?? ""; - const friendlyError = friendlyExplain({ - error: errorMessage, - code: inputCode, - runtime: "skulpt", - }); - if (friendlyError?.html) { - errorMessage = friendlyError.html; + if (friendlyErrorsEnabled) { + const inputCode = + projectCode?.find((c) => c.name === "main" && c.extension === "py") + ?.content ?? ""; + const friendlyError = friendlyExplain({ + error: errorMessage, + code: inputCode, + runtime: "skulpt", + }); + if (friendlyError?.html) { + errorMessage = friendlyError.html; + } } } diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index df4aa7cc4..7f8d30872 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -3,6 +3,7 @@ import { useSelector, useDispatch } from "react-redux"; import { disableTheming, setSenseHatAlwaysEnabled, + setFriendlyErrorsEnabled, setLoadRemixDisabled, setReactAppApiEndpoint, setScratchApiEndpoint, @@ -68,6 +69,7 @@ const WebComponentLoader = (props) => { scratchApiEndpoint = process.env.REACT_APP_API_ENDPOINT, readOnly = false, senseHatAlwaysEnabled = false, + friendlyErrorsEnabled = false, showSavePrompt = false, sidebarOptions = [], useEditorStyles = false, // If true use the standard editor styling for the web component @@ -185,6 +187,10 @@ const WebComponentLoader = (props) => { dispatch(setSenseHatAlwaysEnabled(senseHatAlwaysEnabled)); }, [senseHatAlwaysEnabled, dispatch]); + useEffect(() => { + dispatch(setFriendlyErrorsEnabled(friendlyErrorsEnabled)); + }, [friendlyErrorsEnabled, dispatch]); + useEffect(() => { dispatch(setLoadRemixDisabled(loadRemixDisabled)); }, [loadRemixDisabled, dispatch]); diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index ec6cbb9f5..a97c25ad0 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -125,6 +125,7 @@ export const editorInitialState = { runnerBeingLoaded: null | "pyodide" | "skulpt", initialComponents: [], scratchIframeProjectIdentifier: null, + friendlyErrorsEnabled: false, }; const isScratchProject = (state) => @@ -285,6 +286,9 @@ export const EditorSlice = createSlice({ setSenseHatEnabled: (state, action) => { state.senseHatEnabled = action.payload; }, + setFriendlyErrorsEnabled: (state, action) => { + state.friendlyErrorsEnabled = action.payload; + }, setLoadRemixDisabled: (state, action) => { state.loadRemixDisabled = action.payload; }, @@ -516,6 +520,7 @@ export const { setInstructionsEditable, setSenseHatAlwaysEnabled, setSenseHatEnabled, + setFriendlyErrorsEnabled, setLoadRemixDisabled, setReactAppApiEndpoint, setScratchApiEndpoint, diff --git a/src/web-component.js b/src/web-component.js index dd13d40ba..6ad8e2ba7 100644 --- a/src/web-component.js +++ b/src/web-component.js @@ -80,6 +80,7 @@ class WebComponent extends HTMLElement { "with_projectbar", "with_sidebar", "load_cache", + "friendly_errors_enabled", ]; } @@ -98,6 +99,7 @@ class WebComponent extends HTMLElement { "with_projectbar", "with_sidebar", "load_cache", + "friendly_errors_enabled", ]; const jsonAttrs = [ "instructions", From cb9f905118a3c272891ee83ff541286ec0e23ff9 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Mon, 1 Jun 2026 11:34:09 +0100 Subject: [PATCH 06/33] fix errors due to ESM to CJS transform --- package.json | 4 ++-- .../PyodideRunner/VisualOutputPane.test.js | 8 +++++++- src/utils/setupTests.js | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f01088541..fddf6b963 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "lint": "eslint 'src/**/*.{js,jsx}' cypress/**/*.js", "lint:fix": "eslint --fix 'src/**/*.{js,jsx}' cypress/**/*.js", "stylelint": "stylelint src/**/*.scss", - "test": "node scripts/test.js --transformIgnorePatterns 'node_modules/(?!three)/'", + "test": "node scripts/test.js", "watch-css": "sass --load-path=./ -q --watch src:src", "heroku-postbuild": "export PUBLIC_URL='' && yarn build" }, @@ -228,7 +228,7 @@ "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|scss|svg|json)$)": "/node_modules/jest-transform-stub" }, "transformIgnorePatterns": [ - "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$", + "[/\\\\]node_modules[/\\\\](?!three).+\\.(js|jsx|mjs|cjs|ts|tsx)$", "^.+\\.module\\.(css|sass|scss)$" ], "modulePaths": [], diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js b/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js index a028b205a..fa68a2436 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js @@ -4,8 +4,10 @@ import configureStore from "redux-mock-store"; import { Provider } from "react-redux"; import VisualOutputPane from "./VisualOutputPane.jsx"; import Highcharts from "highcharts"; +import Plotly from "plotly.js"; jest.mock("highcharts"); +jest.mock("plotly.js"); const renderPaneWithVisuals = (visuals) => { const middlewares = []; @@ -140,7 +142,11 @@ describe("when there is plotly output", () => { }); test("it renders the plotly chart as an svg", () => { - expect(screen.getByText("Test Plot")).toBeInTheDocument(); + expect(Plotly.newPlot).toHaveBeenCalledWith( + expect.any(Object), + [{ x: [1, 2, 3], y: [4, 5, 6], type: "scatter" }], + { title: { text: "Test Plot" } }, + ); }); }); diff --git a/src/utils/setupTests.js b/src/utils/setupTests.js index 0d001b4d4..c17846e7e 100644 --- a/src/utils/setupTests.js +++ b/src/utils/setupTests.js @@ -52,6 +52,20 @@ jest.mock("../assets/markdown/demoInstructions.md", () => { return "demoInstructions.md"; }); +jest.mock("@raspberrypifoundation/python-friendly-error-messages", () => ({ + loadCopydeckFor: jest.fn(), + registerAdapter: jest.fn(), + pyodideAdapter: {}, + skulptAdapter: {}, + friendlyExplain: jest.fn(), +})); + +jest.mock("plotly.js", () => ({ + newPlot: jest.fn(), + react: jest.fn(), + purge: jest.fn(), +})); + global.Blob = jest.fn(); window.URL.createObjectURL = jest.fn(); window.Worker = PyodideWorker; From 4bf16ecb244af73b2191d23d3cfaf01c89883743 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Mon, 1 Jun 2026 11:52:20 +0100 Subject: [PATCH 07/33] remove unnecessary mock --- .../Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js b/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js index fa68a2436..2c4b4dfb1 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.js @@ -7,7 +7,6 @@ import Highcharts from "highcharts"; import Plotly from "plotly.js"; jest.mock("highcharts"); -jest.mock("plotly.js"); const renderPaneWithVisuals = (visuals) => { const middlewares = []; From 9af9cfe1532476081b5a04d011c81b9d4e302c18 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Mon, 1 Jun 2026 15:13:07 +0100 Subject: [PATCH 08/33] add friendlyError to redux and show in ErrorMessage --- .../Editor/ErrorMessage/ErrorMessage.jsx | 17 +++++++++++++++++ .../PyodideRunner/PyodideRunner.jsx | 12 +++++++++--- .../PythonRunner/SkulptRunner/SkulptRunner.jsx | 11 ++++++++--- src/redux/EditorSlice.js | 5 +++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/components/Editor/ErrorMessage/ErrorMessage.jsx b/src/components/Editor/ErrorMessage/ErrorMessage.jsx index 1d50af989..0a4e98fd1 100644 --- a/src/components/Editor/ErrorMessage/ErrorMessage.jsx +++ b/src/components/Editor/ErrorMessage/ErrorMessage.jsx @@ -6,6 +6,7 @@ import { SettingsContext } from "../../../utils/settings"; const ErrorMessage = () => { const message = useRef(); const error = useSelector((state) => state.editor.error); + const friendlyError = useSelector((state) => state.editor.friendlyError); const settings = useContext(SettingsContext); useEffect(() => { @@ -16,6 +17,22 @@ const ErrorMessage = () => { return error ? (

+      {friendlyError && (
+        
+ {friendlyError.title && ( +

+ )} + {friendlyError.summary && ( +

+ )} +

+ )}
) : null; }; diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx index d36d19b8d..bb2099922 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import classNames from "classnames"; import { setError, + setFriendlyError, codeRunHandled, setLoadedRunner, updateProjectComponent, @@ -237,9 +238,13 @@ const PyodideRunner = ({ code: inputCode, runtime: "pyodide", }); - if (friendlyError?.html) { - errorMessage = friendlyError.html; - } + + dispatch( + setFriendlyError({ + title: friendlyError?.title ?? null, + summary: friendlyError?.summary ?? null, + }), + ); } } @@ -290,6 +295,7 @@ const PyodideRunner = ({ const handleRun = async () => { output.current.innerHTML = ""; dispatch(setError("")); + dispatch(setFriendlyError(null)); setVisuals([]); stdinClosed.current = false; diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx index cb45f051f..2b38280cb 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx @@ -10,6 +10,7 @@ import classNames from "classnames"; import { setError, setErrorDetails, + setFriendlyError, codeRunHandled, stopDraw, setSenseHatEnabled, @@ -453,9 +454,13 @@ const SkulptRunner = ({ code: inputCode, runtime: "skulpt", }); - if (friendlyError?.html) { - errorMessage = friendlyError.html; - } + + dispatch( + setFriendlyError({ + title: friendlyError?.title ?? null, + summary: friendlyError?.summary ?? null, + }), + ); } } diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index a97c25ad0..4f045db2e 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -126,6 +126,7 @@ export const editorInitialState = { initialComponents: [], scratchIframeProjectIdentifier: null, friendlyErrorsEnabled: false, + friendlyError: null, }; const isScratchProject = (state) => @@ -289,6 +290,9 @@ export const EditorSlice = createSlice({ setFriendlyErrorsEnabled: (state, action) => { state.friendlyErrorsEnabled = action.payload; }, + setFriendlyError: (state, action) => { + state.friendlyError = action.payload; + }, setLoadRemixDisabled: (state, action) => { state.loadRemixDisabled = action.payload; }, @@ -545,6 +549,7 @@ export const { setSidebarOption, disableTheming, setErrorDetails, + setFriendlyError, } = EditorSlice.actions; export default EditorSlice.reducer; From 7c05ba70513aa5c5b837caebdb663e38ecb4a24e Mon Sep 17 00:00:00 2001 From: cocomarine Date: Mon, 1 Jun 2026 15:56:47 +0100 Subject: [PATCH 09/33] minor fixes --- .../PythonRunner/PyodideRunner/PyodideRunner.jsx | 14 +++++++++----- .../PythonRunner/SkulptRunner/SkulptRunner.jsx | 14 +++++++++----- src/redux/EditorSlice.js | 3 +++ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx index bb2099922..a4ab684d2 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx @@ -54,7 +54,7 @@ const PyodideRunner = ({ }); registerAdapter("pyodide", pyodideAdapter); } - }, []); + }, [friendlyErrorsEnabled]); useEffect(() => { if (active) { @@ -240,10 +240,14 @@ const PyodideRunner = ({ }); dispatch( - setFriendlyError({ - title: friendlyError?.title ?? null, - summary: friendlyError?.summary ?? null, - }), + setFriendlyError( + friendlyError?.title || friendlyError?.summary + ? { + title: friendlyError?.title ?? null, + summary: friendlyError?.summary ?? null, + } + : null, + ), ); } } diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx index 2b38280cb..b787c4654 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx @@ -181,7 +181,7 @@ const SkulptRunner = ({ }); registerAdapter("skulpt", skulptAdapter); } - }, []); + }, [friendlyErrorsEnabled]); useEffect(() => { if (!codeRunTriggered) { @@ -456,10 +456,14 @@ const SkulptRunner = ({ }); dispatch( - setFriendlyError({ - title: friendlyError?.title ?? null, - summary: friendlyError?.summary ?? null, - }), + setFriendlyError( + friendlyError?.title || friendlyError?.summary + ? { + title: friendlyError?.title ?? null, + summary: friendlyError?.summary ?? null, + } + : null, + ), ); } } diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index 4f045db2e..4710aabf1 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -289,6 +289,9 @@ export const EditorSlice = createSlice({ }, setFriendlyErrorsEnabled: (state, action) => { state.friendlyErrorsEnabled = action.payload; + if (!action.payload) { + state.friendlyError = null; + } }, setFriendlyError: (state, action) => { state.friendlyError = action.payload; From c1b803fa66cf4ff806f500a8a7ac36ff6ca11ed3 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Tue, 2 Jun 2026 08:34:38 +0100 Subject: [PATCH 10/33] update editorslice and errormessage tests --- .../Editor/ErrorMessage/ErrorMessage.test.js | 45 ++++++++++++++++++- src/redux/EditorSlice.test.js | 43 ++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/components/Editor/ErrorMessage/ErrorMessage.test.js b/src/components/Editor/ErrorMessage/ErrorMessage.test.js index 59abd00ef..09cca8774 100644 --- a/src/components/Editor/ErrorMessage/ErrorMessage.test.js +++ b/src/components/Editor/ErrorMessage/ErrorMessage.test.js @@ -8,7 +8,7 @@ describe("When error is set", () => { const middlewares = []; const mockStore = configureStore(middlewares); - describe("When error is set", () => { + describe("When error is set and friendlyError is not set", () => { beforeEach(() => { const initialState = { editor: { @@ -35,6 +35,49 @@ describe("When error is set", () => { const errorMessage = screen.queryByText("Oops").parentElement; expect(errorMessage).toHaveClass("error-message--myFontSize"); }); + + test("Does not display friendly error elements", () => { + expect( + document.querySelector(".error-message__friendly"), + ).not.toBeInTheDocument(); + }); + }); + + describe("When friendlyError is set", () => { + beforeEach(() => { + const initialState = { + editor: { + error: "An error occurred", + friendlyError: { + title: "Friendly Error Title", + summary: "This is a more user-friendly explanation of the error.", + }, + }, + }; + const store = mockStore(initialState); + render( + + + + + , + ); + }); + + test("Friendly error title and summary display", () => { + expect(screen.getByText("Friendly Error Title")).toBeInTheDocument(); + expect( + screen.getByText( + "This is a more user-friendly explanation of the error.", + ), + ).toBeInTheDocument(); + }); + + test("original error message also displays", () => { + expect(screen.queryByText("An error occurred")).toBeInTheDocument(); + }); }); it("should render links correctly within the error message", () => { diff --git a/src/redux/EditorSlice.test.js b/src/redux/EditorSlice.test.js index 721b3909c..8065c5470 100644 --- a/src/redux/EditorSlice.test.js +++ b/src/redux/EditorSlice.test.js @@ -20,6 +20,8 @@ import reducer, { scratchSaveStarted, scratchSaveSucceeded, scratchSaveFailed, + setFriendlyErrorsEnabled, + setFriendlyError, } from "./EditorSlice"; const mockCreateRemix = jest.fn(); @@ -238,6 +240,47 @@ test("closing rename modal updates showing status", () => { expect(reducer(previousState, closeRenameFileModal())).toEqual(expectedState); }); +test("Action setFriendlyErrorsEnabled sets friendlyErrorsEnabled to true", () => { + const previousState = { + friendlyErrorsEnabled: false, + }; + const expectedState = { + friendlyErrorsEnabled: true, + }; + expect(reducer(previousState, setFriendlyErrorsEnabled(true))).toEqual( + expectedState, + ); +}); + +test("Action setFriendlyErrorsEnabled sets friendlyErrorsEnabled to false and clears friendlyError", () => { + const previousState = { + friendlyErrorsEnabled: true, + friendlyError: { title: "Error title", summary: "Error summary" }, + }; + const expectedState = { + friendlyErrorsEnabled: false, + friendlyError: null, + }; + expect(reducer(previousState, setFriendlyErrorsEnabled(false))).toEqual( + expectedState, + ); +}); + +test("Action setFriendlyError sets friendlyError", () => { + const previousState = { + friendlyError: null, + }; + const expectedState = { + friendlyError: { title: "Error title", summary: "Error summary" }, + }; + expect( + reducer( + previousState, + setFriendlyError({ title: "Error title", summary: "Error summary" }), + ), + ).toEqual(expectedState); +}); + describe("When project has no identifier", () => { const dispatch = jest.fn(); const project = { From 0958129d416d805e87d15b65d1ae02aad9530a3f Mon Sep 17 00:00:00 2001 From: cocomarine Date: Tue, 2 Jun 2026 10:07:31 +0100 Subject: [PATCH 11/33] update pyodide and skulpt runner tests --- .../PyodideRunner/PyodideRunner.test.js | 53 +++++++++++++++++++ .../SkulptRunner/SkulptRunner.test.js | 46 ++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js index fef816ebe..2d54264ab 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js @@ -20,6 +20,7 @@ import { setSenseHatAlwaysEnabled, openFile, setFocussedFileIndex, + setFriendlyError, } from "../../../../../redux/EditorSlice.js"; import store from "../../../../../app/store"; @@ -435,6 +436,58 @@ describe("When an error is received", () => { payload: "SyntaxError: something's wrong on line 2 of main.py", }); }); + + describe("When friendly errors are enabled", () => { + let loadCopydeckFor; + let registerAdapter; + let friendlyExplain; + + beforeEach(() => { + ({ + // Using the global mock in setupTests.js to track calls to these functions + loadCopydeckFor, + registerAdapter, + friendlyExplain, + } = require("@raspberrypifoundation/python-friendly-error-messages")); + + friendlyExplain.mockReturnValue({ + title: "Friendly error title", + summary: "A friendly summaryof the error", + }); + + render( + + + , + ); + + const worker = PyodideWorker.getLastInstance(); + worker.postMessageFromWorker({ + method: "handleError", + line: 2, + file: "main.py", + type: "SyntaxError", + info: "something's wrong", + }); + }); + + test("loadCopydeckFor is called", () => { + expect(loadCopydeckFor).toHaveBeenCalled(); + }); + + test("registerAdapter is called for pyodide", () => { + expect(registerAdapter).toHaveBeenCalledWith("pyodide", {}); + }); + + test("dispatches setFriendlyError with title and summary", () => { + expect(dispatchSpy).toHaveBeenCalledWith( + setFriendlyError({ + title: "Friendly error title", + summary: "A friendly summaryof the error", + }), + ); + }); + }); }); describe("When the code run is interrupted", () => { diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js index 8539acc03..3bfaf10f8 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js @@ -10,6 +10,7 @@ import { setError, setErrorDetails, triggerDraw, + setFriendlyError, } from "../../../../../redux/EditorSlice"; import { SettingsContext } from "../../../../../utils/settings"; import { matchMedia, setMedia } from "mock-match-media"; @@ -222,6 +223,51 @@ describe("When an error occurs", () => { ]), ); }); + + describe("When friendly errors are enabled", () => { + let loadCopydeckFor; + let registerAdapter; + let friendlyExplain; + + beforeEach(() => { + ({ + // Using the global mock in setupTests.js to track calls to these functions + loadCopydeckFor, + registerAdapter, + friendlyExplain, + } = require("@raspberrypifoundation/python-friendly-error-messages")); + + friendlyExplain.mockReturnValue({ + title: "Friendly error title", + summary: "A friendly summary of the error", + }); + + render( + + + , + ); + }); + + test("loadCopydeckFor is called", () => { + expect(loadCopydeckFor).toHaveBeenCalled(); + }); + + test("registerAdapter is called for skulpt", () => { + expect(registerAdapter).toHaveBeenCalledWith("skulpt", {}); + }); + + test("dispatches setFriendlyError with title and summary", () => { + expect(store.getActions()).toEqual( + expect.arrayContaining([ + setFriendlyError({ + title: "Friendly error title", + summary: "A friendly summary of the error", + }), + ]), + ); + }); + }); }); describe("When an error originates in the sense_hat shim", () => { From 6ff35ecd4021a18bf5f138ddce6961c412f39fb4 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Tue, 2 Jun 2026 10:17:46 +0100 Subject: [PATCH 12/33] plain text friendly error for now --- .../Editor/ErrorMessage/ErrorMessage.jsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/Editor/ErrorMessage/ErrorMessage.jsx b/src/components/Editor/ErrorMessage/ErrorMessage.jsx index 0a4e98fd1..db370dbd2 100644 --- a/src/components/Editor/ErrorMessage/ErrorMessage.jsx +++ b/src/components/Editor/ErrorMessage/ErrorMessage.jsx @@ -20,16 +20,14 @@ const ErrorMessage = () => { {friendlyError && (
{friendlyError.title && ( -

+

+ {friendlyError.title} +

)} {friendlyError.summary && ( -

+

+ {friendlyError.summary} +

)}
)} From 0ac29914d2d1f8443ddae834609858431e637fda Mon Sep 17 00:00:00 2001 From: cocomarine Date: Tue, 2 Jun 2026 15:41:18 +0100 Subject: [PATCH 13/33] add dompurify --- package.json | 1 + yarn.lock | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/package.json b/package.json index fddf6b963..c143af3fb 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "codemirror": "^6.0.1", "container-query-polyfill": "^1.0.2", "date-fns": "^4.1.0", + "dompurify": "^3.4.6", "eslint-config-prettier": "^8.8.0", "file-saver": "^2.0.5", "fs-extra": "^9.0.1", diff --git a/yarn.lock b/yarn.lock index e9082dbc2..98d6e8c85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4460,6 +4460,7 @@ __metadata: curl: "npm:^0.1.4" cypress: "npm:14.5.4" date-fns: "npm:^4.1.0" + dompurify: "npm:^3.4.6" dotenv: "npm:8.2.0" dotenv-expand: "npm:5.1.0" dotenv-webpack: "npm:8.1.0" @@ -10683,6 +10684,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^3.4.6": + version: 3.4.6 + resolution: "dompurify@npm:3.4.6" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10/950bfadc9ad6ee5706ccdfde09313c9c8f6a299206f77ccb621b1355947443753163031770cbb46236f1b6910e57e6710c34970b13c8a9e0fd7c93fac77c4815 + languageName: node + linkType: hard + "domutils@npm:^1.5.1, domutils@npm:^1.7.0": version: 1.7.0 resolution: "domutils@npm:1.7.0" From 64c2c4c4061a811791ae7706340e0c28f434dd05 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Tue, 2 Jun 2026 15:43:17 +0100 Subject: [PATCH 14/33] dispatch friendly error html and use css to restrict to title and summary --- src/assets/stylesheets/ErrorMessage.scss | 10 +++++ .../Editor/ErrorMessage/ErrorMessage.jsx | 26 ++++++------- .../Editor/ErrorMessage/ErrorMessage.test.js | 37 ++++++++++++++++--- .../PyodideRunner/PyodideRunner.jsx | 13 ++----- .../SkulptRunner/SkulptRunner.jsx | 13 ++----- 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/assets/stylesheets/ErrorMessage.scss b/src/assets/stylesheets/ErrorMessage.scss index ef8f07cce..33a3a0af7 100644 --- a/src/assets/stylesheets/ErrorMessage.scss +++ b/src/assets/stylesheets/ErrorMessage.scss @@ -21,4 +21,14 @@ &--large { @include font-size-2(regular); } + + // Only displaying title and summary of friendlyError + &__friendly { + .pfem__why, + .pfem__steps, + .pfem__patch, + .pfem__details { + display: none; + } + } } diff --git a/src/components/Editor/ErrorMessage/ErrorMessage.jsx b/src/components/Editor/ErrorMessage/ErrorMessage.jsx index db370dbd2..b0893d199 100644 --- a/src/components/Editor/ErrorMessage/ErrorMessage.jsx +++ b/src/components/Editor/ErrorMessage/ErrorMessage.jsx @@ -1,6 +1,7 @@ import React, { useContext, useEffect, useRef } from "react"; import "../../../assets/stylesheets/ErrorMessage.scss"; import { useSelector } from "react-redux"; +import DOMPurify from "dompurify"; import { SettingsContext } from "../../../utils/settings"; const ErrorMessage = () => { @@ -14,22 +15,19 @@ const ErrorMessage = () => { message.current.innerHTML = error; } }, [error]); + + const friendlyErrorHtml = friendlyError?.html + ? DOMPurify.sanitize(friendlyError.html) + : null; + return error ? (
-

-      {friendlyError && (
-        
- {friendlyError.title && ( -

- {friendlyError.title} -

- )} - {friendlyError.summary && ( -

- {friendlyError.summary} -

- )} -
+
+      {friendlyErrorHtml && (
+        
)}
) : null; diff --git a/src/components/Editor/ErrorMessage/ErrorMessage.test.js b/src/components/Editor/ErrorMessage/ErrorMessage.test.js index 09cca8774..f9fe32a43 100644 --- a/src/components/Editor/ErrorMessage/ErrorMessage.test.js +++ b/src/components/Editor/ErrorMessage/ErrorMessage.test.js @@ -8,7 +8,7 @@ describe("When error is set", () => { const middlewares = []; const mockStore = configureStore(middlewares); - describe("When error is set and friendlyError is not set", () => { + describe("When friendlyError is not set", () => { beforeEach(() => { const initialState = { editor: { @@ -49,8 +49,7 @@ describe("When error is set", () => { editor: { error: "An error occurred", friendlyError: { - title: "Friendly Error Title", - summary: "This is a more user-friendly explanation of the error.", + html: '
Friendly Error Title
This is a more user-friendly explanation of the error.
A variable created in another place might not be available here.
  • Move the line that makes it to above where you use it.
  • Or set it here just before you use it.
kettle = 0
Original error
An error occurred
', }, }, }; @@ -75,8 +74,36 @@ describe("When error is set", () => { ).toBeInTheDocument(); }); - test("original error message also displays", () => { - expect(screen.queryByText("An error occurred")).toBeInTheDocument(); + it("strips unsafe scripts", () => { + const initialState = { + editor: { + error: "An error occurred", + friendlyError: { + html: ` +
Friendly Error Title
+ + + Bad link + `, + }, + }, + }; + const store = mockStore(initialState); + const { container } = render( + + + + + , + ); + + expect(container.querySelector("script")).not.toBeInTheDocument(); + expect(container.querySelector("[onerror]")).not.toBeInTheDocument(); + expect( + container.querySelector('a[href^="javascript:"]'), + ).not.toBeInTheDocument(); }); }); diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx index a4ab684d2..ebdcf013c 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx @@ -239,16 +239,9 @@ const PyodideRunner = ({ runtime: "pyodide", }); - dispatch( - setFriendlyError( - friendlyError?.title || friendlyError?.summary - ? { - title: friendlyError?.title ?? null, - summary: friendlyError?.summary ?? null, - } - : null, - ), - ); + if (friendlyError?.html) { + dispatch(setFriendlyError({ html: friendlyError.html })); + } } } diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx index b787c4654..19b78d0e6 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx @@ -455,16 +455,9 @@ const SkulptRunner = ({ runtime: "skulpt", }); - dispatch( - setFriendlyError( - friendlyError?.title || friendlyError?.summary - ? { - title: friendlyError?.title ?? null, - summary: friendlyError?.summary ?? null, - } - : null, - ), - ); + if (friendlyError?.html) { + dispatch(setFriendlyError({ html: friendlyError.html })); + } } } From 4fda69e6cbe73656a3b0ce58c29510844c2515f6 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Tue, 2 Jun 2026 15:55:05 +0100 Subject: [PATCH 15/33] update runner tests --- .../PythonRunner/PyodideRunner/PyodideRunner.test.js | 8 +++----- .../PythonRunner/SkulptRunner/SkulptRunner.test.js | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js index 2d54264ab..a3fdbd61c 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js @@ -451,8 +451,7 @@ describe("When an error is received", () => { } = require("@raspberrypifoundation/python-friendly-error-messages")); friendlyExplain.mockReturnValue({ - title: "Friendly error title", - summary: "A friendly summaryof the error", + html: '
Friendly error title
A friendly summary of the error
', }); render( @@ -479,11 +478,10 @@ describe("When an error is received", () => { expect(registerAdapter).toHaveBeenCalledWith("pyodide", {}); }); - test("dispatches setFriendlyError with title and summary", () => { + test("dispatches setFriendlyError", () => { expect(dispatchSpy).toHaveBeenCalledWith( setFriendlyError({ - title: "Friendly error title", - summary: "A friendly summaryof the error", + html: '
Friendly error title
A friendly summary of the error
', }), ); }); diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js index 3bfaf10f8..bdfb9e503 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js @@ -238,8 +238,7 @@ describe("When an error occurs", () => { } = require("@raspberrypifoundation/python-friendly-error-messages")); friendlyExplain.mockReturnValue({ - title: "Friendly error title", - summary: "A friendly summary of the error", + html: '
Friendly error title
A friendly summary of the error
', }); render( @@ -257,12 +256,11 @@ describe("When an error occurs", () => { expect(registerAdapter).toHaveBeenCalledWith("skulpt", {}); }); - test("dispatches setFriendlyError with title and summary", () => { + test("dispatches setFriendlyError", () => { expect(store.getActions()).toEqual( expect.arrayContaining([ setFriendlyError({ - title: "Friendly error title", - summary: "A friendly summary of the error", + html: '
Friendly error title
A friendly summary of the error
', }), ]), ); From 3dc2a2eb55add6ede5fc6d83dced2678ce601088 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Tue, 2 Jun 2026 16:15:54 +0100 Subject: [PATCH 16/33] update editorslice test --- src/redux/EditorSlice.test.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/redux/EditorSlice.test.js b/src/redux/EditorSlice.test.js index 8065c5470..768b7e9de 100644 --- a/src/redux/EditorSlice.test.js +++ b/src/redux/EditorSlice.test.js @@ -255,7 +255,9 @@ test("Action setFriendlyErrorsEnabled sets friendlyErrorsEnabled to true", () => test("Action setFriendlyErrorsEnabled sets friendlyErrorsEnabled to false and clears friendlyError", () => { const previousState = { friendlyErrorsEnabled: true, - friendlyError: { title: "Error title", summary: "Error summary" }, + friendlyError: { + html: '
Friendly error title
A friendly summary of the error
', + }, }; const expectedState = { friendlyErrorsEnabled: false, @@ -271,12 +273,16 @@ test("Action setFriendlyError sets friendlyError", () => { friendlyError: null, }; const expectedState = { - friendlyError: { title: "Error title", summary: "Error summary" }, + friendlyError: { + html: '
Friendly error title
A friendly summary of the error
', + }, }; expect( reducer( previousState, - setFriendlyError({ title: "Error title", summary: "Error summary" }), + setFriendlyError({ + html: '
Friendly error title
A friendly summary of the error
', + }), ), ).toEqual(expectedState); }); From 5be129988978a46084e045a8c55a81f368ba6b69 Mon Sep 17 00:00:00 2001 From: cocomarine Date: Wed, 3 Jun 2026 08:55:36 +0100 Subject: [PATCH 17/33] stub out friendlyerror html in tests --- .../PythonRunner/PyodideRunner/PyodideRunner.test.js | 8 ++++++-- .../PythonRunner/SkulptRunner/SkulptRunner.test.js | 8 ++++++-- src/redux/EditorSlice.test.js | 10 +++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js index a3fdbd61c..582abdcd4 100644 --- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js +++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js @@ -41,6 +41,10 @@ const project = { window.crossOriginIsolated = true; process.env.PUBLIC_URL = "."; +const friendlyErrorHtml = + '
Friendly error title
' + + '
A friendly summary of the error
'; + const updateRunner = ({ project = {}, codeRunTriggered = false }) => { act(() => { if (project) { @@ -451,7 +455,7 @@ describe("When an error is received", () => { } = require("@raspberrypifoundation/python-friendly-error-messages")); friendlyExplain.mockReturnValue({ - html: '
Friendly error title
A friendly summary of the error
', + html: friendlyErrorHtml, }); render( @@ -481,7 +485,7 @@ describe("When an error is received", () => { test("dispatches setFriendlyError", () => { expect(dispatchSpy).toHaveBeenCalledWith( setFriendlyError({ - html: '
Friendly error title
A friendly summary of the error
', + html: friendlyErrorHtml, }), ); }); diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js index bdfb9e503..f5f5bf5b1 100644 --- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js +++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js @@ -32,6 +32,10 @@ const user = { }, }; +const friendlyErrorHtml = + '
Friendly error title
' + + '
A friendly summary of the error
'; + describe("Testing basic input span functionality", () => { let input; let store; @@ -238,7 +242,7 @@ describe("When an error occurs", () => { } = require("@raspberrypifoundation/python-friendly-error-messages")); friendlyExplain.mockReturnValue({ - html: '
Friendly error title
A friendly summary of the error
', + html: friendlyErrorHtml, }); render( @@ -260,7 +264,7 @@ describe("When an error occurs", () => { expect(store.getActions()).toEqual( expect.arrayContaining([ setFriendlyError({ - html: '
Friendly error title
A friendly summary of the error
', + html: friendlyErrorHtml, }), ]), ); diff --git a/src/redux/EditorSlice.test.js b/src/redux/EditorSlice.test.js index 768b7e9de..ef4db7eb7 100644 --- a/src/redux/EditorSlice.test.js +++ b/src/redux/EditorSlice.test.js @@ -38,6 +38,10 @@ jest.mock("../utils/apiCallHandler", () => () => ({ createOrUpdateProject: jest.fn(mockCreateOrUpdateProject), })); +const friendlyErrorHtml = + '
Friendly error title
' + + '
A friendly summary of the error
'; + test("Action stopCodeRun sets codeRunStopped to true", () => { const previousState = { codeRunTriggered: true, @@ -256,7 +260,7 @@ test("Action setFriendlyErrorsEnabled sets friendlyErrorsEnabled to false and cl const previousState = { friendlyErrorsEnabled: true, friendlyError: { - html: '
Friendly error title
A friendly summary of the error
', + html: friendlyErrorHtml, }, }; const expectedState = { @@ -274,14 +278,14 @@ test("Action setFriendlyError sets friendlyError", () => { }; const expectedState = { friendlyError: { - html: '
Friendly error title
A friendly summary of the error
', + html: friendlyErrorHtml, }, }; expect( reducer( previousState, setFriendlyError({ - html: '
Friendly error title
A friendly summary of the error
', + html: friendlyErrorHtml, }), ), ).toEqual(expectedState); From 113797faa759f7aaddca427be28a67aa97dbc9e9 Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Fri, 29 May 2026 16:05:39 +0100 Subject: [PATCH 18/33] refactor: remove defaulting to editor styles --- src/web-component.html | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/web-component.html b/src/web-component.html index 0611083eb..34998332a 100644 --- a/src/web-component.html +++ b/src/web-component.html @@ -1,4 +1,4 @@ - +