diff --git a/cypress/e2e/spec-wc-pyodide.cy.js b/cypress/e2e/spec-wc-pyodide.cy.js
index 371900470..ac45af0ec 100644
--- a/cypress/e2e/spec-wc-pyodide.cy.js
+++ b/cypress/e2e/spec-wc-pyodide.cy.js
@@ -1,6 +1,7 @@
import {
getEditorShadow,
getErrorMessage,
+ getFriendlyErrorMessage,
getFileButtonByName,
getProgramInput,
getPyodideOutput,
@@ -253,3 +254,41 @@ print(text_out)
);
});
});
+
+describe("When friendly errors enabled with pyodide", () => {
+ beforeEach(() => {
+ cy.intercept("GET", "**/python-error-copydecks/**", {
+ fixture: "copydeck.json",
+ }).as("copydeck");
+
+ const params = new URLSearchParams();
+ params.set("friendly_errors_enabled", "true");
+
+ cy.visit({
+ url: `${origin}?${params.toString()}`,
+ headers: {
+ "Cross-Origin-Opener-Policy": "same-origin",
+ "Cross-Origin-Embedder-Policy": "require-corp",
+ },
+ });
+ cy.window().then((win) => {
+ Object.defineProperty(win, "crossOriginIsolated", {
+ value: true,
+ configurable: true,
+ });
+ });
+ cy.wait("@copydeck");
+ });
+
+ it("shows a friendly error message when an error occurs", () => {
+ runCode("print(kitten)");
+ getErrorMessage().should(
+ "contain",
+ "NameError: name 'kitten' is not defined on line 1 of main.py",
+ );
+ getFriendlyErrorMessage().should(
+ "contain",
+ "This variable doesn't exist here",
+ );
+ });
+});
diff --git a/cypress/e2e/spec-wc-skulpt.cy.js b/cypress/e2e/spec-wc-skulpt.cy.js
index 4535072da..d2fe74f18 100644
--- a/cypress/e2e/spec-wc-skulpt.cy.js
+++ b/cypress/e2e/spec-wc-skulpt.cy.js
@@ -1,5 +1,6 @@
import {
getErrorMessage,
+ getFriendlyErrorMessage,
getP5Canvas,
getPythonConsoleOutput,
getSkulptRunner,
@@ -149,3 +150,35 @@ describe("Running the code with skulpt", () => {
);
});
});
+
+describe("When friendly errors enabled with skulpt", () => {
+ beforeEach(() => {
+ cy.intercept("GET", "**/python-error-copydecks/**", {
+ fixture: "copydeck.json",
+ }).as("copydeck");
+
+ const params = new URLSearchParams();
+ params.set("friendly_errors_enabled", "true");
+
+ cy.visit(`${origin}?${params.toString()}`);
+ cy.window().then((win) => {
+ Object.defineProperty(win, "crossOriginIsolated", {
+ value: false,
+ configurable: true,
+ });
+ });
+ cy.wait("@copydeck");
+ });
+
+ it("shows a friendly error message when an error occurs", () => {
+ runCode("import turtle\nprint(kitten)");
+ getErrorMessage().should(
+ "contain",
+ "NameError: name 'kitten' is not defined on line 2 of main.py",
+ );
+ getFriendlyErrorMessage().should(
+ "contain",
+ "This variable doesn't exist here",
+ );
+ });
+});
diff --git a/cypress/fixtures/copydeck.json b/cypress/fixtures/copydeck.json
new file mode 100644
index 000000000..abe369568
--- /dev/null
+++ b/cypress/fixtures/copydeck.json
@@ -0,0 +1,41 @@
+{
+ "errors": {
+ "NameError": {
+ "variants": [
+ {
+ "if": {
+ "not_message": ["is not defined"]
+ },
+ "title": "This variable doesn't exist yet",
+ "summary": "Your code uses the variable {{name}}, but it hasn't been created yet. Check {{loc}}. If you meant to print the text {{name}}, put it in double quotes.",
+ "why": "Without speech marks Python treats {{name}} as a variable, and this variable does not exist yet.",
+ "steps": [
+ "If it is meant to be text, put speech marks around {{name}}.",
+ "If it is meant to be a variable, make it first (for example: {{name}} = 0).",
+ "Check spelling and capital letters."
+ ],
+ "_placeholders": {
+ "name": "The undefined variable name, e.g. kittens",
+ "loc": "Where it was used, e.g. line 2 in main.py"
+ }
+ },
+ {
+ "if": {
+ "match_message": ["is not defined"]
+ },
+ "title": "This variable doesn't exist here",
+ "summary": "{{name}} might be created somewhere else, but you're using it at {{loc}}. If you meant the text {{name}}, put it in double quotes.",
+ "why": "A variable created in another place might not be available here.",
+ "steps": [
+ "Move the line that makes it to above where you use it.",
+ "Or set it here just before you use it."
+ ],
+ "_placeholders": {
+ "name": "The variable name used out of scope, e.g. total",
+ "loc": "Where it was referenced, e.g. line 8 in main.py"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/cypress/helpers/editor.js b/cypress/helpers/editor.js
index eddc88914..432b935a7 100644
--- a/cypress/helpers/editor.js
+++ b/cypress/helpers/editor.js
@@ -61,7 +61,10 @@ export const getP5Canvas = () => getEditorShadow().find(".p5Canvas");
export const getTurtleOutput = () => getEditorShadow().find("#turtleOutput");
export const getErrorMessage = () =>
- getEditorShadow().find(".error-message__content");
+ getEditorShadow().find(".error-message__python");
+
+export const getFriendlyErrorMessage = () =>
+ getEditorShadow().find(".friendly-error-message");
export const getTextOutputTab = () =>
getPyodideOutput().findByLabelText("Text output");
diff --git a/src/assets/icons/cancel_FILL.svg b/src/assets/icons/cancel_FILL.svg
new file mode 100644
index 000000000..5ef0d2fec
--- /dev/null
+++ b/src/assets/icons/cancel_FILL.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/stylesheets/EditorPanel.scss b/src/assets/stylesheets/EditorPanel.scss
index f29850738..2a147f49a 100644
--- a/src/assets/stylesheets/EditorPanel.scss
+++ b/src/assets/stylesheets/EditorPanel.scss
@@ -1,5 +1,6 @@
-@forward '@raspberrypifoundation/design-system-core/scss/properties/spacing';
-@use '@raspberrypifoundation/design-system-core/scss/mixins/typography' as typography;
+@forward "@raspberrypifoundation/design-system-core/scss/properties/spacing";
+@use "@raspberrypifoundation/design-system-core/scss/mixins/typography" as
+ typography;
// Scrollbar-width is needed from scrollbar to show in Chrome
.editor-wrapper {
@@ -31,7 +32,6 @@
overscroll-behavior-x: none;
font-family: var(--wc-font-family-monospace);
-
.cm-content {
flex: 1;
padding-block-start: var(--space-0-5);
@@ -60,5 +60,3 @@
display: none;
}
}
-
-
diff --git a/src/assets/stylesheets/ErrorMessage.scss b/src/assets/stylesheets/ErrorMessage.scss
index 33a3a0af7..ab09a1ef0 100644
--- a/src/assets/stylesheets/ErrorMessage.scss
+++ b/src/assets/stylesheets/ErrorMessage.scss
@@ -1,25 +1,35 @@
-@use "./rpf_design_system/font-size" as *;
-@use "./rpf_design_system/spacing" as *;
+@use "@raspberrypifoundation/design-system-core/scss/mixins/typography" as
+ typography;
.error-message {
- color: #7e0305;
- background-color: #fde2e1;
- padding: $space-0-75 $space-1-25;
+ color: var(--rpf-red-900);
+ background-color: var(--rpf-red-100);
+ border-block-start: 1px solid var(--editor-color-outline);
+ padding: var(--space-1);
overflow-y: auto;
+ scrollbar-width: thin;
+ max-block-size: 50%;
- &__content {
+ &__error {
padding: 0;
margin: 0;
white-space: pre-wrap;
overflow-wrap: break-word;
+ font-weight: bold;
}
- &--medium {
- @include font-size-1-5(regular);
- }
+ &__python {
+ display: flex;
+ gap: var(--space-1);
+ @include typography.style-1;
- &--large {
- @include font-size-2(regular);
+ &--medium {
+ @include typography.style-1;
+ }
+
+ &--large {
+ @include typography.style-2;
+ }
}
// Only displaying title and summary of friendlyError
diff --git a/src/assets/stylesheets/FriendlyErrorMessage.scss b/src/assets/stylesheets/FriendlyErrorMessage.scss
new file mode 100644
index 000000000..09a943264
--- /dev/null
+++ b/src/assets/stylesheets/FriendlyErrorMessage.scss
@@ -0,0 +1,27 @@
+.friendly-error-message {
+ background-color: var(--rpf-white);
+ padding: var(--space-2);
+ color: var(--rpf-text-primary);
+ border-radius: var(--space-1);
+ margin-block-start: var(--space-1);
+
+ .pfem {
+ &__title {
+ display: block;
+ font-weight: 600;
+ margin-block-end: var(--space-1);
+ }
+
+ &__summary {
+ display: block;
+ }
+
+ // Only displaying title and summary of friendlyError
+ &__why,
+ &__steps,
+ &__patch,
+ &__details {
+ display: none;
+ }
+ }
+}
diff --git a/src/assets/stylesheets/InternalStyles.scss b/src/assets/stylesheets/InternalStyles.scss
index 01f136b70..9f36bf398 100644
--- a/src/assets/stylesheets/InternalStyles.scss
+++ b/src/assets/stylesheets/InternalStyles.scss
@@ -43,6 +43,7 @@
@use "./EditorFinalStep.scss" as *;
@use "./Loader.scss" as *;
@use "./LoadFailed.scss" as *;
+@use "./FriendlyErrorMessage" as *;
@use "./settings/fonts" as fonts;
@@ -158,7 +159,9 @@ button:focus-visible {
// Primary button
--rpf-button-primary-background-color: var(--editor-color-theme);
--rpf-button-primary-background-color-focus: var(--editor-color-theme);
- --rpf-button-primary-background-color-hover: var(--editor-color-theme-secondary);
+ --rpf-button-primary-background-color-hover: var(
+ --editor-color-theme-secondary
+ );
--rpf-button-primary-background-color-active: var(--rpf-navy-600);
--rpf-button-primary-background-color-disabled: var(--rpf-navy-200);
--rpf-button-primary-color-disabled: var(--rpf-grey-600);
@@ -167,10 +170,16 @@ button:focus-visible {
// Secondary button
--rpf-button-secondary-background-color: var(--editor-color-theme);
--rpf-button-secondary-background-color-focus: var(--rpf-brand-raspberry);
- --rpf-button-secondary-background-color-hover: var(--editor-color-theme-tertiary);
+ --rpf-button-secondary-background-color-hover: var(
+ --editor-color-theme-tertiary
+ );
--rpf-button-secondary-border-color: var(--editor-color-theme);
- --rpf-button-secondary-border-color-hover: var(--editor-color-theme-secondary);
- --rpf-button-secondary-background-color-active: var(--editor-color-theme-secondary);
+ --rpf-button-secondary-border-color-hover: var(
+ --editor-color-theme-secondary
+ );
+ --rpf-button-secondary-background-color-active: var(
+ --editor-color-theme-secondary
+ );
--rpf-button-secondary-background-color-disabled: var(--rpf-grey-50);
--rpf-button-secondary-text-color: var(--editor-color-theme);
@@ -230,7 +239,9 @@ button:focus-visible {
--rpf-button-secondary-text-color: var(--rpf-white);
--rpf-button-secondary-background-color: var(--editor-color-layer-2);
--rpf-button-secondary-background-color-active: var(--rpf-navy-200);
- --rpf-button-secondary-color-disabled-background: var(--editor-color-layer-3);
+ --rpf-button-secondary-color-disabled-background: var(
+ --editor-color-layer-3
+ );
--rpf-button-secondary-background-color-hover: var(--editor-color-outline);
--rpf-button-secondary-border-color: var(--editor-color-theme);
--rpf-button-secondary-border-color-hover: var(--editor-color-theme);
@@ -240,9 +251,17 @@ button:focus-visible {
// Tertiary button
--rpf-button-tertiary-text-color-hover: var(--rpf-grey-200);
--rpf-button-tertiary-danger-text-color: var(--rpf-red-600);
- --rpf-button-tertiary-danger-background-color-hover: rgba(255, 255, 255, 0.1);
- --rpf-button-tertiary-danger-background-color-active: rgba(255, 255, 255, 0.15);
+ --rpf-button-tertiary-danger-background-color-hover: rgba(
+ 255,
+ 255,
+ 255,
+ 0.1
+ );
+ --rpf-button-tertiary-danger-background-color-active: rgba(
+ 255,
+ 255,
+ 255,
+ 0.15
+ );
}
}
-
-
diff --git a/src/components/Editor/EditorInput/EditorInput.jsx b/src/components/Editor/EditorInput/EditorInput.jsx
index ece8db7ff..98de64507 100644
--- a/src/components/Editor/EditorInput/EditorInput.jsx
+++ b/src/components/Editor/EditorInput/EditorInput.jsx
@@ -15,6 +15,7 @@ import EditorPanel from "../EditorPanel/EditorPanel";
import DraggableTab from "../DraggableTabs/DraggableTab";
import DroppableTabList from "../DraggableTabs/DroppableTabList";
import RunBar from "../../RunButton/RunBar";
+import ErrorMessage from "../ErrorMessage/ErrorMessage";
import "../../../assets/stylesheets/EditorInput.scss";
import RunnerControls from "../../RunButton/RunnerControls";
@@ -186,6 +187,7 @@ const EditorInput = () => {
/>
))}
+
kettle = 0
An error occurred