/**
* Opens each page of the real app (router, navigation, page, footer) with
* fetch stubbed, the way a visitor gets it. A page that throws while loading,
* logs a Vue warning, or goes blank when the backend is down shows up here.
* The checks are about what a page says, not how it looks.
*/
import { openPage, publicFile, stubFetch } from './helpers'
const PAGES = [
['home', '/en'],
['about', '/en/about'],
['communities', '/en/communities'],
['servers', '/en/servers'],
['statistics', '/en/stats'],
['partners', '/en/partners'],
['privacy', '/en/privacy'],
['terms', '/en/terms'],
]
let wrapper
let warnings
beforeEach(() => {
warnings = []
vi.spyOn(console, 'warn').mockImplementation((...args) => warnings.push(args.join(' ')))
vi.spyOn(console, 'error').mockImplementation((...args) => warnings.push(args.join(' ')))
vi.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(() => {
wrapper?.unmount()
wrapper = undefined
document.body.innerHTML = ''
vi.unstubAllGlobals()
vi.restoreAllMocks()
})
describe.each(PAGES)('the %s page', (_name, address) => {
test('opens with the navigation and footer around it, and no Vue warnings', async () => {
stubFetch()
wrapper = await openPage(address)
expect(wrapper.find('nav, header').exists()).toBe(true)
expect(wrapper.find('footer').exists()).toBe(true)
expect(wrapper.find('main').text().length).toBeGreaterThan(0)
expect(warnings.filter((w) => w.includes('[Vue warn]'))).toEqual([])
})
test('still opens, and says something, when the backend is down', async () => {
stubFetch()
wrapper = await openPage(address)
expect(wrapper.find('main').text().trim()).not.toBe('')
expect(wrapper.find('footer').exists()).toBe(true)
})
})
describe('home', () => {
test('shows the live numbers from the metrics feed', async () => {
stubFetch({
metrics: 'beammp_players_online 2783\nbeammp_public_servers 2959\nbeammp_all_servers 4663\n',
})
wrapper = await openPage('/en')
const text = wrapper.text()
expect(text).toContain('2783')
expect(text).toContain('2959')
expect(text).toContain('4663')
})
test('shows N/A rather than a broken number when the metrics are down', async () => {
vi.stubGlobal('fetch', async () => {
throw new Error('network down')
})
wrapper = await openPage('/en')
expect(wrapper.text()).toContain('N/A')
expect(wrapper.text()).not.toContain('undefined')
})
})
describe('the docs links on the home page', () => {
test('point at the docs in the language of the page', async () => {
stubFetch()
wrapper = await openPage('/en')
const english = wrapper.findAll('a').map((a) => a.attributes('href'))
expect(english).toContain('https://docs.beammp.com/')
expect(english).toContain('https://docs.beammp.com/scripting/mod-reference/')
wrapper.unmount()
wrapper = await openPage('/de')
const german = wrapper.findAll('a').map((a) => a.attributes('href'))
expect(german).toContain('https://docs.beammp.com/de/')
expect(german).toContain('https://docs.beammp.com/de/scripting/mod-reference/')
})
})
describe('servers', () => {
const servers = JSON.parse(publicFile('servers.json')).slice(0, 25)
test('lists the servers the backend returns', async () => {
stubFetch({ servers })
wrapper = await openPage('/en/servers')
const text = wrapper.find('main').text()
expect(text).toContain('MTRTR Christmas')
expect(text).not.toContain('Failed to load servers')
})
// A server's name is set by whoever hosts it and is shown as HTML so the
// colour codes can apply, so the page escapes it itself. (The API also strips
// every `<` at heartbeat, which is why the real feed has none: this is the
// second layer, and it holds even if the first ever slips.)
test('a server name with HTML in it is shown as text, not turned into page elements', async () => {
const hostile = {
...servers[0],
sname: '^1Evil
bold',
}
stubFetch({ servers: [hostile] })
wrapper = await openPage('/en/servers')
expect(document.querySelector('img[src="x-probe"]')).toBeNull()
expect(document.querySelector('#probe-b')).toBeNull()
expect(wrapper.find('main').text()).toContain('bold')
})
test('names with & and quotes still read as written, and keep their colours', async () => {
const named = [
{ ...servers[0], ident: 'a1', sname: '^1C&B\'s ^2"Banger" League' },
{ ...servers[1], ident: 'a2', sname: 'Plain & simple' },
]
stubFetch({ servers: named })
wrapper = await openPage('/en/servers')
const text = wrapper.find('main').text()
expect(text).toContain('C&B\'s "Banger" League')
expect(text).toContain('Plain & simple')
expect(text).not.toContain('&')
// ^1 is dark blue: the colour codes are not shown, and the colour is applied
expect(text).not.toContain('^1')
expect(wrapper.find('.name-cell span[style*="color"]').exists()).toBe(true)
})
test('text that looks like an HTML entity is shown exactly as typed', async () => {
stubFetch({
servers: [{ ...servers[0], ident: 'a1', sname: 'Tags <b> and & shown as typed' }],
})
wrapper = await openPage('/en/servers')
expect(wrapper.find('main').text()).toContain('Tags <b> and & shown as typed')
})
describe('searching', () => {
const named = () => [
{
...servers[0],
ident: 'a1',
sname: "^1C&B's ^2Banger League",
sdesc: 'drift night',
owner: 'zed',
map: '/levels/west_coast_usa/info.json',
},
{
...servers[1],
ident: 'a2',
sname: '^4Quiet Valley',
sdesc: 'relaxed driving',
owner: 'amy',
map: '/levels/italy/info.json',
},
]
const search = async (query) => {
await wrapper.find('input.search-input').setValue(query)
return wrapper.findAll('.name-cell').map((cell) => cell.text())
}
test('finds a name containing & or an apostrophe by what you type', async () => {
stubFetch({ servers: named() })
wrapper = await openPage('/en/servers')
expect(await search("c&b's")).toEqual([expect.stringContaining("C&B's Banger League")])
expect(await search('banger league')).toHaveLength(1)
})
test('does not match on the colour markup behind the name', async () => {
stubFetch({ servers: named() })
wrapper = await openPage('/en/servers')
expect(await search('span')).toEqual([])
expect(await search('color')).toEqual([])
})
test('still finds by description and owner', async () => {
stubFetch({ servers: named() })
wrapper = await openPage('/en/servers')
expect(await search('relaxed')).toEqual([expect.stringContaining('Quiet Valley')])
expect(await search('zed')).toEqual([expect.stringContaining("C&B's")])
})
})
test('says so when the servers cannot be loaded', async () => {
stubFetch()
wrapper = await openPage('/en/servers')
expect(wrapper.find('main').text()).toContain('Failed to load servers')
})
})
describe('the policy pages', () => {
test.each([
['privacy', '/en/privacy', 'docs/privacy_policy.md'],
['terms', '/en/terms', 'docs/terms.md'],
])('%s shows the text of its document', async (_name, address, file) => {
stubFetch()
wrapper = await openPage(address)
const firstHeading = publicFile(file)
.split('\n')
.find((line) => /^#\s+\S/.test(line))
.replace(/^#\s+/, '')
.trim()
expect(wrapper.find('main').text()).toContain(firstHeading)
})
})
describe('partners', () => {
test('shows each partner in the partners file', async () => {
stubFetch()
wrapper = await openPage('/en/partners')
const partners = JSON.parse(publicFile('partners.json'))
const list = Array.isArray(partners) ? partners : Object.values(partners).flat()
expect(list.length).toBeGreaterThan(0)
const names = list.map((p) => p.name).filter(Boolean)
expect(names.length).toBeGreaterThan(0)
for (const name of names) expect(wrapper.find('main').text(), name).toContain(name)
})
})
describe('getting around', () => {
// The Communities, Stats and forum entries sit in dropdown menus that only
// draw their links once opened, so they are not on a freshly loaded page.
test('the home page links to the main pages in the current language', async () => {
stubFetch()
wrapper = await openPage('/en')
const hrefs = wrapper.findAll('a').map((a) => a.attributes('href'))
for (const page of ['/en/', '/en/communities', '/en/partners', '/en/servers', '/en/about']) {
expect(hrefs, page).toContain(page)
}
})
test('the navigation bar links out to the docs, the store, GitHub and Patreon', async () => {
stubFetch()
wrapper = await openPage('/en')
const hrefs = wrapper.findAll('nav a, header a').map((a) => a.attributes('href'))
for (const url of [
'https://docs.beammp.com',
'https://store.beammp.com',
'https://github.com/BeamMP/BeamMP',
'https://www.patreon.com/BeamMP',
]) {
expect(hrefs, url).toContain(url)
}
})
test('the language is part of every internal link', async () => {
stubFetch()
wrapper = await openPage('/en')
const internal = wrapper
.findAll('a')
.map((a) => a.attributes('href'))
.filter((h) => h.startsWith('/') && !h.startsWith('/installer'))
expect(internal.length).toBeGreaterThan(4)
for (const href of internal) expect(href).toMatch(/^\/en(\/|$)/)
})
})
describe('the not-found page', () => {
test('opens at a missing address, with its message and a way back', async () => {
stubFetch()
wrapper = await openPage('/en/this-page-does-not-exist')
expect(wrapper.find('main').text()).toContain('404')
expect(wrapper.find('main a').exists()).toBe(true)
expect(warnings.filter((w) => w.includes('[Vue warn]'))).toEqual([])
})
})