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
321 changes: 71 additions & 250 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/react-vrender-utils/__tests__/types/vrender.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module '@visactor/vrender' {
export type IShadowRoot = any;
}
192 changes: 192 additions & 0 deletions packages/react-vrender-utils/__tests__/unit/Html.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import React, { act } from 'react';
import ReactDOM from 'react-dom';

declare const require: any;

type ReactDOMType = typeof import('react-dom');

const createRootMock = jest.fn(() => ({ render: jest.fn(), unmount: jest.fn() }));

jest.mock('react-dom', () => {
const actual: ReactDOMType = jest.requireActual('react-dom');
return {
...actual,
createRoot: (...args: any[]) => (createRootMock as any)(...args)
};
});

const hooks = {
beforeRender: {
tap: jest.fn(),
unTap: jest.fn()
}
};

const stageStub = {
hooks,
window: {
getContainer: jest.fn(() => document.body)
},
renderNextFrame: jest.fn()
};

let stageForRef: any = stageStub;

jest.mock('@visactor/react-vrender', () => {
const React = require('react') as typeof import('react');
return {
ShadowRoot: React.forwardRef<any, any>((props: any, ref: any): any => {
React.useEffect(() => {
if (ref) {
ref.current = {
_uid: 'uid',
stage: stageForRef,
shouldUpdateGlobalMatrix: () => true,
globalTransMatrix: {
toTransformAttrs: () => ({ x: 1, y: 2, scaleX: 1, scaleY: 1, rotateDeg: 0, skewX: 0, skewY: 0 })
}
};
}
}, []);
return null;
})
};
});

import { Html } from '../../src/Html';

describe('react-vrender-utils Html', () => {
beforeEach(() => {
jest.clearAllMocks();
stageForRef = stageStub;
});

test('mount appends div and applies transform + divProps', () => {
const container = document.createElement('div');
document.body.appendChild(container);

stageStub.window.getContainer.mockReturnValue(container);

const mountPoint = document.createElement('div');
document.body.appendChild(mountPoint);

act(() => {
ReactDOM.render(
<Html divProps={{ id: 'test-div', style: { color: 'red' } }}>
<span>child</span>
</Html>,
mountPoint
);
});

const div = container.querySelector('#test-div') as HTMLDivElement;
expect(div).toBeTruthy();
expect(div.style.position).toBe('absolute');
expect(div.style.color).toBe('red');
expect(container.style.position).toBe('relative');

act(() => {
ReactDOM.unmountComponentAtNode(mountPoint);
});

expect(container.querySelector('#test-div')).toBeFalsy();
document.body.removeChild(container);
document.body.removeChild(mountPoint);
});

test('transform=false clears transform styles and does not force container position', () => {
const container = document.createElement('div');
document.body.appendChild(container);
stageStub.window.getContainer.mockReturnValue(container);

const mountPoint = document.createElement('div');
document.body.appendChild(mountPoint);

act(() => {
ReactDOM.render(<Html transform={false} divProps={{ id: 'no-transform', style: { color: 'blue' } }} />, mountPoint);
});

const div = container.querySelector('#no-transform') as HTMLDivElement;
expect(div).toBeTruthy();
expect(div.style.position).toBe('');
expect(div.style.color).toBe('blue');
expect(container.style.position).toBe('');

act(() => {
ReactDOM.unmountComponentAtNode(mountPoint);
});

document.body.removeChild(container);
document.body.removeChild(mountPoint);
});

test('needForceStyle=false keeps container position unchanged', () => {
const container = document.createElement('div');
container.style.position = 'absolute';
document.body.appendChild(container);

const cssSpy = jest.spyOn(window, 'getComputedStyle').mockReturnValue({ position: 'absolute' } as any);

stageStub.window.getContainer.mockReturnValue(container);

const mountPoint = document.createElement('div');
document.body.appendChild(mountPoint);

act(() => {
ReactDOM.render(<Html divProps={{ id: 'keep-container' }} />, mountPoint);
});

expect(container.style.position).toBe('absolute');

act(() => {
ReactDOM.unmountComponentAtNode(mountPoint);
});

cssSpy.mockRestore();
document.body.removeChild(container);
document.body.removeChild(mountPoint);
});

test('early return when getContainer() is null (div not appended)', () => {
stageStub.window.getContainer.mockReturnValue(null as any);

const mountPoint = document.createElement('div');
document.body.appendChild(mountPoint);

act(() => {
ReactDOM.render(<Html divProps={{ id: 'no-container' }} />, mountPoint);
});

expect(document.getElementById('no-container')).toBeFalsy();

act(() => {
ReactDOM.unmountComponentAtNode(mountPoint);
});

document.body.removeChild(mountPoint);
});

test('early return when groupRef has no stage (div not appended)', () => {
stageForRef = null;

const container = document.createElement('div');
document.body.appendChild(container);
stageStub.window.getContainer.mockReturnValue(container);

const mountPoint = document.createElement('div');
document.body.appendChild(mountPoint);

act(() => {
ReactDOM.render(<Html divProps={{ id: 'no-stage' }} />, mountPoint);
});

expect(container.querySelector('#no-stage')).toBeFalsy();

act(() => {
ReactDOM.unmountComponentAtNode(mountPoint);
});

document.body.removeChild(container);
document.body.removeChild(mountPoint);
});
});
41 changes: 41 additions & 0 deletions packages/react-vrender-utils/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const path = require('path');

