Skip to content

Commit

Permalink
Add SceneViewer and throw sceneLoader error to ErrorBoundary (#38)
Browse files Browse the repository at this point in the history
Co-authored-by: Xinyi Xu <[email protected]>
  • Loading branch information
sheilaXu and Xinyi Xu authored Aug 1, 2022
1 parent a4069c3 commit 3419a5f
Show file tree
Hide file tree
Showing 24 changed files with 1,175 additions and 1,266 deletions.
70 changes: 70 additions & 0 deletions packages/scene-composer/src/SceneViewer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable */
const mockSceneComposerApi = {
findSceneNodeRefBy: jest.fn(),
setCameraTarget: jest.fn(),
setSelectedSceneNodeRef: jest.fn(),
};

jest.doMock('./components/SceneComposerInternal', () => {
const original = jest.requireActual('./components/SceneComposerInternal');
return {
...original,
SceneComposerInternal: 'SceneComposerInternal',
useSceneComposerApi: () => mockSceneComposerApi,
}
});

import * as React from 'react';
import renderer, { act } from 'react-test-renderer';
import { useStore } from './store';
import { SceneViewer } from './SceneViewer';
import { KnownComponentType } from './interfaces';
/* eslint-enable */

describe('SceneViewer', () => {
const mockGetSceneObjectFunction = jest.fn();
const mockSceneLoader = {
getSceneUri: () => Promise.resolve('https://test.url'),
getSceneUrl: () => Promise.resolve('https://test.url'),
getSceneObject: mockGetSceneObjectFunction,
};

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', async () => {
let container;
act(() => {
container = renderer.create(<SceneViewer sceneComposerId={'123'} sceneLoader={mockSceneLoader} config={{}} />);
});

expect(container).toMatchSnapshot();
});

it('should call sceneComposerApis when selectedDataBinding is available', async () => {
const mockNodeRef = ['node-123'];
mockSceneComposerApi.findSceneNodeRefBy.mockReturnValueOnce(mockNodeRef);
const mockLabel = { label: 'label-1' };

let container;
act(() => {
container = renderer.create(
<SceneViewer sceneLoader={mockSceneLoader} config={{}} selectedDataBinding={mockLabel} />,
);
});

expect(mockSceneComposerApi.findSceneNodeRefBy).toBeCalledTimes(1);
expect(mockSceneComposerApi.findSceneNodeRefBy).toBeCalledWith(mockLabel, [KnownComponentType.Tag]);
expect(mockSceneComposerApi.setCameraTarget).toBeCalledTimes(1);
expect(mockSceneComposerApi.setCameraTarget).toBeCalledWith(mockNodeRef[0], 'transition');
expect(mockSceneComposerApi.setSelectedSceneNodeRef).toBeCalledTimes(1);
expect(mockSceneComposerApi.setSelectedSceneNodeRef).toBeCalledWith(mockNodeRef[0]);

// not re-setting camera with same data binding values
container.update(<SceneViewer sceneLoader={mockSceneLoader} config={{}} selectedDataBinding={mockLabel} />);

expect(mockSceneComposerApi.findSceneNodeRefBy).toBeCalledTimes(1);
expect(mockSceneComposerApi.setCameraTarget).toBeCalledTimes(1);
});
});
64 changes: 64 additions & 0 deletions packages/scene-composer/src/SceneViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { v4 as uuid } from 'uuid';
import { isEqual } from 'lodash';
import styled from 'styled-components';

import { IAnchorEvent, KnownComponentType, SceneViewerProps } from './interfaces';
import { SceneComposerInternal, useSceneComposerApi } from './components/SceneComposerInternal';

