diff --git a/src/components/Editor/Project/ScratchContainer.jsx b/src/components/Editor/Project/ScratchContainer.jsx index 64117341c..7c5748843 100644 --- a/src/components/Editor/Project/ScratchContainer.jsx +++ b/src/components/Editor/Project/ScratchContainer.jsx @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import { ClickScrollPlugin, OverlayScrollbars } from "overlayscrollbars"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; import { applyScratchProjectIdentifierUpdate } from "../../../redux/EditorSlice"; +import { runStartedEvent } from "../../../events/WebComponentCustomEvents"; import { subscribeToScratchProjectIdentifierUpdates, postMessageToScratchIframe, @@ -66,6 +67,22 @@ export default function ScratchContainer() { }); }, [accessToken, initialAccessToken]); + useEffect(() => { + const allowedOrigin = getScratchAllowedOrigin(); + + const handleScratchRunStarted = (event) => { + if (event.origin !== allowedOrigin) return; + if (event.data?.type !== "scratch-gui-project-run-started") return; + + document.dispatchEvent(runStartedEvent({})); + }; + + window.addEventListener("message", handleScratchRunStarted); + return () => { + window.removeEventListener("message", handleScratchRunStarted); + }; + }, []); + useEffect(() => { const allowedOrigin = getScratchAllowedOrigin(); const authKey = localStorage.getItem("authKey"); diff --git a/src/components/Editor/Project/ScratchContainer.test.js b/src/components/Editor/Project/ScratchContainer.test.js index ea31a3104..77c410abf 100644 --- a/src/components/Editor/Project/ScratchContainer.test.js +++ b/src/components/Editor/Project/ScratchContainer.test.js @@ -170,6 +170,49 @@ describe("ScratchContainer", () => { }); }); + describe("scratch-gui-project-run-started message", () => { + let runStartedHandler; + + beforeEach(() => { + runStartedHandler = jest.fn(); + document.addEventListener("editor-runStarted", runStartedHandler); + }); + + afterEach(() => { + document.removeEventListener("editor-runStarted", runStartedHandler); + }); + + test("dispatches editor-runStarted when scratch-gui-project-run-started is received", () => { + renderScratchContainer(); + + act(() => { + dispatchMessage({ + type: "scratch-gui-project-run-started", + }); + }); + + expect(runStartedHandler).toHaveBeenCalledTimes(1); + expect(runStartedHandler.mock.calls[0][0].detail).toEqual({}); + }); + + test("does not dispatch editor-runStarted after unmount", () => { + const store = buildStore(); + const { unmount } = render( + + + , + ); + + unmount(); + + act(() => { + dispatchMessage({ type: "scratch-gui-project-run-started" }); + }); + + expect(runStartedHandler).not.toHaveBeenCalled(); + }); + }); + test("updates the parent project identifier without reloading the iframe project_id", () => { const { store } = renderScratchContainer(); diff --git a/src/components/ScratchEditor/ScratchIntegrationHOC.jsx b/src/components/ScratchEditor/ScratchIntegrationHOC.jsx index a6cb74c51..f28a1dca4 100644 --- a/src/components/ScratchEditor/ScratchIntegrationHOC.jsx +++ b/src/components/ScratchEditor/ScratchIntegrationHOC.jsx @@ -21,10 +21,12 @@ const ScratchIntegrationHOC = function (WrappedComponent) { this.handleRemix = this.handleRemix.bind(this); this.handleSave = this.handleSave.bind(this); this.handleProjectChanged = this.handleProjectChanged.bind(this); + this.handleProjectRunStart = this.handleProjectRunStart.bind(this); } componentDidMount() { window.addEventListener("message", this.handleMessage); this.props.vm.on("PROJECT_CHANGED", this.handleProjectChanged); + this.props.vm.on("PROJECT_RUN_START", this.handleProjectRunStart); this.props.setStageSize(); } componentWillUnmount() { @@ -33,6 +35,10 @@ const ScratchIntegrationHOC = function (WrappedComponent) { "PROJECT_CHANGED", this.handleProjectChanged, ); + this.props.vm.removeListener( + "PROJECT_RUN_START", + this.handleProjectRunStart, + ); } handleMessage(event) { @@ -90,6 +96,9 @@ const ScratchIntegrationHOC = function (WrappedComponent) { handleProjectChanged() { postScratchGuiEvent("scratch-gui-project-changed"); } + handleProjectRunStart() { + postScratchGuiEvent("scratch-gui-project-run-started"); + } render() { const { loadProject, diff --git a/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx b/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx index d1f5072ff..a2a292982 100644 --- a/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx +++ b/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx @@ -137,4 +137,43 @@ describe("ScratchIntegrationHOC", () => { }); }); }); + + describe("Scratch VM run events", () => { + it("registers a PROJECT_RUN_START listener on mount", () => { + render( + React.createElement(Provider, { store }, React.createElement(Wrapped)), + ); + + expect(mockVm.on).toHaveBeenCalledWith( + "PROJECT_RUN_START", + expect.any(Function), + ); + }); + + it("removes the PROJECT_RUN_START listener on unmount", () => { + const { unmount } = render( + React.createElement(Provider, { store }, React.createElement(Wrapped)), + ); + const handler = getVmHandler("PROJECT_RUN_START"); + + unmount(); + + expect(mockVm.removeListener).toHaveBeenCalledWith( + "PROJECT_RUN_START", + handler, + ); + }); + + it("posts a project-run-started event when PROJECT_RUN_START fires", () => { + render( + React.createElement(Provider, { store }, React.createElement(Wrapped)), + ); + + getVmHandler("PROJECT_RUN_START")(); + + expect(postScratchGuiEvent).toHaveBeenCalledWith( + "scratch-gui-project-run-started", + ); + }); + }); });