switch to ical.js and cleanup obsolete files - v0.4.6 [skip-ci]
All checks were successful
Build SilverBullet Plug / build (push) Successful in 11s

This commit is contained in:
2026-02-21 16:05:54 -08:00
parent 30731c7752
commit 139ab71db7
26 changed files with 174 additions and 454 deletions

View File

@@ -1,80 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('iCalendar Sync E2E', () => {
test('should verify iCalendar sync activity', async ({ page }) => {
const logs: string[] = [];
const errors: string[] = [];
page.on('console', msg => {
const text = msg.text();
if (msg.type() === 'error') errors.push(text);
if (text.includes('[iCalendar]')) {
logs.push(text);
console.log('Detected SB Log:', text);
}
});
// 1. Load Editor
console.log('Navigating to /');
await page.goto('/');
await page.waitForLoadState('networkidle');
console.log('Page reached, waiting for boot sequence...');
// 2. Persistent Monitoring for Sync Activity
let syncDetected = false;
let eventsSynced = 0;
const timeoutMs = 120000; // 2 minutes
const startTime = Date.now();
console.log(`Starting monitoring loop for ${timeoutMs/1000}s...`);
while (Date.now() - startTime < timeoutMs) {
// Check for notifications
const notification = page.locator('.sb-notification:has-text("Synced")');
if (await notification.count() > 0) {
const text = await notification.innerText();
console.log('Detected Sync Notification:', text);
const match = text.match(/Synced (\d+) events/);
if (match) {
eventsSynced = parseInt(match[1], 10);
if (eventsSynced > 0) {
syncDetected = true;
console.log(`SUCCESS: ${eventsSynced} events synced!`);
break;
}
}
}
// Every 30 seconds, try to "poke" it with a keyboard shortcut if not started
const elapsed = Date.now() - startTime;
if (elapsed > 30000 && elapsed < 35000 && !syncDetected) {
console.log('Auto-sync not detected yet, trying manual trigger shortcut...');
await page.keyboard.press('.');
await page.waitForTimeout(1000);
await page.keyboard.type('iCalendar: Sync');
await page.keyboard.press('Enter');
}
await page.waitForTimeout(2000);
}
// 3. Final verification
console.log('Final accumulated [iCalendar] logs:', logs);
// Check if the query rendered meetings in the UI
const meetingItems = page.locator('li:has-text("to"):has-text(":")');
const meetingCount = await meetingItems.count();
console.log(`Meetings found in UI: ${meetingCount}`);
// Filter out expected noise
const relevantErrors = errors.filter(e => !e.includes('401') && !e.includes('favicon'));
expect(relevantErrors, `Found unexpected errors: ${relevantErrors[0]}`).toHaveLength(0);
expect(syncDetected, 'iCalendar sync failed or synced 0 events').toBe(true);
expect(eventsSynced).toBeGreaterThan(0);
// Verify query rendering
expect(meetingCount).toBeGreaterThanOrEqual(12);
console.log('Test Passed.');
});
});

View File

