166 lines
14 KiB
JavaScript
166 lines
14 KiB
JavaScript
import { _electron as electron,expect } from '@playwright/test'
|
|
import electronPath from 'electron'
|
|
import ExcelJS from 'exceljs'
|
|
import { PNG } from 'pngjs'
|
|
import { PDFDocument } from 'pdf-lib'
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
const root=process.cwd(),qa=path.join(root,'.qa'),profile=path.join(qa,'profile'),fixtures=path.join(qa,'fixtures'),output=path.join(qa,'output'),seals=path.join(fixtures,'seals')
|
|
if(!profile.startsWith(root+path.sep))throw Error('Invalid QA path')
|
|
await fs.rm(profile,{recursive:true,force:true})
|
|
await fs.mkdir(profile,{recursive:true})
|
|
await fs.writeFile(path.join(profile,'settings.json'),JSON.stringify({createBatchFolder:false,openAfterExport:false}))
|
|
await fs.mkdir(seals,{recursive:true});await fs.mkdir(output,{recursive:true})
|
|
const image=new PNG({width:240,height:240})
|
|
for(let y=0;y<240;y++)for(let x=0;x<240;x++){const i=(y*240+x)*4,d=Math.hypot(x-120,y-120),red=(d>99&&d<106)||(d>88&&d<91)||(x>65&&x<175&&y>105&&y<133);image.data[i]=195;image.data[i+1]=30;image.data[i+2]=35;image.data[i+3]=red?255:0}
|
|
await fs.writeFile(path.join(seals,'测试印章.png'),PNG.sync.write(image))
|
|
const book=new ExcelJS.Workbook(),sheet=book.addWorksheet('收据数据')
|
|
sheet.addRow(['收款日期','学校','年级','姓名','收费项目','金额','出纳','复核'])
|
|
sheet.addRow(['2026-10-16','某某学校','初二年级','张三','秋季学期服务费',3500])
|
|
sheet.addRow(['2026-10-16','某某学校','高2028届','<script>alert(1)</script>','资料费',12.34])
|
|
sheet.addRow(['2026-10-17','较长名称测试:某某学校课程服务中心','高三年级','李四','课程服务项目及相关资料费用',10001.05])
|
|
sheet.addRow(['2026-02-30','某某学校','高2028届','无效行','服务费','12.345'])
|
|
const excelFile=path.join(fixtures,'收据测试数据.xlsx');await book.xlsx.writeFile(excelFile)
|
|
const env={...process.env};delete env.ELECTRON_RUN_AS_NODE
|
|
const errors=[]
|
|
let desktop
|
|
async function launch(){
|
|
desktop=await electron.launch({executablePath:electronPath,args:['.','--test-mode'],cwd:root,env,timeout:60000})
|
|
desktop.process().stderr.on('data',data=>{const text=data.toString();if(!text.includes('Autofill'))process.stderr.write(text)})
|
|
const page=await desktop.firstWindow();page.setDefaultTimeout(20000)
|
|
page.on('pageerror',e=>errors.push(e.message))
|
|
await expect(page.getByRole('heading',{name:'收据工作台',exact:true})).toBeVisible({timeout:30000})
|
|
await expect(page.getByText('把表格交给收据工坊')).toBeVisible({timeout:30000})
|
|
return page
|
|
}
|
|
try{
|
|
const page=await launch()
|
|
await desktop.evaluate(({dialog,shell},p)=>{
|
|
globalThis.openedDirectories=[]
|
|
shell.openPath=async folder=>{globalThis.openedDirectories.push(folder);return ''}
|
|
dialog.showOpenDialog=async(_window,options)=>({canceled:false,filePaths:[options.title==='选择印章文件夹'?p.seals:options.title==='选择导出文件夹'?p.output:p.excelFile]})
|
|
dialog.showSaveDialog=async()=>({canceled:false,filePath:p.template})
|
|
dialog.showMessageBox=async()=>({response:1,checkboxChecked:false})
|
|
},{seals,output,excelFile,template:path.join(fixtures,'模板.xlsx')})
|
|
await page.screenshot({path:path.join(qa,'01-workspace.png')})
|
|
await page.getByRole('button',{name:'下载模板',exact:true}).click()
|
|
await expect.poll(async()=>!!(await fs.stat(path.join(fixtures,'模板.xlsx')).catch(()=>null))).toBe(true)
|
|
await page.getByRole('button',{name:'偏好设置',exact:true}).click()
|
|
await expect(page.getByRole('button',{name:'更多偏好与数据备份'})).toHaveCount(0)
|
|
await expect(page.getByRole('button',{name:'备份数据'})).toHaveCount(0)
|
|
await expect(page.getByRole('button',{name:'恢复备份'})).toHaveCount(0)
|
|
await expect(page.getByRole('switch',{name:'每次导出建立独立批次文件夹'})).toHaveCount(0)
|
|
await expect(page.getByRole('switch',{name:'生成完成后自动打开输出目录'})).toHaveCount(0)
|
|
await expect(page.getByLabel('导出的文件叫什么名字')).toBeVisible()
|
|
expect(await page.evaluate(()=>({backup:typeof window.receipts.backup,restore:typeof window.receipts.restore}))).toEqual({backup:'undefined',restore:'undefined'})
|
|
expect((await page.evaluate(()=>window.receipts.bootstrap())).settings).toMatchObject({createBatchFolder:true,openAfterExport:true})
|
|
await page.locator('[data-task="settings-folder"][data-task-target="output"]').click()
|
|
await page.locator('[data-task="settings-folder"][data-task-target="seal"]').click()
|
|
await expect(page.locator('[data-region="settings"]')).not.toHaveAttribute('aria-busy','true')
|
|
await page.getByLabel('默认出纳',{exact:true}).fill('李出纳')
|
|
await page.getByLabel('默认复核人',{exact:true}).fill('王复核')
|
|
await page.getByLabel('编号前缀',{exact:true}).fill('QA')
|
|
await page.getByLabel('导出的文件叫什么名字').selectOption('number')
|
|
await expect(page.locator('.filename-example')).toContainText('QA00000001.pdf')
|
|
await page.getByLabel('导出的文件叫什么名字').selectOption('customer_number')
|
|
await expect(page.locator('.filename-example')).toContainText('巴南中学 高2028届 张三_QA00000001.pdf')
|
|
await page.getByLabel('导出的文件叫什么名字').selectOption('number_customer')
|
|
await expect(page.locator('.filename-example')).toContainText('QA00000001_巴南中学 高2028届 张三.pdf')
|
|
await page.getByRole('button',{name:'保存偏好设置',exact:true}).click()
|
|
await expect(page.locator('[data-region="settings"]')).not.toHaveAttribute('aria-busy','true')
|
|
await expect(page.locator('.main-content > .feedback')).toHaveCount(0)
|
|
await page.screenshot({path:path.join(qa,'02-settings.png'),fullPage:true})
|
|
await page.getByRole('button',{name:'印章管理',exact:true}).click()
|
|
await page.getByRole('button',{name:/测试印章/}).click()
|
|
await page.getByRole('button',{name:'收据工作台',exact:true}).click()
|
|
await page.getByRole('button',{name:'导入 Excel',exact:true}).click()
|
|
await expect(page.getByText('已选择 3 条',{exact:true})).toBeVisible()
|
|
await expect(page.getByText('待修改',{exact:true})).toBeVisible()
|
|
const group=page.getByRole('checkbox',{name:'选择分组:某某学校 / 初二年级 / 秋季学期服务费',exact:true})
|
|
await group.uncheck();await expect(page.getByText('已选择 2 条',{exact:true})).toBeVisible()
|
|
await expect(page.getByRole('checkbox',{name:'选择分组:某某学校',exact:true})).toHaveJSProperty('indeterminate',true)
|
|
await group.check();await expect(page.getByText('已选择 3 条',{exact:true})).toBeVisible()
|
|
await expect(page.getByRole('checkbox',{name:'选择分组:某某学校 / 高2028届 / 服务费',exact:true})).toBeDisabled()
|
|
await page.getByLabel('单张 PNG',{exact:true}).check()
|
|
await page.frameLocator('iframe').getByText('李出纳',{exact:false}).waitFor()
|
|
await expect(page.frameLocator('iframe').getByText('某某学校 初二年级 张三',{exact:true})).toBeVisible()
|
|
await page.getByRole('button',{name:'A4 打印排版',exact:true}).click()
|
|
await expect(page.frameLocator('iframe').getByText('某某学校 初二年级 张三',{exact:true})).toBeVisible()
|
|
await expect(page.frameLocator('iframe').getByText('某某学校 高2028届 <script>alert(1)</script>',{exact:true})).toHaveCount(0)
|
|
await page.getByRole('button',{name:'单张收据',exact:true}).click()
|
|
await page.screenshot({path:path.join(qa,'03-import-preview.png'),fullPage:true})
|
|
await page.getByRole('button',{name:/生成 3 张收据/}).click()
|
|
await expect.poll(async()=>page.evaluate(async()=> (await window.receipts.listBatches())[0]?.status),{timeout:120000,intervals:[500,1000]}).toBe('completed')
|
|
const batches=await page.evaluate(()=>window.receipts.listBatches()),batch=batches[0]
|
|
await expect.poll(()=>desktop.evaluate(()=>globalThis.openedDirectories)).toContain(batch.outputDirectory)
|
|
expect(batch.count).toBe(3);expect(batch.firstNumber).toBe('QA00000001');expect(batch.lastNumber).toBe('QA00000003')
|
|
const files=await fs.readdir(batch.outputDirectory,{recursive:true})
|
|
expect(files.filter(f=>f.endsWith('.pdf'))).toHaveLength(6);expect(files.filter(f=>f.endsWith('.png'))).toHaveLength(3)
|
|
const printFiles=files.filter(f=>path.basename(f).startsWith('A4打印版'))
|
|
expect(printFiles).toHaveLength(3)
|
|
const printFile=printFiles.find(f=>f.includes('秋季学期服务费'))
|
|
const pdf=await PDFDocument.load(await fs.readFile(path.join(batch.outputDirectory,printFile)))
|
|
expect(pdf.getPageCount()).toBe(1)
|
|
expect(Math.abs(pdf.getPage(0).getWidth()-595.28)).toBeLessThan(1)
|
|
const singleFile=files.find(f=>path.basename(f).startsWith('QA00000001')&&f.endsWith('.pdf'))
|
|
expect(path.dirname(singleFile)).toBe(path.join('某某学校','初二年级','秋季学期服务费'))
|
|
expect((await PDFDocument.load(await fs.readFile(path.join(batch.outputDirectory,singleFile)))).getPageCount()).toBe(1)
|
|
const pngFile=files.find(f=>path.basename(f).startsWith('QA00000001')&&f.endsWith('.png'))
|
|
const raster=PNG.sync.read(await fs.readFile(path.join(batch.outputDirectory,pngFile)))
|
|
expect(raster.width).toBeGreaterThan(2200);expect(raster.height).toBeGreaterThan(1600)
|
|
await fs.copyFile(path.join(batch.outputDirectory,pngFile),path.join(qa,'04-receipt-300dpi.png'))
|
|
await page.getByLabel('单张 PNG',{exact:true}).uncheck();await page.getByLabel('A4 打印版',{exact:true}).uncheck()
|
|
await page.getByRole('button',{name:/生成记录/}).click()
|
|
await expect(page.getByRole('heading',{name:'最近生成的批次',exact:false})).toBeVisible()
|
|
await expect(page.locator('.history-category')).toContainText('某某学校')
|
|
await page.getByLabel('按学校查找记录').selectOption('某某学校')
|
|
await page.getByLabel('按年级查找记录').selectOption('初二年级')
|
|
await page.getByLabel('按收费项目查找记录').selectOption('资料费')
|
|
await expect(page.locator('.history-table tbody tr')).toHaveCount(0)
|
|
await page.getByRole('button',{name:'清空筛选',exact:true}).click()
|
|
await page.getByLabel('搜索生成记录').fill('QA00000002')
|
|
await expect(page.locator('.history-match')).toContainText('QA00000002')
|
|
await page.getByRole('button',{name:'查看原收据',exact:true}).click()
|
|
await expect(page.locator('.modal-card iframe')).toBeVisible()
|
|
await expect(page.frameLocator('.modal-card iframe').getByText('QA00000002',{exact:false})).toBeVisible()
|
|
await page.getByRole('button',{name:'关闭原收据预览',exact:true}).click()
|
|
await expect(page.locator('[data-region="history"]')).not.toHaveAttribute('aria-busy','true')
|
|
await page.getByLabel('搜索生成记录').fill('李四')
|
|
await expect(page.locator('.history-match')).toContainText('QA00000003')
|
|
await page.screenshot({path:path.join(qa,'05-history-filter.png'),fullPage:true})
|
|
await page.getByRole('button',{name:'清空筛选',exact:true}).click()
|
|
await expect(page.locator('.history-table tbody tr')).toHaveCount(1)
|
|
const originalDirectory=batch.outputDirectory
|
|
await page.getByRole('button',{name:'重新导出',exact:true}).first().click()
|
|
await expect.poll(async()=>page.evaluate(async old => {const b=(await window.receipts.listBatches())[0];return b?.outputDirectory!==old&&b?.status==='completed'},originalDirectory),{timeout:120000}).toBe(true)
|
|
const boot=await page.evaluate(()=>window.receipts.bootstrap())
|
|
expect(boot.nextNumber).toBe('QA00000004')
|
|
expect(boot.batches).toHaveLength(1)
|
|
const retryFiles=await fs.readdir(boot.batches[0].outputDirectory,{recursive:true})
|
|
expect(retryFiles.filter(f=>f.endsWith('.pdf'))).toHaveLength(3)
|
|
expect(retryFiles).toContain(path.join('某某学校','初二年级','秋季学期服务费',path.basename(singleFile)))
|
|
await expect.poll(()=>desktop.evaluate(()=>globalThis.openedDirectories)).toContain(boot.batches[0].outputDirectory)
|
|
// A large number of schools should not mount every nested checkbox at once.
|
|
const largeBook=new ExcelJS.Workbook(),largeSheet=largeBook.addWorksheet('收据数据')
|
|
largeSheet.addRow(['收款日期','学校','年级','姓名','收费项目','金额'])
|
|
for(let i=1;i<=60;i++)largeSheet.addRow(['2026-10-19','分组测试学校'+i,'高一','测试姓名','服务费',100])
|
|
const largeFile=path.join(fixtures,'多学校分组.xlsx');await largeBook.xlsx.writeFile(largeFile)
|
|
await desktop.evaluate(({dialog},file)=>{dialog.showOpenDialog=async()=>({canceled:false,filePaths:[file]})},largeFile)
|
|
await page.getByRole('button',{name:'收据工作台',exact:true}).click()
|
|
await page.locator('.panel-heading [data-task="import"]').click()
|
|
await expect(page.getByText('已选择 60 条',{exact:true})).toBeVisible()
|
|
await expect(page.locator('.group-tree-root > .receipt-group-node')).toHaveCount(50)
|
|
await page.getByRole('button',{name:'显示更多分组(还有 10 项)',exact:true}).click()
|
|
await expect(page.locator('.group-tree-root > .receipt-group-node')).toHaveCount(60)
|
|
await page.getByRole('checkbox',{name:'选择分组:分组测试学校60',exact:true}).uncheck()
|
|
await expect(page.getByText('已选择 59 条',{exact:true})).toBeVisible()
|
|
await desktop.close()
|
|
const restarted=await launch()
|
|
const boot2=await restarted.evaluate(()=>window.receipts.bootstrap())
|
|
expect(boot2.nextNumber).toBe('QA00000004');expect(boot2.settings.sealDirectory).toBe(seals);expect(boot2.batches).toHaveLength(1)
|
|
expect(boot2.settings).toMatchObject({createBatchFolder:true,openAfterExport:true})
|
|
expect(errors).toEqual([])
|
|
await fs.writeFile(path.join(qa,'result.json'),JSON.stringify({passed:true,checks:['Excel 模板','错误行定位','印章文件夹读取','设置持久化','文件命名实时示例','学校/年级/项目分组勾选与错误行禁用','大批分组按需展开与加载','分组 A4 预览','单张 PDF','300 DPI PNG','学校/年级/收费项目分层','分组 A4 打印版','历史组合条件同收据匹配','中间编号及姓名查找','匹配原收据预览','原编号分层重试','备份及更多偏好区域已移除','旧关闭配置自动启用','成功后自动打开批次目录','重启编号连续'],output:batch.outputDirectory,printGroups:printFiles.length,printPages:pdf.getPageCount(),png:{width:raster.width,height:raster.height},rendererErrors:errors},null,2))
|
|
console.log('Desktop verification passed. Artifacts: '+qa)
|
|
}finally{if(desktop)await desktop.close().catch(()=>{})}
|