Skip to content
Closed
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
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
13 changes: 7 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,18 @@ Follow [AI guidelines for Sugar Labs](https://github.com/sugarlabs/sugar-docs/bl

### Before You Push

Run these commands locally before submitting a PR:
Code is automatically formatted via pre-commit hooks. When you commit, ESLint
and Prettier will automatically run on staged files to fix any style issues.

If you need to run linting manually:

```bash
npm run lint # ESLint
npx prettier --check . # Formatting
npm run lint # ESLint check
npm run lint:fix # ESLint fix + Prettier format
npm test # Jest
```

NOTE: Only run ```prettier``` on the files you have modified.

If formatting fails, run `npx prettier --write .` to fix it.
You can bypass the pre-commit hook with `git commit --no-verify` if needed.

### After your PR is merged

Expand Down
87 changes: 68 additions & 19 deletions js/__tests__/logo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ global.doStopVideoCam = jest.fn();
global.CAMERAVALUE = "camera:";
global.VIDEOVALUE = "video:";
global.doUseCamera = jest.fn();
global.delayExecution = jest.fn((ms, callback) => callback());
global.delayExecution = jest.fn((ms, callback) => {
if (typeof callback === "function") {
callback();
}
});
global.getStatsFromNotation = jest.fn();
global.Tone = {
UserMedia: jest.fn().mockImplementation(() => ({
Expand Down Expand Up @@ -179,6 +183,13 @@ describe("Logo Class", () => {
global.instrumentsFilters = { 0: {} };
global.instrumentsEffects = { 0: {} };

// Mock Tone.UserMedia for initMediaDevices tests
global.Tone = {
UserMedia: jest.fn(() => ({
open: jest.fn()
}))
};

mockActivity = {
blocks: {
blockList: [],
Expand Down Expand Up @@ -230,7 +241,8 @@ describe("Logo Class", () => {
},
stage: {
removeEventListener: jest.fn(),
addEventListener: jest.fn()
addEventListener: jest.fn(),
update: jest.fn()
},
onStopTurtle: jest.fn(),
onRunTurtle: jest.fn(),
Expand Down Expand Up @@ -708,7 +720,8 @@ describe("Logo comprehensive method coverage", () => {
stage: {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
dispatchEvent: jest.fn(),
update: jest.fn()
},
errorMsg: jest.fn(),
textMsg: jest.fn(),
Expand Down Expand Up @@ -913,27 +926,27 @@ describe("Logo comprehensive method coverage", () => {
});

test("initMediaDevices sets mic/limit on success and reports microphone errors", () => {
const originalTone = global.Tone;
const open = jest.fn();
global.Tone = { UserMedia: jest.fn(() => ({ open })) };
// Update logo.deps.Tone since it was captured at constructor time
logo.deps.Tone = { UserMedia: jest.fn(() => ({ open })) };

logo.initMediaDevices();
expect(open).toHaveBeenCalled();
expect(logo.limit).toBe(16384);
expect(logo.mic).toBeTruthy();

global.Tone = {
// Test error case - mock open to throw
const errorOpen = jest.fn(() => {
throw new Error("no mic");
});
logo.deps.Tone = {
UserMedia: jest.fn(() => ({
open: jest.fn(() => {
throw new Error("no mic");
})
open: errorOpen
}))
};
logo.initMediaDevices();
expect(mockActivity.errorMsg).toHaveBeenCalledWith("The microphone is not available.");
expect(logo.mic).toBeNull();

global.Tone = originalTone;
});

test("processShow handles image/url/loadFile/default branches", () => {
Expand All @@ -956,23 +969,28 @@ describe("Logo comprehensive method coverage", () => {
fn();
return 2;
});
global.delayExecution = jest.fn(() => Promise.resolve());
// Update logo.deps.utils.delayExecution since it was captured at constructor time
logo.deps.utils.delayExecution = jest.fn((ms, callback) => {
if (typeof callback === "function") {
callback();
}
});

turtle0.singer.embeddedGraphics = {};
await logo.dispatchTurtleSignals(0, 0.5, 3, 0);
expect(global.delayExecution).not.toHaveBeenCalled();
expect(logo.deps.utils.delayExecution).not.toHaveBeenCalled();

turtle0.singer.embeddedGraphics = { 3: [] };
await logo.dispatchTurtleSignals(0, 0.5, 3, 0);
expect(global.delayExecution).not.toHaveBeenCalled();
expect(logo.deps.utils.delayExecution).not.toHaveBeenCalled();

turtle0.singer.embeddedGraphics = { 3: [1] };
logo.blockList = [null, { name: "clear", connections: [] }];
await logo.dispatchTurtleSignals(0, 0.5, 3, 0.1);

expect(turtle0.painter.doSetHeading).toHaveBeenCalledWith(0);
expect(turtle0.painter.doSetXY).toHaveBeenCalledWith(0, 0);
expect(global.delayExecution).toHaveBeenCalledWith(500);
expect(logo.deps.utils.delayExecution).toHaveBeenCalledWith(500);
expect(turtle0.embeddedGraphicsFinished).toBe(true);
});

Expand Down Expand Up @@ -1061,7 +1079,10 @@ describe("Logo comprehensive method coverage", () => {

test("doStopTurtles covers companion/camera/recorder/showBlocks branches", () => {
const clearIntervalSpy = jest.spyOn(global, "clearInterval").mockImplementation(() => {});
global.instruments = { 0: { flute: {} }, 1: { piano: {} } };
// Update deps.instruments since it was captured at constructor time
logo.deps.instruments = { 0: { flute: {} }, 1: { piano: {} } };
// Populate turtleList so the for loop iterates over both turtles
mockActivity.turtles.turtleList = [turtle0, turtle1];
turtle0.singer.killAllVoices = jest.fn();
turtle0.companionTurtle = 1;
turtle1.interval = 888;
Expand Down Expand Up @@ -1141,7 +1162,12 @@ describe("Logo comprehensive method coverage", () => {
logo.parseArg = jest.fn(() => 9);
logo.processShow = jest.fn();
logo.processSpeak = jest.fn();
global.delayExecution = jest.fn(() => Promise.resolve());
// Update logo.deps.utils.delayExecution since it was captured at constructor time
logo.deps.utils.delayExecution = jest.fn((ms, callback) => {
if (typeof callback === "function") {
callback();
}
});

turtle0.singer.suppressOutput = false;
turtle0.embeddedGraphicsFinished = false;
Expand Down Expand Up @@ -1204,7 +1230,7 @@ describe("Logo comprehensive method coverage", () => {
expect(logo.processShow).toHaveBeenCalled();
expect(logo.processSpeak).toHaveBeenCalled();
expect(mockActivity.textMsg).toHaveBeenCalledWith("9");
expect(global.delayExecution).toHaveBeenCalledWith(1000);
expect(logo.deps.utils.delayExecution).toHaveBeenCalledWith(1000);
});

test("constructor supports explicit dependency object mode", () => {
Expand All @@ -1217,7 +1243,30 @@ describe("Logo comprehensive method coverage", () => {
storage: { saveLocally: jest.fn() },
config: { showBlocksAfterRun: false },
callbacks: { onStopTurtle: jest.fn(), onRunTurtle: jest.fn() },
meSpeak: { speak: jest.fn() }
meSpeak: { speak: jest.fn() },
// Add missing required fields
instruments: {},
instrumentsFilters: {},
instrumentsEffects: {},
Singer: {},
Tone: {},
widgetWindows: { isOpen: jest.fn(() => false) },
classes: {
Notation: jest.fn().mockImplementation(() => ({})),
Synth: jest.fn().mockImplementation(() => ({})),
StatusMatrix: jest.fn().mockImplementation(() => ({}))
},
utils: {
doUseCamera: jest.fn(),
doStopVideoCam: jest.fn(),
getIntervalDirection: jest.fn(),
getIntervalNumber: jest.fn(),
mixedNumber: jest.fn(),
rationalToFraction: jest.fn(),
getStatsFromNotation: jest.fn(),
delayExecution: jest.fn(),
last: jest.fn(arr => arr[arr.length - 1])
}
};

const depLogo = new Logo(deps);
Expand Down
1 change: 1 addition & 0 deletions js/__tests__/logoconstants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ describe("logoconstants", () => {
"EMPTYHEAPERRORMSG",
"POSNUMBER",
"INVALIDPITCH",
"MIN_HIGHLIGHT_DURATION_MS",
"NOTATIONNOTE",
"NOTATIONDURATION",
"NOTATIONDOTCOUNT",
Expand Down
3 changes: 0 additions & 3 deletions js/__tests__/palette.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1294,13 +1294,10 @@ describe("Palettes Class", () => {
test("_makeBlockFromPalette handles null protoblk", () => {
palettes.add("test");
const palette = palettes.dict.test;
const consoleSpy = jest.spyOn(console, "debug").mockImplementation(() => {});

const result = palette._makeBlockFromPalette(null, "box", jest.fn());

expect(result).toBeUndefined();
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});

test("_makeBlockFromPalette uses namedbox default when undefined", () => {
Expand Down
Loading
Loading