|
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 |
|
| 17 | +import type { DocumentDataModel, IDocumentData } from '@univerjs/core'; |
| 18 | +import type { RenderUnit } from '@univerjs/engine-render'; |
17 | 19 | import type { IMutiPageParagraphBound, ITableBound } from '../doc-event-manager.service'; |
18 | 20 | import { |
19 | 21 | BlockType, |
20 | 22 | DataStreamTreeTokenType, |
21 | 23 | DOC_RANGE_TYPE, |
22 | 24 | DocumentBlockRangeType, |
23 | 25 | DocumentBlockType, |
| 26 | + DocumentFlavor, |
| 27 | + IPermissionService, |
| 28 | + LocaleService, |
| 29 | + LocaleType, |
24 | 30 | PresetListType, |
| 31 | + Univer, |
| 32 | + UniverInstanceType, |
25 | 33 | } from '@univerjs/core'; |
26 | | -import { createDocumentPermissionPoint, getDocumentParagraphPermissionObjectId } from '@univerjs/docs'; |
27 | | -import { DocumentEditArea } from '@univerjs/engine-render'; |
| 34 | +import { |
| 35 | + createDocumentPermissionPoint, |
| 36 | + DocSkeletonManagerService, |
| 37 | + getDocumentParagraphPermissionObjectId, |
| 38 | + setDocumentPermissionValue, |
| 39 | + UniverDocsPlugin, |
| 40 | +} from '@univerjs/docs'; |
| 41 | +import { |
| 42 | + CanvasColorService, |
| 43 | + DocumentEditArea, |
| 44 | + Documents, |
| 45 | + ICanvasColorService, |
| 46 | + IRenderManagerService, |
| 47 | + RenderManagerService, |
| 48 | + Viewport, |
| 49 | +} from '@univerjs/engine-render'; |
28 | 50 | import { UnitAction } from '@univerjs/protocol'; |
| 51 | +import { |
| 52 | + CanvasPopupService, |
| 53 | + ContextMenuService, |
| 54 | + DesktopLayoutService, |
| 55 | + ICanvasPopupService, |
| 56 | + IContextMenuService, |
| 57 | + ILayoutService, |
| 58 | +} from '@univerjs/ui'; |
29 | 59 | import { BehaviorSubject, Subject } from 'rxjs'; |
30 | | -import { describe, expect, it, vi } from 'vitest'; |
| 60 | +import { describe, expect, it, onTestFinished, vi } from 'vitest'; |
| 61 | +import { buildMoveDocBlockActions } from '../../commands/commands/doc-block-move.command'; |
| 62 | +import { genTableSource } from '../../commands/commands/table/table'; |
| 63 | +import enUS from '../../locale/en-US'; |
31 | 64 | import { |
32 | 65 | DOC_PARAGRAPH_MENU_COMPONENT_KEY, |
33 | 66 | DOC_TABLE_BLOCK_MENU_COMPONENT_KEY, |
34 | 67 | } from '../../views/paragraph-menu/component-keys'; |
35 | 68 | import { |
| 69 | + DocEventManagerService, |
36 | 70 | getPreferredParagraphBoundsInRange, |
37 | 71 | getTableBlockMenuHoverRect, |
38 | 72 | getTableHorizontalViewportGeometry, |
39 | 73 | } from '../doc-event-manager.service'; |
40 | 74 | import { DocLayoutInteractionService } from '../doc-layout-interaction.service'; |
41 | 75 | import { DocParagraphMenuService } from '../doc-paragraph-menu.service'; |
| 76 | +import { DocCanvasPopManagerService } from '../doc-popup-manager.service'; |
| 77 | +import { DocFloatMenuService } from '../float-menu.service'; |
| 78 | +import { DocSelectionRenderService } from '../selection/doc-selection-render.service'; |
| 79 | + |
| 80 | +describe('focused table menus with real render services', () => { |
| 81 | + function createFocusedMenuTestBed() { |
| 82 | + const context = new Proxy({ |
| 83 | + font: '', |
| 84 | + measureText: (text: string) => ({ width: text.length * 8, actualBoundingBoxAscent: 8, actualBoundingBoxDescent: 2 }), |
| 85 | + }, { get: (target, key) => key in target ? Reflect.get(target, key) : () => {} }); |
| 86 | + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context as never); |
| 87 | + const univer = new Univer({ locale: LocaleType.EN_US, locales: { [LocaleType.EN_US]: enUS } }); |
| 88 | + onTestFinished(() => { |
| 89 | + univer.dispose(); |
| 90 | + vi.restoreAllMocks(); |
| 91 | + }); |
| 92 | + univer.registerPlugin(UniverDocsPlugin); |
| 93 | + const injector = univer.__getInjector(); |
| 94 | + injector.get(LocaleService).setDirection('ltr'); |
| 95 | + injector.add([IRenderManagerService, { useClass: RenderManagerService }]); |
| 96 | + injector.add([ICanvasColorService, { useClass: CanvasColorService }]); |
| 97 | + injector.add([ICanvasPopupService, { useClass: CanvasPopupService }]); |
| 98 | + injector.add([IContextMenuService, { useClass: ContextMenuService }]); |
| 99 | + injector.add([ILayoutService, { useClass: DesktopLayoutService }]); |
| 100 | + injector.add([DocCanvasPopManagerService]); |
| 101 | + const table = genTableSource(1, 1, 200); |
| 102 | + const tableStream = `${DataStreamTreeTokenType.TABLE_START}${DataStreamTreeTokenType.TABLE_ROW_START}${DataStreamTreeTokenType.TABLE_CELL_START}Cell\r\n${DataStreamTreeTokenType.TABLE_CELL_END}${DataStreamTreeTokenType.TABLE_ROW_END}${DataStreamTreeTokenType.TABLE_END}`; |
| 103 | + const model = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, { |
| 104 | + id: 'focused-table-doc', |
| 105 | + body: { |
| 106 | + dataStream: `${tableStream}\rBody\r\n`, |
| 107 | + paragraphs: [ |
| 108 | + { startIndex: 7, paragraphId: 'cell' }, |
| 109 | + { startIndex: tableStream.length, paragraphId: 'after-table' }, |
| 110 | + { startIndex: tableStream.length + 5, paragraphId: 'body' }, |
| 111 | + ], |
| 112 | + tables: [{ tableId: table.tableId, startIndex: 0, endIndex: tableStream.length }], |
| 113 | + }, |
| 114 | + tableSource: { [table.tableId]: table }, |
| 115 | + documentStyle: { |
| 116 | + documentFlavor: DocumentFlavor.TRADITIONAL, |
| 117 | + pageSize: { width: 300, height: 400 }, |
| 118 | + marginTop: 20, |
| 119 | + marginBottom: 20, |
| 120 | + marginLeft: 20, |
| 121 | + marginRight: 20, |
| 122 | + }, |
| 123 | + }); |
| 124 | + const render = injector.get(IRenderManagerService).createRender(model.getUnitId()) as RenderUnit; |
| 125 | + render.deactivate(); |
| 126 | + render.engine.resizeBySize(300, 400); |
| 127 | + const canvas = render.engine.getCanvasElement(); |
| 128 | + canvas.style.width = '300px'; |
| 129 | + vi.spyOn(canvas, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 300, 400)); |
| 130 | + const viewport = new Viewport('viewMain', render.scene, { left: 0, top: 0, width: 300, height: 400 }); |
| 131 | + onTestFinished(() => viewport.dispose()); |
| 132 | + render.addRenderDependencies([[DocSkeletonManagerService], [DocLayoutInteractionService]]); |
| 133 | + const skeleton = render.with(DocSkeletonManagerService).getSkeleton(); |
| 134 | + const documents = new Documents('focused-table-documents', skeleton); |
| 135 | + render.mainComponent = documents; |
| 136 | + render.scene.addObject(documents); |
| 137 | + render.addRenderDependencies([ |
| 138 | + [DocSelectionRenderService], |
| 139 | + [DocEventManagerService], |
| 140 | + [DocFloatMenuService], |
| 141 | + [DocParagraphMenuService], |
| 142 | + ]); |
| 143 | + const service = render.with(DocParagraphMenuService); |
| 144 | + const bound = render.with(DocEventManagerService).tableBounds.get(table.tableId)!; |
| 145 | + return { injector, service, bound, render, model }; |
| 146 | + } |
| 147 | + |
| 148 | + it('keeps a focused table trigger independent of paragraph hover and closes each popup independently', () => { |
| 149 | + const { injector, service, bound, render, model } = createFocusedMenuTestBed(); |
| 150 | + const popups = injector.get(ICanvasPopupService); |
| 151 | + service.showTableMenu(bound, true); |
| 152 | + expect(service.focusedTableTarget?.table?.tableId).toBe(bound.tableId); |
| 153 | + expect(service.focusedTableTarget?.moveRange.endOffset) |
| 154 | + .toBe(service.focusedTableTarget!.table!.endIndex + 1); |
| 155 | + const sourceRange = service.focusedTableTarget!.moveRange; |
| 156 | + const snapshot = model.getSnapshot(); |
| 157 | + const moved = buildMoveDocBlockActions({ |
| 158 | + documentData: snapshot, |
| 159 | + sourceRange, |
| 160 | + targetOffset: snapshot.body!.dataStream.length - 1, |
| 161 | + }); |
| 162 | + expect(moved.nextDocumentData.body!.dataStream.endsWith('\r\n')).toBe(true); |
| 163 | + const restored = buildMoveDocBlockActions({ |
| 164 | + documentData: moved.nextDocumentData, |
| 165 | + sourceRange: moved.movedRange, |
| 166 | + targetOffset: sourceRange.startOffset, |
| 167 | + }); |
| 168 | + expect(restored.nextDocumentData.body).toEqual(snapshot.body); |
| 169 | + service.showParagraphMenu(createParagraphBound({ paragraphStart: 3, paragraphEnd: 7, startIndex: 7 })); |
| 170 | + expect(service.activeTarget?.kind).toBe(DocumentBlockType.PARAGRAPH); |
| 171 | + expect(popups.popups).toHaveLength(2); |
| 172 | + service.setParagraphMenuActive(true, true); |
| 173 | + service.hideParagraphMenu(true); |
| 174 | + expect(popups.popups).toHaveLength(1); |
| 175 | + expect(render.with(DocLayoutInteractionService).isActive).toBe(true); |
| 176 | + const dismiss = vi.fn(); |
| 177 | + const subscription = service.focusedTableMenuDismiss$.subscribe(dismiss); |
| 178 | + onTestFinished(() => subscription.unsubscribe()); |
| 179 | + popups.popups[0][1].onClickOutside?.(new PointerEvent('pointerdown')); |
| 180 | + expect(dismiss).toHaveBeenCalledOnce(); |
| 181 | + expect(service.focusedTableTarget?.table?.tableId).toBe(bound.tableId); |
| 182 | + service.hideFocusedTableMenu(); |
| 183 | + expect(popups.popups).toHaveLength(0); |
| 184 | + expect(render.with(DocLayoutInteractionService).isActive).toBe(false); |
| 185 | + }); |
| 186 | + |
| 187 | + it('releases focused popups on permission loss and service disposal', () => { |
| 188 | + const { injector, service, bound } = createFocusedMenuTestBed(); |
| 189 | + const popups = injector.get(ICanvasPopupService); |
| 190 | + service.showTableMenu(bound, true); |
| 191 | + setDocumentPermissionValue(injector.get(IPermissionService), 'focused-table-doc', 'focused-table-doc', UnitAction.Edit, false); |
| 192 | + expect(service.focusedTableTarget).toBeNull(); |
| 193 | + expect(popups.popups).toHaveLength(0); |
| 194 | + setDocumentPermissionValue(injector.get(IPermissionService), 'focused-table-doc', 'focused-table-doc', UnitAction.Edit, true); |
| 195 | + service.showTableMenu(bound, true); |
| 196 | + expect(popups.popups).toHaveLength(1); |
| 197 | + service.dispose(); |
| 198 | + expect(popups.popups).toHaveLength(0); |
| 199 | + }); |
| 200 | +}); |
42 | 201 |
|
43 | 202 | describe('DocParagraphMenuService', () => { |
44 | 203 | it('shows the paragraph menu for empty paragraphs', () => { |
|
0 commit comments