snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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 {
|
||||
CommandService,
|
||||
ConfigService,
|
||||
ContextService,
|
||||
DesktopLogService,
|
||||
DocumentDataModel,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LocalUndoRedoService,
|
||||
UniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DocStateChangeManagerService, IDocStateChangeInterceptorService } from '../doc-state-change-manager.service';
|
||||
import { DocStateEmitService } from '../doc-state-emit.service';
|
||||
|
||||
class DropDocStateChangeInterceptorService {
|
||||
transformChangeStateInfo() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createService(intercept = false) {
|
||||
const injector = new Injector();
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([IUniverInstanceService, { useClass: UniverInstanceService }]);
|
||||
injector.add([IUndoRedoService, { useClass: LocalUndoRedoService }]);
|
||||
injector.add([DocStateEmitService]);
|
||||
if (intercept) {
|
||||
injector.add([IDocStateChangeInterceptorService, { useClass: DropDocStateChangeInterceptorService }]);
|
||||
}
|
||||
injector.add([DocStateChangeManagerService]);
|
||||
return {
|
||||
service: injector.get(DocStateChangeManagerService),
|
||||
emitter: injector.get(DocStateEmitService),
|
||||
undoRedoService: injector.get(IUndoRedoService),
|
||||
univerInstanceService: injector.get(IUniverInstanceService) as UniverInstanceService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('DocStateChangeManagerService', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function createChange(overrides = {}) {
|
||||
return {
|
||||
commandId: 'doc.rich-text-editing',
|
||||
unitId: 'doc-1',
|
||||
trigger: 'keyboard',
|
||||
redoState: { actions: null as never, textRanges: [{ startOffset: 1, endOffset: 1, collapsed: true }] },
|
||||
undoState: { actions: null as never, textRanges: [{ startOffset: 0, endOffset: 0, collapsed: true }] },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
it('turns emitted document edits into history items and collaboration change events', () => {
|
||||
const { service, emitter, undoRedoService, univerInstanceService } = createService();
|
||||
const changes: unknown[] = [];
|
||||
const sub = service.docStateChange$.subscribe((value) => changes.push(value));
|
||||
univerInstanceService.__addUnit(new DocumentDataModel({ id: 'doc-1' }));
|
||||
univerInstanceService.focusUnit('doc-1');
|
||||
const change = createChange();
|
||||
|
||||
emitter.emitStateChangeInfo(change);
|
||||
|
||||
expect(undoRedoService.pitchTopUndoElement()).toMatchObject({
|
||||
unitID: 'doc-1',
|
||||
undoMutations: [{ id: 'doc.rich-text-editing' }],
|
||||
redoMutations: [{ id: 'doc.rich-text-editing' }],
|
||||
});
|
||||
expect(changes.at(-1)).toMatchObject({
|
||||
commandId: 'doc.rich-text-editing',
|
||||
unitId: 'doc-1',
|
||||
trigger: 'keyboard',
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('filters sync or intercepted state changes without changing caches', () => {
|
||||
const synced = createService();
|
||||
synced.emitter.emitStateChangeInfo(createChange({ isSync: true }));
|
||||
expect(synced.service.getStateCache('doc-1')).toEqual({ history: [], collaboration: [] });
|
||||
|
||||
const intercepted = createService(true);
|
||||
intercepted.emitter.emitStateChangeInfo(createChange());
|
||||
expect(intercepted.service.getStateCache('doc-1')).toEqual({ history: [], collaboration: [] });
|
||||
});
|
||||
|
||||
it('keeps no-history changes out of undo and collaboration streams', () => {
|
||||
const { service, emitter, undoRedoService } = createService();
|
||||
const changes: unknown[] = [];
|
||||
const sub = service.docStateChange$.subscribe((value) => changes.push(value));
|
||||
|
||||
emitter.emitStateChangeInfo(createChange({ noHistory: true }));
|
||||
|
||||
expect(undoRedoService.pitchTopUndoElement()).toBeNull();
|
||||
expect(changes.at(-1)).toBeNull();
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('debounces history and collaboration flushing for composed edits', () => {
|
||||
vi.useFakeTimers();
|
||||
const { service, emitter } = createService();
|
||||
const changes: unknown[] = [];
|
||||
const sub = service.docStateChange$.subscribe((value) => changes.push(value));
|
||||
|
||||
emitter.emitStateChangeInfo(createChange({ debounce: true }));
|
||||
expect(service.getStateCache('doc-1').history).toHaveLength(1);
|
||||
expect(changes.at(-1)).toBeNull();
|
||||
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
expect(service.getStateCache('doc-1')).toEqual({ history: [], collaboration: [] });
|
||||
expect(changes.at(-1)).toMatchObject({ unitId: 'doc-1', debounce: true });
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('preserves segment and whole-document selection state in undo and redo mutations', () => {
|
||||
const { emitter, undoRedoService, univerInstanceService } = createService();
|
||||
univerInstanceService.__addUnit(new DocumentDataModel({ id: 'doc-1' }));
|
||||
univerInstanceService.focusUnit('doc-1');
|
||||
|
||||
emitter.emitStateChangeInfo(createChange({
|
||||
segmentId: 'header-1',
|
||||
redoState: {
|
||||
actions: null as never,
|
||||
textRanges: [{ startOffset: 2, endOffset: 2, collapsed: true }],
|
||||
options: { wholeDocument: false },
|
||||
isEditing: true,
|
||||
},
|
||||
undoState: {
|
||||
actions: null as never,
|
||||
textRanges: [{ startOffset: 0, endOffset: 4, collapsed: false }],
|
||||
options: { wholeDocument: true },
|
||||
isEditing: false,
|
||||
},
|
||||
}));
|
||||
|
||||
expect(undoRedoService.pitchTopUndoElement()).toMatchObject({
|
||||
undoMutations: [{
|
||||
params: {
|
||||
segmentId: 'header-1',
|
||||
options: { wholeDocument: true },
|
||||
isEditing: false,
|
||||
},
|
||||
}],
|
||||
redoMutations: [{
|
||||
params: {
|
||||
segmentId: 'header-1',
|
||||
options: { wholeDocument: false },
|
||||
isEditing: true,
|
||||
},
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
it('flushes debounced history before switching document segments', () => {
|
||||
vi.useFakeTimers();
|
||||
const { service, emitter, undoRedoService, univerInstanceService } = createService();
|
||||
univerInstanceService.__addUnit(new DocumentDataModel({ id: 'doc-1' }));
|
||||
univerInstanceService.focusUnit('doc-1');
|
||||
|
||||
emitter.emitStateChangeInfo(createChange({ segmentId: 'header-1', debounce: true }));
|
||||
emitter.emitStateChangeInfo(createChange({ segmentId: '', debounce: true }));
|
||||
|
||||
expect(undoRedoService.pitchTopUndoElement()).toMatchObject({
|
||||
undoMutations: [{ params: { segmentId: 'header-1' } }],
|
||||
redoMutations: [{ params: { segmentId: 'header-1' } }],
|
||||
});
|
||||
expect(service.getStateCache('doc-1').history).toEqual([
|
||||
expect.objectContaining({ segmentId: '' }),
|
||||
]);
|
||||
|
||||
vi.advanceTimersByTime(300);
|
||||
expect(service.getStateCache('doc-1')).toEqual({ history: [], collaboration: [] });
|
||||
});
|
||||
|
||||
it('can restore pre-existing state caches for a document', () => {
|
||||
const { service } = createService();
|
||||
const state = createChange();
|
||||
|
||||
service.setStateCache('doc-1', {
|
||||
history: [state],
|
||||
collaboration: [state],
|
||||
});
|
||||
|
||||
expect(service.getStateCache('doc-1')).toEqual({
|
||||
history: [state],
|
||||
collaboration: [state],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user