|
| 1 | +import { readFileSync } from 'node:fs'; |
| 2 | +import { createServer, type Server } from 'node:http'; |
| 3 | +import type { AddressInfo } from 'node:net'; |
| 4 | +import { buildDocx, ooxmlText, zipEntryText } from './lib/ooxml'; |
| 5 | +import { expect, test } from './lib/l0'; |
| 6 | +import { settleEditor } from './lib/visual'; |
| 7 | + |
| 8 | +declare function post(type: string, payload?: Record<string, unknown>): Promise<any>; |
| 9 | + |
| 10 | +/** |
| 11 | + * The remaining ways a document reaches the editor (matrix section C): |
| 12 | + * - main site `?file=<url>` (also `?src=`), i.e. links from other pages; |
| 13 | + * - embed API `document:open-url`; |
| 14 | + * - static landing pages' `?open=local` hand-off through IndexedDB. |
| 15 | + * The document is served by a throwaway HTTP server on another origin so the |
| 16 | + * fetch is a real cross-origin request (the service worker ignores it, and |
| 17 | + * page.route is not involved -- see the corpus harness lesson). |
| 18 | + */ |
| 19 | +let server: Server; |
| 20 | +let docUrl = ''; |
| 21 | +const DOC = buildDocx('opened from a URL'); |
| 22 | + |
| 23 | +test.beforeAll(async () => { |
| 24 | + server = createServer((req, res) => { |
| 25 | + if (req.url?.startsWith('/url-open.docx')) { |
| 26 | + res.writeHead(200, { |
| 27 | + 'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
| 28 | + 'Access-Control-Allow-Origin': '*', |
| 29 | + }); |
| 30 | + res.end(Buffer.from(DOC)); |
| 31 | + return; |
| 32 | + } |
| 33 | + res.writeHead(404); |
| 34 | + res.end(); |
| 35 | + }); |
| 36 | + await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve)); |
| 37 | + docUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}/url-open.docx`; |
| 38 | +}); |
| 39 | +test.afterAll(async () => { |
| 40 | + await new Promise<void>((resolve) => server.close(() => resolve())); |
| 41 | +}); |
| 42 | + |
| 43 | +test.describe('entry paths (real editor)', () => { |
| 44 | + test.describe.configure({ timeout: 150_000 }); |
| 45 | + |
| 46 | + test.beforeEach(async ({ page }) => { |
| 47 | + await page.addInitScript(() => { |
| 48 | + Object.defineProperty(window, 'showSaveFilePicker', { value: undefined, configurable: true }); |
| 49 | + }); |
| 50 | + }); |
| 51 | + |
| 52 | + test('main site ?file=<url> opens the document and Ctrl+S saves it under its URL name', async ({ page }) => { |
| 53 | + await page.goto(`/?file=${encodeURIComponent(docUrl)}`); |
| 54 | + await settleEditor(page); |
| 55 | + const sdk = page.frameLocator('iframe[name="frameEditor"]').locator('#editor_sdk'); |
| 56 | + await sdk.waitFor({ state: 'visible', timeout: 30_000 }); |
| 57 | + await sdk.click({ position: { x: 300, y: 200 } }); |
| 58 | + const downloadPromise = page.waitForEvent('download', { timeout: 60_000 }); |
| 59 | + await page.keyboard.press('Control+s'); |
| 60 | + const download = await downloadPromise; |
| 61 | + expect(download.suggestedFilename()).toBe('url-open.docx'); |
| 62 | + const bytes = new Uint8Array(readFileSync(await download.path())); |
| 63 | + expect(ooxmlText(zipEntryText(bytes, 'word/document.xml') || '')).toContain('opened from a URL'); |
| 64 | + }); |
| 65 | + |
| 66 | + test('embed document:open-url fetches, opens and round-trips the document', async ({ page }) => { |
| 67 | + await page.goto('/embed-demo.html'); |
| 68 | + await expect(page.locator('#status')).toHaveText('ready', { timeout: 60_000 }); |
| 69 | + const result = await page.evaluate(async (url) => { |
| 70 | + const opened = await post('document:open-url', { url, readonly: false }); |
| 71 | + const saved = await post('document:save', {}); |
| 72 | + const out = new Uint8Array(await saved.file.arrayBuffer()); |
| 73 | + return { opened, name: saved.file.name as string, magic: Array.from(out.slice(0, 2)) }; |
| 74 | + }, docUrl); |
| 75 | + expect(result.opened.readonly).toBe(false); |
| 76 | + expect(result.name).toBe('url-open.docx'); |
| 77 | + expect(result.magic).toEqual([0x50, 0x4b]); |
| 78 | + }); |
| 79 | + |
| 80 | + test('?open=local consumes the file a static landing page stashed in IndexedDB', async ({ page }) => { |
| 81 | + // Same DB/store/key names as public/open-local.js and lib/pending-open.ts. |
| 82 | + await page.goto('/zh-CN/'); |
| 83 | + await page.evaluate(async (b64) => { |
| 84 | + const bin = atob(b64); |
| 85 | + const bytes = new Uint8Array(bin.length); |
| 86 | + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); |
| 87 | + const file = new File([bytes], '落地页交接.docx', { |
| 88 | + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
| 89 | + }); |
| 90 | + await new Promise<void>((resolve, reject) => { |
| 91 | + const req = indexedDB.open('document-handoff', 1); |
| 92 | + req.onupgradeneeded = () => req.result.createObjectStore('files'); |
| 93 | + req.onerror = () => reject(req.error); |
| 94 | + req.onsuccess = () => { |
| 95 | + const db = req.result; |
| 96 | + const tx = db.transaction('files', 'readwrite'); |
| 97 | + tx.objectStore('files').put(file, 'pending'); |
| 98 | + tx.oncomplete = () => { |
| 99 | + db.close(); |
| 100 | + resolve(); |
| 101 | + }; |
| 102 | + tx.onerror = () => reject(tx.error); |
| 103 | + }; |
| 104 | + }); |
| 105 | + }, Buffer.from(DOC).toString('base64')); |
| 106 | + await page.goto('/?locale=zh-CN&open=local'); |
| 107 | + await settleEditor(page); |
| 108 | + const sdk = page.frameLocator('iframe[name="frameEditor"]').locator('#editor_sdk'); |
| 109 | + await sdk.waitFor({ state: 'visible', timeout: 30_000 }); |
| 110 | + await sdk.click({ position: { x: 300, y: 200 } }); |
| 111 | + const downloadPromise = page.waitForEvent('download', { timeout: 60_000 }); |
| 112 | + await page.keyboard.press('Control+s'); |
| 113 | + expect((await downloadPromise).suggestedFilename()).toBe('落地页交接.docx'); |
| 114 | + }); |
| 115 | +}); |
0 commit comments