| src/components/ReportWordEditor/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/utils/inspectionValueFormat.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/business/inspectionTask/components/InspectionWord.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/business/inspectionTask/inspection.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/business/inspectionView/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/workers/InspectionWorker.worker.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| tests/superdoc-script.smoke.mjs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/components/ReportWordEditor/index.vue
@@ -6,7 +6,15 @@ {{ label }} <input v-model.number="margins[side]" type="number" min="0" max="80" step="0.1" :aria-label="`${label}页边è·ï¼æ¯«ç±³ï¼`" :disabled="readonly || !ready || applyingMargins" /> </label> <el-button size="small" :loading="applyingMargins" :disabled="readonly || !ready" @click="applyMargins">åºç¨é¡µè¾¹è·</el-button> <el-button size="small" :loading="applyingMargins" :disabled="readonly || !ready || applyingScript" @click="applyMargins">åºç¨é¡µè¾¹è·</el-button> <div class="script-settings" role="group" aria-label="ä¸ä¸æ 设置"> <span>é䏿åå设置ï¼</span> <el-button v-for="option in scriptOptions" :key="option.value" size="small" :disabled="readonly || !ready || applyingMargins || applyingScript" :aria-label="option.label" :title="option.tip" @pointerdown.prevent="rememberScriptSelection" @mousedown.prevent @click="applyScript(option.value)">{{ option.label }}</el-button> </div> </div> <div ref="toolbar" class="word-toolbar" :inert="readonly || undefined"></div> <div class="word-canvas" :inert="readonly || undefined"> @@ -36,6 +44,12 @@ const editor = shallowRef(null) const ready = ref(false) const applyingMargins = ref(false) const applyingScript = ref(false) const scriptOptions = [ { value: 'superscript', label: '䏿 x²', tip: 'å°é䏿åè®¾ä¸ºä¸æ ï¼ä¾å¦ 10â»Â³ ä¸ç -3' }, { value: 'subscript', label: '䏿 xâ', tip: 'å°é䏿åè®¾ä¸ºä¸æ ï¼ä¾å¦ HâO ä¸ç 2' }, { value: 'baseline', label: 'æ¢å¤æ£å¸¸', tip: 'åæ¶é䏿åç䏿 æä¸æ ' } ] const error = ref('') const margins = ref({ top: 25.4, bottom: 25.4, left: 25.4, right: 25.4 }) const marginLabels = { top: 'ä¸', bottom: 'ä¸', left: 'å·¦', right: 'å³' } @@ -44,6 +58,12 @@ let toolbarObserver let fontSelectionTarget = false let fontSelectionKnown = false let scriptSelection = null function rememberScriptSelection() { // 卿é®ç¦ç¨æç¦ç¹ååä¹åå Worker 读åéåºï¼é¿å ç¹å»åéåºè¢«æ¸ 空ã scriptSelection = editor.value?.activeEditor.doc.selection.current().catch(() => null) } function rememberFontSelection(event) { if (!event.target?.closest?.('[data-item="btn-fontFamily"]')) return @@ -94,6 +114,7 @@ async function openDocument() { const current = ++generation scriptSelection = null clearTimeout(readyTimer) editor.value?.destroy() editor.value = null @@ -150,7 +171,7 @@ } async function applyMargins() { if (!ready.value || props.readonly || applyingMargins.value) return if (!ready.value || props.readonly || applyingMargins.value || applyingScript.value) return applyingMargins.value = true try { const sections = editor.value.activeEditor.doc.sections @@ -176,9 +197,41 @@ finally { applyingMargins.value = false } } async function applyScript(vertAlign) { if (!ready.value || props.readonly || applyingMargins.value || applyingScript.value) return const current = generation const superdoc = editor.value const selectionPromise = scriptSelection || superdoc.activeEditor.doc.selection.current() scriptSelection = null applyingScript.value = true try { // 读åå®é éåºï¼é¿å UI å¿«ç §å°æªæ´æ°æ¶ä¿®æ¹å°ä¹åéä¸çæåã const selection = await selectionPromise if (current !== generation || props.readonly) return if (!selection || selection.empty || !selection.selectionTarget) { ElMessage.info('请å éä¸éè¦è®¾ç½®ä¸ä¸æ çæåï¼ä¾å¦ 10-3 ä¸ç -3') return } const receipt = await superdoc.activeEditor.doc.format.apply({ target: selection.selectionTarget, // æ¸ é¤åææå¨åéä½ç½®ï¼è®© Word æä¸ä¸æ è§åæçã inline: { vertAlign, position: null } }) if (current !== generation) return if (!receipt.success && receipt.failure?.code !== 'NO_OP') { throw new Error(receipt.failure?.message || '设置ä¸ä¸æ 失败') } emit('change') ElMessage.success('æåæ ¼å¼å·²è®¾ç½®ï¼ç¹å»ä¿ååçæ') } catch (cause) { if (current === generation) ElMessage.error(cause.message || '设置ä¸ä¸æ 失败') } finally { applyingScript.value = false } } async function exportDocx() { if (!ready.value || error.value || !editor.value) throw new Error('Word å°æªå è½½å®æï¼ä¸è½ä¿å') if (applyingMargins.value) throw new Error('é¡µè¾¹è·æ£å¨åºç¨ï¼è¯·ç¨ååä¿å') if (applyingScript.value) throw new Error('ä¸ä¸æ æ£å¨è®¾ç½®ï¼è¯·ç¨ååä¿å') const blob = await editor.value.export({ exportType: ['docx'], triggerDownload: false }) if (!(blob instanceof Blob) || !blob.size) throw new Error('Word 导åºå¤±è´¥') return blob @@ -213,6 +266,8 @@ .page-settings { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; padding: 8px; background: #fff; } .page-settings label { display: flex; align-items: center; gap: 4px; } .page-settings input { width: 70px; padding: 5px; border: 1px solid #dcdfe6; border-radius: 4px; } .script-settings { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } .script-settings :deep(.el-button + .el-button) { margin-left: 0; } .word-toolbar { flex-shrink: 0; } .word-canvas { flex: 1; min-height: 0; overflow: auto; background: #f3f4f6; } /* SuperDoc æ»å¨æ¶ä¼æææ¡£å®¹å¨ç¼©å°çº¸å¼ 宽度ï¼å¯¼è´æ´é¡µéæ°è´´å°å·¦è¾¹ã */ src/utils/inspectionValueFormat.js
@@ -74,3 +74,9 @@ const config = parseInspectionValueAnnotation(annotation); return config && config.error ? config.error : null; } /** ç»ä¸è¯å«å¸¦å°æ°ä½çæ ¼å¼åæ°çæ°å¼æ¹æ³¨ã */ export function isInspectionValueAnnotation(annotation, target) { const config = parseInspectionValueAnnotation(annotation); return !!config && !config.error && (!target || config.target === target); } src/views/business/inspectionTask/components/InspectionWord.vue
@@ -111,9 +111,9 @@ </el-option> </el-select> </template> <template v-else-if="n.v.ps != undefined && n.v.ps.value === '计ç®å¼' && state == 1"><span <template v-else-if="n.v.ps != undefined && isInspectionValueAnnotation(n.v.ps.value, '计ç®å¼') && state == 1"><span :style="`font-family:${n.v.ff} !important;`">{{ n.v.v }}</span></template> <template v-else-if="n.v.ps != undefined && n.v.ps.value === 'æç»å¼' && state == 1"> <template v-else-if="n.v.ps != undefined && isInspectionValueAnnotation(n.v.ps.value, 'æç»å¼') && state == 1"> <span :style="`font-family:${n.v.ff} !important;`">{{ n.v.v }}</span> </template> <template v-else-if="n.v.ps != undefined && n.v.ps.value === 'æ ·åç¼å·'"> @@ -251,6 +251,7 @@ </template> <script> import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "@/utils/inspectionValueFormat"; // Vue3 没æ this.$createElementï¼ä¸é¢ç¨ h é VNode å½ $msgbox ç messageï¼ import { h } from 'vue' import { readDetectionTimeRange, isCompleteDetectionTimeRange, toDetectionTimePayload } from '@/utils/detectionTimeRange' @@ -599,6 +600,7 @@ this.stopWorker(); }, methods: { isInspectionValueAnnotation, closeAddVerifyDia() { this.addCheck = false this.checkUser = '' @@ -1089,8 +1091,8 @@ b.v.ps != undefined && typeof b.v.ps.value === 'string' && (b.v.ps.value.includes('æ£éªå¼') || b.v.ps.value === '计ç®å¼' || b.v.ps.value === 'æç»å¼' || isInspectionValueAnnotation(b.v.ps.value, '计ç®å¼') || isInspectionValueAnnotation(b.v.ps.value, 'æç»å¼') || b.v.ps.value === '设å¤åç§°') ) { b.v['v'] = '' @@ -1151,12 +1153,12 @@ if (value === undefined || value === null) { return } cell.v.v = value cell.v.v = formatInspectionValue(value, parseInspectionValueAnnotation(cell.v.ps && cell.v.ps.value)) if (typeof item === 'object' && item.u) { cell.u = item.u } this.param[a.templateId][key] = { v: value, v: cell.v.v, u: typeof item === 'object' ? item.u : cell.u } }) @@ -1553,8 +1555,8 @@ cell.v.ps != undefined && typeof cell.v.ps.value === 'string' && (cell.v.ps.value.includes('æ£éªå¼') || cell.v.ps.value === '计ç®å¼' || cell.v.ps.value === 'æç»å¼' || isInspectionValueAnnotation(cell.v.ps.value, '计ç®å¼') || isInspectionValueAnnotation(cell.v.ps.value, 'æç»å¼') || cell.v.ps.value === '设å¤åç§°') if (!isEditable) { return @@ -1810,6 +1812,23 @@ flex: 0 0 360px; } /* 夿 ¸æ¶éæ©æ¡ç¦ç¨ï¼ç´æ¥å±å¼å®æ´è®¾å¤åç§°åç¼å·ã */ .template-panel:deep(.device-select .el-tag) { height: auto; min-height: 24px; padding-top: 2px; padding-bottom: 2px; max-width: 100%; } .template-panel:deep(.device-select .el-select__tags-text) { white-space: normal; overflow-wrap: anywhere; overflow: visible; text-overflow: clip; line-height: 1.5; } .template-panel__unit { margin-left: 4px; color: var(--lims-text-2); src/views/business/inspectionTask/inspection.vue
@@ -280,14 +280,14 @@ </template> <template v-else-if=" n.v.ps != undefined && n.v.ps.value === '计ç®å¼' && isInspectionValueAnnotation(n.v.ps.value, '计ç®å¼') && state == 1 "><span :style="`font-family:${n.v.ff} !important;`">{{ n.v.v }}</span></template> <template v-else-if=" n.v.ps != undefined && n.v.ps.value === 'æç»å¼' && isInspectionValueAnnotation(n.v.ps.value, 'æç»å¼') && state == 1 "> <span :style="`font-family:${n.v.ff} !important;`">{{ @@ -523,7 +523,7 @@ import { mapGetters } from "@/utils/store-compat"; import viewManHourDia from "@/views/business/inspectionTask/components/viewManHourDia.vue"; import {getDicts} from "@/api/system/dict/data"; import { parseInspectionValueAnnotation, formatInspectionValue } from "@/utils/inspectionValueFormat"; import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "@/utils/inspectionValueFormat"; export default { name: 'Inspection', components: { @@ -838,6 +838,7 @@ this.stopWorker(); }, methods: { isInspectionValueAnnotation, syncRouteContext() { const { sonLaboratory, orderId, state, inspectorList, typeSource, taskSource } = this.$route.query; const orderChanged = this.id !== orderId; @@ -1509,8 +1510,8 @@ b.v.ps != undefined && typeof b.v.ps.value === "string" && (b.v.ps.value.includes("æ£éªå¼") || b.v.ps.value === "计ç®å¼" || b.v.ps.value === "æç»å¼" || isInspectionValueAnnotation(b.v.ps.value, "计ç®å¼") || isInspectionValueAnnotation(b.v.ps.value, "æç»å¼") || b.v.ps.value === "设å¤åç§°") ) { b.v["v"] = ""; @@ -1575,12 +1576,12 @@ if (value === undefined || value === null) { return; } cell.v.v = value; cell.v.v = formatInspectionValue(value, parseInspectionValueAnnotation(cell.v.ps && cell.v.ps.value)); if (typeof item === "object" && item.u) { cell.u = item.u; } this.param[a.templateId][key] = { v: value, v: cell.v.v, u: typeof item === "object" ? item.u : cell.u, }; }); @@ -2012,8 +2013,8 @@ cell.v.ps != undefined && typeof cell.v.ps.value === "string" && (cell.v.ps.value.includes("æ£éªå¼") || cell.v.ps.value === "计ç®å¼" || cell.v.ps.value === "æç»å¼" || isInspectionValueAnnotation(cell.v.ps.value, "计ç®å¼") || isInspectionValueAnnotation(cell.v.ps.value, "æç»å¼") || cell.v.ps.value === "设å¤åç§°"); const isExportResult = resultHeader && exportRows.has(cell.r) && cell.c === resultHeader.c; if (!isEditable && !isExportResult) { @@ -2335,6 +2336,23 @@ flex: 0 0 360px; } /* 夿 ¸æ¶éæ©æ¡ç¦ç¨ï¼ç´æ¥å±å¼å®æ´è®¾å¤åç§°åç¼å·ã */ .template-panel:deep(.device-select .el-tag) { height: auto; min-height: 24px; padding-top: 2px; padding-bottom: 2px; max-width: 100%; } .template-panel:deep(.device-select .el-select__tags-text) { white-space: normal; overflow-wrap: anywhere; overflow: visible; text-overflow: clip; line-height: 1.5; } .template-panel__unit { margin-left: 4px; color: var(--lims-text-2); src/views/business/inspectionView/index.vue
@@ -250,14 +250,14 @@ </template> <template v-else-if=" n.v.ps != undefined && n.v.ps.value === '计ç®å¼' && isInspectionValueAnnotation(n.v.ps.value, '计ç®å¼') && state == 1 "><span :style="`font-family:${n.v.ff} !important;`">{{ n.v.v }}</span></template> <template v-else-if=" n.v.ps != undefined && n.v.ps.value === 'æç»å¼' && isInspectionValueAnnotation(n.v.ps.value, 'æç»å¼') && state == 1 "> <span :style="`font-family:${n.v.ff} !important;`">{{ @@ -489,6 +489,7 @@ </template> <script> import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "@/utils/inspectionValueFormat"; // Vue3 没æ this.$createElementï¼ä¸é¢ç¨ h é VNode å½ $msgbox ç messageï¼ import { h } from 'vue' import { readDetectionTimeRange, isCompleteDetectionTimeRange, toDetectionTimePayload } from '@/utils/detectionTimeRange' @@ -846,6 +847,7 @@ this.stopWorker(); }, methods: { isInspectionValueAnnotation, syncRouteContext() { const { sonLaboratory, orderId, state, inspectorList, typeSource } = this.$route.query; const orderChanged = this.id !== orderId; @@ -1779,8 +1781,8 @@ b.v.ps != undefined && typeof b.v.ps.value === "string" && (b.v.ps.value.includes("æ£éªå¼") || b.v.ps.value === "计ç®å¼" || b.v.ps.value === "æç»å¼" || isInspectionValueAnnotation(b.v.ps.value, "计ç®å¼") || isInspectionValueAnnotation(b.v.ps.value, "æç»å¼") || b.v.ps.value === "设å¤åç§°") ) { b.v["v"] = ""; @@ -1845,12 +1847,12 @@ if (value === undefined || value === null) { return; } cell.v.v = value; cell.v.v = formatInspectionValue(value, parseInspectionValueAnnotation(cell.v.ps && cell.v.ps.value)); if (typeof item === "object" && item.u) { cell.u = item.u; } this.param[a.templateId][key] = { v: value, v: cell.v.v, u: typeof item === "object" ? item.u : cell.u, }; }); @@ -2233,8 +2235,8 @@ cell.v.ps != undefined && typeof cell.v.ps.value === "string" && (cell.v.ps.value.includes("æ£éªå¼") || cell.v.ps.value === "计ç®å¼" || cell.v.ps.value === "æç»å¼" || isInspectionValueAnnotation(cell.v.ps.value, "计ç®å¼") || isInspectionValueAnnotation(cell.v.ps.value, "æç»å¼") || cell.v.ps.value === "设å¤åç§°"); const isExportResult = resultHeader && exportRows.has(cell.r) && cell.c === resultHeader.c; if (!isEditable && !isExportResult) { @@ -2552,6 +2554,23 @@ flex: 0 0 360px; } /* 夿 ¸æ¶éæ©æ¡ç¦ç¨ï¼ç´æ¥å±å¼å®æ´è®¾å¤åç§°åç¼å·ã */ .template-panel:deep(.device-select .el-tag) { height: auto; min-height: 24px; padding-top: 2px; padding-bottom: 2px; max-width: 100%; } .template-panel:deep(.device-select .el-select__tags-text) { white-space: normal; overflow-wrap: anywhere; overflow: visible; text-overflow: clip; line-height: 1.5; } .template-panel__unit { margin-left: 4px; color: var(--lims-text-2); src/workers/InspectionWorker.worker.js
@@ -1,6 +1,6 @@ // å¤çº¿ç¨éé¢éè¦ä¿åçæ°æ® import Big from "big.js"; import { parseInspectionValueAnnotation, formatInspectionValue } from "../utils/inspectionValueFormat"; import { parseInspectionValueAnnotation, formatInspectionValue, isInspectionValueAnnotation } from "../utils/inspectionValueFormat"; let code = ""; // è¡¨æ ¼æ°æ®ï¼æ¸²æï¼ @@ -574,8 +574,8 @@ b.v.ps != undefined && typeof b.v.ps.value == "string" && (b.v.ps.value.includes("æ£éªå¼") || b.v.ps.value === "计ç®å¼" || b.v.ps.value === "æç»å¼" || isInspectionValueAnnotation(b.v.ps.value, "计ç®å¼") || isInspectionValueAnnotation(b.v.ps.value, "æç»å¼") || b.v.ps.value === "设å¤åç§°"); const isExportResult = resultHeader && exportRows.has(b.r) && b.c === resultHeader.c; if (!isEditable && !isExportResult) { tests/superdoc-script.smoke.mjs
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,153 @@ // 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 }) }