| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602 |
- import { describe, expect, it, vi } from 'vitest';
- import { parseHTML } from 'linkedom';
- import {
- FILE_VIEWER_BEFORE_OPERATION_ERROR_PREFIX,
- DEFAULT_FILE_VIEWER_DOWNLOAD_FILENAME,
- DEFAULT_FILE_VIEWER_EXPORT_FILENAME,
- DEFAULT_FILE_VIEWER_PREVIEW_TITLE,
- buildFileViewerLifecycleContext,
- buildFileViewerLifecycleContextFromNormalizedSource,
- buildFileViewerOperationContext,
- buildFileViewerOperationContextFromLifecycleState,
- cloneFileViewerOperationAvailability,
- createFileViewerLifecycleActions,
- createFileViewerLifecycleFacade,
- createFileViewerPublicApi,
- createFileViewerToolbarActions,
- createFileViewerToolbarControllerActionHandlers,
- createFileViewerToolbarZoomSyncSnapshot,
- createFileViewerOperationActionHandlers,
- createFileViewerLifecycleStateController,
- createFileViewerOriginalSourceState,
- createFileViewerOriginalSourceStateFromNormalizedSource,
- createFileViewerPublicOperationActionHandlers,
- dispatchFileViewerLifecycleEvent,
- dispatchFileViewerOperationContextEvent,
- dispatchFileViewerOperationAvailabilityChange,
- dispatchFileViewerZoomChange,
- emitFileViewerComponentLifecycleEvent,
- executeFileViewerDownloadOperation,
- executeFileViewerExportHtmlOperation,
- executeFileViewerPrintOperation,
- FILE_VIEWER_LIFECYCLE_HOOK_ERROR_MESSAGE_PREFIX,
- hasVisibleFileViewerToolbarActions,
- isFileViewerToolbarOperationPermitted,
- isFileViewerZoomButtonDisabled,
- normalizeFileViewerToolbar,
- reportFileViewerLifecycleHookError,
- reportFileViewerOperationError,
- resolveFileViewerBeforeOperationErrorMessage,
- resolveFileViewerLifecycleFallbackSource,
- resolveFileViewerLifecycleHookErrorMessage,
- resolveFileViewerOperationActionErrorMessage,
- resolveFileViewerOperationFilename,
- resolveFileViewerOriginalFilename,
- resolveFileViewerOperationAvailability,
- resolveFileViewerToolbarState,
- resolveFileViewerToolbarPosition,
- resolveFileViewerDisplayFilename,
- resolveVisibleFileViewerToolbar,
- runFileViewerActiveUnloadComplete,
- runFileViewerActiveUnloadStart,
- runFileViewerBeforeOperation,
- runFileViewerLifecycleHook,
- runFileViewerToolbarAvailabilitySync,
- runFileViewerToolbarZoomSync,
- serializeFileViewerContext,
- type FileViewerLifecycleComponentEmit,
- type FileViewerOperationType,
- } from '../packages/core/src';
- describe('@file-viewer/core operation helpers', () => {
- it('builds lifecycle and operation contexts without framework state', () => {
- const context = buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'url',
- url: '/docs/%E6%8A%A5%E5%91%8A.pdf?token=1',
- version: 7,
- startedAt: 1000,
- timestamp: 1240,
- bufferSize: 4096,
- });
- expect(context).toMatchObject({
- phase: 'load-complete',
- type: 'pdf',
- filename: '报告.pdf',
- source: 'url',
- url: '/docs/%E6%8A%A5%E5%91%8A.pdf?token=1',
- size: 4096,
- version: 7,
- timestamp: 1240,
- duration: 240,
- });
- expect(buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'url',
- url: '/docs/zero.pdf',
- version: 7,
- startedAt: 0,
- timestamp: 240,
- }).duration).toBe(240);
- expect(buildFileViewerOperationContext('print', context, 1300)).toMatchObject({
- operation: 'print',
- label: '打印完整渲染内容',
- filename: '报告.pdf',
- timestamp: 1300,
- });
- expect(buildFileViewerLifecycleContextFromNormalizedSource({
- phase: 'load-complete',
- source: {
- kind: 'url',
- filename: '计划.pdf',
- extension: 'pdf',
- url: '/docs/%E8%AE%A1%E5%88%92.pdf',
- size: 2048,
- },
- version: 8,
- startedAt: 0,
- timestamp: 320,
- })).toMatchObject({
- phase: 'load-complete',
- type: 'pdf',
- filename: '计划.pdf',
- source: 'url',
- url: '/docs/%E8%AE%A1%E5%88%92.pdf',
- size: 2048,
- version: 8,
- timestamp: 320,
- duration: 320,
- });
- });
- it('tracks lifecycle state in the framework-neutral core controller', () => {
- const controller = createFileViewerLifecycleStateController();
- const context = buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'file',
- filename: 'demo.pdf',
- version: 3,
- timestamp: 120,
- });
- controller.markLoadStarted(3, 100);
- expect(controller.getLoadStartedAt(3)).toBe(100);
- expect(controller.getActiveDocumentContext()).toBeNull();
- controller.setActiveDocumentContext(context);
- expect(controller.getActiveDocumentContext()).toBe(context);
- expect(controller.buildActiveUnloadContext('unload-start', context, 'replace', 130)).toMatchObject({
- phase: 'unload-start',
- filename: 'demo.pdf',
- reason: 'replace',
- timestamp: 130,
- });
- expect(controller.buildActiveUnloadContext('unload-complete', context, 'replace', 140)).toMatchObject({
- phase: 'unload-complete',
- filename: 'demo.pdf',
- reason: 'replace',
- timestamp: 140,
- });
- controller.clearLoadStarted(3);
- controller.clearActiveDocumentContext();
- expect(controller.getLoadStartedAt(3)).toBeUndefined();
- expect(controller.getActiveDocumentContext()).toBeNull();
- expect(controller.buildActiveUnloadContext('unload-start', null)).toBeNull();
- });
- it('runs active unload lifecycle orchestration without framework code', () => {
- const controller = createFileViewerLifecycleStateController();
- const onLifecycle = vi.fn();
- const context = buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'url',
- filename: 'active.docx',
- url: '/active.docx',
- version: 9,
- timestamp: 500,
- });
- expect(runFileViewerActiveUnloadStart({
- lifecycleState: controller,
- onLifecycle,
- })).toEqual({
- reason: 'replace',
- context: null,
- unloadContext: null,
- });
- expect(onLifecycle).not.toHaveBeenCalled();
- controller.setActiveDocumentContext(context);
- const started = runFileViewerActiveUnloadStart({
- lifecycleState: controller,
- reason: 'replace',
- onLifecycle,
- });
- expect(started.context).toBe(context);
- expect(started.unloadContext).toMatchObject({
- phase: 'unload-start',
- filename: 'active.docx',
- reason: 'replace',
- version: 9,
- });
- expect(onLifecycle).toHaveBeenCalledTimes(1);
- expect(onLifecycle).toHaveBeenLastCalledWith(started.unloadContext);
- const completed = runFileViewerActiveUnloadComplete({
- lifecycleState: controller,
- context: started.context,
- reason: 'component-unmount',
- onLifecycle,
- });
- expect(completed.context).toBe(context);
- expect(completed.unloadContext).toMatchObject({
- phase: 'unload-complete',
- filename: 'active.docx',
- reason: 'component-unmount',
- version: 9,
- });
- expect(onLifecycle).toHaveBeenCalledTimes(2);
- expect(onLifecycle).toHaveBeenLastCalledWith(completed.unloadContext);
- });
- it('dispatches component lifecycle emits and formats beforeOperation errors through core', () => {
- const emitted: string[] = [];
- const emit = ((event: string, context: { filename: string }) => {
- emitted.push(`${event}:${context.filename}`);
- }) as FileViewerLifecycleComponentEmit;
- const context = buildFileViewerLifecycleContext({
- phase: 'load-start',
- source: 'url',
- filename: 'contract.pdf',
- version: 11,
- });
- emitFileViewerComponentLifecycleEvent(emit, context);
- expect(emitted).toEqual(['load-start:contract.pdf']);
- expect(resolveFileViewerBeforeOperationErrorMessage({
- error: new Error('denied'),
- formatErrorMessage: (prefix, error) => `${prefix}:${error instanceof Error ? error.message : String(error)}`,
- })).toBe(`${FILE_VIEWER_BEFORE_OPERATION_ERROR_PREFIX}:denied`);
- expect(resolveFileViewerBeforeOperationErrorMessage({
- error: 'offline',
- prefix: '权限校验失败',
- formatErrorMessage: (prefix, error) => `${prefix}:${String(error)}`,
- })).toBe('权限校验失败:offline');
- const hookError = new Error('hook failed');
- const lifecycleLogs: Array<[string, unknown, string]> = [];
- expect(FILE_VIEWER_LIFECYCLE_HOOK_ERROR_MESSAGE_PREFIX).toBe('FileViewer');
- expect(resolveFileViewerLifecycleHookErrorMessage({ context })).toBe('FileViewer load-start hook failed');
- expect(resolveFileViewerLifecycleHookErrorMessage({
- context,
- prefix: 'Viewer',
- })).toBe('Viewer load-start hook failed');
- expect(reportFileViewerLifecycleHookError({
- error: hookError,
- context,
- onLogError: (message, error, nextContext) => {
- lifecycleLogs.push([message, error, nextContext.phase]);
- },
- })).toBe('FileViewer load-start hook failed');
- expect(lifecycleLogs).toEqual([['FileViewer load-start hook failed', hookError, 'load-start']]);
- const operationContext = buildFileViewerOperationContext('download', context, 220);
- const operationLogs: Array<[unknown, string]> = [];
- expect(reportFileViewerOperationError({
- error: hookError,
- context: operationContext,
- onLogError: (error, nextContext) => {
- operationLogs.push([error, nextContext.operation]);
- },
- })).toBe(hookError);
- expect(operationLogs).toEqual([[hookError, 'download']]);
- });
- it('builds operation contexts from lifecycle state without wrapper fallback logic', () => {
- const controller = createFileViewerLifecycleStateController();
- const activeContext = buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'file',
- filename: 'active.pdf',
- version: 5,
- timestamp: 320,
- });
- controller.setActiveDocumentContext(activeContext);
- expect(buildFileViewerOperationContextFromLifecycleState({
- operation: 'download',
- lifecycleState: controller,
- version: 5,
- fallbackUrl: '/ignored.docx',
- timestamp: 360,
- })).toMatchObject({
- operation: 'download',
- source: 'file',
- filename: 'active.pdf',
- version: 5,
- timestamp: 360,
- });
- controller.clearActiveDocumentContext();
- controller.markLoadStarted(8, 100);
- expect(buildFileViewerOperationContextFromLifecycleState({
- operation: 'print',
- lifecycleState: controller,
- version: 8,
- filename: 'fallback.docx',
- bufferSize: 512,
- fallbackUrl: '/docs/fallback.docx',
- lifecycleTimestamp: 150,
- timestamp: 170,
- })).toMatchObject({
- operation: 'print',
- label: '打印完整渲染内容',
- type: 'docx',
- filename: 'fallback.docx',
- source: 'url',
- url: '/docs/fallback.docx',
- size: 512,
- version: 8,
- timestamp: 170,
- duration: 50,
- });
- });
- it('resolves lifecycle fallback sources without wrapper-specific branching', () => {
- expect(resolveFileViewerLifecycleFallbackSource({
- file: new File(['demo'], 'demo.txt'),
- url: '/ignored.txt',
- })).toEqual({ source: 'file' });
- expect(resolveFileViewerLifecycleFallbackSource({
- url: '/example/demo.pdf',
- })).toEqual({ source: 'url', sourceUrl: '/example/demo.pdf' });
- expect(resolveFileViewerLifecycleFallbackSource()).toEqual({ source: 'empty' });
- });
- it('serializes contexts without leaking File objects', () => {
- const file = new File(['demo'], 'demo.docx');
- const context = buildFileViewerLifecycleContext({
- phase: 'load-start',
- source: 'file',
- file,
- version: 1,
- timestamp: 10,
- });
- expect(serializeFileViewerContext(context)).toEqual({
- phase: 'load-start',
- type: 'docx',
- filename: 'demo.docx',
- source: 'file',
- url: undefined,
- size: 4,
- version: 1,
- timestamp: 10,
- duration: undefined,
- reason: undefined,
- hasFile: true,
- });
- });
- it('dispatches lifecycle and operation context events in wrapper event order', () => {
- const events: string[] = [];
- const lifecycleContext = buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'url',
- filename: 'dispatch.pdf',
- url: '/docs/dispatch.pdf',
- version: 3,
- timestamp: 300,
- });
- const operationContext = buildFileViewerOperationContext('print', lifecycleContext, 320);
- expect(dispatchFileViewerLifecycleEvent({
- context: lifecycleContext,
- hooks: {
- onLoadComplete: context => {
- events.push(`hook:${context.phase}`);
- },
- },
- onChange: (event, context) => {
- events.push(`emit:${event}:${context.filename}`);
- },
- })).toBe(true);
- expect(dispatchFileViewerOperationContextEvent({
- event: 'operation-before',
- context: operationContext,
- onChange: context => {
- events.push(`emit:${context.operation}`);
- },
- })).toBe(true);
- expect(events).toEqual([
- 'emit:load-complete:dispatch.pdf',
- 'hook:load-complete',
- 'emit:print',
- ]);
- });
- it('creates lifecycle actions that reuse dispatch, unload and operation guards', async () => {
- const events: string[] = [];
- const lifecycleState = createFileViewerLifecycleStateController();
- const context = buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'file',
- filename: 'actions.pdf',
- version: 8,
- timestamp: 400,
- });
- lifecycleState.setActiveDocumentContext(context);
- const actions = createFileViewerLifecycleActions({
- lifecycleState,
- getOptions: () => ({
- hooks: {
- onUnloadStart: nextContext => {
- events.push(`hook:${nextContext.phase}`);
- },
- },
- beforeOperation: nextContext => {
- events.push(`guard:${nextContext.operation}`);
- return false;
- },
- }),
- onLifecycleChange: (event, nextContext) => {
- events.push(`emit:${event}:${nextContext.filename}`);
- },
- onOperationBefore: nextContext => {
- events.push(`before:${nextContext.operation}`);
- },
- onOperationCancel: nextContext => {
- events.push(`cancel:${nextContext.operation}`);
- },
- });
- const unloadStartContext = actions.notifyActiveUnloadStart('replace');
- expect(unloadStartContext).toMatchObject({
- phase: 'load-complete',
- reason: undefined,
- filename: 'actions.pdf',
- });
- actions.notifyActiveUnloadComplete(unloadStartContext, 'replace');
- await expect(actions.runBeforeOperation(
- buildFileViewerOperationContext('download', context, 430)
- )).resolves.toBe(false);
- expect(events).toEqual([
- 'emit:unload-start:actions.pdf',
- 'hook:unload-start',
- 'emit:unload-complete:actions.pdf',
- 'before:download',
- 'guard:download',
- 'cancel:download',
- ]);
- });
- it('creates lifecycle facades from wrapper state getters', async () => {
- const events: string[] = [];
- const errors: string[] = [];
- const lifecycle = createFileViewerLifecycleFacade({
- getOptions: () => ({
- beforeOperation: context => {
- events.push(`guard:${context.operation}:${context.filename}`);
- return false;
- },
- }),
- getFilename: () => 'facade.pdf',
- getBufferSize: () => 2048,
- getCurrentFile: () => null,
- getCurrentVersion: () => 12,
- getFallbackFile: () => undefined,
- getFallbackUrl: () => '/fallback.pdf',
- emitLifecycle: (event, context) => {
- events.push(`emit:${event}:${context.filename}`);
- },
- emitOperationBefore: context => {
- events.push(`before:${context.operation}:${context.source}`);
- },
- emitOperationCancel: context => {
- events.push(`cancel:${context.operation}`);
- },
- formatErrorMessage: (prefix, error) => `${prefix}:${error instanceof Error ? error.message : String(error)}`,
- handleLifecycleError: error => {
- errors.push(String(error));
- },
- onOperationErrorMessage: message => {
- errors.push(message);
- },
- });
- lifecycle.markLoadStarted(12, 100);
- const loadStart = lifecycle.buildLoadStartState({
- version: 12,
- source: 'url',
- sourceUrl: '/facade.pdf',
- });
- expect(loadStart.lifecycleContext).toMatchObject({
- phase: 'load-start',
- source: 'url',
- filename: 'facade.pdf',
- size: 2048,
- url: '/facade.pdf',
- });
- lifecycle.notifyLifecycle(loadStart.lifecycleContext);
- const complete = lifecycle.buildRenderCompleteState({
- version: 12,
- source: 'url',
- sourceUrl: '/facade.pdf',
- });
- expect(complete.lifecycleContext).toMatchObject({
- phase: 'load-complete',
- filename: 'facade.pdf',
- });
- lifecycle.setActiveDocumentContext(complete.lifecycleContext);
- expect(lifecycle.buildOperationContext('download')).toMatchObject({
- operation: 'download',
- label: '下载原始文件',
- filename: 'facade.pdf',
- source: 'url',
- size: 2048,
- });
- await expect(lifecycle.runBeforeOperation('download')).resolves.toBe(false);
- lifecycle.clearLoadStarted(12);
- lifecycle.clearActiveDocumentContext();
- expect(lifecycle.buildOperationContext('print')).toMatchObject({
- operation: 'print',
- source: 'url',
- url: '/fallback.pdf',
- filename: 'facade.pdf',
- });
- expect(events).toEqual([
- 'emit:load-start:facade.pdf',
- 'before:download:url',
- 'guard:download:facade.pdf',
- 'cancel:download',
- ]);
- expect(errors).toEqual([]);
- });
- it('dispatches toolbar change notifications in wrapper event order', () => {
- const events: string[] = [];
- const availability = {
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- zoomIn: true,
- zoomOut: false,
- zoomReset: false,
- };
- const zoomState = {
- scale: 1.5,
- label: '150%',
- canZoomIn: true,
- canZoomOut: true,
- canReset: true,
- };
- let emittedAvailability: typeof availability | null = null;
- let emittedZoomState: typeof zoomState | null = null;
- expect(dispatchFileViewerOperationAvailabilityChange({
- availability,
- onChange: payload => {
- events.push('emit:operation-availability-change');
- emittedAvailability = payload;
- payload.download = false;
- },
- })).toBe(true);
- expect(dispatchFileViewerZoomChange({
- state: zoomState,
- onChange: payload => {
- events.push('emit:zoom-change');
- emittedZoomState = payload;
- },
- })).toBe(true);
- expect(events).toEqual([
- 'emit:operation-availability-change',
- 'emit:zoom-change',
- ]);
- expect(emittedAvailability).toEqual({
- ...availability,
- download: false,
- });
- expect(emittedAvailability).not.toBe(availability);
- expect(availability.download).toBe(true);
- expect(emittedZoomState).toBe(zoomState);
- });
- it('creates toolbar actions that reuse notification dispatch and zoom guard rules', () => {
- const events: string[] = [];
- const availability = {
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- zoomIn: true,
- zoomOut: false,
- zoomReset: true,
- };
- let toolbarDisabled = false;
- const zoomState = {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: true,
- };
- const actions = createFileViewerToolbarActions({
- getOperationAvailability: () => availability,
- getToolbarDisabled: () => toolbarDisabled,
- getZoomState: () => zoomState,
- onOperationAvailabilityChange: payload => {
- events.push(`emit:availability:${payload.zoomOut}`);
- },
- onZoomChange: payload => {
- events.push(`emit:zoom:${payload.label}`);
- },
- });
- expect(actions.isZoomButtonDisabled('canZoomIn')).toBe(false);
- expect(actions.isZoomButtonDisabled('canZoomOut')).toBe(true);
- toolbarDisabled = true;
- expect(actions.isZoomButtonDisabled('canZoomIn')).toBe(true);
- expect(actions.notifyOperationAvailabilityChange()).toBe(true);
- expect(actions.notifyZoomChange()).toBe(true);
- expect(events).toEqual([
- 'emit:availability:false',
- 'emit:zoom:100%',
- ]);
- });
- it('runs toolbar sync notifications and keeps zoom sync snapshot fields stable', () => {
- const events: string[] = [];
- const availability = {
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- zoomIn: true,
- zoomOut: true,
- zoomReset: false,
- };
- const zoomState = {
- scale: 1.25,
- label: '125%',
- canZoomIn: true,
- canZoomOut: true,
- canReset: true,
- minScale: 0.25,
- maxScale: 4,
- };
- const toolbarActions = {
- notifyOperationAvailabilityChange(nextAvailability = availability) {
- events.push(`availability:${nextAvailability.zoomReset}`);
- return true;
- },
- notifyZoomChange(nextState = zoomState) {
- events.push(`zoom:${nextState.label}`);
- return true;
- },
- };
- expect(createFileViewerToolbarZoomSyncSnapshot(zoomState)).toEqual([
- 1.25,
- '125%',
- true,
- true,
- true,
- ]);
- expect(runFileViewerToolbarAvailabilitySync({
- toolbarActions,
- availability,
- })).toBe(true);
- expect(runFileViewerToolbarZoomSync({
- toolbarActions,
- state: zoomState,
- })).toBe(true);
- expect(events).toEqual([
- 'availability:false',
- 'zoom:125%',
- ]);
- });
- it('creates reusable toolbar controller action handlers for framework wrappers', () => {
- const events: string[] = [];
- const zoomState = {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: true,
- minScale: 0.25,
- maxScale: 4,
- };
- let buffer: ArrayBuffer | null = new ArrayBuffer(8);
- let hasError = false;
- let loading = false;
- const controller = createFileViewerToolbarControllerActionHandlers({
- getAdapter: () => ({ exportHtml: true }),
- getBuffer: () => buffer,
- getExtension: () => 'pdf',
- getFile: () => null,
- getHasError: () => hasError,
- getLoading: () => loading,
- getOptions: () => undefined,
- getSourceUrl: () => null,
- getToolbar: () => ({
- download: true,
- print: true,
- exportHtml: true,
- zoom: true,
- }),
- getRenderedReady: () => true,
- getZoomState: () => zoomState,
- zoomSyncState: zoomState,
- onOperationAvailabilityChange: availability => {
- events.push(`emit:availability:${availability.download}:${availability.zoomOut}`);
- },
- onZoomChange: state => {
- events.push(`emit:zoom:${state.label}`);
- },
- });
- const state = controller.resolveToolbarState();
- expect(state.operationAvailability).toMatchObject({
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- zoomIn: true,
- zoomOut: false,
- zoomReset: true,
- });
- expect(state.visibleToolbar).toEqual({
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- });
- expect(state.toolbarPosition).toBe('bottom-right');
- expect(state.toolbarDisabled).toBe(false);
- expect(controller.createZoomSyncSnapshot()).toEqual([
- 1,
- '100%',
- true,
- false,
- true,
- ]);
- expect(controller.isZoomButtonDisabled('canZoomIn')).toBe(false);
- expect(controller.isZoomButtonDisabled('canZoomOut')).toBe(true);
- expect(controller.syncOperationAvailability()).toBe(true);
- expect(controller.syncZoomChange()).toBe(true);
- expect(events).toEqual([
- 'emit:availability:true:false',
- 'emit:zoom:100%',
- ]);
- buffer = null;
- hasError = true;
- loading = true;
- const disabledState = controller.resolveToolbarState();
- expect(disabledState.operationAvailability.download).toBe(false);
- expect(disabledState.operationAvailability.exportHtml).toBe(false);
- expect(disabledState.toolbarDisabled).toBe(true);
- expect(controller.isZoomButtonDisabled('canZoomIn')).toBe(true);
- });
- it('runs lifecycle hooks and operation guards in deterministic order', async () => {
- const events: string[] = [];
- const context = buildFileViewerLifecycleContext({
- phase: 'load-start',
- source: 'file',
- filename: 'guard.pdf',
- version: 1,
- timestamp: 10,
- });
- const operationContext = buildFileViewerOperationContext('download', context, 12);
- await runFileViewerLifecycleHook(context, {
- onLoadStart: nextContext => events.push(`lifecycle:${nextContext.phase}`),
- });
- const allowed = await runFileViewerBeforeOperation({
- context: operationContext,
- options: {
- beforeOperation: nextContext => {
- events.push(`global:${nextContext.operation}`);
- },
- toolbar: {
- beforeOperation: nextContext => {
- events.push(`toolbar:${nextContext.operation}`);
- },
- beforeDownload: () => {
- events.push('download:false');
- return false;
- },
- },
- },
- onBefore: nextContext => events.push(`before:${nextContext.operation}`),
- onCancel: nextContext => events.push(`cancel:${nextContext.operation}`),
- });
- expect(allowed).toBe(false);
- expect(events).toEqual([
- 'lifecycle:load-start',
- 'before:download',
- 'global:download',
- 'toolbar:download',
- 'download:false',
- 'cancel:download',
- ]);
- });
- it('blocks public operations through toolbar permission maps before custom hooks run', async () => {
- const events: string[] = [];
- const context = buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'url',
- url: '/secure.pdf',
- version: 1,
- timestamp: 10,
- });
- const operationContext = buildFileViewerOperationContext('print', context, 12);
- const allowed = await runFileViewerBeforeOperation({
- context: operationContext,
- options: {
- beforeOperation: nextContext => {
- events.push(`global:${nextContext.operation}`);
- },
- toolbar: {
- permissions: {
- print: false,
- },
- },
- },
- onBefore: nextContext => events.push(`before:${nextContext.operation}`),
- onCancel: nextContext => events.push(`cancel:${nextContext.operation}`),
- });
- expect(allowed).toBe(false);
- expect(events).toEqual([
- 'before:print',
- 'cancel:print',
- ]);
- });
- it('normalizes toolbar visibility and operation availability in core', () => {
- expect(normalizeFileViewerToolbar(undefined)).toEqual({
- download: true,
- print: true,
- exportHtml: true,
- zoom: true,
- });
- expect(normalizeFileViewerToolbar({ toolbar: false })).toEqual({
- download: false,
- print: false,
- exportHtml: false,
- zoom: false,
- });
- expect(normalizeFileViewerToolbar({
- toolbar: {
- download: true,
- print: true,
- exportHtml: true,
- zoom: true,
- items: {
- print: false,
- 'zoom-out': false,
- },
- permissions: {
- 'export-html': false,
- 'zoom-reset': false,
- },
- position: 'bottom-right',
- },
- })).toMatchObject({
- download: true,
- print: false,
- exportHtml: false,
- zoom: true,
- items: {
- print: false,
- 'zoom-out': false,
- },
- permissions: {
- 'export-html': false,
- 'zoom-reset': false,
- },
- position: 'bottom-right',
- });
- const availability = resolveFileViewerOperationAvailability({
- extension: 'pdf',
- hasOriginalSource: true,
- renderedReady: true,
- adapter: { toHtml: () => '<main>pdf</main>' },
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: false,
- },
- });
- expect(availability).toMatchObject({
- download: true,
- print: true,
- exportHtml: true,
- zoom: true,
- zoomIn: true,
- zoomOut: false,
- zoomReset: false,
- });
- expect(resolveVisibleFileViewerToolbar({ download: true, print: false, exportHtml: true, zoom: true }, availability)).toEqual({
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- });
- expect(hasVisibleFileViewerToolbarActions({
- download: false,
- print: false,
- exportHtml: false,
- zoom: false,
- })).toBe(false);
- expect(hasVisibleFileViewerToolbarActions({
- download: false,
- print: false,
- exportHtml: true,
- zoom: false,
- })).toBe(true);
- expect(isFileViewerZoomButtonDisabled({
- action: 'canZoomIn',
- availability,
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: false,
- },
- })).toBe(false);
- expect(isFileViewerZoomButtonDisabled({
- action: 'canZoomOut',
- availability,
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: false,
- },
- })).toBe(true);
- expect(isFileViewerZoomButtonDisabled({
- action: 'canZoomIn',
- availability,
- toolbarDisabled: true,
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: false,
- },
- })).toBe(true);
- expect(resolveFileViewerToolbarState({
- extension: 'pdf',
- hasOriginalSource: true,
- renderedReady: true,
- adapter: { toHtml: () => '<main>pdf</main>' },
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: false,
- },
- toolbar: {
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- },
- loading: true,
- })).toMatchObject({
- operationAvailability: {
- download: true,
- print: true,
- exportHtml: true,
- zoom: true,
- },
- visibleToolbar: {
- download: true,
- print: false,
- exportHtml: true,
- zoom: true,
- },
- showToolbar: true,
- toolbarPosition: 'bottom-right',
- toolbarDisabled: true,
- });
- const permissionState = resolveFileViewerToolbarState({
- extension: 'pdf',
- hasOriginalSource: true,
- renderedReady: true,
- adapter: { toHtml: () => '<main>pdf</main>' },
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: true,
- canReset: true,
- },
- toolbar: normalizeFileViewerToolbar({
- toolbar: {
- download: true,
- print: true,
- exportHtml: true,
- zoom: true,
- permissions: {
- download: false,
- 'zoom-in': false,
- },
- },
- }),
- options: {
- toolbar: {
- permissions: {
- download: false,
- 'zoom-in': false,
- },
- },
- },
- });
- expect(permissionState.operationAvailability).toMatchObject({
- download: false,
- print: true,
- exportHtml: true,
- zoom: true,
- zoomIn: false,
- zoomOut: true,
- zoomReset: true,
- });
- expect(permissionState.visibleToolbar).toMatchObject({
- download: false,
- print: true,
- exportHtml: true,
- zoom: true,
- });
- expect(isFileViewerToolbarOperationPermitted({ permissions: { print: false } }, 'print')).toBe(false);
- expect(isFileViewerZoomButtonDisabled({
- action: 'canZoomIn',
- availability: permissionState.operationAvailability,
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: true,
- canReset: true,
- },
- })).toBe(true);
- expect(resolveFileViewerOperationAvailability({
- extension: 'docx',
- source: {
- file: new File(['demo'], 'demo.docx'),
- },
- renderedReady: false,
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: false,
- canZoomOut: false,
- canReset: false,
- },
- }).download).toBe(true);
- expect(resolveFileViewerOperationAvailability({
- extension: 'docx',
- source: {},
- renderedReady: false,
- zoomState: {
- scale: 1,
- label: '100%',
- canZoomIn: false,
- canZoomOut: false,
- canReset: false,
- },
- }).download).toBe(false);
- expect(resolveFileViewerToolbarPosition(undefined, 'pdf')).toBe('bottom-right');
- expect(resolveFileViewerToolbarPosition({ toolbar: { position: 'top' } }, 'pdf')).toBe('top');
- });
- it('resolves operation filenames from display names, original source metadata, and core defaults', () => {
- expect(resolveFileViewerOperationFilename({
- filename: '显示文档.pdf',
- fallback: DEFAULT_FILE_VIEWER_DOWNLOAD_FILENAME,
- })).toBe('显示文档.pdf');
- expect(resolveFileViewerOperationFilename({
- source: {
- file: new File(['demo'], '原始文档.docx'),
- },
- fallback: DEFAULT_FILE_VIEWER_DOWNLOAD_FILENAME,
- })).toBe('原始文档.docx');
- expect(resolveFileViewerOperationFilename({
- fallback: DEFAULT_FILE_VIEWER_DOWNLOAD_FILENAME,
- })).toBe(DEFAULT_FILE_VIEWER_DOWNLOAD_FILENAME);
- expect(resolveFileViewerOperationFilename({
- fallback: DEFAULT_FILE_VIEWER_EXPORT_FILENAME,
- })).toBe(DEFAULT_FILE_VIEWER_EXPORT_FILENAME);
- expect(resolveFileViewerOperationFilename({})).toBe(DEFAULT_FILE_VIEWER_PREVIEW_TITLE);
- expect(resolveFileViewerDisplayFilename({
- kind: 'url',
- filename: '线上报告.pdf',
- extension: 'pdf',
- url: '/docs/report.pdf',
- })).toBe('线上报告.pdf');
- expect(resolveFileViewerDisplayFilename(null)).toBe(DEFAULT_FILE_VIEWER_EXPORT_FILENAME);
- expect(createFileViewerOriginalSourceStateFromNormalizedSource({
- kind: 'url',
- filename: '线上报告.pdf',
- extension: 'pdf',
- url: '/docs/report.pdf',
- size: 1024,
- })).toMatchObject({
- buffer: null,
- file: null,
- url: '/docs/report.pdf',
- filename: '线上报告.pdf',
- });
- });
- it('clones operation availability snapshots without sharing mutable references', () => {
- const availability = resolveFileViewerOperationAvailability({
- extension: 'pdf',
- hasOriginalSource: true,
- renderedReady: true,
- adapter: { toHtml: () => '<main>pdf</main>' },
- zoomState: {
- scale: 1.25,
- label: '125%',
- canZoomIn: false,
- canZoomOut: true,
- canReset: true,
- },
- });
- const snapshot = cloneFileViewerOperationAvailability(availability);
- expect(snapshot).toEqual(availability);
- expect(snapshot).not.toBe(availability);
- snapshot.download = false;
- snapshot.zoomReset = false;
- expect(availability.download).toBe(true);
- expect(availability.zoomReset).toBe(true);
- });
- it('creates public api facades with cloned operation availability snapshots', async () => {
- const availability = {
- download: true,
- print: true,
- exportHtml: false,
- zoom: true,
- zoomIn: true,
- zoomOut: false,
- zoomReset: true,
- };
- const calls: string[] = [];
- const api = createFileViewerPublicApi({
- getOperationAvailability: () => availability,
- downloadOriginalFile: async () => { calls.push('download'); },
- printRenderedHtml: async () => { calls.push('print'); },
- exportRenderedHtml: async () => { calls.push('export-html'); },
- zoomIn: async () => ({
- scale: 1.25,
- label: '125%',
- canZoomIn: true,
- canZoomOut: true,
- canReset: true,
- }),
- zoomOut: async () => ({
- scale: 0.75,
- label: '75%',
- canZoomIn: true,
- canZoomOut: true,
- canReset: true,
- }),
- resetZoom: async () => ({
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: false,
- }),
- getZoomState: () => ({
- scale: 1,
- label: '100%',
- canZoomIn: true,
- canZoomOut: false,
- canReset: false,
- }),
- getScrollContainer: () => null,
- searchDocument: async query => ({
- query,
- total: 0,
- currentIndex: -1,
- current: null,
- matches: [],
- }),
- clearDocumentSearch: async () => ({
- query: '',
- total: 0,
- currentIndex: -1,
- current: null,
- matches: [],
- }),
- nextSearchResult: async () => ({
- query: '',
- total: 0,
- currentIndex: -1,
- current: null,
- matches: [],
- }),
- previousSearchResult: async () => ({
- query: '',
- total: 0,
- currentIndex: -1,
- current: null,
- matches: [],
- }),
- getSearchState: () => ({
- query: '',
- total: 0,
- currentIndex: -1,
- current: null,
- matches: [],
- }),
- collectDocumentAnchors: async () => [],
- scrollToAnchor: async () => true,
- scrollToLine: async () => true,
- getDocumentTextChunks: () => [],
- });
- const snapshot = api.getOperationAvailability();
- snapshot.download = false;
- expect(api.getOperationAvailability().download).toBe(true);
- await api.downloadOriginalFile();
- await api.printRenderedHtml();
- await api.exportRenderedHtml();
- expect(calls).toEqual(['download', 'print', 'export-html']);
- });
- it('reports operation guard errors and cancels the operation', async () => {
- const onError = vi.fn();
- const onCancel = vi.fn();
- const context = buildFileViewerOperationContext('print', buildFileViewerLifecycleContext({
- phase: 'load-complete',
- source: 'file',
- filename: 'broken.pdf',
- version: 1,
- timestamp: 10,
- }));
- await expect(runFileViewerBeforeOperation({
- context,
- options: {
- beforeOperation: () => {
- throw new Error('denied');
- },
- },
- onError,
- onCancel,
- })).resolves.toBe(false);
- expect(onError).toHaveBeenCalledTimes(1);
- expect(onCancel).toHaveBeenCalledWith(context);
- });
- it('executes download, export and print through framework-neutral core operations', async () => {
- const { document } = parseHTML('<main id="root"><article><h1>Preview</h1></article></main>');
- const root = document.getElementById('root') as HTMLElement;
- const originalDocument = globalThis.document;
- const originalWindow = globalThis.window;
- const originalCreateObjectUrl = URL.createObjectURL;
- const originalRevokeObjectUrl = URL.revokeObjectURL;
- const originalCreateElement = document.createElement.bind(document);
- const beforeOperations: string[] = [];
- let downloadedName = '';
- let printed = false;
- let printHtml = '';
- document.createElement = ((tagName: string) => {
- const element = originalCreateElement(tagName);
- if (tagName.toLowerCase() === 'a') {
- Object.defineProperty(element, 'click', {
- configurable: true,
- value: () => {
- downloadedName = (element as HTMLAnchorElement).download;
- },
- });
- }
- return element;
- }) as Document['createElement'];
- Object.defineProperty(globalThis, 'document', { configurable: true, value: document });
- Object.defineProperty(globalThis, 'window', {
- configurable: true,
- value: {
- requestAnimationFrame: (callback: () => void) => {
- callback();
- return 1;
- },
- setTimeout: (callback: () => void) => {
- callback();
- return 1;
- },
- },
- });
- URL.createObjectURL = () => 'blob:viewer-operation-test';
- URL.revokeObjectURL = () => undefined;
- const beforeOperation = (operation: FileViewerOperationType) => {
- beforeOperations.push(operation);
- return true;
- };
- const printWindow = {
- document: {
- readyState: 'complete',
- images: [],
- open: () => undefined,
- write: (nextHtml: string) => {
- printHtml = nextHtml;
- },
- close: () => undefined,
- },
- focus: () => undefined,
- print: () => {
- printed = true;
- },
- requestAnimationFrame: (callback: () => void) => {
- callback();
- return 1;
- },
- setTimeout: (callback: () => void) => {
- callback();
- return 1;
- },
- } as unknown as Window;
- try {
- const html = await executeFileViewerExportHtmlOperation({
- source: root,
- adapter: {
- includeDocumentStyles: false,
- toHtml: options => `<section data-mode="${options.mode}">${options.title}</section>`,
- },
- title: 'demo.viewer',
- filename: 'demo.viewer',
- download: false,
- beforeOperation,
- });
- expect(html).toContain('data-mode="export"');
- expect(html).toContain('demo.viewer');
- await executeFileViewerPrintOperation({
- source: root,
- title: 'demo.viewer',
- printWindow,
- beforeOperation,
- });
- expect(printed).toBe(true);
- expect(printHtml).toContain('<h1>Preview</h1>');
- await executeFileViewerDownloadOperation({
- source: {
- buffer: new Uint8Array([1, 2, 3]).buffer,
- filename: 'demo.bin',
- },
- beforeOperation,
- });
- expect(downloadedName).toBe('demo.bin');
- expect(beforeOperations).toEqual(['export-html', 'print', 'download']);
- await expect(executeFileViewerDownloadOperation({
- source: {},
- throwOnMissingSource: false,
- })).resolves.toBe(false);
- } finally {
- document.createElement = originalCreateElement as Document['createElement'];
- URL.createObjectURL = originalCreateObjectUrl;
- URL.revokeObjectURL = originalRevokeObjectUrl;
- Object.defineProperty(globalThis, 'document', { configurable: true, value: originalDocument });
- Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow });
- }
- });
- it('creates framework-neutral file operation actions for wrappers', async () => {
- const { document } = parseHTML('<main id="root"><article><h1>Action facade</h1></article></main>');
- const root = document.getElementById('root') as HTMLElement;
- const originalDocument = globalThis.document;
- const originalWindow = globalThis.window;
- const originalCreateObjectUrl = URL.createObjectURL;
- const originalRevokeObjectUrl = URL.revokeObjectURL;
- const originalCreateElement = document.createElement.bind(document);
- const beforeOperations: string[] = [];
- const errors: Array<{ operation: string; error: unknown }> = [];
- const errorMessages: string[] = [];
- let downloadedName = '';
- let printHtml = '';
- let printed = false;
- let printAvailable = false;
- document.createElement = ((tagName: string) => {
- const element = originalCreateElement(tagName);
- if (tagName.toLowerCase() === 'a') {
- Object.defineProperty(element, 'click', {
- configurable: true,
- value: () => {
- downloadedName = (element as HTMLAnchorElement).download;
- },
- });
- }
- return element;
- }) as Document['createElement'];
- const printWindow = {
- document: {
- readyState: 'complete',
- images: [],
- open: () => undefined,
- write: (nextHtml: string) => {
- printHtml = nextHtml;
- },
- close: () => undefined,
- },
- focus: () => undefined,
- print: () => {
- printed = true;
- },
- requestAnimationFrame: (callback: () => void) => {
- callback();
- return 1;
- },
- setTimeout: (callback: () => void) => {
- callback();
- return 1;
- },
- } as unknown as Window;
- Object.defineProperty(globalThis, 'document', { configurable: true, value: document });
- Object.defineProperty(globalThis, 'window', {
- configurable: true,
- value: {
- open: () => printWindow,
- requestAnimationFrame: (callback: () => void) => {
- callback();
- return 1;
- },
- setTimeout: (callback: () => void) => {
- callback();
- return 1;
- },
- },
- });
- URL.createObjectURL = () => 'blob:viewer-action-test';
- URL.revokeObjectURL = () => undefined;
- const actions = createFileViewerOperationActionHandlers({
- getBuffer: () => new Uint8Array([4, 5, 6]).buffer,
- getFile: () => null,
- getUrl: () => null,
- getFilename: () => 'suite.viewer',
- getMimeType: () => 'application/octet-stream',
- getRenderedSource: () => root,
- getAdapter: () => ({
- includeDocumentStyles: false,
- toHtml: options => `<section data-mode="${options.mode}">${options.title}</section>`,
- }),
- getWatermarkInlineStyle: () => '--viewer-watermark: demo;',
- getPrintAvailable: () => printAvailable,
- beforeOperation: operation => {
- beforeOperations.push(operation);
- return true;
- },
- onError: context => {
- errors.push(context);
- },
- formatErrorMessage: (prefix, error) => `${prefix}:${error instanceof Error ? error.message : String(error)}`,
- onErrorMessage: message => {
- errorMessages.push(message);
- },
- });
- try {
- const html = await actions.exportRenderedHtml();
- expect(html).toContain('data-mode="export"');
- expect(downloadedName).toBe('suite.viewer.rendered.html');
- await expect(actions.downloadOriginalFile()).resolves.toBe(true);
- expect(downloadedName).toBe('suite.viewer');
- await expect(actions.printRenderedHtml()).resolves.toBeUndefined();
- expect(errors).toHaveLength(1);
- expect(errors[0].operation).toBe('print');
- expect(errorMessages[0]).toContain('打印失败:当前文件类型不支持完整打印');
- expect(printed).toBe(false);
- printAvailable = true;
- await expect(actions.printRenderedHtml()).resolves.toBe(true);
- expect(printed).toBe(true);
- expect(printHtml).toContain('data-mode="print"');
- expect(beforeOperations).toEqual(['export-html', 'download', 'print']);
- } finally {
- document.createElement = originalCreateElement as Document['createElement'];
- URL.createObjectURL = originalCreateObjectUrl;
- URL.revokeObjectURL = originalRevokeObjectUrl;
- Object.defineProperty(globalThis, 'document', { configurable: true, value: originalDocument });
- Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow });
- }
- });
- it('creates public operation action facades that preserve the void component contract', async () => {
- const { document } = parseHTML('<main id="root"><article>Document</article></main>');
- const root = document.querySelector('#root') as HTMLElement;
- const beforeOperations: FileViewerOperationType[] = [];
- const actions = createFileViewerPublicOperationActionHandlers({
- getBuffer: () => null,
- getFile: () => null,
- getUrl: () => '/example/report.pdf',
- getFilename: () => 'report.pdf',
- getRenderedSource: () => root,
- getPrintAvailable: () => true,
- beforeOperation: operation => {
- beforeOperations.push(operation);
- return false;
- },
- });
- await expect(actions.downloadOriginalFile()).resolves.toBeUndefined();
- await expect(actions.exportRenderedHtml()).resolves.toBeUndefined();
- await expect(actions.printRenderedHtml()).resolves.toBeUndefined();
- expect(beforeOperations).toEqual(['download', 'export-html', 'print']);
- });
- it('formats operation action errors through core defaults', () => {
- const message = resolveFileViewerOperationActionErrorMessage({
- context: {
- operation: 'export-html',
- error: new Error('missing DOM'),
- },
- formatErrorMessage: (prefix, error) => `${prefix}:${error instanceof Error ? error.message : String(error)}`,
- });
- const customMessage = resolveFileViewerOperationActionErrorMessage({
- context: {
- operation: 'download',
- error: 'offline',
- },
- prefixes: {
- download: '保存失败',
- },
- formatErrorMessage: (prefix, error) => `${prefix}:${String(error)}`,
- });
- expect(message).toBe('导出 HTML 失败:missing DOM');
- expect(customMessage).toBe('保存失败:offline');
- });
- it('normalizes original source state in core', () => {
- const file = new File(['demo'], 'demo.docx', {
- type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- });
- const source = createFileViewerOriginalSourceState({
- file,
- filename: 'display.docx',
- });
- expect(source).toEqual({
- buffer: null,
- file,
- url: null,
- filename: 'display.docx',
- mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- });
- expect(resolveFileViewerOriginalFilename(source)).toBe('display.docx');
- expect(createFileViewerOriginalSourceState({
- buffer: new ArrayBuffer(2),
- mimeType: 'application/octet-stream',
- }).mimeType).toBe('application/octet-stream');
- });
- });
|