test 修复 & 异步 require 改成 dynamic import

This commit is contained in:
2betop 2020-12-03 13:07:58 +08:00
parent a12d4e35af
commit deb546bce4
22 changed files with 394 additions and 248 deletions

17
__tests__/jest.setup.js Normal file
View File

@ -0,0 +1,17 @@
const originalWarn = console.warn.bind(console.warn);
global.beforeAll(() => {
console.warn = msg => {
// warning 先关了,实在太吵。
// const str = msg.toString();
// if (
// str.includes('componentWillMount') ||
// str.includes('componentWillReceiveProps')
// ) {
// return;
// }
// originalWarn(msg);
};
});
global.afterAll(() => {
console.warn = originalWarn;
});

View File

@ -15,7 +15,9 @@ exports[`Renderer:city 1`] = `
<span
class="a-TplField"
>
The form
<span>
The form
</span>
</span>
</h3>
</div>
@ -28,6 +30,7 @@ exports[`Renderer:city 1`] = `
>
<div
class="a-Form-item a-Form-item--normal"
data-role="form-item"
>
<label
class="a-Form-label"
@ -58,7 +61,12 @@ exports[`Renderer:city 1`] = `
</div>
<span
class="a-Select-arrow"
/>
>
<icon-mock
classname="icon icon-caret"
icon="caret"
/>
</span>
</div>
<div
aria-expanded="false"
@ -79,7 +87,12 @@ exports[`Renderer:city 1`] = `
</div>
<span
class="a-Select-arrow"
/>
>
<icon-mock
classname="icon icon-caret"
icon="caret"
/>
</span>
</div>
<div
aria-expanded="false"
@ -100,7 +113,12 @@ exports[`Renderer:city 1`] = `
</div>
<span
class="a-Select-arrow"
/>
>
<icon-mock
classname="icon icon-caret"
icon="caret"
/>
</span>
</div>
</div>
</div>

View File

@ -1,38 +1,40 @@
import React = require('react');
import {render, fireEvent} from 'react-testing-library';
import '../../../src/themes/default';
import {
render as amisRender
} from '../../../src/index';
import { makeEnv } from '../../helper';
import {render as amisRender} from '../../../src/index';
import {makeEnv, wait} from '../../helper';
test('Renderer:city', async () => {
const {
container,
getByText
} = render(amisRender({
const {container, getByText} = render(
amisRender(
{
type: 'form',
api: '/api/xxx',
controls: [
{
type: 'city',
name: 'a',
label: 'city',
allowDistrict: true,
allowCity: true
}
{
type: 'city',
name: 'a',
label: 'city',
allowDistrict: true,
allowCity: true
}
],
title: 'The form',
actions: []
}, {}, makeEnv({
})));
},
{},
makeEnv({})
)
);
fireEvent.click(getByText('请选择'));
fireEvent.click(getByText('北京市'));
fireEvent.click(getByText('请选择'));
fireEvent.click(getByText('北京市市辖区'));
fireEvent.click(getByText('请选择'));
fireEvent.click(getByText('东城区'));
await wait(200);
expect(container).toMatchSnapshot();
});
fireEvent.click(getByText('请选择'));
fireEvent.click(getByText('北京市'));
fireEvent.click(getByText('请选择'));
fireEvent.click(getByText('北京市市辖区'));
fireEvent.click(getByText('请选择'));
fireEvent.click(getByText('东城区'));
expect(container).toMatchSnapshot();
});

View File

@ -1,73 +1,95 @@
import { getSnapshot, getEnv, onSnapshot } from 'mobx-state-tree';
import { ServiceStore } from '../../src/store/service';
import { RendererStore } from '../../src/store';
import {getSnapshot, getEnv, onSnapshot} from 'mobx-state-tree';
import {StoreNode} from '../../src/store/node';
import {ServiceStore} from '../../src/store/service';
import {RendererStore} from '../../src/store';
import omit = require('lodash/omit');
test('store:ServiceStore', () => {
const store = ServiceStore.create({
id: '1',
storeType: ServiceStore.name
});
const store = ServiceStore.create({
id: '1',
storeType: ServiceStore.name
});
expect(getSnapshot(store)).toMatchSnapshot();
expect(getSnapshot(store)).toMatchSnapshot();
});
test('store:ServiceStore fetchInitData success', async () => {
const fetcher = jest.fn().mockImplementationOnce(() => Promise.resolve({
ok: true,
data: {
a: 1,
b: 2
}
}));
const mainStore = RendererStore.create({}, {
fetcher
});
const states:Array<any> = [];
const fetcher = jest.fn().mockImplementationOnce(() =>
Promise.resolve({
ok: true,
data: {
a: 1,
b: 2
}
})
);
const isCancel = jest.fn(() => false);
const mainStore = RendererStore.create(
{},
{
fetcher,
isCancel
}
);
const states: Array<any> = [];
const store = ServiceStore.create(
{
id: '1',
storeType: ServiceStore.name
},
{
fetcher,
isCancel
}
);
mainStore.addStore(store);
const store = ServiceStore.create({
id: '1',
storeType: ServiceStore.name
});
mainStore.addStore(store);
onSnapshot(store, snapshot => states.push(snapshot));
onSnapshot(store, (snapshot) => states.push(snapshot));
await store.fetchInitData('/api/xxx');
await store.fetchInitData('/api/xxx');
const ignoreUdatedAt = states.map(snapshot => omit(snapshot, ['updatedAt']));
expect(ignoreUdatedAt).toMatchSnapshot();
const ignoreUdatedAt = states.map(snapshot => omit(snapshot, ['updatedAt']));
expect(ignoreUdatedAt).toMatchSnapshot();
expect(states.length).toBe(2);
expect(states[1].updatedAt).not.toEqual(states[0].updatedAt);
expect(states.length).toBe(2);
expect(states[1].updatedAt).not.toEqual(states[0].updatedAt);
});
test('store:ServiceStore fetchInitData failed', async () => {
const fetcher = jest.fn().mockImplementationOnce(() => Promise.reject('Network Error'));
const notify = jest.fn();
const isCancel = jest.fn(() => false);
const mainStore = RendererStore.create({}, {
fetcher,
notify,
isCancel
});
const states:Array<any> = [];
const fetcher = jest
.fn()
.mockImplementationOnce(() => Promise.reject('Network Error'));
const notify = jest.fn();
const isCancel = jest.fn(() => false);
const mainStore = RendererStore.create(
{},
{
fetcher,
notify,
isCancel
}
);
const states: Array<any> = [];
const store = ServiceStore.create(
{
id: '1',
storeType: ServiceStore.name
},
{
fetcher,
notify,
isCancel
}
);
mainStore.addStore(store);
const store = ServiceStore.create({
id: '1',
storeType: ServiceStore.name
});
mainStore.addStore(store);
onSnapshot(store, snapshot => states.push(snapshot));
onSnapshot(store, (snapshot) => states.push(snapshot));
await store.fetchInitData('/api/xxx');
expect(states).toMatchSnapshot();
expect(notify).toHaveBeenCalled();
expect(notify).toHaveBeenLastCalledWith("error", "Network Error");
expect(isCancel).toHaveBeenCalled();
});
await store.fetchInitData('/api/xxx');
expect(states).toMatchSnapshot();
expect(notify).toHaveBeenCalled();
expect(notify).toHaveBeenLastCalledWith('error', 'Network Error');
expect(isCancel).toHaveBeenCalled();
});

View File

@ -110,24 +110,23 @@ fis.match('/docs/**.md', {
parser: [
parserMarkdown,
function (contents, file) {
return contents.replace(/\bhref=\\('|")(.+?)\\\1/g, function (
_,
quota,
link
) {
if (/\.md($|#)/.test(link) && !/^https?\:/.test(link)) {
let parts = link.split('#');
parts[0] = parts[0].replace('.md', '');
return contents.replace(
/\bhref=\\('|")(.+?)\\\1/g,
function (_, quota, link) {
if (/\.md($|#)/.test(link) && !/^https?\:/.test(link)) {
let parts = link.split('#');
parts[0] = parts[0].replace('.md', '');
if (parts[0][0] !== '/') {
parts[0] = path.resolve(path.dirname(file.subpath), parts[0]);
if (parts[0][0] !== '/') {
parts[0] = path.resolve(path.dirname(file.subpath), parts[0]);
}
return 'href=\\' + quota + parts.join('#') + '\\' + quota;
}
return 'href=\\' + quota + parts.join('#') + '\\' + quota;
return _;
}
return _;
});
);
}
],
isMod: true
@ -170,7 +169,14 @@ fis.match('{*.ts,*.jsx,*.tsx,/src/**.js,/src/**.ts}', {
}),
function (content) {
return content.replace(/\b[a-zA-Z_0-9$]+\.__uri\s*\(/g, '__uri(');
return content
.replace(/\b[a-zA-Z_0-9$]+\.__uri\s*\(/g, '__uri(')
.replace(
/return\s+(tslib_\d+)\.__importStar\(require\(('|")(.*?)\2\)\);/g,
function (_, tslib, quto, value) {
return `return new Promise(function(resolve){require(['${value}'], function(ret) {resolve(${tslib}.__importStar(ret));})});`;
}
);
}
],
preprocessor: fis.plugin('js-require-css'),
@ -239,19 +245,26 @@ if (fis.project.currentMedia() === 'publish') {
allowUmdGlobalAccess: true
}),
function (contents) {
return contents.replace(
/(?:\w+\.)?\b__uri\s*\(\s*('|")(.*?)\1\s*\)/g,
function (_, quote, value) {
let str = quote + value + quote;
return (
'(function(){try {return __uri(' +
str +
')} catch(e) {return ' +
str +
'}})()'
);
}
);
return contents
.replace(
/(?:\w+\.)?\b__uri\s*\(\s*('|")(.*?)\1\s*\)/g,
function (_, quote, value) {
let str = quote + value + quote;
return (
'(function(){try {return __uri(' +
str +
')} catch(e) {return ' +
str +
'}})()'
);
}
)
.replace(
/return\s+(tslib_\d+)\.__importStar\(require\(('|")(.*?)\2\)\);/g,
function (_, tslib, quto, value) {
return `return new Promise(function(resolve){require(['${value}'], function(ret) {resolve(${tslib}.__importStar(ret));})});`;
}
);
}
],
preprocessor: null
@ -345,9 +358,15 @@ if (fis.project.currentMedia() === 'publish') {
experimentalDecorators: true,
sourceMap: false
}),
function (content) {
return content.replace(/\b[a-zA-Z_0-9$]+\.__uri\s*\(/g, '__uri(');
return content
.replace(/\b[a-zA-Z_0-9$]+\.__uri\s*\(/g, '__uri(')
.replace(
/return\s+(tslib_\d+)\.__importStar\(require\(('|")(.*?)\2\)\);/g,
function (_, tslib, quto, value) {
return `return new Promise(function(resolve){require(['${value}'], function(ret) {resolve(${tslib}.__importStar(ret));})});`;
}
);
}
],
preprocessor: fis.plugin('js-require-css'),
@ -521,24 +540,25 @@ if (fis.project.currentMedia() === 'publish') {
parser: [
parserMarkdown,
function (contents, file) {
return contents.replace(/\bhref=\\('|")(.+?)\\\1/g, function (
_,
quota,
link
) {
if (/\.md($|#)/.test(link) && !/^https?\:/.test(link)) {
let parts = link.split('#');
parts[0] = parts[0].replace('.md', '');
return contents.replace(
/\bhref=\\('|")(.+?)\\\1/g,
function (_, quota, link) {
if (/\.md($|#)/.test(link) && !/^https?\:/.test(link)) {
let parts = link.split('#');
parts[0] = parts[0].replace('.md', '');
if (parts[0][0] !== '/') {
parts[0] = path.resolve(path.dirname(file.subpath), parts[0]);
if (parts[0][0] !== '/') {
parts[0] = path.resolve(path.dirname(file.subpath), parts[0]);
}
return (
'href=\\' + quota + '/amis' + parts.join('#') + '\\' + quota
);
}
return 'href=\\' + quota + '/amis' + parts.join('#') + '\\' + quota;
return _;
}
return _;
});
);
}
]
});
@ -658,18 +678,17 @@ if (fis.project.currentMedia() === 'publish') {
DocJs.getContent(),
ExampleJs.getContent()
].join('\n');
source.replace(/\bpath\b\s*\:\s*('|")(.*?)\1/g, function (
_,
qutoa,
path
) {
if (path === '*') {
return;
}
source.replace(
/\bpath\b\s*\:\s*('|")(.*?)\1/g,
function (_, qutoa, path) {
if (path === '*') {
return;
}
pages.push(path.replace(/^\//, ''));
return _;
});
pages.push(path.replace(/^\//, ''));
return _;
}
);
const contents = indexHtml.getContent();
pages.forEach(function (path) {
@ -713,7 +732,30 @@ if (fis.project.currentMedia() === 'publish') {
sourceMap: false,
importHelpers: true,
esModuleInterop: true
})
}),
function (contents) {
return contents
.replace(
/(?:\w+\.)?\b__uri\s*\(\s*('|")(.*?)\1\s*\)/g,
function (_, quote, value) {
let str = quote + value + quote;
return (
'(function(){try {return __uri(' +
str +
')} catch(e) {return ' +
str +
'}})()'
);
}
)
.replace(
/return\s+(tslib_\d+)\.__importStar\(require\(('|")(.*?)\2\)\);/g,
function (_, tslib, quto, value) {
return `return new Promise(function(resolve){require(['${value}'], function(ret) {resolve(${tslib}.__importStar(ret));})});`;
}
);
}
]
});
ghPages.match('*', {

View File

@ -93,6 +93,7 @@
"@types/async": "^2.0.45",
"@types/classnames": "^2.2.3",
"@types/dom-helpers": "^3.4.1",
"@types/echarts": "^4.9.2",
"@types/history": "^4.6.0",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/jest": "^24.9.1",
@ -183,9 +184,40 @@
"\\.(css|less|sass|scss)$": "<rootDir>/__mocks__/styleMock.js",
"\\.(svg)$": "<rootDir>/__mocks__/svgMock.js"
},
"setupFilesAfterEnv": [
"<rootDir>/__tests__/jest.setup.js"
],
"globals": {
"ts-jest": {
"diagnostics": false
"diagnostics": false,
"tsconfig": {
"module": "commonjs",
"target": "es5",
"lib": [
"es6",
"dom",
"ES2015"
],
"sourceMap": true,
"jsx": "react",
"moduleResolution": "node",
"rootDir": ".",
"importHelpers": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceRoot": ".",
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": false,
"typeRoots": [
"./node_modules/@types",
"./types"
],
"skipLibCheck": true
}
}
}
},

View File

@ -15,6 +15,7 @@
flex-grow: 1;
line-height: 1;
white-space: nowrap;
display: flex;
> input {
display: inline-block;

View File

@ -176,9 +176,7 @@ export class Editor extends React.Component<EditorProps, any> {
}
loadMonaco() {
(require as any)(['monaco-editor'], (monaco: any) => {
this.initMonaco(monaco);
});
import('monaco-editor').then(monaco => this.initMonaco(monaco));
}
initMonaco(monaco: any) {

View File

@ -14,7 +14,7 @@ export interface ResultBoxProps
onChange?: (value: string) => void;
onResultClick?: (e: React.MouseEvent<HTMLElement>) => void;
result?: Array<any> | any;
itemRender: (value: any) => JSX.Element;
itemRender: (value: any) => JSX.Element | string;
onResultChange?: (value: Array<any>) => void;
allowInput?: boolean;
inputPlaceholder: string;

View File

@ -67,7 +67,7 @@ export class QueryBuilder extends React.Component<ConditionBuilderProps> {
}
@autobind
handleDragOver(e: React.DragEvent) {
handleDragOver(e: DragEvent) {
e.preventDefault();
const item = (e.target as HTMLElement).closest('[data-id]') as HTMLElement;

View File

@ -1706,7 +1706,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
<Button
classPrefix={ns}
onClick={() => {
(require as any)(['papaparse'], (papaparse: any) => {
import('papaparse').then((papaparse: any) => {
const csvText = papaparse.unparse(store.data.items);
if (csvText) {
const blob = new Blob([csvText], {

View File

@ -224,32 +224,34 @@ export class Chart extends React.Component<ChartProps> {
refFn(ref: any) {
const chartRef = this.props.chartRef;
if (ref) {
(require as any)(
[
'echarts',
'echarts/extension/dataTool',
'echarts/extension/bmap/bmap',
'echarts/map/js/china',
'echarts/map/js/world'
],
(echarts: any, dataTool: any) => {
(window as any).echarts = echarts;
echarts.dataTool = dataTool;
this.echarts = echarts.init(ref);
this.echarts.on('click', this.handleClick);
this.unSensor = resizeSensor(ref, () => {
const width = ref.offsetWidth;
const height = ref.offsetHeight;
this.echarts.resize({
width,
height
});
});
Promise.all([
import('echarts'),
chartRef && chartRef(this.echarts);
this.renderChart();
}
);
// @ts-ignore
import('echarts/extension/dataTool'),
// @ts-ignore
import('echarts/extension/bmap/bmap'),
// @ts-ignore
import('echarts/map/js/china'),
// @ts-ignore
import('echarts/map/js/world')
]).then(([echarts, dataTool]: any) => {
(window as any).echarts = echarts;
echarts.dataTool = dataTool;
this.echarts = echarts.init(ref);
this.echarts.on('click', this.handleClick);
this.unSensor = resizeSensor(ref, () => {
const width = ref.offsetWidth;
const height = ref.offsetHeight;
this.echarts.resize({
width,
height
});
});
chartRef && chartRef(this.echarts);
this.renderChart();
});
} else {
chartRef && chartRef(null);
this.unSensor && this.unSensor();

View File

@ -134,19 +134,33 @@ export class CityPicker extends React.Component<
return;
}
(require as any)(['./CityDB'], (db: any) =>
import('./CityDB').then(db => {
this.setState(
{
db: {
...db.default,
province: db.province,
province: db.province as any,
city: db.city,
district: db.district
}
},
callback
)
);
);
});
// require.ensure(['./CityDB'], (db: any) =>
// this.setState(
// {
// db: {
// ...db.default,
// province: db.province,
// city: db.city,
// district: db.district
// }
// },
// callback
// )
// );
}
@autobind

View File

@ -35,12 +35,8 @@ export interface DiffControlSchema extends FormBaseControl {
options?: any;
}
function loadComponent(): Promise<React.ReactType> {
return new Promise(resolve =>
(require as any)(['../../components/Editor'], (component: any) =>
resolve(component.default)
)
);
function loadComponent(): Promise<any> {
return import('../../components/Editor');
}
export interface DiffEditorProps

View File

@ -26,17 +26,11 @@ export interface RichTextProps extends FormControlProps {
function loadRichText(
type: 'tinymce' | 'froala' = 'froala'
): () => Promise<React.ReactType> {
): () => Promise<any> {
return () =>
new Promise(resolve =>
type === 'tinymce'
? (require as any)(['../../components/Tinymce'], (component: any) =>
resolve(component.default)
)
: (require as any)(['../../components/RichText'], (component: any) =>
resolve(component.default)
)
);
type === 'tinymce'
? import('../../components/Tinymce')
: import('../../components/RichText');
}
export default class RichTextControl extends React.Component<

View File

@ -1753,7 +1753,7 @@ export default class Table extends React.Component<TableProps, object> {
<Button
classPrefix={ns}
onClick={() => {
(require as any)(['exceljs'], async (ExcelJS: any) => {
import('exceljs').then(async (ExcelJS: any) => {
if (!store.data.items || store.data.items.length === 0) {
return;
}

View File

@ -221,7 +221,7 @@ export class FlvSource extends React.Component<FlvSourceProps, any> {
setError,
autoPlay
}: any) {
(require as any)(['flv.js'], (flvjs: any) => {
import('flv.js').then((flvjs: any) => {
video = video || (manager.video && manager.video.video);
let flvPlayer = flvjs.createPlayer(
@ -335,7 +335,8 @@ export class HlsSource extends React.Component<HlsSourceProps, any> {
}
initHls({video, manager, src, autoPlay, actions}: any) {
(require as any)(['hls.js'], (Hls: any) => {
// @ts-ignore
import('hls.js').then((Hls: any) => {
// load hls video source base on hls.js
if (Hls.isSupported()) {
video = video || (manager.video && manager.video.video);

View File

@ -2,7 +2,7 @@ import {types, SnapshotIn, isAlive, onAction} from 'mobx-state-tree';
import {iRendererStore} from './iRenderer';
import {FormItemStore} from './formItem';
import {FormStore, IFormStore, IFormItemStore} from './form';
import {getStoreById} from './index';
import {getStoreById} from './manager';
export const UniqueGroup = types
.model('UniqueGroup', {

View File

@ -23,9 +23,8 @@ import {
isEmpty,
mapObject
} from '../utils/helper';
import {IComboStore} from './combo';
import isEqual from 'lodash/isEqual';
import {IRendererStore, getStoreById, removeStore} from '.';
import {getStoreById, removeStore} from './manager';
export const FormStore = ServiceStore.named('FormStore')
.props({

View File

@ -18,6 +18,7 @@ import {TranslateFn} from '../locale';
import find from 'lodash/find';
import {IStoreNode} from './node';
import {FormItemStore} from './formItem';
import {addStore, getStoreById, getStores, removeStore} from './manager';
setLivelynessChecking(
process.env.NODE_ENV === 'production' ? 'ignore' : 'error'
@ -59,7 +60,7 @@ export const RendererStore = types
},
get stores() {
return stores;
return getStores();
}
}))
.actions(self => ({
@ -89,51 +90,3 @@ export {iRendererStore, IIRendererStore};
export const RegisterStore = function (store: any) {
allowedStoreList.push(store as any);
};
const stores: {
[propName: string]: IStoreNode;
} = {};
export function addStore(store: IStoreNode) {
if (stores[store.id]) {
return stores[store.id];
}
stores[store.id] = store;
// drawer dialog 不加进去,否则有些容器就不会自我销毁 store 了。
if (store.parentId && !/(?:dialog|drawer)$/.test(store.path)) {
const parent = stores[store.parentId] as IIRendererStore;
parent.addChildId(store.id);
}
cleanUp();
return store;
}
const toDelete: Array<string> = [];
export function removeStore(store: IStoreNode) {
const id = store.id;
toDelete.push(id);
store.dispose(cleanUp);
}
function cleanUp() {
let index = toDelete.length - 1;
while (index >= 0) {
const id = toDelete[index];
const store = stores[id];
if (store && !isAlive(store)) {
delete stores[id];
toDelete.splice(index, 1);
} else {
index--;
}
}
}
export function getStoreById(id: string) {
return stores[id];
}

55
src/store/manager.ts Normal file
View File

@ -0,0 +1,55 @@
import {isAlive} from 'mobx-state-tree';
import {IIRendererStore} from './iRenderer';
import {IStoreNode} from './node';
const stores: {
[propName: string]: IStoreNode;
} = {};
export function addStore(store: IStoreNode) {
if (stores[store.id]) {
return stores[store.id];
}
stores[store.id] = store;
// drawer dialog 不加进去,否则有些容器就不会自我销毁 store 了。
if (store.parentId && !/(?:dialog|drawer)$/.test(store.path)) {
const parent = stores[store.parentId] as IIRendererStore;
parent.addChildId(store.id);
}
cleanUp();
return store;
}
const toDelete: Array<string> = [];
export function removeStore(store: IStoreNode) {
const id = store.id;
toDelete.push(id);
store.dispose(cleanUp);
}
function cleanUp() {
let index = toDelete.length - 1;
while (index >= 0) {
const id = toDelete[index];
const store = stores[id];
if (store && !isAlive(store)) {
delete stores[id];
toDelete.splice(index, 1);
} else {
index--;
}
}
}
export function getStoreById(id: string) {
return stores[id];
}
export function getStores() {
return stores;
}

View File

@ -1,5 +1,5 @@
import {types, destroy, isAlive, detach, getEnv} from 'mobx-state-tree';
import {getStoreById} from './index';
import {getStoreById} from './manager';
export const StoreNode = types
.model('StoreNode', {