2026-05-14 15:26:22 +08:00

276 lines
8.8 KiB
TypeScript

/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { defineComponent, h, nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import CodeEditor from '@editor/layouts/CodeEditor.vue';
const {
vsEditorInstance,
vsDiffEditorInstance,
monacoInstance,
blurHandlers,
contentChangeHandlers,
diffContentChangeHandlers,
} = vi.hoisted(() => ({
vsEditorInstance: {
getValue: vi.fn(() => 'editor-value'),
setValue: vi.fn(),
getPosition: vi.fn(() => ({ lineNumber: 1, column: 1 })),
setPosition: vi.fn(),
focus: vi.fn(),
layout: vi.fn(),
setScrollTop: vi.fn(),
revealLine: vi.fn(),
dispose: vi.fn(),
getOptions: vi.fn(() => ({ get: vi.fn(() => 20) })),
onDidChangeModelContent: vi.fn(),
onDidBlurEditorWidget: vi.fn(),
updateOptions: vi.fn(),
} as any,
vsDiffEditorInstance: {
getModifiedEditor: vi.fn(),
getPosition: vi.fn(() => null),
setPosition: vi.fn(),
setModel: vi.fn(),
focus: vi.fn(),
layout: vi.fn(),
dispose: vi.fn(),
updateOptions: vi.fn(),
} as any,
monacoInstance: {
editor: {
createModel: vi.fn(),
EditorOption: { scrollBeyondLastLine: 1, padding: 2, lineHeight: 3 },
},
} as any,
blurHandlers: [] as any[],
contentChangeHandlers: [] as any[],
diffContentChangeHandlers: [] as any[],
}));
vi.mock('@editor/utils/monaco-editor', () => ({
default: vi.fn(async () => monacoInstance),
}));
vi.mock('@editor/utils/config', () => ({
getEditorConfig: vi.fn((k: string) => {
if (k === 'parseDSL') return (s: string) => JSON.parse(s);
if (k === 'customCreateMonacoEditor') {
return (_m: any, _el: any, _opts: any) => vsEditorInstance;
}
if (k === 'customCreateMonacoDiffEditor') {
return (_m: any, _el: any, _opts: any) => vsDiffEditorInstance;
}
return undefined;
}),
}));
vi.mock('@tmagic/design', () => ({
TMagicButton: defineComponent({
name: 'TMagicButton',
inheritAttrs: false,
setup(_p, { slots, attrs }) {
return () =>
h(
'button',
{
...attrs,
class: ['fake-btn', (attrs as any).class].filter(Boolean).join(' '),
},
slots.default?.(),
);
},
}),
}));
vi.mock('@editor/components/Icon.vue', () => ({
default: defineComponent({
name: 'IconStub',
props: ['icon'],
setup() {
return () => h('i', { class: 'fake-icon' });
},
}),
}));
class FakeResizeObserver {
observe() {}
disconnect() {}
}
(globalThis as any).ResizeObserver = FakeResizeObserver;
beforeEach(() => {
vi.clearAllMocks();
blurHandlers.length = 0;
contentChangeHandlers.length = 0;
diffContentChangeHandlers.length = 0;
vsEditorInstance.onDidChangeModelContent.mockImplementation((cb: any) => {
contentChangeHandlers.push(cb);
});
vsEditorInstance.onDidBlurEditorWidget.mockImplementation((cb: any) => {
blurHandlers.push(cb);
});
const modifiedEditor = {
getValue: vi.fn(() => 'modified-value'),
onDidChangeModelContent: vi.fn((cb: any) => diffContentChangeHandlers.push(cb)),
};
vsDiffEditorInstance.getModifiedEditor.mockReturnValue(modifiedEditor);
});
const flush = async () => {
await nextTick();
await new Promise((r) => setTimeout(r, 50));
await nextTick();
};
describe('CodeEditor', () => {
test('挂载时初始化 monaco 编辑器', async () => {
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
await flush();
expect(wrapper.find('.fake-btn').exists()).toBe(true);
expect(wrapper.emitted('initd')).toBeTruthy();
wrapper.unmount();
});
test('disabledFullScreen 时不显示按钮', async () => {
const wrapper = mount(CodeEditor, {
props: { initValues: 'abc', disabledFullScreen: true } as any,
attachTo: document.body,
});
await flush();
expect(wrapper.find('.magic-code-editor-full-screen-icon').exists()).toBe(false);
wrapper.unmount();
});
test('点击全屏按钮切换 fullScreen', async () => {
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
await flush();
await wrapper.find('.fake-btn').trigger('click');
await new Promise((r) => setTimeout(r, 10));
expect(vsEditorInstance.layout).toHaveBeenCalled();
wrapper.unmount();
});
test('blur 自动保存触发 save 事件', async () => {
const wrapper = mount(CodeEditor, { props: { initValues: 'abc', autoSave: true } as any, attachTo: document.body });
await flush();
vsEditorInstance.getValue.mockReturnValue('new-value');
blurHandlers.forEach((cb) => cb());
expect(wrapper.emitted('save')?.[0]?.[0]).toBe('new-value');
wrapper.unmount();
});
test('parse: true 时解析后再 emit save', async () => {
const wrapper = mount(CodeEditor, {
props: { initValues: '{}', autoSave: true, parse: true, language: 'json' } as any,
attachTo: document.body,
});
await flush();
vsEditorInstance.getValue.mockReturnValue('{"foo":1}');
blurHandlers.forEach((cb) => cb());
expect(wrapper.emitted('save')?.[0]?.[0]).toEqual({ foo: 1 });
wrapper.unmount();
});
test('Ctrl+S 触发 save', async () => {
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
await flush();
const editorEl = wrapper.find('.magic-code-editor-content').element as HTMLDivElement;
vsEditorInstance.getValue.mockReturnValue('save-content');
const event = new KeyboardEvent('keydown', { keyCode: 83, ctrlKey: true } as any);
editorEl.dispatchEvent(event);
expect(wrapper.emitted('save')?.[0]?.[0]).toBe('save-content');
wrapper.unmount();
});
test('diff 模式下创建 diff 编辑器', async () => {
const wrapper = mount(CodeEditor, {
props: { type: 'diff', initValues: 'a', modifiedValues: 'b' } as any,
attachTo: document.body,
});
await flush();
expect(vsDiffEditorInstance.setModel).toHaveBeenCalled();
wrapper.unmount();
});
test('autosize 时根据内容计算高度', async () => {
const wrapper = mount(CodeEditor, {
props: { initValues: 'a\nb\nc', autosize: { minRows: 1, maxRows: 10 } } as any,
attachTo: document.body,
});
await flush();
contentChangeHandlers.forEach((cb) => cb());
await flush();
expect(true).toBe(true);
wrapper.unmount();
});
test('options 变化时调用 updateOptions', async () => {
const wrapper = mount(CodeEditor, {
props: { initValues: 'abc', options: { tabSize: 2 } } as any,
attachTo: document.body,
});
await flush();
await wrapper.setProps({ options: { tabSize: 4 } } as any);
await flush();
expect(vsEditorInstance.updateOptions).toHaveBeenCalled();
wrapper.unmount();
});
test('initValues 改变时调用 setEditorValue', async () => {
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
await flush();
await wrapper.setProps({ initValues: 'xyz' } as any);
await flush();
expect(vsEditorInstance.setValue).toHaveBeenCalledWith('xyz');
wrapper.unmount();
});
test('expose getEditor / focus / setEditorValue', async () => {
vsEditorInstance.getValue.mockReturnValue('editor-value');
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
await flush();
expect((wrapper.vm as any).getEditor()).toBe(vsEditorInstance);
expect((wrapper.vm as any).getVsEditor()).toBe(vsEditorInstance);
(wrapper.vm as any).focus();
expect(vsEditorInstance.focus).toHaveBeenCalled();
expect((wrapper.vm as any).getEditorValue()).toBe('editor-value');
wrapper.unmount();
});
test('卸载时 dispose', async () => {
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
await flush();
wrapper.unmount();
expect(vsEditorInstance.dispose).toHaveBeenCalled();
});
test('toString 处理 javascript 对象', async () => {
const wrapper = mount(CodeEditor, {
props: { initValues: { a: 1 }, language: 'javascript' } as any,
attachTo: document.body,
});
await flush();
expect(vsEditorInstance.setValue).toHaveBeenCalled();
const callArg = vsEditorInstance.setValue.mock.calls[0][0];
expect(callArg).toMatch(/^\(/);
wrapper.unmount();
});
test('toString 处理 json 对象', async () => {
const wrapper = mount(CodeEditor, {
props: { initValues: { a: 1 }, language: 'json' } as any,
attachTo: document.body,
});
await flush();
const callArg = vsEditorInstance.setValue.mock.calls[0][0];
expect(JSON.parse(callArg)).toEqual({ a: 1 });
wrapper.unmount();
});
});