mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
feat(writing-studio): add sprint engine with WPM tracking
Amit Haridas
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
class SprintEngine {
|
||||||
|
/**
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {function} opts.onEvent - callback(event_name, data)
|
||||||
|
*/
|
||||||
|
constructor(opts = {}) {
|
||||||
|
this.onEvent = opts.onEvent || (() => {});
|
||||||
|
this._active = false;
|
||||||
|
this._startTime = null;
|
||||||
|
this._duration = 0;
|
||||||
|
this._initialWords = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(durationMinutes, currentWordCount) {
|
||||||
|
if (this._active) throw new Error('Sprint already active');
|
||||||
|
this._active = true;
|
||||||
|
this._startTime = Date.now();
|
||||||
|
this._duration = durationMinutes * 60 * 1000;
|
||||||
|
this._initialWords = currentWordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(currentWordCount) {
|
||||||
|
if (!this._active) throw new Error('No active sprint');
|
||||||
|
const elapsed = Date.now() - this._startTime;
|
||||||
|
const wordDelta = Math.max(0, currentWordCount - this._initialWords);
|
||||||
|
const elapsedMinutes = elapsed / 60000;
|
||||||
|
const wpm = elapsedMinutes > 0 ? Math.round(wordDelta / elapsedMinutes) : 0;
|
||||||
|
this._active = false;
|
||||||
|
this._startTime = null;
|
||||||
|
return { wordDelta, elapsed, wpm };
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(elapsedMs) {
|
||||||
|
if (!this._active) return;
|
||||||
|
const remaining = Math.max(0, this._duration - elapsedMs);
|
||||||
|
this.onEvent('sprint:tick', { remaining, elapsed: elapsedMs });
|
||||||
|
if (remaining <= 0) {
|
||||||
|
this._active = false;
|
||||||
|
this.onEvent('sprint:complete', { expired: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive() { return this._active; }
|
||||||
|
|
||||||
|
getRemaining() {
|
||||||
|
if (!this._active) return 0;
|
||||||
|
return Math.max(0, this._duration - (Date.now() - this._startTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { SprintEngine };
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const { SprintEngine } = require('../src/plugins/built-in/writing-studio/sprint-engine');
|
||||||
|
|
||||||
|
describe('SprintEngine', () => {
|
||||||
|
let engine;
|
||||||
|
let events;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
events = [];
|
||||||
|
engine = new SprintEngine({
|
||||||
|
onEvent: (name, data) => events.push({ name, data })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starts a sprint with duration and initial word count', () => {
|
||||||
|
engine.start(25, 100);
|
||||||
|
expect(engine.isActive()).toBe(true);
|
||||||
|
expect(engine.getRemaining()).toBeLessThanOrEqual(25 * 60 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start throws if sprint already active', () => {
|
||||||
|
engine.start(25, 100);
|
||||||
|
expect(() => engine.start(10, 50)).toThrow('Sprint already active');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stop calculates WPM from word delta', () => {
|
||||||
|
engine.start(25, 100);
|
||||||
|
const result = engine.stop(350); // 250 words added
|
||||||
|
expect(result.wordDelta).toBe(250);
|
||||||
|
expect(engine.isActive()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stop throws if no active sprint', () => {
|
||||||
|
expect(() => engine.stop(100)).toThrow('No active sprint');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tick emits progress event with remaining time', () => {
|
||||||
|
engine.start(25, 100);
|
||||||
|
engine.tick(200);
|
||||||
|
expect(events.length).toBe(1);
|
||||||
|
expect(events[0].name).toBe('sprint:tick');
|
||||||
|
expect(events[0].data.remaining).toBeDefined();
|
||||||
|
expect(events[0].data.elapsed).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tick auto-stops when time expires and emits sprint:complete', () => {
|
||||||
|
engine.start(1, 100); // 1 minute
|
||||||
|
engine.tick(60 * 1000 + 1); // just past
|
||||||
|
expect(events.some(e => e.name === 'sprint:complete')).toBe(true);
|
||||||
|
expect(engine.isActive()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRemaining returns 0 when no sprint active', () => {
|
||||||
|
expect(engine.getRemaining()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getStats returns elapsed, WPM, wordDelta after stop', () => {
|
||||||
|
engine.start(25, 100);
|
||||||
|
const result = engine.stop(350);
|
||||||
|
expect(result).toHaveProperty('wordDelta', 250);
|
||||||
|
expect(result).toHaveProperty('elapsed');
|
||||||
|
expect(result).toHaveProperty('wpm');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user