Files
zyrarbot/frontend/smoke-test.mjs
T
mmacierzanka d6bd302165
Backend / Lint i formatowanie (pull_request) Successful in 3s
Backend / Testy i kontrakt API (pull_request) Successful in 10s
Panel / Lint, typy i build (pull_request) Successful in 18s
Backend / Testowy build obrazu (pull_request) Successful in 15s
feat: restore the last queue per server
2026-08-21 17:10:40 +02:00

161 lines
5.6 KiB
JavaScript

/**
* Test dymny zbudowanego panelu.
*
* Ładuje artefakt z dist/ do jsdom, podstawia atrapę WebSocketu i fetcha,
* a następnie sprawdza dwie rzeczy: czy interfejs renderuje dane z serwera
* i czy kliknięcia trafiają w odpowiednie endpointy API.
*
* Uruchomienie: npm run build && npm run smoke
*/
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { JSDOM } from 'jsdom'
const DIST = resolve(dirname(fileURLToPath(import.meta.url)), 'dist')
const status = {
playing: true,
paused: false,
loop_single: false,
loop_playlist: true,
volume: 42,
current: {
id: 'x1',
title: 'Testowy utwór',
url: 'https://youtu.be/abc',
duration: 200,
elapsed: 12.3,
plays_left: 4,
},
queue: [
{ id: 'x1', title: 'Testowy utwór', url: 'https://youtu.be/abc', plays_left: 4 },
{ id: 'x2', title: 'Drugi utwór', url: null, plays_left: 2 },
],
queue_index: 0,
history: [
{
id: 'h1',
title: 'Stary utwór',
url: 'https://youtu.be/old',
duration: 65,
is_local: false,
filepath: null,
},
],
voice_channels: [{ guild_id: '1', guild_name: 'Serwer', channel_id: '10', channel_name: 'Muzyka' }],
active_voice_channel: {
guild_id: '1',
guild_name: 'Serwer',
channel_id: '10',
channel_name: 'Muzyka',
},
text_channels: [],
active_text_channel_id: null,
guilds: [
{
guild_id: '1',
guild_name: 'Serwer',
text_channels: [{ channel_id: '20', channel_name: 'ogolny' }],
active_notification_channel_id: '20',
},
],
default_plays: 5,
saved_queue_available: true,
}
const calls = []
const html = readFileSync(`${DIST}/index.html`, 'utf8')
const dom = new JSDOM(html, {
url: 'http://localhost:8000/',
runScripts: 'dangerously',
pretendToBeVisual: true,
beforeParse(window) {
window.WebSocket = class {
constructor(url) {
this.url = url
setTimeout(() => {
this.onopen?.()
this.onmessage?.({ data: JSON.stringify(status) })
}, 0)
}
close() {}
send() {}
}
window.fetch = async (path, init) => {
calls.push(`${init?.method ?? 'GET'} ${path}`)
return {
ok: true,
status: 200,
json: async () =>
String(path).includes('/api/uploads')
? { files: [{ filename: 'sample.mp3', size_mb: 3.2, mtime: 1 }] }
: { ...status, mode: 'single' },
}
}
},
})
const scriptSrc = html.match(/src="(\/assets\/[^"]+\.js)"/)[1]
dom.window.eval(readFileSync(`${DIST}${scriptSrc}`, 'utf8'))
const settle = () => new Promise((done) => setTimeout(done, 250))
await settle()
const doc = dom.window.document
const app = doc.getElementById('app')
const text = () => app.textContent
const byTitle = (title) => app.querySelector(`[title="${title}"]`)
const results = []
const check = (name, ok) => results.push([name, ok])
const hasCall = (expected) => calls.some((call) => call === expected || call.startsWith(`${expected}?`))
check('aplikacja zamontowana', app.children.length > 0)
check('nagłówek', text().includes('ŻyrarBOT'))
check('status połączenia', text().includes('Połączony: Muzyka (Serwer)'))
check('aktualny utwór', text().includes('Testowy utwór'))
check('drugi utwór z kolejki', text().includes('Drugi utwór'))
check('odmiana licznika kolejki', text().includes('2 utwory'))
check('historia', text().includes('Stary utwór'))
check('plik z serwerowej bazy', text().includes('sample.mp3'))
check('odmiana licznika plików', text().includes('1 plik'))
check('badge zapętlania kolejki', text().includes('Zapętlenie kolejki'))
check('głośność z serwera', text().includes('42%'))
check('sformatowany czas trwania', text().includes('3:20'))
check('przycisk Odłącz przy połączeniu', text().includes('Odłącz'))
check('ikony renderowane jako SVG', app.querySelectorAll('svg').length > 10)
check('pozycje list', app.querySelectorAll('.queue-item').length === 4)
check('podświetlona aktywna pozycja', app.querySelectorAll('.playing-item').length === 1)
check('animacja winylu przy odtwarzaniu', app.querySelector('.vinyl-record.playing') !== null)
check('pobrano listę plików na starcie', hasCall('GET /api/uploads'))
check('przycisk przywracania kolejki', text().includes('Przywróć ostatnią kolejkę'))
// --- Interakcje: kliknięcie ma trafić w konkretny endpoint ---------------
async function clickExpects(title, expected) {
const before = calls.length
byTitle(title)?.click()
await settle()
check(`${title} -> ${expected}`, calls.slice(before).some((call) =>
call === expected || call.startsWith(`${expected}?`)))
}
await clickExpects('Wstrzymaj', 'POST /api/pause')
await clickExpects('Pomiń utwór', 'POST /api/skip')
await clickExpects('Zatrzymaj i wyczyść', 'POST /api/stop')
await clickExpects('Zapętlenie: cała kolejka (kliknij, aby zapętlić jeden utwór)', 'POST /api/loop_cycle')
await clickExpects('Odtwarzaj od tej pozycji', 'POST /api/jump')
await clickExpects('Usuń z kolejki', 'POST /api/remove')
await clickExpects('Przesuń w dół', 'POST /api/reorder')
await clickExpects('Zwiększ o 1', 'POST /api/queue/plays')
await clickExpects('Dodaj do kolejki', 'POST /api/play_file')
await clickExpects('Dodaj ponownie do kolejki', 'POST /api/play')
await clickExpects('Usuń z serwera', 'DELETE /api/uploads/sample.mp3')
const failed = results.filter(([, ok]) => !ok)
for (const [name, ok] of results) console.log(`${ok ? 'OK ' : 'FAIL'} ${name}`)
console.log(failed.length ? `\n${failed.length} z ${results.length} niepowodzeń` : `\nOK - ${results.length} sprawdzeń`)
process.exit(failed.length ? 1 : 0)