Files
TX/artifacts/tx-os/tests/upcoming-meeting-alert-animation.spec.mjs
T
riyadhafraa 7a2ae8434d Update project documentation and code comments to remove platform-specific references
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
2026-05-14 06:23:49 +00:00

117 lines
4.0 KiB
JavaScript

// lightweight render test for the upcoming-meeting alert
// entrance animation. Asserts the alert panel renders with the
// matching scale-in/opacity transition, an accent ring (boxShadow with
// the user's accent color), and a pulsing animate-ping halo around the
// drag handle — the visual language we adopted from the incoming-note
// popup.
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");
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(),
]);
}
function todayLocal() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function timePlusMinutes(delta) {
const d = new Date(Date.now() + delta * 60_000);
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:00`;
}
test.afterAll(async () => {
if (createdMeetingIds.length > 0) {
await pool.query(
`DELETE FROM executive_meeting_alert_state 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_meeting_attendees WHERE meeting_id = ANY($1::int[])`,
[createdMeetingIds],
);
await pool.query(
`DELETE FROM executive_meetings WHERE id = ANY($1::int[])`,
[createdMeetingIds],
);
}
await pool.end();
});
test("upcoming-meeting alert renders with scale-in entrance, accent ring, and ping halo", async ({
page,
}) => {
test.setTimeout(60_000);
const date = todayLocal();
const startTime = timePlusMinutes(3);
const endTime = timePlusMinutes(33);
const { rows: dnRows } = await pool.query(
`SELECT COALESCE(MAX(daily_number), 0) + 1 AS n FROM executive_meetings WHERE meeting_date = $1`,
[date],
);
const { rows } = await pool.query(
`INSERT INTO executive_meetings
(daily_number, title_ar, title_en, meeting_date, start_time, end_time, status)
VALUES ($1, $2, $3, $4, $5, $6, 'scheduled')
RETURNING id`,
[
dnRows[0].n,
"ALERT_ANIM_TEST AR",
"ALERT_ANIM_TEST EN",
date,
startTime,
endTime,
],
);
createdMeetingIds.push(rows[0].id);
await loginViaUi(page, "admin", "admin123");
await page.goto("/");
const alert = page.getByTestId("upcoming-meeting-alert");
await expect(alert).toBeVisible({ timeout: 15_000 });
// After the entrance animation settles the panel is fully visible
// (opacity-100 / scale-100). We assert the resolved styles describe
// the scale + opacity transition we configured.
const transition = await alert.evaluate(
(el) => getComputedStyle(el).transition,
);
expect(transition).toMatch(/transform/);
expect(transition).toMatch(/opacity/);
// The cubic-bezier spring curve we use for the entrance.
expect(transition).toMatch(/cubic-bezier\(0\.34,\s*1\.56,\s*0\.64,\s*1\)/);
// Accent ring is implemented via boxShadow (so the color tracks the
// user's accent). Verify a 4px inset-less colored ring is the first
// shadow layer alongside the existing drop shadow.
const boxShadow = await alert.evaluate(
(el) => getComputedStyle(el).boxShadow,
);
expect(boxShadow).toMatch(/rgba?\([^)]*\)\s+0px\s+0px\s+0px\s+4px/);
// Pulsing accent halo around the drag handle (animate-ping element).
const handle = page.getByTestId("alert-drag-handle");
await expect(handle).toBeVisible();
const halo = handle.locator(".animate-ping").first();
await expect(halo).toHaveCount(1);
});