116 lines
5.9 KiB
JavaScript
116 lines
5.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
const core = await import('../core.js').catch(error => {
|
|
if (error.code === 'ERR_MODULE_NOT_FOUND') return {};
|
|
throw error;
|
|
});
|
|
|
|
function invoke(name, ...args) {
|
|
assert.equal(typeof core[name], 'function', `${name} is required for renewal calculations`);
|
|
return core[name](...args);
|
|
}
|
|
|
|
test('days advance across a year boundary', () => {
|
|
assert.equal(invoke('addDays', '2026-12-31', 2), '2027-01-02');
|
|
});
|
|
|
|
test('calendar months clamp January 31 to the last day of February', () => {
|
|
assert.equal(invoke('addCycle', '2026-01-31', 1, 'months'), '2026-02-28');
|
|
});
|
|
|
|
test('calendar months respect leap years', () => {
|
|
assert.equal(invoke('addCycle', '2028-01-31', 1, 'months'), '2028-02-29');
|
|
});
|
|
|
|
test('days use whole business dates independent of daylight saving time', () => {
|
|
assert.equal(invoke('daysUntil', '2026-03-09', '2026-03-07'), 2);
|
|
assert.equal(invoke('daysUntil', '2026-10-03', '2026-10-04'), -1);
|
|
});
|
|
|
|
test('completion-based renewal starts from the actual completion date', () => {
|
|
const rule = {dueDate:'2026-10-03', interval:90, unit:'days', anchor:'completion', anchorDate:'2026-07-05'};
|
|
assert.equal(invoke('nextAfterCompletion', rule, '2026-10-04'), '2027-01-02');
|
|
});
|
|
|
|
test('early fixed renewal advances one scheduled cycle rather than retaining the completed deadline', () => {
|
|
const rule = {dueDate:'2026-10-10', interval:1, unit:'months', anchor:'scheduled', anchorDate:'2026-10-10'};
|
|
assert.equal(invoke('nextAfterCompletion', rule, '2026-10-04'), '2026-11-10');
|
|
});
|
|
|
|
test('overdue fixed renewal skips elapsed cycles', () => {
|
|
const rule = {dueDate:'2026-01-31', interval:1, unit:'months', anchor:'scheduled', anchorDate:'2026-01-31'};
|
|
assert.equal(invoke('nextAfterCompletion', rule, '2026-04-01'), '2026-04-30');
|
|
});
|
|
|
|
test('fixed monthly schedules restore the original day after a short month', () => {
|
|
const rule = {dueDate:'2026-02-28', interval:1, unit:'months', anchor:'scheduled', anchorDate:'2026-01-31'};
|
|
assert.equal(invoke('nextAfterCompletion', rule, '2026-02-28'), '2026-03-31');
|
|
});
|
|
|
|
test('invalid dates and cycles are rejected instead of silently overflowing', () => {
|
|
assert.throws(() => invoke('addCycle', '2026-02-30', 1, 'months'), /日期/);
|
|
assert.throws(() => invoke('addCycle', '2026-10-04', 0, 'days'), /周期/);
|
|
assert.throws(() => invoke('addCycle', '2026-10-04', -1, 'days'), /周期/);
|
|
assert.throws(() => invoke('addCycle', '2026-10-04', 1.5, 'days'), /周期/);
|
|
});
|
|
|
|
test('recording one rule leaves another renewal rule untouched', () => {
|
|
const card = {id:'c1', activatedAt:'2026-01-01', rules:[
|
|
{id:'sms', dueDate:'2026-10-03', interval:90, unit:'days', anchor:'completion', anchorDate:'2026-10-03'},
|
|
{id:'plan', dueDate:'2026-10-20', interval:1, unit:'months', anchor:'scheduled', anchorDate:'2026-10-20'}
|
|
]};
|
|
const result = invoke('completeRenewal', card, 'sms', '2026-10-04', '2026-10-04', '发了一条短信');
|
|
assert.equal(result.card.rules[0].dueDate, '2027-01-02');
|
|
assert.equal(result.card.rules[1].dueDate, '2026-10-20');
|
|
assert.equal(card.rules[0].dueDate, '2026-10-03');
|
|
assert.equal(result.event.oldDueDate, '2026-10-03');
|
|
assert.equal(result.event.newDueDate, '2027-01-02');
|
|
});
|
|
|
|
test('completion cannot be in the future, before activation or before the last completion', () => {
|
|
const card = {activatedAt:'2026-02-01', rules:[{id:'r1', dueDate:'2026-10-20', lastCompletedAt:'2026-09-20', interval:30, unit:'days', anchor:'completion'}]};
|
|
assert.throws(() => invoke('completeRenewal', card, 'r1', '2026-10-05', '2026-10-04'), /未来/);
|
|
assert.throws(() => invoke('completeRenewal', card, 'r1', '2026-01-01', '2026-10-04'), /开通/);
|
|
assert.throws(() => invoke('completeRenewal', card, 'r1', '2026-09-01', '2026-10-04'), /上次/);
|
|
});
|
|
|
|
test('archived cards are excluded and remaining tasks sort by their actual deadline', () => {
|
|
const cards = [
|
|
{id:'a', archived:false, rules:[{id:'late',dueDate:'2026-10-20'},{id:'soon',dueDate:'2026-10-03'}]},
|
|
{id:'b', archived:true, rules:[{id:'archived',dueDate:'2026-09-01'}]},
|
|
{id:'c', archived:false, rules:[{id:'middle',dueDate:'2026-10-10'}]}
|
|
];
|
|
assert.deepEqual(invoke('getTasks', cards).map(task => task.rule.id), ['soon','middle','late']);
|
|
});
|
|
|
|
test('user timezone determines today around the UTC midnight boundary', () => {
|
|
const instant = new Date('2026-10-03T17:00:00Z');
|
|
assert.equal(invoke('todayString', 'Asia/Shanghai', instant), '2026-10-04');
|
|
assert.equal(invoke('todayString', 'America/New_York', instant), '2026-10-03');
|
|
});
|
|
|
|
test('editing activation cannot invalidate an older renewal event even when the latest completion is later', () => {
|
|
const card = {id:'c1', activatedAt:'2026-01-01', rules:[{dueDate:'2026-12-01',lastCompletedAt:'2026-10-04'}]};
|
|
const events = [
|
|
{cardId:'c1',completedAt:'2026-06-01'},
|
|
{cardId:'c1',completedAt:'2026-10-04'},
|
|
];
|
|
assert.throws(() => invoke('validateActivationChange', card, events, '2026-08-01', '2026-10-04'), /续期记录/);
|
|
});
|
|
|
|
test('editing activation ignores another card history and permits a correction before its own history', () => {
|
|
const card = {id:'c1',activatedAt:'2026-01-01',rules:[{dueDate:'2026-12-01'}]};
|
|
const events = [{cardId:'c2',completedAt:'2026-02-01'},{cardId:'c1',completedAt:'2026-06-01'}];
|
|
assert.doesNotThrow(() => invoke('validateActivationChange', card, events, '2026-05-01', '2026-10-04'));
|
|
});
|
|
|
|
test('HTTP browsers without randomUUID can still generate distinct valid card identifiers', () => {
|
|
const httpCrypto = {getRandomValues:globalThis.crypto.getRandomValues.bind(globalThis.crypto)};
|
|
const first = invoke('createId', httpCrypto);
|
|
const second = invoke('createId', httpCrypto);
|
|
assert.match(first, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
assert.match(second, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
assert.notEqual(first,second);
|
|
});
|