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
117 lines
4.0 KiB
JavaScript
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);
|
|
});
|