From 3ec220a3d7f0684388bc2928ceea8e83655f500c Mon Sep 17 00:00:00 2001 From: Brownie Woom <161313394+brownie-cake@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:44:15 +0800 Subject: [PATCH 1/4] chore: stage DOCX issue workspace --- .github/workflows/docx-issue-workspace.yml | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/docx-issue-workspace.yml diff --git a/.github/workflows/docx-issue-workspace.yml b/.github/workflows/docx-issue-workspace.yml new file mode 100644 index 00000000..23c58d0e --- /dev/null +++ b/.github/workflows/docx-issue-workspace.yml @@ -0,0 +1,58 @@ +name: DOCX issue workspace + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + package: + if: github.head_ref == 'feature/docx-layout-155-161' + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - name: Download public issue fixtures + shell: bash + run: | + set -euo pipefail + mkdir -p issue-fixtures + curl --fail --location --retry 3 \ + 'https://github.com/user-attachments/files/30333140/Japanese-Template.docx' \ + --output issue-fixtures/issue-155-japanese-template.docx + curl --fail --location --retry 3 \ + 'https://github.com/user-attachments/files/30398444/-.W00213.docx' \ + --output issue-fixtures/issue-161-resume.docx + unzip -t issue-fixtures/issue-155-japanese-template.docx + unzip -t issue-fixtures/issue-161-resume.docx + sha256sum issue-fixtures/*.docx > issue-fixtures/SHA256SUMS + + - name: Stage source workspace + shell: bash + run: | + set -euo pipefail + root=/tmp/file-viewer-docx-workspace + rm -rf "$root" + mkdir -p "$root/file-viewer" + rsync -a ./ "$root/file-viewer/" \ + --exclude '.git/' \ + --exclude 'node_modules/' \ + --exclude '**/dist/' \ + --exclude '**/.vitepress/cache/' + printf 'head=%s\n' "$GITHUB_SHA" > "$root/file-viewer/WORKSPACE_SOURCE.txt" + + - name: Upload workspace + uses: actions/upload-artifact@v4 + with: + name: file-viewer-docx-issues-155-161-workspace + path: /tmp/file-viewer-docx-workspace + if-no-files-found: error + retention-days: 2 + compression-level: 6 From 0487d7b187b61723b9ebd6da5e45197b9a7079b5 Mon Sep 17 00:00:00 2001 From: Brownie Woom <161313394+brownie-cake@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:48:18 +0800 Subject: [PATCH 2/4] chore: add DOCX issue diagnostics --- .github/workflows/docx-issue-diagnostics.yml | 181 +++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 .github/workflows/docx-issue-diagnostics.yml diff --git a/.github/workflows/docx-issue-diagnostics.yml b/.github/workflows/docx-issue-diagnostics.yml new file mode 100644 index 00000000..45be189e --- /dev/null +++ b/.github/workflows/docx-issue-diagnostics.yml @@ -0,0 +1,181 @@ +name: DOCX issue diagnostics + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + diagnose: + if: github.head_ref == 'feature/docx-layout-155-161' + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + with: + version: 11.0.9 + run_install: false + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Download public issue fixtures + shell: bash + run: | + set -euo pipefail + mkdir -p apps/viewer-demo/public/issue-fixtures + curl --fail --location --retry 3 \ + 'https://github.com/user-attachments/files/30333140/Japanese-Template.docx' \ + --output apps/viewer-demo/public/issue-fixtures/issue-155-japanese-template.docx + curl --fail --location --retry 3 \ + 'https://github.com/user-attachments/files/30398444/-.W00213.docx' \ + --output apps/viewer-demo/public/issue-fixtures/issue-161-resume.docx + + - name: Build + run: pnpm build + + - name: Install Chromium + run: pnpm exec playwright install --with-deps chromium + + - name: Render diagnostic snapshots + shell: bash + run: | + cat > /tmp/render-docx-diagnostics.mjs <<'NODE' + import { createReadStream, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs' + import { createServer } from 'node:http' + import { extname, join, normalize, resolve } from 'node:path' + import { chromium } from 'playwright' + + const distDir = resolve('apps/viewer-demo/dist') + const outputDir = resolve('/tmp/docx-issue-diagnostics') + mkdirSync(outputDir, { recursive: true }) + const entryPath = join(distDir, 'index.html') + if (!existsSync(entryPath)) throw new Error(`Missing demo build: ${entryPath}`) + const types = { + '.css': 'text/css; charset=utf-8', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', '.png': 'image/png', '.svg': 'image/svg+xml', '.wasm': 'application/wasm' + } + const server = createServer((request, response) => { + const url = new URL(request.url || '/', 'http://127.0.0.1') + const clean = decodeURIComponent(url.pathname) + const candidate = clean === '/' ? entryPath : join(distDir, normalize(clean).replace(/^[/\\]+/, '')) + const file = existsSync(candidate) && statSync(candidate).isFile() ? candidate : entryPath + response.writeHead(200, { 'Content-Type': types[extname(file).toLowerCase()] || 'application/octet-stream', 'Cache-Control': 'no-store' }) + createReadStream(file).pipe(response) + }) + await new Promise((resolveListen, rejectListen) => { + server.once('error', rejectListen) + server.listen(0, '127.0.0.1', resolveListen) + }) + const address = server.address() + const baseUrl = `http://127.0.0.1:${address.port}` + const browser = await chromium.launch({ headless: true }) + + const cases = [ + ['155', '/issue-fixtures/issue-155-japanese-template.docx'], + ['161', '/issue-fixtures/issue-161-resume.docx'] + ] + try { + for (const [id, url] of cases) { + const page = await browser.newPage({ viewport: { width: 1600, height: 1100 }, deviceScaleFactor: 1 }) + const consoleErrors = [] + page.on('console', message => { if (message.type() === 'error') consoleErrors.push(message.text()) }) + page.on('pageerror', error => consoleErrors.push(error.stack || error.message)) + await page.goto(`${baseUrl}/?lang=zh&url=${encodeURIComponent(url)}&diagnostic=issue-${id}`, { waitUntil: 'domcontentloaded', timeout: 120000 }) + await page.waitForSelector('.file-viewer .content:not(.hidden) .docx-fit-viewer', { timeout: 120000 }) + await page.waitForTimeout(5000) + await page.screenshot({ path: join(outputDir, `issue-${id}-viewport.png`), fullPage: false }) + const viewer = page.locator('.file-viewer').first() + await viewer.screenshot({ path: join(outputDir, `issue-${id}-viewer.png`) }) + const data = await page.evaluate(() => { + const root = document.querySelector('.docx-fit-viewer') + const wrapper = root?.querySelector('.docx-wrapper') + const frames = Array.from(root?.querySelectorAll('.docx-page-frame, .docx-flow-frame') || []) + const sections = Array.from(root?.querySelectorAll('section.docx') || []) + const articles = Array.from(root?.querySelectorAll('section.docx > article') || []) + const textNodes = [] + const walker = root ? document.createTreeWalker(root, NodeFilter.SHOW_TEXT) : null + let node + while (walker && (node = walker.nextNode())) { + const text = node.textContent?.trim() + const parent = node.parentElement + if (!text || !parent) continue + const rect = parent.getBoundingClientRect() + const style = getComputedStyle(parent) + textNodes.push({ + text: text.slice(0, 120), tag: parent.tagName, className: parent.className, + left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height, + fontSize: style.fontSize, lineHeight: style.lineHeight, fontFamily: style.fontFamily, + position: style.position, transform: style.transform, whiteSpace: style.whiteSpace, + verticalAlign: style.verticalAlign, display: style.display + }) + if (textNodes.length >= 1500) break + } + const elementSummary = elements => elements.map((element, index) => { + const rect = element.getBoundingClientRect() + const style = getComputedStyle(element) + return { + index, className: element.className, left: rect.left, top: rect.top, width: rect.width, height: rect.height, + scrollWidth: element.scrollWidth, scrollHeight: element.scrollHeight, + position: style.position, overflow: style.overflow, transform: style.transform, + pageWidth: style.getPropertyValue('--docx-page-width'), pageHeight: style.getPropertyValue('--docx-page-height') + } + }) + return { + href: location.href, + rootHtmlLength: root?.innerHTML.length || 0, + wrapper: wrapper ? elementSummary([wrapper])[0] : null, + frames: elementSummary(frames), sections: elementSummary(sections), articles: elementSummary(articles), + textNodes, + counts: { + frames: frames.length, sections: sections.length, articles: articles.length, + drawings: root?.querySelectorAll('svg, canvas, img').length || 0, + absolute: root ? Array.from(root.querySelectorAll('*')).filter(el => getComputedStyle(el).position === 'absolute').length : 0 + } + } + }) + data.consoleErrors = consoleErrors + writeFileSync(join(outputDir, `issue-${id}-dom.json`), JSON.stringify(data, null, 2)) + const html = await page.locator('.docx-fit-viewer').first().evaluate(element => element.outerHTML) + writeFileSync(join(outputDir, `issue-${id}-rendered.html`), html) + await page.close() + } + } finally { + await browser.close() + await new Promise(resolveClose => server.close(resolveClose)) + } + NODE + node /tmp/render-docx-diagnostics.mjs + + - name: Copy DOCX engine package + shell: bash + run: | + set -euo pipefail + engine=$(find node_modules/.pnpm -type d -path '*/node_modules/@file-viewer/docx' | head -n 1) + test -n "$engine" + mkdir -p /tmp/docx-issue-diagnostics/engine + cp -a "$engine"/. /tmp/docx-issue-diagnostics/engine/ + printf '%s\n' "$engine" > /tmp/docx-issue-diagnostics/engine-source-path.txt + + - name: Upload diagnostics + uses: actions/upload-artifact@v4 + with: + name: file-viewer-docx-issues-155-161-diagnostics + path: /tmp/docx-issue-diagnostics + if-no-files-found: error + retention-days: 2 + compression-level: 6 From b7f00e8d886bb3ba4c7fd0e67ba45d423d0beb83 Mon Sep 17 00:00:00 2001 From: Brownie Woom <161313394+brownie-cake@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:54:32 +0800 Subject: [PATCH 3/4] chore: make DOCX diagnostics artifact-safe --- .github/workflows/docx-issue-diagnostics.yml | 116 ++++++++++++++----- 1 file changed, 88 insertions(+), 28 deletions(-) diff --git a/.github/workflows/docx-issue-diagnostics.yml b/.github/workflows/docx-issue-diagnostics.yml index 45be189e..42339de1 100644 --- a/.github/workflows/docx-issue-diagnostics.yml +++ b/.github/workflows/docx-issue-diagnostics.yml @@ -51,8 +51,12 @@ jobs: run: pnpm exec playwright install --with-deps chromium - name: Render diagnostic snapshots + id: render + continue-on-error: true shell: bash run: | + set -o pipefail + mkdir -p /tmp/docx-issue-diagnostics cat > /tmp/render-docx-diagnostics.mjs <<'NODE' import { createReadStream, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs' import { createServer } from 'node:http' @@ -65,16 +69,25 @@ jobs: const entryPath = join(distDir, 'index.html') if (!existsSync(entryPath)) throw new Error(`Missing demo build: ${entryPath}`) const types = { - '.css': 'text/css; charset=utf-8', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', - '.mjs': 'text/javascript; charset=utf-8', '.png': 'image/png', '.svg': 'image/svg+xml', '.wasm': 'application/wasm' + '.css': 'text/css; charset=utf-8', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.wasm': 'application/wasm' } const server = createServer((request, response) => { const url = new URL(request.url || '/', 'http://127.0.0.1') const clean = decodeURIComponent(url.pathname) const candidate = clean === '/' ? entryPath : join(distDir, normalize(clean).replace(/^[/\\]+/, '')) const file = existsSync(candidate) && statSync(candidate).isFile() ? candidate : entryPath - response.writeHead(200, { 'Content-Type': types[extname(file).toLowerCase()] || 'application/octet-stream', 'Cache-Control': 'no-store' }) + response.writeHead(200, { + 'Content-Type': types[extname(file).toLowerCase()] || 'application/octet-stream', + 'Cache-Control': 'no-store' + }) createReadStream(file).pipe(response) }) await new Promise((resolveListen, rejectListen) => { @@ -84,23 +97,33 @@ jobs: const address = server.address() const baseUrl = `http://127.0.0.1:${address.port}` const browser = await chromium.launch({ headless: true }) - const cases = [ ['155', '/issue-fixtures/issue-155-japanese-template.docx'], ['161', '/issue-fixtures/issue-161-resume.docx'] ] + try { for (const [id, url] of cases) { const page = await browser.newPage({ viewport: { width: 1600, height: 1100 }, deviceScaleFactor: 1 }) const consoleErrors = [] - page.on('console', message => { if (message.type() === 'error') consoleErrors.push(message.text()) }) + page.on('console', message => { + if (message.type() === 'error') consoleErrors.push(message.text()) + }) page.on('pageerror', error => consoleErrors.push(error.stack || error.message)) - await page.goto(`${baseUrl}/?lang=zh&url=${encodeURIComponent(url)}&diagnostic=issue-${id}`, { waitUntil: 'domcontentloaded', timeout: 120000 }) - await page.waitForSelector('.file-viewer .content:not(.hidden) .docx-fit-viewer', { timeout: 120000 }) + + await page.goto(`${baseUrl}/?lang=zh&url=${encodeURIComponent(url)}&diagnostic=issue-${id}`, { + waitUntil: 'domcontentloaded', + timeout: 120000 + }) + await page.waitForSelector('.file-viewer .content.docx-fit-viewer:not(.hidden)', { timeout: 120000 }) await page.waitForTimeout(5000) await page.screenshot({ path: join(outputDir, `issue-${id}-viewport.png`), fullPage: false }) - const viewer = page.locator('.file-viewer').first() - await viewer.screenshot({ path: join(outputDir, `issue-${id}-viewer.png`) }) + + const firstFrame = page.locator('.docx-page-frame, .docx-flow-frame').first() + if (await firstFrame.count()) { + await firstFrame.screenshot({ path: join(outputDir, `issue-${id}-first-frame.png`) }) + } + const data = await page.evaluate(() => { const root = document.querySelector('.docx-fit-viewer') const wrapper = root?.querySelector('.docx-wrapper') @@ -117,34 +140,62 @@ jobs: const rect = parent.getBoundingClientRect() const style = getComputedStyle(parent) textNodes.push({ - text: text.slice(0, 120), tag: parent.tagName, className: parent.className, - left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height, - fontSize: style.fontSize, lineHeight: style.lineHeight, fontFamily: style.fontFamily, - position: style.position, transform: style.transform, whiteSpace: style.whiteSpace, - verticalAlign: style.verticalAlign, display: style.display + text: text.slice(0, 160), + tag: parent.tagName, + className: parent.className, + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + width: rect.width, + height: rect.height, + fontSize: style.fontSize, + lineHeight: style.lineHeight, + fontFamily: style.fontFamily, + position: style.position, + transform: style.transform, + whiteSpace: style.whiteSpace, + verticalAlign: style.verticalAlign, + display: style.display }) - if (textNodes.length >= 1500) break + if (textNodes.length >= 2500) break } const elementSummary = elements => elements.map((element, index) => { const rect = element.getBoundingClientRect() const style = getComputedStyle(element) return { - index, className: element.className, left: rect.left, top: rect.top, width: rect.width, height: rect.height, - scrollWidth: element.scrollWidth, scrollHeight: element.scrollHeight, - position: style.position, overflow: style.overflow, transform: style.transform, - pageWidth: style.getPropertyValue('--docx-page-width'), pageHeight: style.getPropertyValue('--docx-page-height') + index, + className: element.className, + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + scrollWidth: element.scrollWidth, + scrollHeight: element.scrollHeight, + position: style.position, + overflow: style.overflow, + transform: style.transform, + pageWidth: style.getPropertyValue('--docx-page-width'), + pageHeight: style.getPropertyValue('--docx-page-height') } }) + const allElements = root ? Array.from(root.querySelectorAll('*')) : [] return { href: location.href, + rootClassName: root?.className || '', rootHtmlLength: root?.innerHTML.length || 0, wrapper: wrapper ? elementSummary([wrapper])[0] : null, - frames: elementSummary(frames), sections: elementSummary(sections), articles: elementSummary(articles), + frames: elementSummary(frames), + sections: elementSummary(sections), + articles: elementSummary(articles), textNodes, counts: { - frames: frames.length, sections: sections.length, articles: articles.length, + frames: frames.length, + sections: sections.length, + articles: articles.length, drawings: root?.querySelectorAll('svg, canvas, img').length || 0, - absolute: root ? Array.from(root.querySelectorAll('*')).filter(el => getComputedStyle(el).position === 'absolute').length : 0 + absolute: allElements.filter(element => getComputedStyle(element).position === 'absolute').length, + alternateChoiceMarkers: root?.querySelectorAll('[data-mc-choice], [data-alternate-choice]').length || 0 } } }) @@ -159,19 +210,24 @@ jobs: await new Promise(resolveClose => server.close(resolveClose)) } NODE - node /tmp/render-docx-diagnostics.mjs + node /tmp/render-docx-diagnostics.mjs 2>&1 | tee /tmp/docx-issue-diagnostics/render.log - name: Copy DOCX engine package + if: always() shell: bash run: | set -euo pipefail - engine=$(find node_modules/.pnpm -type d -path '*/node_modules/@file-viewer/docx' | head -n 1) - test -n "$engine" mkdir -p /tmp/docx-issue-diagnostics/engine - cp -a "$engine"/. /tmp/docx-issue-diagnostics/engine/ - printf '%s\n' "$engine" > /tmp/docx-issue-diagnostics/engine-source-path.txt + engine=$(find node_modules/.pnpm -type d -path '*/node_modules/@file-viewer/docx' -print -quit) + if [ -n "$engine" ]; then + cp -a "$engine"/. /tmp/docx-issue-diagnostics/engine/ + printf '%s\n' "$engine" > /tmp/docx-issue-diagnostics/engine-source-path.txt + else + printf 'DOCX engine package not found\n' > /tmp/docx-issue-diagnostics/engine-copy-error.txt + fi - name: Upload diagnostics + if: always() uses: actions/upload-artifact@v4 with: name: file-viewer-docx-issues-155-161-diagnostics @@ -179,3 +235,7 @@ jobs: if-no-files-found: error retention-days: 2 compression-level: 6 + + - name: Fail when rendering diagnostics failed + if: steps.render.outcome == 'failure' + run: exit 1 From 4d4e39c3edd4b7be8963d95aff5ddefe2a9c6415 Mon Sep 17 00:00:00 2001 From: Brownie Woom <161313394+brownie-cake@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:03:29 +0800 Subject: [PATCH 4/4] chore: rerun DOCX diagnostics from workspace root --- .../workflows/docx-issue-diagnostics-v2.yml | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 .github/workflows/docx-issue-diagnostics-v2.yml diff --git a/.github/workflows/docx-issue-diagnostics-v2.yml b/.github/workflows/docx-issue-diagnostics-v2.yml new file mode 100644 index 00000000..6e156379 --- /dev/null +++ b/.github/workflows/docx-issue-diagnostics-v2.yml @@ -0,0 +1,158 @@ +name: DOCX issue diagnostics v2 + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + diagnose: + if: github.head_ref == 'feature/docx-layout-155-161' + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + with: + version: 11.0.9 + run_install: false + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Download public fixtures + run: | + mkdir -p apps/viewer-demo/public/issue-fixtures + curl --fail --location --retry 3 'https://github.com/user-attachments/files/30333140/Japanese-Template.docx' --output apps/viewer-demo/public/issue-fixtures/issue-155.docx + curl --fail --location --retry 3 'https://github.com/user-attachments/files/30398444/-.W00213.docx' --output apps/viewer-demo/public/issue-fixtures/issue-161.docx + - run: pnpm build + - run: pnpm exec playwright install --with-deps chromium + + - name: Capture current rendering + shell: bash + run: | + mkdir -p /tmp/docx-diagnostics-v2 + cat > .docx-diagnostics-v2.mjs <<'NODE' + import { createReadStream, existsSync, statSync, writeFileSync } from 'node:fs' + import { createServer } from 'node:http' + import { extname, join, normalize, resolve } from 'node:path' + import { chromium } from 'playwright' + + const dist = resolve('apps/viewer-demo/dist') + const entry = join(dist, 'index.html') + const outputDir = '/tmp/docx-diagnostics-v2' + const mime = { + '.css': 'text/css; charset=utf-8', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.wasm': 'application/wasm' + } + const server = createServer((request, response) => { + const pathname = decodeURIComponent(new URL(request.url || '/', 'http://127.0.0.1').pathname) + const candidate = pathname === '/' ? entry : join(dist, normalize(pathname).replace(/^[/\\]+/, '')) + const file = existsSync(candidate) && statSync(candidate).isFile() ? candidate : entry + response.writeHead(200, { 'Content-Type': mime[extname(file).toLowerCase()] || 'application/octet-stream', 'Cache-Control': 'no-store' }) + createReadStream(file).pipe(response) + }) + await new Promise((resolveListen, rejectListen) => { + server.once('error', rejectListen) + server.listen(0, '127.0.0.1', resolveListen) + }) + const address = server.address() + const base = `http://127.0.0.1:${address.port}` + const browser = await chromium.launch({ headless: true }) + const cases = [['155', 'issue-155.docx'], ['161', 'issue-161.docx']] + + try { + for (const [id, filename] of cases) { + const page = await browser.newPage({ viewport: { width: 1600, height: 1100 } }) + const errors = [] + page.on('console', message => { if (message.type() === 'error') errors.push(message.text()) }) + page.on('pageerror', error => errors.push(error.stack || error.message)) + await page.goto(`${base}/?lang=zh&url=${encodeURIComponent(`/issue-fixtures/${filename}`)}`, { waitUntil: 'domcontentloaded', timeout: 120000 }) + await page.waitForSelector('.file-viewer .content.docx-fit-viewer:not(.hidden)', { timeout: 120000 }) + await page.waitForTimeout(4000) + + const data = await page.evaluate(() => { + const root = document.querySelector('.docx-fit-viewer') + const frames = Array.from(root?.querySelectorAll('.docx-page-frame, .docx-flow-frame') || []) + const sections = Array.from(root?.querySelectorAll('section.docx') || []) + const elements = root ? Array.from(root.querySelectorAll('*')) : [] + const leaves = elements.filter(element => !element.children.length && element.textContent?.trim()) + const textNodes = leaves.slice(0, 3000).map(element => { + const rect = element.getBoundingClientRect() + const style = getComputedStyle(element) + return { + text: element.textContent.trim().slice(0, 180), + tag: element.tagName, + className: element.className, + rect: [rect.left, rect.top, rect.width, rect.height], + fontSize: style.fontSize, + lineHeight: style.lineHeight, + fontFamily: style.fontFamily, + position: style.position, + transform: style.transform, + overflow: style.overflow + } + }) + const summary = nodes => nodes.map((node, index) => { + const rect = node.getBoundingClientRect() + const style = getComputedStyle(node) + return { + index, + className: node.className, + rect: [rect.left, rect.top, rect.width, rect.height], + scroll: [node.scrollWidth, node.scrollHeight], + position: style.position, + overflow: style.overflow, + transform: style.transform, + section: node.dataset.docxSection, + sectionNumber: node.dataset.docxSectionNumber + } + }) + return { + rootClassName: root?.className || '', + rootHtmlLength: root?.innerHTML.length || 0, + counts: { + frames: frames.length, + sections: sections.length, + drawings: root?.querySelectorAll('svg,img,canvas').length || 0, + absolute: elements.filter(element => getComputedStyle(element).position === 'absolute').length + }, + frames: summary(frames), + sections: summary(sections), + textNodes + } + }) + data.consoleErrors = errors + writeFileSync(`${outputDir}/issue-${id}-dom.json`, JSON.stringify(data, null, 2)) + writeFileSync(`${outputDir}/issue-${id}-rendered.html`, await page.locator('.docx-fit-viewer').first().evaluate(element => element.outerHTML)) + await page.screenshot({ path: `${outputDir}/issue-${id}-viewport.png` }) + const first = page.locator('.docx-page-frame, .docx-flow-frame').first() + if (await first.count()) await first.screenshot({ path: `${outputDir}/issue-${id}-first-frame.png` }) + await page.close() + } + } finally { + await browser.close() + await new Promise(resolveClose => server.close(resolveClose)) + } + NODE + node .docx-diagnostics-v2.mjs 2>&1 | tee /tmp/docx-diagnostics-v2/render.log + rm .docx-diagnostics-v2.mjs + + - name: Upload diagnostics + uses: actions/upload-artifact@v4 + with: + name: file-viewer-docx-issues-155-161-diagnostics-v2 + path: /tmp/docx-diagnostics-v2 + if-no-files-found: error + retention-days: 2