From aa18526be1b5230eb334af16952ca6d614de6f66 Mon Sep 17 00:00:00 2001 From: manvirsingh01 Date: Sun, 25 Jan 2026 16:20:51 +0530 Subject: [PATCH 1/3] style: format files with Prettier --- js/__tests__/loader.test.js | 65 ++++++------------------------------- 1 file changed, 10 insertions(+), 55 deletions(-) diff --git a/js/__tests__/loader.test.js b/js/__tests__/loader.test.js index 65af3faeb1..0d485ac151 100644 --- a/js/__tests__/loader.test.js +++ b/js/__tests__/loader.test.js @@ -8,9 +8,7 @@ describe("loader.js coverage", () => { beforeEach(() => { jest.resetModules(); - consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { - // Mock empty implementation - }); + consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); document.body.innerHTML = `
Original Title
@@ -22,8 +20,7 @@ describe("loader.js coverage", () => { init: jest.fn(), changeLanguage: jest.fn(), t: jest.fn(key => `TRANSLATED_${key}`), - on: jest.fn(), - isInitialized: false + on: jest.fn() }; mockI18nextHttpBackend = {}; @@ -31,67 +28,41 @@ describe("loader.js coverage", () => { mockRequireJSConfig = jest.fn(); mockRequireJS = jest.fn(); mockRequireJS.config = mockRequireJSConfig; - mockRequireJS.defined = jest.fn(() => false); global.requirejs = mockRequireJS; - global.define = jest.fn(); global.window = document.defaultView; - global.window.createjs = { - Stage: jest.fn(), - Ticker: { framerate: 60, addEventListener: jest.fn() } - }; }); afterEach(() => { - delete global.define; jest.restoreAllMocks(); }); - const loadScript = async ({ initError = false, langError = false } = {}) => { + const loadScript = async ({ initError = false } = {}) => { mockRequireJS.mockImplementation((deps, callback) => { - if (deps.includes("highlight")) { - if (callback) callback(null); - } else if (deps.includes("i18next")) { + if (deps[0] === "i18next") { mockI18next.init.mockImplementation((config, cb) => { if (initError) { cb("Init Failed"); } else { - mockI18next.isInitialized = true; cb(null); } }); - mockI18next.changeLanguage.mockImplementation((lang, cb) => { - if (langError) cb("Lang Change Failed"); - else cb(null); - }); - if (callback) { - callback(mockI18next, mockI18nextHttpBackend); - } - } else if (deps.includes("easeljs.min")) { - // Phase 1 bootstrap - // Mock globals expected by verification - window.createDefaultStack = jest.fn(); - window.Logo = jest.fn(); - window.Blocks = jest.fn(); - window.Turtles = jest.fn(); - if (callback) callback(); - } else if (deps.includes("activity/activity")) { - // Phase 2 bootstrap - if (callback) callback(); + + return callback(mockI18next, mockI18nextHttpBackend); } return null; }); require("../loader.js"); - await new Promise(resolve => setTimeout(resolve, 200)); // More time - }; // Allow async main() to proceed + await new Promise(resolve => process.nextTick(resolve)); + }; test("Configures requirejs correctly", async () => { await loadScript(); expect(mockRequireJSConfig).toHaveBeenCalledWith( expect.objectContaining({ - baseUrl: "./", + baseUrl: "lib", paths: expect.any(Object), shim: expect.any(Object) }) @@ -113,8 +84,6 @@ describe("loader.js coverage", () => { ); expect(window.i18next).toBe(mockI18next); - expect(mockI18next.changeLanguage).toHaveBeenCalledWith("en", expect.any(Function)); - const title = document.querySelector('[data-i18n="title"]'); const label = document.querySelector('[data-i18n="label"]'); @@ -125,12 +94,7 @@ describe("loader.js coverage", () => { expect(mockI18next.on).toHaveBeenCalledWith("languageChanged", expect.any(Function)); - // Verify Phase 2 was reached - expect(mockRequireJS).toHaveBeenCalledWith( - ["activity/activity"], - expect.any(Function), - expect.any(Function) - ); + expect(mockRequireJS).toHaveBeenCalledWith(["utils/utils", "activity/activity"]); }); test("Handles i18next initialization error", async () => { @@ -140,15 +104,6 @@ describe("loader.js coverage", () => { expect(window.i18next).toBe(mockI18next); }); - test("Handles changeLanguage error", async () => { - await loadScript({ langError: true }); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error changing language:", - "Lang Change Failed" - ); - }); - test("Handles DOMContentLoaded when document is loading", async () => { Object.defineProperty(document, "readyState", { value: "loading", From ff97390fdc5a178bca6e62d8e5750d92bea2d191 Mon Sep 17 00:00:00 2001 From: manvirsingh01 Date: Wed, 18 Feb 2026 18:52:35 +0530 Subject: [PATCH 2/3] Hide stop button until playback --- index.html | 4 +++- js/__tests__/toolbar.test.js | 4 +++- js/activity.js | 14 ++++++++++++-- js/toolbar.js | 2 ++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index a65caf2ee8..ade4ea58f6 100644 --- a/index.html +++ b/index.html @@ -333,7 +333,9 @@ class="material-icons main">play_circle_filled
  • - stop +
  • diff --git a/js/__tests__/toolbar.test.js b/js/__tests__/toolbar.test.js index d9c986e945..65d9fde9ec 100644 --- a/js/__tests__/toolbar.test.js +++ b/js/__tests__/toolbar.test.js @@ -283,7 +283,7 @@ describe("Toolbar Class", () => { }); test("renderStopIcon sets onclick and updates stop button behavior", () => { - const stopIcon = { onclick: null, style: { color: "" } }; + const stopIcon = { onclick: null, style: { color: "", display: "" } }; const recordButton = { className: "recording" }; global.docById.mockImplementation(id => @@ -297,6 +297,8 @@ describe("Toolbar Class", () => { const mockOnClick = jest.fn(); toolbar.renderStopIcon(mockOnClick); + expect(stopIcon.style.display).toBe("none"); + stopIcon.onclick(); expect(mockOnClick).toHaveBeenCalled(); diff --git a/js/activity.js b/js/activity.js index 36ec729e76..9bc0890c83 100644 --- a/js/activity.js +++ b/js/activity.js @@ -4599,14 +4599,24 @@ class Activity { * When turtle stops running restore stop button to normal state */ this.onStopTurtle = () => { - // TODO: plugin support + const stopButton = document.getElementById("stop"); + if (stopButton) { + stopButton.style.color = "white"; + stopButton.style.display = "none"; + } }; /* * When turtle starts running change stop button to running state */ this.onRunTurtle = () => { - // TODO: plugin support + const stopButton = document.getElementById("stop"); + if (stopButton) { + stopButton.style.display = "inline-block"; + stopButton.style.color = this.toolbar + ? this.toolbar.stopIconColorWhenPlaying + : window.platformColor.stopIconcolor; + } }; /* diff --git a/js/toolbar.js b/js/toolbar.js index f6ac4a2a06..5a3e502a35 100644 --- a/js/toolbar.js +++ b/js/toolbar.js @@ -448,6 +448,8 @@ class Toolbar { renderStopIcon(onclick) { const stopIcon = docById("stop"); const recordButton = docById("record"); + stopIcon.style.display = "none"; + stopIcon.style.color = "white"; stopIcon.onclick = () => { onclick(this.activity); stopIcon.style.color = "white"; From 94bef7271cd57ab4514b21176162e18949973f7d Mon Sep 17 00:00:00 2001 From: manvirsingh01 Date: Wed, 18 Feb 2026 19:30:18 +0530 Subject: [PATCH 3/3] fix: resolve toTitleCase and doSearch ReferenceErrors - Add local toTitleCase() function in planet/js/GlobalTag.js since it runs inside the Planet iframe without access to the global from js/utils/utils.js. - Remove orphaned doSearch() call from index.html that ran at $(document).ready() time before the Activity object was created. Fixes: toTitleCase is not defined, doSearch is not defined --- .github/workflows/pr-jest-tests.yml | 2 +- js/__tests__/loader.test.js | 65 ++++++++++++++++++++++++----- planet/js/GlobalTag.js | 15 ++++++- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-jest-tests.yml b/.github/workflows/pr-jest-tests.yml index 72e8e61148..f412dc0707 100644 --- a/.github/workflows/pr-jest-tests.yml +++ b/.github/workflows/pr-jest-tests.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22.x' - name: Install Dependencies run: npm install diff --git a/js/__tests__/loader.test.js b/js/__tests__/loader.test.js index 0d485ac151..65af3faeb1 100644 --- a/js/__tests__/loader.test.js +++ b/js/__tests__/loader.test.js @@ -8,7 +8,9 @@ describe("loader.js coverage", () => { beforeEach(() => { jest.resetModules(); - consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { + // Mock empty implementation + }); document.body.innerHTML = `
    Original Title
    @@ -20,7 +22,8 @@ describe("loader.js coverage", () => { init: jest.fn(), changeLanguage: jest.fn(), t: jest.fn(key => `TRANSLATED_${key}`), - on: jest.fn() + on: jest.fn(), + isInitialized: false }; mockI18nextHttpBackend = {}; @@ -28,41 +31,67 @@ describe("loader.js coverage", () => { mockRequireJSConfig = jest.fn(); mockRequireJS = jest.fn(); mockRequireJS.config = mockRequireJSConfig; + mockRequireJS.defined = jest.fn(() => false); global.requirejs = mockRequireJS; + global.define = jest.fn(); global.window = document.defaultView; + global.window.createjs = { + Stage: jest.fn(), + Ticker: { framerate: 60, addEventListener: jest.fn() } + }; }); afterEach(() => { + delete global.define; jest.restoreAllMocks(); }); - const loadScript = async ({ initError = false } = {}) => { + const loadScript = async ({ initError = false, langError = false } = {}) => { mockRequireJS.mockImplementation((deps, callback) => { - if (deps[0] === "i18next") { + if (deps.includes("highlight")) { + if (callback) callback(null); + } else if (deps.includes("i18next")) { mockI18next.init.mockImplementation((config, cb) => { if (initError) { cb("Init Failed"); } else { + mockI18next.isInitialized = true; cb(null); } }); - - return callback(mockI18next, mockI18nextHttpBackend); + mockI18next.changeLanguage.mockImplementation((lang, cb) => { + if (langError) cb("Lang Change Failed"); + else cb(null); + }); + if (callback) { + callback(mockI18next, mockI18nextHttpBackend); + } + } else if (deps.includes("easeljs.min")) { + // Phase 1 bootstrap + // Mock globals expected by verification + window.createDefaultStack = jest.fn(); + window.Logo = jest.fn(); + window.Blocks = jest.fn(); + window.Turtles = jest.fn(); + if (callback) callback(); + } else if (deps.includes("activity/activity")) { + // Phase 2 bootstrap + if (callback) callback(); } return null; }); require("../loader.js"); - await new Promise(resolve => process.nextTick(resolve)); - }; + await new Promise(resolve => setTimeout(resolve, 200)); // More time + }; // Allow async main() to proceed test("Configures requirejs correctly", async () => { await loadScript(); expect(mockRequireJSConfig).toHaveBeenCalledWith( expect.objectContaining({ - baseUrl: "lib", + baseUrl: "./", paths: expect.any(Object), shim: expect.any(Object) }) @@ -84,6 +113,8 @@ describe("loader.js coverage", () => { ); expect(window.i18next).toBe(mockI18next); + expect(mockI18next.changeLanguage).toHaveBeenCalledWith("en", expect.any(Function)); + const title = document.querySelector('[data-i18n="title"]'); const label = document.querySelector('[data-i18n="label"]'); @@ -94,7 +125,12 @@ describe("loader.js coverage", () => { expect(mockI18next.on).toHaveBeenCalledWith("languageChanged", expect.any(Function)); - expect(mockRequireJS).toHaveBeenCalledWith(["utils/utils", "activity/activity"]); + // Verify Phase 2 was reached + expect(mockRequireJS).toHaveBeenCalledWith( + ["activity/activity"], + expect.any(Function), + expect.any(Function) + ); }); test("Handles i18next initialization error", async () => { @@ -104,6 +140,15 @@ describe("loader.js coverage", () => { expect(window.i18next).toBe(mockI18next); }); + test("Handles changeLanguage error", async () => { + await loadScript({ langError: true }); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Error changing language:", + "Lang Change Failed" + ); + }); + test("Handles DOMContentLoaded when document is loading", async () => { Object.defineProperty(document, "readyState", { value: "loading", diff --git a/planet/js/GlobalTag.js b/planet/js/GlobalTag.js index 0b0c749d5a..38d08c65f5 100644 --- a/planet/js/GlobalTag.js +++ b/planet/js/GlobalTag.js @@ -12,13 +12,26 @@ /* global - _, toTitleCase + _ */ /* exported GlobalTag */ + +/** + * Converts the first character of a string to uppercase. + * Defined locally because GlobalTag.js runs inside the Planet iframe + * and does not have access to the toTitleCase exported by js/utils/utils.js. + * @param {string} str - The string to convert. + * @returns {string|undefined} The converted string or undefined if input is not a string. + */ +const toTitleCase = (str) => { + if (typeof str !== "string") return; + return str.charAt(0).toUpperCase() + str.slice(1); +}; + class GlobalTag { /* this.tagNames = [