mirror of
https://github.com/dream-num/univer.git
synced 2026-09-19 02:18:42 +08:00
test: reorganize and deduplicate service tests (#7255)
This commit is contained in:
@@ -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<ITestBoardData, UniverInstanceType.UNIVER_BOARD> {
|
||||
override readonly type = UniverInstanceType.UNIVER_BOARD;
|
||||
override name$ = new BehaviorSubject('');
|
||||
private readonly _snapshot: ITestBoardData;
|
||||
|
||||
constructor(snapshot: Partial<ITestBoardData> = {}) {
|
||||
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<ITestBoardData, MockBoardUnit>(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<ITestBoardData>(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<ITestBoardData, MockBoardUnit>(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']);
|
||||
});
|
||||
});
|
||||
@@ -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<IPermissionPoint<boolean>>(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!');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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<IDocumentData['resources']>): Partial<IDocumentData> {
|
||||
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<ITestSlideData> {
|
||||
override type = UniverInstanceType.UNIVER_SLIDE;
|
||||
override name$ = new BehaviorSubject('');
|
||||
private readonly _snapshot: ITestSlideData;
|
||||
|
||||
constructor(snapshot: Partial<ITestSlideData> = {}) {
|
||||
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<ITestBoardData, UniverInstanceType.UNIVER_BOARD> {
|
||||
override readonly type = UniverInstanceType.UNIVER_BOARD;
|
||||
override name$ = new BehaviorSubject('');
|
||||
private readonly _snapshot: ITestBoardData;
|
||||
|
||||
constructor(snapshot: Partial<ITestBoardData> = {}) {
|
||||
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<string, unknown> = {};
|
||||
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<string, unknown> = {};
|
||||
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<IDocumentData>('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<ITestSlideData, MockSlideUnit>(UniverInstanceType.UNIVER_SLIDE, {
|
||||
id: 'slide-resource',
|
||||
resources: [
|
||||
{ name: pluginName, data: '{"kind":"loaded"}' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(loads).toContainEqual(['slide-resource', 'loaded']);
|
||||
expect(resourceLoaderService.saveUnit<ITestSlideData>('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<ITestSlideData, MockSlideUnit>(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<ITestBoardData, MockBoardUnit>(UniverInstanceType.UNIVER_BOARD, {
|
||||
id: 'board-resource',
|
||||
resources: [
|
||||
{ name: pluginName, data: '{"kind":"loaded"}' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(loads).toContainEqual(['board-resource', 'loaded']);
|
||||
expect(resourceLoaderService.saveUnit<ITestBoardData>('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<ITestBoardData, MockBoardUnit>(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<ITestSlideData, MockSlideUnit>(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<ITestSlideData, MockSlideUnit>(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();
|
||||
});
|
||||
});
|
||||
@@ -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<string>);
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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<ITestBoardData, UniverInstanceType.UNIVER_BOARD> {
|
||||
override readonly type = UniverInstanceType.UNIVER_BOARD;
|
||||
override name$ = new BehaviorSubject('');
|
||||
private readonly _snapshot: ITestBoardData;
|
||||
|
||||
constructor(snapshot: Partial<ITestBoardData> = {}) {
|
||||
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<ITestBoardData, MockBoardUnit>(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<ITestBoardData>(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<ITestBoardData, MockBoardUnit>(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 }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<IPermissionPoint<boolean>>(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!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+481
-109
@@ -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<IDocumentData['resources']>): Partial<IDocumentData> {
|
||||
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<ITestSlideData> {
|
||||
override type = UniverInstanceType.UNIVER_SLIDE;
|
||||
override name$ = new BehaviorSubject('');
|
||||
private readonly _snapshot: ITestSlideData;
|
||||
|
||||
constructor(snapshot: Partial<ITestSlideData> = {}) {
|
||||
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<ITestBoardData, UniverInstanceType.UNIVER_BOARD> {
|
||||
override readonly type = UniverInstanceType.UNIVER_BOARD;
|
||||
override name$ = new BehaviorSubject('');
|
||||
private readonly _snapshot: ITestBoardData;
|
||||
|
||||
constructor(snapshot: Partial<ITestBoardData> = {}) {
|
||||
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<ITestBaseData, UniverInstanceType.UNIVER_BASE> {
|
||||
override readonly type = UniverInstanceType.UNIVER_BASE;
|
||||
override name$ = new BehaviorSubject('');
|
||||
private readonly _snapshot: ITestBaseData;
|
||||
|
||||
constructor(snapshot: Partial<ITestBaseData> = {}) {
|
||||
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<never>;
|
||||
let sheetAdded$: Subject<unknown>;
|
||||
let docAdded$: Subject<unknown>;
|
||||
let slideAdded$: Subject<unknown>;
|
||||
let baseAdded$: Subject<unknown>;
|
||||
let sheetDisposed$: Subject<unknown>;
|
||||
let docDisposed$: Subject<unknown>;
|
||||
let baseDisposed$: Subject<unknown>;
|
||||
let slideDisposed$: Subject<unknown>;
|
||||
let resourceManagerService: {
|
||||
getAllResourceHooks: ReturnType<typeof vi.fn>;
|
||||
register$: Subject<never>;
|
||||
loadResources: ReturnType<typeof vi.fn>;
|
||||
unloadResources: ReturnType<typeof vi.fn>;
|
||||
getResources: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let univerInstanceService: {
|
||||
getAllUnitsForType: ReturnType<typeof vi.fn>;
|
||||
getTypeOfUnitAdded$: ReturnType<typeof vi.fn>;
|
||||
getTypeOfUnitDisposed$: ReturnType<typeof vi.fn>;
|
||||
getUnit: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
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<string, unknown> = {};
|
||||
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<string, unknown> = {};
|
||||
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<IDocumentData>('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<ITestSlideData, MockSlideUnit>(UniverInstanceType.UNIVER_SLIDE, {
|
||||
id: 'slide-resource',
|
||||
resources: [
|
||||
{ name: pluginName, data: '{"kind":"loaded"}' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(loads).toContainEqual(['slide-resource', 'loaded']);
|
||||
expect(resourceLoaderService.saveUnit<ITestSlideData>('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<ITestSlideData, MockSlideUnit>(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<ITestBoardData, MockBoardUnit>(UniverInstanceType.UNIVER_BOARD, {
|
||||
id: 'board-resource',
|
||||
resources: [
|
||||
{ name: pluginName, data: '{"kind":"loaded"}' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(loads).toContainEqual(['board-resource', 'loaded']);
|
||||
expect(resourceLoaderService.saveUnit<ITestBoardData>('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<ITestBaseData, MockBaseUnit>(UniverInstanceType.UNIVER_BASE, {
|
||||
id: 'base-resource',
|
||||
resources: [
|
||||
{ name: pluginName, data: '{"kind":"loaded"}' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(loads).toEqual([['base-resource', 'loaded']]);
|
||||
expect(resourceLoaderService.saveUnit<ITestBaseData>('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<ITestBoardData, MockBoardUnit>(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<ITestSlideData, MockSlideUnit>(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<ITestSlideData, MockSlideUnit>(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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user