Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions e2e/someday/drag-grid-event-to-sidebar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { expect, type Page, test } from "@playwright/test";
import {
createEventTitle,
ensureSidebarOpen,
expectSomedayEventVisible,
fillTitleAndSaveEventForm,
openSomedayEventFormWithMouse,
prepareCalendarPage,
} from "../utils/event-test-utils";

test.skip(
({ isMobile }) => isMobile,
"Mouse flows are desktop-only in week view.",
);

const sidebarButton = (page: Page, name: string) =>
page.locator("#sidebar").getByRole("button", { name });

const centerOf = async (
page: Page,
name: string,
scope: "sidebar" | "grid",
) => {
const locator =
scope === "sidebar"
? sidebarButton(page, name)
: page.locator("#mainGrid").getByRole("button", { name });
const box = await locator.boundingBox();

if (!box) {
throw new Error(`Expected ${scope} element "${name}" to have a box.`);
}

return { x: box.x + box.width / 2, y: box.y + box.height / 2, box };
};

const createSomeday = async (
page: Page,
section: "week" | "month",
prefix: string,
) => {
const title = createEventTitle(prefix);
await openSomedayEventFormWithMouse(page, section);
await fillTitleAndSaveEventForm(page, title);
await expectSomedayEventVisible(page, title);
return title;
};

// Creates a TALL timed event via vertical drag-select and waits for its
// optimistic/pending state to settle, so it can be reliably grabbed for a drag
// (a short, freshly-created event lands on the resize handle / refuses to drag).
const createTimed = async (page: Page, prefix: string) => {
const title = createEventTitle(prefix);
const grid = await page.locator("#mainGrid").boundingBox();

if (!grid) throw new Error("Expected the week grid to be visible.");

const gx = grid.x + grid.width * 0.4;
const gy = grid.y + grid.height * 0.25;

await page.mouse.move(gx, gy);
await page.mouse.down();
await page.mouse.move(gx, gy + 160, { steps: 12 });
await page.mouse.up();
await fillTitleAndSaveEventForm(page, title);

await expect(
page.locator("#mainGrid").getByRole("button", { name: title }),
).toBeVisible({ timeout: 8000 });
await page.waitForTimeout(2500);

return title;
};

test("drags a timed grid event into the Someday week list and reorders rows live", async ({
page,
}) => {
await prepareCalendarPage(page);
await ensureSidebarOpen(page);

const weekA = await createSomeday(page, "week", "Week A");
const weekB = await createSomeday(page, "week", "Week B");
const timed = await createTimed(page, "Timed T");

const start = await centerOf(page, timed, "grid");
const a = await centerOf(page, weekA, "sidebar");
const b = await centerOf(page, weekB, "sidebar");

// Aim for the seam between A and B in the week list.
const target = { x: a.x, y: (a.y + b.y) / 2 };

await page.mouse.move(start.x, start.y);
await page.mouse.down();
await page.mouse.move(target.x, target.y, { steps: 16 });
await page.waitForTimeout(250);

await page.screenshot({
path: "test-results/repro/week-list-mid-drag.png",
});

// The dragged event should appear in the sidebar between A and B (the existing
// rows opened a gap). The dragged row drops its `button` role, so match text.
const placeholder = page
.locator("#sidebar")
.getByText(timed, { exact: false });
await expect(placeholder).toBeVisible();

const placeholderBox = await placeholder.boundingBox();
const aBox = await sidebarButton(page, weekA).boundingBox();
const bBox = await sidebarButton(page, weekB).boundingBox();

await page.mouse.up();

expect(placeholderBox).not.toBeNull();
expect(aBox).not.toBeNull();
expect(bBox).not.toBeNull();

if (placeholderBox && aBox && bBox) {
const placeholderMid = placeholderBox.y + placeholderBox.height / 2;
const aMid = aBox.y + aBox.height / 2;
const bMid = bBox.y + bBox.height / 2;

expect(aMid).toBeLessThan(placeholderMid);
expect(placeholderMid).toBeLessThan(bMid);
}
});

test("snaps to the nearest Someday list when dropped between the lists", async ({
page,
}) => {
await prepareCalendarPage(page);
await ensureSidebarOpen(page);

const weekA = await createSomeday(page, "week", "Week A");
const monthM = await createSomeday(page, "month", "Month M");
const timed = await createTimed(page, "Timed T");

const start = await centerOf(page, timed, "grid");
const a = await centerOf(page, weekA, "sidebar");
const m = await centerOf(page, monthM, "sidebar");

// A point in the empty space between the week list (above) and the month
// list (below), biased toward the month list so "nearest" should be Month.
const gapTarget = { x: a.x, y: m.box.y - 8 };

await page.mouse.move(start.x, start.y);
await page.mouse.down();
await page.mouse.move(gapTarget.x, gapTarget.y, { steps: 16 });
await page.waitForTimeout(250);

await page.screenshot({
path: "test-results/repro/between-lists-mid-drag.png",
});

await page.mouse.up();
await page.waitForTimeout(400);

await page.screenshot({
path: "test-results/repro/between-lists-after-drop.png",
});

// It should have converted to a Someday event (nearest = Month), not jumped
// back onto the grid.
await expect(
page.locator("#mainGrid").getByRole("button", { name: timed }),
).toHaveCount(0, { timeout: 8000 });
await expect(sidebarButton(page, timed)).toBeVisible({ timeout: 8000 });
});
72 changes: 71 additions & 1 deletion packages/core/src/mappers/map.event.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ObjectId } from "bson";
import { Origin, Priorities } from "@core/constants/core.constants";
import { MapEvent } from "@core/mappers/map.event";
import { type Schema_Event } from "@core/types/event.types";
import { Categories_Event, type Schema_Event } from "@core/types/event.types";
import {
createMockBaseEvent,
createMockInstance,
Expand All @@ -25,3 +26,72 @@ describe("MapEvent.removeProviderData", () => {
expect((result as Schema_Event).recurrence?.eventId).toBeUndefined();
});
});

