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
230 lines
8.8 KiB
JavaScript
230 lines
8.8 KiB
JavaScript
// Verifies the consolidated row-actions kebab menu introduced as part
|
|
// of — the three previously-separate row overlays (delete,
|
|
// row color, merge cells) are now collapsed into a single menu so they
|
|
// no longer collide visually inside narrow cells. Each sub-action
|
|
// keeps its original testid, so the rest of the suite + any external
|
|
// tooling that targeted them keeps working without changes.
|
|
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(),
|
|
]);
|
|
}
|
|
|
|
async function ensureEditModeOn(page) {
|
|
const toggle = page.getByTestId("em-edit-mode-toggle");
|
|
await expect(toggle).toBeVisible();
|
|
if ((await toggle.getAttribute("aria-pressed")) === "false") {
|
|
await toggle.click();
|
|
}
|
|
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
|
}
|
|
|
|
test("Schedule: row actions menu exposes delete + color + merge in a single popover and supports back navigation", 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");
|
|
await ensureEditModeOn(page);
|
|
|
|
// Exactly one consolidated kebab trigger per row, replacing what used
|
|
// to be three separate overlay icons (Trash, Palette, Combine).
|
|
// We require at least one row to be present — `>= 1` keeps the assertion
|
|
// resilient to the dev DB seed varying across runs while still confirming
|
|
// the kebab is mounted at all.
|
|
const kebabs = page.locator('[data-testid^="em-row-actions-"]');
|
|
const kebabCount = await kebabs.count();
|
|
expect(kebabCount).toBeGreaterThanOrEqual(1);
|
|
// Should be exactly one kebab per visible meeting row — rule out
|
|
// duplicate triggers leaking across multiple cells of the same row.
|
|
const visibleRows = await page
|
|
.locator('tr[data-testid^="em-row-"]')
|
|
.count();
|
|
if (visibleRows > 0) {
|
|
expect(kebabCount).toBe(visibleRows);
|
|
}
|
|
const kebab = kebabs.first();
|
|
|
|
// Closed state: none of the legacy testids are mounted yet.
|
|
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,
|
|
);
|
|
|
|
// The kebab is opacity-0 until row hover, so force the click.
|
|
await kebab.click({ force: true });
|
|
|
|
// Main menu shows all three legacy testids — preserves selectors used
|
|
// elsewhere in the suite.
|
|
await expect(page.locator('[data-testid^="em-delete-row-"]')).toHaveCount(1);
|
|
await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(1);
|
|
await expect(page.locator('[data-testid^="em-merge-trigger-"]')).toHaveCount(
|
|
1,
|
|
);
|
|
|
|
// Drill into the Row color sub-view: Delete + Merge get hidden,
|
|
// a Back affordance + the swatch row appear.
|
|
await page.getByTestId("em-row-color-trigger").click();
|
|
await expect(page.locator('[data-testid^="em-delete-row-"]')).toHaveCount(0);
|
|
await expect(page.locator('[data-testid^="em-merge-trigger-"]')).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("em-row-actions-back")).toBeVisible();
|
|
|
|
// Back returns to the main 3-item view.
|
|
await page.getByTestId("em-row-actions-back").click();
|
|
await expect(page.locator('[data-testid^="em-delete-row-"]')).toHaveCount(1);
|
|
await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(1);
|
|
await expect(page.locator('[data-testid^="em-merge-trigger-"]')).toHaveCount(
|
|
1,
|
|
);
|
|
|
|
// Drill into the Merge sub-view — three merge options appear and the
|
|
// other items disappear.
|
|
await page.locator('[data-testid^="em-merge-trigger-"]').click();
|
|
await expect(
|
|
page.locator('[data-testid^="em-merge-opt-meeting-attendees-"]'),
|
|
).toHaveCount(1);
|
|
await expect(
|
|
page.locator('[data-testid^="em-merge-opt-meeting-time-"]'),
|
|
).toHaveCount(1);
|
|
await expect(page.locator('[data-testid^="em-merge-opt-all-"]')).toHaveCount(
|
|
1,
|
|
);
|
|
await expect(page.locator('[data-testid^="em-delete-row-"]')).toHaveCount(0);
|
|
await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(0);
|
|
|
|
// Escape closes the popover entirely.
|
|
await page.keyboard.press("Escape");
|
|
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,
|
|
);
|
|
});
|
|
|
|
// Regression guard: when a user picks a row color (e.g. red), the
|
|
// chosen tint must wrap the *entire* row, including the leading "#"
|
|
// (number) cell. Previously the # cell was hard-coded to bg-white and
|
|
// stayed visually disconnected from the rest of the row, which broke
|
|
// the meaning of the color (e.g. a "danger" red row with a white #
|
|
// looked half-flagged). This test picks the red swatch via the
|
|
// consolidated kebab menu, verifies the # cell adopts the same
|
|
// background as the meeting cell next to it, then resets to "default"
|
|
// and confirms the # cell returns to white.
|
|
test("Schedule: picking a row color tints the # cell to match the rest of the row", async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript(() => {
|
|
try {
|
|
window.localStorage.setItem("tx-lang", "en");
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
await loginViaUi(page, "admin", "admin123");
|
|
await page.goto("/executive-meetings");
|
|
// Reset persisted row colors so we always start from the default
|
|
// (white) state, regardless of leftover state from prior runs.
|
|
await page.evaluate(() => {
|
|
try {
|
|
window.localStorage.removeItem("em-schedule-row-colors-v1");
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
await page.reload();
|
|
await ensureEditModeOn(page);
|
|
|
|
// We need a row whose # cell starts WHITE — i.e. it's not a current
|
|
// meeting (data-current-meeting="true" → highlightColor wash) and
|
|
// not a cancelled / status-flagged row (bg-red-600 badge). Both of
|
|
// those higher-priority states would mask the regression.
|
|
// `data-current-meeting` lives on the <tr> itself, so the
|
|
// `:not([data-current-meeting="true"])` CSS selector is the
|
|
// reliable way to exclude current rows (Playwright's
|
|
// `filter({ hasNot })` looks at *descendants*, which never matches
|
|
// an attribute on the parent <tr>). To also exclude the red-badge
|
|
// rows, we then walk through the candidates and pick the first one
|
|
// whose # cell's computed background is actually white.
|
|
await page
|
|
.locator('tr[data-testid^="em-row-"]')
|
|
.first()
|
|
.waitFor({ state: "visible", timeout: 10_000 });
|
|
const candidateRows = page.locator(
|
|
'tr[data-testid^="em-row-"]:not([data-current-meeting="true"])',
|
|
);
|
|
const candidateCount = await candidateRows.count();
|
|
test.skip(
|
|
candidateCount === 0,
|
|
"No non-current rows present; cannot exercise the row-color tint regression.",
|
|
);
|
|
|
|
let targetRow = null;
|
|
let numberCell = null;
|
|
for (let i = 0; i < candidateCount; i++) {
|
|
const row = candidateRows.nth(i);
|
|
const cell = row.locator("td").first();
|
|
const bg = await cell.evaluate(
|
|
(el) => getComputedStyle(el).backgroundColor,
|
|
);
|
|
if (/255,\s*255,\s*255/.test(bg)) {
|
|
targetRow = row;
|
|
numberCell = cell;
|
|
break;
|
|
}
|
|
}
|
|
test.skip(
|
|
targetRow === null,
|
|
"No row with a default white # cell; every visible row is current or cancelled.",
|
|
);
|
|
await expect(targetRow).toBeVisible();
|
|
|
|
// Open the row's kebab → Row color → red swatch.
|
|
const kebab = targetRow.locator('[data-testid^="em-row-actions-"]').first();
|
|
await kebab.click({ force: true });
|
|
await page.getByTestId("em-row-color-trigger").click();
|
|
await page.getByTestId("em-row-color-red").click();
|
|
await page.keyboard.press("Escape");
|
|
|
|
// The red swatch is #fee2e2 → rgb(254, 226, 226). The # cell should
|
|
// now share that background. We poll because the row tint applies
|
|
// through React state and may take a frame to settle.
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
numberCell.evaluate((el) => getComputedStyle(el).backgroundColor),
|
|
{ timeout: 5_000 },
|
|
)
|
|
.toMatch(/254,\s*226,\s*226/);
|
|
|
|
// Reset to "default" and confirm the # cell returns to white.
|
|
await kebab.click({ force: true });
|
|
await page.getByTestId("em-row-color-trigger").click();
|
|
await page.getByTestId("em-row-color-default").click();
|
|
await page.keyboard.press("Escape");
|
|
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
numberCell.evaluate((el) => getComputedStyle(el).backgroundColor),
|
|
{ timeout: 5_000 },
|
|
)
|
|
.toMatch(/255,\s*255,\s*255/);
|
|
});
|