@@ -1,6 +1,6 @@
import { assertEquals, assert } from "jsr:@std/assert";
import { convertIcsCalendar } from "https://esm.sh/ts-ics@2.4.0";
import { expandRecurrences, resolveEventStart, localDateString } from "../icalendar.ts";
import ICAL from "ical.js";
import { expandRecurrences, resolveEventStart, dateToTimezoneString } from "../icalendar.ts";
Deno.test("Integration - parse and expand real-world ICS samples", async () => {
const testDataDir = "./test_data";
@@ -39,30 +39,39 @@ Deno.test("Integration - parse and expand real-world ICS samples", async () => {
for (const file of files) {
console.log(` Testing file: ${file}`);
const text = await Deno.readTextFile(file);
const calendar = convertIcsCalendar(undefined, text);
const jcalData = ICAL.parse(text);
const vcalendar = new ICAL.Component(jcalData);
const vevents = vcalendar.getAllSubcomponents("vevent");
assert(calendar && calendar.events, `Failed to parse ${file}`);
assert(vevents.length > 0, `Failed to parse ${file} or no events found`);
for (const icsEvent of calendar.events) {
if (icsEvent.status?.toUpperCase() === "CANCELLED") continue;
for (const vevent of vevents) {
const icsEvent = new ICAL.Event(vevent);
const status = vevent.getFirstPropertyValue("status") as string | null;
if (status?.toUpperCase() === "CANCELLED") continue;
const finalDate = await resolveEventStart(icsEvent);
if (!finalDate) continue;
const startDateUTC = icsEvent.startDate.toJSDate();
const summary = icsEvent.summary;
const rrule = vevent.getFirstPropertyValue("rrule");
const exdates = vevent.getAllProperties("exdate").map((p: any) => p.getFirstValue().toJSDate().toISOString());
const localIso = localDateString(finalDate);
const localIso = dateToTimezoneString(startDateUTC, "UTC");
const baseEvent = {
...icsEvent,
name: icsEvent.summary || "Untitled Event",
start: localIso,
summary,
name: summary || "Untitled Event",
start: startDateUTC.toISOString(),
startLocal: localIso,
tag: "ical-event",
sourceName: "IntegrationTest"
sourceName: "IntegrationTest",
rrule: rrule ? rrule.toString() : undefined,
exdate: exdates.length > 0 ? exdates : undefined
};
try {
const expanded = expandRecurrences(baseEvent, 30);
assert(expanded.length >= 1, `Expected at least 1 occurrence for event "${icsEvent.summary}" in ${file}`);
assert(expanded.length >= 1, `Expected at least 1 occurrence for event "${summary}" in ${file}`);
} catch (err) {
console.error(`❌ Error expanding recurrence for event "${icsEvent.summary}" in ${file}:`, err);
console.error(`❌ Error expanding recurrence for event "${summary}" in ${file}:`, err);
throw err;
}
}

View File

@@ -1,5 +1,5 @@
import { assertEquals, assert } from "jsr:@std/assert";
import { resolveEventStart, expandRecurrences, localDateString } from "../icalendar.ts";
import { resolveEventStart, expandRecurrences, dateToTimezoneString } from "../icalendar.ts";
const TEST_NOW = new Date("2026-01-20T12:00:00Z");
@@ -42,24 +42,26 @@ Deno.test("Variation: Transparent Meeting (Free)", async () => {
});
Deno.test("Variation: Recurring Weekly (Multi-day: MO,TU,WE,TH,FR)", () => {
const start = new Date("2026-01-16T13:00:00");
const icsEvent = {
summary: "BUSY Weekly",
start: "2026-01-16T13:00:00",
start: start.toISOString(),
rrule: "FREQ=WEEKLY;UNTIL=20260814T170000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;WKST=SU"
};
// Use TEST_NOW to ensure the window matches
const results = expandRecurrences(icsEvent, 7, TEST_NOW);
const results = expandRecurrences(icsEvent, 7, "UTC", TEST_NOW);
// Should have multiple occurrences per week
assert(results.length > 1);
assert(results.some(r => r.start.includes("2026-01-19"))); // Monday
assert(results.some(r => r.start.includes("2026-01-20"))); // Tuesday
assert(results.some(r => r.startLocal.includes("2026-01-19"))); // Monday
assert(results.some(r => r.startLocal.includes("2026-01-20"))); // Tuesday
});
Deno.test("Variation: Recurring with WORKWEEKSTART (Outlook style)", () => {
const start = new Date("2026-01-20T08:30:00");
const icsEvent = {
summary: "Outlook Style Meeting",
start: "2026-01-20T08:30:00",
start: start.toISOString(),
rrule: {
frequency: "WEEKLY",
interval: 1,
@@ -68,40 +70,42 @@ Deno.test("Variation: Recurring with WORKWEEKSTART (Outlook style)", () => {
}
};
const results = expandRecurrences(icsEvent, 30, TEST_NOW);
const results = expandRecurrences(icsEvent, 30, "UTC", TEST_NOW);
assert(results.length > 0);
assert(results[0].start.includes("2026-01-20"));
assert(results[0].startLocal.includes("2026-01-20"));
});
Deno.test("Variation: Recurring with EXDATE (Exclusion)", () => {
const start = new Date("2026-01-20T08:30:00");
const icsEvent = {
summary: "HPE-Veeam check-in",
start: "2026-01-20T08:30:00",
start: start.toISOString(),
rrule: "FREQ=WEEKLY;UNTIL=20260324T143000Z;INTERVAL=1;BYDAY=TU;WKST=SU",
exdate: ["2026-02-03T08:30:00"]
};
const results = expandRecurrences(icsEvent, 60, TEST_NOW);
const dates = results.map(r => r.start);
assert(dates.includes("2026-01-20T08:30:00"));
assert(dates.includes("2026-01-27T08:30:00"));
assert(!dates.includes("2026-02-03T08:30:00"), "EXDATE should be excluded");
assert(dates.includes("2026-02-10T08:30:00"));
const results = expandRecurrences(icsEvent, 60, "UTC", TEST_NOW);
const dates = results.map(r => r.startLocal);
assert(dates.some(d => d.includes("2026-01-20T")), "Should find first occurrence");
assert(dates.some(d => d.includes("2026-01-27T")), "Should find second occurrence");
assert(!dates.some(d => d.includes("2026-02-03T")), "EXDATE 2026-02-03 should be excluded");
assert(dates.some(d => d.includes("2026-02-10T")), "Should find fourth occurrence");
});
Deno.test("Variation: Monthly Recurring (Last Friday)", () => {
const start = new Date("2026-01-30T10:00:00");
const icsEvent = {
summary: "Monthly Planning",
start: "2026-01-30T10:00:00", // This is the last Friday of Jan 2026
start: start.toISOString(), // This is the last Friday of Jan 2026
rrule: "FREQ=MONTHLY;UNTIL=20260731T170000Z;INTERVAL=1;BYDAY=-1FR"
};
const results = expandRecurrences(icsEvent, 100, TEST_NOW);
const dates = results.map(r => r.start);
const results = expandRecurrences(icsEvent, 100, "UTC", TEST_NOW);
const dates = results.map(r => r.startLocal);
assert(dates.includes("2026-01-30T10:00:00"));
assert(dates.includes("2026-02-27T10:00:00")); // Last Friday of Feb 2026
assert(dates.includes("2026-03-27T10:00:00")); // Last Friday of Mar 2026
assert(dates.some(d => d.includes("2026-01-30")), "Should find Jan occurrence");
assert(dates.some(d => d.includes("2026-02-27")), "Should find Feb occurrence");
assert(dates.some(d => d.includes("2026-03-27")), "Should find Mar occurrence");
});
Deno.test("Variation: Tentative Meeting", async () => {
@@ -134,13 +138,13 @@ Deno.test("Feature: Unlimited lookback window", () => {
const start = new Date(TEST_NOW.getTime() - 500 * 86400000); // 500 days ago
const icsEvent = {
summary: "Event from 500 days ago",
start: localDateString(start),
start: start.toISOString(),
rrule: "FREQ=DAILY;COUNT=1000"
};
const results = expandRecurrences(icsEvent, 30, TEST_NOW);
const results = expandRecurrences(icsEvent, 30, "UTC", TEST_NOW);
// Should include events from 500 days ago because there is now no limit
assert(results.some(r => r.start === localDateString(start)), "Should find occurrence from 500 days ago");
assert(results.some(r => r.startLocal === dateToTimezoneString(start, "UTC")), "Should find occurrence from 500 days ago");
});
Deno.test("Feature: Hash Collision Prevention (Same UID/Start, Different Summary)", async () => {