import { test, expect, Page } from '@playwright/test'; import { HS, createRoom, ensureUser, hsReachable, loginUI, openRoom, uniq, TestUser, } from './localHs'; // [Gitea #124] Status banner from the homeserver's Uptime Kuma page. Kuma is // answered by fixtures here; nothing contacts a real Kuma. const KUMA = 'https://isitup.lotusguild.org'; const PAGE_CFG = { url: KUMA, slug: 'matrix' }; const UP = 1; const DOWN = 0; type Fixture = { hs?: number; calls?: number; staleMinutes?: number; fail?: boolean }; const kumaTime = (ms: number) => new Date(ms).toISOString().replace('T', ' ').slice(0, 23); /** Serve config.json with a status page for `serverName` and answer Kuma from `fx()`. */ async function mockKuma(page: Page, serverName: string | null, fx: () => Fixture) { const kumaRequests: string[] = []; await page.route(/\/config\.json(\?.*)?$/, async (route) => { const res = await route.fetch(); const cfg = await res.json(); cfg.statusPages = serverName ? { [serverName]: PAGE_CFG } : {}; await route.fulfill({ response: res, json: cfg }); }); await page.route(/isitup\.lotusguild\.org\/api\/status-page\//, async (route) => { const url = route.request().url(); kumaRequests.push(url); const f = fx(); if (f.fail) { await route.fulfill({ status: 500, body: 'oops' }); return; } const cors = { 'access-control-allow-origin': '*' }; if (url.includes('/heartbeat/')) { // Two checks, the latest `staleMinutes` ago (0 = just now). const last = Date.now() - (f.staleMinutes ?? 0) * 60_000; const beats = (s: number) => [ { status: s, time: kumaTime(last - 60_000) }, { status: s, time: kumaTime(last) }, ]; await route.fulfill({ headers: cors, json: { heartbeatList: { 1: beats(f.hs ?? UP), 2: beats(f.calls ?? UP) }, uptimeList: {} }, }); return; } await route.fulfill({ headers: cors, json: { config: { slug: 'matrix' }, incidents: [], maintenanceList: [], publicGroupList: [ { name: 'Homeserver', monitorList: [{ id: 1 }] }, { name: 'Voice calls', monitorList: [{ id: 2 }] }, ], }, }); }); return kumaRequests; } const serverOf = (u: TestUser) => u.userId.split(':').slice(1).join(':'); const strip = (page: Page, text: RegExp) => page.locator('[role="status"], [role="alert"]').filter({ hasText: text }); const cutSync = (page: Page) => page.route(/\/_matrix\/client\/v3\/sync/, (route) => route.abort('connectionfailed')); test.describe('server status banner (#124)', () => { let alice: TestUser; let room: string; test.beforeAll(async () => { test.skip(!(await hsReachable()), `no local homeserver at ${HS} (set E2E_LOCAL_HS)`); alice = await ensureUser(uniq('e2e_status_')); room = await createRoom(alice, 'Status Room'); }); test('calls down: amber strip, dismiss sticks across a reload', async ({ page }) => { await mockKuma(page, serverOf(alice), () => ({ calls: DOWN })); await loginUI(page, alice); await openRoom(page, room); const calls = strip(page, /Voice calls are down right now\. Messages still work\./); await expect(calls).toBeVisible(); await calls.getByRole('button', { name: 'Dismiss' }).click(); await expect(calls).toHaveCount(0); await page.reload(); await openRoom(page, room); await page.waitForTimeout(2000); await expect(calls).toHaveCount(0); }); test('other homeservers never contact Kuma', async ({ page }) => { const requests = await mockKuma(page, 'some.other.server', () => ({ hs: DOWN })); await loginUI(page, alice); await openRoom(page, room); await page.waitForTimeout(3000); expect(requests).toEqual([]); }); test('Kuma failing shows nothing (fail quiet)', async ({ page }) => { const requests = await mockKuma(page, serverOf(alice), () => ({ fail: true })); await loginUI(page, alice); await openRoom(page, room); await expect.poll(() => requests.length).toBeGreaterThan(0); await page.waitForTimeout(1500); await expect(strip(page, /server|Voice calls|Maintenance/i)).toHaveCount(0); }); test('connection lost + Kuma says the server is down: critical strip, not "Connection Lost"', async ({ page, }) => { test.setTimeout(150_000); await mockKuma(page, serverOf(alice), () => ({ hs: DOWN })); await loginUI(page, alice); await openRoom(page, room); // Connected: the server is "having problems" for others, not for us. await expect(strip(page, /server is having problems/)).toBeVisible(); await cutSync(page); const down = page.getByRole('alert').filter({ hasText: /server is down\. We're on it/ }); await expect(down).toBeVisible({ timeout: 120_000 }); await expect(page.getByText('Connection Lost! Reconnecting...')).toHaveCount(0); }); test('connection lost + Kuma checked since and the server is up: "may be your connection"', async ({ page, }) => { test.setTimeout(150_000); await mockKuma(page, serverOf(alice), () => ({ hs: UP })); await loginUI(page, alice); await openRoom(page, room); await cutSync(page); await expect(page.getByText(/server is up, so it may be your internet connection/)).toBeVisible( { timeout: 120_000, }, ); }); test('connection lost + only a stale "up" from Kuma: plain "Connection Lost"', async ({ page, }) => { test.setTimeout(150_000); await mockKuma(page, serverOf(alice), () => ({ hs: UP, staleMinutes: 10 })); await loginUI(page, alice); await openRoom(page, room); await cutSync(page); await expect(page.getByText('Connection Lost! Reconnecting...')).toBeVisible({ timeout: 120_000, }); await page.waitForTimeout(3000); await expect(page.getByText(/may be your internet connection/)).toHaveCount(0); }); });