From c53fcc5baed3f0d9f480b1f155904bbbdae635e2 Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Fri, 13 Mar 2026 15:48:59 +0100 Subject: [PATCH 1/8] test: upload test --- cypress/e2e/spec-scratch.cy.js | 50 +++++++++++++++++++++++ cypress/fixtures/upload-test-project.sb3 | Bin 0 -> 2417 bytes 2 files changed, 50 insertions(+) create mode 100644 cypress/fixtures/upload-test-project.sb3 diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index e17a8714c..d7993cecc 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -18,6 +18,25 @@ const getIframeBody = () => { .then(cy.wrap); }; +// Consider +const openSaveAndDownloadPanel = () => { + cy.get("editor-wc").shadow().find("[title='Download project']").click(); + cy.get("editor-wc") + .shadow() + .find(".sidebar__panel-heading") + .contains("Save & download") + .should("be.visible"); + + return { + uploadProject: (fixturePath) => { + cy.get("editor-wc") + .shadow() + .find("[data-testid='upload-file-input']") + .selectFile(fixturePath, { force: true }); + }, + }; +}; + describe("Scratch", () => { beforeEach(() => { cy.visit(origin); @@ -37,4 +56,35 @@ describe("Scratch", () => { .should("exist") .and("not.be.visible"); }); + + it("uploads project and shows upload in Scratch iframe", () => { + getIframeBody().find("button [title='Go']").should("be.visible"); + + // confirm set up is different to loaded project + getIframeBody() + .find("[role='button']") + .contains("teapot") + .should("be.visible"); + + getIframeBody() + .find("[role='button']") + .contains("test sprite") + .should("not.exist"); + + const saveAndDownloadPanel = openSaveAndDownloadPanel(); + saveAndDownloadPanel.uploadProject( + "cypress/fixtures/upload-test-project.sb3" + ); + + // confirm project has been uploaded + getIframeBody() + .find("[role='button']") + .contains("test sprite") + .should("be.visible"); + + getIframeBody() + .find("[role='button']") + .contains("teapot") + .should("not.exist"); + }); }); diff --git a/cypress/fixtures/upload-test-project.sb3 b/cypress/fixtures/upload-test-project.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..5d59a729516e3f7b847ccca9ae9588e6b248c0d2 GIT binary patch literal 2417 zcma)8do&aLA0A_dk&F=0RIZg-Cc~CXLRjQh+H&27xizwVwO`JMBAKj(A4=kq=1dCqyx`99D2SX%)3ga7~l7@&B`7dH!G znojZo0AV5k0A%Mifa32>a1T1_9q8{@;YfBI)0OzCPAesUv8=H2y3$C*?>4lNejlql zPJ!3*sQC_1OZKaA>pwq;iOpK6nu2=eGBH~VkztnskUn`LOa+GqC~L{gxmvquy+ACd zF_|v9%5ktl(Yc1Zm63qs^^knl#<;+x42ea40g5KLD_1kkK; z>#a%qPT%$|m?mYPi`!Tps2q$dx-eMN@@c*=O@BBh*tk2rRq;tod=i_qXrr)&ms&f_ zssgd6LN_*UuCQ2ZLsIFqlLA{U+e;%oUmq8!jexGL`sRlqu6s@@&W6a6?_s;q(a{b^mIk{jKSm#OWXu9}B*{_;ILF8qji}6iM&O3l`H-6v-geQH4wru=x82c4E8g|w zf)DuSaXd41G_Y0NPo;LOU7?o_v81P;`CoQPxM5HOJIJ1@WxphAB$K_1vWM;%7DzP0 z@r|FI6maDxgFV=$p)%D6t zq{XNfJX>c-TAhK!tqw+Qj+W!JAHMBxtV)%78^)1Cs5H;@IDVyySKlb=%C1BjYrL`G zIroy3(cqf`Ol7O(-NJ?}VPE*Q`}}NXp{shYQ;UCyvx#+>q};Tt25P{XI+FJFVs}hM zXJ^@80s6@X80!S+=0>*Z!njW6{|6g*e9DWWi(!^#(qTn!PFi67(a*5yWT_?5r=RpZe; zSGii#V7~^P!@hr?qPuE2GfHe|agY{isz_oEwE0FJ3tVoP>6YAkUM&2;D;n+y)W0rdA&3zT?FZ#MEVd` zz3IS;2>JO<2yP)_#HqRZo4Gn%7-k3a&sjxTvBdYntZb`M;DJQfWn(1r=h72!N{MpN z*vPN4v9e1ow9V1cI$6@=BShKPpUFN@l48kcW!EEz3mpEL+;J9{u_g5tGcb!5%2g z0oQ2LM-(Gm@)q&8m`dtsPPgfeQ@Z12p@78P(tjb~($VGZ=JExAjAe z&*@^!A^DNSun4^qoRj#GmYyduofFa4|BAzFZ^d}hcVe(>rw{J5y9YuOsj1}wM;_B8 zdJu_j2m}JIMSv6BkpvGNE!U%gAtYZb3(>g!62svsta^c$bgdVK=4akPYfs19b+kTy z$6+~TTM;#L{AF1@4$#7HHAbC>j40^y36il$KSu4qU{27=d00B5AlB;5fR1TGmZw*| znd(Z=6b4eL_U5clyXk|@BI^BLvDI{PRdcCu%=$}d+_y1cj=a2oL+}?YjUQxse47u0 z8Y_twbg`6LP$@$-+bX-v$t{y}!0>M>VQwN10xND=}I&PZo zC=@{prLFl})D1h19|sPJ{Okzc3-^gBHhYi0zQvdpVQVxbi-!jYkE? zTv&6lnMivbXb?7dKV(9p`I$HM-sz*NjSKT7yY1B}Z$7Dgl@&j;aG?ha71lgyewt?$-I-C!1!M+UocgMGAp zKKqT`(NeLnIa8aqQD8Rd0j=(wJ$^gSIKY9_FCbse_3Mcd7FL4Bd-=sul-v^qD(bF$ z5gb-8{_d3DYAXhU#@1$eVq8j~G+R-jLI=*J7I6YzjxtooBpP>)Ij^~8zU50dpU#^< z(nc9i;F3FOK-a3}$NV(}EXZPwFGt0zxc8t~BDyf)gZQpVB!hT!+&y1l`&i6uwt%3r zwmws$?N&}Y{M&aToy*MdE5q3GG23Jp0%u)(aE=#kS*=GXA~~v66(}X-m*kNLx$2iK zx>$4Gy>&8)r@}_rvTP9{w2_(}Fx Date: Fri, 13 Mar 2026 16:15:22 +0100 Subject: [PATCH 2/8] test: refactor methods and create helpers --- cypress/e2e/spec-scratch.cy.js | 71 +++++++++------------------------- cypress/helpers/editor.js | 16 ++++++++ cypress/helpers/scratch.js | 9 +++++ 3 files changed, 44 insertions(+), 52 deletions(-) create mode 100644 cypress/helpers/editor.js create mode 100644 cypress/helpers/scratch.js diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index d7993cecc..a66372158 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -1,3 +1,9 @@ +import { + getEditorShadow, + openSaveAndDownloadPanel, +} from "../helpers/editor.js"; +import { getScratchIframeBody } from "../helpers/scratch.js"; + const origin = "http://localhost:3011/web-component.html"; beforeEach(() => { @@ -8,35 +14,6 @@ beforeEach(() => { cy.viewport(1400, 800); }); -const getIframeBody = () => { - return cy - .get("editor-wc") - .shadow() - .findByTitle("Scratch") - .its("0.contentDocument.body") - .should("not.be.empty") - .then(cy.wrap); -}; - -// Consider -const openSaveAndDownloadPanel = () => { - cy.get("editor-wc").shadow().find("[title='Download project']").click(); - cy.get("editor-wc") - .shadow() - .find(".sidebar__panel-heading") - .contains("Save & download") - .should("be.visible"); - - return { - uploadProject: (fixturePath) => { - cy.get("editor-wc") - .shadow() - .find("[data-testid='upload-file-input']") - .selectFile(fixturePath, { force: true }); - }, - }; -}; - describe("Scratch", () => { beforeEach(() => { cy.visit(origin); @@ -44,47 +21,37 @@ describe("Scratch", () => { }); it("loads Scratch in an iframe", () => { - getIframeBody().find("button [title='Go']").should("be.visible"); + getScratchIframeBody().findByTitle("Go").should("be.visible"); }); it("hides text size in settings for Scratch", () => { - getIframeBody().find("button [title='Go']").should("be.visible"); - cy.get("editor-wc").shadow().find("[title='Settings']").first().click(); - cy.get("editor-wc") - .shadow() + getScratchIframeBody().findByTitle("Go").should("be.visible"); + getEditorShadow() + .findByRole("button", { name: "Settings" }) + .first() + .click(); + getEditorShadow() .find(".settings-panel__text-size") .should("exist") .and("not.be.visible"); }); it("uploads project and shows upload in Scratch iframe", () => { - getIframeBody().find("button [title='Go']").should("be.visible"); + getScratchIframeBody().findByTitle("Go").should("be.visible"); // confirm set up is different to loaded project - getIframeBody() - .find("[role='button']") - .contains("teapot") - .should("be.visible"); - - getIframeBody() - .find("[role='button']") - .contains("test sprite") + getScratchIframeBody() + .findByRole("button", { name: "test sprite" }) .should("not.exist"); const saveAndDownloadPanel = openSaveAndDownloadPanel(); saveAndDownloadPanel.uploadProject( - "cypress/fixtures/upload-test-project.sb3" + "cypress/fixtures/upload-test-project.sb3", ); // confirm project has been uploaded - getIframeBody() - .find("[role='button']") - .contains("test sprite") + getScratchIframeBody() + .findByRole("button", { name: "test sprite" }) .should("be.visible"); - - getIframeBody() - .find("[role='button']") - .contains("teapot") - .should("not.exist"); }); }); diff --git a/cypress/helpers/editor.js b/cypress/helpers/editor.js new file mode 100644 index 000000000..727e1db50 --- /dev/null +++ b/cypress/helpers/editor.js @@ -0,0 +1,16 @@ +export const getEditorShadow = () => cy.get("editor-wc").shadow(); + +export const openSaveAndDownloadPanel = () => { + getEditorShadow().findByRole("button", { name: "Download project" }).click(); + getEditorShadow() + .findByRole("heading", { name: "Save & download" }) + .should("be.visible"); + + return { + uploadProject: (fixturePath) => { + getEditorShadow() + .findByTestId("upload-file-input") + .selectFile(fixturePath, { force: true }); + }, + }; +}; diff --git a/cypress/helpers/scratch.js b/cypress/helpers/scratch.js new file mode 100644 index 000000000..d53dc7438 --- /dev/null +++ b/cypress/helpers/scratch.js @@ -0,0 +1,9 @@ +import { getEditorShadow } from "./editor.js"; + +export const getScratchIframeBody = () => { + return getEditorShadow() + .findByTitle("Scratch") + .its("0.contentDocument.body") + .should("not.be.empty") + .then(cy.wrap); +}; From 990d6c257358b9569ae60727ceb81473baac7f7d Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Fri, 13 Mar 2026 17:14:51 +0100 Subject: [PATCH 3/8] test: add in download checks --- .gitignore | 1 + cypress.config.mjs | 49 ++++++++++++++++++++++++++++++++++ cypress/e2e/spec-scratch.cy.js | 24 ++++++++++++++++- cypress/helpers/editor.js | 6 +++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index efe2593f4..56ae9ffea 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules # testing /coverage +/cypress/downloads /cypress/screenshots /cypress/videos diff --git a/cypress.config.mjs b/cypress.config.mjs index bbbcf5b57..228d61a19 100644 --- a/cypress.config.mjs +++ b/cypress.config.mjs @@ -1,8 +1,13 @@ import { defineConfig } from "cypress"; import dotenv from "dotenv"; +import fs from "fs"; +import path from "path"; +import JSZip from "jszip"; dotenv.config(); +const downloadsFolder = path.join(process.cwd(), "cypress", "downloads"); + export default defineConfig({ e2e: { chromeWebSecurity: false, @@ -10,6 +15,7 @@ export default defineConfig({ video: false, defaultBrowser: "chrome", testIsolation: true, + downloadsFolder: "cypress/downloads", setupNodeEvents(on, config) { on("task", { log(message) { @@ -17,6 +23,45 @@ export default defineConfig({ return null; }, + clearDownloads() { + if (fs.existsSync(downloadsFolder)) { + const files = fs.readdirSync(downloadsFolder); + for (const file of files) { + fs.unlinkSync(path.join(downloadsFolder, file)); + } + } else { + fs.mkdirSync(downloadsFolder, { recursive: true }); + } + + return null; + }, + getNewestSb3() { + if (!fs.existsSync(downloadsFolder)) { + return null; + } + const files = fs.readdirSync(downloadsFolder); + const sb3Files = files + .filter((f) => f.endsWith(".sb3")) + .map((f) => ({ + name: f, + path: path.join(downloadsFolder, f), + mtime: fs.statSync(path.join(downloadsFolder, f)).mtimeMs, + })) + .sort((a, b) => b.mtime - a.mtime); + + return sb3Files.length > 0 ? sb3Files[0].path : null; + }, + async readSb3(filePath) { + const buf = fs.readFileSync(filePath); + const zip = await JSZip.loadAsync(buf); + const fileNames = Object.keys(zip.files); + const projectJsonFile = zip.file("project.json"); + const projectJson = projectJsonFile + ? JSON.parse(await projectJsonFile.async("string")) + : null; + + return { fileNames, projectJson }; + }, }); on("before:browser:launch", (browser = {}, launchOptions) => { if (browser.name === "chrome") { @@ -24,6 +69,10 @@ export default defineConfig({ launchOptions.args.push("--enable-features=SharedArrayBuffer"); launchOptions.args.push("--disable-site-isolation-trials"); launchOptions.args.push("--enable-unsafe-swiftshader"); + launchOptions.preferences = { + ...launchOptions.preferences, + "download.default_directory": downloadsFolder, + }; } return launchOptions; }); diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index a66372158..10aa68a8a 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -46,12 +46,34 @@ describe("Scratch", () => { const saveAndDownloadPanel = openSaveAndDownloadPanel(); saveAndDownloadPanel.uploadProject( - "cypress/fixtures/upload-test-project.sb3", + "cypress/fixtures/upload-test-project.sb3" ); // confirm project has been uploaded getScratchIframeBody() .findByRole("button", { name: "test sprite" }) .should("be.visible"); + + cy.task("clearDownloads"); + + // download project + saveAndDownloadPanel.downloadProject(); + + cy.wait(1000); + + // assert on the file + cy.task("getNewestSb3").then((filePath) => { + expect(filePath).to.be.a("string"); + expect(filePath).to.match(/\.sb3$/); + cy.task("readSb3", filePath).then(({ fileNames, projectJson }) => { + expect(fileNames).to.include("project.json"); + expect(projectJson).to.be.an("object"); + expect(projectJson.targets).to.be.an("array"); + const spriteNames = projectJson.targets + .filter((t) => t.isStage === false) + .map((t) => t.name); + expect(spriteNames).to.include("test sprite"); + }); + }); }); }); diff --git a/cypress/helpers/editor.js b/cypress/helpers/editor.js index 727e1db50..e8951f415 100644 --- a/cypress/helpers/editor.js +++ b/cypress/helpers/editor.js @@ -12,5 +12,11 @@ export const openSaveAndDownloadPanel = () => { .findByTestId("upload-file-input") .selectFile(fixturePath, { force: true }); }, + downloadProject: () => { + getEditorShadow() + .find(".download-panel__download-section") + .findByRole("button", { name: "Download project" }) + .click(); + }, }; }; From 89e0b830330dfb5c582c00b3fc3766f3a12d9f32 Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Fri, 13 Mar 2026 17:27:35 +0100 Subject: [PATCH 4/8] refactor: clean up a little --- cypress.config.mjs | 9 ++++++--- cypress/e2e/spec-scratch.cy.js | 26 +++++++++++++++----------- cypress/helpers/editor.js | 4 ++++ cypress/helpers/scratch.js | 7 +++++++ 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/cypress.config.mjs b/cypress.config.mjs index 228d61a19..06238eefd 100644 --- a/cypress.config.mjs +++ b/cypress.config.mjs @@ -23,7 +23,7 @@ export default defineConfig({ return null; }, - clearDownloads() { + resetDownloads() { if (fs.existsSync(downloadsFolder)) { const files = fs.readdirSync(downloadsFolder); for (const file of files) { @@ -37,7 +37,7 @@ export default defineConfig({ }, getNewestSb3() { if (!fs.existsSync(downloadsFolder)) { - return null; + throw new Error("Downloads folder not found"); } const files = fs.readdirSync(downloadsFolder); const sb3Files = files @@ -49,7 +49,10 @@ export default defineConfig({ })) .sort((a, b) => b.mtime - a.mtime); - return sb3Files.length > 0 ? sb3Files[0].path : null; + if (sb3Files.length === 0) { + throw new Error("No .sb3 file found in downloads folder"); + } + return sb3Files[0].path; }, async readSb3(filePath) { const buf = fs.readFileSync(filePath); diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index 10aa68a8a..a870bcc89 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -2,7 +2,10 @@ import { getEditorShadow, openSaveAndDownloadPanel, } from "../helpers/editor.js"; -import { getScratchIframeBody } from "../helpers/scratch.js"; +import { + assertScratchIsRendered, + getScratchIframeBody, +} from "../helpers/scratch.js"; const origin = "http://localhost:3011/web-component.html"; @@ -21,11 +24,12 @@ describe("Scratch", () => { }); it("loads Scratch in an iframe", () => { - getScratchIframeBody().findByTitle("Go").should("be.visible"); + assertScratchIsRendered(); }); it("hides text size in settings for Scratch", () => { - getScratchIframeBody().findByTitle("Go").should("be.visible"); + assertScratchIsRendered(); + getEditorShadow() .findByRole("button", { name: "Settings" }) .first() @@ -36,17 +40,17 @@ describe("Scratch", () => { .and("not.be.visible"); }); - it("uploads project and shows upload in Scratch iframe", () => { - getScratchIframeBody().findByTitle("Go").should("be.visible"); + it("can perform uploads and downloads of Scratch projects via the save and download panel", () => { + assertScratchIsRendered(); - // confirm set up is different to loaded project + // confirm set up is different to loaded project and does not contain a sprite with this name getScratchIframeBody() .findByRole("button", { name: "test sprite" }) .should("not.exist"); const saveAndDownloadPanel = openSaveAndDownloadPanel(); saveAndDownloadPanel.uploadProject( - "cypress/fixtures/upload-test-project.sb3" + "cypress/fixtures/upload-test-project.sb3", ); // confirm project has been uploaded @@ -54,9 +58,8 @@ describe("Scratch", () => { .findByRole("button", { name: "test sprite" }) .should("be.visible"); - cy.task("clearDownloads"); + cy.task("resetDownloads"); - // download project saveAndDownloadPanel.downloadProject(); cy.wait(1000); @@ -65,13 +68,14 @@ describe("Scratch", () => { cy.task("getNewestSb3").then((filePath) => { expect(filePath).to.be.a("string"); expect(filePath).to.match(/\.sb3$/); + cy.task("readSb3", filePath).then(({ fileNames, projectJson }) => { expect(fileNames).to.include("project.json"); - expect(projectJson).to.be.an("object"); - expect(projectJson.targets).to.be.an("array"); + const spriteNames = projectJson.targets .filter((t) => t.isStage === false) .map((t) => t.name); + expect(spriteNames).to.include("test sprite"); }); }); diff --git a/cypress/helpers/editor.js b/cypress/helpers/editor.js index e8951f415..7a872d611 100644 --- a/cypress/helpers/editor.js +++ b/cypress/helpers/editor.js @@ -8,6 +8,10 @@ export const openSaveAndDownloadPanel = () => { return { uploadProject: (fixturePath) => { + getEditorShadow() + .find(".download-panel__download-section") + .findByRole("button", { name: "Upload project" }) + .should("be.visible"); getEditorShadow() .findByTestId("upload-file-input") .selectFile(fixturePath, { force: true }); diff --git a/cypress/helpers/scratch.js b/cypress/helpers/scratch.js index d53dc7438..a1e33c736 100644 --- a/cypress/helpers/scratch.js +++ b/cypress/helpers/scratch.js @@ -7,3 +7,10 @@ export const getScratchIframeBody = () => { .should("not.be.empty") .then(cy.wrap); }; + +export const getScratchGoButton = () => + getScratchIframeBody().find("button [title='Go']"); + +export const assertScratchIsRendered = () => { + getScratchGoButton().should("be.visible"); +}; From 94a50fa5c4f18835deb8c7dd8c40642af283c73a Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Fri, 13 Mar 2026 19:25:28 +0100 Subject: [PATCH 5/8] chore: amendments - add polling --- cypress.config.mjs | 58 ++++++++++++++++++++++------------ cypress/e2e/spec-scratch.cy.js | 2 -- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/cypress.config.mjs b/cypress.config.mjs index 06238eefd..f617cdd75 100644 --- a/cypress.config.mjs +++ b/cypress.config.mjs @@ -6,8 +6,6 @@ import JSZip from "jszip"; dotenv.config(); -const downloadsFolder = path.join(process.cwd(), "cypress", "downloads"); - export default defineConfig({ e2e: { chromeWebSecurity: false, @@ -17,6 +15,11 @@ export default defineConfig({ testIsolation: true, downloadsFolder: "cypress/downloads", setupNodeEvents(on, config) { + const projectRoot = config.projectRoot ?? process.cwd(); + const downloadsFolder = path.resolve( + projectRoot, + config.downloadsFolder || "cypress/downloads", + ); on("task", { log(message) { console.log(message); @@ -35,33 +38,48 @@ export default defineConfig({ return null; }, - getNewestSb3() { - if (!fs.existsSync(downloadsFolder)) { - throw new Error("Downloads folder not found"); + async getNewestSb3() { + const pollMs = 100; + const timeoutMs = 1500; + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + if (!fs.existsSync(downloadsFolder)) { + await new Promise((r) => setTimeout(r, pollMs)); + continue; + } + const files = fs.readdirSync(downloadsFolder); + const sb3Files = files + .filter((f) => f.endsWith(".sb3")) + .map((f) => ({ + name: f, + path: path.join(downloadsFolder, f), + mtime: fs.statSync(path.join(downloadsFolder, f)).mtimeMs, + })) + .sort((a, b) => b.mtime - a.mtime); + + if (sb3Files.length > 0) { + return sb3Files[0].path; + } + await new Promise((r) => setTimeout(r, pollMs)); } - const files = fs.readdirSync(downloadsFolder); - const sb3Files = files - .filter((f) => f.endsWith(".sb3")) - .map((f) => ({ - name: f, - path: path.join(downloadsFolder, f), - mtime: fs.statSync(path.join(downloadsFolder, f)).mtimeMs, - })) - .sort((a, b) => b.mtime - a.mtime); - if (sb3Files.length === 0) { - throw new Error("No .sb3 file found in downloads folder"); + if (!fs.existsSync(downloadsFolder)) { + throw new Error("Downloads folder not found"); } - return sb3Files[0].path; + throw new Error("No .sb3 file found in downloads folder"); }, async readSb3(filePath) { const buf = fs.readFileSync(filePath); const zip = await JSZip.loadAsync(buf); const fileNames = Object.keys(zip.files); const projectJsonFile = zip.file("project.json"); - const projectJson = projectJsonFile - ? JSON.parse(await projectJsonFile.async("string")) - : null; + + if (!projectJsonFile) { + throw new Error("Invalid .sb3 file: missing project.json"); + } + + const projectJson = JSON.parse(await projectJsonFile.async("string")); return { fileNames, projectJson }; }, diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index a870bcc89..4c8150006 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -62,8 +62,6 @@ describe("Scratch", () => { saveAndDownloadPanel.downloadProject(); - cy.wait(1000); - // assert on the file cy.task("getNewestSb3").then((filePath) => { expect(filePath).to.be.a("string"); From 64b74434ba2062ac6b8f8713118835ef47521064 Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Mon, 16 Mar 2026 18:48:04 +0100 Subject: [PATCH 6/8] test: add in the iframe env variable --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5c70a8ad1..94f329074 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -114,6 +114,7 @@ jobs: PUBLIC_URL: "http://localhost:3011" ASSETS_URL: "http://localhost:3011" REACT_APP_PLAUSIBLE_SOURCE: "" + REACT_APP_ALLOWED_IFRAME_ORIGINS: "http://localhost:3011" - name: Archive cypress artifacts uses: actions/upload-artifact@v4.6.0 From 9bd1f230e8a6ccaede6f310b56a2d23072b7f63e Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Mon, 16 Mar 2026 19:26:59 +0100 Subject: [PATCH 7/8] chore: make task reusable for other download checks --- cypress.config.mjs | 18 ++++++++++++------ cypress/e2e/spec-scratch.cy.js | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cypress.config.mjs b/cypress.config.mjs index f617cdd75..f554a5669 100644 --- a/cypress.config.mjs +++ b/cypress.config.mjs @@ -38,7 +38,13 @@ export default defineConfig({ return null; }, - async getNewestSb3() { + async getNewestDownload(extension) { + if (!extension || !extension.startsWith(".")) { + throw new Error( + "getNewestDownload requires a file extension starting with . (e.g. .sb3)", + ); + } + const ext = extension; const pollMs = 100; const timeoutMs = 1500; const deadline = Date.now() + timeoutMs; @@ -49,8 +55,8 @@ export default defineConfig({ continue; } const files = fs.readdirSync(downloadsFolder); - const sb3Files = files - .filter((f) => f.endsWith(".sb3")) + const matching = files + .filter((f) => f.endsWith(ext)) .map((f) => ({ name: f, path: path.join(downloadsFolder, f), @@ -58,8 +64,8 @@ export default defineConfig({ })) .sort((a, b) => b.mtime - a.mtime); - if (sb3Files.length > 0) { - return sb3Files[0].path; + if (matching.length > 0) { + return matching[0].path; } await new Promise((r) => setTimeout(r, pollMs)); } @@ -67,7 +73,7 @@ export default defineConfig({ if (!fs.existsSync(downloadsFolder)) { throw new Error("Downloads folder not found"); } - throw new Error("No .sb3 file found in downloads folder"); + throw new Error(`No ${ext} file found in downloads folder`); }, async readSb3(filePath) { const buf = fs.readFileSync(filePath); diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index 4c8150006..1f8ce986c 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -63,7 +63,7 @@ describe("Scratch", () => { saveAndDownloadPanel.downloadProject(); // assert on the file - cy.task("getNewestSb3").then((filePath) => { + cy.task("getNewestDownload", ".sb3").then((filePath) => { expect(filePath).to.be.a("string"); expect(filePath).to.match(/\.sb3$/); From adcad37a4eadfd176e1450922a32c9eb8f0d9be3 Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Tue, 17 Mar 2026 10:25:03 +0100 Subject: [PATCH 8/8] chore: remove unneeded test --- cypress/e2e/spec-scratch.cy.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index 1f8ce986c..4a6933c40 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -23,10 +23,6 @@ describe("Scratch", () => { cy.findByText("cool-scratch").click(); }); - it("loads Scratch in an iframe", () => { - assertScratchIsRendered(); - }); - it("hides text size in settings for Scratch", () => { assertScratchIsRendered();