const SceneComposerContainer = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
canvas {
outline: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */
}
`;

export const SceneViewer: React.FC<SceneViewerProps> = ({ sceneComposerId, config, ...props }: SceneViewerProps) => {
const composerId = useMemo(() => {
return sceneComposerId || uuid();
}, [sceneComposerId]);
const composerApis = useSceneComposerApi(composerId);
const prevSelectedRef: any = useRef();

useEffect(() => {
if (isEqual(prevSelectedRef.current, props.selectedDataBinding)) {
return;
}
prevSelectedRef.current = props.selectedDataBinding;

const nodeRefs = composerApis.findSceneNodeRefBy(props.selectedDataBinding || '', [KnownComponentType.Tag]);
if (nodeRefs && nodeRefs.length > 0) {
// TODO: auto select the first node for now, handle multiple nodes selection later.
composerApis.setCameraTarget(nodeRefs[0], 'transition');
composerApis.setSelectedSceneNodeRef(nodeRefs[0]);
} else {
composerApis.setSelectedSceneNodeRef(undefined);
}
}, [props.selectedDataBinding]);

const onAnchorClick = (data: IAnchorEvent) => {
if (props.onTargetObjectChanged) {
props.onTargetObjectChanged({ data: data });
}
};

return (
<SceneComposerContainer data-testid={'webgl-root'}>
<SceneComposerInternal
sceneComposerId={composerId}
onAnchorClick={onAnchorClick}
config={{
...config,
mode: 'Viewing',
}}
{...props}
/>
</SceneComposerContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SceneViewer should render correctly 1`] = `
.c0 {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.c0 canvas {
outline: none;
-webkit-tap-highlight-color: rgba(255,255,255,0);
}
<div
className="c0"
data-testid="webgl-root"
>
<SceneComposerInternal
config={
Object {
"mode": "Viewing",
}
}
onAnchorClick={[Function]}
sceneComposerId="123"
sceneLoader={
Object {
"getSceneObject": [MockFunction],
"getSceneUri": [Function],
"getSceneUrl": [Function],
}
}
/>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import * as React from 'react';
import renderer, { act } from 'react-test-renderer';
import str2ab from 'string-to-arraybuffer';

import { COMPOSER_FEATURES, SceneComposer, SceneComposerApi, setFeatureConfig, useSceneComposerApi } from '../src';
import * as SceneLayoutComponents from '../src/layouts/scene-layout';
import { SceneComposerInternal, SceneComposerApi, useSceneComposerApi } from '..';
import * as SceneLayoutComponents from '../layouts/scene-layout';

import ResizeObserver from './__mocks__/ResizeObserver';
import { invalidTestScenes, testScenes } from './testData';
import ResizeObserver from '../../__mocks__/ResizeObserver';
import { invalidTestScenes, testScenes } from '../../tests/testData';

jest.mock('../src/components/StaticLayout', () => ({
jest.mock('./StaticLayout', () => ({
StaticLayout: 'StaticLayout',
}));

Expand All @@ -28,17 +28,16 @@ function createSceneLoaderMock(sceneContent: string) {
};
}

describe('SceneComposer', () => {
describe('SceneComposerInternal', () => {
beforeEach(() => {
jest.clearAllMocks();
setFeatureConfig({});
});

it('should render correctly with an empty scene in editing mode', async () => {
let container;
act(() => {
container = renderer.create(
<SceneComposer config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock('')} />,
<SceneComposerInternal config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock('')} />,
);
});

Expand All @@ -52,7 +51,7 @@ describe('SceneComposer', () => {
let container;
act(() => {
container = renderer.create(
<SceneComposer config={{ mode: 'Viewing' }} sceneLoader={createSceneLoaderMock('')} />,
<SceneComposerInternal config={{ mode: 'Viewing' }} sceneLoader={createSceneLoaderMock('')} />,
);
});

Expand All @@ -66,7 +65,7 @@ describe('SceneComposer', () => {
let container;
act(() => {
container = renderer.create(
<SceneComposer config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene1)} />,
<SceneComposerInternal config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene1)} />,
);
});

Expand All @@ -80,7 +79,7 @@ describe('SceneComposer', () => {
let container;
act(() => {
container = renderer.create(
<SceneComposer
<SceneComposerInternal
config={{ mode: 'Editing' }}
sceneLoader={createSceneLoaderMock(invalidTestScenes.unsupportedMinorVersionScene)}
/>,
Expand All @@ -96,7 +95,7 @@ describe('SceneComposer', () => {
let container;
act(() => {
container = renderer.create(
<SceneComposer
<SceneComposerInternal
config={{ mode: 'Editing' }}
sceneLoader={createSceneLoaderMock(invalidTestScenes.unsupportedMajorVersion)}
/>,
Expand All @@ -112,7 +111,7 @@ describe('SceneComposer', () => {
let container;
act(() => {
container = renderer.create(
<SceneComposer
<SceneComposerInternal
config={{ mode: 'Editing' }}
sceneLoader={createSceneLoaderMock(invalidTestScenes.invalidSpecVersionScene)}
/>,
Expand All @@ -125,14 +124,12 @@ describe('SceneComposer', () => {
});

it('should support rendering multiple valid scenes', async () => {
setFeatureConfig({ [COMPOSER_FEATURES.MOTION_INDICATOR]: true });

let container;
act(() => {
container = renderer.create(
<div>
<SceneComposer config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene1)} />
<SceneComposer config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene2)} />
<SceneComposerInternal config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene1)} />
<SceneComposerInternal config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene2)} />
</div>,
);
});
Expand All @@ -148,11 +145,11 @@ describe('SceneComposer', () => {
act(() => {
container = renderer.create(
<div>
<SceneComposer
<SceneComposerInternal
config={{ mode: 'Editing' }}
sceneLoader={createSceneLoaderMock(invalidTestScenes.invalidJson)}
/>
<SceneComposer config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene1)} />
<SceneComposerInternal config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock(testScenes.scene1)} />
</div>,
);
});
Expand All @@ -166,7 +163,7 @@ describe('SceneComposer', () => {
let container;
act(() => {
container = renderer.create(
<SceneComposer
<SceneComposerInternal
config={{ mode: 'Editing' }}
sceneLoader={createSceneLoaderMock(invalidTestScenes.invalidJson)}
/>,
Expand All @@ -184,7 +181,7 @@ describe('SceneComposer', () => {
});

const container = renderer.create(
<SceneComposer config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock('')} />,
<SceneComposerInternal config={{ mode: 'Editing' }} sceneLoader={createSceneLoaderMock('')} />,
);

await new Promise((resolve) => setTimeout(resolve, 1));
Expand All @@ -203,7 +200,7 @@ describe('SceneComposer', () => {
sut = useSceneComposerApi('test');

return (
<SceneComposer
<SceneComposerInternal
sceneComposerId={sceneComposerId}
config={{ mode: 'Editing' }}
sceneLoader={createSceneLoaderMock(testScenes.scene1)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, { useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { ThemeProvider } from 'styled-components';
import { applyMode, Mode } from '@awsui/global-styles';

import LogProvider from './logger/react-logger/log-provider';
import IntlProvider from './components/IntlProvider';
import { darkTheme, lightTheme } from './theme';
import { GlobalStyles } from './GlobalStyles';
import { useStore } from './store';
import { sceneComposerIdContext } from './common/sceneComposerIdContext';
import { generateUUID } from './utils/mathUtils';
import StateManager from './components/StateManager';
import DefaultErrorFallback from './components/DefaultErrorFallback';
import { SCENE_BODY_CLASS } from './common/constants';
import { SceneComposerInternalProps } from './interfaces';
import LogProvider from '../logger/react-logger/log-provider';
import { darkTheme, lightTheme } from '../theme';
import { GlobalStyles } from '../GlobalStyles';
import { useStore } from '../store';
import { sceneComposerIdContext } from '../common/sceneComposerIdContext';
import { generateUUID } from '../utils/mathUtils';
import { SCENE_BODY_CLASS } from '../common/constants';
import { SceneComposerInternalProps } from '../interfaces';

import StateManager from './StateManager';
import DefaultErrorFallback from './DefaultErrorFallback';
import IntlProvider from './IntlProvider';

export const SceneComposerInternal: React.FC<SceneComposerInternalProps> = ({
sceneComposerId,
Expand All @@ -31,6 +33,14 @@ export const SceneComposerInternal: React.FC<SceneComposerInternalProps> = ({
// label body as being used by the scene. this allows other components to identify it vs. other page components
document.body.className = document.body.className + ' ' + SCENE_BODY_CLASS;

useEffect(() => {
if (config.colorTheme === 'light') {
applyMode(Mode.Light);
} else {
applyMode(Mode.Dark);
}
}, [config.colorTheme]);

return (
<ThemeProvider theme={theme}>
<GlobalStyles />
Expand Down
Loading

0 comments on commit 3419a5f

Please sign in to comment.