From b8303adaf647a5a9efb482a04be701fa85b3ab8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E7=86=B1?= Date: Tue, 14 Jul 2026 16:29:00 +0800 Subject: [PATCH] test: reorganize and deduplicate service tests (#7255) --- .../authz-resource.integration.spec.ts | 273 -------- .../src/services/__tests__/permission.spec.ts | 131 ---- .../services/__tests__/plugin-version.spec.ts | 63 -- .../src/services/__tests__/resources.spec.ts | 437 ------------- .../__tests__/support-services.spec.ts | 121 ---- .../core/src/services/__tests__/theme.spec.ts | 109 ---- .../__tests__/authz-io-local.service.spec.ts | 227 ++++++- .../error/__tests__/error.service.spec.ts | 11 + .../__tests__/permission.service.spec.ts | 125 +++- .../plugin/__tests__/plugin.service.spec.ts | 52 ++ .../__tests__/resource-loader.service.spec.ts | 590 ++++++++++++++---- .../theme/__tests__/theme.service.spec.ts | 95 ++- .../shared/__tests__/after-init-apply.spec.ts | 70 +++ .../__tests__/create-command-test-bed.ts | 14 +- .../__tests__/editor-bridge.service.spec.ts | 16 - .../src/services/auto-height.service.ts | 11 +- .../services/canvas-pop-manager.service.ts | 33 +- .../services/cell-dropdown-manager.service.ts | 8 +- .../src/services/editor-bridge.service.ts | 15 +- .../src/services/hover-manager.service.ts | 8 +- .../sheet-skeleton-manager.service.ts | 5 +- .../src/services/utils/defined-name-utils.ts | 8 +- .../src/services/utils/doc-skeleton-util.ts | 16 +- .../shortcut/__tests__/keycode.spec.ts | 61 -- .../__tests__/shortcut.service.spec.ts | 16 - 25 files changed, 1082 insertions(+), 1433 deletions(-) delete mode 100644 packages/core/src/services/__tests__/authz-resource.integration.spec.ts delete mode 100644 packages/core/src/services/__tests__/permission.spec.ts delete mode 100644 packages/core/src/services/__tests__/plugin-version.spec.ts delete mode 100644 packages/core/src/services/__tests__/resources.spec.ts delete mode 100644 packages/core/src/services/__tests__/support-services.spec.ts delete mode 100644 packages/core/src/services/__tests__/theme.spec.ts create mode 100644 packages/core/src/shared/__tests__/after-init-apply.spec.ts delete mode 100644 packages/ui/src/services/shortcut/__tests__/keycode.spec.ts diff --git a/packages/core/src/services/__tests__/authz-resource.integration.spec.ts b/packages/core/src/services/__tests__/authz-resource.integration.spec.ts deleted file mode 100644 index 15006b5795..0000000000 --- a/packages/core/src/services/__tests__/authz-resource.integration.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Univer } from '../../univer'; -import { UnitAction, UnitObject, UnitRole } from '@univerjs/protocol'; -import { BehaviorSubject } from 'rxjs'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { UnitModel, UniverInstanceType } from '../../common/unit'; -import { IAuthzIoService } from '../authz-io/type'; -import { IUniverInstanceService } from '../instance/instance.service'; -import { IMentionIOService } from '../mention-io/type'; -import { IResourceLoaderService } from '../resource-loader/type'; -import { UserManagerService } from '../user-manager/user-manager.service'; -import { createTestBed } from './create-test-bed'; - -interface ITestBoardData { - id: string; - name?: string; - resources?: Array<{ name: string; data: string }>; -} - -class MockBoardUnit extends UnitModel { - override readonly type = UniverInstanceType.UNIVER_BOARD; - override name$ = new BehaviorSubject(''); - private readonly _snapshot: ITestBoardData; - - constructor(snapshot: Partial = {}) { - super(); - this._snapshot = { - id: 'board-resource', - name: '', - ...snapshot, - }; - this.name$.next(this._snapshot.name ?? ''); - } - - override getUnitId(): string { - return this._snapshot.id; - } - - override setName(name: string): void { - this._snapshot.name = name; - this.name$.next(name); - } - - override getSnapshot(): ITestBoardData { - return this._snapshot; - } - - override getRev(): number { - return 1; - } - - override incrementRev(): void { } - - override setRev(): void { } -} - -describe('Authz/resource integration', () => { - let univer: Univer; - let unitId: string; - - beforeEach(() => { - const instance = createTestBed(); - univer = instance.univer; - unitId = instance.unitId; - }); - - afterEach(() => { - univer.dispose(); - }); - - it('should persist and reload permission resources through a real workbook lifecycle', async () => { - const injector = univer.__getInjector(); - const authzIoService = injector.get(IAuthzIoService); - const resourceLoaderService = injector.get(IResourceLoaderService); - const userManagerService = injector.get(UserManagerService); - - userManagerService.setCurrentUser({ - userID: 'Owner_real-user', - name: 'Owner User', - avatar: '', - }); - - const objectID = await authzIoService.create({ - objectType: UnitObject.SelectRange, - selectRangeObject: { - unitID: unitId, - name: 'Protected range', - collaborators: [], - scope: undefined, - }, - }); - - expect(await authzIoService.allowed({ unitID: unitId, objectID, objectType: UnitObject.SelectRange, actions: [UnitAction.Edit] })).toEqual([ - { action: UnitAction.Edit, allowed: true }, - ]); - - await authzIoService.update({ - objectType: UnitObject.SelectRange, - objectID, - unitID: unitId, - share: undefined, - name: 'Protected range', - strategies: [{ action: UnitAction.Edit, role: UnitRole.Reader }], - scope: undefined, - collaborators: undefined, - }); - - expect(await authzIoService.allowed({ unitID: unitId, objectID, objectType: UnitObject.SelectRange, actions: [UnitAction.Edit] })).toEqual([ - { action: UnitAction.Edit, allowed: false }, - ]); - - const listed = await authzIoService.list({ - unitID: unitId, - objectIDs: [objectID], - actions: [UnitAction.Edit, UnitAction.View], - }); - - expect(listed).toHaveLength(1); - expect(listed[0].name).toBe('Protected range'); - expect(listed[0].actions).toContainEqual({ action: UnitAction.Edit, allowed: false }); - - const batched = await authzIoService.batchAllowed([ - { unitID: unitId, objectID, objectType: UnitObject.SelectRange, actions: [UnitAction.Edit] }, - { unitID: unitId, objectID: 'missing', objectType: UnitObject.SelectRange, actions: [UnitAction.View] }, - ]); - - expect(batched[0].actions).toEqual([{ action: UnitAction.Edit, allowed: false }]); - expect(batched[1].actions).toEqual([{ action: UnitAction.View, allowed: true }]); - - expect(await authzIoService.listCollaborators({ unitID: unitId, objectID })).toEqual([]); - expect(await authzIoService.listRoles({ objectType: UnitObject.SelectRange })).toEqual({ roles: [], actions: [] }); - await expect(authzIoService.createCollaborator({ unitID: unitId, objectID } as never)).resolves.toBeUndefined(); - await expect(authzIoService.updateCollaborator({ unitID: unitId, objectID } as never)).resolves.toBeUndefined(); - await expect(authzIoService.deleteCollaborator({ unitID: unitId, objectID } as never)).resolves.toBeUndefined(); - await expect(authzIoService.putCollaborators({ unitID: unitId, objectID, collaborators: [] })).resolves.toBeUndefined(); - - const snapshot = resourceLoaderService.saveUnit(unitId); - const authzResource = snapshot?.resources.find((resource) => resource.name === 'SHEET_AuthzIoMockService_PLUGIN'); - - expect(authzResource?.data).toContain(objectID); - - injector.get(IUniverInstanceService).disposeUnit(unitId); - - const unloaded = await authzIoService.list({ - unitID: unitId, - objectIDs: [objectID], - actions: [UnitAction.Edit], - }); - - expect(unloaded[0].name).toBe(''); - expect(unloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: false }]); - - univer.createUnit(UniverInstanceType.UNIVER_SHEET, snapshot!); - - const reloaded = await authzIoService.list({ - unitID: unitId, - objectIDs: [objectID], - actions: [UnitAction.Edit], - }); - - expect(reloaded[0].name).toBe('Protected range'); - expect(reloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: false }]); - }); - - it('should persist and reload permission resources for board units', async () => { - const injector = univer.__getInjector(); - const authzIoService = injector.get(IAuthzIoService); - const resourceLoaderService = injector.get(IResourceLoaderService); - const univerInstanceService = injector.get(IUniverInstanceService); - - univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BOARD, MockBoardUnit); - const board = univer.createUnit(UniverInstanceType.UNIVER_BOARD, { - id: 'board-resource', - }); - const objectID = await authzIoService.create({ - objectType: UnitObject.Workbook, - worksheetObject: { - unitID: board.getUnitId(), - name: 'Board permission', - collaborators: [], - strategies: [{ action: UnitAction.Edit, role: UnitRole.Owner }], - scope: undefined, - }, - }); - - const snapshot = resourceLoaderService.saveUnit(board.getUnitId()); - const authzResource = snapshot?.resources?.find((resource) => resource.name === 'SHEET_AuthzIoMockService_PLUGIN'); - - expect(authzResource?.data).toContain(objectID); - - expect(univerInstanceService.disposeUnit(board.getUnitId())).toBe(true); - - const unloaded = await authzIoService.list({ - unitID: board.getUnitId(), - objectIDs: [objectID], - actions: [UnitAction.Edit], - }); - - expect(unloaded[0].name).toBe(''); - expect(unloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: true }]); - - univer.createUnit(UniverInstanceType.UNIVER_BOARD, snapshot!); - - const reloaded = await authzIoService.list({ - unitID: board.getUnitId(), - objectIDs: [objectID], - actions: [UnitAction.Edit], - }); - - expect(reloaded[0].name).toBe('Board permission'); - expect(reloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: true }]); - }); - - it('should expose current user data consistently through mention and user services', async () => { - const injector = univer.__getInjector(); - const userManagerService = injector.get(UserManagerService); - const mentionIOService = injector.get(IMentionIOService); - const userEvents: string[] = []; - - userManagerService.userChange$.subscribe((event) => { - userEvents.push(event.type); - }); - - userManagerService.setCurrentUser({ - userID: 'Owner_alice', - name: 'Alice', - avatar: 'alice.png', - }); - userManagerService.addUser({ userID: 'Editor_bob', name: 'Bob' }); - - expect(userManagerService.getCurrentUser().name).toBe('Alice'); - expect(userManagerService.getUser('Editor_bob')?.name).toBe('Bob'); - expect(userManagerService.list()).toHaveLength(2); - - const mentionResult = await mentionIOService.list({ page: 2, size: 5 }); - expect(mentionResult.page).toBe(2); - expect(mentionResult.size).toBe(5); - expect(mentionResult.list[0].mentions[0]).toMatchObject({ - objectId: 'Owner_alice', - label: 'Alice', - metadata: { - icon: 'alice.png', - }, - }); - - let callbackCalled = false; - expect(userManagerService.getUser('missing-user', () => { - callbackCalled = true; - })).toBeUndefined(); - expect(callbackCalled).toBe(true); - - userManagerService.delete('Editor_bob'); - userManagerService.clear(); - - expect(userManagerService.list()).toEqual([]); - expect(userEvents).toEqual(['add', 'add', 'delete', 'clear']); - }); -}); diff --git a/packages/core/src/services/__tests__/permission.spec.ts b/packages/core/src/services/__tests__/permission.spec.ts deleted file mode 100644 index 71aec6ec82..0000000000 --- a/packages/core/src/services/__tests__/permission.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Univer } from '../../univer'; -import type { IPermissionPoint } from '../permission/type'; -import { UnitAction, UnitObject } from '@univerjs/protocol'; -import { BehaviorSubject, firstValueFrom } from 'rxjs'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { PermissionService } from '../permission/permission.service'; -import { PermissionStatus } from '../permission/type'; -import { createTestBed } from './create-test-bed'; - -class TestPermissionPoint implements IPermissionPoint { - type: UnitObject.Workbook; - id; - status: PermissionStatus.INIT; - subType: UnitAction.Copy; - value: boolean = false; - - constructor(id: string) { - this.id = `${UnitObject.Workbook}.${UnitAction.CreateSheet}.${id}` as string; - } -} - -describe('Test permission service', () => { - let univer: Univer; - let permissionService: PermissionService; - - beforeEach(() => { - univer?.dispose(); - const instance = createTestBed([[PermissionService]]); - univer = instance.univer; - permissionService = instance.get(PermissionService); - }); - - it('test get permission from permissionService', () => { - const point = new TestPermissionPoint('test'); - permissionService.addPermissionPoint(point); - const result = permissionService.getPermissionPoint(point.id); - expect(result).toBe(point); - }); - - it('test get permission$ from permissionService', async () => { - const point = new TestPermissionPoint('test'); - const initValue = point.value; - permissionService.addPermissionPoint(point); - const result$ = permissionService.getPermissionPoint$(point.id)!; - permissionService.updatePermissionPoint(point.id, !initValue); - const v = await firstValueFrom(result$); - expect(v.value).toBe(!initValue); - }); - - it('test compose permission', () => { - const point1 = new TestPermissionPoint('test1'); - const point2 = new TestPermissionPoint('test2'); - permissionService.addPermissionPoint(point1); - permissionService.addPermissionPoint(point2); - const result = permissionService.composePermission([point1.id, point2.id]); - expect(result).toEqual([point1, point2]); - }); - - it('test compose permission$', async () => { - const point1 = new TestPermissionPoint('test1'); - const point2 = new TestPermissionPoint('test2'); - permissionService.addPermissionPoint(point1); - permissionService.addPermissionPoint(point2); - const result$ = permissionService.composePermission$([point1.id, point2.id]); - permissionService.updatePermissionPoint(point2.id, !point2.value); - const v = await firstValueFrom(result$); - expect(v).toEqual([point1, point2]); - }); - - it('should manage permission updates, duplicates and visibility flags', () => { - const point = new TestPermissionPoint('subject'); - const pointSubject = new BehaviorSubject>(point); - const updates: string[] = []; - const warningSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); - const subscription = permissionService.permissionPointUpdate$.subscribe((permission) => { - updates.push(`${permission.id}:${String(permission.value)}`); - }); - - expect(permissionService.getShowComponents()).toBe(true); - permissionService.setShowComponents(false); - expect(permissionService.getShowComponents()).toBe(false); - - expect(permissionService.addPermissionPoint(pointSubject)).toBe(true); - expect(permissionService.addPermissionPoint(point)).toBe(false); - - permissionService.updatePermissionPoint(point.id, true); - - expect(permissionService.getPermissionPoint(point.id)).toMatchObject({ - value: true, - status: PermissionStatus.DONE, - }); - expect(updates).toEqual([ - `${point.id}:false`, - `${point.id}:true`, - ]); - - const snapshot = permissionService.getAllPermissionPoint(); - snapshot.clear(); - expect(permissionService.getAllPermissionPoint().size).toBe(1); - - permissionService.deletePermissionPoint(point.id); - expect(permissionService.getPermissionPoint(point.id)).toBeUndefined(); - - permissionService.clearPermissionMap(); - expect(permissionService.getAllPermissionPoint().size).toBe(0); - - subscription.unsubscribe(); - warningSpy.mockRestore(); - }); - - it('should throw clear errors when composing missing permission points', () => { - expect(() => permissionService.composePermission(['missing.permission'])).toThrow('[PermissionService]: missing.permission permissionPoint does not exist!'); - expect(() => permissionService.composePermission$(['missing.permission'])).toThrow('[PermissionService]: missing.permission permissionPoint does not exist!'); - }); -}); diff --git a/packages/core/src/services/__tests__/plugin-version.spec.ts b/packages/core/src/services/__tests__/plugin-version.spec.ts deleted file mode 100644 index feb373a0e4..0000000000 --- a/packages/core/src/services/__tests__/plugin-version.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Injector } from '../../common/di'; -import { describe, expect, it, vi } from 'vitest'; -import { UniverInstanceType } from '../../common/unit'; -import { Univer } from '../../univer'; -import { Plugin } from '../plugin/plugin.service'; - -describe('plugin version check', () => { - it('should allow registering plugin with same version as core', () => { - class SameVersionPlugin extends Plugin { - static override pluginName = 'same-version-plugin'; - static override packageName = '@univerjs/same-version-plugin'; - static override version = Plugin.version; - static override type = UniverInstanceType.UNIVER_SHEET; - - protected override _injector!: Injector; - } - - const univer = new Univer(); - expect(() => univer.registerPlugin(SameVersionPlugin)).not.toThrow(); - }); - - it('should log error with package name when plugin version mismatches', () => { - class MismatchVersionPlugin extends Plugin { - static override pluginName = 'mismatch-version-plugin'; - static override packageName = '@univerjs/mismatch-version-plugin'; - static override version = '__MISMATCH_VERSION__'; - static override type = UniverInstanceType.UNIVER_SHEET; - - protected override _injector!: Injector; - } - - const univer = new Univer(); - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - expect(() => univer.registerPlugin(MismatchVersionPlugin)).not.toThrow(); - expect(errorSpy).toHaveBeenCalledWith( - expect.stringContaining('[PluginService]'), - expect.stringContaining('Plugin version mismatch.') - ); - expect(errorSpy).toHaveBeenCalledWith( - expect.stringContaining('[PluginService]'), - expect.stringContaining('package: "@univerjs/mismatch-version-plugin"') - ); - - errorSpy.mockRestore(); - }); -}); diff --git a/packages/core/src/services/__tests__/resources.spec.ts b/packages/core/src/services/__tests__/resources.spec.ts deleted file mode 100644 index fa2d239aec..0000000000 --- a/packages/core/src/services/__tests__/resources.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { IDocumentData } from '../../types/interfaces'; -import type { Univer } from '../../univer'; -import type { IResources } from '../resource-manager/type'; -import { BehaviorSubject } from 'rxjs'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DOCS_NORMAL_EDITOR_UNIT_ID_KEY } from '../../common/const'; -import { UnitModel, UniverInstanceType } from '../../common/unit'; -import { IUniverInstanceService } from '../instance/instance.service'; -import { IResourceLoaderService } from '../resource-loader/type'; -import { IResourceManagerService } from '../resource-manager/type'; -import { createTestBed } from './create-test-bed'; - -function createDocData(id: string, resources?: NonNullable): Partial { - return { - id, - resources, - body: { - dataStream: 'Hello\r\n', - }, - documentStyle: { - pageSize: { width: 100, height: 100 }, - marginTop: 0, - marginBottom: 0, - marginLeft: 0, - marginRight: 0, - }, - }; -} - -interface ITestSlideData { - id: string; - name?: string; - resources?: IResources; -} - -interface ITestBoardData { - id: string; - name?: string; - resources?: IResources; -} - -class MockSlideUnit extends UnitModel { - override type = UniverInstanceType.UNIVER_SLIDE; - override name$ = new BehaviorSubject(''); - private readonly _snapshot: ITestSlideData; - - constructor(snapshot: Partial = {}) { - super(); - this._snapshot = { - id: 'slide-resource', - name: '', - ...snapshot, - }; - this.name$.next(this._snapshot.name ?? ''); - } - - override getUnitId(): string { - return this._snapshot.id; - } - - override setName(name: string): void { - this._snapshot.name = name; - this.name$.next(name); - } - - override getSnapshot(): ITestSlideData { - return this._snapshot; - } - - override getRev(): number { - return 1; - } - - override incrementRev(): void { } - - override setRev(): void { } -} - -class MockBoardUnit extends UnitModel { - override readonly type = UniverInstanceType.UNIVER_BOARD; - override name$ = new BehaviorSubject(''); - private readonly _snapshot: ITestBoardData; - - constructor(snapshot: Partial = {}) { - super(); - this._snapshot = { - id: 'board-resource', - name: '', - ...snapshot, - }; - this.name$.next(this._snapshot.name ?? ''); - } - - override getUnitId(): string { - return this._snapshot.id; - } - - override setName(name: string): void { - this._snapshot.name = name; - this.name$.next(name); - } - - override getSnapshot(): ITestBoardData { - return this._snapshot; - } - - override getRev(): number { - return 1; - } - - override incrementRev(): void { } - - override setRev(): void { } -} - -describe('Test resources service', () => { - let univer: Univer; - - beforeEach(() => { - univer?.dispose(); - const instance = createTestBed(); - univer = instance.univer; - }); - - it('test register resources', () => { - const resourceManagerService = univer.__getInjector().get(IResourceManagerService); - const resourceLoaderService = univer.__getInjector().get(IResourceLoaderService); - const pluginName = 'SHEET_test_PLUGIN'; - const model: Record = {}; - resourceManagerService.registerPluginResource({ - pluginName, - businesses: [UniverInstanceType.UNIVER_SHEET], - onLoad: () => { }, - onUnLoad: () => { }, - toJson: () => JSON.stringify(model), - parseJson: (bytes) => JSON.parse(bytes), - }); - const snapshot = resourceLoaderService.saveUnit('test'); - const resource = snapshot?.resources.find((item) => item.name === pluginName); - expect(!!resource).toBeTruthy(); - expect(resource?.data).toBe(JSON.stringify(model)); - model.a = 123; - const snapshotRev1 = resourceLoaderService.saveUnit('test'); - const resourceRev1 = snapshotRev1?.resources.find((item) => item.name === pluginName); - expect(resourceRev1?.data).toBe(JSON.stringify(model)); - }); - - it('test resources load', () => { - const resourceManagerService = univer.__getInjector().get(IResourceManagerService); - const pluginName = 'SHEET_test_PLUGIN'; - const model: Record = {}; - let result = ''; - resourceManagerService.registerPluginResource({ - pluginName, - businesses: [UniverInstanceType.UNIVER_SHEET], - onLoad: (_unitId, resource) => { result = resource; }, - onUnLoad: () => { }, - toJson: () => JSON.stringify(model), - parseJson: (bytes) => JSON.parse(bytes), - }); - expect(result).toEqual({ a: 123 }); - }); - - it('should load and unload workbook/doc resources through the real unit lifecycle', () => { - const injector = univer.__getInjector(); - const resourceManagerService = injector.get(IResourceManagerService); - const resourceLoaderService = injector.get(IResourceLoaderService); - const univerInstanceService = injector.get(IUniverInstanceService); - const loads: Array<[string, string]> = []; - const unloads: string[] = []; - - resourceManagerService.registerPluginResource({ - pluginName: 'DOC_test_PLUGIN', - businesses: [UniverInstanceType.UNIVER_DOC], - onLoad: (unitId, model: { kind: string }) => loads.push([unitId, model.kind]), - onUnLoad: (unitId) => unloads.push(unitId), - toJson: (unitId) => JSON.stringify({ unitId, kind: 'saved' }), - parseJson: (bytes) => JSON.parse(bytes), - }); - - const doc = univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData('doc-resource', [ - { name: 'DOC_test_PLUGIN', data: '{"kind":"doc"}' }, - ])); - const internalDoc = univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, [ - { name: 'DOC_test_PLUGIN', data: '{"kind":"internal"}' }, - ])); - - expect(loads).toContainEqual(['doc-resource', 'doc']); - expect(loads).not.toContainEqual([DOCS_NORMAL_EDITOR_UNIT_ID_KEY, 'internal']); - expect(resourceLoaderService.saveUnit('missing-unit')).toBeNull(); - expect(resourceLoaderService.saveUnit('doc-resource')?.resources).toEqual([ - { name: 'DOC_test_PLUGIN', data: JSON.stringify({ unitId: 'doc-resource', kind: 'saved' }) }, - ]); - - expect(univerInstanceService.disposeUnit(doc.getUnitId())).toBe(true); - expect(univerInstanceService.disposeUnit(internalDoc.getUnitId())).toBe(true); - expect(unloads).toEqual(expect.arrayContaining(['doc-resource', DOCS_NORMAL_EDITOR_UNIT_ID_KEY])); - }); - - it('should load and unload slide resources through the real unit lifecycle', () => { - const injector = univer.__getInjector(); - const resourceManagerService = injector.get(IResourceManagerService); - const resourceLoaderService = injector.get(IResourceLoaderService); - const univerInstanceService = injector.get(IUniverInstanceService); - const pluginName = 'SLIDE_TEST_PLUGIN' as never; - const loads: Array<[string, string]> = []; - const unloads: string[] = []; - - univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); - resourceManagerService.registerPluginResource<{ kind: string }>({ - pluginName, - businesses: [UniverInstanceType.UNIVER_SLIDE], - onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), - onUnLoad: (unitId) => unloads.push(unitId), - toJson: (unitId) => JSON.stringify({ kind: `saved:${unitId}` }), - parseJson: (bytes) => JSON.parse(bytes), - }); - - const slide = univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { - id: 'slide-resource', - resources: [ - { name: pluginName, data: '{"kind":"loaded"}' }, - ], - }); - - expect(loads).toContainEqual(['slide-resource', 'loaded']); - expect(resourceLoaderService.saveUnit('slide-resource')?.resources).toEqual([ - { name: pluginName, data: '{"kind":"saved:slide-resource"}' }, - ]); - - expect(univerInstanceService.disposeUnit(slide.getUnitId())).toBe(true); - expect(unloads).toEqual(['slide-resource']); - }); - - it('should load resources for existing slide units when hooks register later', () => { - const injector = univer.__getInjector(); - const resourceManagerService = injector.get(IResourceManagerService); - const univerInstanceService = injector.get(IUniverInstanceService); - const pluginName = 'SLIDE_LATE_PLUGIN' as never; - const loads: Array<[string, string]> = []; - - univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); - univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { - id: 'slide-late-resource', - resources: [ - { name: pluginName, data: '{"kind":"late"}' }, - ], - }); - - resourceManagerService.registerPluginResource<{ kind: string }>({ - pluginName, - businesses: [UniverInstanceType.UNIVER_SLIDE], - onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), - onUnLoad: () => undefined, - toJson: () => '{}', - parseJson: (bytes) => JSON.parse(bytes), - }); - - expect(loads).toEqual([['slide-late-resource', 'late']]); - }); - - it('should load and unload board resources through the real unit lifecycle', () => { - const injector = univer.__getInjector(); - const resourceManagerService = injector.get(IResourceManagerService); - const resourceLoaderService = injector.get(IResourceLoaderService); - const univerInstanceService = injector.get(IUniverInstanceService); - const pluginName = 'BOARD_TEST_PLUGIN' as never; - const loads: Array<[string, string]> = []; - const unloads: string[] = []; - - univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BOARD, MockBoardUnit); - resourceManagerService.registerPluginResource<{ kind: string }>({ - pluginName, - businesses: [UniverInstanceType.UNIVER_BOARD], - onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), - onUnLoad: (unitId) => unloads.push(unitId), - toJson: (unitId) => JSON.stringify({ kind: `saved:${unitId}` }), - parseJson: (bytes) => JSON.parse(bytes), - }); - - const board = univer.createUnit(UniverInstanceType.UNIVER_BOARD, { - id: 'board-resource', - resources: [ - { name: pluginName, data: '{"kind":"loaded"}' }, - ], - }); - - expect(loads).toContainEqual(['board-resource', 'loaded']); - expect(resourceLoaderService.saveUnit('board-resource')?.resources).toEqual([ - { name: pluginName, data: '{"kind":"saved:board-resource"}' }, - ]); - - expect(univerInstanceService.disposeUnit(board.getUnitId())).toBe(true); - expect(unloads).toEqual(['board-resource']); - }); - - it('should load resources for existing board units when hooks register later', () => { - const injector = univer.__getInjector(); - const resourceManagerService = injector.get(IResourceManagerService); - const univerInstanceService = injector.get(IUniverInstanceService); - const pluginName = 'BOARD_LATE_PLUGIN' as never; - const loads: Array<[string, string]> = []; - - univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BOARD, MockBoardUnit); - univer.createUnit(UniverInstanceType.UNIVER_BOARD, { - id: 'board-late-resource', - resources: [ - { name: pluginName, data: '{"kind":"late"}' }, - ], - }); - - resourceManagerService.registerPluginResource<{ kind: string }>({ - pluginName, - businesses: [UniverInstanceType.UNIVER_BOARD], - onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), - onUnLoad: () => undefined, - toJson: () => '{}', - parseJson: (bytes) => JSON.parse(bytes), - }); - - expect(loads).toEqual([['board-late-resource', 'late']]); - }); - - it('should load array-shaped slide plugin resources through the unit lifecycle', () => { - const injector = univer.__getInjector(); - const resourceManagerService = injector.get(IResourceManagerService); - const univerInstanceService = injector.get(IUniverInstanceService); - const pluginName = 'SLIDE_ARRAY_PLUGIN' as never; - const loads: Array<[string, string]> = []; - - univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); - resourceManagerService.registerPluginResource<{ kind: string }>({ - pluginName, - businesses: [UniverInstanceType.UNIVER_SLIDE], - onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), - onUnLoad: () => undefined, - toJson: () => '{}', - parseJson: (bytes) => JSON.parse(bytes), - }); - - univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { - id: 'slide-array-resource', - resources: [ - { name: pluginName, data: '{"kind":"array"}' }, - ], - }); - - expect(loads).toEqual([['slide-array-resource', 'array']]); - }); - - it('should load serialized object slide plugin resources through the unit lifecycle', () => { - const injector = univer.__getInjector(); - const resourceManagerService = injector.get(IResourceManagerService); - const univerInstanceService = injector.get(IUniverInstanceService); - const pluginName = 'SLIDE_SERIALIZED_OBJECT_PLUGIN' as never; - const loads: Array<[string, string]> = []; - - univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); - resourceManagerService.registerPluginResource<{ kind: string }>({ - pluginName, - businesses: [UniverInstanceType.UNIVER_SLIDE], - onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), - onUnLoad: () => undefined, - toJson: () => '{}', - parseJson: (bytes) => JSON.parse(bytes), - }); - - univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { - id: 'slide-serialized-object-resource', - resources: [ - { name: pluginName, data: '{"kind":"serialized-object"}' }, - ], - }); - - expect(loads).toEqual([['slide-serialized-object-resource', 'serialized-object']]); - }); - - it('should load empty persisted resource payloads when hooks are registered later', () => { - const resourceManagerService = univer.__getInjector().get(IResourceManagerService); - const onLoad = vi.fn(); - - univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData('doc-empty-resource', [ - { name: 'DOC_EMPTY_PLUGIN', data: '' }, - ])); - - resourceManagerService.registerPluginResource({ - pluginName: 'DOC_EMPTY_PLUGIN', - businesses: [UniverInstanceType.UNIVER_DOC], - onLoad, - onUnLoad: () => undefined, - toJson: () => '{}', - parseJson: (bytes) => bytes ? JSON.parse(bytes) : {}, - }); - - expect(onLoad).toHaveBeenCalledWith('doc-empty-resource', {}); - }); - - it('should ignore malformed persisted resource payloads when hooks are registered later', () => { - const resourceManagerService = univer.__getInjector().get(IResourceManagerService); - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - const onLoad = vi.fn(); - - univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData('doc-bad-resource', [ - { name: 'DOC_BAD_PLUGIN', data: '{bad json}' }, - ])); - - resourceManagerService.registerPluginResource({ - pluginName: 'DOC_BAD_PLUGIN', - businesses: [UniverInstanceType.UNIVER_DOC], - onLoad, - onUnLoad: () => undefined, - toJson: () => '{}', - parseJson: (bytes) => JSON.parse(bytes), - }); - - expect(onLoad).not.toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalledWith('Load Document{doc-bad-resource} Resources{DOC_BAD_PLUGIN} Data Error.'); - - errorSpy.mockRestore(); - }); -}); diff --git a/packages/core/src/services/__tests__/support-services.spec.ts b/packages/core/src/services/__tests__/support-services.spec.ts deleted file mode 100644 index 0100720127..0000000000 --- a/packages/core/src/services/__tests__/support-services.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { Injector } from '../../common/di'; -import { afterInitApply } from '../../shared/after-init-apply'; -import { CommandService, CommandType, ICommandService } from '../command/command.service'; -import { ConfigService, IConfigService } from '../config/config.service'; -import { TestConfirmService } from '../confirm/confirm.service'; -import { ContextService, IContextService } from '../context/context.service'; -import { ErrorService } from '../error/error.service'; -import { DesktopLogService, ILogService } from '../log/log.service'; - -afterEach(() => { - vi.useRealTimers(); - vi.restoreAllMocks(); -}); - -function createCommandInjector(): Injector { - const injector = new Injector(); - - injector.add([ICommandService, { useClass: CommandService }]); - injector.add([ILogService, { useClass: DesktopLogService }]); - injector.add([IContextService, { useClass: ContextService }]); - injector.add([IConfigService, { useClass: ConfigService }]); - - return injector; -} - -describe('support services and helpers', () => { - it('should emit errors through ErrorService obtained from DI and complete on dispose', () => { - const injector = new Injector([[ErrorService]]); - const service = injector.get(ErrorService); - const received: string[] = []; - let completed = false; - - service.error$.subscribe({ - next: (error) => received.push(error.errorKey), - complete: () => { - completed = true; - }, - }); - - service.emit('formula.ref'); - service.emit('permission.denied'); - injector.dispose(); - - expect(received).toEqual(['formula.ref', 'permission.denied']); - expect(completed).toBe(true); - }); - - it('should resolve and fail through TestConfirmService obtained from DI', async () => { - const injector = new Injector([[TestConfirmService]]); - const service = injector.get(TestConfirmService); - let completed = false; - - service.confirmOptions$.subscribe({ - complete: () => { - completed = true; - }, - }); - - await expect(service.confirm('continue')).resolves.toBe(true); - expect(() => service.open('open')).toThrow('This is not implemented in the test service!'); - expect(() => service.close('id')).toThrow('This is not implemented in the test service!'); - - injector.dispose(); - expect(completed).toBe(true); - }); - - it('should resolve afterInitApply on mutation execution before fallback timer', async () => { - vi.useFakeTimers(); - - const injector = createCommandInjector(); - const commandService = injector.get(ICommandService); - - commandService.registerCommand({ - id: 'support.mutation', - type: CommandType.MUTATION, - handler: () => true, - }); - - const pending = afterInitApply(commandService); - await commandService.executeCommand('support.mutation'); - await vi.advanceTimersByTimeAsync(16); - - await expect(pending).resolves.toBeUndefined(); - injector.dispose(); - }); - - it('should resolve afterInitApply through the fallback timer when no mutation runs', async () => { - vi.useFakeTimers(); - - const injector = createCommandInjector(); - const commandService = injector.get(ICommandService); - let settled = false; - - const pending = afterInitApply(commandService).then(() => { - settled = true; - }); - - await vi.advanceTimersByTimeAsync(320); - await pending; - - expect(settled).toBe(true); - injector.dispose(); - }); -}); diff --git a/packages/core/src/services/__tests__/theme.spec.ts b/packages/core/src/services/__tests__/theme.spec.ts deleted file mode 100644 index 8e6374a2bd..0000000000 --- a/packages/core/src/services/__tests__/theme.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Univer } from '../../univer'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { ThemeService } from '../theme/theme.service'; -import { createTestBed } from './create-test-bed'; - -describe('Test theme service', () => { - let univer: Univer; - - beforeEach(() => { - univer?.dispose(); - const instance = createTestBed(); - univer = instance.univer; - }); - - beforeEach(() => { - univer?.dispose(); - const instance = createTestBed(); - univer = instance.univer; - }); - - it('should get default theme', () => { - const themeService = univer.__getInjector().get(ThemeService); - expect(themeService.getCurrentTheme()).toBeDefined(); - }); - - it('should set and get theme', () => { - const themeService = univer.__getInjector().get(ThemeService); - const oldTheme = themeService.getCurrentTheme(); - const theme = { - ...oldTheme, - primary: { - ...oldTheme.primary, - 600: '#123456', - }, - }; - themeService.setTheme(theme); - expect(themeService.getCurrentTheme().primary[600]).toBe('#123456'); - }); - - it('should set and get dark mode', () => { - const themeService = univer.__getInjector().get(ThemeService); - themeService.setDarkMode(true); - expect(themeService.darkMode).toBe(true); - themeService.setDarkMode(false); - expect(themeService.darkMode).toBe(false); - }); - - it('should validate theme color', () => { - const themeService = univer.__getInjector().get(ThemeService); - expect(themeService.isValidThemeColor('primary.600')).toBe(true); - expect(themeService.isValidThemeColor('notexist')).toBe(false); - }); - - it('should get color from theme', () => { - const themeService = univer.__getInjector().get(ThemeService); - const oldTheme = themeService.getCurrentTheme(); - const theme = { - ...oldTheme, - primary: { - ...oldTheme.primary, - 600: '#abcdef', - }, - }; - themeService.setTheme(theme); - expect(themeService.getColorFromTheme('primary.600')).toBe('#abcdef'); - }); - - it('should get semantic highlight background token from theme', () => { - const themeService = univer.__getInjector().get(ThemeService); - const token = themeService.getColorFromTheme<{ color: string; alpha: number }>('highlight.background.1'); - - expect(token).toEqual({ color: 'purple.500', alpha: 0.3 }); - expect(themeService.getColorFromTheme(token.color)).toBe('#9061F9'); - }); - - it('should tap cached theme color', () => { - const themeService = univer.__getInjector().get(ThemeService); - const oldTheme = themeService.getCurrentTheme(); - const theme = { - ...oldTheme, - primary: { - ...oldTheme.primary, - 600: '#abcdef', - }, - }; - themeService.setTheme(theme); - - expect(themeService.isValidThemeColor('primary.600')).toBe(true); - expect(themeService.isValidThemeColor('primary.600')).toBe(true); // Should hit the cache - expect(themeService.getColorFromTheme('primary.600')).toBe('#abcdef'); - expect(themeService.getColorFromTheme('primary.600')).toBe('#abcdef'); // Should hit the cache - }); -}); diff --git a/packages/core/src/services/authz-io/__tests__/authz-io-local.service.spec.ts b/packages/core/src/services/authz-io/__tests__/authz-io-local.service.spec.ts index 95b8b544c6..eb69506acf 100644 --- a/packages/core/src/services/authz-io/__tests__/authz-io-local.service.spec.ts +++ b/packages/core/src/services/authz-io/__tests__/authz-io-local.service.spec.ts @@ -14,49 +14,214 @@ * limitations under the License. */ -import { UnitObject } from '@univerjs/protocol'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { Injector } from '../../../common/di'; -import { DesktopLogService, ILogService } from '../../log/log.service'; -import { ResourceManagerService } from '../../resource-manager/resource-manager.service'; -import { IResourceManagerService } from '../../resource-manager/type'; +import type { Univer } from '../../../univer'; +import { UnitAction, UnitObject, UnitRole } from '@univerjs/protocol'; +import { BehaviorSubject } from 'rxjs'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { UnitModel, UniverInstanceType } from '../../../common/unit'; +import { createTestBed } from '../../__tests__/create-test-bed'; +import { IUniverInstanceService } from '../../instance/instance.service'; +import { IResourceLoaderService } from '../../resource-loader/type'; import { UserManagerService } from '../../user-manager/user-manager.service'; -import { AuthzIoLocalService } from '../authz-io-local.service'; +import { IAuthzIoService } from '../type'; + +interface ITestBoardData { + id: string; + name?: string; + resources?: Array<{ name: string; data: string }>; +} + +class MockBoardUnit extends UnitModel { + override readonly type = UniverInstanceType.UNIVER_BOARD; + override name$ = new BehaviorSubject(''); + private readonly _snapshot: ITestBoardData; + + constructor(snapshot: Partial = {}) { + super(); + this._snapshot = { + id: 'board-resource', + name: '', + ...snapshot, + }; + this.name$.next(this._snapshot.name ?? ''); + } + + override getUnitId(): string { + return this._snapshot.id; + } + + override setName(name: string): void { + this._snapshot.name = name; + this.name$.next(name); + } + + override getSnapshot(): ITestBoardData { + return this._snapshot; + } + + override getRev(): number { + return 1; + } + + override incrementRev(): void { } + + override setRev(): void { } +} describe('AuthzIoLocalService', () => { - let service: AuthzIoLocalService; + let univer: Univer; + let unitId: string; beforeEach(() => { - const injector = new Injector(); - injector.add([ILogService, { useClass: DesktopLogService }]); - injector.add([IResourceManagerService, { useClass: ResourceManagerService }]); - injector.add([UserManagerService]); - injector.add([AuthzIoLocalService]); - service = injector.get(AuthzIoLocalService); + const instance = createTestBed(); + univer = instance.univer; + unitId = instance.unitId; }); - it('creates range permission objects and evaluates default owner actions', async () => { - const objectID = await service.create({ - objectType: UnitObject.SelectRange, - selectRangeObject: { unitID: 'book-1', name: 'Protected range' }, - } as never); + afterEach(() => { + univer.dispose(); + }); - await expect(service.allowed({ unitID: 'book-1', objectID, actions: [6, 16] } as never)).resolves.toEqual([ - { action: 6, allowed: true }, - { action: 16, allowed: true }, + it('should persist and reload permission resources through a real workbook lifecycle', async () => { + const injector = univer.__getInjector(); + const authzIoService = injector.get(IAuthzIoService); + const resourceLoaderService = injector.get(IResourceLoaderService); + const userManagerService = injector.get(UserManagerService); + + userManagerService.setCurrentUser({ + userID: 'Owner_real-user', + name: 'Owner User', + avatar: '', + }); + + const objectID = await authzIoService.create({ + objectType: UnitObject.SelectRange, + selectRangeObject: { + unitID: unitId, + name: 'Protected range', + collaborators: [], + scope: undefined, + }, + }); + + expect(await authzIoService.allowed({ unitID: unitId, objectID, objectType: UnitObject.SelectRange, actions: [UnitAction.Edit] })).toEqual([ + { action: UnitAction.Edit, allowed: true }, ]); + + await authzIoService.update({ + objectType: UnitObject.SelectRange, + objectID, + unitID: unitId, + share: undefined, + name: 'Protected range', + strategies: [{ action: UnitAction.Edit, role: UnitRole.Reader }], + scope: undefined, + collaborators: undefined, + }); + + expect(await authzIoService.allowed({ unitID: unitId, objectID, objectType: UnitObject.SelectRange, actions: [UnitAction.Edit] })).toEqual([ + { action: UnitAction.Edit, allowed: false }, + ]); + + const listed = await authzIoService.list({ + unitID: unitId, + objectIDs: [objectID], + actions: [UnitAction.Edit, UnitAction.View], + }); + + expect(listed).toHaveLength(1); + expect(listed[0].name).toBe('Protected range'); + expect(listed[0].actions).toContainEqual({ action: UnitAction.Edit, allowed: false }); + + const batched = await authzIoService.batchAllowed([ + { unitID: unitId, objectID, objectType: UnitObject.SelectRange, actions: [UnitAction.Edit] }, + { unitID: unitId, objectID: 'missing', objectType: UnitObject.SelectRange, actions: [UnitAction.View] }, + ]); + + expect(batched[0].actions).toEqual([{ action: UnitAction.Edit, allowed: false }]); + expect(batched[1].actions).toEqual([{ action: UnitAction.View, allowed: true }]); + + expect(await authzIoService.listCollaborators({ unitID: unitId, objectID })).toEqual([]); + expect(await authzIoService.listRoles({ objectType: UnitObject.SelectRange })).toEqual({ roles: [], actions: [] }); + await expect(authzIoService.createCollaborator({ unitID: unitId, objectID } as never)).resolves.toBeUndefined(); + await expect(authzIoService.updateCollaborator({ unitID: unitId, objectID } as never)).resolves.toBeUndefined(); + await expect(authzIoService.deleteCollaborator({ unitID: unitId, objectID } as never)).resolves.toBeUndefined(); + await expect(authzIoService.putCollaborators({ unitID: unitId, objectID, collaborators: [] })).resolves.toBeUndefined(); + + const snapshot = resourceLoaderService.saveUnit(unitId); + const authzResource = snapshot?.resources.find((resource) => resource.name === 'SHEET_AuthzIoMockService_PLUGIN'); + + expect(authzResource?.data).toContain(objectID); + + injector.get(IUniverInstanceService).disposeUnit(unitId); + + const unloaded = await authzIoService.list({ + unitID: unitId, + objectIDs: [objectID], + actions: [UnitAction.Edit], + }); + + expect(unloaded[0].name).toBe(''); + expect(unloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: false }]); + + univer.createUnit(UniverInstanceType.UNIVER_SHEET, snapshot!); + + const reloaded = await authzIoService.list({ + unitID: unitId, + objectIDs: [objectID], + actions: [UnitAction.Edit], + }); + + expect(reloaded[0].name).toBe('Protected range'); + expect(reloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: false }]); }); - it('lists created permission objects with their range metadata', async () => { - const objectID = await service.create({ - objectType: UnitObject.SelectRange, - selectRangeObject: { unitID: 'book-1', name: 'Protected range' }, - } as never); + it('should persist and reload permission resources for board units', async () => { + const injector = univer.__getInjector(); + const authzIoService = injector.get(IAuthzIoService); + const resourceLoaderService = injector.get(IResourceLoaderService); + const univerInstanceService = injector.get(IUniverInstanceService); - const [permissionPoint] = await service.list({ unitID: 'book-1', objectIDs: [objectID], actions: [6] } as never); + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BOARD, MockBoardUnit); + const board = univer.createUnit(UniverInstanceType.UNIVER_BOARD, { + id: 'board-resource', + }); + const objectID = await authzIoService.create({ + objectType: UnitObject.Workbook, + worksheetObject: { + unitID: board.getUnitId(), + name: 'Board permission', + collaborators: [], + strategies: [{ action: UnitAction.Edit, role: UnitRole.Owner }], + scope: undefined, + }, + }); - expect(permissionPoint.objectID).toBe(objectID); - expect(permissionPoint.name).toBe('Protected range'); - expect(permissionPoint.unitID).toBe('book-1'); + const snapshot = resourceLoaderService.saveUnit(board.getUnitId()); + const authzResource = snapshot?.resources?.find((resource) => resource.name === 'SHEET_AuthzIoMockService_PLUGIN'); + + expect(authzResource?.data).toContain(objectID); + + expect(univerInstanceService.disposeUnit(board.getUnitId())).toBe(true); + + const unloaded = await authzIoService.list({ + unitID: board.getUnitId(), + objectIDs: [objectID], + actions: [UnitAction.Edit], + }); + + expect(unloaded[0].name).toBe(''); + expect(unloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: true }]); + + univer.createUnit(UniverInstanceType.UNIVER_BOARD, snapshot!); + + const reloaded = await authzIoService.list({ + unitID: board.getUnitId(), + objectIDs: [objectID], + actions: [UnitAction.Edit], + }); + + expect(reloaded[0].name).toBe('Board permission'); + expect(reloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: true }]); }); }); diff --git a/packages/core/src/services/error/__tests__/error.service.spec.ts b/packages/core/src/services/error/__tests__/error.service.spec.ts index dc9de41382..482ae1f81a 100644 --- a/packages/core/src/services/error/__tests__/error.service.spec.ts +++ b/packages/core/src/services/error/__tests__/error.service.spec.ts @@ -36,4 +36,15 @@ describe('ErrorService', () => { expect(errors).toEqual(['permission-denied', 'network-timeout']); }); + + it('completes the error stream when its injector is disposed', () => { + const injector = new Injector([[ErrorService]]); + const disposableService = injector.get(ErrorService); + let completed = false; + + disposableService.error$.subscribe({ complete: () => completed = true }); + injector.dispose(); + + expect(completed).toBe(true); + }); }); diff --git a/packages/core/src/services/permission/__tests__/permission.service.spec.ts b/packages/core/src/services/permission/__tests__/permission.service.spec.ts index 0064a2401f..0f37632176 100644 --- a/packages/core/src/services/permission/__tests__/permission.service.spec.ts +++ b/packages/core/src/services/permission/__tests__/permission.service.spec.ts @@ -14,49 +14,118 @@ * limitations under the License. */ +import type { Univer } from '../../../univer'; import type { IPermissionPoint } from '../type'; import { UnitAction, UnitObject } from '@univerjs/protocol'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { Injector } from '../../../common/di'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createTestBed } from '../../__tests__/create-test-bed'; import { PermissionService } from '../permission.service'; import { PermissionStatus } from '../type'; -describe('PermissionService', () => { - let service: PermissionService; +class TestPermissionPoint implements IPermissionPoint { + type: UnitObject.Workbook; + id; + status: PermissionStatus.INIT; + subType: UnitAction.Copy; + value: boolean = false; - const permissionPoint = (id: string, value: boolean, status: PermissionStatus): IPermissionPoint => ({ - id, - value, - status, - type: UnitObject.Workbook, - subType: UnitAction.Edit, - }); + constructor(id: string) { + this.id = `${UnitObject.Workbook}.${UnitAction.CreateSheet}.${id}` as string; + } +} + +describe('PermissionService', () => { + let univer: Univer; + let permissionService: PermissionService; beforeEach(() => { - const injector = new Injector(); - injector.add([PermissionService]); - service = injector.get(PermissionService); + univer?.dispose(); + const instance = createTestBed([[PermissionService]]); + univer = instance.univer; + permissionService = instance.get(PermissionService); }); - it('updates a permission point and composes the latest decision set', () => { + it('test get permission from permissionService', () => { + const point = new TestPermissionPoint('test'); + permissionService.addPermissionPoint(point); + const result = permissionService.getPermissionPoint(point.id); + expect(result).toBe(point); + }); + + it('test get permission$ from permissionService', async () => { + const point = new TestPermissionPoint('test'); + const initValue = point.value; + permissionService.addPermissionPoint(point); + const result$ = permissionService.getPermissionPoint$(point.id)!; + permissionService.updatePermissionPoint(point.id, !initValue); + const v = await firstValueFrom(result$); + expect(v.value).toBe(!initValue); + }); + + it('test compose permission', () => { + const point1 = new TestPermissionPoint('test1'); + const point2 = new TestPermissionPoint('test2'); + permissionService.addPermissionPoint(point1); + permissionService.addPermissionPoint(point2); + const result = permissionService.composePermission([point1.id, point2.id]); + expect(result).toEqual([point1, point2]); + }); + + it('test compose permission$', async () => { + const point1 = new TestPermissionPoint('test1'); + const point2 = new TestPermissionPoint('test2'); + permissionService.addPermissionPoint(point1); + permissionService.addPermissionPoint(point2); + const result$ = permissionService.composePermission$([point1.id, point2.id]); + permissionService.updatePermissionPoint(point2.id, !point2.value); + const v = await firstValueFrom(result$); + expect(v).toEqual([point1, point2]); + }); + + it('should manage permission updates, duplicates and visibility flags', () => { + const point = new TestPermissionPoint('subject'); + const pointSubject = new BehaviorSubject>(point); const updates: string[] = []; - service.permissionPointUpdate$.subscribe((point) => updates.push(`${point.id}:${String(point.value)}`)); + const warningSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const subscription = permissionService.permissionPointUpdate$.subscribe((permission) => { + updates.push(`${permission.id}:${String(permission.value)}`); + }); - expect(service.addPermissionPoint(permissionPoint('sheet-edit', false, PermissionStatus.INIT))).toBe(true); - expect(service.addPermissionPoint(permissionPoint('sheet-view', true, PermissionStatus.DONE))).toBe(true); + expect(permissionService.getShowComponents()).toBe(true); + permissionService.setShowComponents(false); + expect(permissionService.getShowComponents()).toBe(false); - service.updatePermissionPoint('sheet-edit', true); + expect(permissionService.addPermissionPoint(pointSubject)).toBe(true); + expect(permissionService.addPermissionPoint(point)).toBe(false); - expect(service.composePermission(['sheet-edit', 'sheet-view']).map((point) => point.value)).toEqual([true, true]); - expect(service.getPermissionPoint('sheet-edit')?.status).toBe(PermissionStatus.DONE); - expect(updates).toEqual(['sheet-edit:false', 'sheet-view:true', 'sheet-edit:true']); + permissionService.updatePermissionPoint(point.id, true); + + expect(permissionService.getPermissionPoint(point.id)).toMatchObject({ + value: true, + status: PermissionStatus.DONE, + }); + expect(updates).toEqual([ + `${point.id}:false`, + `${point.id}:true`, + ]); + + const snapshot = permissionService.getAllPermissionPoint(); + snapshot.clear(); + expect(permissionService.getAllPermissionPoint().size).toBe(1); + + permissionService.deletePermissionPoint(point.id); + expect(permissionService.getPermissionPoint(point.id)).toBeUndefined(); + + permissionService.clearPermissionMap(); + expect(permissionService.getAllPermissionPoint().size).toBe(0); + + subscription.unsubscribe(); + warningSpy.mockRestore(); }); - it('removes permission decisions that no longer apply to the current document', () => { - service.addPermissionPoint(permissionPoint('range-lock', true, PermissionStatus.DONE)); - service.deletePermissionPoint('range-lock'); - - expect(service.getPermissionPoint('range-lock')).toBeUndefined(); - expect(() => service.composePermission(['range-lock'])).toThrow('[PermissionService]: range-lock permissionPoint does not exist!'); + it('should throw clear errors when composing missing permission points', () => { + expect(() => permissionService.composePermission(['missing.permission'])).toThrow('[PermissionService]: missing.permission permissionPoint does not exist!'); + expect(() => permissionService.composePermission$(['missing.permission'])).toThrow('[PermissionService]: missing.permission permissionPoint does not exist!'); }); }); diff --git a/packages/core/src/services/plugin/__tests__/plugin.service.spec.ts b/packages/core/src/services/plugin/__tests__/plugin.service.spec.ts index bf28731bc1..1c21721629 100644 --- a/packages/core/src/services/plugin/__tests__/plugin.service.spec.ts +++ b/packages/core/src/services/plugin/__tests__/plugin.service.spec.ts @@ -519,4 +519,56 @@ describe('PluginService', () => { expect(constructed).toEqual(['started-sheet-plugin']); univer.dispose(); }); + + it('should accept plugins built with the current core version', () => { + class SameVersionPlugin extends Plugin { + static override pluginName = 'same-version-plugin'; + static override packageName = '@univerjs/same-version-plugin'; + static override version = Plugin.version; + static override type = UniverInstanceType.UNIVER_SHEET; + + constructor( + _config: undefined, + @Inject(Injector) override readonly _injector: Injector + ) { + super(); + } + } + + const univer = new Univer(); + + expect(() => univer.registerPlugin(SameVersionPlugin)).not.toThrow(); + univer.dispose(); + }); + + it('should report the package name when a plugin version does not match core', () => { + class MismatchVersionPlugin extends Plugin { + static override pluginName = 'mismatch-version-plugin'; + static override packageName = '@univerjs/mismatch-version-plugin'; + static override version = '__MISMATCH_VERSION__'; + static override type = UniverInstanceType.UNIVER_SHEET; + + constructor( + _config: undefined, + @Inject(Injector) override readonly _injector: Injector + ) { + super(); + } + } + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const univer = new Univer(); + + expect(() => univer.registerPlugin(MismatchVersionPlugin)).not.toThrow(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('[PluginService]'), + expect.stringContaining('Plugin version mismatch.') + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('[PluginService]'), + expect.stringContaining('package: "@univerjs/mismatch-version-plugin"') + ); + + univer.dispose(); + }); }); diff --git a/packages/core/src/services/resource-loader/__tests__/resource-loader.service.spec.ts b/packages/core/src/services/resource-loader/__tests__/resource-loader.service.spec.ts index 71214e8837..63179deb57 100644 --- a/packages/core/src/services/resource-loader/__tests__/resource-loader.service.spec.ts +++ b/packages/core/src/services/resource-loader/__tests__/resource-loader.service.spec.ts @@ -14,130 +14,502 @@ * limitations under the License. */ -import { Subject } from 'rxjs'; +import type { IDocumentData } from '../../../types/interfaces'; +import type { Univer } from '../../../univer'; +import type { IResources } from '../../resource-manager/type'; +import { BehaviorSubject } from 'rxjs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Injector } from '../../../common/di'; -import { UniverInstanceType } from '../../../common/unit'; +import { DOCS_NORMAL_EDITOR_UNIT_ID_KEY } from '../../../common/const'; +import { UnitModel, UniverInstanceType } from '../../../common/unit'; +import { createTestBed } from '../../__tests__/create-test-bed'; import { IUniverInstanceService } from '../../instance/instance.service'; import { IResourceManagerService } from '../../resource-manager/type'; -import { ResourceLoaderService } from '../resource-loader.service'; +import { IResourceLoaderService } from '../type'; + +function createDocData(id: string, resources?: NonNullable): Partial { + return { + id, + resources, + body: { + dataStream: 'Hello\r\n', + }, + documentStyle: { + pageSize: { width: 100, height: 100 }, + marginTop: 0, + marginBottom: 0, + marginLeft: 0, + marginRight: 0, + }, + }; +} + +interface ITestSlideData { + id: string; + name?: string; + resources?: IResources; +} + +interface ITestBoardData { + id: string; + name?: string; + resources?: IResources; +} + +interface ITestBaseData { + id: string; + name?: string; + resources?: IResources; +} + +class MockSlideUnit extends UnitModel { + override type = UniverInstanceType.UNIVER_SLIDE; + override name$ = new BehaviorSubject(''); + private readonly _snapshot: ITestSlideData; + + constructor(snapshot: Partial = {}) { + super(); + this._snapshot = { + id: 'slide-resource', + name: '', + ...snapshot, + }; + this.name$.next(this._snapshot.name ?? ''); + } + + override getUnitId(): string { + return this._snapshot.id; + } + + override setName(name: string): void { + this._snapshot.name = name; + this.name$.next(name); + } + + override getSnapshot(): ITestSlideData { + return this._snapshot; + } + + override getRev(): number { + return 1; + } + + override incrementRev(): void { } + + override setRev(): void { } +} + +class MockBoardUnit extends UnitModel { + override readonly type = UniverInstanceType.UNIVER_BOARD; + override name$ = new BehaviorSubject(''); + private readonly _snapshot: ITestBoardData; + + constructor(snapshot: Partial = {}) { + super(); + this._snapshot = { + id: 'board-resource', + name: '', + ...snapshot, + }; + this.name$.next(this._snapshot.name ?? ''); + } + + override getUnitId(): string { + return this._snapshot.id; + } + + override setName(name: string): void { + this._snapshot.name = name; + this.name$.next(name); + } + + override getSnapshot(): ITestBoardData { + return this._snapshot; + } + + override getRev(): number { + return 1; + } + + override incrementRev(): void { } + + override setRev(): void { } +} + +class MockBaseUnit extends UnitModel { + override readonly type = UniverInstanceType.UNIVER_BASE; + override name$ = new BehaviorSubject(''); + private readonly _snapshot: ITestBaseData; + + constructor(snapshot: Partial = {}) { + super(); + this._snapshot = { + id: 'base-resource', + name: '', + ...snapshot, + }; + this.name$.next(this._snapshot.name ?? ''); + } + + override getUnitId(): string { + return this._snapshot.id; + } + + override setName(name: string): void { + this._snapshot.name = name; + this.name$.next(name); + } + + override getSnapshot(): ITestBaseData { + return this._snapshot; + } + + override getRev(): number { + return 1; + } + + override incrementRev(): void { } + + override setRev(): void { } +} describe('ResourceLoaderService', () => { - let service: ResourceLoaderService; - let register$: Subject; - let sheetAdded$: Subject; - let docAdded$: Subject; - let slideAdded$: Subject; - let baseAdded$: Subject; - let sheetDisposed$: Subject; - let docDisposed$: Subject; - let baseDisposed$: Subject; - let slideDisposed$: Subject; - let resourceManagerService: { - getAllResourceHooks: ReturnType; - register$: Subject; - loadResources: ReturnType; - unloadResources: ReturnType; - getResources: ReturnType; - }; - let univerInstanceService: { - getAllUnitsForType: ReturnType; - getTypeOfUnitAdded$: ReturnType; - getTypeOfUnitDisposed$: ReturnType; - getUnit: ReturnType; - }; + let univer: Univer; beforeEach(() => { - register$ = new Subject(); - sheetAdded$ = new Subject(); - docAdded$ = new Subject(); - slideAdded$ = new Subject(); - baseAdded$ = new Subject(); - sheetDisposed$ = new Subject(); - docDisposed$ = new Subject(); - baseDisposed$ = new Subject(); - slideDisposed$ = new Subject(); - resourceManagerService = { - getAllResourceHooks: vi.fn(() => []), - register$, - loadResources: vi.fn(), - unloadResources: vi.fn(), - getResources: vi.fn(() => [{ name: 'plugin', data: '{}' }]), - }; - univerInstanceService = { - getAllUnitsForType: vi.fn(() => []), - getTypeOfUnitAdded$: vi.fn((type) => { - if (type === UniverInstanceType.UNIVER_SHEET) return sheetAdded$; - if (type === UniverInstanceType.UNIVER_DOC) return docAdded$; - if (type === UniverInstanceType.UNIVER_BASE) return baseAdded$; - return slideAdded$; - }), - getTypeOfUnitDisposed$: vi.fn((type) => { - if (type === UniverInstanceType.UNIVER_SHEET) return sheetDisposed$; - if (type === UniverInstanceType.UNIVER_DOC) return docDisposed$; - if (type === UniverInstanceType.UNIVER_BASE) return baseDisposed$; - return slideDisposed$; - }), - getUnit: vi.fn(), - }; - class TestResourceManagerService { - getAllResourceHooks = resourceManagerService.getAllResourceHooks; - register$ = resourceManagerService.register$; - loadResources = resourceManagerService.loadResources; - unloadResources = resourceManagerService.unloadResources; - getResources = resourceManagerService.getResources; - } - - class TestUniverInstanceService { - getAllUnitsForType = univerInstanceService.getAllUnitsForType; - getTypeOfUnitAdded$ = univerInstanceService.getTypeOfUnitAdded$; - getTypeOfUnitDisposed$ = univerInstanceService.getTypeOfUnitDisposed$; - getUnit = univerInstanceService.getUnit; - } - - const injector = new Injector(); - injector.add([IResourceManagerService, { useClass: TestResourceManagerService as never }]); - injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]); - injector.add([ResourceLoaderService]); - service = injector.get(ResourceLoaderService); + univer?.dispose(); + const instance = createTestBed(); + univer = instance.univer; }); - it('loads resources when a workbook unit is added and unloads them when disposed', () => { - const workbook = { - getUnitId: () => 'book-1', - getSnapshot: () => ({ resources: [{ name: 'sheet-plugin', data: '{}' }] }), - }; - - sheetAdded$.next({ unit: workbook }); - sheetDisposed$.next(workbook); - - expect(resourceManagerService.loadResources).toHaveBeenCalledWith('book-1', [{ name: 'sheet-plugin', data: '{}' }]); - expect(resourceManagerService.unloadResources).toHaveBeenCalledWith('book-1', UniverInstanceType.UNIVER_SHEET); + it('test register resources', () => { + const resourceManagerService = univer.__getInjector().get(IResourceManagerService); + const resourceLoaderService = univer.__getInjector().get(IResourceLoaderService); + const pluginName = 'SHEET_test_PLUGIN'; + const model: Record = {}; + resourceManagerService.registerPluginResource({ + pluginName, + businesses: [UniverInstanceType.UNIVER_SHEET], + onLoad: () => { }, + onUnLoad: () => { }, + toJson: () => JSON.stringify(model), + parseJson: (bytes) => JSON.parse(bytes), + }); + const snapshot = resourceLoaderService.saveUnit('test'); + const resource = snapshot?.resources.find((item) => item.name === pluginName); + expect(!!resource).toBeTruthy(); + expect(resource?.data).toBe(JSON.stringify(model)); + model.a = 123; + const snapshotRev1 = resourceLoaderService.saveUnit('test'); + const resourceRev1 = snapshotRev1?.resources.find((item) => item.name === pluginName); + expect(resourceRev1?.data).toBe(JSON.stringify(model)); }); - it('loads resources when a base unit is added and unloads them when disposed', () => { - const base = { - getUnitId: () => 'base-1', - getSnapshot: () => ({ resources: [{ name: 'base-plugin', data: '{}' }] }), - }; - - baseAdded$.next({ unit: base }); - baseDisposed$.next(base); - - expect(resourceManagerService.loadResources).toHaveBeenCalledWith('base-1', [{ name: 'base-plugin', data: '{}' }]); - expect(resourceManagerService.unloadResources).toHaveBeenCalledWith('base-1', UniverInstanceType.UNIVER_BASE); + it('test resources load', () => { + const resourceManagerService = univer.__getInjector().get(IResourceManagerService); + const pluginName = 'SHEET_test_PLUGIN'; + const model: Record = {}; + let result = ''; + resourceManagerService.registerPluginResource({ + pluginName, + businesses: [UniverInstanceType.UNIVER_SHEET], + onLoad: (_unitId, resource) => { result = resource; }, + onUnLoad: () => { }, + toJson: () => JSON.stringify(model), + parseJson: (bytes) => JSON.parse(bytes), + }); + expect(result).toEqual({ a: 123 }); }); - it('saves a unit snapshot with current plugin resources', () => { - univerInstanceService.getUnit.mockReturnValue({ - type: UniverInstanceType.UNIVER_SHEET, - getSnapshot: () => ({ id: 'book-1', sheets: {} }), + it('should load and unload workbook/doc resources through the real unit lifecycle', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const resourceLoaderService = injector.get(IResourceLoaderService); + const univerInstanceService = injector.get(IUniverInstanceService); + const loads: Array<[string, string]> = []; + const unloads: string[] = []; + + resourceManagerService.registerPluginResource({ + pluginName: 'DOC_test_PLUGIN', + businesses: [UniverInstanceType.UNIVER_DOC], + onLoad: (unitId, model: { kind: string }) => loads.push([unitId, model.kind]), + onUnLoad: (unitId) => unloads.push(unitId), + toJson: (unitId) => JSON.stringify({ unitId, kind: 'saved' }), + parseJson: (bytes) => JSON.parse(bytes), }); - expect(service.saveUnit('book-1')).toEqual({ - id: 'book-1', - sheets: {}, - resources: [{ name: 'plugin', data: '{}' }], + const doc = univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData('doc-resource', [ + { name: 'DOC_test_PLUGIN', data: '{"kind":"doc"}' }, + ])); + const internalDoc = univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, [ + { name: 'DOC_test_PLUGIN', data: '{"kind":"internal"}' }, + ])); + + expect(loads).toContainEqual(['doc-resource', 'doc']); + expect(loads).not.toContainEqual([DOCS_NORMAL_EDITOR_UNIT_ID_KEY, 'internal']); + expect(resourceLoaderService.saveUnit('missing-unit')).toBeNull(); + expect(resourceLoaderService.saveUnit('doc-resource')?.resources).toEqual([ + { name: 'DOC_test_PLUGIN', data: JSON.stringify({ unitId: 'doc-resource', kind: 'saved' }) }, + ]); + + expect(univerInstanceService.disposeUnit(doc.getUnitId())).toBe(true); + expect(univerInstanceService.disposeUnit(internalDoc.getUnitId())).toBe(true); + expect(unloads).toEqual(expect.arrayContaining(['doc-resource', DOCS_NORMAL_EDITOR_UNIT_ID_KEY])); + }); + + it('should load and unload slide resources through the real unit lifecycle', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const resourceLoaderService = injector.get(IResourceLoaderService); + const univerInstanceService = injector.get(IUniverInstanceService); + const pluginName = 'SLIDE_TEST_PLUGIN' as never; + const loads: Array<[string, string]> = []; + const unloads: string[] = []; + + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); + resourceManagerService.registerPluginResource<{ kind: string }>({ + pluginName, + businesses: [UniverInstanceType.UNIVER_SLIDE], + onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), + onUnLoad: (unitId) => unloads.push(unitId), + toJson: (unitId) => JSON.stringify({ kind: `saved:${unitId}` }), + parseJson: (bytes) => JSON.parse(bytes), }); + + const slide = univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { + id: 'slide-resource', + resources: [ + { name: pluginName, data: '{"kind":"loaded"}' }, + ], + }); + + expect(loads).toContainEqual(['slide-resource', 'loaded']); + expect(resourceLoaderService.saveUnit('slide-resource')?.resources).toEqual([ + { name: pluginName, data: '{"kind":"saved:slide-resource"}' }, + ]); + + expect(univerInstanceService.disposeUnit(slide.getUnitId())).toBe(true); + expect(unloads).toEqual(['slide-resource']); + }); + + it('should load resources for existing slide units when hooks register later', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const univerInstanceService = injector.get(IUniverInstanceService); + const pluginName = 'SLIDE_LATE_PLUGIN' as never; + const loads: Array<[string, string]> = []; + + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); + univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { + id: 'slide-late-resource', + resources: [ + { name: pluginName, data: '{"kind":"late"}' }, + ], + }); + + resourceManagerService.registerPluginResource<{ kind: string }>({ + pluginName, + businesses: [UniverInstanceType.UNIVER_SLIDE], + onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), + onUnLoad: () => undefined, + toJson: () => '{}', + parseJson: (bytes) => JSON.parse(bytes), + }); + + expect(loads).toEqual([['slide-late-resource', 'late']]); + }); + + it('should load and unload board resources through the real unit lifecycle', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const resourceLoaderService = injector.get(IResourceLoaderService); + const univerInstanceService = injector.get(IUniverInstanceService); + const pluginName = 'BOARD_TEST_PLUGIN' as never; + const loads: Array<[string, string]> = []; + const unloads: string[] = []; + + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BOARD, MockBoardUnit); + resourceManagerService.registerPluginResource<{ kind: string }>({ + pluginName, + businesses: [UniverInstanceType.UNIVER_BOARD], + onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), + onUnLoad: (unitId) => unloads.push(unitId), + toJson: (unitId) => JSON.stringify({ kind: `saved:${unitId}` }), + parseJson: (bytes) => JSON.parse(bytes), + }); + + const board = univer.createUnit(UniverInstanceType.UNIVER_BOARD, { + id: 'board-resource', + resources: [ + { name: pluginName, data: '{"kind":"loaded"}' }, + ], + }); + + expect(loads).toContainEqual(['board-resource', 'loaded']); + expect(resourceLoaderService.saveUnit('board-resource')?.resources).toEqual([ + { name: pluginName, data: '{"kind":"saved:board-resource"}' }, + ]); + + expect(univerInstanceService.disposeUnit(board.getUnitId())).toBe(true); + expect(unloads).toEqual(['board-resource']); + }); + + it('should load and unload base resources through the real unit lifecycle', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const resourceLoaderService = injector.get(IResourceLoaderService); + const univerInstanceService = injector.get(IUniverInstanceService); + const pluginName = 'BASE_TEST_PLUGIN' as never; + const loads: Array<[string, string]> = []; + const unloads: string[] = []; + + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BASE, MockBaseUnit); + resourceManagerService.registerPluginResource<{ kind: string }>({ + pluginName, + businesses: [UniverInstanceType.UNIVER_BASE], + onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), + onUnLoad: (unitId) => unloads.push(unitId), + toJson: (unitId) => JSON.stringify({ kind: `saved:${unitId}` }), + parseJson: (bytes) => JSON.parse(bytes), + }); + + const base = univer.createUnit(UniverInstanceType.UNIVER_BASE, { + id: 'base-resource', + resources: [ + { name: pluginName, data: '{"kind":"loaded"}' }, + ], + }); + + expect(loads).toEqual([['base-resource', 'loaded']]); + expect(resourceLoaderService.saveUnit('base-resource')?.resources).toEqual([ + { name: pluginName, data: '{"kind":"saved:base-resource"}' }, + ]); + + expect(univerInstanceService.disposeUnit(base.getUnitId())).toBe(true); + expect(unloads).toEqual(['base-resource']); + }); + + it('should load resources for existing board units when hooks register later', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const univerInstanceService = injector.get(IUniverInstanceService); + const pluginName = 'BOARD_LATE_PLUGIN' as never; + const loads: Array<[string, string]> = []; + + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BOARD, MockBoardUnit); + univer.createUnit(UniverInstanceType.UNIVER_BOARD, { + id: 'board-late-resource', + resources: [ + { name: pluginName, data: '{"kind":"late"}' }, + ], + }); + + resourceManagerService.registerPluginResource<{ kind: string }>({ + pluginName, + businesses: [UniverInstanceType.UNIVER_BOARD], + onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), + onUnLoad: () => undefined, + toJson: () => '{}', + parseJson: (bytes) => JSON.parse(bytes), + }); + + expect(loads).toEqual([['board-late-resource', 'late']]); + }); + + it('should load array-shaped slide plugin resources through the unit lifecycle', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const univerInstanceService = injector.get(IUniverInstanceService); + const pluginName = 'SLIDE_ARRAY_PLUGIN' as never; + const loads: Array<[string, string]> = []; + + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); + resourceManagerService.registerPluginResource<{ kind: string }>({ + pluginName, + businesses: [UniverInstanceType.UNIVER_SLIDE], + onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), + onUnLoad: () => undefined, + toJson: () => '{}', + parseJson: (bytes) => JSON.parse(bytes), + }); + + univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { + id: 'slide-array-resource', + resources: [ + { name: pluginName, data: '{"kind":"array"}' }, + ], + }); + + expect(loads).toEqual([['slide-array-resource', 'array']]); + }); + + it('should load serialized object slide plugin resources through the unit lifecycle', () => { + const injector = univer.__getInjector(); + const resourceManagerService = injector.get(IResourceManagerService); + const univerInstanceService = injector.get(IUniverInstanceService); + const pluginName = 'SLIDE_SERIALIZED_OBJECT_PLUGIN' as never; + const loads: Array<[string, string]> = []; + + univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, MockSlideUnit as never); + resourceManagerService.registerPluginResource<{ kind: string }>({ + pluginName, + businesses: [UniverInstanceType.UNIVER_SLIDE], + onLoad: (unitId, resource) => loads.push([unitId, resource.kind]), + onUnLoad: () => undefined, + toJson: () => '{}', + parseJson: (bytes) => JSON.parse(bytes), + }); + + univer.createUnit(UniverInstanceType.UNIVER_SLIDE, { + id: 'slide-serialized-object-resource', + resources: [ + { name: pluginName, data: '{"kind":"serialized-object"}' }, + ], + }); + + expect(loads).toEqual([['slide-serialized-object-resource', 'serialized-object']]); + }); + + it('should load empty persisted resource payloads when hooks are registered later', () => { + const resourceManagerService = univer.__getInjector().get(IResourceManagerService); + const onLoad = vi.fn(); + + univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData('doc-empty-resource', [ + { name: 'DOC_EMPTY_PLUGIN', data: '' }, + ])); + + resourceManagerService.registerPluginResource({ + pluginName: 'DOC_EMPTY_PLUGIN', + businesses: [UniverInstanceType.UNIVER_DOC], + onLoad, + onUnLoad: () => undefined, + toJson: () => '{}', + parseJson: (bytes) => bytes ? JSON.parse(bytes) : {}, + }); + + expect(onLoad).toHaveBeenCalledWith('doc-empty-resource', {}); + }); + + it('should ignore malformed persisted resource payloads when hooks are registered later', () => { + const resourceManagerService = univer.__getInjector().get(IResourceManagerService); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const onLoad = vi.fn(); + + univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData('doc-bad-resource', [ + { name: 'DOC_BAD_PLUGIN', data: '{bad json}' }, + ])); + + resourceManagerService.registerPluginResource({ + pluginName: 'DOC_BAD_PLUGIN', + businesses: [UniverInstanceType.UNIVER_DOC], + onLoad, + onUnLoad: () => undefined, + toJson: () => '{}', + parseJson: (bytes) => JSON.parse(bytes), + }); + + expect(onLoad).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith('Load Document{doc-bad-resource} Resources{DOC_BAD_PLUGIN} Data Error.'); + + errorSpy.mockRestore(); }); }); diff --git a/packages/core/src/services/theme/__tests__/theme.service.spec.ts b/packages/core/src/services/theme/__tests__/theme.service.spec.ts index e717a12053..b220b03e06 100644 --- a/packages/core/src/services/theme/__tests__/theme.service.spec.ts +++ b/packages/core/src/services/theme/__tests__/theme.service.spec.ts @@ -14,43 +14,90 @@ * limitations under the License. */ -import { defaultTheme } from '@univerjs/themes'; +import type { Univer } from '../../../univer'; import { beforeEach, describe, expect, it } from 'vitest'; -import { Injector } from '../../../common/di'; +import { createTestBed } from '../../__tests__/create-test-bed'; import { ThemeService } from '../theme.service'; describe('ThemeService', () => { - let service: ThemeService; + let univer: Univer; beforeEach(() => { - const injector = new Injector(); - injector.add([ThemeService]); - service = injector.get(ThemeService); + univer?.dispose(); + const instance = createTestBed(); + univer = instance.univer; }); - it('publishes theme and dark-mode changes used by UI renderers', () => { - const darkModes: boolean[] = []; - const primaryColors: string[] = []; - service.darkMode$.subscribe((darkMode) => darkModes.push(darkMode)); - service.currentTheme$.subscribe((theme) => primaryColors.push(theme.primary[600])); + it('should get default theme', () => { + const themeService = univer.__getInjector().get(ThemeService); + expect(themeService.getCurrentTheme()).toBeDefined(); + }); - service.setDarkMode(true); - service.setTheme({ - ...defaultTheme, + it('should set and get theme', () => { + const themeService = univer.__getInjector().get(ThemeService); + const oldTheme = themeService.getCurrentTheme(); + const theme = { + ...oldTheme, primary: { - ...defaultTheme.primary, + ...oldTheme.primary, 600: '#123456', }, - }); - - expect(darkModes).toEqual([false, true]); - expect(primaryColors).toEqual([defaultTheme.primary[600], '#123456']); - expect(service.getColorFromTheme('primary.600')).toBe('#123456'); + }; + themeService.setTheme(theme); + expect(themeService.getCurrentTheme().primary[600]).toBe('#123456'); }); - it('validates only theme color tokens that exist in the active theme shape', () => { - expect(service.isValidThemeColor('primary.600')).toBe(true); - expect(service.isValidThemeColor('missing.600')).toBe(false); - expect(service.isValidThemeColor('primary.999')).toBe(false); + it('should set and get dark mode', () => { + const themeService = univer.__getInjector().get(ThemeService); + themeService.setDarkMode(true); + expect(themeService.darkMode).toBe(true); + themeService.setDarkMode(false); + expect(themeService.darkMode).toBe(false); + }); + + it('should validate theme color', () => { + const themeService = univer.__getInjector().get(ThemeService); + expect(themeService.isValidThemeColor('primary.600')).toBe(true); + expect(themeService.isValidThemeColor('notexist')).toBe(false); + }); + + it('should get color from theme', () => { + const themeService = univer.__getInjector().get(ThemeService); + const oldTheme = themeService.getCurrentTheme(); + const theme = { + ...oldTheme, + primary: { + ...oldTheme.primary, + 600: '#abcdef', + }, + }; + themeService.setTheme(theme); + expect(themeService.getColorFromTheme('primary.600')).toBe('#abcdef'); + }); + + it('should get semantic highlight background token from theme', () => { + const themeService = univer.__getInjector().get(ThemeService); + const token = themeService.getColorFromTheme<{ color: string; alpha: number }>('highlight.background.1'); + + expect(token).toEqual({ color: 'purple.500', alpha: 0.3 }); + expect(themeService.getColorFromTheme(token.color)).toBe('#9061F9'); + }); + + it('should tap cached theme color', () => { + const themeService = univer.__getInjector().get(ThemeService); + const oldTheme = themeService.getCurrentTheme(); + const theme = { + ...oldTheme, + primary: { + ...oldTheme.primary, + 600: '#abcdef', + }, + }; + themeService.setTheme(theme); + + expect(themeService.isValidThemeColor('primary.600')).toBe(true); + expect(themeService.isValidThemeColor('primary.600')).toBe(true); // Should hit the cache + expect(themeService.getColorFromTheme('primary.600')).toBe('#abcdef'); + expect(themeService.getColorFromTheme('primary.600')).toBe('#abcdef'); // Should hit the cache }); }); diff --git a/packages/core/src/shared/__tests__/after-init-apply.spec.ts b/packages/core/src/shared/__tests__/after-init-apply.spec.ts new file mode 100644 index 0000000000..0a75eb16be --- /dev/null +++ b/packages/core/src/shared/__tests__/after-init-apply.spec.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { Injector } from '../../common/di'; +import { CommandService, CommandType, ICommandService } from '../../services/command/command.service'; +import { ConfigService, IConfigService } from '../../services/config/config.service'; +import { ContextService, IContextService } from '../../services/context/context.service'; +import { DesktopLogService, ILogService } from '../../services/log/log.service'; +import { afterInitApply } from '../after-init-apply'; + +function createCommandInjector(): Injector { + return new Injector([ + [ICommandService, { useClass: CommandService }], + [ILogService, { useClass: DesktopLogService }], + [IContextService, { useClass: ContextService }], + [IConfigService, { useClass: ConfigService }], + ]); +} + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe('afterInitApply', () => { + it('resolves after a mutation executes before the fallback timer', async () => { + vi.useFakeTimers(); + const injector = createCommandInjector(); + const commandService = injector.get(ICommandService); + + commandService.registerCommand({ + id: 'after-init-apply.mutation', + type: CommandType.MUTATION, + handler: () => true, + }); + + const pending = afterInitApply(commandService); + await commandService.executeCommand('after-init-apply.mutation'); + await vi.advanceTimersByTimeAsync(16); + + await expect(pending).resolves.toBeUndefined(); + injector.dispose(); + }); + + it('resolves through the fallback timer when no mutation executes', async () => { + vi.useFakeTimers(); + const injector = createCommandInjector(); + const commandService = injector.get(ICommandService); + + const pending = afterInitApply(commandService); + await vi.advanceTimersByTimeAsync(320); + + await expect(pending).resolves.toBeUndefined(); + injector.dispose(); + }); +}); diff --git a/packages/sheets-sort/src/commands/commands/__tests__/create-command-test-bed.ts b/packages/sheets-sort/src/commands/commands/__tests__/create-command-test-bed.ts index 1c77553fe2..10c04b0ab2 100644 --- a/packages/sheets-sort/src/commands/commands/__tests__/create-command-test-bed.ts +++ b/packages/sheets-sort/src/commands/commands/__tests__/create-command-test-bed.ts @@ -15,7 +15,19 @@ */ import type { Dependency, IWorkbookData, Workbook } from '@univerjs/core'; -import { BooleanNumber, ILogService, Inject, Injector, IUniverInstanceService, LocaleType, LogLevel, Plugin, Tools, Univer, UniverInstanceType } from '@univerjs/core'; +import { + BooleanNumber, + ILogService, + Inject, + Injector, + IUniverInstanceService, + LocaleType, + LogLevel, + Plugin, + Tools, + Univer, + UniverInstanceType, +} from '@univerjs/core'; import { FormulaDataModel } from '@univerjs/engine-formula'; import { SheetInterceptorService, SheetSkeletonService, SheetsSelectionsService } from '@univerjs/sheets'; import { SheetsSortController } from '../../../controllers/sheets-sort.controller'; diff --git a/packages/sheets-ui/src/services/__tests__/editor-bridge.service.spec.ts b/packages/sheets-ui/src/services/__tests__/editor-bridge.service.spec.ts index 307217839f..495c34dc8b 100644 --- a/packages/sheets-ui/src/services/__tests__/editor-bridge.service.spec.ts +++ b/packages/sheets-ui/src/services/__tests__/editor-bridge.service.spec.ts @@ -14,22 +14,6 @@ * limitations under the License. */ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { DOCS_NORMAL_EDITOR_UNIT_ID_KEY, IContextService, diff --git a/packages/sheets-ui/src/services/auto-height.service.ts b/packages/sheets-ui/src/services/auto-height.service.ts index 58b2f79437..0681677da2 100644 --- a/packages/sheets-ui/src/services/auto-height.service.ts +++ b/packages/sheets-ui/src/services/auto-height.service.ts @@ -16,9 +16,16 @@ import type { IRange, Workbook } from '@univerjs/core'; import type { IRenderContext, IRenderModule, SpreadsheetSkeleton } from '@univerjs/engine-render'; -import type { ICancelMarkDirtyRowAutoHeightOperationParams, IMarkDirtyRowAutoHeightOperationParams } from '@univerjs/sheets'; +import type { + ICancelMarkDirtyRowAutoHeightOperationParams, + IMarkDirtyRowAutoHeightOperationParams, +} from '@univerjs/sheets'; import { createIdentifier, Disposable, ICommandService, Inject, Rectangle } from '@univerjs/core'; -import { CancelMarkDirtyRowAutoHeightOperation, MarkDirtyRowAutoHeightOperation, SetWorksheetRowAutoHeightMutation } from '@univerjs/sheets'; +import { + CancelMarkDirtyRowAutoHeightOperation, + MarkDirtyRowAutoHeightOperation, + SetWorksheetRowAutoHeightMutation, +} from '@univerjs/sheets'; import { SheetSkeletonManagerService } from './sheet-skeleton-manager.service'; export interface IAutoHeightTask { diff --git a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts index f40a737dae..5a7a4db03b 100644 --- a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts +++ b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts @@ -14,14 +14,41 @@ * limitations under the License. */ -import type { DrawingTypeEnum, ICommandInfo, IDisposable, INeedCheckDisposable, Injector, IRange, Nullable, Workbook, Worksheet } from '@univerjs/core'; +import type { + DrawingTypeEnum, + ICommandInfo, + IDisposable, + INeedCheckDisposable, + Injector, + IRange, + Nullable, + Workbook, + Worksheet, +} from '@univerjs/core'; import type { BaseObject, IBoundRectNoAngle, IRender, IShapeProps, Shape, SpreadsheetSkeleton, Viewport } from '@univerjs/engine-render'; import type { ISetWorksheetRowAutoHeightMutationParams, ISheetLocationBase } from '@univerjs/sheets'; import type { IPopup } from '@univerjs/ui'; import type { Observable } from 'rxjs'; -import { Disposable, DisposableCollection, fromEventSubject, ICommandService, Inject, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; +import { + Disposable, + DisposableCollection, + fromEventSubject, + ICommandService, + Inject, + IUniverInstanceService, + toDisposable, + UniverInstanceType, +} from '@univerjs/core'; import { IRenderManagerService, RENDER_CLASS_TYPE } from '@univerjs/engine-render'; -import { COMMAND_LISTENER_SKELETON_CHANGE, IRefSelectionsService, RefRangeService, SetFrozenMutation, SetSelectionsOperation, SetWorksheetRowAutoHeightMutation, SheetsSelectionsService } from '@univerjs/sheets'; +import { + COMMAND_LISTENER_SKELETON_CHANGE, + IRefSelectionsService, + RefRangeService, + SetFrozenMutation, + SetSelectionsOperation, + SetWorksheetRowAutoHeightMutation, + SheetsSelectionsService, +} from '@univerjs/sheets'; import { ICanvasPopupService } from '@univerjs/ui'; import { BehaviorSubject, map, throttleTime } from 'rxjs'; import { SetScrollOperation } from '../commands/operations/scroll.operation'; diff --git a/packages/sheets-ui/src/services/cell-dropdown-manager.service.ts b/packages/sheets-ui/src/services/cell-dropdown-manager.service.ts index eceb28f3cd..b061cabb4b 100644 --- a/packages/sheets-ui/src/services/cell-dropdown-manager.service.ts +++ b/packages/sheets-ui/src/services/cell-dropdown-manager.service.ts @@ -17,7 +17,13 @@ import type { IDisposable } from '@univerjs/core'; import type { ISheetLocation } from '@univerjs/sheets'; import type { ICellDropdown } from '../views/dropdown'; -import { createIdentifier, Disposable, DisposableCollection, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, Inject } from '@univerjs/core'; +import { + createIdentifier, + Disposable, + DisposableCollection, + DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, + Inject, +} from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; import { ComponentManager } from '@univerjs/ui'; import { dropdownMap } from '../views/dropdown'; diff --git a/packages/sheets-ui/src/services/editor-bridge.service.ts b/packages/sheets-ui/src/services/editor-bridge.service.ts index 2b517c802f..0e304eb6a5 100644 --- a/packages/sheets-ui/src/services/editor-bridge.service.ts +++ b/packages/sheets-ui/src/services/editor-bridge.service.ts @@ -34,8 +34,19 @@ import { UniverInstanceType, } from '@univerjs/core'; import { getCanvasOffsetByEngine, IEditorService } from '@univerjs/docs-ui'; -import { convertTextRotation, convertTransformToOffsetX, convertTransformToOffsetY, DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render'; -import { attachPrimaryWithCoord, BEFORE_CELL_EDIT, SheetInterceptorService, SheetSkeletonService } from '@univerjs/sheets'; +import { + convertTextRotation, + convertTransformToOffsetX, + convertTransformToOffsetY, + DeviceInputEventType, + IRenderManagerService, +} from '@univerjs/engine-render'; +import { + attachPrimaryWithCoord, + BEFORE_CELL_EDIT, + SheetInterceptorService, + SheetSkeletonService, +} from '@univerjs/sheets'; import { BehaviorSubject, map, switchMap } from 'rxjs'; import { ISheetSelectionRenderService } from './selection/base-selection-render.service'; diff --git a/packages/sheets-ui/src/services/hover-manager.service.ts b/packages/sheets-ui/src/services/hover-manager.service.ts index 9ea176e99d..c9627a95cd 100644 --- a/packages/sheets-ui/src/services/hover-manager.service.ts +++ b/packages/sheets-ui/src/services/hover-manager.service.ts @@ -15,7 +15,13 @@ */ import type { ICustomRange, IParagraph, IPosition, Nullable, Workbook, Worksheet } from '@univerjs/core'; -import type { IBoundRectNoAngle, IDocumentSkeletonDrawing, IMouseEvent, IPointerEvent, IRender } from '@univerjs/engine-render'; +import type { + IBoundRectNoAngle, + IDocumentSkeletonDrawing, + IMouseEvent, + IPointerEvent, + IRender, +} from '@univerjs/engine-render'; import type { ISheetLocation, ISheetLocationBase, ISheetSkeletonManagerParam } from '@univerjs/sheets'; import { CellValueType, Disposable, isRealNum, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { IRenderManagerService, SHEET_VIEWPORT_KEY, Vector2 } from '@univerjs/engine-render'; diff --git a/packages/sheets-ui/src/services/sheet-skeleton-manager.service.ts b/packages/sheets-ui/src/services/sheet-skeleton-manager.service.ts index cf3556c49e..d0f7ef695c 100644 --- a/packages/sheets-ui/src/services/sheet-skeleton-manager.service.ts +++ b/packages/sheets-ui/src/services/sheet-skeleton-manager.service.ts @@ -21,7 +21,10 @@ import { Disposable, Inject } from '@univerjs/core'; import { SHEET_VIEWPORT_KEY } from '@univerjs/engine-render'; import { SheetSkeletonService, SheetsSelectionsService } from '@univerjs/sheets'; import { BehaviorSubject } from 'rxjs'; -import { SetColumnHeaderHeightCommand, SetRowHeaderWidthCommand } from '../commands/commands/headersize-changed.command'; +import { + SetColumnHeaderHeightCommand, + SetRowHeaderWidthCommand, +} from '../commands/commands/headersize-changed.command'; import { ISheetSelectionRenderService } from './selection/base-selection-render.service'; export interface ISheetSkeletonManagerSearch { diff --git a/packages/sheets-ui/src/services/utils/defined-name-utils.ts b/packages/sheets-ui/src/services/utils/defined-name-utils.ts index bf775fc139..f2336d93ed 100644 --- a/packages/sheets-ui/src/services/utils/defined-name-utils.ts +++ b/packages/sheets-ui/src/services/utils/defined-name-utils.ts @@ -15,7 +15,13 @@ */ import type { IUniverInstanceService, Workbook } from '@univerjs/core'; -import type { IDefinedNamesService, IDefinedNamesServiceParam, IFunctionService, ISuperTableService, LexerTreeBuilder } from '@univerjs/engine-formula'; +import type { + IDefinedNamesService, + IDefinedNamesServiceParam, + IFunctionService, + ISuperTableService, + LexerTreeBuilder, +} from '@univerjs/engine-formula'; import type { ISelectionWithStyle } from '@univerjs/sheets'; import { AbsoluteRefType } from '@univerjs/core'; import { isReferenceStringWithEffectiveColumn, serializeRangeWithSheet } from '@univerjs/engine-formula'; diff --git a/packages/sheets-ui/src/services/utils/doc-skeleton-util.ts b/packages/sheets-ui/src/services/utils/doc-skeleton-util.ts index fa198ffc1e..7ff867f29c 100644 --- a/packages/sheets-ui/src/services/utils/doc-skeleton-util.ts +++ b/packages/sheets-ui/src/services/utils/doc-skeleton-util.ts @@ -15,8 +15,20 @@ */ import type { ICellWithCoord, ICustomRange, Injector, IParagraph, ITextRangeParam, Workbook } from '@univerjs/core'; -import type { DocumentSkeleton, IBoundRectNoAngle, IDocumentSkeletonGlyph, IFontCacheItem } from '@univerjs/engine-render'; -import { CustomRangeType, HorizontalAlign, IUniverInstanceService, PresetListType, UniverInstanceType, VerticalAlign } from '@univerjs/core'; +import type { + DocumentSkeleton, + IBoundRectNoAngle, + IDocumentSkeletonGlyph, + IFontCacheItem, +} from '@univerjs/engine-render'; +import { + CustomRangeType, + HorizontalAlign, + IUniverInstanceService, + PresetListType, + UniverInstanceType, + VerticalAlign, +} from '@univerjs/core'; import { DocSkeletonManagerService } from '@univerjs/docs'; import { DOC_VERTICAL_PADDING, getLineBounding, NodePositionConvertToCursor } from '@univerjs/docs-ui'; import { IRenderManagerService } from '@univerjs/engine-render'; diff --git a/packages/ui/src/services/shortcut/__tests__/keycode.spec.ts b/packages/ui/src/services/shortcut/__tests__/keycode.spec.ts deleted file mode 100644 index 04dd38d8ac..0000000000 --- a/packages/ui/src/services/shortcut/__tests__/keycode.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { describe, expect, it } from 'vitest'; -import { KeyCode, KeyCodeToChar, MetaKeys } from '../keycode'; - -describe('keycode mappings', () => { - it('should map common control keys to labels', () => { - expect(KeyCodeToChar[KeyCode.BACKSPACE]).toBe('Backspace'); - expect(KeyCodeToChar[KeyCode.ENTER]).toBe('Enter'); - expect(KeyCodeToChar[KeyCode.DELETE]).toBe('Del'); - expect(KeyCodeToChar[KeyCode.ESC]).toBe('Esc'); - expect(KeyCodeToChar[KeyCode.SPACE]).toBe('Space'); - expect(KeyCodeToChar[KeyCode.HOME]).toBe('Home'); - expect(KeyCodeToChar[KeyCode.END]).toBe('End'); - }); - - it('should map alphanumeric and function keys', () => { - expect(KeyCodeToChar[KeyCode.Digit0]).toBe('0'); - expect(KeyCodeToChar[KeyCode.Digit9]).toBe('9'); - expect(KeyCodeToChar[KeyCode.A]).toBe('A'); - expect(KeyCodeToChar[KeyCode.Z]).toBe('Z'); - expect(KeyCodeToChar[KeyCode.F1]).toBe('F1'); - expect(KeyCodeToChar[KeyCode.F12]).toBe('F12'); - }); - - it('should map punctuation and arrow keys', () => { - expect(KeyCodeToChar[KeyCode.MINUS]).toBe('-'); - expect(KeyCodeToChar[KeyCode.EQUAL]).toBe('='); - expect(KeyCodeToChar[KeyCode.PERIOD]).toBe('.'); - expect(KeyCodeToChar[KeyCode.COMMA]).toBe(','); - expect(KeyCodeToChar[KeyCode.BACK_SLASH]).toBe('\\'); - expect(KeyCodeToChar[KeyCode.ARROW_LEFT]).toBe('←'); - expect(KeyCodeToChar[KeyCode.ARROW_RIGHT]).toBe('→'); - expect(KeyCodeToChar[KeyCode.ARROW_UP]).toBe('↑'); - expect(KeyCodeToChar[KeyCode.ARROW_DOWN]).toBe('↓'); - }); - - it('should return undefined for unknown key code', () => { - expect(KeyCodeToChar[KeyCode.UNKNOWN]).toBeUndefined(); - }); - - it('should define non-overlapping meta key bitmasks', () => { - expect(MetaKeys.SHIFT & MetaKeys.ALT).toBe(0); - expect(MetaKeys.ALT & MetaKeys.CTRL_COMMAND).toBe(0); - expect(MetaKeys.CTRL_COMMAND & MetaKeys.MAC_CTRL).toBe(0); - }); -}); diff --git a/packages/ui/src/services/shortcut/__tests__/shortcut.service.spec.ts b/packages/ui/src/services/shortcut/__tests__/shortcut.service.spec.ts index c76acd08be..73064a1c12 100644 --- a/packages/ui/src/services/shortcut/__tests__/shortcut.service.spec.ts +++ b/packages/ui/src/services/shortcut/__tests__/shortcut.service.spec.ts @@ -14,22 +14,6 @@ * limitations under the License. */ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /** * @vitest-environment jsdom */