7a2ae8434d
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu Replit-Helium-Checkpoint-Created: true
303 lines
11 KiB
JavaScript
303 lines
11 KiB
JavaScript
import { test, expect } from "@playwright/test";
|
|
|
|
async function loginViaUi(page, username, password) {
|
|
await page.goto("/login");
|
|
await page.locator("#username").fill(username);
|
|
await page.locator("#password").fill(password);
|
|
await Promise.all([
|
|
page.waitForURL((url) => !url.pathname.endsWith("/login"), {
|
|
timeout: 15_000,
|
|
}),
|
|
page.locator('form button[type="submit"]').click(),
|
|
]);
|
|
}
|
|
|
|
test("Schedule: Edit toggle hides editing affordances by default, reveals them when on, and resets to view mode on reload / re-navigation (#484)", async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript(() => {
|
|
try {
|
|
window.localStorage.setItem("tx-lang", "en");
|
|
} catch {
|
|
/* localStorage may not be ready before navigation */
|
|
}
|
|
});
|
|
|
|
await loginViaUi(page, "admin", "admin123");
|
|
await page.goto("/executive-meetings");
|
|
|
|
// Toggle is rendered (admin has edit permission), and starts in
|
|
// pressed=false / view mode.
|
|
const toggle = page.getByTestId("em-edit-mode-toggle");
|
|
await expect(toggle).toBeVisible();
|
|
await expect(toggle).toHaveAttribute("aria-pressed", "false");
|
|
|
|
// In view mode the row-level "Add meeting" button must be hidden.
|
|
await expect(page.getByTestId("em-add-row-button")).toHaveCount(0);
|
|
// And no row-level overlays (delete, color picker, merge trigger,
|
|
// grip handle) should exist anywhere in the table.
|
|
await expect(page.locator('[data-testid^="em-delete-row-"]')).toHaveCount(0);
|
|
await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(0);
|
|
await expect(page.locator('[data-testid^="em-merge-trigger-"]')).toHaveCount(0);
|
|
await expect(page.locator('[data-testid^="em-row-grip-"]')).toHaveCount(0);
|
|
|
|
// Flip toggle on. aria-pressed becomes true and the affordances appear.
|
|
await toggle.click();
|
|
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
|
|
|
// The "+ Add meeting" row button is back.
|
|
await expect(page.getByTestId("em-add-row-button")).toBeVisible();
|
|
|
|
// #484: edit mode is intentionally NOT persisted. Reloading the
|
|
// page must drop the toggle back to view mode.
|
|
await page.reload();
|
|
const toggleAfterReload = page.getByTestId("em-edit-mode-toggle");
|
|
await expect(toggleAfterReload).toHaveAttribute("aria-pressed", "false");
|
|
await expect(page.getByTestId("em-add-row-button")).toHaveCount(0);
|
|
|
|
// #484: navigating away to another app and coming back also resets
|
|
// the toggle (the page unmounts on route change).
|
|
await toggleAfterReload.click();
|
|
await expect(toggleAfterReload).toHaveAttribute("aria-pressed", "true");
|
|
await page.goto("/");
|
|
await page.goto("/executive-meetings");
|
|
const toggleAfterRenav = page.getByTestId("em-edit-mode-toggle");
|
|
await expect(toggleAfterRenav).toHaveAttribute("aria-pressed", "false");
|
|
await expect(page.getByTestId("em-add-row-button")).toHaveCount(0);
|
|
|
|
// Within a single visit the toggle still works normally.
|
|
await toggleAfterRenav.click();
|
|
await expect(toggleAfterRenav).toHaveAttribute("aria-pressed", "true");
|
|
await expect(page.getByTestId("em-add-row-button")).toBeVisible();
|
|
await toggleAfterRenav.click();
|
|
await expect(toggleAfterRenav).toHaveAttribute("aria-pressed", "false");
|
|
await expect(page.getByTestId("em-add-row-button")).toHaveCount(0);
|
|
});
|
|
|
|
test("Schedule: turning edit mode OFF cancels any open inline editor and discards the draft", async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript(() => {
|
|
try {
|
|
window.localStorage.setItem("tx-lang", "en");
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
|
|
await loginViaUi(page, "admin", "admin123");
|
|
await page.goto("/executive-meetings");
|
|
// Start clean (view mode), then flip edit mode on.
|
|
await page.evaluate(() => {
|
|
try {
|
|
const keys = Object.keys(window.localStorage);
|
|
for (const k of keys) {
|
|
if (k.startsWith("em-schedule-edit-mode-v1")) {
|
|
window.localStorage.removeItem(k);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
await page.reload();
|
|
|
|
const toggle = page.getByTestId("em-edit-mode-toggle");
|
|
await expect(toggle).toBeVisible();
|
|
await toggle.click();
|
|
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
|
|
|
// Make sure there's at least one row to edit. If the schedule is
|
|
// empty for today, add a row via the now-visible "+ Add meeting"
|
|
// button so the test has a stable target.
|
|
const addRow = page.getByTestId("em-add-row-button");
|
|
await expect(addRow).toBeVisible();
|
|
let titleCells = page.locator('[data-testid^="em-edit-title-"]');
|
|
if ((await titleCells.count()) === 0) {
|
|
await addRow.click();
|
|
await expect(titleCells.first()).toBeVisible({ timeout: 5_000 });
|
|
}
|
|
|
|
// Open the first inline title editor and type a draft we expect
|
|
// to be discarded.
|
|
const firstTitle = titleCells.first();
|
|
const originalText = (await firstTitle.innerText()).trim();
|
|
await firstTitle.click();
|
|
// The editor renders a contenteditable inside the cell. Type a
|
|
// unique draft so we can later assert it never landed.
|
|
const draft = "DRAFT-DO-NOT-PERSIST-" + Date.now();
|
|
await page.keyboard.type(draft);
|
|
|
|
// Now flip edit mode OFF while the editor is still open. The
|
|
// EditableCell `disabled` effect should exit edit mode and reset
|
|
// the displayed content back to its saved value.
|
|
await toggle.click();
|
|
await expect(toggle).toHaveAttribute("aria-pressed", "false");
|
|
|
|
// The displayed cell should NOT contain our draft text — i.e. the
|
|
// open editor was cancelled and the unsaved draft was discarded.
|
|
const afterText = (await firstTitle.innerText()).trim();
|
|
expect(afterText).not.toContain(draft);
|
|
expect(afterText).toBe(originalText);
|
|
});
|
|
|
|
// Shared setup for the attendee-prefix specs below.
|
|
async function gotoSchedule(page, lang) {
|
|
await page.addInitScript((l) => {
|
|
try {
|
|
window.localStorage.setItem("tx-lang", l);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, lang);
|
|
|
|
await loginViaUi(page, "admin", "admin123");
|
|
await page.goto("/executive-meetings");
|
|
// Reset edit-mode toggle so we start in view mode.
|
|
await page.evaluate(() => {
|
|
try {
|
|
const keys = Object.keys(window.localStorage);
|
|
for (const k of keys) {
|
|
if (k.startsWith("em-schedule-edit-mode-v1")) {
|
|
window.localStorage.removeItem(k);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
await page.reload();
|
|
|
|
// Wait for the schedule to render at least one row (or bail
|
|
// out if the page genuinely has no meetings — see below).
|
|
await page
|
|
.locator('[data-testid^="em-meeting-row-"]')
|
|
.first()
|
|
.waitFor({ state: "visible", timeout: 10_000 })
|
|
.catch(() => {
|
|
/* let the caller decide whether to skip */
|
|
});
|
|
}
|
|
|
|
// Regression guard for the "number stacked above the name" bug
|
|
// (Tasks #173 / #175). Attendee names are saved as tiptap HTML
|
|
// (`<p>name</p>`); without a fix, the block-level <p> pushes the
|
|
// name onto its own visual row beneath the small index span. We
|
|
// assert that for at least one attendee in a multi-attendee group,
|
|
// the index span and the name wrapper share roughly the same
|
|
// vertical center, in BOTH view mode and edit mode, AND in BOTH
|
|
// LTR (English) and RTL (Arabic) locales — since the bug originally
|
|
// surfaced on the Arabic schedule.
|
|
//
|
|
// the index prefix is intentionally hidden when the
|
|
// attendee group has only one entry, so this test scopes itself to
|
|
// rows that actually carry an `em-attendee-index-*` span.
|
|
for (const lang of ["en", "ar"]) {
|
|
test(`Schedule [${lang}]: attendee index prefix renders on the same visual line as the name (multi-attendee group)`, async ({
|
|
page,
|
|
}) => {
|
|
await gotoSchedule(page, lang);
|
|
|
|
// Only consider rows that actually have an index span — i.e. rows
|
|
// whose attendee group has 2+ entries. Single-attendee rows render
|
|
// no `em-attendee-index-*` (see the sibling spec below) and would
|
|
// make the layout assertion meaningless.
|
|
const multiAttendeeRow = page
|
|
.locator('[data-testid^="em-attendee-row-"]')
|
|
.filter({ has: page.locator('[data-testid^="em-attendee-index-"]') })
|
|
.first();
|
|
|
|
if ((await multiAttendeeRow.count()) === 0) {
|
|
test.skip(
|
|
true,
|
|
`No multi-attendee group present on the schedule for lang=${lang}; cannot assert layout. Seed two attendees in one group to enable this guard.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
async function assertSameLine(rowLocator, nameLocator) {
|
|
await expect(rowLocator).toBeVisible();
|
|
const indexBox = await rowLocator
|
|
.locator('[data-testid^="em-attendee-index-"]')
|
|
.boundingBox();
|
|
const nameBox = await nameLocator.boundingBox();
|
|
expect(indexBox).not.toBeNull();
|
|
expect(nameBox).not.toBeNull();
|
|
const indexCenter = indexBox.y + indexBox.height / 2;
|
|
const nameCenter = nameBox.y + nameBox.height / 2;
|
|
// If the name wrapped onto a row below the index, centers would
|
|
// differ by ~a full line height (~18-22px). 8px of slack covers
|
|
// normal inline-vs-inline-block baseline differences.
|
|
expect(Math.abs(indexCenter - nameCenter)).toBeLessThan(8);
|
|
}
|
|
|
|
// 1. View mode (toggle off): name renders as a plain <span>.
|
|
await assertSameLine(
|
|
multiAttendeeRow,
|
|
multiAttendeeRow.locator('[data-testid^="em-attendee-name-"]'),
|
|
);
|
|
|
|
// 2. Edit mode (toggle on): name renders inside an EditableCell.
|
|
const toggle = page.getByTestId("em-edit-mode-toggle");
|
|
if (await toggle.isVisible().catch(() => false)) {
|
|
await toggle.click();
|
|
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
|
const multiAttendeeRowEditable = page
|
|
.locator('[data-testid^="em-attendee-row-"]')
|
|
.filter({ has: page.locator('[data-testid^="em-attendee-index-"]') })
|
|
.filter({
|
|
has: page.locator('[data-testid^="em-edit-attendee-"]'),
|
|
})
|
|
.first();
|
|
if ((await multiAttendeeRowEditable.count()) > 0) {
|
|
await assertSameLine(
|
|
multiAttendeeRowEditable,
|
|
multiAttendeeRowEditable.locator(
|
|
'[data-testid^="em-edit-attendee-"]',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
// when an attendee group has exactly one entry the
|
|
// leading `1-` index prefix is hidden because there is nothing to
|
|
// enumerate. Assert that for at least one solo-attendee row, no
|
|
// `em-attendee-index-*` span exists inside it, in BOTH locales.
|
|
test(`Schedule [${lang}]: attendee index prefix is hidden when the group has only one attendee`, async ({
|
|
page,
|
|
}) => {
|
|
await gotoSchedule(page, lang);
|
|
|
|
// A solo-attendee row is one that contains an attendee name span
|
|
// but does NOT contain an index span. Filtering with `hasNot`
|
|
// keeps the spec robust against future per-group changes.
|
|
const soloAttendeeRow = page
|
|
.locator('[data-testid^="em-attendee-row-"]')
|
|
.filter({ has: page.locator('[data-testid^="em-attendee-name-"]') })
|
|
.filter({
|
|
hasNot: page.locator('[data-testid^="em-attendee-index-"]'),
|
|
})
|
|
.first();
|
|
|
|
if ((await soloAttendeeRow.count()) === 0) {
|
|
test.skip(
|
|
true,
|
|
`No single-attendee group present on the schedule for lang=${lang}; cannot assert prefix-hidden. Seed a meeting with exactly one attendee to enable this guard.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
await expect(soloAttendeeRow).toBeVisible();
|
|
// The name itself is still rendered.
|
|
await expect(
|
|
soloAttendeeRow.locator('[data-testid^="em-attendee-name-"]'),
|
|
).toBeVisible();
|
|
// ...but the index span must be absent inside the row.
|
|
await expect(
|
|
soloAttendeeRow.locator('[data-testid^="em-attendee-index-"]'),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
}
|