Files
wordpress-theme-aksrt/tests/forum/browser-smoke.cjs
T

208 lines
14 KiB
JavaScript

const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { spawn } = require('node:child_process')
const edge = process.env.JIO_FORUM_BROWSER || 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
const base = process.env.JIO_FORUM_BASE_URL || 'http://localhost:9081'
const lockedUrl = process.env.JIO_FORUM_LOCKED_URL || ''
const multiVoteUrl = process.env.JIO_FORUM_MULTI_VOTE_URL || ''
const browserPassword = process.env.JIO_FORUM_TEST_PASSWORD || ''
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'jio-forum-browser-'))
const browser = spawn(edge, [
'--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
'--remote-debugging-port=0', `--user-data-dir=${profile}`, 'about:blank',
], { stdio: 'ignore', windowsHide: true })
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const until = async (fn, timeout = 15000) => {
const end = Date.now() + timeout
while (Date.now() < end) {
const result = await fn().catch(() => null)
if (result) return result
await sleep(100)
}
throw new Error('browser timeout')
}
async function run() {
const portFile = path.join(profile, 'DevToolsActivePort')
const port = await until(async () => fs.existsSync(portFile) && Number(fs.readFileSync(portFile, 'utf8').split('\n')[0]))
const target = await until(async () => {
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
return list.find((item) => item.type === 'page')?.webSocketDebuggerUrl
})
const socket = new WebSocket(target)
await new Promise((resolve, reject) => {
socket.addEventListener('open', resolve, { once: true })
socket.addEventListener('error', reject, { once: true })
})
let nextId = 0
const pending = new Map()
const problems = []
socket.addEventListener('message', ({ data }) => {
const item = JSON.parse(data)
if (item.id && pending.has(item.id)) {
const { resolve, reject } = pending.get(item.id)
pending.delete(item.id)
item.error ? reject(new Error(item.error.message)) : resolve(item.result)
}
if (item.method === 'Runtime.exceptionThrown') problems.push(item.params.exceptionDetails.text)
if (item.method === 'Runtime.consoleAPICalled' && item.params.type === 'error') {
problems.push(item.params.args.map((arg) => arg.value || arg.description || '').join(' '))
}
if (item.method === 'Network.loadingFailed' && !item.params.canceled) problems.push(item.params.errorText)
})
const call = (method, params = {}) => new Promise((resolve, reject) => {
const id = ++nextId
pending.set(id, { resolve, reject })
socket.send(JSON.stringify({ id, method, params }))
})
await Promise.all([call('Page.enable'), call('Runtime.enable'), call('Network.enable')])
const check = async (width, height, url, label) => {
await call('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: width < 600 })
await call('Page.navigate', { url: base + url })
await until(async () => {
const result = await call('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })
return result.result.value === 'complete'
})
await sleep(800)
const result = await call('Runtime.evaluate', {
expression: `({forum:!!document.getElementById('forum'),posts:document.querySelectorAll('.jio-forum__post').length,locked:document.body.textContent.includes('内容已隐藏'),leaked:document.documentElement.innerHTML.includes('需要积分查看的正文'),overflow:document.documentElement.scrollWidth>document.documentElement.clientWidth,width:innerWidth})`,
returnByValue: true,
})
const state = result.result.value
if (!state.forum || state.overflow || (url === '/forums/' && state.posts < 1) || (url === lockedUrl && (!state.locked || state.leaked))) {
throw new Error(`${label}: ${JSON.stringify(state)}`)
}
console.log(`${label}: ${JSON.stringify(state)}`)
}
try {
await check(1440, 900, '/forums/', 'desktop home')
await check(390, 844, '/forums/', 'mobile home')
if (lockedUrl) {
await check(390, 844, lockedUrl, 'mobile locked post')
await call('Runtime.evaluate', { expression: `document.body.classList.remove('dark-theme');document.documentElement.classList.remove('dark')` })
await sleep(650)
const before = await call('Runtime.evaluate', { expression: `getComputedStyle(document.querySelector('.jio-forum__locked')).backgroundColor`, returnByValue: true })
await call('Runtime.evaluate', { expression: `document.body.classList.add('dark-theme');document.documentElement.classList.add('dark')` })
await sleep(650)
const after = await call('Runtime.evaluate', { expression: `getComputedStyle(document.querySelector('.jio-forum__locked')).backgroundColor`, returnByValue: true })
if (before.result.value === after.result.value) throw new Error(`forum dark mode does not change locked surface: ${before.result.value}`)
console.log(`dark mode: ${JSON.stringify({ light: before.result.value, dark: after.result.value })}`)
}
if (browserPassword) {
await call('Page.navigate', { url: base + '/sign/?tab=signin' })
await until(async () => {
const state = await call('Runtime.evaluate', { expression: 'document.readyState === "complete" && !!window.jioData?.restNonce', returnByValue: true })
return state.result.value
})
const login = await call('Runtime.evaluate', {
expression: `(async () => { const response=await fetch('/wp-json/jio/v1/auth/login',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-WP-Nonce':window.jioData.restNonce},body:JSON.stringify({target:'forum_admin',password:${JSON.stringify(browserPassword)},redirect_to:${JSON.stringify(base + '/wp-admin/')}})});return {status:response.status,body:await response.json()}})()`,
awaitPromise: true,
returnByValue: true,
})
if (login.result.value?.status !== 200) throw new Error(`login failed: ${JSON.stringify(login.result.value)}`)
for (const [width, height, label] of [[1440, 900, 'desktop admin'], [390, 844, 'mobile admin']]) {
await call('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: width < 600 })
await call('Page.navigate', { url: base + '/wp-admin/admin.php?page=jio-settings#/forum/general' })
const state = await until(async () => {
const result = await call('Runtime.evaluate', { expression: `({app:!!document.querySelector('#jio-admin-app'),ready:document.querySelector('#jio-admin-app')?.textContent.includes('启用论坛'),width:innerWidth,overflow:document.documentElement.scrollWidth>document.documentElement.clientWidth})`, returnByValue: true })
return result.result.value?.ready && result.result.value
}, 20000)
if (!state.app || state.overflow) throw new Error(`${label}: ${JSON.stringify(state)}`)
console.log(`${label}: ${JSON.stringify(state)}`)
}
if (multiVoteUrl) {
await call('Page.navigate', { url: base + multiVoteUrl })
await until(async () => {
const result = await call('Runtime.evaluate', { expression: `document.querySelectorAll('.jio-forum__vote input[type=checkbox]').length >= 2`, returnByValue: true })
return result.result.value
})
await call('Runtime.evaluate', { expression: `(() => {const form=document.querySelector('[data-forum-form=vote]');const boxes=form.querySelectorAll('input[type=checkbox]');boxes[0].checked=true;boxes[1].checked=true;form.requestSubmit();return true})()`, returnByValue: true })
const voted = await until(async () => {
const result = await call('Runtime.evaluate', { expression: `({done:document.querySelector('.jio-forum__vote')?.textContent.includes('已投票'),error:document.querySelector('.jio-forum__vote .jio-forum__message')?.textContent||''})`, returnByValue: true })
return result.result.value?.done && result.result.value
}, 12000)
if (voted.error) throw new Error(`multiple vote browser error: ${voted.error}`)
console.log(`multiple vote browser: ${JSON.stringify(voted)}`)
}
await call('Page.navigate', { url: base + '/posts-edit/' })
const editorState = await until(async () => {
const result = await call('Runtime.evaluate', { expression: `({form:!!document.querySelector('[data-forum-form=post]'),editor:!!document.querySelector('#jio_forum_content'),upload:!!document.querySelector('[data-forum-upload=image]')})`, returnByValue: true })
return result.result.value?.form && result.result.value
})
if (!editorState.editor || !editorState.upload) throw new Error(`forum editor unavailable: ${JSON.stringify(editorState)}`)
console.log(`forum editor: ${JSON.stringify(editorState)}`)
const editorLayout = await call('Runtime.evaluate', { expression: `({overflow:document.documentElement.scrollWidth>document.documentElement.clientWidth,css:[...document.styleSheets].some((sheet)=>sheet.href?.includes('/forum.css'))})`, returnByValue: true })
if (editorLayout.result.value?.overflow || !editorLayout.result.value?.css) throw new Error(`mobile editor layout: ${JSON.stringify(editorLayout.result.value)}`)
console.log(`mobile editor layout: ${JSON.stringify(editorLayout.result.value)}`)
const upload = await call('Runtime.evaluate', {
expression: `(async()=>{const source=await fetch('/wp-content/themes/jio/assets/img/topic.jpg');const blob=await source.blob();const body=new FormData();body.append('file',new File([blob],'topic.jpg',{type:'image/jpeg'}));const response=await fetch('/wp-json/jio/v1/forum/upload/image',{method:'POST',credentials:'same-origin',headers:{'X-WP-Nonce':window.jioForum.nonce},body});const payload=await response.json();return {status:response.status,id:payload.data?.id,url:payload.data?.url,message:payload.message}})()`,
awaitPromise: true,
returnByValue: true,
})
if (upload.result.value?.status !== 200 || !upload.result.value?.url) throw new Error(`forum upload failed: ${JSON.stringify(upload.result.value)}`)
console.log(`forum upload: ${JSON.stringify({ status: upload.result.value.status, id: upload.result.value.id })}`)
const submitted = await call('Runtime.evaluate', {
expression: `(() => { const form=document.querySelector('[data-forum-form=post]');form.querySelector('[name=title]').value='浏览器表单测试';const plate=form.querySelector('[name=plate_id]');plate.value=plate.querySelector('option[value]:not([value=""])')?.value||'';const editor=window.tinymce?.get('jio_forum_content');if(editor&&!editor.isHidden())editor.setContent('<p>浏览器表单正文</p>');else form.querySelector('[name=content]').value='浏览器表单正文';form.requestSubmit();return {plate:plate.value} })()`,
returnByValue: true,
})
if (!submitted.result.value?.plate) throw new Error('no publishable plate in editor')
const saved = await until(async () => {
const result = await call('Runtime.evaluate', { expression: `({url:location.pathname,error:document.querySelector('.jio-forum__form .jio-forum__message')?.textContent||''})`, returnByValue: true })
return result.result.value?.url.startsWith('/forum-post/') && result.result.value
}, 12000)
console.log(`browser form submit: ${JSON.stringify(saved)}`)
await call('Page.navigate', { url: base + '/user/forum/' })
const center = await until(async () => {
const result = await call('Runtime.evaluate', { expression: `({panel:!!document.querySelector('#panel-forum'),posts:document.querySelector('#panel-forum')?.textContent.includes('帖子'),plates:document.querySelector('#panel-forum')?.textContent.includes('版块'),css:[...document.styleSheets].some((sheet)=>sheet.href?.includes('/forum.css'))})`, returnByValue: true })
return result.result.value?.panel && result.result.value
})
if (!center.posts || !center.plates || !center.css) throw new Error(`forum user panel unavailable: ${JSON.stringify(center)}`)
console.log(`user center: ${JSON.stringify(center)}`)
await call('Page.navigate', { url: base + '/author/forum_admin/' })
const publicHome = await until(async () => {
const result = await call('Runtime.evaluate', { expression: `({forum:!!document.querySelector('#panel-forum'),css:[...document.styleSheets].some((sheet)=>sheet.href?.includes('/forum.css'))})`, returnByValue: true })
return result.result.value?.forum && result.result.value
})
if (!publicHome.css) throw new Error(`public user home CSS missing: ${JSON.stringify(publicHome)}`)
console.log(`public user home: ${JSON.stringify(publicHome)}`)
}
if (problems.length) throw new Error(`browser errors: ${problems.join('; ')}`)
} finally {
call('Browser.close').catch(() => {})
await sleep(150)
socket.close()
}
}
const cleanup = async () => {
const target = path.resolve(profile)
const tempRoot = path.resolve(os.tmpdir()) + path.sep
if (target.startsWith(tempRoot) && path.basename(target).startsWith('jio-forum-browser-')) {
for (let attempt = 0; attempt < 20; attempt++) {
try {
fs.rmSync(target, { recursive: true, force: true })
return
} catch (error) {
if (error.code !== 'EBUSY' && error.code !== 'EPERM') throw error
await sleep(250)
}
}
throw new Error('browser profile is still in use')
}
}
run().then(async () => {
browser.kill()
await cleanup()
}).catch(async (error) => {
console.error(error)
browser.kill()
await cleanup().catch((cleanupError) => console.error(cleanupError))
process.exitCode = 1
})