mirror of
https://github.com/wth461694678/text-block-timer.git
synced 2026-07-22 05:46:04 +00:00
387 lines
21 KiB
JavaScript
387 lines
21 KiB
JavaScript
import { sleep, assert, log, WAIT_AFTER_COMMAND, idbGetTimer, idbGetDailyByTimer, shared } from '../e2e-timer-test.mjs';
|
|
|
|
// ─── Chain: passive_delete ───────────────────────────────────────────────────
|
|
// Tests passive deletion detection (user deletes timer span in editor) and
|
|
// passive restoration (Ctrl+Z / paste restores deleted timer span).
|
|
// Self-contained: creates its own test lines and cleans up after itself.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export async function chain_passive_delete(runner) {
|
|
const today = shared.today;
|
|
|
|
// ── Setup: create a test line with a paused timer ────────────────────────
|
|
|
|
const pdTestLine = await runner.eval(`(() => {
|
|
const ed = app.workspace.activeEditor.editor;
|
|
const last = ed.lineCount() - 1;
|
|
ed.replaceRange('\\nE2E_PASSIVE_DELETE_' + Date.now(), { line: last, ch: ed.getLine(last).length });
|
|
const n = ed.lineCount() - 1;
|
|
ed.setCursor({ line: n, ch: 0 });
|
|
return n;
|
|
})()`);
|
|
log('📝', `Passive delete test line at L${pdTestLine}`);
|
|
await sleep(500);
|
|
|
|
// Create a timer (running) then pause it
|
|
await runner.setCursorToLine(pdTestLine);
|
|
await sleep(200);
|
|
await runner.executeCommand('text-block-timer:toggle-timer');
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
let lineText = await runner.eval(`app.workspace.activeEditor.editor.getLine(${pdTestLine})`);
|
|
const pdTimerId = lineText.match(/id="([^"]+)"/)?.[1];
|
|
assert(pdTimerId, 'Failed to create timer for passive delete test');
|
|
log('🆔', `Passive delete timer ID: ${pdTimerId}`);
|
|
|
|
// Pause it
|
|
await runner.setCursorToLine(pdTestLine);
|
|
await sleep(200);
|
|
await runner.executeCommand('text-block-timer:toggle-timer');
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Verify it's paused
|
|
const stateBeforeDelete = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(stateBeforeDelete === 'paused', `Expected paused before delete, got ${stateBeforeDelete}`);
|
|
|
|
// Save the paused line text for restore tests
|
|
const pdOriginalLineText = await runner.eval(`app.workspace.activeEditor.editor.getLine(${pdTestLine})`);
|
|
log('📝', `Original line text saved (${pdOriginalLineText.length} chars)`);
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// TC-01: Passive delete paused timer
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('TC-01: Passive delete paused timer', async() => {
|
|
// Delete line content via CM6 dispatch (ensures undo history is recorded)
|
|
await runner.eval(`(() => {
|
|
const view = app.workspace.activeEditor.editor.cm;
|
|
const line = view.state.doc.line(${pdTestLine} + 1);
|
|
view.dispatch({ changes: { from: line.from, to: line.to, insert: '' } });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Verify JSON state
|
|
const jsonState = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(jsonState === 'deleted', `JSON state should be deleted, got ${jsonState}`);
|
|
|
|
// Verify IDB state
|
|
const idbRaw = await runner.evalAsync(idbGetTimer(pdTimerId));
|
|
assert(idbRaw, 'Timer should still exist in IDB');
|
|
const idbEntry = JSON.parse(idbRaw);
|
|
assert(idbEntry.state === 'deleted', `IDB state should be deleted, got ${idbEntry.state}`);
|
|
|
|
log('📊', `TC-01: JSON=${jsonState}, IDB=${idbEntry.state}`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// TC-03: Ctrl+Z restore paused timer
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('TC-03: Restore passively deleted timer to paused (simulated undo)', async() => {
|
|
// Restore the original line text via CM6 dispatch to simulate undo/paste-back
|
|
// This triggers updateListener which should detect the timer span reappearing
|
|
await runner.eval(`(() => {
|
|
const view = app.workspace.activeEditor.editor.cm;
|
|
const line = view.state.doc.line(${pdTestLine} + 1);
|
|
view.dispatch({ changes: { from: line.from, to: line.to, insert: ${JSON.stringify(pdOriginalLineText)} } });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Check state
|
|
const jsonState = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(jsonState === 'paused', `JSON state should be paused after restore, got ${jsonState}`);
|
|
|
|
// Verify IDB state
|
|
const idbRaw = await runner.evalAsync(idbGetTimer(pdTimerId));
|
|
const idbEntry = JSON.parse(idbRaw);
|
|
assert(idbEntry.state === 'paused', `IDB state should be paused after restore, got ${idbEntry.state}`);
|
|
|
|
// Verify span is back in the line
|
|
const restoredLine = await runner.eval(`app.workspace.activeEditor.editor.getLine(${pdTestLine})`);
|
|
assert(restoredLine.includes(`id="${pdTimerId}"`), 'Timer span should be restored in editor');
|
|
|
|
log('📊', `TC-03: JSON=${jsonState}, IDB=${idbEntry.state}, span restored`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// TC-07: Idempotency — calling handlePassiveDelete on already deleted timer
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('TC-07: Idempotency — handlePassiveDelete on deleted timer is no-op', async() => {
|
|
// First, passive delete again
|
|
await runner.eval(`(() => {
|
|
const ed = app.workspace.activeEditor.editor;
|
|
const lineLen = ed.getLine(${pdTestLine}).length;
|
|
ed.replaceRange('', { line: ${pdTestLine}, ch: 0 }, { line: ${pdTestLine}, ch: lineLen });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
const stateBefore = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(stateBefore === 'deleted', `Should be deleted before idempotency test, got ${stateBefore}`);
|
|
|
|
// Get daily_dur count before
|
|
const dailyBefore = await runner.evalAsync(idbGetDailyByTimer(pdTimerId));
|
|
const dailyBeforeCount = JSON.parse(dailyBefore).length;
|
|
|
|
// Call handlePassiveDelete directly — should be no-op
|
|
await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
p.handlePassiveDelete('${pdTimerId}');
|
|
return true;
|
|
})()`);
|
|
await sleep(500);
|
|
|
|
// Verify state unchanged
|
|
const stateAfter = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(stateAfter === 'deleted', `State should still be deleted, got ${stateAfter}`);
|
|
|
|
// Verify daily_dur unchanged
|
|
const dailyAfter = await runner.evalAsync(idbGetDailyByTimer(pdTimerId));
|
|
const dailyAfterCount = JSON.parse(dailyAfter).length;
|
|
assert(dailyAfterCount === dailyBeforeCount, `daily_dur count changed: ${dailyBeforeCount} → ${dailyAfterCount}`);
|
|
|
|
log('📊', `TC-07: Idempotent — state=${stateAfter}, daily_dur count unchanged (${dailyAfterCount})`);
|
|
});
|
|
|
|
// Restore for TC-05 (write back original text via CM6 dispatch)
|
|
await runner.eval(`(() => {
|
|
const view = app.workspace.activeEditor.editor.cm;
|
|
const line = view.state.doc.line(${pdTestLine} + 1);
|
|
view.dispatch({ changes: { from: line.from, to: line.to, insert: ${JSON.stringify(pdOriginalLineText)} } });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// TC-05: Cut and paste in same file
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('TC-05: Cut-paste same file — delete then restore', async() => {
|
|
// Save the line content
|
|
const savedLine = await runner.eval(`app.workspace.activeEditor.editor.getLine(${pdTestLine})`);
|
|
assert(savedLine.includes(`id="${pdTimerId}"`), 'Timer should be present before cut');
|
|
|
|
// Cut (delete the line content) via CM6 dispatch
|
|
await runner.eval(`(() => {
|
|
const view = app.workspace.activeEditor.editor.cm;
|
|
const line = view.state.doc.line(${pdTestLine} + 1);
|
|
view.dispatch({ changes: { from: line.from, to: line.to, insert: '' } });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Verify deleted after cut
|
|
const cutState = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(cutState === 'deleted', `After cut, state should be deleted, got ${cutState}`);
|
|
|
|
// Paste back the saved line content via CM6 dispatch
|
|
await runner.eval(`(() => {
|
|
const view = app.workspace.activeEditor.editor.cm;
|
|
const line = view.state.doc.line(${pdTestLine} + 1);
|
|
view.dispatch({ changes: { from: line.from, to: line.to, insert: ${JSON.stringify(pdOriginalLineText)} } });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Verify restored after paste/undo
|
|
const pasteState = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(pasteState === 'paused', `After paste, state should be paused, got ${pasteState}`);
|
|
|
|
log('📊', `TC-05: Cut state=${cutState}, Paste state=${pasteState}`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// TC-08: Editing non-timer line does not trigger passive delete
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('TC-08: Editing non-timer line does not affect timer state', async() => {
|
|
const stateBefore = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
|
|
// Edit a different line (line 0)
|
|
await runner.eval(`(() => {
|
|
const ed = app.workspace.activeEditor.editor;
|
|
const line0 = ed.getLine(0);
|
|
ed.replaceRange(' ', { line: 0, ch: line0.length });
|
|
return true;
|
|
})()`);
|
|
await sleep(1000);
|
|
|
|
const stateAfter = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(stateAfter === stateBefore, `Timer state changed from ${stateBefore} to ${stateAfter} after editing unrelated line`);
|
|
|
|
// Clean up the extra space
|
|
await runner.eval(`(() => {
|
|
const ed = app.workspace.activeEditor.editor;
|
|
const line0 = ed.getLine(0);
|
|
ed.replaceRange('', { line: 0, ch: line0.length - 1 }, { line: 0, ch: line0.length });
|
|
return true;
|
|
})()`);
|
|
|
|
log('📊', `TC-08: State unchanged (${stateAfter})`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// TC-02: Passive delete running timer + duration settlement
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
// First, make the timer running again
|
|
await runner.setCursorToLine(pdTestLine);
|
|
await sleep(200);
|
|
await runner.executeCommand('text-block-timer:toggle-timer');
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Verify it's running
|
|
const runState = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(runState === 'running', `Expected running before TC-02, got ${runState}`);
|
|
|
|
// Save the running line text for restore test (TC-04)
|
|
const pdRunningLineText = await runner.eval(`app.workspace.activeEditor.editor.getLine(${pdTestLine})`);
|
|
|
|
// Wait a bit so there's some accumulated time
|
|
log('⏳', 'Waiting for timer to accumulate time...');
|
|
await sleep(3000);
|
|
|
|
await runner.run('TC-02: Passive delete running timer — settles duration', async() => {
|
|
// Record daily_dur before delete
|
|
const dailyBefore = await runner.evalAsync(idbGetDailyByTimer(pdTimerId));
|
|
const dailyBeforeRecs = JSON.parse(dailyBefore);
|
|
const dailyBeforeDur = dailyBeforeRecs.find(r => r.stat_date === today)?.duration_sec ?? 0;
|
|
|
|
// Delete the line via CM6 dispatch
|
|
await runner.eval(`(() => {
|
|
const view = app.workspace.activeEditor.editor.cm;
|
|
const line = view.state.doc.line(${pdTestLine} + 1);
|
|
view.dispatch({ changes: { from: line.from, to: line.to, insert: '' } });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Verify state = deleted
|
|
const jsonState = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(jsonState === 'deleted', `JSON state should be deleted, got ${jsonState}`);
|
|
|
|
// Verify total_dur_sec > 0
|
|
const totalDur = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.total_dur_sec ?? 0;
|
|
})()`);
|
|
assert(totalDur > 0, `total_dur_sec should be > 0, got ${totalDur}`);
|
|
|
|
// Verify TimerManager no longer has this timer
|
|
const managerHas = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.manager.getTimerData('${pdTimerId}') !== null;
|
|
})()`);
|
|
assert(!managerHas, 'TimerManager should not have the timer after passive delete');
|
|
|
|
// Verify daily_dur was written
|
|
await sleep(500); // Wait for IDB async
|
|
const dailyAfter = await runner.evalAsync(idbGetDailyByTimer(pdTimerId));
|
|
const dailyAfterRecs = JSON.parse(dailyAfter);
|
|
const dailyAfterDur = dailyAfterRecs.find(r => r.stat_date === today)?.duration_sec ?? 0;
|
|
assert(dailyAfterDur >= dailyBeforeDur, `daily_dur should not decrease: ${dailyBeforeDur} → ${dailyAfterDur}`);
|
|
|
|
log('📊', `TC-02: state=${jsonState}, totalDur=${totalDur}s, daily: ${dailyBeforeDur}→${dailyAfterDur}s`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// TC-04: Ctrl+Z restore running timer → becomes paused (not running)
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('TC-04: Restore running timer as paused (not running)', async() => {
|
|
// Restore via CM6 dispatch (simulating undo/paste-back)
|
|
await runner.eval(`(() => {
|
|
const view = app.workspace.activeEditor.editor.cm;
|
|
const line = view.state.doc.line(${pdTestLine} + 1);
|
|
view.dispatch({ changes: { from: line.from, to: line.to, insert: ${JSON.stringify(pdRunningLineText)} } });
|
|
return true;
|
|
})()`);
|
|
await sleep(WAIT_AFTER_COMMAND);
|
|
|
|
// Verify state = paused (NOT running)
|
|
const jsonState = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.database.getEntry('${pdTimerId}')?.state ?? 'missing';
|
|
})()`);
|
|
assert(jsonState === 'paused', `After restore, state should be paused (not running), got ${jsonState}`);
|
|
|
|
// Verify TimerManager does NOT have it running
|
|
const managerHas = await runner.eval(`(() => {
|
|
const p = app.plugins.plugins['text-block-timer'];
|
|
return p.manager.getTimerData('${pdTimerId}') !== null;
|
|
})()`);
|
|
assert(!managerHas, 'TimerManager should not auto-resume timer after restore');
|
|
|
|
// Verify span is back
|
|
const restoredLine = await runner.eval(`app.workspace.activeEditor.editor.getLine(${pdTestLine})`);
|
|
assert(restoredLine.includes(`id="${pdTimerId}"`), 'Timer span should be restored after dispatch');
|
|
|
|
// Verify span class consistency: timer-r should have been patched to timer-p
|
|
// The async span fix runs via setTimeout(0), wait a bit for it to apply
|
|
await sleep(500);
|
|
const fixedLine = await runner.eval(`app.workspace.activeEditor.editor.getLine(${pdTestLine})`);
|
|
const hasTimerP = fixedLine.includes('class="timer-p"');
|
|
const hasTimerR = fixedLine.includes('class="timer-r"');
|
|
log('🔍', `TC-04 span class check: timer-p=${hasTimerP}, timer-r=${hasTimerR}`);
|
|
assert(hasTimerP && !hasTimerR, `Span class should be timer-p after restore, got line: ${fixedLine.substring(0, 120)}`);
|
|
|
|
log('📊', `TC-04: state=${jsonState}, managerRunning=${managerHas}, spanClass=timer-p ✓`);
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// Cleanup: remove passive delete test lines
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await runner.run('Cleanup: remove passive delete test lines', async() => {
|
|
await runner.eval(`(() => {
|
|
const ed = app.workspace.activeEditor.editor;
|
|
for (let i = ed.lineCount() - 1; i >= 0; i--) {
|
|
if (ed.getLine(i).includes('E2E_PASSIVE_DELETE_')) {
|
|
const from = Math.max(0, i - 1);
|
|
const fromCh = i > 0 ? ed.getLine(from).length : 0;
|
|
ed.replaceRange('', { line: from, ch: fromCh }, { line: i, ch: ed.getLine(i).length });
|
|
}
|
|
}
|
|
return true;
|
|
})()`);
|
|
await sleep(500);
|
|
log('🧹', 'Passive delete test lines removed');
|
|
});
|
|
}
|
|
|