chore(test): 增加 hotkey / host 部分单测

This commit is contained in:
力皓 2020-11-24 14:59:12 +08:00
parent 077b904171
commit 22aa77610a
18 changed files with 498 additions and 55 deletions

View File

@ -3,7 +3,7 @@ module.exports = {
ignorePatterns: [ 'tests/* '],
rules: {
'react/no-multi-comp': 0,
'no-unused-expressions': 1,
'no-unused-expressions': 0,
'implicit-arrow-linebreak': 1,
'no-nested-ternary': 1,
'no-mixed-operators': 1,

View File

@ -11,6 +11,7 @@ module.exports = {
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
setupFiles: ['./tests/fixtures/unhandled-rejection.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [

View File

@ -24,7 +24,7 @@
},
"devDependencies": {
"@ali/lowcode-test-mate": "^1.0.1",
"@alib/build-scripts": "^0.1.18",
"@alib/build-scripts": "^0.1.29",
"@types/classnames": "^2.2.7",
"@types/medium-editor": "^5.0.3",
"@types/node": "^13.7.1",

View File

@ -473,12 +473,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
true,
);
this.disableDetecting = () => {
detecting.leave(this.project.currentDocument);
doc.removeEventListener('mouseover', hover, true);
doc.removeEventListener('mouseleave', leave, false);
this.disableDetecting = undefined;
};
// this.disableDetecting = () => {
// detecting.leave(this.project.currentDocument);
// doc.removeEventListener('mouseover', hover, true);
// doc.removeEventListener('mouseleave', leave, false);
// this.disableDetecting = undefined;
// };
}
readonly liveEditing = new LiveEditing();
@ -525,21 +525,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
* @see ISimulator
*/
setSuspense(suspended: boolean) {
if (suspended) {
/*
if (this.disableDetecting) {
this.disableDetecting();
}
*/
// sleep some autorun reaction
} else {
// weekup some autorun reaction
/*
if (!this.disableDetecting) {
this.setupDetecting();
}
*/
}
return false;
// if (suspended) {
// /*
// if (this.disableDetecting) {
// this.disableDetecting();
// }
// */
// // sleep some autorun reaction
// } else {
// // weekup some autorun reaction
// /*
// if (!this.disableDetecting) {
// this.setupDetecting();
// }
// */
// }
}
setupContextMenu() {

View File

@ -247,8 +247,9 @@ export class Prop implements IPropParent {
value: valueToSource(val),
};
}
const editor = globalContext.get(Editor);
editor.emit('node.prop.change', { prop: this, node: this.owner });
if (globalContext.has(Editor)) {
globalContext.get(Editor).emit('node.prop.change', { prop: this, node: this.owner });
}
this.dispose();
}

View File

@ -6,7 +6,7 @@ import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import TestRenderer from 'react-test-renderer';
import { configure, render } from 'enzyme';
import { configure, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
@ -16,7 +16,7 @@ import { BuiltinSimulatorHostView } from '../../src/builtin-simulator/host-view'
configure({ adapter: new Adapter() });
const editor = new Editor();
describe('setting-prop-entry 测试', () => {
describe('host-view 测试', () => {
let designer: Designer;
beforeEach(() => {
designer = new Designer({ editor });
@ -26,9 +26,7 @@ describe('setting-prop-entry 测试', () => {
designer = null;
});
it('xxx', () => {
// console.log(JSON.stringify(TestRenderer.create(<BuiltinSimulatorHostView project={designer.project} />).toJSON()));
console.log(render(<BuiltinSimulatorHostView project={designer.project} ref={(xxx) => { console.log('xxx', xxx)}}/>))
it('host-view', () => {
const hostView = render(<BuiltinSimulatorHostView project={designer.project} />);
})
});

View File

@ -3,17 +3,25 @@ import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import {
AssetLevel,
Asset,
AssetList,
assetBundle,
assetItem,
AssetType,
} from '@ali/lowcode-utils';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
import { getMockDocument, getMockWindow, getMockEvent } from '../utils';
import { BuiltinSimulatorHost } from '../../src/builtin-simulator/host';
import { eq } from 'lodash';
const editor = new Editor();
describe('setting-prop-entry 测试', () => {
describe('host 测试', () => {
let designer: Designer;
beforeEach(() => {
designer = new Designer({ editor });
@ -23,7 +31,115 @@ describe('setting-prop-entry 测试', () => {
designer = null;
});
it('dummy test', () => {
console.log(new BuiltinSimulatorHost(designer.project));
it('基础方法测试', async () => {
const host = new BuiltinSimulatorHost(designer.project);
expect(host.currentDocument).toBe(designer.project.currentDocument);
expect(host.renderEnv).toBe('default');
expect(host.device).toBe('default');
expect(host.deviceClassName).toBeUndefined;
host.setProps({
renderEnv: 'rax',
device: 'mobile',
deviceClassName: 'mobile-rocks',
componentsAsset: [{
type: AssetType.JSText,
content: 'console.log(1)',
}, {
type: AssetType.JSUrl,
content: '//path/to/js',
}],
theme: {
type: AssetType.CSSText,
content: '.theme {font-size: 50px;}',
}
});
expect(host.renderEnv).toBe('rax');
expect(host.device).toBe('mobile');
expect(host.deviceClassName).toBe('mobile-rocks');
expect(host.componentsAsset).toEqual([{
type: AssetType.JSText,
content: 'console.log(1)',
}, {
type: AssetType.JSUrl,
content: '//path/to/js',
}]);
expect(host.theme).toEqual({
type: AssetType.CSSText,
content: '.theme {font-size: 50px;}',
});
expect(host.componentsMap).toBe(designer.componentsMap);
host.set('renderEnv', 'vue');
expect(host.renderEnv).toBe('vue');
expect(host.getComponentContext).toThrow('Method not implemented.');
});
it('事件测试', async () => {
const host = new BuiltinSimulatorHost(designer.project);
const mockDocument = getMockDocument();
const mockWindow = getMockWindow(mockDocument);
const mockIframe = {
contentWindow: mockWindow,
contentDocument: mockDocument,
dispatchEvent() {},
};
// 非法分支测试
host.mountContentFrame();
expect(host._iframe).toBeUndefined();
host.set('library', [{
package: '@ali/vc-deep',
library: 'lib',
urls: ['a.js', 'b.js']
}]);
host.componentsConsumer.consume(() => {});
host.injectionConsumer.consume(() => {});
await host.mountContentFrame(mockIframe);
expect(host.contentWindow).toBe(mockWindow);
mockDocument.triggerEventListener(
'mouseover',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mouseleave',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mousedown',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mouseup',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mousemove',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'click',
getMockEvent(document.createElement('input')),
host,
);
mockDocument.triggerEventListener(
'dblclick',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'contextmenu',
getMockEvent(mockDocument.createElement('div')),
host,
);
})
});

View File

@ -3,7 +3,7 @@ import { parseMetadata } from '../../src/builtin-simulator/utils/parse-metadata'
describe('parseMetadata', () => {
it('parseMetadata', async () => {
console.log(parseMetadata('Div'))
console.log(parseMetadata({ componentName: 'Div' }));
const md1 = parseMetadata('Div');
const md2 = parseMetadata({ componentName: 'Div' });
});
});

View File

@ -0,0 +1,21 @@
import React from 'react';
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import TestRenderer from 'react-test-renderer';
import { configure, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getMockRenderer } from '../utils';
import { isSimulatorRenderer } from '../../src/builtin-simulator/renderer';
describe('renderer 测试', () => {
it('renderer', () => {
expect(isSimulatorRenderer(getMockRenderer())).toBeTruthy;
})
});

View File

@ -0,0 +1,202 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Editor, globalContext } from '@ali/lowcode-editor-core';
import { Designer } from '../../src/designer/designer';
import { Project } from '../../src/project/project';
import formSchema from '../fixtures/schema/form';
import '../../src/designer/builtin-hotkey';
const editor = new Editor();
let designer: Designer;
beforeAll(() => {
globalContext.register(editor, Editor);
});
beforeEach(() => {
designer = new Designer({ editor });
designer.project.open(formSchema);
});
afterEach(() => {
designer = null;
});
// keyCode 对应表https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
// hotkey 模块底层用的 keyCode所以还不能用 key / code 测试
describe('快捷键测试', () => {
it('right', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbj')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 39 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy;
});
it('left', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 37 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbj')).toBeTruthy;
});
it('down', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 40 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbm')).toBeTruthy;
});
it('up', () => {
const secondCardNode = designer.currentDocument?.getNode('node_k1ow3cbm')!;
secondCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 38 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy;
});
// 跟右侧节点调换位置
it('option + right', () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
firstButtonNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 39, altKey: true });
document.dispatchEvent(event);
expect(firstButtonNode.prevSibling?.getId()).toBe('node_k1ow3cbp');
});
// 跟左侧节点调换位置
it('option + left', () => {
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
secondButtonNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 37, altKey: true });
document.dispatchEvent(event);
expect(secondButtonNode.nextSibling?.getId()).toBe('node_k1ow3cbn');
});
// 向父级移动该节点
it('option + up', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 38, altKey: true });
document.dispatchEvent(event);
});
// 将节点移入到兄弟节点中
it('option + up', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 40, altKey: true });
document.dispatchEvent(event);
});
// 撤销
it('command + z', async () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstButtonNode.remove();
expect(secondButtonNode.getParent()?.children.size).toBe(1);
await new Promise(resolve => setTimeout(resolve, 1000));
let event = new KeyboardEvent('keydown', { keyCode: 90, metaKey: true });
document.dispatchEvent(event);
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(2);
});
// 重做
it('command + y', async () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstButtonNode.remove();
expect(secondButtonNode.getParent()?.children.size).toBe(1);
await new Promise(resolve => setTimeout(resolve, 1000));
let event = new KeyboardEvent('keydown', { keyCode: 90, metaKey: true });
document.dispatchEvent(event);
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(2);
await new Promise(resolve => setTimeout(resolve, 1000));
event = new KeyboardEvent('keydown', { keyCode: 89, metaKey: true });
document.dispatchEvent(event);
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(1);
});
it('command + c', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 67, metaKey: true });
document.dispatchEvent(event);
});
it('command + v', async () => {
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
secondButtonNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 67, metaKey: true });
document.dispatchEvent(event);
event = new KeyboardEvent('keydown', { keyCode: 86, metaKey: true });
document.dispatchEvent(event);
await new Promise(resolve => setTimeout(resolve, 1000));
// clipboard 异步,先注释
// expect(secondButtonNode.getParent()?.children.size).toBe(3);
});
// 撤销所有选中
it('escape', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
expect(designer.currentSelection!.selected.includes('node_k1ow3cbp')).toBeTruthy;
let event = new KeyboardEvent('keydown', { keyCode: 27 });
document.dispatchEvent(event);
expect(designer.currentSelection!.selected.length).toBe(0);
});
// 删除节点
it('delete', () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstButtonNode.select();
expect(secondButtonNode.prevSibling.id).toBe('node_k1ow3cbn');
let event = new KeyboardEvent('keydown', { keyCode: 46 });
document.dispatchEvent(event);
expect(secondButtonNode.prevSibling).toBeNull;
});
});

