test(frontend): test dymny zbudowanego panelu
Ładuje artefakt z dist/ w jsdom, podstawiając atrapy WebSocketu i fetcha, a następnie sprawdza dwie rzeczy naraz: - czy dane z serwera faktycznie renderują się w interfejsie (aktualny utwór, kolejka, historia, baza plików, głośność, tryb zapętlania, odmiana liczebników, formatowanie czasu) - czy kliknięcia trafiają we właściwe endpointy - każdy przycisk sterujący porównywany jest z oczekiwaną parą metoda + ścieżka Razem 31 sprawdzeń, uruchamiane przez npm run smoke po npm run build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bbd8a321f4
commit
2feb963e6a
1 file changed
+167
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
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', calls.includes('GET /api/uploads'))
|
||||
|
||||
// --- 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).includes(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')
|
||||
|
||||
// Modal ustawień otwiera się i zapisuje domyślną liczbę odtworzeń.
|
||||
;[...app.querySelectorAll('button')].find((b) => b.textContent.includes('Ustawienia'))?.click()
|
||||
await settle()
|
||||
check('modal ustawień pokazuje serwery', doc.body.textContent.includes('ID: 1'))
|
||||
|
||||
const before = calls.length
|
||||
;[...doc.querySelectorAll('button')].find((b) => b.textContent.includes('Zapisz'))?.click()
|
||||
await settle()
|
||||
check('zapis ustawień -> POST /api/settings/default_plays',
|
||||
calls.slice(before).includes('POST /api/settings/default_plays'))
|
||||
|
||||
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)
|
||||
Reference in new issue
Block a user