Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,47 @@

import { BehaviorSubject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { CompleteRecordingActionCommand, StartRecordingActionCommand, StopRecordingActionCommand } from '../commands/commands/record.command';
import { ReplayLocalRecordCommand, ReplayLocalRecordOnActiveCommand, ReplayLocalRecordOnNamesakeCommand } from '../commands/commands/replay.command';
import { CloseRecordPanelOperation, OpenRecordPanelOperation } from '../commands/operations/operation';
import { menuSchema, OpenRecorderMenuItemFactory, RECORD_MENU_ITEM_ID, RecordMenuItemFactory, ReplayLocalRecordMenuItemFactory, ReplayLocalRecordOnActiveMenuItemFactory, ReplayLocalRecordOnNamesakeMenuItemFactory } from '../menu/action-recorder.menu';
import {
ReplayLocalRecordCommand,
ReplayLocalRecordOnActiveCommand,
ReplayLocalRecordOnNamesakeCommand,
} from '../commands/commands/replay.command';
import { OpenRecordPanelOperation } from '../commands/operations/operation';
import {
menuSchema,
OpenRecorderMenuItemFactory,
RECORD_MENU_ITEM_ID,
RecordMenuItemFactory,
ReplayLocalRecordMenuItemFactory,
ReplayLocalRecordOnActiveMenuItemFactory,
ReplayLocalRecordOnNamesakeMenuItemFactory,
} from '../menu/action-recorder.menu';
import { ActionRecorderController } from './action-recorder.controller';

describe('action-recorder controller/menu', () => {
it('should create menu items', () => {
const recordItem = RecordMenuItemFactory();
expect(recordItem.id).toBe(RECORD_MENU_ITEM_ID);
describe('action-recorder menu factories', () => {
it('RecordMenuItemFactory should produce a menu item with the record item id', () => {
const item = RecordMenuItemFactory();
expect(item.id).toBe(RECORD_MENU_ITEM_ID);
});

it('OpenRecorderMenuItemFactory should produce a menu item linked to the open-panel operation', () => {
const panelOpened$ = new BehaviorSubject(false);
const openItem = OpenRecorderMenuItemFactory({
const item = OpenRecorderMenuItemFactory({
get: vi.fn(() => ({ panelOpened$: panelOpened$.asObservable() })),
} as never);
expect(openItem.id).toBe(OpenRecordPanelOperation.id);
expect(openItem.disabled$).toBeDefined();

expect(item.id).toBe(OpenRecordPanelOperation.id);
});

it('replay menu factories should produce items with matching command ids', () => {
expect(ReplayLocalRecordMenuItemFactory().id).toBe(ReplayLocalRecordCommand.id);
expect(ReplayLocalRecordOnNamesakeMenuItemFactory().id).toBe(ReplayLocalRecordOnNamesakeCommand.id);
expect(ReplayLocalRecordOnActiveMenuItemFactory().id).toBe(ReplayLocalRecordOnActiveCommand.id);
expect(Object.keys(menuSchema).length).toBeGreaterThan(0);
});
});

it('should register commands/ui/menu and sheet-recorded commands', () => {
describe('ActionRecorderController', () => {
it('should register commands, UI components, icons and menu schema on construction', () => {
const registerCommand = vi.fn();
const registerComponent = vi.fn();
const mergeMenu = vi.fn();
Expand All @@ -56,24 +72,36 @@ describe('action-recorder controller/menu', () => {
{} as never
);

expect(registerCommand).toHaveBeenCalledWith(StartRecordingActionCommand);
expect(registerCommand).toHaveBeenCalledWith(StopRecordingActionCommand);
expect(registerCommand).toHaveBeenCalledWith(CompleteRecordingActionCommand);
expect(registerCommand).toHaveBeenCalledWith(OpenRecordPanelOperation);
expect(registerCommand).toHaveBeenCalledWith(CloseRecordPanelOperation);
expect(registerCommand).toHaveBeenCalledWith(ReplayLocalRecordCommand);
expect(registerCommand).toHaveBeenCalledWith(ReplayLocalRecordOnNamesakeCommand);
expect(registerCommand).toHaveBeenCalledWith(ReplayLocalRecordOnActiveCommand);

expect(registerCommand).toHaveBeenCalled();
expect(registerComponent).toHaveBeenCalledTimes(1);
const componentFactory = registerComponent.mock.calls[0][1] as () => unknown;
expect(componentFactory()).toBeDefined();
expect(registerIcon).toHaveBeenCalledWith('RecordIcon', expect.anything());
expect(mergeMenu).toHaveBeenCalledWith(menuSchema);

expect(registerRecordedCommand).toHaveBeenCalled();
expect(registerRecordedCommand.mock.calls.length).toBeGreaterThan(20);

controller.dispose();
});

it('should clean up resources on dispose', () => {
const disposables: Array<{ dispose: () => void }> = [];
const registerIcon = vi.fn(() => {
const d = { dispose: vi.fn() };
disposables.push(d);
return d;
});

const controller = new ActionRecorderController(
{ registerCommand: vi.fn() } as never,
{ registerComponent: vi.fn() } as never,
{ mergeMenu: vi.fn() } as never,
{ register: registerIcon } as never,
{ registerRecordedCommand: vi.fn() } as never,
{} as never
);

controller.dispose();

for (const d of disposables) {
expect(d.dispose).toHaveBeenCalled();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,73 @@ describe('ActionRecorderService', () => {
expect(panelStates[panelStates.length - 1]).toBe(false);
expect(recordingStates[recordingStates.length - 1]).toBe(false);
});

it('should handle replaceId gracefully when unit or sheet is not found', () => {
let commandCallback: ((commandInfo: { id: string; type: CommandType; params?: Record<string, unknown> }) => void) | undefined;
const onCommandExecuted = vi.fn((callback: typeof commandCallback) => {
commandCallback = callback;
return { dispose: vi.fn() };
});

const service = new ActionRecorderService(
{ onCommandExecuted } as never,
{ error: vi.fn() } as never,
{ downloadFile: vi.fn() } as never,
{
getFocusedUnit: vi.fn(() => ({ getUnitId: () => 'unit-1' })),
getUnit: vi.fn(() => ({
getSheetBySheetId: vi.fn(() => null),
})),
} as never
);

service.registerRecordedCommand({ id: 'cmd-1', type: CommandType.COMMAND } as never);

const recordedCommands: string[][] = [];
service.recordedCommands$.subscribe((v) => recordedCommands.push(v.map((cmd) => cmd.id)));

service.startRecording(true);
commandCallback?.({
id: 'cmd-1',
type: CommandType.COMMAND,
params: { unitId: 'unit-1', subUnitId: 'sheet-1' },
});

expect(recordedCommands[recordedCommands.length - 1]).toEqual(['cmd-1']);
});

it('should reset all recorded states when stopRecording is called directly', () => {
let commandCallback: ((commandInfo: { id: string; type: CommandType; params?: Record<string, unknown> }) => void) | undefined;
const recorderDisposable = { dispose: vi.fn() };
const onCommandExecuted = vi.fn((callback: typeof commandCallback) => {
commandCallback = callback;
return recorderDisposable;
});

const service = new ActionRecorderService(
{ onCommandExecuted } as never,
{ error: vi.fn() } as never,
{ downloadFile: vi.fn() } as never,
{
getFocusedUnit: vi.fn(() => ({ getUnitId: () => 'unit-1' })),
getUnit: vi.fn(),
} as never
);

service.registerRecordedCommand({ id: 'cmd-1', type: CommandType.COMMAND } as never);

const recordingStates: boolean[] = [];
const recordedCommands: string[][] = [];
service.recording$.subscribe((v) => recordingStates.push(v));
service.recordedCommands$.subscribe((v) => recordedCommands.push(v.map((cmd) => cmd.id)));

service.startRecording();
commandCallback?.({ id: 'cmd-1', type: CommandType.COMMAND, params: {} });
expect(recordedCommands[recordedCommands.length - 1]).toEqual(['cmd-1']);

service.stopRecording();
expect(recordingStates[recordingStates.length - 1]).toBe(false);
expect(recordedCommands[recordedCommands.length - 1]).toEqual([]);
expect(recorderDisposable.dispose).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,52 @@
*/

import { describe, expect, it, vi } from 'vitest';
import { QuickInsertButton } from '../../views/QuickInsertButton';
import { DocQuickInsertUIController } from '../doc-quick-insert-ui.controller';

describe('DocQuickInsertUIController', () => {
it('registers commands, components and the built-in slash popup', () => {
const registerCommand = vi.fn(() => ({ dispose: vi.fn() }));
const register = vi.fn(() => ({ dispose: vi.fn() }));
const unregisterPopup = vi.fn();
const registerPopup = vi.fn(() => unregisterPopup);
it('should register the slash popup with correct keyword and preconditions', () => {
const popups: any[] = [];
const registerPopup = vi.fn((popup) => {
popups.push(popup);
return () => {
const idx = popups.indexOf(popup);
if (idx > -1) popups.splice(idx, 1);
};
});

const controller = new DocQuickInsertUIController(
{ registerCommand } as never,
{ registerCommand: vi.fn(() => ({ dispose: vi.fn() })) } as never,
{ registerPopup } as never,
{ register } as never
{ register: vi.fn(() => ({ dispose: vi.fn() })) } as never
);

expect(registerCommand).toHaveBeenCalledTimes(3);
expect(register).toHaveBeenCalledWith(QuickInsertButton.componentKey, QuickInsertButton);
expect(registerPopup).toHaveBeenCalledTimes(1);

const firstRegisterPopupCall = registerPopup.mock.calls[0] as unknown[] | undefined;
expect(firstRegisterPopupCall).toBeDefined();

const slashPopup = firstRegisterPopupCall?.[0] as unknown as {
keyword: string;
preconditions: (params: { range: { startNodePosition?: { glyph?: number } } }) => boolean;
};
expect(slashPopup.keyword).toBe('/');
const slashPopup = popups.find((p) => p.keyword === '/');
expect(slashPopup).toBeTruthy();
expect(slashPopup.preconditions({ range: { startNodePosition: { glyph: 0 } } })).toBe(true);
expect(slashPopup.preconditions({ range: { startNodePosition: { glyph: 2 } } })).toBe(false);

controller.dispose();
expect(unregisterPopup).toHaveBeenCalledTimes(1);
});

it('should clean up registered popups on dispose', () => {
const unregisterFns: Array<() => void> = [];
const registerPopup = vi.fn(() => {
const fn = vi.fn();
unregisterFns.push(fn);
return fn;
});

const controller = new DocQuickInsertUIController(
{ registerCommand: vi.fn(() => ({ dispose: vi.fn() })) } as never,
{ registerPopup } as never,
{ register: vi.fn(() => ({ dispose: vi.fn() })) } as never
);

expect(unregisterFns.length).toBeGreaterThan(0);
controller.dispose();

for (const fn of unregisterFns) {
expect(fn).toHaveBeenCalledTimes(1);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,40 @@ describe('DocQuickInsertPopupService', () => {

service.dispose();
});

it('should safely close popup when no popup is active', () => {
const { service } = createServiceTestBed();
expect(() => service.closePopup()).not.toThrow();
expect(service.editPopup).toBeUndefined();
});

it('should return early when paragraph bound cannot be found', () => {
const { service, popupEntries } = createServiceTestBed();
const popup = { keyword: '/', menus$: of([]) };

service.showPopup({ popup, index: 999, unitId: 'doc-1' });
expect(popupEntries).toHaveLength(0);
expect(service.editPopup).toBeUndefined();
});

it('should allow unregistering menu selected callbacks', () => {
vi.useFakeTimers();

const { service } = createServiceTestBed();
const onSelected = vi.fn();
const unregister = service.onMenuSelected(onSelected);

service.setInputOffset({ start: 0, end: 1 });
service.emitMenuSelected({ id: 'menu-1', title: 'Text' } as never);
vi.runAllTimers();
expect(onSelected).toHaveBeenCalledTimes(1);

onSelected.mockClear();
unregister();
service.emitMenuSelected({ id: 'menu-2', title: 'Divider' } as never);
vi.runAllTimers();
expect(onSelected).not.toHaveBeenCalled();

vi.useRealTimers();
});
});

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading