Skip to content

Commit ed1216c

Browse files
authored
Fix feedback panel in scratch e2e tests (#1497)
issue: [1489](RaspberryPiFoundation/digital-editor-issues#1489) ## Summary Upgrade editor-ui to React 19 (matching editor-standalone) and bump related dependencies so the Scratch classroom flow works again after standalone’s React 19 upgrade. ## Why the Scratch e2e test was failing The test failed at the final step: **teacher opens a student’s Scratch remix** from Student work. The Scratch iframe never loaded because **`editor-wc` crashed** with: `Objects are not valid as a React child` That only happens on this step because it is the first time **`isViewingStudentWork`** is true, which registers the **feedback sidebar plugin**. ## Feedback plugin + sidebar: the underlying issue Classroom (`editor-standalone`) passes a sidebar plugin into the web component (`editor-ui`) via `setSidebarPlugins`. That plugin includes **React elements and components created by standalone’s React** (`FeedbackIcon`, `panel()`, `buttons()`), which `Sidebar` then renders inside **editor-ui’s React tree**. Standalone and editor-ui are **separate bundles with separate React instances**. Passing elements across that boundary is unsupported. This was **latent for years** but broke when standalone moved to **React 19** (June 9, PR #932) while editor-ui stayed on React 18 — React 19 elements are rejected by React 18’s reconciler. **Why it’s brittle:** even with both on React 19, this remains fragile. Two bundled React copies can appear to work for simple JSX but can still break (hooks, context, portaled UI). A robust long-term fix would avoid passing React elements across the web component boundary (e.g. mount callbacks / string icon names only). ## Fix Bump **editor-ui to React 19.2.7** to align with **editor-standalone**. Verified locally: teacher → student Scratch remix flow works again. ## Other dependency bumps | Package | Change | Why | |---------|--------|-----| | **react / react-dom** | 18 → **19.2.7** | Match standalone; fixes cross-version plugin crash | | **react-toastify** | 8 → **11.1.0** | v8 uses removed `react-dom` `render()` API; v11 supports React 18/19. Updated toast positions and icon usage in `Notifications.js`; removed v8 `ToastContainer.defaultProps` workaround | | **@react-three/fiber** | 8 → **9.6.1** | R3F v8 pairs with React 18 only; v9 required for React 19 (Astro Pi 3D simulator) | | **@react-three/drei** | 9 → **10.0.0** | Companion to R3F v9 (helpers used by Astro Pi `Simulator` / `FlightCase`) | | **@react-three/test-renderer** | 8 → **9.0.0** | Test support for R3F v9 | ## Scratch React 18 vendor aliases ```json "scratchReactVendor": "npm:react@18.3.1", "scratchReactDomVendor": "npm:react-dom@18.3.1" ``` The main app uses **React 19**, but the **Scratch iframe** (`scratch.html`) loads **`scratch-gui` via `<script>` tags** and expects **React 18 UMD** globals (`vendor/react.production.min.js`). React 19 **no longer ships UMD builds**, so we cannot copy from the main `react` dependency. These aliases install React 18 solely so webpack can copy UMD files into `build/vendor/` for the Scratch iframe.
1 parent 550010c commit ed1216c

9 files changed

Lines changed: 430 additions & 326 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ ASSETS_URL='http://localhost:3011'
66
HTML_RENDERER_URL='http://localhost:3011'
77
REACT_APP_GOOGLE_TAG_MANAGER_ID=''
88
REACT_APP_API_ENDPOINT='http://localhost:3009'
9-
REACT_APP_ALLOWED_IFRAME_ORIGINS='http://localhost:3011,http://localhost:3012,http://classroom.localhost:3012'
9+
REACT_APP_ALLOWED_IFRAME_ORIGINS='http://localhost:3011,http://localhost:3012,http://classroom.localhost:3013'
1010

1111

cypress/e2e/spec-wc-resize.cy.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
dragEditorResizeHandle,
3+
dragSidebarResizeHandle,
4+
getEditorResizeHandle,
5+
getProjEditorContainer,
6+
getSidebarPanel,
7+
loadPythonStarterProject,
8+
} from "../helpers/editor.js";
9+
10+
const origin = "http://localhost:3011/web-component.html";
11+
12+
beforeEach(() => {
13+
cy.intercept("*", (req) => {
14+
req.headers.Origin = origin;
15+
req.continue();
16+
});
17+
});
18+
19+
const visitAndLoadPythonProject = () => {
20+
cy.visit(origin);
21+
loadPythonStarterProject();
22+
getProjEditorContainer().should("exist");
23+
};
24+
25+
describe("layout resize", () => {
26+
it("resizes editor/output split on desktop", () => {
27+
cy.viewport(1280, 800);
28+
visitAndLoadPythonProject();
29+
30+
getEditorResizeHandle("verticalHandle").should("exist");
31+
32+
let initialWidth;
33+
getProjEditorContainer().then(($el) => {
34+
initialWidth = $el[0].getBoundingClientRect().width;
35+
});
36+
37+
dragEditorResizeHandle("verticalHandle", { deltaX: -100 });
38+
39+
getProjEditorContainer().should(($el) => {
40+
const nextWidth = $el[0].getBoundingClientRect().width;
41+
expect(nextWidth).to.be.lessThan(initialWidth);
42+
});
43+
});
44+
45+
it("resizes editor/output split on stacked layout", () => {
46+
// 700px is deliberately between two layout breakpoints:
47+
// - Wider than 600px: still uses the desktop editor (with resize handles).
48+
// - Narrower than 720px in the project area: editor sits above output, with a
49+
// horizontal drag bar between them (not side-by-side).
50+
cy.viewport(700, 900);
51+
visitAndLoadPythonProject();
52+
53+
getEditorResizeHandle("horizontalHandle").should("exist");
54+
55+
let initialHeight;
56+
getProjEditorContainer().then(($el) => {
57+
initialHeight = $el[0].getBoundingClientRect().height;
58+
});
59+
60+
dragEditorResizeHandle("horizontalHandle", { deltaY: 80 });
61+
62+
getProjEditorContainer().should(($el) => {
63+
const nextHeight = $el[0].getBoundingClientRect().height;
64+
expect(nextHeight).not.to.equal(initialHeight);
65+
});
66+
});
67+
68+
it("resizes sidebar file panel on desktop", () => {
69+
cy.viewport(1280, 800);
70+
visitAndLoadPythonProject();
71+
72+
getSidebarPanel().should("be.visible");
73+
74+
let initialWidth;
75+
getSidebarPanel().then(($el) => {
76+
initialWidth = $el[0].getBoundingClientRect().width;
77+
});
78+
79+
dragSidebarResizeHandle({ deltaX: 80 });
80+
81+
getSidebarPanel().should(($el) => {
82+
const nextWidth = $el[0].getBoundingClientRect().width;
83+
expect(nextWidth).to.be.greaterThan(initialWidth);
84+
});
85+
});
86+
});

cypress/helpers/editor.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
export const getEditorShadow = () => cy.get("editor-wc").shadow();
44

5-
const getSidebarPanel = () => getEditorShadow().findByTestId("sidebar__panel");
5+
export const getSidebarPanel = () =>
6+
getEditorShadow().findByTestId("sidebar__panel");
67

78
// Buttons / controls
89

@@ -77,6 +78,52 @@ export const getSettingsPanel = () => getEditorShadow().find(".settings-panel");
7778
export const getTextSizeSetting = () =>
7879
getEditorShadow().find(".settings-panel__text-size");
7980

81+
export const getProjEditorContainer = () =>
82+
getEditorShadow().findByTestId("proj-editor-container");
83+
84+
export const getEditorResizeHandle = (handleTestId) =>
85+
getProjEditorContainer().findByTestId(handleTestId).parent("div");
86+
87+
export const getSidebarResizeHandle = () =>
88+
getSidebarPanel().findByTestId("verticalHandle").parent("div");
89+
90+
const dragHandle = ($handle, { deltaX = 0, deltaY = 0 }) => {
91+
cy.wrap($handle)
92+
.realMouseDown({
93+
button: "left",
94+
position: "center",
95+
scrollBehavior: false,
96+
})
97+
.realMouseMove(deltaX, deltaY, {
98+
position: "center",
99+
scrollBehavior: false,
100+
})
101+
.realMouseUp({ position: "center", scrollBehavior: false });
102+
};
103+
104+
export const dragEditorResizeHandle = (
105+
handleTestId,
106+
{ deltaX = 0, deltaY = 0 } = {},
107+
) => {
108+
getEditorResizeHandle(handleTestId).then(($handle) => {
109+
dragHandle($handle, { deltaX, deltaY });
110+
});
111+
};
112+
113+
export const dragSidebarResizeHandle = ({ deltaX = 80 } = {}) => {
114+
getSidebarResizeHandle().then(($handle) => {
115+
dragHandle($handle, { deltaX, deltaY: 0 });
116+
});
117+
};
118+
119+
export const openFilePanel = () =>
120+
getEditorShadow().find("[title='Project files']").click();
121+
122+
export const loadPythonStarterProject = () => {
123+
cy.findByText("blank-python-starter").click();
124+
getEditorShadow().findByRole("button", { name: /run/i }).should("be.visible");
125+
};
126+
80127
// Test output
81128

82129
export const getResults = () => cy.get("#results");

cypress/support/e2e.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
import "@testing-library/cypress/add-commands";
2+
import "cypress-real-events";

package.json

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
"@juggle/resize-observer": "^3.3.1",
1717
"@raspberrypifoundation/design-system-react": "^2.7.0",
1818
"@raspberrypifoundation/python-friendly-error-messages": "0.7.0",
19-
"@react-three/drei": "9.114.3",
20-
"@react-three/fiber": "^8.0.13",
19+
"@react-three/drei": "^10.0.0",
20+
"@react-three/fiber": "^9.6.1",
2121
"@reduxjs/toolkit": "^1.6.2",
2222
"@replit/codemirror-indentation-markers": "^6.1.0",
2323
"@sentry/react": "7.16.0",
@@ -50,24 +50,26 @@
5050
"prismjs": "^1.29.0",
5151
"prop-types": "^15.8.1",
5252
"raw-loader": "^4.0.2",
53-
"re-resizable": "6.9.9",
54-
"react": "^18.1.0",
53+
"re-resizable": "6.11.2",
54+
"react": "^19.2.7",
5555
"react-app-polyfill": "^2.0.0",
5656
"react-confirm-alert": "^2.8.0",
5757
"react-container-query": "^0.13.0",
5858
"react-cookie": "^4.1.1",
59-
"react-dom": "^18.1.0",
59+
"react-dom": "^19.2.7",
6060
"react-i18next": "^12.0.0",
6161
"react-modal": "^3.14.4",
6262
"react-redux": "^8.1.3",
6363
"react-responsive": "^9.0.2",
6464
"react-router-dom": "^6.7.0",
6565
"react-tabs": "^3.2.3",
6666
"react-timer-hook": "^3.0.5",
67-
"react-toastify": "^8.1.0",
67+
"react-toastify": "^11.1.0",
6868
"react-toggle": "^4.1.3",
6969
"redux": "^4.2.1",
7070
"redux-oidc": "^4.0.0-beta1",
71+
"scratchReactDomVendor": "npm:react-dom@18.3.1",
72+
"scratchReactVendor": "npm:react@18.3.1",
7173
"skulpt": "^1.2.0",
7274
"stream-browserify": "^3.0.0",
7375
"three": "0.169.0",
@@ -97,7 +99,7 @@
9799
]
98100
},
99101
"devDependencies": {
100-
"@react-three/test-renderer": "8.2.1",
102+
"@react-three/test-renderer": "^9.0.0",
101103
"@svgr/webpack": "5.5.0",
102104
"@testing-library/cypress": "^10.1.0",
103105
"@testing-library/jest-dom": "^5.16.5",
@@ -114,6 +116,7 @@
114116
"copy-webpack-plugin": "12.0.2",
115117
"css-loader": "4.3.0",
116118
"cypress": "14.5.4",
119+
"cypress-real-events": "^1.15.0",
117120
"dotenv": "8.2.0",
118121
"dotenv-expand": "5.1.0",
119122
"dotenv-webpack": "8.1.0",

src/containers/WebComponentLoader.jsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,6 @@ import {
3939
const CODE_EDITOR_FEEDBACK_URL =
4040
"https://form.raspberrypi.org/f/code-editor-feedback";
4141

42-
const TOAST_CONTAINER_DEFAULTS = {
43-
...(ToastContainer.defaultProps || {}),
44-
};
45-
46-
// react-toastify v8 uses defaultProps on a function component, which React
47-
// warns about in development. We pass the same defaults explicitly instead.
48-
// We should upgrade to version 10 in a different commit, this removes the warning
49-
if (process.env.NODE_ENV === "development") {
50-
ToastContainer.defaultProps = undefined;
51-
}
52-
5342
const WebComponentLoader = (props) => {
5443
const {
5544
assetsIdentifier,
@@ -247,7 +236,6 @@ const WebComponentLoader = (props) => {
247236
}`}
248237
>
249238
<ToastContainer
250-
{...TOAST_CONTAINER_DEFAULTS}
251239
enableMultiContainer
252240
containerId="top-center"
253241
position="top-center"

src/utils/Notifications.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const CloseButton = ({ closeToast }) => {
1818
};
1919

2020
const bottomCenterSettings = {
21-
position: toast.POSITION.BOTTOM_CENTER,
21+
position: "bottom-center",
2222
autoClose: 3000,
2323
className: "toast--bottom-center__message",
2424
closeButton: false,
@@ -27,7 +27,7 @@ const bottomCenterSettings = {
2727
};
2828

2929
const topCenterSettings = {
30-
position: toast.POSITION.TOP_CENTER,
30+
position: "top-center",
3131
autoClose: 6000,
3232
className: "toast--top-center__message",
3333
closeButton: CloseButton,
@@ -39,28 +39,28 @@ export const showSavePrompt = () => {
3939
toast(i18n.t("notifications.savePrompt"), {
4040
...topCenterSettings,
4141
className: `${topCenterSettings.className} toast--info`,
42-
icon: InfoIcon,
42+
icon: <InfoIcon />,
4343
});
4444
};
4545

4646
export const showLoginPrompt = () => {
4747
toast(i18n.t("notifications.loginPrompt"), {
4848
...topCenterSettings,
4949
className: `${topCenterSettings.className} toast--info`,
50-
icon: InfoIcon,
50+
icon: <InfoIcon />,
5151
});
5252
};
5353

5454
export const showSavedMessage = () => {
5555
toast(i18n.t("notifications.projectSaved"), {
5656
...bottomCenterSettings,
57-
icon: TickIcon,
57+
icon: <TickIcon />,
5858
});
5959
};
6060

6161
export const showRenamedMessage = () => {
6262
toast(i18n.t("notifications.projectRenamed"), {
6363
...bottomCenterSettings,
64-
icon: TickIcon,
64+
icon: <TickIcon />,
6565
});
6666
};

webpack.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,11 @@ const scratchConfig = {
303303
{ from: `${scratchStaticDir}/assets`, to: "vendor/static/assets" },
304304
{ from: scratchChunkDir, to: "chunks" },
305305
{
306-
from: "node_modules/react/umd/react.production.min.js",
306+
from: "node_modules/scratchReactVendor/umd/react.production.min.js",
307307
to: "vendor/react.production.min.js",
308308
},
309309
{
310-
from: "node_modules/react-dom/umd/react-dom.production.min.js",
310+
from: "node_modules/scratchReactDomVendor/umd/react-dom.production.min.js",
311311
to: "vendor/react-dom.production.min.js",
312312
},
313313
{

0 commit comments

Comments
 (0)