Skip to content

Commit e85aafa

Browse files
committed
test(coverage): add integration tests for all user stories (#190)
Step 0 — infra fixes: - read-templates: add afterEach to restore journal.scopes after suite - phase1-regression: use fixed past date to avoid midnight-brittle offset - week-input: drop moment().week() calls; assert valid range and relative offset to survive year-rollover week 52→1 - notes-sync: capture last editor text and include in timeout message Steps 1–7 — new and extended suites (happy + error path per story): - codeaction-tasks.test.ts: OpenTaskActions and CompletedTaskActions provideCodeActions → WorkspaceEdit round-trip; non-matching lines return empty - commands-inject.test.ts: Inject.injectInput for memo and task; empty text returns early with no error logged - commands-note.test.ts: ShowNoteCommand creates note file; cancelled input triggers showError - commands-weekly.test.ts: Reader.loadEntryForWeek creates and preserves weekly file; week 0 handled gracefully - commands-entry.test.ts: real-FS suite for today/yesterday/tomorrow commands; reader-throws path triggers showError - commands-prev-next.test.ts: scoped navigation helper-layer tests via findAdjacentEntry with explicit Anchor; verifies scope isolation and null result at history start; this.slow() thresholds throughout - commands-print.test.ts: error paths for non-numeric sum and single-cursor duration Performance: this.slow(ms) set on each new suite so CI output flags slow tests without hard-failing on timing. #190
1 parent 6c73b16 commit e85aafa

11 files changed

Lines changed: 591 additions & 21 deletions
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as assert from 'assert';
2+
import * as vscode from 'vscode';
3+
import { OpenTaskActions } from '../../provider/codeactions/for-open-tasks';
4+
import { CompletedTaskActions } from '../../provider/codeactions/for-completed-tasks';
5+
import * as J from '../..';
6+
import { createMockCtrl, openEditor } from './command-test-helpers';
7+
8+
function makeRange(line: number, startChar: number, endChar: number): vscode.Range {
9+
return new vscode.Range(line, startChar, line, endChar);
10+
}
11+
12+
function makeContext(): vscode.CodeActionContext {
13+
return {
14+
diagnostics: [],
15+
only: undefined,
16+
triggerKind: vscode.CodeActionTriggerKind.Invoke
17+
};
18+
}
19+
20+
suite('Code actions — task state transitions', function () {
21+
this.slow(3000);
22+
23+
suite('OpenTaskActions (open → completed)', () => {
24+
test('happy: open-task line returns Complete action; applying edit marks [x] + timestamp', async () => {
25+
const editor = await openEditor('- [ ] write tests\n');
26+
const doc = editor.document;
27+
const line = doc.lineAt(0);
28+
const range = makeRange(0, 0, line.text.length);
29+
const token = new vscode.CancellationTokenSource().token;
30+
const ctrl = createMockCtrl({
31+
config: {
32+
getTaskInlineTemplate: async () => ({ template: '- [ ] ${input}', value: '- [ ] ${input}' } as J.Model.InlineTemplate),
33+
getTimeStringTemplate: async () => ({ value: '12:34' } as J.Model.ScopedTemplate),
34+
getBasePath: () => '/tmp/journal-tests',
35+
getBasePathForLocalOpen: () => '/tmp/journal-tests'
36+
}
37+
});
38+
39+
const provider = new OpenTaskActions(ctrl);
40+
const actions = await provider.provideCodeActions(doc, range, makeContext(), token) as vscode.CodeAction[];
41+
42+
assert.ok(Array.isArray(actions) && actions.length > 0, 'expected at least one action');
43+
const completeAction = actions[0];
44+
assert.ok(completeAction.edit, 'action must have a WorkspaceEdit');
45+
46+
await vscode.workspace.applyEdit(completeAction.edit!);
47+
48+
const updated = doc.getText();
49+
assert.ok(updated.includes('[x]'), `expected [x] in: ${updated}`);
50+
assert.ok(updated.includes('done:'), `expected done: timestamp in: ${updated}`);
51+
});
52+
53+
test('error: non-task line returns no actions', async () => {
54+
const editor = await openEditor('# Just a heading\n');
55+
const doc = editor.document;
56+
const range = makeRange(0, 0, doc.lineAt(0).text.length);
57+
const token = new vscode.CancellationTokenSource().token;
58+
const ctrl = createMockCtrl();
59+
60+
const provider = new OpenTaskActions(ctrl);
61+
const result = await provider.provideCodeActions(doc, range, makeContext(), token);
62+
63+
assert.ok(!result || (Array.isArray(result) && result.length === 0), 'expected no actions for non-task line');
64+
});
65+
});
66+
67+
suite('CompletedTaskActions (completed → open)', () => {
68+
test('happy: completed-task line returns Reopen action; applying edit restores [ ] and strips annotation', async () => {
69+
const editor = await openEditor('- [x] write tests (done: 2026-05-15 10:00)\n');
70+
const doc = editor.document;
71+
const range = makeRange(0, 0, doc.lineAt(0).text.length);
72+
const token = new vscode.CancellationTokenSource().token;
73+
const ctrl = createMockCtrl();
74+
75+
const provider = new CompletedTaskActions(ctrl);
76+
const actions = await provider.provideCodeActions(doc, range, makeContext(), token) as vscode.CodeAction[];
77+
78+
assert.ok(Array.isArray(actions) && actions.length > 0, 'expected at least one action');
79+
const reopenAction = actions[0];
80+
assert.ok(reopenAction.edit, 'action must have a WorkspaceEdit');
81+
82+
await vscode.workspace.applyEdit(reopenAction.edit!);
83+
84+
const updated = doc.getText();
85+
assert.ok(updated.includes('[ ]'), `expected [ ] in: ${updated}`);
86+
assert.ok(!updated.includes('done:'), `expected done: annotation removed, got: ${updated}`);
87+
});
88+
89+
test('error: open-task line returns no actions', async () => {
90+
const editor = await openEditor('- [ ] still open\n');
91+
const doc = editor.document;
92+
const range = makeRange(0, 0, doc.lineAt(0).text.length);
93+
const token = new vscode.CancellationTokenSource().token;
94+
const ctrl = createMockCtrl();
95+
96+
const provider = new CompletedTaskActions(ctrl);
97+
const result = await provider.provideCodeActions(doc, range, makeContext(), token);
98+
99+
assert.ok(!result || (Array.isArray(result) && result.length === 0), 'expected no actions for open-task line');
100+
});
101+
});
102+
});

src/test/suite/commands-entry.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import * as assert from 'assert';
2+
import * as os from 'os';
3+
import * as path from 'path';
24
import * as vscode from 'vscode';
35
import * as J from '../..';
46
import { ShowEntryForInputCommand } from '../../provider/commands/show-entry-for-input';
57
import { ShowEntryForTodayCommand } from '../../provider/commands/show-entry-for-today';
68
import { ShowEntryForTomorrowCommand } from '../../provider/commands/show-entry-for-tomorrow';
79
import { ShowEntryForYesterdayCommand } from '../../provider/commands/show-entry-for-yesterday';
810
import { createMockCtrl, tick } from './command-test-helpers';
11+
import { TestLogger } from '../test-logger';
12+
import { fileExists } from '../../util/fs-exists';
13+
14+
async function buildCtrl(tmpBase: string): Promise<{ ctrl: J.Util.Ctrl; logger: TestLogger }> {
15+
const config = vscode.workspace.getConfiguration('journal');
16+
await config.update('base', tmpBase, vscode.ConfigurationTarget.Workspace);
17+
const refreshed = vscode.workspace.getConfiguration('journal');
18+
const ctrl = new J.Util.Ctrl(refreshed);
19+
const logger = new TestLogger(false);
20+
ctrl.logger = logger;
21+
return { ctrl, logger };
22+
}
923

1024
suite('Command suites - entry commands', () => {
1125
test('exposes command metadata for entry command classes', async () => {
@@ -187,3 +201,87 @@ suite('Command suites - entry commands', () => {
187201
}
188202
});
189203
});
204+
205+
suite('Entry commands — real filesystem', function () {
206+
this.slow(8000);
207+
208+
let originalBase: string | undefined;
209+
let tmpBase: string;
210+
let ctrl: J.Util.Ctrl;
211+
let logger: TestLogger;
212+
213+
setup(async () => {
214+
const config = vscode.workspace.getConfiguration('journal');
215+
originalBase = config.get<string>('base');
216+
tmpBase = path.join(os.tmpdir(), `entry-real-${Date.now()}`);
217+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(tmpBase));
218+
({ ctrl, logger } = await buildCtrl(tmpBase));
219+
(ctrl.ui as any).showDocument = async (_doc: vscode.TextDocument) => undefined;
220+
});
221+
222+
teardown(async () => {
223+
const config = vscode.workspace.getConfiguration('journal');
224+
await config.update('base', originalBase, vscode.ConfigurationTarget.Workspace);
225+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
226+
try { await vscode.workspace.fs.delete(vscode.Uri.file(tmpBase), { recursive: true }); } catch { /* ignore */ }
227+
});
228+
229+
test('happy: ShowEntryForTodayCommand creates today\'s entry file', async () => {
230+
const input = new J.Model.Input(0);
231+
const cmd = new (ShowEntryForTodayCommand as any)(ctrl) as ShowEntryForTodayCommand;
232+
await cmd.execute(input);
233+
234+
const today = new Date();
235+
const year = String(today.getFullYear());
236+
const month = String(today.getMonth() + 1).padStart(2, '0');
237+
const day = String(today.getDate()).padStart(2, '0');
238+
const expected = vscode.Uri.file(path.join(tmpBase, year, month, `${day}.md`));
239+
240+
assert.ok(await fileExists(expected), `today's entry should exist at ${expected.fsPath}`);
241+
assert.strictEqual(logger.errors.length, 0, `unexpected errors: ${JSON.stringify(logger.errors)}`);
242+
});
243+
244+
test('happy: ShowEntryForYesterdayCommand creates yesterday\'s entry file', async () => {
245+
const input = new J.Model.Input(-1);
246+
const cmd = new (ShowEntryForYesterdayCommand as any)(ctrl) as ShowEntryForYesterdayCommand;
247+
await cmd.execute(input);
248+
249+
const yesterday = new Date();
250+
yesterday.setDate(yesterday.getDate() - 1);
251+
const year = String(yesterday.getFullYear());
252+
const month = String(yesterday.getMonth() + 1).padStart(2, '0');
253+
const day = String(yesterday.getDate()).padStart(2, '0');
254+
const expected = vscode.Uri.file(path.join(tmpBase, year, month, `${day}.md`));
255+
256+
assert.ok(await fileExists(expected), `yesterday's entry should exist at ${expected.fsPath}`);
257+
assert.strictEqual(logger.errors.length, 0, `unexpected errors: ${JSON.stringify(logger.errors)}`);
258+
});
259+
260+
test('happy: ShowEntryForTomorrowCommand creates tomorrow\'s entry file', async () => {
261+
const input = new J.Model.Input(1);
262+
const cmd = new (ShowEntryForTomorrowCommand as any)(ctrl) as ShowEntryForTomorrowCommand;
263+
await cmd.execute(input);
264+
265+
const tomorrow = new Date();
266+
tomorrow.setDate(tomorrow.getDate() + 1);
267+
const year = String(tomorrow.getFullYear());
268+
const month = String(tomorrow.getMonth() + 1).padStart(2, '0');
269+
const day = String(tomorrow.getDate()).padStart(2, '0');
270+
const expected = vscode.Uri.file(path.join(tmpBase, year, month, `${day}.md`));
271+
272+
assert.ok(await fileExists(expected), `tomorrow's entry should exist at ${expected.fsPath}`);
273+
assert.strictEqual(logger.errors.length, 0, `unexpected errors: ${JSON.stringify(logger.errors)}`);
274+
});
275+
276+
test('error: reader throws — showError is called', async () => {
277+
let errorCalled = false;
278+
(ctrl.reader as any).loadEntryForInput = async () => { throw new Error('simulated reader failure'); };
279+
(ctrl.ui as any).showError = async (_msg: string) => { errorCalled = true; };
280+
281+
const input = new J.Model.Input(0);
282+
const cmd = new (ShowEntryForTodayCommand as any)(ctrl) as ShowEntryForTodayCommand;
283+
await cmd.execute(input);
284+
285+
assert.ok(errorCalled, 'expected showError to be called when reader throws');
286+
});
287+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import * as assert from 'assert';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import * as vscode from 'vscode';
5+
import * as J from '../..';
6+
import { TestLogger } from '../test-logger';
7+
8+
async function buildCtrl(tmpBase: string): Promise<{ ctrl: J.Util.Ctrl; logger: TestLogger }> {
9+
const config = vscode.workspace.getConfiguration('journal');
10+
await config.update('base', tmpBase, vscode.ConfigurationTarget.Workspace);
11+
const refreshed = vscode.workspace.getConfiguration('journal');
12+
const ctrl = new J.Util.Ctrl(refreshed);
13+
const logger = new TestLogger(false);
14+
ctrl.logger = logger;
15+
return { ctrl, logger };
16+
}
17+
18+
suite('Inject — memo and task insertion', function () {
19+
this.slow(5000);
20+
21+
let originalBase: string | undefined;
22+
let tmpBase: string;
23+
let ctrl: J.Util.Ctrl;
24+
let logger: TestLogger;
25+
26+
setup(async () => {
27+
const config = vscode.workspace.getConfiguration('journal');
28+
originalBase = config.get<string>('base');
29+
tmpBase = path.join(os.tmpdir(), `inject-base-${Date.now()}`);
30+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(tmpBase));
31+
({ ctrl, logger } = await buildCtrl(tmpBase));
32+
});
33+
34+
teardown(async () => {
35+
const config = vscode.workspace.getConfiguration('journal');
36+
await config.update('base', originalBase, vscode.ConfigurationTarget.Workspace);
37+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
38+
try { await vscode.workspace.fs.delete(vscode.Uri.file(tmpBase), { recursive: true }); } catch { /* ignore */ }
39+
});
40+
41+
test('happy: memo text is injected into today\'s entry', async () => {
42+
const doc = await ctrl.reader.loadEntryForDay(new Date());
43+
assert.ok(doc, 'expected today\'s entry document');
44+
45+
const input = new J.Model.Input(0);
46+
input.flags = 'memo';
47+
input.text = 'lorem ipsum memo';
48+
49+
await ctrl.inject.injectInput(doc, input);
50+
51+
// applyEdit modifies the in-memory document; getText() reflects the change without a disk save
52+
const content = doc.getText();
53+
assert.ok(content.includes('lorem ipsum memo'), `memo not found in entry. Content: ${content}`);
54+
assert.strictEqual(logger.errors.length, 0, `unexpected errors: ${JSON.stringify(logger.errors)}`);
55+
});
56+
57+
test('happy: task text is injected into today\'s entry', async () => {
58+
const doc = await ctrl.reader.loadEntryForDay(new Date());
59+
assert.ok(doc, 'expected today\'s entry document');
60+
61+
const input = new J.Model.Input(0);
62+
input.flags = 'task';
63+
input.text = 'implement something';
64+
65+
await ctrl.inject.injectInput(doc, input);
66+
67+
const content = doc.getText();
68+
assert.ok(content.includes('implement something'), `task text not found in entry. Content: ${content}`);
69+
assert.ok(content.includes('[]') || content.includes('[ ]'), `task bullet not found in entry. Content: ${content}`);
70+
assert.strictEqual(logger.errors.length, 0, `unexpected errors: ${JSON.stringify(logger.errors)}`);
71+
});
72+
73+
test('error: empty memo text — injectInput returns early, no error logged, doc unchanged', async () => {
74+
const doc = await ctrl.reader.loadEntryForDay(new Date());
75+
const contentBefore = doc.getText();
76+
77+
const input = new J.Model.Input(0);
78+
// hasMemo() requires text.length > 0 — empty text causes early return
79+
input.flags = 'memo';
80+
input.text = '';
81+
82+
const result = await ctrl.inject.injectInput(doc, input);
83+
84+
assert.ok(result, 'injectInput should return the document');
85+
assert.strictEqual(logger.errors.length, 0, `unexpected errors: ${JSON.stringify(logger.errors)}`);
86+
assert.strictEqual(result.getText(), contentBefore, 'document should be unchanged for empty memo');
87+
});
88+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as assert from 'assert';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import * as vscode from 'vscode';
5+
import * as J from '../..';
6+
import { ShowNoteCommand } from '../../provider/commands/show-note';
7+
import { TestLogger } from '../test-logger';
8+
import { fileExists } from '../../util/fs-exists';
9+
10+
async function buildCtrl(tmpBase: string): Promise<{ ctrl: J.Util.Ctrl; logger: TestLogger }> {
11+
const config = vscode.workspace.getConfiguration('journal');
12+
await config.update('base', tmpBase, vscode.ConfigurationTarget.Workspace);
13+
const refreshed = vscode.workspace.getConfiguration('journal');
14+
const ctrl = new J.Util.Ctrl(refreshed);
15+
const logger = new TestLogger(false);
16+
ctrl.logger = logger;
17+
return { ctrl, logger };
18+
}
19+
20+
suite('ShowNoteCommand — note creation', function () {
21+
this.slow(5000);
22+
23+
let originalBase: string | undefined;
24+
let tmpBase: string;
25+
let ctrl: J.Util.Ctrl;
26+
let logger: TestLogger;
27+
28+
setup(async () => {
29+
const config = vscode.workspace.getConfiguration('journal');
30+
originalBase = config.get<string>('base');
31+
tmpBase = path.join(os.tmpdir(), `note-base-${Date.now()}`);
32+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(tmpBase));
33+
({ ctrl, logger } = await buildCtrl(tmpBase));
34+
});
35+
36+
teardown(async () => {
37+
const config = vscode.workspace.getConfiguration('journal');
38+
await config.update('base', originalBase, vscode.ConfigurationTarget.Workspace);
39+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
40+
try { await vscode.workspace.fs.delete(vscode.Uri.file(tmpBase), { recursive: true }); } catch { /* ignore */ }
41+
});
42+
43+
test('happy: note file is created and shown when user provides a title', async () => {
44+
let shownDoc: vscode.TextDocument | undefined;
45+
(ctrl.ui as any).getUserInput = async (_tip: string) => 'my test note';
46+
(ctrl.ui as any).showDocument = async (doc: vscode.TextDocument) => {
47+
shownDoc = doc;
48+
return undefined;
49+
};
50+
51+
const cmd = new (ShowNoteCommand as any)(ctrl) as ShowNoteCommand;
52+
await cmd.execute();
53+
54+
assert.ok(shownDoc, 'expected showDocument to be called');
55+
assert.ok(await fileExists(shownDoc!.uri), `note file should exist at ${shownDoc!.uri.fsPath}`);
56+
assert.strictEqual(logger.errors.length, 0, `unexpected errors: ${JSON.stringify(logger.errors)}`);
57+
});
58+
59+
test('error: showError called when getUserInput throws (user cancels)', async () => {
60+
let errorCalled = false;
61+
(ctrl.ui as any).getUserInput = async (_tip: string) => { throw new Error('user cancelled'); };
62+
(ctrl.ui as any).showError = async (_msg: string) => { errorCalled = true; };
63+
64+
const cmd = new (ShowNoteCommand as any)(ctrl) as ShowNoteCommand;
65+
await cmd.execute();
66+
67+
assert.ok(errorCalled, 'expected showError to be called on cancellation');
68+
});
69+
});

0 commit comments

Comments
 (0)