| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // node tests/superdoc-script.smoke.mjs (requires Playwright and Chrome) |
| | | import assert from 'node:assert/strict' |
| | | import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises' |
| | | import { tmpdir } from 'node:os' |
| | | import path from 'node:path' |
| | | import { createServer } from 'node:http' |
| | | import { fileURLToPath } from 'node:url' |
| | | import { createServer as createViteServer } from 'vite' |
| | | import vue from '@vitejs/plugin-vue' |
| | | import JSZip from 'jszip' |
| | | import superdocHttpWorker from '../vite/plugins/superdoc-http-worker.js' |
| | | |
| | | const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || 'playwright') |
| | | const project = fileURLToPath(new URL('../', import.meta.url)) |
| | | const temporary = await mkdtemp(path.join(tmpdir(), 'lims-superdoc-script-')) |
| | | const fixture = path.join(project, 'tests/.superdoc-script-smoke.html') |
| | | let browser, server, vite |
| | | try { |
| | | await writeFile(fixture, `<!doctype html><html><body><div id="app"></div><script type="module"> |
| | | import { createApp, ref, h } from 'vue'; |
| | | import ElementPlus from 'element-plus'; |
| | | import 'element-plus/dist/index.css'; |
| | | import ReportWordEditor from '../src/components/ReportWordEditor/index.vue'; |
| | | window.documentState = ref(null); |
| | | window.readonlyState = ref(false); |
| | | window.changes = 0; |
| | | const component = ref(); |
| | | window.openDocument = async (blob) => { |
| | | window.result = null; |
| | | documentState.value = { blob, title: 'script-test.docx' }; |
| | | }; |
| | | window.exportDocument = async () => Array.from(new Uint8Array(await (await component.value.exportDocx()).arrayBuffer())); |
| | | createApp({ setup: () => () => documentState.value && h(ReportWordEditor, { |
| | | ref: component, document: documentState.value, readonly: readonlyState.value, |
| | | onReady: () => window.result = 'ready', onError: error => window.result = error, |
| | | onChange: () => window.changes++ |
| | | }) }).use(ElementPlus).mount('#app'); |
| | | fetch('/fixture.docx').then(response => response.blob()).then(openDocument); |
| | | </script></body></html>`) |
| | | vite = await createViteServer({ configFile: false, root: project, logLevel: 'warn', |
| | | cacheDir: path.join(temporary, 'cache'), server: { middlewareMode: true }, appType: 'custom', |
| | | optimizeDeps: { entries: [fixture] }, |
| | | resolve: { alias: { '@': path.join(project, 'src') } }, |
| | | plugins: [vue(), superdocHttpWorker(), { |
| | | name: 'capture-test-editor', enforce: 'pre', |
| | | resolveId(id, importer) { |
| | | if (importer === '\0test-editor-loader') return |
| | | if (id === '@/utils/loadSuperDoc' || id.replaceAll('\\', '/').replace(/\.js$/, '') === path.join(project, 'src/utils/loadSuperDoc').replaceAll('\\', '/')) { |
| | | return '\0test-editor-loader' |
| | | } |
| | | }, |
| | | load(id) { |
| | | if (id !== '\0test-editor-loader') return |
| | | return `import { loadSuperDoc as load } from ${JSON.stringify(path.join(project, 'src/utils/loadSuperDoc.js').replaceAll('\\', '/'))}; |
| | | export async function loadSuperDoc() { |
| | | const module = await load(); |
| | | return { SuperDoc: class extends module.SuperDoc { |
| | | constructor(options) { super(options); window.superdoc = this; } |
| | | } }; |
| | | }` |
| | | } |
| | | }] }) |
| | | const zip = new JSZip() |
| | | zip.file('[Content_Types].xml', '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>') |
| | | zip.file('_rels/.rels', '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>') |
| | | // Real report values live inside table cells; include existing bold to check preservation. |
| | | zip.file('word/document.xml', '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/></w:tblPr><w:tblGrid><w:gridCol w:w="9000"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:w="9000" w:type="dxa"/></w:tcPr><w:p><w:r><w:rPr><w:b/></w:rPr><w:t>10-3 H2O</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:p/><w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr></w:body></w:document>') |
| | | const docx = await zip.generateAsync({ type: 'nodebuffer' }) |
| | | server = createServer(async (req, res) => { |
| | | try { |
| | | const url = new URL(req.url, 'http://localhost') |
| | | if (url.pathname === '/fixture.docx') { res.end(docx); return } |
| | | if (url.pathname === '/') { |
| | | res.setHeader('Content-Type', 'text/html') |
| | | res.end(await vite.transformIndexHtml('/tests/.superdoc-script-smoke.html', await readFile(fixture, 'utf8'))) |
| | | return |
| | | } |
| | | vite.middlewares(req, res, () => res.writeHead(404).end()) |
| | | } catch { res.writeHead(404).end() } |
| | | }) |
| | | await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) |
| | | browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true }) |
| | | const page = await browser.newPage({ viewport: { width: 1500, height: 1000 } }) |
| | | const errors = [] |
| | | page.on('pageerror', error => errors.push(error.message)) |
| | | await page.goto(`http://127.0.0.1:${server.address().port}/`) |
| | | async function ready() { |
| | | await page.waitForFunction(() => window.result, null, { timeout: 120000 }) |
| | | assert.equal(await page.evaluate(() => window.result), 'ready') |
| | | } |
| | | await ready() |
| | | async function select(start, end) { |
| | | await page.locator('.superdoc-page').first().click({ position: { x: 120, y: 110 } }) |
| | | await page.keyboard.press('Control+Home') |
| | | for (let i = 0; i < start; i++) await page.keyboard.press('ArrowRight') |
| | | for (let i = start; i < end; i++) await page.keyboard.press('Shift+ArrowRight') |
| | | if (start !== end) await page.waitForFunction(async () => !(await superdoc.activeEditor.doc.selection.current()).empty) |
| | | } |
| | | async function xml() { |
| | | const bytes = await page.evaluate(() => exportDocument()) |
| | | return (await JSZip.loadAsync(Buffer.from(bytes))).file('word/document.xml').async('string') |
| | | } |
| | | function runs(xml) { |
| | | return [...xml.matchAll(/<w:r\b[^>]*>([\s\S]*?)<\/w:r>/g)].map(match => ({ |
| | | text: [...match[1].matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)].map(m => m[1]).join(''), |
| | | align: match[1].match(/<w:vertAlign\b[^>]*w:val="([^"]+)"/)?.[1], |
| | | bold: /<w:b(?:\s|\/|>)/.test(match[1]) |
| | | })) |
| | | } |
| | | async function apply(label) { |
| | | const before = await page.evaluate(() => changes) |
| | | await page.getByRole('button', { name: label, exact: true }).click() |
| | | await page.locator('.el-message').last().waitFor() |
| | | const message = await page.locator('.el-message').last().textContent() |
| | | try { await page.waitForFunction(before => changes > before, before, { timeout: 5000 }) } |
| | | catch (error) { throw new Error(`${error.message}: ${message}`) } |
| | | } |
| | | await select(2, 4) |
| | | await apply('䏿 x²') |
| | | let formatted = runs(await xml()) |
| | | assert.ok(formatted.some(run => run.text === '-3' && run.align === 'superscript' && run.bold)) |
| | | assert.ok(formatted.filter(run => run.text && run.text !== '-3').every(run => run.align !== 'superscript')) |
| | | await select(6, 7) |
| | | await apply('䏿 xâ') |
| | | formatted = runs(await xml()) |
| | | assert.ok(formatted.some(run => run.text === '2' && run.align === 'subscript')) |
| | | const saved = await page.evaluate(() => exportDocument()) |
| | | await page.evaluate(bytes => openDocument(new Blob([new Uint8Array(bytes)])), saved) |
| | | await ready() |
| | | formatted = runs(await xml()) |
| | | assert.ok(formatted.some(run => run.text === '-3' && run.align === 'superscript')) |
| | | assert.ok(formatted.some(run => run.text === '2' && run.align === 'subscript')) |
| | | await select(2, 4) |
| | | await apply('æ¢å¤æ£å¸¸') |
| | | assert.ok(runs(await xml()).filter(run => run.text.includes('-3')).every(run => !run.align || run.align === 'baseline')) |
| | | await select(0, 0) |
| | | const before = await xml() |
| | | await page.getByRole('button', { name: '䏿 x²', exact: true }).click() |
| | | await page.getByText('请å
éä¸éè¦è®¾ç½®ä¸ä¸æ çæåï¼ä¾å¦ 10-3 ä¸ç -3', { exact: true }).waitFor() |
| | | assert.equal(await xml(), before) |
| | | await page.evaluate(() => readonlyState.value = true) |
| | | assert.equal(await page.getByRole('button', { name: '䏿 x²', exact: true }).isDisabled(), true) |
| | | assert.deepEqual(errors, []) |
| | | console.log(JSON.stringify({ result: 'PASS', superscript: true, subscript: true, preservedBold: true, |
| | | restoredBaseline: true, reopened: true, emptySelectionUnchanged: true, readonlyDisabled: true, browserErrors: errors }, null, 2)) |
| | | } finally { |
| | | await browser?.close() |
| | | await vite?.close() |
| | | if (server) await new Promise(resolve => server.close(resolve)) |
| | | await rm(fixture, { force: true }) |
| | | assert.ok(path.resolve(temporary).startsWith(path.resolve(tmpdir()) + path.sep + 'lims-superdoc-script-')) |
| | | await rm(temporary, { recursive: true, force: true }) |
| | | } |