View File

@ -1,14 +1,14 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import '../../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import settingSchema from '../fixtures/schema/setting';
import divMeta from '../fixtures/prototype/div-meta';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
import { Project } from '../../../src/project/project';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../../fixtures/schema/form';
import settingSchema from '../../fixtures/schema/setting';
import divMeta from '../../fixtures/prototype/div-meta';
import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils';
const editor = new Editor();

View File

@ -1,14 +1,14 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import '../../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import settingSchema from '../fixtures/schema/setting';
import divMeta from '../fixtures/prototype/div-meta';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
import { Project } from '../../../src/project/project';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import settingSchema from '../../fixtures/schema/setting';
import divMeta from '../../fixtures/prototype/div-meta';
import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils';
const editor = new Editor();

View File

@ -0,0 +1,7 @@
if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
process.on('unhandledRejection', reason => {
throw reason;
})
// Avoid memory leak by adding too many listeners
process.env.LISTENING_TO_UNHANDLED_REJECTION = true;
}

View File

@ -0,0 +1,77 @@
import { getMockRenderer } from './renderer';
interface MockDocument extends Document {
// open(): any;
// write(): any;
// close(): any;
// addEventListener(): any;
// removeEventListener(): any;
triggerEventListener(): any;
// createElement(): any;
// appendChild(): any;
// removeChild(): any;
}
const eventsMap : Map<string, Set<Function>> = new Map<string, Set<Function>>();
const mockAddEventListener = jest.fn((eventName: string, cb) => {
if (!eventsMap.has(eventName)) {
eventsMap.set(eventName, new Set([cb]));
return;
}
eventsMap.get(eventName)!.add(cb);
});
const mockRemoveEventListener = jest.fn((eventName: string, cb) => {
if (!eventsMap.has(eventName)) return;
if (!cb) {
eventsMap.delete(eventName);
return;
}
eventsMap.get(eventName)?.delete(cb);
});
const mockTriggerEventListener = jest.fn((eventName: string, data: any, context: object = {}) => {
if (!eventsMap.has(eventName)) return;
for (const cb of eventsMap.get(eventName)) {
cb.call(context, data);
}
});
const mockCreateElement = jest.fn((tagName) => {
return {
style: {},
appendChild() {},
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
}
})
export function getMockDocument(): MockDocument {
return {
open() {},
write() {},
close() {},
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
createElement: mockCreateElement,
removeChild() {},
body: { appendChild() {}, removeChild() {} },
};
}
export function getMockWindow(doc?: MockDocument) {
return {
SimulatorRenderer: getMockRenderer(),
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
document: doc || getMockDocument(),
};
}
export function clearEventsMap() {
eventsMap.clear();
}

View File

@ -0,0 +1,8 @@
export function getMockEvent(target, options) {
return {
target,
preventDefault() {},
stopPropagation() {},
...options,
};
}

View File

@ -1 +1,4 @@
export { getIdsFromSchema, getNodeFromSchemaById } from '@ali/lowcode-test-mate/es/utils';
export { getMockDocument, getMockWindow } from './bom';
export { getMockEvent } from './event';
export { getMockRenderer } from './renderer';

View File

@ -0,0 +1,8 @@
export function getMockRenderer() {
return {
isSimulatorRenderer: true,
run() {
console.log('renderer run');
}
}
}

View File

@ -7,7 +7,7 @@ export function isFormEvent(e: KeyboardEvent | MouseEvent) {
if (t.form || /^(INPUT|SELECT|TEXTAREA)$/.test(t.tagName)) {
return true;
}
if (/write/.test(window.getComputedStyle(t).getPropertyValue('-webkit-user-modify'))) {
if (t instanceof HTMLElement && /write/.test(window.getComputedStyle(t).getPropertyValue('-webkit-user-modify'))) {
return true;
}
return false;