chore: 移除冒烟测试脚本与截图
18 个文件变更
+3
-900
fqh@fangqihang.cn
| •.gitignore | +3 -0 |
| •.smoke/01-splash.png | 二进制 |
| •.smoke/02-search.png | 二进制 |
| •.smoke/02b-theme-blue.png | 二进制 |
| •.smoke/03-playlists.png | 二进制 |
| •.smoke/04-settings.png | 二进制 |
| •.smoke/04b-settings-bottom.png | 二进制 |
| •.smoke/04c-theme-blue.png | 二进制 |
| •.smoke/05-account.png | 二进制 |
| •.smoke/06-about.png | 二进制 |
| •.smoke/07-search-back.png | 二进制 |
| •.smoke/icon-preview.png | 二进制 |
| •.smoke/pe-version.cjs | +0 -288 |
| •.smoke/release.txt | +0 -4 |
| •.smoke/report.json | +0 -66 |
| •.smoke/smoke.cjs | +0 -284 |
| •.smoke/verify-icon.cjs | +0 -92 |
| •.smoke/verify-nsis-text.cjs | +0 -166 |
变更内容
diff --git a/.gitignore b/.gitignore
index 909ecc5..fc0fb1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ client.log
# OS
.DS_Store
Thumbs.db
+
+# Smoke 测试
+.smoke/
diff --git a/.smoke/01-splash.png b/.smoke/01-splash.png
deleted file mode 100644
index 8b76e42..0000000
Binary files a/.smoke/01-splash.png and /dev/null differ
diff --git a/.smoke/02-search.png b/.smoke/02-search.png
deleted file mode 100644
index 507913c..0000000
Binary files a/.smoke/02-search.png and /dev/null differ
diff --git a/.smoke/02b-theme-blue.png b/.smoke/02b-theme-blue.png
deleted file mode 100644
index 21b4b9f..0000000
Binary files a/.smoke/02b-theme-blue.png and /dev/null differ
diff --git a/.smoke/03-playlists.png b/.smoke/03-playlists.png
deleted file mode 100644
index a382481..0000000
Binary files a/.smoke/03-playlists.png and /dev/null differ
diff --git a/.smoke/04-settings.png b/.smoke/04-settings.png
deleted file mode 100644
index 7f6a247..0000000
Binary files a/.smoke/04-settings.png and /dev/null differ
diff --git a/.smoke/04b-settings-bottom.png b/.smoke/04b-settings-bottom.png
deleted file mode 100644
index dad6a8d..0000000
Binary files a/.smoke/04b-settings-bottom.png and /dev/null differ
diff --git a/.smoke/04c-theme-blue.png b/.smoke/04c-theme-blue.png
deleted file mode 100644
index 2d94709..0000000
Binary files a/.smoke/04c-theme-blue.png and /dev/null differ
diff --git a/.smoke/05-account.png b/.smoke/05-account.png
deleted file mode 100644
index aff37d8..0000000
Binary files a/.smoke/05-account.png and /dev/null differ
diff --git a/.smoke/06-about.png b/.smoke/06-about.png
deleted file mode 100644
index 0af58d5..0000000
Binary files a/.smoke/06-about.png and /dev/null differ
diff --git a/.smoke/07-search-back.png b/.smoke/07-search-back.png
deleted file mode 100644
index 507913c..0000000
Binary files a/.smoke/07-search-back.png and /dev/null differ
diff --git a/.smoke/icon-preview.png b/.smoke/icon-preview.png
deleted file mode 100644
index f36ebbe..0000000
Binary files a/.smoke/icon-preview.png and /dev/null differ
diff --git a/.smoke/pe-version.cjs b/.smoke/pe-version.cjs
deleted file mode 100644
index ea572e0..0000000
--- a/.smoke/pe-version.cjs
+++ /dev/null
@@ -1,288 +0,0 @@
-/**
- * PE 版本资源(RT_VERSION)解析器
- *
- * 为什么要自己写:本环境下 PowerShell 的 FileVersionInfo 能读,但 Shell.Application(COM)
- * 与 Add-Type 都被安全策略拦截。而 Windows 资源管理器「详细信息」标签页读取的正是
- * RT_VERSION 资源,所以这里直接解析 PE 与版本块结构给出结论。
- *
- * 输出:固定版本号、VarFileInfo\Translation、每个语言块的 StringFileInfo 键值。
- *
- * 用法:node .smoke/pe-version.cjs [exe 路径...]
- */
-
-const fs = require('fs')
-const path = require('path')
-
-const RT_VERSION = 16
-const align4 = (n) => (n + 3) & ~3
-
-const DEFAULT_TARGETS = [
- path.resolve(__dirname, '..', 'build', 'bin', 'starmusic.exe'),
- path.resolve(__dirname, '..', 'build', 'bin', 'starmusic-pc-1.3.0-Setup.exe'),
-]
-
-// ============================================================
-// PE 结构
-// ============================================================
-
-function readWStr(buf, offset, maxChars = 256) {
- const chars = []
- for (let i = 0; i < maxChars; i++) {
- const c = buf.readUInt16LE(offset + i * 2)
- if (c === 0) break
- chars.push(c)
- }
- return String.fromCharCode(...chars)
-}
-
-function parsePE(buf) {
- if (buf.readUInt16LE(0) !== 0x5a4d) throw new Error('缺少 MZ 头,不是 PE 文件')
- const peOff = buf.readUInt32LE(0x3c)
- if (buf.readUInt32LE(peOff) !== 0x00004550) throw new Error('缺少 PE 签名')
- const coff = peOff + 4
- const numSections = buf.readUInt16LE(coff + 2)
- const optSize = buf.readUInt16LE(coff + 16)
- const opt = coff + 20
- const is64 = buf.readUInt16LE(opt) === 0x20b
- const ddOff = opt + (is64 ? 112 : 96)
- const resRVA = buf.readUInt32LE(ddOff + 2 * 8)
- const resSize = buf.readUInt32LE(ddOff + 2 * 8 + 4)
-
- const sections = []
- const secOff = opt + optSize
- for (let i = 0; i < numSections; i++) {
- const o = secOff + i * 40
- sections.push({
- name: buf.toString('ascii', o, o + 8).replace(/\0+$/, ''),
- virtualSize: buf.readUInt32LE(o + 8),
- virtualAddress: buf.readUInt32LE(o + 12),
- rawSize: buf.readUInt32LE(o + 16),
- rawPtr: buf.readUInt32LE(o + 20),
- })
- }
- return { sections, resRVA, resSize, is64 }
-}
-
-function rvaToOffset(sections, rva) {
- for (const s of sections) {
- const size = Math.max(s.virtualSize, s.rawSize)
- if (rva >= s.virtualAddress && rva < s.virtualAddress + size) {
- return s.rawPtr + (rva - s.virtualAddress)
- }
- }
- return -1
-}
-
-/** 取指定资源类型的数据项列表 */
-function findResources(buf, pe, typeId) {
- const resBase = rvaToOffset(pe.sections, pe.resRVA)
- if (resBase < 0) return []
- const dirAt = (rel) => ({
- named: buf.readUInt16LE(resBase + rel + 12),
- ided: buf.readUInt16LE(resBase + rel + 14),
- })
- const entryAt = (relDir, i) => {
- const o = resBase + relDir + 16 + i * 8
- return { name: buf.readUInt32LE(o), child: buf.readUInt32LE(o + 4) }
- }
-
- const out = []
- const root = dirAt(0)
- for (let t = 0; t < root.named + root.ided; t++) {
- const te = entryAt(0, t)
- if (te.name & 0x80000000) continue
- if ((te.name & 0xffff) !== typeId) continue
-
- const typeDirOff = te.child & 0x7fffffff
- const typeDir = dirAt(typeDirOff)
- for (let n = 0; n < typeDir.named + typeDir.ided; n++) {
- const ne = entryAt(typeDirOff, n)
- const langDirOff = ne.child & 0x7fffffff
- const langDir = dirAt(langDirOff)
- for (let l = 0; l < langDir.named + langDir.ided; l++) {
- const le = entryAt(langDirOff, l)
- const dataOff = resBase + (le.child & 0x7fffffff)
- const rva = buf.readUInt32LE(dataOff)
- const size = buf.readUInt32LE(dataOff + 4)
- const offset = rvaToOffset(pe.sections, rva)
- if (offset < 0) continue
- out.push({ langId: le.name & 0x80000000 ? null : le.name & 0xffff, offset, size })
- }
- }
- }
- return out
-}
-
-// ============================================================
-// VERSIONINFO 块
-// ============================================================
-
-function parseNode(buf, start) {
- const length = buf.readUInt16LE(start)
- const valueLength = buf.readUInt16LE(start + 2)
- const type = buf.readUInt16LE(start + 4)
- const key = readWStr(buf, start + 6)
- const keyBytes = (key.length + 1) * 2
- return {
- start,
- length,
- valueLength,
- type,
- key,
- keyBytes,
- valueOffset: start + align4(6 + keyBytes),
- childrenOffset: start + align4(6 + keyBytes + valueLength),
- }
-}
-
-/** 解析 StringFileInfo → [{ langKey, strings }] */
-function parseStringFileInfo(buf, blk) {
- const node = parseNode(buf, blk)
- const limit = blk + node.length
- const blocks = []
- let p = node.childrenOffset
- while (p + 6 <= limit) {
- const langNode = parseNode(buf, p)
- if (!langNode.length) break
- const strings = {}
- const llimit = p + langNode.length
- let q = langNode.childrenOffset
- while (q + 6 <= llimit) {
- const sNode = parseNode(buf, q)
- if (!sNode.length) break
- strings[sNode.key] = readWStr(buf, sNode.valueOffset, 4096)
- q = align4(q + sNode.length)
- }
- blocks.push({ langKey: langNode.key, strings })
- p = align4(p + langNode.length)
- }
- return blocks
-}
-
-/** 解析 VarFileInfo\Translation → [{lang, codePage}] */
-function parseTranslations(buf, vfiBlk) {
- // VarFileInfo 自身没有值,真正的数据在其子节点 Translation 的 value 里
- const trBlk = findChild(buf, vfiBlk, 'Translation')
- if (trBlk < 0) return []
- const node = parseNode(buf, trBlk)
- const total = node.valueLength
- const out = []
- for (let i = 0; i + 4 <= total; i += 4) {
- const d = buf.readUInt32LE(node.valueOffset + i)
- out.push({ lang: d & 0xffff, codePage: (d >>> 16) & 0xffff })
- }
- return out
-}
-
-/** 在根节点的子节点里按 key 找 */
-function findChild(buf, rootBlk, key) {
- const root = parseNode(buf, rootBlk)
- const limit = rootBlk + root.length
- let p = root.childrenOffset
- while (p + 6 <= limit) {
- const n = parseNode(buf, p)
- if (!n.length) break
- if (n.key === key) return p
- p = align4(p + n.length)
- }
- return -1
-}
-
-// ============================================================
-// 主流程
-// ============================================================
-
-function analyze(file) {
- console.log(`\n===== ${path.basename(file)} =====`)
- const buf = fs.readFileSync(file)
- let pe
- try {
- pe = parsePE(buf)
- } catch (e) {
- console.log(` ✗ ${e.message}`)
- return false
- }
-
- const vers = findResources(buf, pe, RT_VERSION)
- if (!vers.length) {
- console.log(' ✗ 未找到 RT_VERSION 资源')
- return false
- }
- console.log(
- ` RT_VERSION 资源:${vers.length} 个,资源条目语言 ID = ${vers
- .map((v) => (v.langId == null ? '具名' : '0x' + v.langId.toString(16).padStart(4, '0')))
- .join(', ')}`
- )
-
- const target = vers[0]
- const root = parseNode(buf, target.offset)
- console.log(` 根节点 key : "${root.key}"${root.key === 'VS_VERSION_INFO' ? ' ✓' : ' ✗ 异常'}`)
- if (root.key !== 'VS_VERSION_INFO') return false
-
- const sig = buf.readUInt32LE(root.valueOffset)
- const fileMS = buf.readUInt32LE(root.valueOffset + 8)
- const fileLS = buf.readUInt32LE(root.valueOffset + 12)
- const prodMS = buf.readUInt32LE(root.valueOffset + 16)
- const prodLS = buf.readUInt32LE(root.valueOffset + 20)
- const fmt = (ms, ls) => `${ms >>> 16}.${ms & 0xffff}.${ls >>> 16}.${ls & 0xffff}`
- console.log(` Signature : 0x${sig.toString(16)}${sig === 0xfeef04bd ? ' ✓' : ' ✗'}`)
- console.log(` FileVersion : ${fmt(fileMS, fileLS)}`)
- console.log(` ProductVersion : ${fmt(prodMS, prodLS)}`)
-
- const sfiBlk = findChild(buf, target.offset, 'StringFileInfo')
- const vfiBlk = findChild(buf, target.offset, 'VarFileInfo')
-
- if (vfiBlk >= 0) {
- const tr = parseTranslations(buf, vfiBlk)
- console.log(
- ` Translation : ${tr.map((t) => `lang=0x${t.lang.toString(16).padStart(4, '0')} cp=${t.codePage}`).join(', ')}`
- )
- } else {
- console.log(' Translation : ✗ 缺少 VarFileInfo(Win32 API 将无法定位字符串表)')
- }
-
- if (sfiBlk < 0) {
- console.log(' StringFileInfo : ✗ 缺失')
- return false
- }
-
- const blocks = parseStringFileInfo(buf, sfiBlk)
- if (!blocks.length) {
- console.log(' StringFileInfo : ✗ 无可解析的语言块')
- return false
- }
-
- let nonEmpty = 0
- let total = 0
- for (const b of blocks) {
- console.log(`\n 语言块 "${b.langKey}" (资源管理器会按 Translation 拼出 "\\StringFileInfo\\${b.langKey}\\<键>" 来取值)`)
- for (const [k, v] of Object.entries(b.strings)) {
- total++
- const ok = v.length > 0
- if (ok) nonEmpty++
- console.log(` ${k.padEnd(18)} = ${ok ? JSON.stringify(v) : '(空)'} ${ok ? '✓' : '✗'}`)
- }
- }
-
- // Translation 与语言块 key 是否对得上(这是能否被 Win32 API 读到的关键)
- if (vfiBlk >= 0) {
- const tr = parseTranslations(buf, vfiBlk)
- const expected = tr.map((t) => `${t.lang.toString(16).padStart(4, '0')}${t.codePage.toString(16).padStart(4, '0')}`)
- const actual = blocks.map((b) => b.langKey.toLowerCase())
- const matched = expected.every((e) => actual.includes(e))
- console.log(
- `\n Translation 与语言块 key 对应关系:期望 [${expected.join(', ')}],实际 [${actual.join(', ')}] → ${
- matched ? '✓ 一致' : '✗ 不一致(API 取不到字符串)'
- }`
- )
- if (!matched) return false
- }
-
- console.log(`\n 结论:${nonEmpty}/${total} 个字符串字段有值`)
- return nonEmpty > 0
-}
-
-const targets = process.argv.slice(2).length ? process.argv.slice(2) : DEFAULT_TARGETS
-const results = targets.map(analyze)
-console.log('')
-process.exit(results.every(Boolean) ? 0 : 1)
diff --git a/.smoke/release.txt b/.smoke/release.txt
deleted file mode 100644
index 0c6c25c..0000000
--- a/.smoke/release.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-=== 分发产物 (pc/build/bin) ===
- starmusic.exe 11.8 MB
- starmusic-pc-1.3.0-Portable.zip 4.7 MB
- starmusic-pc-1.3.0-Setup.exe 6.6 MB
diff --git a/.smoke/report.json b/.smoke/report.json
deleted file mode 100644
index 8c6254e..0000000
--- a/.smoke/report.json
+++ /dev/null
@@ -1,66 +0,0 @@
-{
- "diag": {
- "platform": "pc",
- "native": false,
- "viewSearch": 1,
- "navItems": 5,
- "playerBar": true,
- "rootStyle": "--theme-color: #31c27c; --theme-color-rgb: 49, 194, 124; --theme-color-dark: #1ba56a; --theme-color-soft: #e8f8f0;",
- "audioMethods": {
- "onPlay": "function",
- "onPause": "function",
- "onStop": "function",
- "onEnded": "function",
- "onCanplay": "function",
- "onError": "function",
- "onTimeUpdate": "function",
- "offTimeUpdate": "function",
- "onSeeked": "function"
- },
- "bootBanner": null,
- "accountCard": "未登录点击登录账号",
- "themeBefore": "#31c27c",
- "themeAfter": "#3b82f6",
- "storedSetting": "{\"common.apiSource\":\"\",\"common.theme\":\"blue\",\"common.bgImageEnabled\":false,\"common.bgImageBlur\":12,\"player.playQuality\":\"128k\",\"player.downloadQuality\":\"\",\"player.togglePlayMethod\":\"listLoop\",\"search.isShowHotSearch\":false,\"search.isShowHistorySearch\":false,\"player.volume\":1,\"player.playbackRate\":1}"
- },
- "exceptions": [],
- "consoleSample": [
- {
- "type": "log:error",
- "text": "Failed to load resource: the server responded with a status of 404 (Not Found)"
- },
- {
- "type": "log:error",
- "text": "Access to fetch at 'http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1¬race=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1' from origin 'http://127.0.0.1:8899' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
- },
- {
- "type": "log:error",
- "text": "Failed to load resource: net::ERR_FAILED"
- },
- {
- "type": "log",
- "text": "[httpFetch error] http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1¬race=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1 Error: request:fail Failed to fetch\n at fail (http://127.0.0.1:8899/assets/index-9I8XxacX.js:32:32178)\n at http://127.0.0.1:8899/assets/index-9I8XxacX.js:31:5793"
- },
- {
- "type": "log",
- "text": "[easter-egg] 拉取成功:3 条彩蛋配置"
- },
- {
- "type": "log:verbose",
- "text": "[DOM] Password field is not contained in a form: (More info: https://www.chromium.org/developers/design-documents/create-amazing-password-forms) %o"
- },
- {
- "type": "log:error",
- "text": "Access to fetch at 'http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1¬race=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1' from origin 'http://127.0.0.1:8899' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
- },
- {
- "type": "log:error",
- "text": "Failed to load resource: net::ERR_FAILED"
- },
- {
- "type": "log",
- "text": "[httpFetch error] http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1¬race=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1 Error: request:fail Failed to fetch\n at fail (http://127.0.0.1:8899/assets/index-9I8XxacX.js:32:32178)\n at http://127.0.0.1:8899/assets/index-9I8XxacX.js:31:5793"
- }
- ],
- "consoleErrorCount": 5
-}
\ No newline at end of file
diff --git a/.smoke/smoke.cjs b/.smoke/smoke.cjs
deleted file mode 100644
index db604aa..0000000
--- a/.smoke/smoke.cjs
+++ /dev/null
@@ -1,284 +0,0 @@
-/**
- * 冒烟测试:用无头 Edge(Chromium) 加载 PC 版前端产物,
- * 通过 CDP 收集控制台输出 / 未捕获异常,并逐屏截图。
- *
- * 说明:浏览器环境没有 Wails 注入的 window.go,hasNative() 返回 false,
- * 网络请求会走 fetch 并因 CORS 失败——这些属于预期内的情况,会单独标注。
- */
-const http = require('http')
-const fs = require('fs')
-const path = require('path')
-const { spawn } = require('child_process')
-
-const DIST = 'C:\\Users\\admin\\Documents\\HBuilderProjects\\starmusic\\pc\\frontend\\dist'
-const OUT = 'C:\\Users\\admin\\Documents\\HBuilderProjects\\starmusic\\pc\\.smoke'
-const PORT = 8899
-const CDP_PORT = 9222
-const EDGE = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
-const USER_DATA = path.join(OUT, 'edge-profile')
-
-const MIME = {
- '.html': 'text/html; charset=utf-8',
- '.js': 'text/javascript; charset=utf-8',
- '.css': 'text/css; charset=utf-8',
- '.svg': 'image/svg+xml',
- '.png': 'image/png',
- '.json': 'application/json; charset=utf-8',
- '.woff2': 'font/woff2',
-}
-
-const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
-
-function startServer() {
- const server = http.createServer((req, res) => {
- let rel = decodeURIComponent(req.url.split('?')[0])
- if (rel === '/') rel = '/index.html'
- const file = path.join(DIST, rel)
- if (!file.startsWith(DIST) || !fs.existsSync(file) || fs.statSync(file).isDirectory()) {
- res.writeHead(404)
- res.end('not found')
- return
- }
- res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' })
- fs.createReadStream(file).pipe(res)
- })
- return new Promise((resolve) => server.listen(PORT, '127.0.0.1', () => resolve(server)))
-}
-
-function getJSON(url) {
- return new Promise((resolve, reject) => {
- http
- .get(url, (res) => {
- let body = ''
- res.on('data', (c) => (body += c))
- res.on('end', () => {
- try {
- resolve(JSON.parse(body))
- } catch (e) {
- reject(e)
- }
- })
- })
- .on('error', reject)
- })
-}
-
-async function waitForTarget(timeoutMs = 20000) {
- const started = Date.now()
- while (Date.now() - started < timeoutMs) {
- try {
- const list = await getJSON(`http://127.0.0.1:${CDP_PORT}/json/list`)
- const page = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl)
- if (page) return page
- } catch (e) {
- /* 还没起来 */
- }
- await sleep(400)
- }
- throw new Error('CDP target not found')
-}
-
-async function main() {
- fs.mkdirSync(OUT, { recursive: true })
- if (fs.existsSync(USER_DATA)) fs.rmSync(USER_DATA, { recursive: true, force: true })
-
- const server = await startServer()
- console.log('[smoke] static server up on', PORT)
-
- const child = spawn(
- EDGE,
- [
- '--headless=new',
- '--disable-gpu',
- '--hide-scrollbars',
- '--no-first-run',
- '--no-default-browser-check',
- `--remote-debugging-port=${CDP_PORT}`,
- `--user-data-dir=${USER_DATA}`,
- '--window-size=1400,900',
- `http://127.0.0.1:${PORT}/`,
- ],
- { stdio: 'ignore', detached: false },
- )
-
- const target = await waitForTarget()
- console.log('[smoke] page target:', target.url)
-
- const ws = new WebSocket(target.webSocketDebuggerUrl)
- let msgId = 0
- const pending = new Map()
- const consoleLines = []
- const exceptions = []
-
- const send = (method, params = {}) =>
- new Promise((resolve, reject) => {
- const id = ++msgId
- pending.set(id, { resolve, reject })
- ws.send(JSON.stringify({ id, method, params }))
- })
-
- await new Promise((resolve, reject) => {
- ws.addEventListener('open', resolve)
- ws.addEventListener('error', reject)
- })
-
- ws.addEventListener('message', (ev) => {
- let msg
- try {
- msg = JSON.parse(ev.data)
- } catch (e) {
- return
- }
- if (msg.id && pending.has(msg.id)) {
- const { resolve, reject } = pending.get(msg.id)
- pending.delete(msg.id)
- if (msg.error) reject(new Error(JSON.stringify(msg.error)))
- else resolve(msg.result)
- return
- }
- if (msg.method === 'Runtime.consoleAPICalled') {
- const text = (msg.params.args || [])
- .map((a) => (a.value !== undefined ? a.value : a.description || a.type))
- .join(' ')
- consoleLines.push({ type: msg.params.type, text })
- }
- if (msg.method === 'Runtime.exceptionThrown') {
- const d = msg.params.exceptionDetails || {}
- exceptions.push({
- text: d.text || '',
- desc: (d.exception && (d.exception.description || d.exception.value)) || '',
- line: d.lineNumber,
- url: d.url,
- })
- }
- if (msg.method === 'Log.entryAdded') {
- const e = msg.params.entry || {}
- consoleLines.push({ type: `log:${e.level}`, text: e.text || '' })
- }
- })
-
- await send('Runtime.enable')
- await send('Log.enable')
- await send('Page.enable')
-
- const shot = async (name) => {
- try {
- const res = await send('Page.captureScreenshot', { format: 'png' })
- fs.writeFileSync(path.join(OUT, `${name}.png`), Buffer.from(res.data, 'base64'))
- console.log('[smoke] screenshot:', name)
- } catch (e) {
- console.log('[smoke] screenshot failed', name, e.message)
- }
- }
-
- const evaluate = async (expr) => {
- try {
- const res = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
- return res.result ? res.result.value : undefined
- } catch (e) {
- return `<<eval error: ${e.message}>>`
- }
- }
-
- // 启动页
- await sleep(1600)
- await shot('01-splash')
-
- // 主界面(启动页 2.6s 后自动进入)
- await sleep(3200)
- await shot('02-search')
-
- const diag = {}
- diag.platform = await evaluate('window.uni && window.uni.__platform')
- diag.native = await evaluate('window.uni && window.uni.__native')
- diag.viewSearch = await evaluate("document.querySelectorAll('.view-search').length")
- diag.navItems = await evaluate("document.querySelectorAll('.nav-item').length")
- diag.playerBar = await evaluate("!!document.querySelector('.player-bar')")
- diag.rootStyle = await evaluate(
- "(()=>{const el=document.querySelector('.app-root');return el?el.getAttribute('style'):null})()",
- )
-
- // 音频上下文:验证 onTimeUpdate 等事件注册方法存在(此前这里是致命 bug)
- diag.audioMethods = await evaluate(
- "(()=>{const a=window.uni.createInnerAudioContext();const keys=['onPlay','onPause','onStop','onEnded','onCanplay','onError','onTimeUpdate','offTimeUpdate','onSeeked'];const r={};keys.forEach(k=>{r[k]=typeof a[k]});return r})()",
- )
- diag.bootBanner = await evaluate(
- "(()=>{const el=document.querySelector('.boot-error');return el?el.textContent.trim():null})()",
- )
-
- // 逐屏切换
- const clickNav = async (index, name) => {
- await evaluate(`(()=>{const a=document.querySelectorAll('.nav-item');if(a[${index}])a[${index}].click()})()`)
- await sleep(900)
- await shot(name)
- }
-
- await clickNav(1, '03-playlists')
- await clickNav(2, '04-settings')
-
- // 账号入口:验证侧栏底部账号卡片渲染(未登录态显示「未登录 / 点击登录账号」)
- diag.accountCard = await evaluate(
- "(()=>{const el=document.querySelector('.account-card');return el?el.textContent.replace(/\\s+/g,' ').trim():null})()",
- )
-
- // 主题切换:验证 CSS 变量是否随设置变化(确认 .app-root 的内联变量生效)
- // 主题快切入口已在设置页(侧栏原来那排 .swatch 色块改成了账号入口),
- // 所以这里点设置页的 .theme-item
- diag.themeBefore = await evaluate(
- "getComputedStyle(document.querySelector('.app-root')).getPropertyValue('--theme-color').trim()",
- )
- await evaluate("(()=>{const t=document.querySelectorAll('.theme-item');if(t[1])t[1].click()})()")
- await sleep(600)
- diag.themeAfter = await evaluate(
- "getComputedStyle(document.querySelector('.app-root')).getPropertyValue('--theme-color').trim()",
- )
- diag.storedSetting = await evaluate(
- "(()=>{try{return localStorage.getItem('starmusic_setting')}catch(e){return 'ERR'}})()",
- )
- await shot('04c-theme-blue')
- // 切回默认主题,避免影响后续截图
- await evaluate("(()=>{const t=document.querySelectorAll('.theme-item');if(t[0])t[0].click()})()")
- await sleep(400)
-
- // 设置页较长,滚到底部验证「诊断 / 关于」区域(数据目录、日志、更新等)
- await evaluate(
- "(()=>{const el=document.querySelector('.view-settings');if(el)el.scrollTop=el.scrollHeight})()",
- )
- await sleep(600)
- await shot('04b-settings-bottom')
-
- await clickNav(3, '05-account')
- await clickNav(4, '06-about')
-
- // 回到搜索页,验证搜索历史 / 热搜区域渲染
- await clickNav(0, '07-search-back')
-
- const result = {
- diag,
- exceptions,
- consoleSample: consoleLines.slice(0, 60),
- consoleErrorCount: consoleLines.filter((l) => l.type === 'error' || l.type === 'log:error').length,
- }
- fs.writeFileSync(path.join(OUT, 'report.json'), JSON.stringify(result, null, 2))
-
- console.log('\n===== DIAG =====')
- console.log(JSON.stringify(diag, null, 2))
- console.log('\n===== EXCEPTIONS (' + exceptions.length + ') =====')
- exceptions.slice(0, 20).forEach((e) => console.log(' -', e.text, '|', String(e.desc).split('\n')[0]))
- console.log('\n===== CONSOLE ERRORS/WARNS =====')
- consoleLines
- .filter((l) => /error|warn/.test(l.type))
- .slice(0, 30)
- .forEach((l) => console.log(` [${l.type}] ${l.text.slice(0, 220)}`))
-
- ws.close()
- child.kill()
- server.close()
- await sleep(300)
- process.exit(0)
-}
-
-main().catch((e) => {
- console.error('[smoke] FAILED', e)
- process.exit(1)
-})
diff --git a/.smoke/verify-icon.cjs b/.smoke/verify-icon.cjs
deleted file mode 100644
index bc224c3..0000000
--- a/.smoke/verify-icon.cjs
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * 验证 exe 内是否嵌入了指定图标
- *
- * 原理:我们的 icon.ico 里每个尺寸都是 PNG 条目。Wails 会把 icon.ico 作为
- * Windows 资源(RT_GROUP_ICON / RT_ICON)写入 exe。若 exe 中能找到 icon.ico
- * 里某个尺寸的完整 PNG 字节序列,就说明新图标确实被嵌入(且不是模板默认图标)。
- *
- * 用法:node .smoke/verify-icon.cjs
- */
-
-const fs = require('fs')
-const path = require('path')
-
-const ROOT = path.resolve(__dirname, '..')
-const ICO = path.join(ROOT, 'build', 'windows', 'icon.ico')
-const DEFAULT_ICO_MARKER_SIZE = 256
-
-const targets = [
- path.join(ROOT, 'build', 'bin', 'starmusic.exe'),
- path.join(ROOT, 'build', 'bin', 'starmusic-pc-1.3.0-Setup.exe'),
-]
-
-/** 解析 ICO,返回 [{ size, png }] */
-function parseICO(buf) {
- const count = buf.readUInt16LE(4)
- const entries = []
- for (let i = 0; i < count; i++) {
- const o = 6 + i * 16
- const size = buf[o] === 0 ? 256 : buf[o]
- const len = buf.readUInt32LE(o + 8)
- const off = buf.readUInt32LE(o + 12)
- const data = buf.subarray(off, off + len)
- const isPng = data[0] === 0x89 && data[1] === 0x50
- entries.push({ size, len, isPng, png: isPng ? data : null })
- }
- return entries
-}
-
-const ico = fs.readFileSync(ICO)
-const entries = parseICO(ico)
-
-console.log(`icon.ico:${ico.length} 字节,${entries.length} 档尺寸`)
-console.log(
- ' 尺寸分布:' + entries.map((e) => `${e.size}${e.isPng ? 'P' : 'B'}`).join(' ') + ' (P=PNG 条目, B=BMP 条目)'
-)
-console.log('')
-
-let failed = 0
-
-for (const exe of targets) {
- if (!fs.existsSync(exe)) {
- console.log(`✗ 找不到 ${path.basename(exe)}`)
- failed++
- continue
- }
- const bin = fs.readFileSync(exe)
- const hits = []
- for (const e of entries) {
- if (!e.png) continue
- const found = bin.includes(e.png)
- if (found) hits.push(e.size)
- }
- const name = path.basename(exe)
- if (hits.length) {
- console.log(`✓ ${name}(${(bin.length / 1024 / 1024).toFixed(1)} MB)`)
- console.log(` 命中内嵌图标尺寸:${hits.join(', ')} → 新图标已嵌入`)
- } else {
- console.log(`✗ ${name}:未找到任何一档图标字节,图标可能未被嵌入`)
- failed++
- }
-}
-
-console.log('')
-// 对照:确认模板默认图标已不在(避免"改了但没生效")
-const templateIco = 'C:\\Users\\admin\\AppData\\Local\\Temp\\wailstpl\\probe\\build\\windows\\icon.ico'
-if (fs.existsSync(templateIco)) {
- const t = parseICO(fs.readFileSync(templateIco))
- const tEntry = t.find((e) => e.png)
- const exe = fs.readFileSync(targets[0])
- if (tEntry && exe.includes(tEntry.png)) {
- console.log('! 警告:exe 中仍能找到 Wails 模板默认图标的字节,请确认图标替换是否生效')
- failed++
- } else {
- console.log('✓ 对照检查:exe 中不含 Wails 模板默认图标(已彻底替换)')
- }
-}
-
-if (failed) {
- console.error(`\n验证失败:${failed} 项`)
- process.exit(1)
-}
-console.log('\n图标嵌入验证通过')
diff --git a/.smoke/verify-nsis-text.cjs b/.smoke/verify-nsis-text.cjs
deleted file mode 100644
index 8cba458..0000000
--- a/.smoke/verify-nsis-text.cjs
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * 验证安装包中文编码不会乱码
- *
- * 为什么需要单独验证:NSIS 在 `Unicode true` 下若读不到 UTF-8 BOM,会按系统 ANSI 代码页
- * (中文系统为 GBK)解码 .nsi —— 编译不会报错,但用户看到的向导是全屏乱码。这个坑不会
- * 在任何构建日志里暴露,必须主动检查。
- *
- * 验证链(三条互相独立,任一不成立都会报错):
- * A. project.nsi 必须带 UTF-8 BOM,且内容为合法 UTF-8
- * B. makensis 编译时须自己报告 `Processing script file: "project.nsi" (UTF8)`
- * —— 这是 makensis 的编码识别结果,等价于"它按 UTF-8 读了"
- * C. 最终安装包(未压缩的 PE 版本资源区)中能找到**仅来自 project.nsi** 的中文文案
- * 的正确 UTF-16LE 编码 —— 证明中文从源文件一路正确走到了成品
- *
- * 说明:向导正文(MUI 文案 / MessageBox / DetailPrint)会被 NSIS 压缩存放,
- * 无法在成品中直接搜索;它们与 C 中的文案同处一个文件、同一条编码链路,
- * 因此 A + B + C 成立即可判定全部中文正确。
- *
- * 用法:node .smoke/verify-nsis-text.cjs
- */
-
-const fs = require('fs')
-const path = require('path')
-const { execFileSync } = require('child_process')
-
-const ROOT = path.resolve(__dirname, '..')
-const INSTALLER_DIR = path.join(ROOT, 'build', 'windows', 'installer')
-const WINDOWS_DIR = path.join(ROOT, 'build', 'windows')
-const PROBE_DIR = path.join(ROOT, '.smoke', 'nsis-probe')
-const MAKENSIS = process.env.MAKENSIS || 'C:/Users/admin/AppData/Local/nsis-3.10/makensis.exe'
-const APP_EXE = path.join(ROOT, 'build', 'bin', 'starmusic.exe')
-const REAL_INSTALLER = path.join(ROOT, 'build', 'bin', 'starmusic-pc-1.3.0-Setup.exe')
-
-let failed = 0
-const ok = (m) => console.log(`✓ ${m}`)
-const bad = (m) => {
- console.error(`✗ ${m}`)
- failed++
-}
-
-function cleanup() {
- try {
- fs.rmSync(PROBE_DIR, { recursive: true, force: true })
- } catch (e) {
- /* 忽略 */
- }
-}
-
-// ============================================================
-// A. 源文件编码
-// ============================================================
-console.log('--- A. project.nsi 源文件编码 ---')
-const nsiPath = path.join(INSTALLER_DIR, 'project.nsi')
-if (!fs.existsSync(nsiPath)) {
- bad('找不到 build/windows/installer/project.nsi')
-} else {
- const raw = fs.readFileSync(nsiPath)
- const hasBom = raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf
- hasBom ? ok('带 UTF-8 BOM(NSIS 据此按 UTF-8 解码)') : bad('缺少 UTF-8 BOM —— 中文会按 GBK 解码成乱码')
-
- const body = raw.subarray(3).toString('utf8')
- const cjk = (body.match(/[\u4e00-\u9fff]/g) || []).length
- body.includes('\uFFFD')
- ? bad('内容不是合法 UTF-8(含替换字符)')
- : ok(`内容为合法 UTF-8,含 ${cjk} 个中文字符`)
-}
-
-// ============================================================
-// B. makensis 的编码识别结果
-// ============================================================
-console.log('\n--- B. makensis 编码识别 ---')
-let makensisReportedUtf8 = false
-if (!fs.existsSync(MAKENSIS)) {
- bad(`找不到 makensis:${MAKENSIS}`)
-} else if (!fs.existsSync(APP_EXE)) {
- bad(`找不到待打包 exe:${APP_EXE}`)
-} else {
- cleanup()
- fs.mkdirSync(PROBE_DIR, { recursive: true })
-
- const src = fs.readFileSync(nsiPath).toString('utf8')
- let nsi = src.replace(
- '!include "wails_tools.nsh"',
- '!include "wails_tools.nsh"\n\nSetCompressor zlib'
- )
- // 探针目录结构与正式目录不同:MUI_ICON 的 "..\icon.ico" 改为同目录引用
- nsi = nsi.replace(/!define\s+MUI_ICON\s+"\.\.\\icon\.ico"/, '!define MUI_ICON "icon.ico"')
- nsi = nsi.replace(/!define\s+MUI_UNICON\s+"\.\.\\icon\.ico"/, '!define MUI_UNICON "icon.ico"')
- // 产物改到探针目录,避免覆盖正式安装包
- nsi = nsi.replace(/OutFile\s+"[^"]+"/, `OutFile "${path.join(PROBE_DIR, 'probe.exe')}"`)
- fs.writeFileSync(path.join(PROBE_DIR, 'project.nsi'), Buffer.from(nsi, 'utf8'))
- for (const f of ['wails_tools.nsh', 'license.txt']) {
- const p = path.join(INSTALLER_DIR, f)
- if (fs.existsSync(p)) fs.copyFileSync(p, path.join(PROBE_DIR, f))
- }
- const tmpDir = path.join(INSTALLER_DIR, 'tmp')
- if (fs.existsSync(tmpDir)) fs.cpSync(tmpDir, path.join(PROBE_DIR, 'tmp'), { recursive: true })
- fs.copyFileSync(path.join(WINDOWS_DIR, 'icon.ico'), path.join(PROBE_DIR, 'icon.ico'))
-
- try {
- const out = execFileSync(
- MAKENSIS,
- [`-DARG_WAILS_AMD64_BINARY=${APP_EXE}`, 'project.nsi'],
- { cwd: PROBE_DIR, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
- )
- const line = out.split(/\r?\n/).find((l) => /Processing script file/.test(l))
- makensisReportedUtf8 = !!line && /\(UTF8\)/i.test(line)
- if (makensisReportedUtf8) ok(`makensis 报告:${line.trim()}`)
- else bad(`makensis 未报告 UTF8:${line || '(无输出)'}`)
-
- const errs = out.split(/\r?\n/).filter((l) => /^Error|aborting/i.test(l.trim()))
- if (errs.length) {
- bad('编译报错:')
- errs.slice(0, 6).forEach((e) => console.error(' ' + e.trim()))
- }
- } catch (e) {
- bad('探针编译失败')
- console.error(String(e.stdout || '').split(/\r?\n/).slice(0, 12).join('\n'))
- console.error(String(e.stderr || '').split(/\r?\n/).slice(0, 12).join('\n'))
- }
-}
-
-// ============================================================
-// C. 成品中未压缩版本资源的中文比对
-// ============================================================
-console.log('\n--- C. 成品安装包中的中文(PE 版本资源,未压缩)---')
-
-/** 仅来自 project.nsi 的中文断言:[期望在成品中出现的完整字符串, 来源说明] */
-const CLAIMS = [
- ['星音乐 安装程序', 'FileDescription —— 由 project.nsi 的 INFO_PRODUCTNAME + 字面量拼接'],
- ['2026 仰望星辰工作室', 'LegalCopyright —— project.nsi 的 INFO_COPYRIGHT'],
-]
-
-if (!fs.existsSync(REAL_INSTALLER)) {
- bad(`找不到安装包:${REAL_INSTALLER}`)
-} else {
- const bin = fs.readFileSync(REAL_INSTALLER)
- console.log(`安装包:${(bin.length / 1024 / 1024).toFixed(1)} MB`)
-
- for (const [text, from] of CLAIMS) {
- const needle = Buffer.from(text, 'utf16le')
- if (bin.includes(needle)) {
- ok(`命中「${text}」 (${from})`)
- } else {
- bad(`未命中「${text}」 (${from}) —— 中文可能在编译环节被错误解码`)
- }
- }
-
- // 反向确认:确认我们对比的确实是从 project.nsi 读到的文本,而非凭空构造
- const nsiText = fs.readFileSync(nsiPath).subarray(3).toString('utf8')
- const bothFromSource = CLAIMS.every(([t]) => nsiText.includes(t.split(' ').pop()))
- bothFromSource
- ? ok('断言的文案片段均可回溯到 project.nsi 源文件')
- : bad('断言文案与源文件不匹配(验证脚本自身有问题)')
-}
-
-// ============================================================
-// 汇总
-// ============================================================
-cleanup()
-console.log('')
-if (failed) {
- console.error(`验证失败:${failed} 项`)
- process.exit(1)
-}
-console.log('安装包中文编码验证通过:A(BOM/UTF-8)+ B(makensis 按 UTF-8 解码)+ C(成品中文正确)全部成立')