module.exports = {
preset: 'ts-jest',
runner: 'jest-electron/runner',
testEnvironment: 'jest-electron/environment',
testTimeout: 30000,
testRegex: '/__tests__/.*test\\.(ts|tsx)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
setupFilesAfterEnv: ['jest-extended/all'],
silent: true,
globals: {
'ts-jest': {
resolveJsonModule: true,
esModuleInterop: true,
experimentalDecorators: true,
module: 'ESNext',
tsconfig: './tsconfig.test.json'
},
__DEV__: true
},
verbose: true,
coverageReporters: ['json-summary', 'lcov', 'text'],
setupFiles: ['./setup-mock.js'],
coveragePathIgnorePatterns: ['node_modules', '__tests__', 'interface.ts', '.d.ts', 'typings', 'type.ts'],
collectCoverageFrom: [
'**/src/**',
'!**/vite/**',
'!**/cjs/**',
'!**/dist/**',
'!**/es/**',
'!**/node_modules/**',
'!**/__tests__/**',
'!**/interface/**',
'!**/interface.ts',
'!**/**.d.ts'
],
moduleNameMapper: {
'@visactor/react-vrender': path.resolve(__dirname, '../react-vrender/src/index.ts')
}
};
9 changes: 8 additions & 1 deletion packages/react-vrender-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"eslint": "eslint --debug --fix src/",
"build": "cross-env DEBUG='Bundler*' bundle",
"dev": "cross-env DEBUG='Bundler*' bundle --clean -f es -w",
"test": "",
"test": "jest",
"test-cov": "jest --coverage",
"test-watch": "cross-env DEBUG_MODE=1 jest --watch",
"start": "vite ./vite"
},
"peerDependencies": {
Expand All @@ -40,6 +42,11 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "3.1.0",
"@types/jest": "^26.0.0",
"jest": "^26.0.0",
"jest-electron": "^0.1.12",
"jest-extended": "^1.2.1",
"ts-jest": "^26.0.0",
"eslint": "~8.18.0",
"vite": "3.2.6",
"typescript": "4.9.5",
Expand Down
2 changes: 2 additions & 0 deletions packages/react-vrender-utils/setup-mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global.__DEV__ = true;
global.__VERSION__ = true;
10 changes: 10 additions & 0 deletions packages/react-vrender-utils/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"paths": {
"@visactor/react-vrender": ["../react-vrender/src"]
}
},
"include": ["src", "__tests__"],
"references": []
}
109 changes: 109 additions & 0 deletions packages/react-vrender/__tests__/unit/Stage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createRoot } from 'react-dom/client';

jest.mock('@visactor/vrender', () => {
return {
createStage: jest.fn(() => ({
stage: null,
set3dOptions: jest.fn(),
setViewBox: jest.fn(),
setDpr: jest.fn(),
resize: jest.fn()
}))
};
});

jest.mock('../../src/hostConfig', () => {
return {
reconcilor: {
createContainer: jest.fn(() => ({ _fiber: true })),
updateContainer: jest.fn()
}
};
});

import { createStage } from '@visactor/vrender';
import { reconcilor } from '../../src/hostConfig';
import { Stage } from '../../src/Stage';

const StageAny: any = Stage;

describe('react-vrender Stage', () => {
test('mount/unmount lifecycle calls', () => {
const container = document.createElement('div');
document.body.appendChild(container);

const root = createRoot(container);
const ref = React.createRef<any>();

act(() => {
root.render(
<StageAny ref={ref} width={200} height={100} stage3dOptions={{ enable: true } as any}>
<></>
</StageAny>
);
});

const stageStub = (createStage as any).mock.results[0].value;
stageStub.stage = stageStub;

expect(createStage).toHaveBeenCalledTimes(1);
expect(reconcilor.createContainer).toHaveBeenCalledTimes(1);
expect(ref.current).toBe(stageStub);
expect(stageStub.set3dOptions).toHaveBeenCalledTimes(1);

act(() => {
root.unmount();
});

expect(reconcilor.updateContainer).toHaveBeenCalled();
});

test('initedRef gate for viewBox/dpr/resize', () => {
const container = document.createElement('div');
document.body.appendChild(container);

const root = createRoot(container);

act(() => {
root.render(
<StageAny
width={200}
height={100}
dpr={2}
viewBox={{ x1: 0, y1: 0, x2: 10, y2: 20 } as any}
>
<></>
</StageAny>
);
});

const stageStub = (createStage as any).mock.results[(createStage as any).mock.results.length - 1].value;
stageStub.stage = stageStub;

// 初次 mount 时不应直接同步 props(受 initedRef 控制)
expect(stageStub.setViewBox).toHaveBeenCalledTimes(0);
expect(stageStub.setDpr).toHaveBeenCalledTimes(0);
expect(stageStub.resize).toHaveBeenCalledTimes(0);

act(() => {
root.render(
<StageAny
width={300}
height={150}
dpr={3}
viewBox={{ x1: 1, y1: 2, x2: 11, y2: 22 } as any}
>
<></>
</StageAny>
);
});

expect(stageStub.setViewBox).toHaveBeenCalledWith(1, 2, 10, 20, false);
expect(stageStub.setDpr).toHaveBeenCalledWith(3);
expect(stageStub.resize).toHaveBeenCalledWith(300, 150);

act(() => root.unmount());
});
});
Loading
Loading