- Each person can subscribe their dates in Google Calendar, Outlook or
Apple Calendar: GET /api/calendar/{token}.ics (no session) brings
their unfinished tickets with a due date (assigned or followed), the
planned and running sprints and the planned releases of their
projects, as all-day events linking to the web. The token is made,
renewed or dropped under /me/calendar (and in GraphQL); in the web,
from «Calendario» in «Mi trabajo».
- The backlog panel keeps the selected rows' background inside its
rounded corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
import { buildIcs } from './ics';
|
|
|
|
const NOW = new Date('2026-10-14T09:30:00.000Z');
|
|
|
|
describe('buildIcs', () => {
|
|
it('writes all-day events with an exclusive end', () => {
|
|
const ics = buildIcs(
|
|
'TaskStream',
|
|
[
|
|
{
|
|
uid: 'issue-1@taskstream',
|
|
start: '2026-10-20',
|
|
summary: 'WEB-1 · Login',
|
|
},
|
|
{
|
|
uid: 'sprint-1@taskstream',
|
|
start: '2026-10-05',
|
|
end: '2026-10-19',
|
|
summary: 'Sprint 4',
|
|
},
|
|
],
|
|
NOW,
|
|
);
|
|
expect(ics.startsWith('BEGIN:VCALENDAR\r\nVERSION:2.0\r\n')).toBe(true);
|
|
expect(ics.endsWith('END:VCALENDAR\r\n')).toBe(true);
|
|
expect(ics).toContain('DTSTAMP:20261014T093000Z');
|
|
expect(ics).toContain(
|
|
'DTSTART;VALUE=DATE:20261020\r\nDTEND;VALUE=DATE:20261021',
|
|
);
|
|
expect(ics).toContain(
|
|
'DTSTART;VALUE=DATE:20261005\r\nDTEND;VALUE=DATE:20261020',
|
|
);
|
|
expect(ics.split('BEGIN:VEVENT')).toHaveLength(3);
|
|
});
|
|
|
|
it('escapes text and skips what has no date', () => {
|
|
const ics = buildIcs(
|
|
'Ana; López, QA',
|
|
[
|
|
{ uid: 'a', start: '2026-10-20', summary: 'Pago; roto, en\nSafari' },
|
|
{ uid: 'b', start: '', summary: 'sin fecha' },
|
|
],
|
|
NOW,
|
|
);
|
|
expect(ics).toContain('X-WR-CALNAME:Ana\\; López\\, QA');
|
|
expect(ics).toContain('SUMMARY:Pago\\; roto\\, en\\nSafari');
|
|
expect(ics).not.toContain('sin fecha');
|
|
});
|
|
|
|
it('folds long lines at 75 octets without splitting a character', () => {
|
|
const summary = 'Revisión de la exportación á'.repeat(5);
|
|
const ics = buildIcs(
|
|
'x',
|
|
[{ uid: 'u', start: '2026-10-20', summary }],
|
|
NOW,
|
|
);
|
|
for (const line of ics.split('\r\n'))
|
|
expect(Buffer.byteLength(line, 'utf8')).toBeLessThanOrEqual(75);
|
|
// Unfolding gives the text back.
|
|
expect(ics.replace(/\r\n /g, '')).toContain(`SUMMARY:${summary}`);
|
|
});
|
|
});
|