Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SettingsContext } from "../../../../../utils/settings";
import RunnerControls from "../../../../RunButton/RunnerControls";
import { MOBILE_MEDIA_QUERY } from "../../../../../utils/mediaQueryBreakpoints";
import { getPythonImports } from "../../../../../utils/getPythonImports";
import { configureTurtleGraphics } from "../../../../../utils/configureTurtleGraphics";

const externalLibraries = {
"./pygal/__init__.js": {
Expand Down Expand Up @@ -409,6 +410,18 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
}
dispatch(setSenseHatEnabled(false));

// runCode runs from the current runner's useEffect before VisualOutputPane’s useEffect
// so Sk.TurtleGraphics.target / assets are set here so they exist before import
const host = document.querySelector("editor-wc");
const turtleOutputElement =
host?.shadowRoot?.getElementById("turtleOutput") ||
document.getElementById("turtleOutput");

configureTurtleGraphics({
targetEl: turtleOutputElement,
projectImages: project.image_list || [],
});

var prog = mainComponent?.content || "";

if (prog.includes(`# ${t("input.comment.py5")}`)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1329,3 +1329,59 @@ describe("When not active and a code run has been triggered", () => {
expect(Sk.misceval.asyncToPromise).not.toHaveBeenCalled();
});
});

describe("Turtle graphics is wired before Skulpt import", () => {
const asyncToPromise = Sk.misceval.asyncToPromise;

afterEach(() => {
Sk.misceval.asyncToPromise = asyncToPromise;
});

test("sets Sk.TurtleGraphics.target and assets before asyncToPromise runs", () => {
Sk.misceval.asyncToPromise = jest.fn((_computeFn) => {
const turtleElement = document.getElementById("turtleOutput");
expect(turtleElement).not.toBeNull();
expect(Sk.TurtleGraphics.target).toBe(turtleElement);
expect(Sk.TurtleGraphics.assets["pic.png"]).toBe(
"https://example.com/img.png",
);
return Promise.resolve();
});

const middlewares = [];
const mockStore = configureStore(middlewares);
const initialState = {
editor: {
project: {
components: [
{
name: "main",
extension: "py",
content: "print('hello')",
},
],
image_list: [
{
name: "pic",
extension: "png",
url: "https://example.com/img.png",
},
],
},
codeRunTriggered: true,
isEmbedded: false,
},
auth: {
user,
},
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<SkulptRunner active={true} />
</Provider>,
);

expect(Sk.misceval.asyncToPromise).toHaveBeenCalled();
});
});
13 changes: 5 additions & 8 deletions src/components/Editor/Runners/PythonRunner/VisualOutputPane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useDispatch, useSelector } from "react-redux";
import Sk from "skulpt";
import AstroPiModel from "../../../AstroPiModel/AstroPiModel";
import { codeRunHandled, setError } from "../../../../redux/EditorSlice";
import { configureTurtleGraphics } from "../../../../utils/configureTurtleGraphics";

const VisualOutputPane = () => {
const codeRunTriggered = useSelector(
Expand Down Expand Up @@ -43,14 +44,10 @@ const VisualOutputPane = () => {

(Sk.pygal || (Sk.pygal = {})).outputCanvas = pygalOutput.current;

(Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target =
turtleOutput.current;
Sk.TurtleGraphics.assets = Object.assign(
{},
...projectImages.map((image) => ({
[`${image.name}.${image.extension}`]: image.url,
})),
);
configureTurtleGraphics({
targetEl: turtleOutput.current,
projectImages,
});
}
}, [codeRunTriggered, projectImages]);

Expand Down
18 changes: 18 additions & 0 deletions src/utils/configureTurtleGraphics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Sk from "skulpt";

/**
* Wires Skulpt turtle to a DOM container and project image URLs for stamps.
* Asset keys follow `${name}.${extension}` → url (Skulpt turtle convention).
*/
export function configureTurtleGraphics({ targetEl, projectImages = [] }) {
if (!targetEl) {
return;
}
(Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = targetEl;
Sk.TurtleGraphics.assets = Object.assign(
{},
...projectImages.map((image) => ({
[`${image.name}.${image.extension}`]: image.url,
})),
);
}
24 changes: 24 additions & 0 deletions src/utils/configureTurtleGraphics.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Sk from "skulpt";
import { configureTurtleGraphics } from "./configureTurtleGraphics";

describe("configureTurtleGraphics", () => {
test("sets Sk.TurtleGraphics target and assets map from project images", () => {
const el = document.createElement("div");
configureTurtleGraphics({
targetEl: el,
projectImages: [
{ name: "a", extension: "png", url: "https://x.test/a.png" },
{ name: "b", extension: "gif", url: "https://x.test/b.gif" },
],
});
expect(Sk.TurtleGraphics.target).toBe(el);
expect(Sk.TurtleGraphics.assets["a.png"]).toBe("https://x.test/a.png");
expect(Sk.TurtleGraphics.assets["b.gif"]).toBe("https://x.test/b.gif");
});

test("no-ops when targetEl is null", () => {
expect(() =>
configureTurtleGraphics({ targetEl: null, projectImages: [] }),
).not.toThrow();
});
});
Loading