Release Time Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 05:05:59 +02:00
parent 3c82e7a305
commit 5e462f881d
30 changed files with 1830 additions and 110 deletions
+60 -1
View File
@@ -43,6 +43,65 @@ test("converts a negative nanosecond epoch exactly", async ({ page }) => {
expect(external).toEqual([]);
});
test("inspects imported recurrence and keeps calendar URLs inert", async ({
page,
}) => {
const external = await localOnly(page);
await page.goto("/deep/nested/time/");
await page.getByRole("button", { name: "iCalendar" }).click();
await page
.getByLabel("Paste iCalendar source")
.fill(
[
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Browser fixture//EN",
"BEGIN:VEVENT",
"UID:fixture@example",
"DTSTART:20260901T090000Z",
"RRULE:FREQ=DAILY;COUNT=2",
"SUMMARY:Fixture",
"URL:https://example.invalid/never-fetch",
"END:VEVENT",
"END:VCALENDAR",
].join("\r\n"),
);
await page
.getByRole("button", { name: "Validate and inspect calendar" })
.click();
await expect(
page.getByRole("heading", { name: "iCalendar inspection" }),
).toBeVisible();
await expect(page.locator(".result > pre")).toContainText('"inert": true');
await expect(page.locator(".result > pre")).toContainText('"occurrences": [');
expect(external).toEqual([]);
});
test("plans cross-zone meetings and previews an explicit cron dialect", async ({
page,
}) => {
await page.goto("/deep/nested/time/");
await page.getByRole("button", { name: "Meeting planner" }).click();
await page.getByLabel("Search from instant").fill("2026-09-01T00:00:00Z");
await page
.getByRole("button", { name: "Find shared work-hour slots" })
.click();
await expect(
page.getByRole("heading", { name: "Meeting candidates" }),
).toBeVisible();
await expect(page.locator(".result > pre")).toContainText("Europe/Berlin");
await expect(page.locator(".result > pre")).toContainText("America/New_York");
await page.getByRole("button", { name: "Cron & RRULE" }).click();
await page.getByLabel("Cron dialect").selectOption("github-actions");
await page.getByLabel("Time zone").fill("UTC");
await page.getByRole("button", { name: "Preview cron" }).click();
await expect(page.locator(".result > pre")).toContainText("github-actions");
await expect(page.locator(".result > pre")).toContainText(
"normalizedPattern",
);
});
test("serves the release identity and hardened headers", async ({
request,
}) => {
@@ -55,7 +114,7 @@ test("serves the release identity and hardened headers", async ({
const manifest = await request.get("/deep/nested/time/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.time-tools",
version: "0.1.0",
version: "0.2.0",
entry: "./",
});
});
+18
View File
@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";
test("keeps the primary workspace inside a narrow viewport", async ({
page,
}) => {
await page.goto("/deep/nested/time/");
await expect(page.locator("main").first()).toBeVisible();
await expect(
page.locator("main .loading, main .workbench-loading"),
).toHaveCount(0);
const widths = await page.evaluate(() => ({
content: document.documentElement.scrollWidth,
viewport: document.documentElement.clientWidth,
}));
expect(widths.viewport).toBeLessThanOrEqual(430);
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
});
+225 -1
View File
@@ -2,7 +2,13 @@ import { describe, expect, it } from "vitest";
import { addBusinessDays, compareArithmetic } from "../../src/time/arithmetic";
import { inspectEpoch } from "../../src/time/epoch";
import { createUtcEvent, foldIcsLine } from "../../src/time/ics";
import { previewCron, previewRRule } from "../../src/time/recurrence";
import { inspectIcs } from "../../src/time/ics-inspect";
import { planMeetings } from "../../src/time/meeting";
import {
previewCron,
previewCronDialect,
previewRRule,
} from "../../src/time/recurrence";
import {
compareTimeZones,
nextTransitions,
@@ -102,6 +108,100 @@ describe("bounded recurrence previews", () => {
expect(result).toHaveLength(3);
expect(result[0]?.zoned).toContain("2026-09-01T09:00:00");
});
it("makes cron dialect field and timezone semantics explicit", () => {
const unix = previewCronDialect(
"0 9 * * MON-FRI",
"2026-09-01T00:00:00Z",
"Europe/Berlin",
2,
"unix-vixie",
);
expect(unix).toMatchObject({
dialect: "unix-vixie",
dayCombination: "or",
});
expect(unix.occurrences[0]?.zoned).toContain("T09:00:00");
expect(() =>
previewCronDialect(
"0 9 * * *",
"2026-09-01T00:00:00Z",
"Europe/Berlin",
1,
"github-actions",
),
).toThrow(/UTC only/u);
expect(
previewCronDialect(
"* * * * *",
"2026-09-01T00:00:00Z",
"UTC",
2,
"github-actions",
).warnings.join(" "),
).toMatch(/five-minute minimum/u);
const aws = previewCronDialect(
"0 9 ? * MON-FRI 2026",
"2026-09-01T00:00:00Z",
"UTC",
1,
"aws-eventbridge",
);
expect(aws.normalizedPattern).toBe("0 0 9 ? * MON-FRI 2026");
expect(aws.dayCombination).toBe("exclusive-question-mark");
});
});
describe("time-zone meeting planning", () => {
it("finds only shared recurring local work-hour windows", () => {
const result = planMeetings({
start: "2026-09-01T00:00:00Z",
days: 2,
durationMinutes: 60,
stepMinutes: 30,
maxResults: 2,
participants: [
{
name: "Berlin",
timeZone: "Europe/Berlin",
workStart: "09:00",
workEnd: "17:00",
},
{
name: "New York",
timeZone: "America/New_York",
workStart: "09:00",
workEnd: "17:00",
},
],
});
expect(result.candidates).toHaveLength(2);
expect(
result.candidates[0]?.participants.map((item) => item.startLocal),
).toEqual([
expect.stringContaining("T15:00:00"),
expect.stringContaining("T09:00:00"),
]);
});
it("bounds participants and rejects overnight work-window ambiguity", () => {
expect(() =>
planMeetings({
start: "2026-09-01T00:00:00Z",
days: 1,
durationMinutes: 60,
stepMinutes: 30,
participants: [
{
name: "Night",
timeZone: "UTC",
workStart: "22:00",
workEnd: "06:00",
},
],
}),
).toThrow(/end after/u);
});
});
describe("iCalendar output", () => {
@@ -129,3 +229,127 @@ describe("iCalendar output", () => {
).toBeLessThanOrEqual(75);
});
});
describe("iCalendar import and inspection", () => {
it("imports multiple events and applies exclusions and moved/cancelled overrides", () => {
const result = inspectIcs(
[
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Fixture//EN",
"BEGIN:VEVENT",
"UID:series@example",
"DTSTART;TZID=Europe/Berlin:20260901T090000",
"RRULE:FREQ=DAILY;COUNT=4",
"EXDATE;TZID=Europe/Berlin:20260902T090000",
"SUMMARY:Master",
"END:VEVENT",
"BEGIN:VEVENT",
"UID:series@example",
"RECURRENCE-ID;TZID=Europe/Berlin:20260903T090000",
"DTSTART;TZID=Europe/Berlin:20260903T110000",
"SUMMARY:Moved",
"END:VEVENT",
"BEGIN:VEVENT",
"UID:series@example",
"RECURRENCE-ID;TZID=Europe/Berlin:20260904T090000",
"DTSTART;TZID=Europe/Berlin:20260904T090000",
"STATUS:CANCELLED",
"SUMMARY:Cancelled",
"END:VEVENT",
"BEGIN:VEVENT",
"UID:single@example",
"DTSTART:20260905T120000Z",
"SUMMARY:Single",
"URL:https://example.invalid/private",
"ATTACH;FMTTYPE=application/pdf:https://example.invalid/file.pdf",
"END:VEVENT",
"END:VCALENDAR",
"",
].join("\r\n"),
20,
);
expect(result.events).toHaveLength(4);
expect(result.occurrences.map((item) => item.summary)).toEqual([
"Master",
"Moved",
"Single",
]);
expect(result.occurrences[1]).toMatchObject({ source: "override" });
expect(result.references).toHaveLength(2);
expect(result.references.every((reference) => reference.inert)).toBe(true);
});
it("inspects VTIMEZONE while withholding unknown custom-zone expansion", () => {
const result = inspectIcs(
[
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Fixture//EN",
"BEGIN:VTIMEZONE",
"TZID:Custom/Office",
"BEGIN:STANDARD",
"DTSTART:19700101T000000",
"TZOFFSETFROM:+0100",
"TZOFFSETTO:+0100",
"END:STANDARD",
"END:VTIMEZONE",
"BEGIN:VEVENT",
"UID:custom@example",
"DTSTART;TZID=Custom/Office:20260901T090000",
"RRULE:FREQ=DAILY;COUNT=2",
"SUMMARY:Custom zone",
"ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png:AAAA",
"END:VEVENT",
"END:VCALENDAR",
].join("\r\n"),
);
expect(result.timeZones[0]).toMatchObject({
tzid: "Custom/Office",
usableByBrowser: false,
});
expect(result.references[0]).toMatchObject({
kind: "embedded-binary",
encodedCharacters: 4,
});
expect(result.diagnostics.map((item) => item.code)).toContain(
"custom-vtimezone",
);
expect(result.occurrences).toEqual([]);
});
it("expands all-day recurrences without pretending they are instants", () => {
const result = inspectIcs(
[
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Fixture//EN",
"BEGIN:VEVENT",
"UID:days@example",
"DTSTART;VALUE=DATE:20260901",
"RRULE:FREQ=DAILY;COUNT=3",
"EXDATE;VALUE=DATE:20260902",
"SUMMARY:All day",
"END:VEVENT",
"END:VCALENDAR",
].join("\r\n"),
);
expect(result.occurrences.map((item) => item.start)).toEqual([
"2026-09-01 (all-day)",
"2026-09-03 (all-day)",
]);
});
it("rejects mismatched and excessively nested components", () => {
expect(() => inspectIcs("BEGIN:VCALENDAR\r\nEND:VEVENT\r\n")).toThrow(
/Mismatched/u,
);
const nested = [
"BEGIN:VCALENDAR",
...Array(17).fill("BEGIN:X"),
...Array(17).fill("END:X"),
"END:VCALENDAR",
].join("\r\n");
expect(() => inspectIcs(nested)).toThrow(/nesting limit/u);
});
});