Add browser test for drag-to-reorder schedule columns (task #187)
Adds a Playwright scenario to artifacts/tx-os/tests/executive-meetings-schedule-features.spec.mjs that: - Logs in as admin, seeds one meeting on a far-future date, and lands on the schedule for that date. - Hops to the Settings tab to confirm the column customizer panel (em-customize-columns-panel) still surfaces the feature, then bounces back to the schedule. - Enters edit mode (required for SortableHeader to register dnd-kit attributes/listeners) and asserts the default header order (number, meeting, attendees, time) via th[data-testid^=em-col-header-]. - Drags the "attendees" header above the "meeting" header using a warm-up move past the 6px PointerSensor activation distance and a stepped pointer move whose end point lands slightly LEFT of the target's horizontal center (horizontalListSortingStrategy needs the drop point on the leading side to insert before the target). - Polls the rendered headers for the new order (number, attendees, meeting, time), then reads em-schedule-cols-v1 from localStorage to confirm persistence. - Reloads the page and re-asserts the new order to prove it restores from localStorage rather than reverting to DEFAULT_COLUMNS. Deviation from the task wording: the task description says the test should "open the customize-columns popover" and "drag one column chip above another". The customize panel is no longer a popover — it was moved into the Settings tab in #265 — and it never had draggable chips; column reordering is wired to the SortableHeader cells inside the schedule's floating thead. The new test honors the spirit of the task by visiting the Settings panel for sanity, then performing the actual reorder gesture on the table headers (the only mechanism the codebase exposes). Validation: the new test passes in isolation and as part of the schedule-features suite. One unrelated existing test ("custom highlight color paints the current meeting's box-shadow ring") is currently flaky/failing on its own without my changes; left untouched as it is outside this task's scope. Replit-Task-Id: 06c14e68-096b-407d-86b9-bd5a45674aee
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
@@ -703,3 +703,122 @@ test("Schedule: editing an attendee name with bold + color via the Tiptap toolba
|
||||
// The original plain text is still readable inside the formatted markup.
|
||||
expect(storedName).toContain(originalName.toLowerCase());
|
||||
});
|
||||
|
||||
test("Schedule: dragging the attendees column header above the meeting column reorders headers and persists across reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setLangEn(page);
|
||||
|
||||
// Seed a single meeting so the schedule actually renders rows. The
|
||||
// floating sticky thead (which carries the interactive
|
||||
// SortableHeader cells) is rendered regardless, but dragging into
|
||||
// an empty grid is brittle — having a row keeps the layout stable.
|
||||
const date = uniqueFutureDate(5);
|
||||
const uniq = `${Date.now().toString(36)}_${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 6)}`;
|
||||
const meetingId = await insertMeeting({
|
||||
meetingDate: date,
|
||||
dailyNumber: 1,
|
||||
titleEn: `ColReorder ${uniq}`,
|
||||
titleAr: `إعادة ترتيب ${uniq}`,
|
||||
startTime: "09:00:00",
|
||||
endTime: "10:00:00",
|
||||
});
|
||||
|
||||
await loginViaUi(page, "admin", "admin123");
|
||||
await page.goto("/executive-meetings");
|
||||
await resetSchedulePrefs(page);
|
||||
await page.reload();
|
||||
|
||||
await page.locator('input[type="date"]').first().fill(date);
|
||||
await expect(page.getByTestId(`em-row-${meetingId}`)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Sanity: the column customizer panel is still surfaced from the
|
||||
// Settings tab (#265 moved it out of a popover into Settings). We
|
||||
// hop over to confirm it exists, then bounce back to the schedule
|
||||
// where the actual header drag happens — column reordering is wired
|
||||
// to the SortableHeader cells inside the schedule's floating thead,
|
||||
// not to chips inside the customizer panel.
|
||||
await page.getByTestId("em-nav-settings").click();
|
||||
await expect(page.getByTestId("em-customize-columns-panel")).toBeVisible();
|
||||
await page.getByTestId("em-nav-schedule").click();
|
||||
|
||||
// Edit mode must be on for SortableHeader to register dnd-kit
|
||||
// attributes/listeners (dragEnabled === effectiveCanMutate).
|
||||
const toggle = page.getByTestId("em-edit-mode-toggle");
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
// Read the initial header order. DEFAULT_COLUMNS is
|
||||
// [number, meeting, attendees, time].
|
||||
const readHeaderOrder = () =>
|
||||
page
|
||||
.locator('th[data-testid^="em-col-header-"]')
|
||||
.evaluateAll((els) => els.map((el) => el.getAttribute("data-testid")));
|
||||
|
||||
expect(await readHeaderOrder()).toEqual([
|
||||
"em-col-header-number",
|
||||
"em-col-header-meeting",
|
||||
"em-col-header-attendees",
|
||||
"em-col-header-time",
|
||||
]);
|
||||
|
||||
const attendeesHeader = page.getByTestId("em-col-header-attendees");
|
||||
const meetingHeader = page.getByTestId("em-col-header-meeting");
|
||||
await expect(attendeesHeader).toBeVisible();
|
||||
await expect(meetingHeader).toBeVisible();
|
||||
|
||||
const fromBox = await attendeesHeader.boundingBox();
|
||||
const toBox = await meetingHeader.boundingBox();
|
||||
expect(fromBox).not.toBeNull();
|
||||
expect(toBox).not.toBeNull();
|
||||
|
||||
// dnd-kit's PointerSensor activates only after a 6px move from the
|
||||
// pointerdown position, so we issue a small "warm-up" move before
|
||||
// the long move to the target. SortableContext uses
|
||||
// horizontalListSortingStrategy here, so we drop the pointer
|
||||
// slightly LEFT of the target's horizontal center to insert the
|
||||
// dragged header BEFORE the target instead of after.
|
||||
const startX = fromBox.x + fromBox.width / 2;
|
||||
const startY = fromBox.y + fromBox.height / 2;
|
||||
const endX = toBox.x + 4;
|
||||
const endY = toBox.y + toBox.height / 2;
|
||||
|
||||
await page.mouse.move(startX, startY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(startX - 12, startY, { steps: 5 });
|
||||
await page.mouse.move(endX, endY, { steps: 15 });
|
||||
await page.mouse.up();
|
||||
|
||||
// After the drag, the headers should reflect the new order:
|
||||
// attendees moves above meeting.
|
||||
const expectedOrder = [
|
||||
"em-col-header-number",
|
||||
"em-col-header-attendees",
|
||||
"em-col-header-meeting",
|
||||
"em-col-header-time",
|
||||
];
|
||||
await expect.poll(readHeaderOrder, { timeout: 5_000 }).toEqual(expectedOrder);
|
||||
|
||||
// The new order must be persisted to localStorage under the key
|
||||
// documented in the task (em-schedule-cols-v1).
|
||||
const stored = await page.evaluate(() =>
|
||||
window.localStorage.getItem("em-schedule-cols-v1"),
|
||||
);
|
||||
expect(stored).not.toBeNull();
|
||||
const storedIds = JSON.parse(stored).map((c) => c.id);
|
||||
expect(storedIds).toEqual(["number", "attendees", "meeting", "time"]);
|
||||
|
||||
// Reload to confirm persistence — the order must restore from
|
||||
// localStorage rather than reverting to DEFAULT_COLUMNS.
|
||||
await page.reload();
|
||||
await page.locator('input[type="date"]').first().fill(date);
|
||||
await expect(page.getByTestId(`em-row-${meetingId}`)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
expect(await readHeaderOrder()).toEqual(expectedOrder);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user