| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- import { createReadStream, existsSync, statSync } from 'node:fs'
- import { createServer } from 'node:http'
- import { createRequire } from 'node:module'
- import { delimiter, extname, join, normalize, relative, resolve, sep } from 'node:path'
- import { pathToFileURL } from 'node:url'
- const outputDir = resolve(process.env.WRAPPER_DEMO_OUTPUT_DIR || 'apps/component-demo/dist')
- const externalUrl = process.env.WRAPPER_DEMO_URL
- const timeout = Number(process.env.WRAPPER_BROWSER_SMOKE_TIMEOUT || 30000)
- const mimeTypes = new Map([
- ['.css', 'text/css; charset=utf-8'],
- ['.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
- ['.html', 'text/html; charset=utf-8'],
- ['.ico', 'image/x-icon'],
- ['.js', 'text/javascript; charset=utf-8'],
- ['.json', 'application/json; charset=utf-8'],
- ['.png', 'image/png'],
- ['.wasm', 'application/wasm']
- ])
- const require = createRequire(import.meta.url)
- const fail = message => {
- console.error(`[wrapper-browser-smoke] ${message}`)
- process.exit(1)
- }
- const importPlaywright = async () => {
- try {
- return await import('playwright')
- } catch (error) {
- const candidatePaths = process.env.PATH
- ?.split(delimiter)
- .filter(pathEntry => pathEntry.endsWith(`${sep}node_modules${sep}.bin`))
- .map(binDir => resolve(binDir, '..'))
- .filter(pathEntry => existsSync(pathEntry)) || []
- for (const candidatePath of candidatePaths) {
- try {
- const playwrightEntry = require.resolve('playwright', { paths: [candidatePath] })
- return await import(pathToFileURL(playwrightEntry).href)
- } catch {
- // Continue probing npm exec / npx injected package roots.
- }
- }
- fail([
- 'Missing playwright module.',
- 'Run with: npm exec --yes --package playwright -- node scripts/verify-wrapper-browser-smoke.mjs',
- `Original error: ${error instanceof Error ? error.message : String(error)}`
- ].join('\n'))
- }
- }
- const startStaticServer = async () => {
- if (!existsSync(join(outputDir, 'manual-iife.html'))) {
- fail(`Missing wrapper demo build output: ${join(outputDir, 'manual-iife.html')}`)
- }
- const server = createServer((request, response) => {
- const requestUrl = new URL(request.url || '/', 'http://127.0.0.1')
- const decodedPath = decodeURIComponent(requestUrl.pathname)
- const normalizedPath = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, '')
- const relativePath = normalizedPath === '/' ? 'index.html' : normalizedPath.replace(/^[/\\]+/, '')
- const filePath = resolve(outputDir, relativePath)
- if (!filePath.startsWith(outputDir) || relative(outputDir, filePath).startsWith('..')) {
- response.writeHead(403)
- response.end('Forbidden')
- return
- }
- if (!existsSync(filePath) || !statSync(filePath).isFile()) {
- response.writeHead(404)
- response.end('Not Found')
- return
- }
- response.writeHead(200, {
- 'Content-Type': mimeTypes.get(extname(filePath).toLowerCase()) || 'application/octet-stream'
- })
- createReadStream(filePath).pipe(response)
- })
- await new Promise((resolveServer, rejectServer) => {
- server.once('error', rejectServer)
- server.listen(0, '127.0.0.1', resolveServer)
- })
- const address = server.address()
- if (!address || typeof address === 'string') {
- fail('Unable to resolve wrapper demo smoke server address.')
- }
- return {
- server,
- url: `http://127.0.0.1:${address.port}`
- }
- }
- const launchChromium = async chromium => {
- try {
- return await chromium.launch({ headless: true })
- } catch (error) {
- try {
- return await chromium.launch({ channel: 'chrome', headless: true })
- } catch {
- throw error
- }
- }
- }
- const assertNoBrowserFailures = (failures, context) => {
- if (failures.length) {
- fail([context, ...failures].join('\n'))
- }
- }
- const assertNativeViewerMounted = async (page, selector, label) => {
- await page.waitForSelector(selector, { timeout })
- let mounted
- try {
- mounted = await page.waitForFunction(
- targetSelector => {
- const host = document.querySelector(targetSelector)
- const viewerSelector = [
- '.file-viewer',
- '.file-render',
- '.docx-fit-viewer',
- '[class*="file-viewer"]',
- '[class*="file-render"]',
- '[data-viewer-zoom-provider]',
- '[data-rendered-document]'
- ].join(',')
- return Boolean(
- host &&
- (host.matches(viewerSelector) || host.querySelector(viewerSelector))
- )
- },
- selector,
- { timeout }
- )
- } catch (error) {
- const snapshot = await page.evaluate(targetSelector => {
- const host = document.querySelector(targetSelector)
- return {
- selector: targetSelector,
- title: document.title,
- bodyStatus: document.body.getAttribute('data-viewer-status') || '',
- hostExists: Boolean(host),
- hostClass: host?.getAttribute('class') || '',
- hostHtml: host?.outerHTML.slice(0, 800) || '',
- bodyText: document.body.innerText.slice(0, 800)
- }
- }, selector)
- fail([
- `${label} did not render a native viewer before timeout.`,
- JSON.stringify(snapshot, null, 2),
- error instanceof Error ? error.message : String(error)
- ].join('\n'))
- }
- if (!mounted) {
- fail(`${label} did not render a native viewer.`)
- }
- }
- const verifyWrapperIndexDemo = async (page, baseUrl, failures) => {
- await page.goto(`${baseUrl}/index.html`, {
- waitUntil: 'domcontentloaded',
- timeout
- })
- await page.waitForFunction(
- () => document.body.innerText.includes('React') && document.body.innerText.includes('Web'),
- { timeout }
- )
- await assertNativeViewerMounted(page, '[data-testid="react-viewer"]', 'React wrapper')
- await assertNativeViewerMounted(page, '[data-testid="web-viewer-host"]', 'Pure Web wrapper')
- assertNoBrowserFailures(failures, 'Wrapper index demo emitted browser errors.')
- failures.length = 0
- }
- const verifySingleWrapperDemo = async (page, baseUrl, config, failures) => {
- await page.goto(`${baseUrl}/${config.path}`, {
- waitUntil: 'domcontentloaded',
- timeout
- })
- await page.waitForFunction(
- wrapper => (
- document.body.getAttribute('data-component') ||
- document.body.getAttribute('data-wrapper')
- ) === wrapper,
- config.wrapper,
- { timeout }
- )
- await assertNativeViewerMounted(page, config.hostSelector, config.label)
- assertNoBrowserFailures(failures, `${config.label} demo emitted browser errors.`)
- failures.length = 0
- }
- const verifyIifeDemo = async (page, baseUrl, failures) => {
- await page.goto(`${baseUrl}/manual-iife.html`, {
- waitUntil: 'domcontentloaded',
- timeout
- })
- await page.waitForFunction(
- () => {
- const api = window.FlyfishFileViewerWeb
- return Boolean(
- api &&
- typeof api.mountViewer === 'function' &&
- typeof api.createViewerControllerHandle === 'function' &&
- typeof api.defineFileViewerElement === 'function' &&
- customElements.get('flyfish-file-viewer')
- )
- },
- { timeout }
- )
- await assertNativeViewerMounted(page, '#viewer', 'IIFE script tag demo')
- const status = await page.getAttribute('body', 'data-viewer-status')
- if (status === 'missing-global') {
- fail('IIFE global API was not available on window.')
- }
- assertNoBrowserFailures(failures, 'IIFE script tag demo emitted browser errors.')
- failures.length = 0
- }
- const verifyManualJsDemo = async (page, baseUrl, failures) => {
- await page.goto(`${baseUrl}/manual-js.html`, {
- waitUntil: 'domcontentloaded',
- timeout
- })
- await assertNativeViewerMounted(page, '#viewer', 'Manual JS demo')
- try {
- await page.waitForFunction(
- () => {
- const status = document.querySelector('#status')?.textContent?.trim()
- return Boolean(status && status !== 'loading')
- },
- { timeout }
- )
- } catch (error) {
- const snapshot = await page.evaluate(() => ({
- statusText: document.querySelector('#status')?.textContent?.trim() || '',
- bodyStatus: document.body.getAttribute('data-viewer-status') || '',
- hasGlobalApi: Boolean(window.FlyfishFileViewerWeb?.mountViewer),
- hostHtml: document.querySelector('#viewer')?.innerHTML.slice(0, 240) || ''
- }))
- fail([
- 'Manual JS native demo did not report a mounted status.',
- JSON.stringify(snapshot, null, 2),
- ...failures,
- error instanceof Error ? error.message : String(error)
- ].join('\n'))
- }
- assertNoBrowserFailures(failures, 'Manual JS native demo emitted browser errors.')
- failures.length = 0
- }
- const run = async () => {
- const playwrightModule = await importPlaywright()
- const { chromium } = playwrightModule.chromium ? playwrightModule : playwrightModule.default
- const serverHandle = externalUrl
- ? { server: null, url: externalUrl.replace(/\/$/, '') }
- : await startStaticServer()
- const browser = await launchChromium(chromium)
- const page = await browser.newPage()
- const failures = []
- page.on('pageerror', error => {
- failures.push(`pageerror: ${error.message}`)
- })
- page.on('console', message => {
- if (message.type() === 'error') {
- failures.push(`console.error: ${message.text()}`)
- }
- })
- page.on('response', response => {
- const responseUrl = new URL(response.url())
- if (responseUrl.origin === serverHandle.url && response.status() >= 400) {
- failures.push(`HTTP ${response.status()}: ${response.url()}`)
- }
- })
- try {
- await verifyWrapperIndexDemo(page, serverHandle.url, failures)
- await verifySingleWrapperDemo(page, serverHandle.url, {
- wrapper: 'jquery',
- hostSelector: '[data-testid="jquery-viewer-host"]',
- label: 'jQuery wrapper',
- path: 'jquery.html'
- }, failures)
- await verifySingleWrapperDemo(page, serverHandle.url, {
- wrapper: 'custom-element',
- hostSelector: '[data-testid="custom-element-viewer-host"]',
- label: 'Vanilla JS custom element wrapper',
- path: 'custom-element.html'
- }, failures)
- await verifySingleWrapperDemo(page, serverHandle.url, {
- wrapper: 'vue3',
- hostSelector: '[data-testid="vue3-viewer-host"]',
- label: 'Vue 3 wrapper',
- path: 'vue3.html'
- }, failures)
- await verifySingleWrapperDemo(page, serverHandle.url, {
- wrapper: 'svelte-action',
- hostSelector: '[data-testid="svelte-viewer-host"]',
- label: 'Svelte action wrapper',
- path: 'svelte-action.html'
- }, failures)
- await verifyManualJsDemo(page, serverHandle.url, failures)
- await verifyIifeDemo(page, serverHandle.url, failures)
- } finally {
- await browser.close()
- await new Promise(resolveClose => serverHandle.server?.close(resolveClose) ?? resolveClose())
- }
- console.log(`[wrapper-browser-smoke] Verified React, Web, Vanilla JS custom element, Vue 3, jQuery, Svelte action, manual JS, and script tag IIFE native demos at ${serverHandle.url}`)
- }
- run().catch(error => {
- const hint = String(error?.message || error)
- if (hint.includes('Executable doesn') || hint.includes('browserType.launch')) {
- console.error(
- [
- '[wrapper-browser-smoke] Playwright browser is not installed.',
- 'Run: npm exec --yes --package playwright -- playwright install chromium',
- `Original error: ${hint}`
- ].join('\n')
- )
- process.exit(1)
- }
- fail(error instanceof Error ? error.stack || error.message : String(error))
- })
|