diff --git a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx index 9f042832..f65cef3f 100644 --- a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx +++ b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx @@ -958,7 +958,10 @@ function CascadePromptBlock({ ellipsis instead of pushing the times off-screen, and the From → To column always fits on one line. */} - + {/* w-20 leaves room for the Arabic header "رقم الاجتماع" + with whitespace-nowrap; w-12 was too tight at common + font sizes. */} + diff --git a/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs b/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs index 6e993ec7..58118b89 100644 --- a/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs +++ b/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs @@ -1291,6 +1291,84 @@ test("Upcoming-meeting alert: details panel renders attendees as numbered tables await expect(secondGroupRows.first()).toContainText("Internal One"); }); +test("Postpone dialog: cascade-followers table sits fully inside the dialog (no horizontal spill, RTL)", async ({ + page, +}) => { + // #485: regression for the cascade preview overlapping the dialog + // edge in Arabic. Seed a primary meeting + two followers later in + // the same day so the cascade prompt definitely fires after + // confirming a postpone, then assert the followers-list bounding + // box sits within the dialog's bounding box. + await setLang(page, "ar"); + const primary = await insertImminentMeeting({ + titleAr: `${TEST_TAG} cascade-layout primary AR ${"بعنوان طويل جداً يكفي لاختبار القص بالنقاط في عمود العنوان داخل اللوحة"}`, + titleEn: `${TEST_TAG} cascade-layout primary EN with a sufficiently long title to verify column truncation`, + deltaMins: 3, + }); + await insertImminentMeeting({ + titleAr: `${TEST_TAG} cascade-layout follower-1 AR`, + titleEn: `${TEST_TAG} cascade-layout follower-1 EN`, + deltaMins: 30, + }); + await insertImminentMeeting({ + titleAr: `${TEST_TAG} cascade-layout follower-2 AR`, + titleEn: `${TEST_TAG} cascade-layout follower-2 EN`, + deltaMins: 60, + }); + await page.setViewportSize({ width: 1280, height: 800 }); + await loginViaUi(page, "admin", "admin123"); + await page.goto("/"); + await expect(page.getByTestId("upcoming-meeting-alert")).toBeVisible({ + timeout: 15_000, + }); + await page.getByTestId("alert-postpone").click(); + const dialog = page.getByTestId("postpone-dialog"); + await expect(dialog).toBeVisible(); + await page.getByTestId("postpone-chip-10").click(); + await expect(page.getByTestId("postpone-confirm-block")).toBeVisible(); + await page.getByTestId("postpone-confirm-yes").click(); + + // Cascade prompt must appear with this seeding (two later meetings + // on the same day). Geometry checks then verify no spill. + const followersList = page.getByTestId("cascade-followers-list"); + await expect(followersList).toBeVisible({ timeout: 8_000 }); + + const dialogBox = await dialog.boundingBox(); + const tableBox = await followersList.boundingBox(); + expect(dialogBox).not.toBeNull(); + expect(tableBox).not.toBeNull(); + // Table must sit fully inside the dialog (1px tolerance for + // sub-pixel rounding). + expect(tableBox.x + 1).toBeGreaterThanOrEqual(dialogBox.x); + expect(tableBox.x + tableBox.width).toBeLessThanOrEqual( + dialogBox.x + dialogBox.width + 1, + ); + + // The truncating title cell must not push the times column out of + // the dialog. Check the first follower row's last cell (times). + const lastCell = page + .locator('[data-testid^="cascade-follower-"]') + .first() + .locator("td") + .last(); + const lastBox = await lastCell.boundingBox(); + expect(lastBox).not.toBeNull(); + expect(lastBox.x + lastBox.width).toBeLessThanOrEqual( + dialogBox.x + dialogBox.width + 1, + ); + + // Politely back out so this seeded meeting doesn't preempt later + // tests' alerts. + await page.getByTestId("cascade-back").click(); + await pool.query( + `UPDATE executive_meetings + SET start_time = (start_time::time + interval '2 hours')::time, + end_time = (end_time::time + interval '2 hours')::time + WHERE id = $1`, + [primary.id], + ); +}); + test("Postpone dialog: reschedule date / start / end inputs sit fully inside the dialog (no clipping at desktop width, RTL)", async ({ page, }) => {