Add upload library management and ops diagnostics
This commit is contained in:
65
tests/admin-ops.spec.js
Normal file
65
tests/admin-ops.spec.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
test('monitor, P2P diagnostics and upload manager render and send guarded actions', async ({ page }) => {
|
||||
const dir = path.resolve('plans/admin-ops-shots'); fs.mkdirSync(dir, { recursive: true });
|
||||
const calls = [];
|
||||
await page.route('**/api/**', async route => {
|
||||
const req = route.request(), url = new URL(req.url()); calls.push({ path: url.pathname, method: req.method(), body: req.postData() });
|
||||
let body = { ok: true };
|
||||
if (url.pathname === '/api/admin/me') body = { ok: true, enabled: true, admin: true };
|
||||
if (url.pathname === '/api/admin/ops/metrics') body = { ok: true, current: { at: Date.now(), inflight: 2, rssBytes: 88_000_000, cpuPercent: 11.2, ytdlp: { queued: 1, workers: 2, busy: 1 }, media: { queued: 3, active: 'vid' }, cache: { hits: 9, misses: 2 }, routes: { '/api/version': { requests: 30, p50: 2, p95: 9, p99: 14, errors: 0, errorRate: 0 } }, volumes: [{ name: 'media', freeBytes: 5_000_000_000, totalBytes: 20_000_000_000 }] }, history: [{ at: Date.now() - 60_000, routes: { '/api/version': { p95: 10 } } }, { at: Date.now(), routes: { '/api/version': { p95: 9 } } }] };
|
||||
if (url.pathname === '/api/admin/p2p/devices') body = { ok: true, network: { online: 1, content: 14, intakeActive: 0, intakeOpen: 1 }, devices: [{ id: 'dev_0123456789abcdef', fingerprintShort: 'phone-4821', profile: 'family', userAgent: 'iPhone · iOS', banned: false, online: true, lastSeenAt: Date.now(), holdings: 8, holdingBytes: 900_000_000, bytesUp: 12_000, bytesDown: 0 }] };
|
||||
if (url.pathname === '/api/admin/uploads/manage') body = { ok: true, total: 1, offset: 0, owners: [{ owner: '(admin)', count: 1, bytes: 1_500_000 }], uploads: [{ id: 'upl_0123456789ab', title: 'Sunday service', artist: 'Choir', album: 'Live', ext: 'mp3', size: 1_500_000, gen: 3, listed: true }], orphans: [] };
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
|
||||
});
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
await page.goto('/admin.html#editor');
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
for (const theme of ['dark', 'light', 'contrast']) for (const width of [390, 768, 1280]) for (const tab of ['ops', 'devices', 'uploads']) {
|
||||
await page.evaluate(theme => { document.documentElement.dataset.editorTheme = theme; }, theme);
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const nav = width <= 760 ? '#tabsBottom' : '#tabsTop';
|
||||
const button = page.locator(`${nav} [data-tab="${tab}"]`); await button.scrollIntoViewIfNeeded(); await button.click();
|
||||
await expect(page.locator(`#p-${tab}`)).toBeVisible();
|
||||
await page.waitForTimeout(30);
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth);
|
||||
expect(overflow, `${tab} page overflow at ${width}/${theme}`).toBe(false);
|
||||
const ui = await page.locator(`#p-${tab}`).evaluate(panel => {
|
||||
const rgb = value => (value.match(/[\d.]+/g) || []).slice(0, 3).map(Number);
|
||||
const luminance = value => {
|
||||
const [r, g, b] = rgb(value).map(n => n / 255).map(n => n <= .04045 ? n / 12.92 : ((n + .055) / 1.055) ** 2.4);
|
||||
return .2126 * r + .7152 * g + .0722 * b;
|
||||
};
|
||||
const ratio = (fg, bg) => { const a = luminance(fg), b = luminance(bg); return (Math.max(a, b) + .05) / (Math.min(a, b) + .05); };
|
||||
const bodyBg = getComputedStyle(document.body).backgroundColor;
|
||||
const checks = [...panel.querySelectorAll('.btn:not([disabled]), .field')].filter(el => el.getClientRects().length).map(el => ({ tag: el.tagName, h: el.getBoundingClientRect().height }));
|
||||
const text = [...panel.querySelectorAll('h1.page, h2, .lead, .muted, .metric span, .metric small, .tbl td, .tbl th')].filter(el => el.getClientRects().length).map(el => ratio(getComputedStyle(el).color, bodyBg));
|
||||
return { checks, text };
|
||||
});
|
||||
expect(ui.checks.every(el => el.h >= 43.5), `${tab} controls below 44px at ${width}/${theme}: ${JSON.stringify(ui.checks)}`).toBe(true);
|
||||
expect(ui.text.every(value => value >= 4.5), `${tab} heading/lead contrast below AA at ${width}/${theme}: ${ui.text}`).toBe(true);
|
||||
if (width <= 760) {
|
||||
const lastCard = page.locator(`#p-${tab} .card`).last();
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
const clearance = await lastCard.evaluate(el => ({ bottom: el.getBoundingClientRect().bottom, navTop: document.querySelector('#tabsBottom').getBoundingClientRect().top }));
|
||||
expect(clearance.bottom, `${tab} last card remains behind bottom nav at ${width}/${theme}`).toBeLessThanOrEqual(clearance.navTop);
|
||||
}
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.screenshot({ path: path.join(dir, `${tab}-${theme}-${width}.png`) });
|
||||
}
|
||||
await page.locator('#tabsTop [data-tab="devices"]').click();
|
||||
await page.locator('[data-device-sync]').click();
|
||||
await page.locator('[data-device-holdings]').click();
|
||||
await page.locator('[data-device-ban]').click();
|
||||
await page.locator('#tabsTop [data-tab="uploads"]').click();
|
||||
await page.locator('[data-upload-save]').click();
|
||||
await page.locator('[data-upload-select]').check();
|
||||
await page.locator('#uploadBulkAction').selectOption('unlist'); await page.locator('#uploadBulkApply').click();
|
||||
expect(calls.some(c => c.path.endsWith('/reannounce'))).toBe(true);
|
||||
expect(calls.some(c => c.path.endsWith('/holdings') && c.method === 'DELETE')).toBe(true);
|
||||
expect(calls.some(c => c.path.endsWith('/ban') && c.method === 'POST')).toBe(true);
|
||||
expect(calls.some(c => c.path.endsWith('/upl_0123456789ab') && c.method === 'PATCH')).toBe(true);
|
||||
expect(calls.some(c => c.path === '/api/admin/uploads/bulk')).toBe(true);
|
||||
});
|
||||
Reference in New Issue
Block a user