Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
eb2a2c5
interface tests
acrosman Mar 16, 2026
ba98367
Initial draft of preload tests
acrosman Mar 17, 2026
ef3eb06
Getting tests to pass.
acrosman Mar 19, 2026
1279d44
Testing progress
acrosman Mar 19, 2026
52690db
Initial plan
Copilot Mar 17, 2026
0ad682d
feat: add High Speed Memory game plugin
Copilot Mar 17, 2026
88a9736
fix: apply code review feedback (it→test, Initialize spelling)
Copilot Mar 17, 2026
10d9fe8
Update instructions for game patterns.
acrosman Mar 18, 2026
eac5dee
feat: revamp High Speed Memory per feedback (square grids, triplets, …
Copilot Mar 18, 2026
e00d4ef
fix: address code review - use window.webkitAudioContext, remove trai…
Copilot Mar 18, 2026
b03c521
Adding AI generated memory images
acrosman Mar 18, 2026
82450f3
Removed generated images.
acrosman Mar 19, 2026
23bda47
feat: update image scheme, fix grid display, add return-to-menu
Copilot Mar 19, 2026
fdc2e7e
fix: inject game stylesheet via interface.js so CSS grid renders corr…
Copilot Mar 19, 2026
6eac93f
Adjust game timing
acrosman Mar 19, 2026
6df96b1
Updated test for starting pause.
acrosman Mar 19, 2026
47e6ac8
feat: require 3 consecutive correct rounds to advance a level in High…
Copilot Mar 19, 2026
f39e4ae
Update images
acrosman Mar 19, 2026
c4ce4e6
feat: primary image on instructions, wrong-guess restarts round, remo…
Copilot Mar 19, 2026
33551be
Improved test coverage
acrosman Mar 19, 2026
3bca03f
Revert "Testing progress"
acrosman Mar 19, 2026
81c3256
Initial plan
Copilot Mar 19, 2026
1608459
Fix fast piggie flash to scope to play area and remove canvas border
Copilot Mar 19, 2026
16c0f69
Update end game modal
acrosman Mar 22, 2026
b7485d3
NPM Update
acrosman Mar 21, 2026
0d1f0cc
Fix to wrong answer guidance.
acrosman Mar 22, 2026
b64347b
First draft of field of view game.
acrosman Mar 22, 2026
65fac97
Update game name in index file.
acrosman Mar 22, 2026
b3a54b8
Adding images, updating to use images.
acrosman Mar 22, 2026
3d330a4
Improved game play
acrosman Mar 22, 2026
3d820e5
Further improved game play.
acrosman Mar 22, 2026
f9755c5
Initial plan
Copilot Mar 22, 2026
50e41f2
Address review feedback: fix imports, manifest, split index.js, aria …
Copilot Mar 22, 2026
525efa2
Revert response panel hide/show during trial phases to prevent layout…
Copilot Mar 22, 2026
b62d340
Package file clean up
acrosman Mar 22, 2026
70184e4
First draft of bunny memory
acrosman Mar 22, 2026
46e95d4
Updates to improve game play of rabbit memory
acrosman Mar 22, 2026
8c8ae77
Improved orbit memory game play.
acrosman Mar 22, 2026
e74f943
Initial plan
Copilot Mar 22, 2026
f7513e8
Fix timer ordering in submitSelection, update JSDoc for init and crea…
Copilot Mar 22, 2026
cf0359e
Fix linting issues.
acrosman Mar 22, 2026
c69615a
Fix test errors, expand testing.
acrosman Mar 23, 2026
9a4fcfb
Fixes to errors post rebase/merge with main
acrosman Mar 23, 2026
40e153e
Merge branch 'main' into feature/interface-preload-tests
acrosman Mar 23, 2026
54c8e01
lint fixes
acrosman Mar 23, 2026
44d3bf2
Expanded coverage.
acrosman Mar 23, 2026
f230c6b
Adding coverage to reach all function calls and 80% of branches
acrosman Mar 23, 2026
8508665
Initial plan
Copilot Mar 23, 2026
8831360
Fix preload ESM mocking and remove duplicate gameCard field-of-view b…
Copilot Mar 23, 2026
a43ae79
Improve branch coverage: fix fast-piggie imagesHiddenAt and add targe…
Copilot Mar 23, 2026
9ff91c0
Fix game loading: safe electron import, error handling in loadAndInit…
Copilot Mar 23, 2026
e214cf0
Revert preload.js to require('electron') and restore original test shim
Copilot Mar 23, 2026
9d24e1a
Merge pull request #26 from acrosman/copilot/sub-pr-7
acrosman Mar 24, 2026
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
9 changes: 9 additions & 0 deletions __mocks__/electron.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export const contextBridge = {
exposeInMainWorld: jest.fn(),
};

export const ipcRenderer = {
send: jest.fn(),
on: jest.fn(),
invoke: jest.fn(),
};
import { jest } from '@jest/globals';

export const app = {
Expand Down
3 changes: 2 additions & 1 deletion app/games/fast-piggie/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ function _runRound() {

_roundTimer = setTimeout(() => {
clearImages(_ctx, width, height, wedgeCount);
_currentRound._imagesHiddenAt = Date.now();
_clickEnabled = true;
_hoveredWedge = -1;
_selectedWedge = -1;
Expand Down Expand Up @@ -486,7 +487,7 @@ function _resolveRound(wedge) {
// Track answer speed (time from images hidden to answer)
// We'll store the time when images are hidden in _currentRound._imagesHiddenAt
let answerSpeedMs = null;
if (_currentRound._imagesHiddenAt) {
if (_currentRound._imagesHiddenAt != null) {
answerSpeedMs = Date.now() - _currentRound._imagesHiddenAt;
}

Expand Down
49 changes: 49 additions & 0 deletions app/games/fast-piggie/tests/game.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
calculateWedgeIndex,
addScore,
addMiss,
getBestStats,
getScore,
getRoundsPlayed,
getLevel,
Expand Down Expand Up @@ -416,3 +417,51 @@ describe('isRunning()', () => {
expect(isRunning()).toBe(false);
});
});

describe('getBestStats()', () => {
it('returns an object with maxScore, mostRounds, mostGuineaPigs, topSpeedMs', () => {
const stats = getBestStats();
expect(stats).toHaveProperty('maxScore');
expect(stats).toHaveProperty('mostRounds');
expect(stats).toHaveProperty('mostGuineaPigs');
expect(stats).toHaveProperty('topSpeedMs');
});

it('maxScore reflects the highest score achieved since module load', () => {
startGame();
addScore();
addScore();
stopGame();
const stats = getBestStats();
expect(stats.maxScore).toBeGreaterThanOrEqual(2);
});

it('mostGuineaPigs updates when addScore is called with a guineaPigs count', () => {
addScore(7);
const stats = getBestStats();
expect(stats.mostGuineaPigs).toBeGreaterThanOrEqual(7);
});

it('topSpeedMs updates when addScore is called with an answerSpeedMs value', () => {
addScore(3, 500);
const stats = getBestStats();
expect(stats.topSpeedMs).toBeLessThanOrEqual(500);
});

it('topSpeedMs is null when no speed has been recorded', () => {
// initGame() resets the session but session-best trackers are module-level;
// to get null we rely on a fresh import (module-level state).
// Here we just verify the type contract: null or a non-negative number.
const stats = getBestStats();
expect(stats.topSpeedMs === null || typeof stats.topSpeedMs === 'number').toBe(true);
});

it('mostRounds reflects rounds played across sessions', () => {
startGame();
addScore();
addMiss();
stopGame();
const stats = getBestStats();
expect(stats.mostRounds).toBeGreaterThanOrEqual(2);
});
});
171 changes: 171 additions & 0 deletions app/games/fast-piggie/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1094,3 +1094,174 @@ describe('progress saving', () => {
delete globalThis.api;
});
});

// ===========================================================================
// stop() end-panel flow and return-to-main-menu wiring
// ===========================================================================
describe('stop() end-panel flow and _returnToMainMenu', () => {
it('stop() shows the end panel with score and high score', async () => {
plugin.start();
await plugin.stop();
const endPanel = container.querySelector('#fp-end-panel');
expect(endPanel.hidden).toBe(false);
expect(container.querySelector('#fp-final-score').textContent).toBe('3');
expect(container.querySelector('#fp-final-high-score').textContent).toBe('3');
});

it('stop() hides the game area and End Game button', async () => {
plugin.start();
await plugin.stop();
expect(container.querySelector('#fp-game-area').hidden).toBe(true);
expect(container.querySelector('#fp-stop-btn').hidden).toBe(true);
});

it('clicking #fp-return-btn dispatches bsx:return-to-main-menu', () => {
let fired = false;
window.addEventListener('bsx:return-to-main-menu', () => { fired = true; }, { once: true });
container.querySelector('#fp-return-btn').click();
expect(fired).toBe(true);
});
});

// ===========================================================================
// start button click callback (f[34] in index.js)
// ===========================================================================
describe('start button click event', () => {
it('clicking #fp-start-btn triggers plugin.start()', () => {
const startBtn = container.querySelector('#fp-start-btn');
startBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(game.startGame).toHaveBeenCalled();
});
});

// ===========================================================================
// _triggerFlash setTimeout callback and _resolveRound next-round timer
// (f[18] and f[30] in index.js)
// ===========================================================================
describe('_triggerFlash and next-round timers', () => {
function fireCorrectClick() {
container.querySelector('#fp-canvas').dispatchEvent(
new MouseEvent('click', { clientX: 250, clientY: 100, bubbles: true }),
);
}

beforeEach(() => {
game.calculateWedgeIndex.mockReturnValue(2);
game.checkAnswer.mockReturnValue(true);
plugin.start();
jest.runAllTimers(); // advance past displayDurationMs → _clickEnabled = true
});

it('flash class is removed after the 600ms _triggerFlash timeout fires', () => {
fireCorrectClick();
const flash = container.querySelector('#fp-flash');
expect(flash.classList.contains('fp-flash--correct')).toBe(true);
jest.runAllTimers(); // fires 600ms flash timer + 1000ms next-round + next displayDurationMs
expect(flash.classList.contains('fp-flash--correct')).toBe(false);
});

it('next-round 1000ms timer calls _runRound (generateRound is called again)', () => {
game.generateRound.mockClear();
fireCorrectClick();
jest.runAllTimers(); // fires 600ms flash + 1000ms next-round → _runRound → generateRound
expect(game.generateRound).toHaveBeenCalled();
});
});

// ===========================================================================
// loadImages onerror path and init() .catch() fallback
// (f[4] and f[33] in index.js)
// ===========================================================================
describe('loadImages() — onerror path', () => {
it('rejects when the image fails to load (onerror fired)', async () => {
const OriginalImage = globalThis.Image;
globalThis.Image = class {
set src(_) {
if (this.onerror) this.onerror(new Error('load failed'));
}
};

let rejected = false;
try {
await loadImages('bad.png');
} catch {
rejected = true;
}
expect(rejected).toBe(true);

globalThis.Image = OriginalImage;
});
});

describe('init() — loadImages .catch() fallback', () => {
it('does not throw when image loading fails (falls back to null images)', async () => {
const OriginalImage = globalThis.Image;
globalThis.Image = class {
set src(_) {
if (this.onerror) this.onerror(new Error('load failed'));
}
};

// init() calls loadImages which will reject; .catch() sets _images = [null, null]
expect(() => plugin.init(buildContainer())).not.toThrow();
// Flush microtasks so the .catch() callback executes
await Promise.resolve();
await Promise.resolve();

globalThis.Image = OriginalImage;
});
});

// ===========================================================================
// _resolveRound — no slot assignment when imageCount equals wedgeCount
// (covers the `return outlierWedgeIndex` else-path in _getCorrectWedgeIndex)
// ===========================================================================
describe('_resolveRound — no slot assignment when imageCount equals wedgeCount', () => {
beforeEach(() => {
game.generateRound.mockReturnValueOnce({
wedgeCount: 6,
imageCount: 6,
displayDurationMs: 2000,
outlierWedgeIndex: 2,
});
game.calculateWedgeIndex.mockReturnValue(2);
game.checkAnswer.mockReturnValue(true);
plugin.start();
jest.runAllTimers();
});

it('resolves round without slot assignment (imageCount === wedgeCount)', () => {
const canvas = container.querySelector('#fp-canvas');
canvas.dispatchEvent(new MouseEvent('click', { clientX: 250, clientY: 100, bubbles: true }));
expect(game.addScore).toHaveBeenCalled();
});
});

// ===========================================================================
// _showEndPanel and _returnToMainMenu
// ===========================================================================
describe('_showEndPanel and _returnToMainMenu', () => {
it('end panel contains Game Over heading after stop()', async () => {
plugin.start();
await plugin.stop();
const endPanel = container.querySelector('#fp-end-panel');
expect(endPanel.textContent).toContain('Game Over');
});

it('clicking #fp-play-again-btn resets and restarts game', () => {
const resetSpy = jest.spyOn(plugin, 'reset');
const startSpy = jest.spyOn(plugin, 'start');
container.querySelector('#fp-play-again-btn').click();
expect(resetSpy).toHaveBeenCalled();
expect(startSpy).toHaveBeenCalled();
resetSpy.mockRestore();
startSpy.mockRestore();
});

it('clicking #fp-return-btn can be triggered repeatedly without throwing', () => {
expect(() => {
container.querySelector('#fp-return-btn').click();
container.querySelector('#fp-return-btn').click();
}).not.toThrow();
});
});
Loading
Loading