core-registry.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. import { beforeEach, describe, expect, it } from 'vitest';
  2. import { parseHTML } from 'linkedom';
  3. import {
  4. DEFAULT_RENDERER_DEFINITIONS,
  5. DEFAULT_SUPPORTED_EXTENSIONS,
  6. clearFileViewerAutoRendererPresets,
  7. createFileViewerRendererDispatcher,
  8. createFileViewerUnsupportedState,
  9. createFileViewerZoomState,
  10. createRendererRegistry,
  11. createViewer,
  12. getExtension,
  13. normalizeSource,
  14. readFileViewerBuffer,
  15. registerFileViewerAutoRendererPreset,
  16. registerFileViewerZoomProvider,
  17. resolveFileViewerRendererInstallHint,
  18. unregisterFileViewerZoomProvider,
  19. type FileViewerZoomProvider,
  20. type RendererDefinition,
  21. wrapFileViewerFileRef,
  22. } from '../packages/core/src';
  23. import { allRenderers } from '../packages/presets/all/src';
  24. const setRect = (element: Element, rect: Partial<DOMRect>) => {
  25. Object.defineProperty(element, 'getBoundingClientRect', {
  26. configurable: true,
  27. value: () => ({
  28. bottom: rect.bottom ?? (rect.top || 0) + (rect.height || 0),
  29. height: rect.height ?? 0,
  30. left: rect.left ?? 0,
  31. right: rect.right ?? (rect.left || 0) + (rect.width || 0),
  32. top: rect.top ?? 0,
  33. width: rect.width ?? 0,
  34. x: rect.x ?? rect.left ?? 0,
  35. y: rect.y ?? rect.top ?? 0,
  36. toJSON: () => ({}),
  37. } as DOMRect),
  38. });
  39. };
  40. describe('@file-viewer/core registry', () => {
  41. beforeEach(() => {
  42. clearFileViewerAutoRendererPresets();
  43. });
  44. it('keeps the current public format matrix at 206 extensions', () => {
  45. expect(DEFAULT_SUPPORTED_EXTENSIONS).toHaveLength(206);
  46. expect(DEFAULT_SUPPORTED_EXTENSIONS).toContain('pdf');
  47. expect(DEFAULT_SUPPORTED_EXTENSIONS).toContain('docx');
  48. expect(DEFAULT_SUPPORTED_EXTENSIONS).toContain('dwf');
  49. expect(DEFAULT_SUPPORTED_EXTENSIONS).toContain('zip');
  50. expect(DEFAULT_SUPPORTED_EXTENSIONS).toContain('typst');
  51. });
  52. it('distinguishes missing renderer assembly from truly unsupported formats', () => {
  53. const pdfHint = resolveFileViewerRendererInstallHint('pdf');
  54. expect(pdfHint).toMatchObject({
  55. rendererId: 'pdf',
  56. rendererPackage: '@file-viewer/renderer-pdf',
  57. presetPackage: '@file-viewer/preset-office',
  58. vitePreset: 'office',
  59. });
  60. const missingPdf = createFileViewerUnsupportedState('pdf');
  61. expect(missingPdf.title).toBe('需要装配预览能力');
  62. expect(missingPdf.message).toContain('已在支持矩阵中');
  63. expect(missingPdf.description).toContain('@file-viewer/preset-office');
  64. expect(missingPdf.description).toContain('@file-viewer/renderer-pdf');
  65. const unsupported = createFileViewerUnsupportedState('unknown-format');
  66. expect(resolveFileViewerRendererInstallHint('unknown-format')).toBeNull();
  67. expect(unsupported.title).toBe('暂不支持在线预览');
  68. expect(unsupported.message).toContain('不支持.unknown-format格式');
  69. });
  70. it('normalizes file names, query strings and explicit type overrides', () => {
  71. expect(getExtension('/docs/report.PDF?token=1')).toBe('pdf');
  72. expect(normalizeSource({ url: '/example/PDF%20%E6%B2%89%E6%B5%B8.pdf?token=1' })).toMatchObject({
  73. kind: 'url',
  74. filename: 'PDF 沉浸.pdf',
  75. extension: 'pdf',
  76. });
  77. expect(normalizeSource({ url: 'https://example.com/a/b/合同.docx?download=1' })).toMatchObject({
  78. kind: 'url',
  79. filename: '合同.docx',
  80. extension: 'docx',
  81. });
  82. expect(normalizeSource({ buffer: new ArrayBuffer(8), filename: 'raw.bin', type: '.ofd' })).toMatchObject({
  83. kind: 'buffer',
  84. filename: 'raw.bin',
  85. extension: 'ofd',
  86. size: 8,
  87. });
  88. });
  89. it('wraps browser file inputs and reads ArrayBuffer data in core', async () => {
  90. const file = wrapFileViewerFileRef(new Blob(['hello'], { type: 'text/plain' }), '/tmp/%E6%96%87%E6%A1%A3.txt?download=1');
  91. expect(file.name).toBe('文档.txt');
  92. expect(file.type).toBe('text/plain');
  93. expect(await readFileViewerBuffer(file)).toEqual(new TextEncoder().encode('hello').buffer);
  94. const rawFile = wrapFileViewerFileRef(new Uint8Array([1, 2, 3]).buffer, 'raw.bin');
  95. expect(rawFile.name).toBe('raw.bin');
  96. expect(await readFileViewerBuffer(rawFile)).toEqual(new Uint8Array([1, 2, 3]).buffer);
  97. });
  98. it('resolves renderers by extension and rejects duplicate extension ownership', () => {
  99. const registry = createRendererRegistry();
  100. expect(registry.getByExtension('.PDF')?.id).toBe('pdf');
  101. expect(registry.getByExtension('dwfx')?.id).toBe('cad');
  102. expect(registry.getByExtension('zip')?.id).toBe('archive');
  103. expect(() => {
  104. registry.register({
  105. id: 'duplicate-pdf',
  106. label: 'Duplicate PDF',
  107. category: 'document',
  108. extensions: ['pdf'],
  109. });
  110. }).toThrow(/already registered/);
  111. });
  112. it('builds framework-neutral renderer dispatchers from renderer ids', () => {
  113. const registry = createRendererRegistry([
  114. {
  115. id: 'alpha',
  116. label: 'Alpha',
  117. category: 'document',
  118. extensions: ['Foo', '.bar'],
  119. },
  120. {
  121. id: 'missing',
  122. label: 'Missing',
  123. category: 'document',
  124. extensions: ['miss'],
  125. },
  126. ]);
  127. const alphaHandler = () => 'alpha';
  128. const fallbackHandler = () => 'fallback';
  129. const dispatcher = createFileViewerRendererDispatcher({
  130. registry,
  131. handlers: [
  132. {
  133. rendererId: 'alpha',
  134. handler: alphaHandler,
  135. },
  136. ],
  137. fallbackHandler,
  138. });
  139. expect(dispatcher.missingRendererIds).toEqual(['missing']);
  140. expect(dispatcher.get('foo')).toBe(alphaHandler);
  141. expect(dispatcher.get('.BAR')).toBe(alphaHandler);
  142. expect(dispatcher.resolve('miss')).toBe(fallbackHandler);
  143. expect(dispatcher.has('error')).toBe(true);
  144. expect(dispatcher.listExtensions()).toEqual(['bar', 'error', 'foo']);
  145. });
  146. it('runs a minimal framework-neutral load and unload lifecycle', async () => {
  147. const events: string[] = [];
  148. const renderer: RendererDefinition = {
  149. id: 'fixture',
  150. label: 'Fixture',
  151. category: 'document',
  152. extensions: ['fixture'],
  153. capabilities: { download: true, print: true, exportHtml: true, zoom: true, search: true },
  154. load: async ({ surface, source }) => {
  155. surface.container.dataset.loaded = source.filename;
  156. return {
  157. destroy: () => {
  158. surface.container.dataset.destroyed = 'true';
  159. },
  160. getAvailability: () => ({
  161. zoomIn: true,
  162. zoomOut: true,
  163. zoomReset: true,
  164. }),
  165. };
  166. },
  167. };
  168. const registry = createRendererRegistry([
  169. ...DEFAULT_RENDERER_DEFINITIONS,
  170. renderer,
  171. ]);
  172. const { document } = parseHTML('<main id="viewer"></main>');
  173. const container = document.getElementById('viewer') as HTMLElement;
  174. const viewer = createViewer(container, {
  175. registry,
  176. options: {
  177. hooks: {
  178. onLoadStart: context => events.push(`${context.phase}:${context.version}:${context.type}`),
  179. onLoadComplete: context => events.push(`${context.phase}:${context.version}:${context.filename}`),
  180. onUnloadStart: context => events.push(`${context.phase}:${context.version}:${context.reason}`),
  181. onUnloadComplete: context => events.push(`${context.phase}:${context.version}:${context.reason}`),
  182. },
  183. },
  184. });
  185. await viewer.load({ buffer: new ArrayBuffer(1), filename: 'demo.fixture' });
  186. expect(container.dataset.loaded).toBe('demo.fixture');
  187. expect(viewer.getCapabilities()).toMatchObject({
  188. download: true,
  189. print: true,
  190. exportHtml: true,
  191. zoom: true,
  192. zoomIn: true,
  193. zoomOut: true,
  194. zoomReset: true,
  195. });
  196. await viewer.destroy();
  197. expect(container.dataset.destroyed).toBe('true');
  198. expect(events).toEqual([
  199. 'load-start:1:fixture',
  200. 'load-complete:1:demo.fixture',
  201. 'unload-start:1:component-unmount',
  202. 'unload-complete:1:component-unmount',
  203. ]);
  204. });
  205. it('installs renderer plugins in replace mode for lightweight viewers', async () => {
  206. const { document } = parseHTML('<main id="viewer"></main>');
  207. const container = document.getElementById('viewer') as HTMLElement;
  208. const viewer = createViewer(container, {
  209. options: {
  210. rendererMode: 'replace',
  211. renderers: {
  212. id: 'fixture-plugin',
  213. definitions: [{
  214. id: 'fixture-plugin-renderer',
  215. label: 'Fixture Plugin',
  216. category: 'document',
  217. extensions: ['lite'],
  218. capabilities: { download: true, print: false, exportHtml: false, zoom: false, search: false },
  219. }],
  220. handlers: [{
  221. rendererId: 'fixture-plugin-renderer',
  222. handler: async (_buffer, target, type) => {
  223. target.dataset.pluginRenderer = type || '';
  224. return {
  225. unmount() {
  226. target.dataset.pluginDestroyed = 'true';
  227. },
  228. };
  229. },
  230. }],
  231. },
  232. },
  233. });
  234. await viewer.load({ buffer: new ArrayBuffer(1), filename: 'demo.lite' });
  235. expect(container.dataset.pluginRenderer).toBe('lite');
  236. expect(viewer.getRenderer('lite')?.id).toBe('fixture-plugin-renderer');
  237. expect(viewer.getRenderer('pdf')).toBeUndefined();
  238. await viewer.destroy();
  239. expect(container.dataset.pluginDestroyed).toBe('true');
  240. });
  241. it('supports lite built-in renderers without registering heavy document handlers', async () => {
  242. const { document } = parseHTML('<main id="viewer"></main>');
  243. const container = document.getElementById('viewer') as HTMLElement;
  244. const viewer = createViewer(container, {
  245. options: {
  246. builtinRenderers: 'lite',
  247. },
  248. });
  249. await viewer.load({ buffer: new ArrayBuffer(1), filename: 'demo.pdf' });
  250. expect(viewer.getRenderer('pdf')).toBeUndefined();
  251. expect(viewer.getRenderer('png')?.load).toBeTypeOf('function');
  252. const missingRendererText = container.querySelector('.file-viewer-missing-renderer')?.textContent || '';
  253. expect(missingRendererText).toContain('.pdf 格式已在支持矩阵中');
  254. expect(missingRendererText).toContain('@file-viewer/preset-office');
  255. });
  256. it('extends the bundled renderer matrix with renderer presets by default', async () => {
  257. const { document } = parseHTML('<main id="viewer"></main>');
  258. const container = document.getElementById('viewer') as HTMLElement;
  259. const viewer = createViewer(container, {
  260. options: {
  261. renderers: [{
  262. id: 'fixture-preset',
  263. label: 'Fixture preset',
  264. renderers: [{
  265. id: 'fixture-extend-plugin',
  266. definitions: [{
  267. id: 'fixture-extend-renderer',
  268. label: 'Fixture Extend',
  269. category: 'document',
  270. extensions: ['plus'],
  271. capabilities: { download: true, print: false, exportHtml: false, zoom: false, search: false },
  272. }],
  273. handlers: [{
  274. rendererId: 'fixture-extend-renderer',
  275. handler: async (_buffer, target, type) => {
  276. target.dataset.extendedRenderer = type || '';
  277. return { unmount() {} };
  278. },
  279. }],
  280. }],
  281. }],
  282. },
  283. });
  284. await viewer.load({ buffer: new ArrayBuffer(1), filename: 'demo.plus' });
  285. expect(container.dataset.extendedRenderer).toBe('plus');
  286. expect(viewer.getRenderer('plus')?.id).toBe('fixture-extend-renderer');
  287. expect(viewer.getRenderer('pdf')?.id).toBe('pdf');
  288. expect(viewer.getRenderer('pdf')?.load).toBeUndefined();
  289. });
  290. it('consumes auto-registered renderer presets while preserving strict manual control', async () => {
  291. registerFileViewerAutoRendererPreset({
  292. id: 'fixture-auto-preset',
  293. label: 'Fixture auto preset',
  294. renderers: [{
  295. id: 'fixture-auto-plugin',
  296. definitions: [{
  297. id: 'fixture-auto-renderer',
  298. label: 'Fixture Auto',
  299. category: 'document',
  300. extensions: ['auto'],
  301. capabilities: { download: true, print: false, exportHtml: false, zoom: false, search: false },
  302. }],
  303. handlers: [{
  304. rendererId: 'fixture-auto-renderer',
  305. handler: async (_buffer, target, type) => {
  306. target.dataset.autoRenderer = type || '';
  307. return { unmount() {} };
  308. },
  309. }],
  310. }],
  311. }, {
  312. id: 'fixture-auto',
  313. packageName: '@file-viewer/preset-fixture',
  314. });
  315. const { document } = parseHTML('<main id="auto"></main><main id="strict"></main>');
  316. const autoContainer = document.getElementById('auto') as HTMLElement;
  317. const autoViewer = createViewer(autoContainer, {
  318. options: {
  319. builtinRenderers: 'none',
  320. },
  321. });
  322. await autoViewer.load({ buffer: new ArrayBuffer(1), filename: 'demo.auto' });
  323. expect(autoContainer.dataset.autoRenderer).toBe('auto');
  324. expect(autoViewer.getRenderer('auto')?.id).toBe('fixture-auto-renderer');
  325. const strictContainer = document.getElementById('strict') as HTMLElement;
  326. const strictViewer = createViewer(strictContainer, {
  327. options: {
  328. builtinRenderers: 'none',
  329. autoRenderers: false,
  330. },
  331. });
  332. await strictViewer.load({ buffer: new ArrayBuffer(1), filename: 'demo.auto' });
  333. expect(strictViewer.getRenderer('auto')).toBeUndefined();
  334. expect(strictContainer.querySelector('.file-viewer-missing-renderer')?.textContent).toContain('不支持.auto格式');
  335. });
  336. it('can restore the full format matrix through @file-viewer/preset-all in replace mode', async () => {
  337. const { document } = parseHTML('<main id="viewer"></main>');
  338. const container = document.getElementById('viewer') as HTMLElement;
  339. const viewer = createViewer(container, {
  340. options: {
  341. rendererMode: 'replace',
  342. renderers: allRenderers,
  343. },
  344. });
  345. await viewer.load({ filename: 'empty' });
  346. expect(viewer.getRenderer('pdf')?.load).toBeTypeOf('function');
  347. expect(viewer.getRenderer('docx')?.load).toBeTypeOf('function');
  348. expect(viewer.getRenderer('dwg')?.load).toBeTypeOf('function');
  349. expect(viewer.getRenderer('zip')?.load).toBeTypeOf('function');
  350. expect(viewer.getRenderer('typst')?.load).toBeTypeOf('function');
  351. });
  352. it('exposes framework-neutral viewer interaction APIs', async () => {
  353. const { document } = parseHTML('<main id="viewer"></main>');
  354. const container = document.getElementById('viewer') as HTMLElement;
  355. const beforeOperations: string[] = [];
  356. let scale = 1;
  357. let scrolledToLine = 0;
  358. Object.defineProperty(container, 'clientHeight', { configurable: true, value: 120 });
  359. Object.defineProperty(container, 'scrollHeight', { configurable: true, value: 520 });
  360. Object.defineProperty(container, 'clientWidth', { configurable: true, value: 320 });
  361. Object.defineProperty(container, 'scrollWidth', { configurable: true, value: 320 });
  362. Object.defineProperty(container, 'scrollTop', { configurable: true, writable: true, value: 0 });
  363. Object.defineProperty(container, 'scrollLeft', { configurable: true, writable: true, value: 0 });
  364. container.scrollTo = (options?: ScrollToOptions | number, y?: number) => {
  365. if (typeof options === 'number') {
  366. container.scrollLeft = options;
  367. container.scrollTop = y || 0;
  368. return;
  369. }
  370. container.scrollTop = Number(options?.top || 0);
  371. container.scrollLeft = Number(options?.left || 0);
  372. };
  373. setRect(container, { top: 0, left: 0, width: 320, height: 120 });
  374. const renderer: RendererDefinition = {
  375. id: 'core-api-fixture',
  376. label: 'Core API Fixture',
  377. category: 'document',
  378. extensions: ['coreapi'],
  379. capabilities: { download: true, zoom: 'provider', search: true },
  380. load: async ({ surface }) => {
  381. surface.container.innerHTML = `
  382. <section id="zoom-host" data-viewer-zoom-provider="fixture">
  383. <article>
  384. <h1>Alpha PDF heading</h1>
  385. <p>Beta pdf line with enough content for text chunks.</p>
  386. </article>
  387. </section>
  388. `;
  389. const zoomHost = surface.container.querySelector('#zoom-host') as HTMLElement;
  390. const heading = surface.container.querySelector('h1') as HTMLElement;
  391. const paragraph = surface.container.querySelector('p') as HTMLElement;
  392. const getState = () => createFileViewerZoomState({
  393. scale,
  394. canZoomIn: scale < 2,
  395. canZoomOut: scale > 0.5,
  396. canReset: scale !== 1,
  397. });
  398. const zoomProvider: FileViewerZoomProvider = {
  399. zoomIn: () => {
  400. scale = 1.25;
  401. return getState();
  402. },
  403. zoomOut: () => {
  404. scale = 0.75;
  405. return getState();
  406. },
  407. resetZoom: () => {
  408. scale = 1;
  409. return getState();
  410. },
  411. getState,
  412. };
  413. registerFileViewerZoomProvider(zoomHost, zoomProvider);
  414. setRect(zoomHost, { top: 0, left: 0, width: 320, height: 160 });
  415. setRect(heading, { top: 12, left: 16, width: 260, height: 32 });
  416. setRect(paragraph, { top: 68, left: 16, width: 280, height: 24 });
  417. heading.scrollIntoView = () => {
  418. scrolledToLine = 1;
  419. };
  420. paragraph.scrollIntoView = () => {
  421. scrolledToLine = 2;
  422. };
  423. return {
  424. destroy: () => {
  425. unregisterFileViewerZoomProvider(zoomHost);
  426. },
  427. getAvailability: () => ({
  428. zoomIn: true,
  429. zoomOut: true,
  430. zoomReset: true,
  431. }),
  432. };
  433. },
  434. };
  435. const registry = createRendererRegistry([
  436. ...DEFAULT_RENDERER_DEFINITIONS,
  437. renderer,
  438. ]);
  439. const viewer = createViewer(container, {
  440. registry,
  441. options: {
  442. search: { maxMatches: 10 },
  443. ai: { chunkSize: 200 },
  444. beforeOperation: context => {
  445. beforeOperations.push(context.operation);
  446. return context.operation !== 'zoom-out';
  447. },
  448. },
  449. });
  450. await viewer.load({ buffer: new ArrayBuffer(4), filename: 'demo.coreapi' });
  451. await expect(viewer.zoomIn()).resolves.toMatchObject({ scale: 1.25, label: '125%' });
  452. await expect(viewer.zoomOut()).resolves.toMatchObject({ scale: 1.25 });
  453. expect(beforeOperations).toEqual(['zoom-in', 'zoom-out']);
  454. expect(viewer.getZoomState()).toMatchObject({ scale: 1.25, canReset: true });
  455. await expect(viewer.search('pdf')).resolves.toMatchObject({ query: 'pdf', total: 2, currentIndex: 0 });
  456. await expect(viewer.nextSearchResult()).resolves.toMatchObject({ currentIndex: 1 });
  457. expect(viewer.getSearchState().matches.map(match => match.text)).toEqual(['PDF', 'pdf']);
  458. const anchors = await viewer.collectDocumentAnchors();
  459. expect(anchors.map(anchor => anchor.label)).toEqual([
  460. 'Alpha PDF heading',
  461. 'Beta pdf line with enough content for text chunks.',
  462. ]);
  463. expect(viewer.getCurrentDocumentAnchor()?.line).toBe(1);
  464. await expect(viewer.scrollToLine(2)).resolves.toBe(true);
  465. expect(scrolledToLine).toBe(2);
  466. expect(viewer.getDocumentTextChunks()).toHaveLength(2);
  467. await viewer.destroy();
  468. });
  469. it('exposes framework-neutral viewer file operations', async () => {
  470. const { document } = parseHTML('<html><body><main id="viewer"></main></body></html>');
  471. const container = document.getElementById('viewer') as HTMLElement;
  472. const beforeOperations: string[] = [];
  473. let downloadedName = '';
  474. let revokedUrl = '';
  475. let printed = false;
  476. let printHtml = '';
  477. const originalDocument = globalThis.document;
  478. const originalWindow = globalThis.window;
  479. const originalCreateObjectUrl = URL.createObjectURL;
  480. const originalRevokeObjectUrl = URL.revokeObjectURL;
  481. const originalCreateElement = document.createElement.bind(document);
  482. document.createElement = ((tagName: string) => {
  483. const element = originalCreateElement(tagName);
  484. if (tagName.toLowerCase() === 'a') {
  485. Object.defineProperty(element, 'click', {
  486. configurable: true,
  487. value: () => {
  488. downloadedName = (element as HTMLAnchorElement).download;
  489. },
  490. });
  491. }
  492. return element;
  493. }) as Document['createElement'];
  494. Object.defineProperty(globalThis, 'document', { configurable: true, value: document });
  495. Object.defineProperty(globalThis, 'window', {
  496. configurable: true,
  497. value: {
  498. setTimeout: (callback: () => void) => {
  499. callback();
  500. return 1;
  501. },
  502. },
  503. });
  504. URL.createObjectURL = () => 'blob:viewer-test';
  505. URL.revokeObjectURL = url => {
  506. revokedUrl = url;
  507. };
  508. try {
  509. const renderer: RendererDefinition = {
  510. id: 'core-file-operation-fixture',
  511. label: 'Core File Operation Fixture',
  512. category: 'document',
  513. extensions: ['coreops'],
  514. capabilities: { download: true, print: 'adapter', exportHtml: 'adapter' },
  515. load: async ({ surface, registerExportAdapter }) => {
  516. surface.container.innerHTML = '<article><h1>Rendered preview</h1></article>';
  517. registerExportAdapter?.({
  518. exportHtml: true,
  519. print: true,
  520. includeDocumentStyles: false,
  521. printStyle: '@page { size: A4; }',
  522. toHtml: options => `<section data-mode="${options.mode}">Adapter ${options.title}</section>`,
  523. });
  524. return {};
  525. },
  526. };
  527. const viewer = createViewer(container, {
  528. registry: createRendererRegistry([
  529. ...DEFAULT_RENDERER_DEFINITIONS,
  530. renderer,
  531. ]),
  532. options: {
  533. watermark: { text: 'Confidential' },
  534. beforeOperation: context => {
  535. beforeOperations.push(context.operation);
  536. },
  537. },
  538. });
  539. await viewer.load({ buffer: new Uint8Array([1, 2, 3]).buffer, filename: 'demo.coreops' });
  540. expect(viewer.getExportAdapter()).toMatchObject({
  541. exportHtml: true,
  542. print: true,
  543. });
  544. const html = await viewer.exportHtml({ download: false });
  545. expect(html).toContain('data-mode="export"');
  546. expect(html).toContain('Adapter demo.coreops');
  547. expect(html).toContain('viewer-export-watermark');
  548. expect(html).toContain('Confidential');
  549. const printWindow = {
  550. document: {
  551. readyState: 'complete',
  552. images: [],
  553. open: () => undefined,
  554. write: (nextHtml: string) => {
  555. printHtml = nextHtml;
  556. },
  557. close: () => undefined,
  558. },
  559. focus: () => undefined,
  560. print: () => {
  561. printed = true;
  562. },
  563. requestAnimationFrame: (callback: () => void) => {
  564. callback();
  565. return 1;
  566. },
  567. setTimeout: (callback: () => void) => {
  568. callback();
  569. return 1;
  570. },
  571. } as unknown as Window;
  572. await viewer.print({ printWindow });
  573. expect(printed).toBe(true);
  574. expect(printHtml).toContain('data-mode="print"');
  575. expect(printHtml).toContain('@page { size: A4; }');
  576. await viewer.download();
  577. expect(downloadedName).toBe('demo.coreops');
  578. expect(revokedUrl).toBe('blob:viewer-test');
  579. expect(beforeOperations).toEqual(['export-html', 'print', 'download']);
  580. await viewer.destroy();
  581. expect(viewer.getExportAdapter()).toBeNull();
  582. } finally {
  583. document.createElement = originalCreateElement as Document['createElement'];
  584. URL.createObjectURL = originalCreateObjectUrl;
  585. URL.revokeObjectURL = originalRevokeObjectUrl;
  586. Object.defineProperty(globalThis, 'document', { configurable: true, value: originalDocument });
  587. Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow });
  588. }
  589. });
  590. });