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
311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
// Coverage for — quick-preview indicators on the row-actions
|
|
// kebab menu's main view:
|
|
//
|
|
// 1. The "Row color" item shows a tiny swatch reflecting the row's
|
|
// currently saved color (or a hatched neutral dot for "default"),
|
|
// so users can tell at a glance which color is set without
|
|
// drilling into the swatch sub-view.
|
|
//
|
|
// 2. The "Merge cells" item shows a "Merged: <cols>" badge when the
|
|
// row has a stored merge, naming the spanned columns. Hidden
|
|
// entirely on rows with no stored merge.
|
|
//
|
|
// The badge text + dot color must update whenever the underlying state
|
|
// changes. To keep selectors stable, the indicators ride along inside
|
|
// the existing `em-row-color-trigger` / `em-merge-trigger-{id}` buttons
|
|
// rather than introducing new top-level menu items.
|
|
//
|
|
// Setup follows the same pattern as the merge spec: insert seeded
|
|
// meetings on a far-future date directly via pg, drive the UI through
|
|
// the schedule's date picker, then clean up via afterAll.
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
import pg from "pg";
|
|
|
|
const DATABASE_URL = process.env.DATABASE_URL;
|
|
if (!DATABASE_URL) {
|
|
throw new Error(
|
|
"DATABASE_URL must be set to run the row-actions previews UI tests",
|
|
);
|
|
}
|
|
|
|
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
|
const createdMeetingIds = [];
|
|
|
|
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 setLangEn(page) {
|
|
await page.addInitScript(() => {
|
|
try {
|
|
window.localStorage.setItem("tx-lang", "en");
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
}
|
|
|
|
async function resetSchedulePrefs(page) {
|
|
await page.evaluate(() => {
|
|
try {
|
|
const keys = Object.keys(window.localStorage);
|
|
for (const k of keys) {
|
|
if (
|
|
k.startsWith("em-schedule-edit-mode-v1") ||
|
|
k === "em-schedule-cols-v1" ||
|
|
k === "em-schedule-row-colors-v1" ||
|
|
k === "em-current-meeting-highlight-v1"
|
|
) {
|
|
window.localStorage.removeItem(k);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
async function insertMeeting({
|
|
meetingDate,
|
|
dailyNumber,
|
|
titleEn,
|
|
titleAr,
|
|
startTime,
|
|
endTime,
|
|
mergeStartColumn = null,
|
|
mergeEndColumn = null,
|
|
mergeText = null,
|
|
}) {
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO executive_meetings
|
|
(daily_number, title_ar, title_en, meeting_date, start_time, end_time,
|
|
status, merge_start_column, merge_end_column, merge_text)
|
|
VALUES ($1, $2, $3, $4, $5, $6, 'scheduled', $7, $8, $9)
|
|
RETURNING id`,
|
|
[
|
|
dailyNumber,
|
|
titleAr,
|
|
titleEn,
|
|
meetingDate,
|
|
startTime,
|
|
endTime,
|
|
mergeStartColumn,
|
|
mergeEndColumn,
|
|
mergeText,
|
|
],
|
|
);
|
|
const id = rows[0].id;
|
|
createdMeetingIds.push(id);
|
|
return id;
|
|
}
|
|
|
|
const RANDOM_DATE_BASE_DAYS =
|
|
365 + Math.floor(Math.random() * 1000) + ((Date.now() / 1000) | 0) % 500;
|
|
function uniqueFutureDate(offsetDays) {
|
|
const d = new Date();
|
|
d.setUTCDate(d.getUTCDate() + RANDOM_DATE_BASE_DAYS + offsetDays * 11);
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
|
|
test.afterAll(async () => {
|
|
if (createdMeetingIds.length > 0) {
|
|
await pool.query(
|
|
`DELETE FROM executive_meeting_attendees WHERE meeting_id = ANY($1::int[])`,
|
|
[createdMeetingIds],
|
|
);
|
|
await pool.query(
|
|
`DELETE FROM executive_meeting_audit_logs WHERE entity_type = 'meeting' AND entity_id = ANY($1::int[])`,
|
|
[createdMeetingIds],
|
|
);
|
|
await pool.query(
|
|
`DELETE FROM executive_meetings WHERE id = ANY($1::int[])`,
|
|
[createdMeetingIds],
|
|
);
|
|
}
|
|
await pool.end();
|
|
});
|
|
|
|
test("Row actions menu: Row color item shows an inline swatch reflecting the saved color", async ({
|
|
page,
|
|
}) => {
|
|
await setLangEn(page);
|
|
|
|
const date = uniqueFutureDate(3);
|
|
const uniq = `${Date.now().toString(36)}_${Math.random()
|
|
.toString(36)
|
|
.slice(2, 6)}`;
|
|
const meetingId = await insertMeeting({
|
|
meetingDate: date,
|
|
dailyNumber: 1,
|
|
titleEn: `Color preview ${uniq}`,
|
|
titleAr: `معاينة اللون ${uniq}`,
|
|
startTime: "09:00:00",
|
|
endTime: "09:30: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);
|
|
|
|
const row = page.getByTestId(`em-row-${meetingId}`);
|
|
await expect(row).toBeVisible({ timeout: 10_000 });
|
|
await ensureEditModeOn(page);
|
|
|
|
const kebab = row.locator('[data-testid^="em-row-actions-"]').first();
|
|
await kebab.click({ force: true });
|
|
|
|
// First open: no color picked yet → indicator advertises "default".
|
|
// The dot itself is decorative (aria-hidden), so we assert via its
|
|
// data-color-key attribute rather than scraping background CSS.
|
|
const indicator = page.getByTestId(`em-row-color-indicator-${meetingId}`);
|
|
await expect(indicator).toHaveCount(1);
|
|
await expect(indicator).toHaveAttribute("data-color-key", "default");
|
|
|
|
// Pick the red swatch via the existing sub-view.
|
|
await page.getByTestId("em-row-color-trigger").click();
|
|
await page.getByTestId("em-row-color-red").click();
|
|
// Picking a color closes the popover (existing behavior).
|
|
await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(0);
|
|
|
|
// Re-open the kebab and confirm the inline preview now reflects red.
|
|
// The picker uses #fee2e2 → rgb(254, 226, 226), so we also sanity
|
|
// check the computed background to make sure the dot is actually
|
|
// rendering the chosen tint (not just dataset-tagged correctly).
|
|
await kebab.click({ force: true });
|
|
await expect(indicator).toHaveAttribute("data-color-key", "red");
|
|
const bg = await indicator.evaluate(
|
|
(el) => getComputedStyle(el).backgroundColor,
|
|
);
|
|
expect(bg).toMatch(/254,\s*226,\s*226/);
|
|
|
|
// Reset back to default and confirm the indicator returns to the
|
|
// hatched / "default" state, so future runs against this meeting (or
|
|
// its leftover localStorage) don't lock in red.
|
|
await page.getByTestId("em-row-color-trigger").click();
|
|
await page.getByTestId("em-row-color-default").click();
|
|
await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(0);
|
|
await kebab.click({ force: true });
|
|
await expect(indicator).toHaveAttribute("data-color-key", "default");
|
|
});
|
|
|
|
test("Row actions menu: Merge cells item shows a 'Merged: …' badge naming the spanned columns", async ({
|
|
page,
|
|
}) => {
|
|
await setLangEn(page);
|
|
|
|
const date = uniqueFutureDate(4);
|
|
const uniq = `${Date.now().toString(36)}_${Math.random()
|
|
.toString(36)
|
|
.slice(2, 6)}`;
|
|
|
|
// Row WITHOUT a stored merge — should not render a merge badge at all.
|
|
const plainId = await insertMeeting({
|
|
meetingDate: date,
|
|
dailyNumber: 1,
|
|
titleEn: `Plain row ${uniq}`,
|
|
titleAr: `صف عادي ${uniq}`,
|
|
startTime: "09:00:00",
|
|
endTime: "09:30:00",
|
|
});
|
|
|
|
// Row WITH a meeting+attendees merge — kebab anchors on the merged
|
|
// cell, and the badge should read "Merged: Meeting + Attendees".
|
|
const mergedId = await insertMeeting({
|
|
meetingDate: date,
|
|
dailyNumber: 2,
|
|
titleEn: `Merged row ${uniq}`,
|
|
titleAr: `صف مدموج ${uniq}`,
|
|
startTime: "10:00:00",
|
|
endTime: "10:30:00",
|
|
mergeStartColumn: "meeting",
|
|
mergeEndColumn: "attendees",
|
|
mergeText: "merged note",
|
|
});
|
|
|
|
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);
|
|
|
|
const plainRow = page.getByTestId(`em-row-${plainId}`);
|
|
const mergedRow = page.getByTestId(`em-row-${mergedId}`);
|
|
await expect(plainRow).toBeVisible({ timeout: 10_000 });
|
|
await expect(mergedRow).toBeVisible({ timeout: 10_000 });
|
|
await ensureEditModeOn(page);
|
|
|
|
// Plain row: open the kebab and confirm there is NO merge indicator.
|
|
const plainKebab = plainRow
|
|
.locator('[data-testid^="em-row-actions-"]')
|
|
.first();
|
|
await plainKebab.click({ force: true });
|
|
await expect(
|
|
page.getByTestId(`em-merge-trigger-${plainId}`),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId(`em-merge-indicator-${plainId}`),
|
|
).toHaveCount(0);
|
|
// Close the popover and WAIT for it to actually unmount before
|
|
// poking the next row. Without this, a force:true click on the
|
|
// merged-row kebab can land on the still-open Delete item that
|
|
// sits over the merged row, triggering an accidental delete confirm
|
|
// and never opening the merged-row popover at all.
|
|
await page.keyboard.press("Escape");
|
|
await expect(
|
|
page.getByTestId(`em-merge-trigger-${plainId}`),
|
|
).toHaveCount(0);
|
|
|
|
// Resolve the rendered column-header labels at runtime so the
|
|
// assertion is language-agnostic — admin's preferredLanguage gets
|
|
// applied by AuthContext after login and may override the
|
|
// tx-lang=en init script seed, so we cannot hard-code the English
|
|
// strings here.
|
|
const meetingColLabel = (
|
|
await page.getByTestId("em-col-header-meeting").first().textContent()
|
|
)?.trim();
|
|
const attendeesColLabel = (
|
|
await page.getByTestId("em-col-header-attendees").first().textContent()
|
|
)?.trim();
|
|
expect(meetingColLabel).toBeTruthy();
|
|
expect(attendeesColLabel).toBeTruthy();
|
|
|
|
// Merged row: open the kebab and confirm the badge appears, naming
|
|
// the spanned columns. We check both `data-testid` and that the
|
|
// visible text contains both column labels in canonical order
|
|
// joined with " + ", which is what the activeBadge i18n template
|
|
// produces in either language.
|
|
const mergedKebab = mergedRow
|
|
.locator('[data-testid^="em-row-actions-"]')
|
|
.first();
|
|
await mergedKebab.click({ force: true });
|
|
// Sanity: the popover actually opened on the merged row.
|
|
await expect(
|
|
page.getByTestId(`em-merge-trigger-${mergedId}`),
|
|
).toBeVisible();
|
|
const mergedBadge = page.getByTestId(`em-merge-indicator-${mergedId}`);
|
|
await expect(mergedBadge).toBeVisible();
|
|
await expect(mergedBadge).toContainText(
|
|
`${meetingColLabel} + ${attendeesColLabel}`,
|
|
);
|
|
});
|