describe("MapEvent.toSomeday", () => {
const baseEvent: Schema_Event = {
_id: "event-1",
title: "Grid event",
startDate: "2024-03-19T10:00:00.000Z",
endDate: "2024-03-19T11:00:00.000Z",
isAllDay: false,
isSomeday: false,
origin: Origin.COMPASS,
priority: Priorities.WORK,
user: "user-1",
};

it("maps a calendar event into a someday payload with the given dates", () => {
const result = MapEvent.toSomeday(baseEvent, {
category: Categories_Event.SOMEDAY_WEEK,
endDate: "2024-03-23",
order: 2,
startDate: "2024-03-17",
});

expect(result).toEqual(
expect.objectContaining({
_id: "event-1",
endDate: "2024-03-23",
isAllDay: false,
isSomeday: true,
order: 2,
priority: Priorities.WORK,
startDate: "2024-03-17",
title: "Grid event",
}),
);
});

it("rewrites the recurrence FREQ for the destination list", () => {
const result = MapEvent.toSomeday(
{ ...baseEvent, recurrence: { rule: ["RRULE:FREQ=DAILY;COUNT=5"] } },
{
category: Categories_Event.SOMEDAY_MONTH,
endDate: "2024-03-31",
order: 0,
startDate: "2024-03-01",
},
);

expect(result.recurrence?.rule).toEqual(["RRULE:FREQ=MONTHLY;COUNT=5"]);
});

it("defaults missing priority and user", () => {
const result = MapEvent.toSomeday(
{
...baseEvent,
priority: undefined,
user: undefined as unknown as string,
},
{
category: Categories_Event.SOMEDAY_WEEK,
endDate: "2024-03-23",
order: 0,
startDate: "2024-03-17",
},
);

expect(result.priority).toBe(Priorities.UNASSIGNED);
expect(result.user).toBe("");
});
});
27 changes: 27 additions & 0 deletions packages/core/src/mappers/map.event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import mergeWith from "lodash.mergewith";
import { Origin, Priorities } from "@core/constants/core.constants";
import { BaseError } from "@core/errors/errors.base";
import { rewriteRecurrenceFreq } from "@core/mappers/map.recurrence";
import {
CalendarProvider,
type Categories_Event,
type Event_Core,
type Schema_Event,
type Schema_Event_Recur_Base,
Expand Down Expand Up @@ -78,6 +80,31 @@ export namespace MapEvent {
return coreEvent;
};

export const toSomeday = (
event: Schema_Event,
{
category,
endDate,
order,
startDate,
}: {
category: Categories_Event.SOMEDAY_WEEK | Categories_Event.SOMEDAY_MONTH;
endDate: string;
order: number;
startDate: string;
},
): Schema_Event => ({
...event,
endDate,
isAllDay: false,
isSomeday: true,
order,
priority: event.priority ?? Priorities.UNASSIGNED,
recurrence: rewriteRecurrenceFreq(event.recurrence, category),
startDate,
user: event.user ?? "",
});

export const toGcal = (
event: Schema_Event,
{ status = "confirmed" }: Pick<gSchema$Event, "status"> = {},
Expand Down
61 changes: 61 additions & 0 deletions packages/core/src/mappers/map.recurrence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { rewriteRecurrenceFreq } from "@core/mappers/map.recurrence";
import { Categories_Event } from "@core/types/event.types";

describe("rewriteRecurrenceFreq", () => {
it("rewrites FREQ to WEEKLY for the week list", () => {
const result = rewriteRecurrenceFreq(
{ rule: ["RRULE:FREQ=DAILY;COUNT=10;INTERVAL=1"] },
Categories_Event.SOMEDAY_WEEK,
);

expect(result?.rule).toEqual(["RRULE:FREQ=WEEKLY;COUNT=10;INTERVAL=1"]);
});

it("rewrites FREQ to MONTHLY for the month list", () => {
const result = rewriteRecurrenceFreq(
{ rule: ["RRULE:FREQ=WEEKLY;COUNT=10"] },
Categories_Event.SOMEDAY_MONTH,
);

expect(result?.rule).toEqual(["RRULE:FREQ=MONTHLY;COUNT=10"]);
});

it("rewrites FREQ with no trailing tokens", () => {
const result = rewriteRecurrenceFreq(
{ rule: ["RRULE:FREQ=DAILY"] },
Categories_Event.SOMEDAY_WEEK,
);

expect(result?.rule).toEqual(["RRULE:FREQ=WEEKLY"]);
});

it("leaves non-RRULE lines untouched", () => {
const result = rewriteRecurrenceFreq(
{ rule: ["RRULE:FREQ=DAILY", "EXDATE:20240101T000000Z"] },
Categories_Event.SOMEDAY_WEEK,
);

expect(result?.rule).toEqual([
"RRULE:FREQ=WEEKLY",
"EXDATE:20240101T000000Z",
]);
});

it("preserves the recurrence eventId", () => {
const result = rewriteRecurrenceFreq(
{ rule: ["RRULE:FREQ=DAILY"], eventId: "abc" },
Categories_Event.SOMEDAY_WEEK,
);

expect(result?.eventId).toBe("abc");
});

it("returns the recurrence unchanged when there is no rule", () => {
expect(
rewriteRecurrenceFreq(undefined, Categories_Event.SOMEDAY_WEEK),
).toBeUndefined();
expect(
rewriteRecurrenceFreq({ rule: null }, Categories_Event.SOMEDAY_WEEK),
).toEqual({ rule: null });
});
});
Loading