Files
TX/artifacts/tx-os/tests/notification-sound-webaudio.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

146 lines
4.9 KiB
JavaScript

// E2E test for: notification sounds use Web Audio so they
// reliably play on iPad Safari after a single user gesture. Verifies
// (1) the AudioContext reaches `running` state after the user clicks
// the per-sound preview button in notification settings, (2) the play
// counter increments, and (3) the testPlay path is not subject to the
// 3-second burst-coalescing throttle.
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 this UI test");
}
const TEST_PASSWORD = "TestPass123!";
const TEST_PASSWORD_HASH =
"$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";
const pool = new pg.Pool({ connectionString: DATABASE_URL });
const createdUserIds = [];
function uniq() {
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}
async function createUser(prefix, displayEn) {
const username = `${prefix}_${uniq()}`;
const { rows } = await pool.query(
`INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
VALUES ($1, $2, $3, $4, 'en', true) RETURNING id`,
[username, `${username}@example.com`, TEST_PASSWORD_HASH, displayEn],
);
const id = rows[0].id;
createdUserIds.push(id);
await pool.query(
`INSERT INTO user_roles (user_id, role_id)
SELECT $1, id FROM roles WHERE name = 'user'`,
[id],
);
return { id, username };
}
test.afterAll(async () => {
if (createdUserIds.length > 0) {
await pool.query(
`DELETE FROM notifications WHERE user_id = ANY($1::int[])`,
[createdUserIds],
);
await pool.query(
`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`,
[createdUserIds],
);
await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [
createdUserIds,
]);
}
await pool.end();
});
async function loginViaUi(page, username) {
await page.goto("/login");
await page.locator("#username").fill(username);
await page.locator("#password").fill(TEST_PASSWORD);
await Promise.all([
page.waitForURL((url) => !url.pathname.endsWith("/login"), {
timeout: 15_000,
}),
page.locator('form button[type="submit"]').click(),
]);
}
test("preview sound button activates Web Audio and increments play counter", async ({
browser,
}) => {
test.setTimeout(60_000);
const user = await createUser("audio_unlock", "Audio Unlock");
const ctx = await browser.newContext();
const page = await ctx.newPage();
try {
await loginViaUi(page, user.username);
await page.goto("/");
// Reset counter so we can assert exactly the clicks we drive below.
await page.evaluate(() => {
window.__txosNotifPlayCount = 0;
window.__txosNotifLastSound = undefined;
});
// Open the notification settings popover via its bell trigger
// (aria-label "Notification settings" / "إعدادات الإشعارات"). The
// login defaults to English.
await page.getByRole("button", { name: /notification sounds/i }).click();
// Radix Popover renders into a portal — wait until the popover
// body is on screen before reaching for the preview buttons.
await expect(
page.getByText(/notification sounds/i).last(),
).toBeVisible({ timeout: 5_000 });
// First per-sound preview play — this is the gesture that should
// bring the AudioContext to `running` state on every browser.
const previews = page.getByRole("button", { name: /^preview$/i });
await expect(previews.first()).toBeVisible({ timeout: 5_000 });
await previews.first().click();
// The play counter is bumped synchronously inside play().
await expect
.poll(() => page.evaluate(() => window.__txosNotifPlayCount ?? 0), {
timeout: 3_000,
})
.toBe(1);
const lastSound = await page.evaluate(() => window.__txosNotifLastSound);
expect(typeof lastSound).toBe("string");
expect(lastSound && lastSound.length).toBeGreaterThan(0);
// testPlay must bypass the 3 s throttle so users can audition
// several sounds back-to-back from settings. Click a second
// preview immediately and require the counter to advance.
await previews.nth(1).click();
await expect
.poll(() => page.evaluate(() => window.__txosNotifPlayCount ?? 0), {
timeout: 3_000,
})
.toBe(2);
// Verify the actual production singleton player's AudioContext
// reached `running` state as a result of the preview-button
// gesture. This is what iPad Safari requires to subsequently
// play out-of-gesture, socket-driven chimes.
await expect
.poll(
() =>
page.evaluate(() => {
const fn = /** @type {any} */ (window).__txosNotifCtxState;
return typeof fn === "function" ? fn() : null;
}),
{ timeout: 3_000 },
)
.toBe("running");
} finally {
await ctx.close();
}
});