Files
hub/tools/chores-test.js
T
nmemmertandClaude Sonnet 5.5 0383da87f5
build-container / build (push) Successful in 4m6s
Chores, Family page, genre search, child accounts, wider Search and Notifications center
- Chores tab with repeats, points and a week/month board
- Family page: calendar, chores, shopping, notes, chat and tasks on one start page
- Genre search in the Store, Free Books and LibriVox
- Child accounts: safe content, daily limit and bedtime, ask a parent
- Search now covers notes, chat, contacts, chores, recipes and your books
- Notifications center: bell with a per-person list of chat, reminders and announcements
- Roadmap, README and tests updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 11:48:55 -04:00

40 lines
7.2 KiB
JavaScript

// Chores: when each kind of chore is due, who it is given to, ticking off and undoing, who may tick what, and the points for the week.
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-chores-'));
const users = require('../users'), chores = require('../chores');
const P = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['chores'], handle: 'Yg' }), c: users.create({ name: 'Cy', role: 'user', tabs: ['chores'], handle: 'Yw' }), d: users.create({ name: 'Di', role: 'user', tabs: ['music'], handle: 'ZA' }) };
const hub = http.createServer((req, res) => users.runAs(P[req.headers['x-user'] || 'a'], () => chores.handle(req, res))).listen(0, async () => {
const B = 'http://127.0.0.1:' + hub.address().port + '/api/chores', call = (p, o = {}, who) => fetch(B + p, { ...o, headers: { ...(o.headers || {}), ...(who ? { 'x-user': who } : {}) } }), post = (p, b, who) => call(p, { method: 'POST', body: JSON.stringify(b) }, who), j = r => r.json();
const today = chores.ymd(new Date()), board = async (who, date = today) => (await j(await call('/?date=' + date, {}, who))).board;
try {
// when chores are due (pure rules, on known dates: 2026-10-26 is a Monday)
const c = (o) => ({ active: true, ...o }), mon = '2026-10-26', sat = '2026-10-31', sun = '2026-11-01';
assert.equal(chores.dueKey(c({ repeat: 'daily' }), sat), sat); assert.equal(chores.dueKey(c({ repeat: 'weekdays' }), sat), null); assert.equal(chores.dueKey(c({ repeat: 'weekdays' }), mon), mon);
assert.equal(chores.dueKey(c({ repeat: 'weekly', days: [6] }), sat), sat); assert.equal(chores.dueKey(c({ repeat: 'weekly', days: [6] }), sun), null);
assert.equal(chores.dueKey(c({ repeat: 'weekly', days: [] }), sun), mon, 'a weekly chore with no chosen day is due all week, filed under its Monday'); assert.equal(chores.dueKey(c({ repeat: 'weekly', days: [] }), '2026-11-02'), '2026-11-02');
assert.equal(chores.dueKey(c({ repeat: 'monthly', dom: 15 }), '2026-10-14'), null); assert.equal(chores.dueKey(c({ repeat: 'monthly', dom: 15 }), '2026-10-20'), '2026-10', 'a monthly chore stays due after its day'); assert.equal(chores.dueKey(c({ repeat: 'monthly', dom: 31 }), '2026-11-30'), '2026-11', 'day 31 in a short month means the last day');
assert.equal(chores.dueKey(c({ repeat: 'once', date: '2026-10-28' }), mon), null); assert.equal(chores.dueKey(c({ repeat: 'once', date: '2026-10-28' }), '2026-11-03'), 'once'); assert.equal(chores.dueKey(c({ repeat: 'daily', active: false }), mon), null); assert.equal(chores.dueKey(c({ repeat: 'daily', start: '2026-11-01' }), mon), null);
// permissions and creating
assert.equal((await post('/', { title: 'Dishes' }, 'b')).status, 403, 'only an admin makes chores'); assert.equal((await post('/', { title: ' ' })).status, 400); assert.equal((await post('/', { title: 'Once', repeat: 'once' })).status, 400, 'a one-off needs a day');
const dishes = await j(await post('/', { title: 'Dishes', repeat: 'daily', assign: [P.b.id, P.c.id, P.d.id, 'ghost'], points: 3 })); assert.deepEqual(dishes.assign.sort(), [P.b.id, P.c.id].sort(), 'only people who have Chores');
const trash = await j(await post('/', { title: 'Take out the trash', repeat: 'daily', assign: 'anyone', points: 5 })); const lawn = await j(await post('/', { title: 'Mow', repeat: 'weekly', days: [0, 6, 9, 'x'], assign: [P.b.id], points: 10 })); assert.deepEqual(lawn.days, [0, 6]); assert.equal((await j(await post('/', { id: lawn.id, title: 'Mow the lawn', repeat: 'weekly', days: [6], assign: [P.b.id], points: 10 }))).title, 'Mow the lawn', 'a chore can be changed');
let bd = await board('b'); assert.equal(bd.filter(x => x.title === 'Dishes').length, 2, 'given to two people: each has their own'); assert.equal(bd.filter(x => x.title === 'Take out the trash').length, 1, 'an anyone chore is one');
assert.ok(bd.find(x => x.title === 'Dishes' && x.user === P.b.id).canDo && !bd.find(x => x.title === 'Dishes' && x.user === P.c.id).canDo, 'you can tick your own, not another person\'s'); assert.ok((await board('a')).every(x => x.canDo), 'an admin can tick any');
// ticking
assert.equal((await post('/done', { chore: dishes.id, date: today, user: P.c.id }, 'b')).status, 403, 'not for someone else'); assert.equal((await post('/done', { chore: dishes.id, date: today }, 'b')).status, 200);
bd = await board('c'); assert.equal(bd.find(x => x.title === 'Dishes' && x.user === P.b.id).done.byName, 'Bob'); assert.equal(bd.find(x => x.title === 'Dishes' && x.user === P.c.id).done, null, 'Cy still has his own to do'); assert.equal((await j(await post('/done', { chore: dishes.id, date: today }, 'b'))).already, true, 'ticking twice counts once');
assert.equal((await post('/done', { chore: trash.id, date: today }, 'c')).status, 200); bd = await board('b'); assert.equal(bd.find(x => x.title === 'Take out the trash').done.byName, 'Cy', 'an anyone chore: whoever does it gets it'); assert.equal((await post('/done', { chore: trash.id, date: today, undo: true }, 'b')).status, 403, 'only who ticked it (or an admin) takes it back'); assert.equal((await post('/done', { chore: trash.id, date: today, undo: true }, 'c')).status, 200); assert.equal((await board('b')).find(x => x.title === 'Take out the trash').done, null);
assert.equal((await post('/done', { chore: dishes.id, date: today, user: P.c.id }, 'a')).status, 200, 'an admin can tick for someone');
assert.equal((await post('/done', { chore: dishes.id, date: '2020-01-01' }, 'b')).status, 400, 'not years back'); assert.equal((await post('/done', { chore: dishes.id, user: P.d.id }, 'a')).status, 400, 'only to people it is given to');
// a one-off carries until done and is marked overdue
const once = await j(await post('/', { title: 'Fix the gate', repeat: 'once', date: chores.addDays(today, -2), assign: 'anyone', points: 8 })); let ob = (await board('b')).find(x => x.title === 'Fix the gate'); assert.equal(ob.overdue, true); await post('/done', { chore: once.id, date: today }, 'b'); ob = (await board('b')).find(x => x.title === 'Fix the gate'); assert.equal(ob.done.byName, 'Bob'); assert.equal(ob.overdue, false);
// points
const sm = await j(await call('/summary?range=week', {}, 'a')); const bob = sm.people.find(x => x.name === 'Bob'), cy = sm.people.find(x => x.name === 'Cy'); assert.equal(bob.points, 3 + 8, 'Bob: dishes 3 and the gate 8'); assert.equal(cy.points, 3, 'Cy: dishes ticked by an admin counts for Cy'); assert.equal(sm.people[0].name, 'Bob', 'most points first'); assert.equal(bob.perDay.length, 7); assert.equal(bob.perDay.at(-1), 2);
// delete removes its history
assert.equal((await call('/' + once.id, { method: 'DELETE' }, 'b')).status, 403); assert.equal((await call('/' + once.id, { method: 'DELETE' })).status, 200); assert.equal((await j(await call('/summary', {}, 'a'))).people.find(x => x.name === 'Bob').points, 3);
assert.equal((await call('/?date=nope')).status, 400);
console.log('chores ok');
} catch (e) { console.error(e); process.exitCode = 1; }
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);
});