Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add configurable Weekend Streak Protection (codecritter.weekendStreakProtection) to protect streaks over Saturday and Sunday (Fixes #31).
- Clean up corrupted Unicode Mojibake artifacts in media/sidebar.html (Fixes #28).
- Update .vscodeignore to exclude internal scripts/tests/CI and add Content-Security-Policy to sidebar webview (Fixes #29).
- Fix pet mood duration and unlock typing combo meter in Feed/Play commands (Fixes #23).

---

Expand Down
21 changes: 13 additions & 8 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,17 @@ function _applyMood(moodKey) {
* @param {string} moodKey
* @param {boolean} [temporary=false]
*/
function setMood(moodKey, temporary = false) {
function setMood(moodKey, temporary = false, durationMs = 2500, onRevert = null) {
if (temporary) {
if (flashTimeout) clearTimeout(flashTimeout);
_applyMood(moodKey);
flashTimeout = setTimeout(() => {
flashTimeout = null;
_applyMood(currentMood);
}, 2500);
if (typeof onRevert === 'function') {
onRevert();
}
}, durationMs);
} else {
if (flashTimeout) return; // don't override a flash
currentMood = moodKey;
Expand Down Expand Up @@ -295,21 +298,23 @@ function activate(context) {
/** Feed Pet */
const cmdFeedPet = vscode.commands.registerCommand('codecritter.feedPet', () => {
vscode.window.showInformationMessage(`🍕 You fed ${petName()}! Nom nom nom.`);
setMood('excited', true);
setMood('excited', true, 5000, () => {
updateDashboardIfOpen(stats, petName(), currentMood);
sidebarProvider.update();
});
updateDashboardIfOpen(stats, petName(), 'excited');
sidebarProvider.update();
if (comboTimeout) clearTimeout(comboTimeout);
comboTimeout = setTimeout(() => { comboTimeout = null; }, 5000); // stay excited for 5s
});

/** Play Pet */
const cmdPlayPet = vscode.commands.registerCommand('codecritter.playPet', () => {
vscode.window.showInformationMessage(`🎾 You played with ${petName()}! So much fun!`);
setMood('celebrating', true);
setMood('celebrating', true, 5000, () => {
updateDashboardIfOpen(stats, petName(), currentMood);
sidebarProvider.update();
});
updateDashboardIfOpen(stats, petName(), 'celebrating');
sidebarProvider.update();
if (comboTimeout) clearTimeout(comboTimeout);
comboTimeout = setTimeout(() => { comboTimeout = null; }, 5000);
});

// ── Event: File Saved ──
Expand Down
129 changes: 129 additions & 0 deletions tests/commands.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
'use strict';

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');

test('Issue #23: Feed/Play commands maintain mood duration for 5s and do not lock comboTimeout', (t, done) => {
const disposable = { dispose() {} };
const commands = {};
let infoMessage = '';
const statusBar = {
text: '',
tooltip: '',
show() {},
hide() {}
};

const vscode = {
DiagnosticSeverity: { Error: 0 },
StatusBarAlignment: { Right: 1 },
MarkdownString: class { constructor(value) { this.value = value; } },
commands: {
registerCommand: (cmd, handler) => {
commands[cmd] = handler;
return disposable;
}
},
window: {
createStatusBarItem: () => statusBar,
registerWebviewViewProvider: () => disposable,
showInformationMessage: msg => { infoMessage = msg; },
visibleTextEditors: []
},
workspace: {
getConfiguration: () => ({ get: (key, fallback) => key === 'weeklyReview' ? false : fallback }),
onDidSaveTextDocument: () => disposable,
onDidChangeTextDocument: () => disposable,
onDidChangeConfiguration: () => disposable
},
languages: {
onDidChangeDiagnostics: () => disposable,
getDiagnostics: () => []
}
};

let sidebarUpdated = 0;
class MockSidebarProvider {
constructor() {}
update() { sidebarUpdated++; }
}

let dashboardUpdatedMood = null;
const dependencies = {
vscode,
'./src/stats': {
StatsEngine: class {
getLevel() { return 1; }
getSnapshot() { return { level: 1, saveCount: 0, linesTyped: 0, currentStreak: 0, errorsFixed: 0 }; }
}
},
'./src/moods': {
getMoodDisplay: mood => ({ statusText: `(${mood})`, label: mood, tip: '' }),
checkMilestones: () => [],
getRandomHiMessage: () => 'hi',
getRandomTip: () => 'tip',
PET_THEMES: { default: { label: 'Default' } }
},
'./src/dashboard': {
showDashboard: () => {},
updateDashboardIfOpen: (stats, name, mood) => { dashboardUpdatedMood = mood; }
},
'./src/sidebar': {
SidebarProvider: MockSidebarProvider
}
};

const timers = [];
const sandbox = {
module: { exports: {} },
require: name => dependencies[name] || require(name),
__dirname: path.join(__dirname, '..'),
setTimeout: (fn, ms) => {
const id = setTimeout(fn, ms);
timers.push(id);
return id;
},
clearTimeout: id => {
clearTimeout(id);
},
setInterval: () => 1,
clearInterval: () => {},
Date
};

const extensionCode = fs.readFileSync(path.join(__dirname, '..', 'extension.js'), 'utf8');
vm.runInNewContext(extensionCode, sandbox);
const activate = sandbox.module.exports.activate;
const deactivate = sandbox.module.exports.deactivate;

const mockContext = {
subscriptions: [],
globalState: { get: () => null, update: () => Promise.resolve() }
};

activate(mockContext);

assert.ok(typeof commands['codecritter.feedPet'] === 'function', 'feedPet command must be registered');
assert.ok(typeof commands['codecritter.playPet'] === 'function', 'playPet command must be registered');

// Trigger feedPet
commands['codecritter.feedPet']();
assert.ok(infoMessage.includes('You fed'), 'info message shown');
assert.ok(statusBar.text.includes('excited'), 'status bar displays excited mood');
assert.equal(dashboardUpdatedMood, 'excited', 'dashboard updated to excited');
assert.ok(sidebarUpdated > 0, 'sidebar was updated');

// Trigger playPet
commands['codecritter.playPet']();
assert.ok(infoMessage.includes('You played'), 'info message shown');
assert.ok(statusBar.text.includes('celebrating'), 'status bar displays celebrating mood');
assert.equal(dashboardUpdatedMood, 'celebrating', 'dashboard updated to celebrating');

// Cleanup
deactivate();
timers.forEach(t => clearTimeout(t));
done();
});
Loading