mirror of
https://github.com/wth461694678/text-block-timer.git
synced 2026-07-22 12:30:24 +00:00
432 lines
25 KiB
JavaScript
432 lines
25 KiB
JavaScript
import { sleep, assert, log, WAIT_AFTER_COMMAND, WAIT_TICK_ACCUMULATE, TICK_TOLERANCE, idbGetTimer, idbGetDailyByTimer, idbGetDailyByDate, idbGetTimersByState, shared } from '../e2e-timer-test.mjs';
|
|
|
|
// ─── Chain: basic ────────────────────────────────────────────────────────────
|
|
|
|
export async function chain_basic(runner) {
|
|
const today = shared.today;
|
|
const initial = shared.initial;
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 1: Insert test line & create timer
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
shared.testLineNum = await runner.eval(`(() => {
|
|
const ed = app.workspace.activeEditor.editor;
|
|
const last = ed.lineCount() - 1;
|
|
ed.replaceRange('\\nE2E_SIDEBAR_TEST_' + Date.now(), { line: last, ch: ed.getLine(last).length });
|
|
const n = ed.lineCount() - 1;
|
|
ed.setCursor({ line: n, ch: 0 });
|
|
return n;
|
|
})()`);
|
|
const testLineNum = shared.testLineNum;
|
|
log('📝', `Inserted test line at L${testLineNum}`);
|
|
await sleep(500);
|
|
|
|
await runner.run('START timer on test line', async() => {
|
|
await runner.setCursorToLine(testLineNum);
|
|
await sleep(200);
|
|
await runner.executeCommand('text-block-timer:toggle-timer');
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
const lineText = await runner.eval(`app.workspace.activeEditor.editor.getLine(${testLineNum})`);
|
|
assert(lineText?.includes('class="timer-r"'), `Expected timer-r span, got: "${lineText}"`);
|
|
|
|
const match = lineText.match(/id="([^"]+)"/);
|
|
assert(match, 'Timer ID not found');
|
|
shared.createdTimerId = match[1];
|
|
log('🆔', `Timer ID: ${shared.createdTimerId}`);
|
|
});
|
|
|
|
const createdTimerId = shared.createdTimerId;
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 2: IDB seeded correctly after START
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('IDB: timer entry exists with state=running after START', async() => {
|
|
const raw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
assert(raw, 'Timer not found in IDB');
|
|
const entry = JSON.parse(raw);
|
|
assert(entry.state === 'running', `Expected state=running, got ${entry.state}`);
|
|
assert(entry.file_path === initial.filePath, `file_path mismatch`);
|
|
assert(entry.total_dur_sec >= 0, `total_dur_sec should be >= 0`);
|
|
log('📊', `IDB timer: state=${entry.state}, dur=${entry.total_dur_sec}s`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 3: Tick accumulation — IDB tickDelta correctness (core fix)
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('IDB tickDelta: daily_dur accumulates ~1s per tick', async() => {
|
|
// Read IDB daily_dur before waiting
|
|
const beforeRaw = await runner.evalAsync(idbGetDailyByTimer(createdTimerId));
|
|
const beforeRecs = JSON.parse(beforeRaw);
|
|
const beforeDur = beforeRecs.find(r => r.stat_date === today)?.duration_sec ?? 0;
|
|
|
|
// Also read IDB timer total_dur_sec
|
|
const beforeTimerRaw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
const beforeTimer = JSON.parse(beforeTimerRaw);
|
|
const beforeTotalDur = beforeTimer.total_dur_sec;
|
|
|
|
// Wait for ticks
|
|
const waitSec = WAIT_TICK_ACCUMULATE / 1000;
|
|
log('⏳', `Waiting ${waitSec}s for tick accumulation...`);
|
|
await sleep(WAIT_TICK_ACCUMULATE);
|
|
|
|
// Read IDB daily_dur after waiting
|
|
const afterRaw = await runner.evalAsync(idbGetDailyByTimer(createdTimerId));
|
|
const afterRecs = JSON.parse(afterRaw);
|
|
const afterDur = afterRecs.find(r => r.stat_date === today)?.duration_sec ?? 0;
|
|
|
|
// Read IDB timer total_dur_sec after
|
|
const afterTimerRaw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
const afterTimer = JSON.parse(afterTimerRaw);
|
|
const afterTotalDur = afterTimer.total_dur_sec;
|
|
|
|
const dailyIncrease = afterDur - beforeDur;
|
|
const totalIncrease = afterTotalDur - beforeTotalDur;
|
|
const minExpected = waitSec - TICK_TOLERANCE;
|
|
|
|
log('📊', `IDB daily_dur increase: ${dailyIncrease}s (expected ~${waitSec}s)`);
|
|
log('📊', `IDB total_dur increase: ${totalIncrease}s (expected ~${waitSec}s)`);
|
|
|
|
assert(dailyIncrease >= minExpected,
|
|
`daily_dur increase ${dailyIncrease}s too small (expected >= ${minExpected}s). ` +
|
|
`This was the core tickDelta bug — if ~0, the fix is not working.`);
|
|
assert(totalIncrease >= minExpected,
|
|
`total_dur increase ${totalIncrease}s too small (expected >= ${minExpected}s)`);
|
|
|
|
// daily_dur should approximately match total_dur increase
|
|
const drift = Math.abs(dailyIncrease - totalIncrease);
|
|
assert(drift <= TICK_TOLERANCE,
|
|
`daily_dur/total_dur drift: ${drift}s (tolerance ${TICK_TOLERANCE}s)`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 4: Sidebar list — data from IDB (running timer visible)
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('Sidebar list: running timer card visible with correct data', async() => {
|
|
const state = JSON.parse(await runner.eval(`(() => {
|
|
const leaves = app.workspace.getLeavesOfType('timer-sidebar');
|
|
if (!leaves.length) return JSON.stringify({ exists: false });
|
|
const c = leaves[0].view.containerEl;
|
|
const cards = c.querySelectorAll('.timer-card');
|
|
const runCards = c.querySelectorAll('.timer-card-running');
|
|
const ourEl = c.querySelector('[data-timer-id="${createdTimerId}"]');
|
|
return JSON.stringify({
|
|
exists: true,
|
|
totalCards: cards.length,
|
|
runningCards: runCards.length,
|
|
ourTimerVisible: !!ourEl,
|
|
ourDurText: ourEl?.textContent ?? ''
|
|
});
|
|
})()`));
|
|
assert(state.exists, 'Sidebar not found');
|
|
assert(state.ourTimerVisible, 'Our timer card not found in sidebar (data-timer-id element missing)');
|
|
assert(state.runningCards >= 1, `Expected >= 1 running card, got ${state.runningCards}`);
|
|
log('📊', `Sidebar: ${state.totalCards} total cards, ${state.runningCards} running, dur="${state.ourDurText}"`);
|
|
});
|
|
|
|
await runner.run('Sidebar summary: running count >= 1', async() => {
|
|
const state = JSON.parse(await runner.eval(`(() => {
|
|
const leaves = app.workspace.getLeavesOfType('timer-sidebar');
|
|
if (!leaves.length) return JSON.stringify({});
|
|
const c = leaves[0].view.containerEl;
|
|
return JSON.stringify({
|
|
count: c.querySelector('[data-summary-count]')?.textContent ?? '0',
|
|
running: c.querySelector('[data-summary-running-count]')?.textContent ?? '0',
|
|
paused: c.querySelector('[data-summary-paused-count]')?.textContent ?? '0',
|
|
totalDur: c.querySelector('[data-summary-total]')?.textContent ?? '',
|
|
runningDur: c.querySelector('[data-summary-running]')?.textContent ?? ''
|
|
});
|
|
})()`));
|
|
assert(parseInt(state.running) >= 1, `Running count should be >= 1, got ${state.running}`);
|
|
assert(parseInt(state.count) >= 1, `Total count should be >= 1, got ${state.count}`);
|
|
log('📊', `Summary: total=${state.count}, running=${state.running}, paused=${state.paused}`);
|
|
log('📊', `Summary dur: total="${state.totalDur}", running="${state.runningDur}"`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 5: Sidebar list dur matches IDB dur (real-time consistency)
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('Sidebar card dur matches IDB timer.total_dur_sec (±2s)', async() => {
|
|
// Read IDB timer dur
|
|
const idbRaw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
const idbEntry = JSON.parse(idbRaw);
|
|
const idbDur = idbEntry.total_dur_sec;
|
|
|
|
// Read sidebar card dur via TimerManager (sidebar patches from manager)
|
|
const managerDur = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
const d = p.manager.getTimerData('${createdTimerId}');
|
|
return d ? d.dur : -1;
|
|
})()`);
|
|
|
|
const diff = Math.abs(idbDur - managerDur);
|
|
log('📊', `IDB dur=${idbDur}s, Manager dur=${managerDur}s, diff=${diff}s`);
|
|
assert(diff <= TICK_TOLERANCE,
|
|
`IDB/Manager dur mismatch: IDB=${idbDur}, Manager=${managerDur}, diff=${diff}s`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 6: Status bar shows running timer info
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('Status bar: shows running timer info', async() => {
|
|
const text = await runner.eval(`
|
|
app.plugins.plugins['text-block-timer'].statusBarItem?.textContent ?? ''
|
|
`);
|
|
assert(text.length > 0, 'Status bar is empty');
|
|
log('📊', `Status bar: "${text}"`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 7: PAUSE timer → verify IDB state + sidebar updates
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
await runner.run('PAUSE timer', async() => {
|
|
await runner.setCursorToLine(testLineNum);
|
|
await sleep(200);
|
|
await runner.executeCommand('text-block-timer:toggle-timer');
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
const lineText = await runner.eval(`app.workspace.activeEditor.editor.getLine(${testLineNum})`);
|
|
assert(lineText?.includes('class="timer-p"'), `Expected timer-p, got: "${lineText}"`);
|
|
|
|
const durMatch = lineText.match(/data-dur="(\d+)"/);
|
|
assert(durMatch, 'data-dur not found');
|
|
shared.pausedDur = parseInt(durMatch[1], 10);
|
|
assert(shared.pausedDur > 0, `Expected dur > 0, got ${shared.pausedDur}`);
|
|
log('⏱', `Paused with dur=${shared.pausedDur}s`);
|
|
});
|
|
|
|
await runner.run('IDB: timer state=paused after PAUSE', async() => {
|
|
const raw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
assert(raw, 'Timer not in IDB after pause');
|
|
const entry = JSON.parse(raw);
|
|
assert(entry.state === 'paused', `Expected paused, got ${entry.state}`);
|
|
// IDB total_dur_sec should match editor dur (±2s because tick timing)
|
|
const diff = Math.abs(entry.total_dur_sec - shared.pausedDur);
|
|
assert(diff <= TICK_TOLERANCE,
|
|
`IDB dur ${entry.total_dur_sec} vs editor dur ${shared.pausedDur}, diff=${diff}s`);
|
|
log('📊', `IDB after pause: state=${entry.state}, dur=${entry.total_dur_sec}s`);
|
|
});
|
|
|
|
await runner.run('IDB: daily_dur not double-written on pause (no pause addDailyDur)', async() => {
|
|
// After pause, IDB daily_dur should be consistent with total_dur_sec
|
|
const dailyRaw = await runner.evalAsync(idbGetDailyByTimer(createdTimerId));
|
|
const dailyRecs = JSON.parse(dailyRaw);
|
|
const dailySum = dailyRecs.reduce((s, r) => s + r.duration_sec, 0);
|
|
|
|
const timerRaw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
const timer = JSON.parse(timerRaw);
|
|
|
|
const drift = Math.abs(dailySum - timer.total_dur_sec);
|
|
log('📊', `IDB daily_sum=${dailySum}s, total_dur=${timer.total_dur_sec}s, drift=${drift}s`);
|
|
assert(drift <= TICK_TOLERANCE,
|
|
`IDB daily_dur sum (${dailySum}) drifts from total_dur (${timer.total_dur_sec}) by ${drift}s. ` +
|
|
`If dailySum >> total, pause likely double-wrote daily_dur.`);
|
|
});
|
|
|
|
await runner.run('Sidebar list: timer card changed to paused', async() => {
|
|
const state = JSON.parse(await runner.eval(`(() => {
|
|
const leaves = app.workspace.getLeavesOfType('timer-sidebar');
|
|
if (!leaves.length) return JSON.stringify({});
|
|
const c = leaves[0].view.containerEl;
|
|
const runningEl = c.querySelector('[data-timer-id="${createdTimerId}"]');
|
|
const pausedCards = c.querySelectorAll('.timer-card-paused');
|
|
return JSON.stringify({
|
|
runningElGone: !runningEl,
|
|
pausedCount: pausedCards.length
|
|
});
|
|
})()`));
|
|
assert(state.runningElGone, 'Running data-timer-id element should disappear after pause');
|
|
assert(state.pausedCount >= 1, `Expected >= 1 paused card, got ${state.pausedCount}`);
|
|
});
|
|
|
|
await runner.run('Sidebar dur EXACTLY matches document dur after pause (0s tolerance)', async() => {
|
|
// Read document dur
|
|
const docDur = await runner.eval(`(() => {
|
|
const lineText = app.workspace.activeEditor.editor.getLine(${testLineNum});
|
|
const match = lineText.match(/data-dur="(\\d+)"/);
|
|
return match ? parseInt(match[1], 10) : -1;
|
|
})()`);
|
|
assert(docDur > 0, 'Document dur should be > 0');
|
|
|
|
// Read sidebar in-memory timerList dur for our timer
|
|
const sidebarDur = await runner.eval(`(() => {
|
|
const leaves = app.workspace.getLeavesOfType('timer-sidebar');
|
|
if (!leaves.length) return -1;
|
|
const view = leaves[0].view;
|
|
const timer = view.timerList ? view.timerList.find(t => t.timerId === '${createdTimerId}') : null;
|
|
return timer ? timer.dur : -1;
|
|
})()`);
|
|
|
|
// Read IDB dur
|
|
const idbRaw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
const idbDur = idbRaw ? JSON.parse(idbRaw).total_dur_sec : -1;
|
|
|
|
// Read JSON dur
|
|
const jsonDur = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
const e = p.database.data?.timers?.['${createdTimerId}'];
|
|
return e ? e.total_dur_sec : -1;
|
|
})()`);
|
|
|
|
log('📊', `After pause exact: doc=${docDur}, sidebar=${sidebarDur}, IDB=${idbDur}, JSON=${jsonDur}`);
|
|
|
|
// Sidebar dur must EXACTLY match document dur (this is the fix for the 1s drift)
|
|
assert(sidebarDur === docDur,
|
|
`Sidebar dur (${sidebarDur}) must exactly match document dur (${docDur}). ` +
|
|
`If off by 1s, the fix for passing newDur through onTimerStateChanged is not working.`);
|
|
|
|
// JSON dur must also exactly match document dur
|
|
assert(jsonDur === docDur,
|
|
`JSON dur (${jsonDur}) must exactly match document dur (${docDur}).`);
|
|
});
|
|
|
|
await runner.run('Sidebar summary: running count decreased after pause', async() => {
|
|
const running = await runner.eval(`(() => {
|
|
const leaves = app.workspace.getLeavesOfType('timer-sidebar');
|
|
if (!leaves.length) return '-1';
|
|
return leaves[0].view.containerEl.querySelector('[data-summary-running-count]')?.textContent ?? '-1';
|
|
})()`);
|
|
log('📊', `Running count after pause: ${running}`);
|
|
// We don't know how many other timers exist, but count should be consistent
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 8: JSON ↔ IDB cross-layer consistency after PAUSE
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('Cross-layer: JSON total_dur ≈ IDB total_dur (±2s)', async() => {
|
|
await sleep(500); // wait for flush
|
|
|
|
const jsonDur = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
const e = p.database.data?.timers?.['${createdTimerId}'];
|
|
return e ? e.total_dur_sec : -1;
|
|
})()`);
|
|
|
|
const idbRaw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
const idbEntry = JSON.parse(idbRaw);
|
|
|
|
const diff = Math.abs(jsonDur - idbEntry.total_dur_sec);
|
|
log('📊', `JSON dur=${jsonDur}s, IDB dur=${idbEntry.total_dur_sec}s, diff=${diff}s`);
|
|
assert(diff <= TICK_TOLERANCE,
|
|
`JSON/IDB total_dur mismatch: JSON=${jsonDur}, IDB=${idbEntry.total_dur_sec}`);
|
|
});
|
|
|
|
await runner.run('Cross-layer: JSON daily_dur ≈ IDB daily_dur (±2s)', async() => {
|
|
const jsonDailyDur = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
const dd = p.database.data?.daily_dur ?? {};
|
|
let sum = 0;
|
|
for (const [date, timerMap] of Object.entries(dd)) {
|
|
if (timerMap['${createdTimerId}'] !== undefined) {
|
|
sum += timerMap['${createdTimerId}'];
|
|
}
|
|
}
|
|
return sum;
|
|
})()`);
|
|
|
|
const idbDailyRaw = await runner.evalAsync(idbGetDailyByTimer(createdTimerId));
|
|
const idbDailyRecs = JSON.parse(idbDailyRaw);
|
|
const idbDailySum = idbDailyRecs.reduce((s, r) => s + r.duration_sec, 0);
|
|
|
|
const diff = Math.abs(jsonDailyDur - idbDailySum);
|
|
log('📊', `JSON daily_sum=${jsonDailyDur}s, IDB daily_sum=${idbDailySum}s, diff=${diff}s`);
|
|
assert(diff <= TICK_TOLERANCE,
|
|
`JSON/IDB daily_dur sum mismatch: JSON=${jsonDailyDur}, IDB=${idbDailySum}`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 9: CONTINUE timer → verify IDB + sidebar
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('CONTINUE timer', async() => {
|
|
await runner.setCursorToLine(testLineNum);
|
|
await sleep(200);
|
|
await runner.executeCommand('text-block-timer:toggle-timer');
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
const lineText = await runner.eval(`app.workspace.activeEditor.editor.getLine(${testLineNum})`);
|
|
assert(lineText?.includes('class="timer-r"'), `Expected timer-r after continue`);
|
|
});
|
|
|
|
await runner.run('IDB: state=running after CONTINUE', async() => {
|
|
const raw = await runner.evalAsync(idbGetTimer(createdTimerId));
|
|
const entry = JSON.parse(raw);
|
|
assert(entry.state === 'running', `Expected running, got ${entry.state}`);
|
|
});
|
|
|
|
await runner.run('Sidebar: running card reappears after CONTINUE', async() => {
|
|
const visible = await runner.eval(`(() => {
|
|
const leaves = app.workspace.getLeavesOfType('timer-sidebar');
|
|
if (!leaves.length) return false;
|
|
return !!leaves[0].view.containerEl.querySelector('[data-timer-id="${createdTimerId}"]');
|
|
})()`);
|
|
assert(visible, 'Timer card should reappear as running in sidebar');
|
|
});
|
|
|
|
// Let it run a bit more to accumulate more daily_dur
|
|
log('⏳', 'Waiting 3s for more tick accumulation...');
|
|
await sleep(3000);
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 10: Sidebar chart data correctness (reads from IDB)
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('Sidebar chart: IDB daily_dur has today entry with dur > 0', async() => {
|
|
const raw = await runner.evalAsync(idbGetDailyByDate(today));
|
|
const recs = JSON.parse(raw);
|
|
const ourRec = recs.find(r => r.timer_id === createdTimerId);
|
|
assert(ourRec, `No IDB daily_dur record for today (${today}) for our timer`);
|
|
assert(ourRec.duration_sec > 0, `Expected duration_sec > 0, got ${ourRec.duration_sec}`);
|
|
log('📊', `IDB daily_dur today: ${ourRec.duration_sec}s`);
|
|
});
|
|
|
|
await runner.run('Sidebar chart: chartDataCache reflects IDB data', async() => {
|
|
// Force chart re-render by toggling statistics
|
|
const chartData = await runner.evalAsync(`
|
|
const leaves = app.workspace.getLeavesOfType('timer-sidebar');
|
|
if (!leaves.length) return null;
|
|
const view = leaves[0].view;
|
|
if (view.chartDataCache) {
|
|
return JSON.stringify({
|
|
projects: view.chartDataCache.projects,
|
|
dates: view.chartDataCache.dates,
|
|
hasTodayDate: view.chartDataCache.dates.includes('${today}')
|
|
});
|
|
}
|
|
return null;
|
|
`);
|
|
if (chartData) {
|
|
const data = JSON.parse(chartData);
|
|
log('📊', `Chart cache: projects=${JSON.stringify(data.projects)}, dates=${JSON.stringify(data.dates)}`);
|
|
// Today should be in the dates array if our timer has daily_dur data
|
|
assert(data.hasTodayDate, `Chart dates should include today (${today})`);
|
|
} else {
|
|
log('⚠️', 'Chart data cache not available — statistics may be hidden');
|
|
}
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 11: Sidebar loadAllData reads from IDB (scope=all)
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('Sidebar loadAllData: IDB getTimersByState returns our timer', async() => {
|
|
const raw = await runner.evalAsync(idbGetTimersByState(['running', 'paused']));
|
|
const entries = JSON.parse(raw);
|
|
const ours = entries.find(e => e.timer_id === createdTimerId);
|
|
assert(ours, 'Our timer not found in IDB getTimersByState(running,paused)');
|
|
assert(ours.state === 'running', `Expected running, got ${ours.state}`);
|
|
log('📊', `IDB getTimersByState: ${entries.length} entries, our state=${ours.state}`);
|
|
});
|
|
}
|
|
|