diff --git a/src/Components/Core/Presentation/Babylon/Avatar/AvatarView.ts b/src/Components/Core/Presentation/Babylon/Avatar/AvatarView.ts index 7654b708c..86ef78055 100644 --- a/src/Components/Core/Presentation/Babylon/Avatar/AvatarView.ts +++ b/src/Components/Core/Presentation/Babylon/Avatar/AvatarView.ts @@ -38,7 +38,7 @@ export default class AvatarView { PRESENTATION_TYPES.IMovementIndicator ); - this.viewModel.movementTarget.subscribe(this.onMovementTargetChange); + this.viewModel.movementTarget.subscribe(this.onMovementTargetChanged); } public async asyncSetup(): Promise { @@ -142,7 +142,7 @@ export default class AvatarView { } @bind - private onMovementTargetChange(newTarget: Nullable): void { + private onMovementTargetChanged(newTarget: Nullable): void { if (newTarget === null) this.movementIndicator.hide(); else this.movementIndicator.display(newTarget); } diff --git a/src/Components/CoreTest/Presentation/Babylon/Avatar/AvatarView.test.ts b/src/Components/CoreTest/Presentation/Babylon/Avatar/AvatarView.test.ts index 5c836181b..379befbfb 100644 --- a/src/Components/CoreTest/Presentation/Babylon/Avatar/AvatarView.test.ts +++ b/src/Components/CoreTest/Presentation/Babylon/Avatar/AvatarView.test.ts @@ -1,13 +1,11 @@ import { AnimationGroup, - EventState, + ISceneLoaderAsyncResult, Material, Mesh, MeshBuilder, NullEngine, - Nullable, Observable, - Observer, Scene, Texture, TransformNode, @@ -27,7 +25,6 @@ import PRESENTATION_TYPES from "../../../../Core/DependencyInjection/Presentatio import MovementIndicator from "../../../../Core/Presentation/Babylon/MovementIndicator/MovementIndicator"; import Logger from "../../../../Core/Adapters/Logger/Logger"; import { LearningSpaceTemplateType } from "../../../../Core/Domain/Types/LearningSpaceTemplateType"; -import StateMachine from "../../../../Core/Presentation/Utils/StateMachine/StateMachine"; const movementIndicatorMock = mock(); @@ -53,12 +50,17 @@ function createAvatarView(): [AvatarView, AvatarViewModel] { return [avatarView, viewModel]; } -function setupMockedMesh(): Mesh { +function setupScenePresenterLoadGTLFModelMock(): Mesh { const mockMesh = mockDeep(); mockMesh.material = mockDeep(); mockMesh.material.name = "Eyes_mat"; mockMesh.material.getActiveTextures.mockReturnValue([]); - scenePresenterMock.loadModel.mockResolvedValue([mockMesh]); + const mockLoadingResult = mockDeep(); + // @ts-ignore + mockLoadingResult.meshes = [mockMesh]; + + scenePresenterMock.loadGLTFModel.mockResolvedValue(mockLoadingResult); + return mockMesh; } @@ -90,260 +92,21 @@ describe("AvatarView", () => { CoreDIContainer.restore(); }); - describe("body animations", () => { - test("async setup gets the animations from the scene", async () => { - navigationMock.Crowd.addAgent = jest.fn().mockReturnValue(42); - //@ts-ignore - navigationMock.IsReady = Promise.resolve(); - - scenePresenterMock.Scene.getTransformNodeByName.mockReturnValue( - new TransformNode("AvatarParentNode", new Scene(new NullEngine())) - ); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() - ); - - setupMockedMesh(); - - await systemUnderTest.asyncSetup(); - - expect(systemUnderTest["viewModel"].idleAnimation).toBeDefined(); - expect(systemUnderTest["viewModel"].walkAnimation).toBeDefined(); - }); - - test("setupAvatarAnimations starts idle and walking animation and sets weights", () => { - scenePresenterMock.Scene.getAnimationGroupByName - .mockReturnValueOnce(mockDeep()) - .mockReturnValueOnce(mockDeep()) - .mockReturnValueOnce(mockDeep()); - - systemUnderTest["setupAvatarAnimations"](); - - expect( - systemUnderTest["viewModel"].idleAnimation.play - ).toHaveBeenCalledTimes(1); - expect( - systemUnderTest["viewModel"].walkAnimation.play - ).toHaveBeenCalledTimes(1); - expect( - systemUnderTest["viewModel"].idleAnimation.setWeightForAllAnimatables - ).toHaveBeenCalledTimes(1); - expect( - systemUnderTest["viewModel"].walkAnimation.setWeightForAllAnimatables - ).toHaveBeenCalledTimes(1); - }); - - test("onBeforeAnimationTransitionObserver removes the given observer from the onBeforeAniamtionObservable of the scene when the transition is done", () => { - const fromAnimation = mock(); - const toAnimation = mock(); - const mockObserver = mock>>(); - - systemUnderTest["onBeforeAnimationTransitionObserver"]( - fromAnimation, - toAnimation, - mockObserver, - () => 1 // transition is done within the first call - ); - - expect( - scenePresenterMock.Scene.onBeforeAnimationsObservable.remove - ).toHaveBeenCalledTimes(1); - expect( - scenePresenterMock.Scene.onBeforeAnimationsObservable.remove - ).toHaveBeenCalledWith(mockObserver); - }); - - test("onBeforeAnimationTransitionObserver sets from animation weight to 0 and to animation weigth to 1 when transition is done", () => { - const fromAnimation = mock(); - const toAnimation = mock(); - const mockObserver = mock>>(); - - systemUnderTest["onBeforeAnimationTransitionObserver"]( - fromAnimation, - toAnimation, - mockObserver, - () => 1 // transition is done within the first call - ); - - expect(fromAnimation.setWeightForAllAnimatables).toHaveBeenCalledTimes(1); - expect(fromAnimation.setWeightForAllAnimatables).toHaveBeenCalledWith(0); - expect(toAnimation.setWeightForAllAnimatables).toHaveBeenCalledTimes(1); - expect(toAnimation.setWeightForAllAnimatables).toHaveBeenCalledWith(1); - }); - - test("onBeforeAnimationTransitionObserver sets from animation weight to 1-increment and to animation weigth to increment for one step of the transition", () => { - const fromAnimation = mock(); - const toAnimation = mock(); - const mockObserver = mock>>(); - - systemUnderTest["onBeforeAnimationTransitionObserver"]( - fromAnimation, - toAnimation, - mockObserver, - () => 0.1 - ); - - expect(fromAnimation.setWeightForAllAnimatables).toHaveBeenCalledTimes(1); - expect(fromAnimation.setWeightForAllAnimatables).toHaveBeenCalledWith( - 0.9 - ); - expect(toAnimation.setWeightForAllAnimatables).toHaveBeenCalledTimes(1); - expect(toAnimation.setWeightForAllAnimatables).toHaveBeenCalledWith(0.1); - }); - - test("transitionFromIdleToWalk resets animationBlendValue to 0", () => { - systemUnderTest["transitionFromIdleToWalk"](); - - expect(systemUnderTest["animationBlendValue"]).toBe(0); - }); - - test("anonymous callback function on onBeforeAnimationsObservable in transitionFromIdleToWalk doesn't throw", () => { - viewModel.idleAnimation = mock(); - viewModel.walkAnimation = mock(); - navigationMock.Crowd.getAgentVelocity = jest - .fn() - .mockReturnValue(new Vector3(1, 2, 3)); - - // setup mock implementation to get a reference to the anonymous callback function - let anonymousCallback: (eventData: Scene, eventState: EventState) => void; - scenePresenterMock.Scene.onBeforeAnimationsObservable.add.mockImplementation( - (callback) => { - anonymousCallback = callback; - return mock>(); - } - ); - systemUnderTest["transitionFromIdleToWalk"](); - - expect(() => - anonymousCallback!(mock(), mock()) - ).not.toThrow(); - }); - - test("getTimedAnimationInterpolationIncrement returns a number", () => { - navigationMock.Crowd.getAgentVelocity = jest - .fn() - .mockReturnValue(new Vector3(1, 2, 3)); - - const result = - systemUnderTest["getTimedAnimationInterpolationIncrement"](100); - - expect(typeof result).toBe("number"); - }); - - test("transitionFromWalkToIdle resets animationBlendValue to 0", () => { - systemUnderTest["transitionFromWalkToIdle"](); - - expect(systemUnderTest["animationBlendValue"]).toBe(0); - }); - - test("anonymous callback function on onBeforeAnimationsObservable in transitionFromWalkToIdle doesn't throw", () => { - viewModel.idleAnimation = mock(); - viewModel.walkAnimation = mock(); - - // setup mock implementation to get a reference to the anonymous callback function - let anonymousCallback: (eventData: Scene, eventState: EventState) => void; - scenePresenterMock.Scene.onBeforeAnimationsObservable.add.mockImplementation( - (callback) => { - anonymousCallback = callback; - return mock>(); - } - ); - systemUnderTest["transitionFromWalkToIdle"](); - - expect(() => - anonymousCallback!(mock(), mock()) - ).not.toThrow(); - }); - - test("getVelocityAnimationInterpolationIncrement returns the a number", () => { - navigationMock.Crowd.getAgentVelocity = jest - .fn() - .mockReturnValue(new Vector3(1, 2, 3)); - - const result = - systemUnderTest["getVelocityAnimationInterpolationIncrement"](); - - expect(typeof result).toBe("number"); - }); - - test("transitionFromIdleOrWalkToInteract resets animationBlendValue to 0", () => { - systemUnderTest["viewModel"].animationStateMachine = new StateMachine< - AvatarAnimationState, - AvatarAnimationAction - >(AvatarAnimationState.Idle, []); - systemUnderTest["viewModel"].interactionAnimation = - mock(); - - systemUnderTest["transitionFromIdleOrWalkToInteract"](); - - expect(systemUnderTest["animationBlendValue"]).toBe(0); - }); - - test("transitionFromIdleOrWalkToInteract plays the interactionAnimation non-looping", () => { - viewModel.animationStateMachine = new StateMachine< - AvatarAnimationState, - AvatarAnimationAction - >(AvatarAnimationState.Walking, []); - viewModel.interactionAnimation = mock(); - - systemUnderTest["transitionFromIdleOrWalkToInteract"](); - - expect(viewModel.interactionAnimation.play).toHaveBeenCalledTimes(1); - expect(viewModel.interactionAnimation.play).toHaveBeenCalledWith(false); - }); - - test("anonymous observer function on onBeforeAnimationsObservable in transitionFromIdleOrWalkToInteract doesn't throw", () => { - viewModel.idleAnimation = mock(); - viewModel.walkAnimation = mock(); - viewModel.interactionAnimation = mock(); - viewModel.animationStateMachine = new StateMachine< - AvatarAnimationState, - AvatarAnimationAction - >(AvatarAnimationState.Idle, []); - - // setup mock implementation to get a reference to the anonymous callback function - let anonymousCallback: (eventData: Scene, eventState: EventState) => void; - scenePresenterMock.Scene.onBeforeAnimationsObservable.add.mockImplementation( - (callback) => { - anonymousCallback = callback; - return mock>(); - } - ); - - systemUnderTest["transitionFromIdleOrWalkToInteract"](); - - expect(() => - anonymousCallback!(mock(), mock()) - ).not.toThrow(); - }); - - test("transitionFromInteractToIdle resets animationBlendValue to 0", () => { - systemUnderTest["transitionFromInteractToIdle"](); - - expect(systemUnderTest["animationBlendValue"]).toBe(0); - }); - - test("anonymous observer function on onBeforeAnimationsObservable in transitionFromInteractToIdle doesn't throw", () => { - viewModel.idleAnimation = mock(); - viewModel.interactionAnimation = mock(); - - // setup mock implementation to get a reference to the anonymous callback function - let anonymousCallback: (eventData: Scene, eventState: EventState) => void; - scenePresenterMock.Scene.onBeforeAnimationsObservable.add.mockImplementation( - (callback) => { - anonymousCallback = callback; - return mock>(); - } - ); - systemUnderTest["transitionFromInteractToIdle"](); - - expect(() => - anonymousCallback!(mock(), mock()) - ).not.toThrow(); + describe("blink animation", () => { + test("setupBlinkAnimation gets the eye texture from the loaded meshes", () => { + const mockEyeMaterial = mockDeep(); + mockEyeMaterial.name = "Eyes_mat"; + const mockMesh = new Mesh("mockMesh", new Scene(new NullEngine())); + mockMesh.material = mockEyeMaterial; + const mockEyeTexture = mock(); + mockEyeMaterial.getActiveTextures.mockReturnValue([mockEyeTexture]); + viewModel.meshes = [mockMesh]; + + systemUnderTest["setupBlinkAnimation"](); + + expect(viewModel.eyeTextures).toEqual([mockEyeTexture]); }); - }); - describe("blink animation", () => { test("setBlinkTimeout calls setTimeout with a number between blinkInterval and blinkInterval plus blickIntervalMaxOffset", () => { jest.useFakeTimers(); const setTimeoutMock = jest.spyOn(global, "setTimeout"); @@ -411,51 +174,31 @@ describe("AvatarView", () => { }); describe("model loading", () => { - test("async setup calls the scenePresenter to load avatar models", async () => { - navigationMock.Crowd.addAgent = jest.fn().mockReturnValue(42); - //@ts-ignore - navigationMock.IsReady = Promise.resolve(); - + test("loadAvatarAsync calls the scenePresenter to load avatar models", async () => { scenePresenterMock.Scene.getTransformNodeByName.mockReturnValue( new TransformNode("AvatarParentNode", new Scene(new NullEngine())) ); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() - ); + setupScenePresenterLoadGTLFModelMock(); - setupMockedMesh(); + await systemUnderTest["loadAvatarAsync"](); - await systemUnderTest.asyncSetup(); - - expect(scenePresenterMock.loadModel).toHaveBeenCalledTimes(1); + expect(scenePresenterMock.loadGLTFModel).toHaveBeenCalledTimes(1); }); - test("async setup gets the parent node for the avatar", async () => { - navigationMock.Crowd.addAgent = jest.fn().mockReturnValue(42); - //@ts-ignore - navigationMock.IsReady = Promise.resolve(); - + test("loadAvatarAsync gets the parent node for the avatar", async () => { scenePresenterMock.Scene.getTransformNodeByName.mockReturnValue( new TransformNode("AvatarParentNode", new Scene(new NullEngine())) ); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() - ); - - setupMockedMesh(); + setupScenePresenterLoadGTLFModelMock(); - await systemUnderTest.asyncSetup(); + await systemUnderTest["loadAvatarAsync"](); expect( scenePresenterMock.Scene.getTransformNodeByName ).toHaveBeenCalledWith("AvatarParentNode"); }); - test("async setup sets the parent node as parent of the first loaded mesh", async () => { - navigationMock.Crowd.addAgent = jest.fn().mockReturnValue(42); - //@ts-ignore - navigationMock.IsReady = Promise.resolve(); - + test("loadAvatarAsync sets the parent node as parent of the first loaded mesh", async () => { const parentNode = new TransformNode( "AvatarParentNode", new Scene(new NullEngine()) @@ -463,151 +206,75 @@ describe("AvatarView", () => { scenePresenterMock.Scene.getTransformNodeByName.mockReturnValue( parentNode ); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() - ); - const mockMesh = setupMockedMesh(); + const mockMesh = setupScenePresenterLoadGTLFModelMock(); - await systemUnderTest.asyncSetup(); + await systemUnderTest["loadAvatarAsync"](); expect(mockMesh.setParent).toHaveBeenCalledTimes(1); expect(mockMesh.setParent).toHaveBeenCalledWith(parentNode); }); - }); - - describe("navigation/movement", () => { - test("onReachMovementTarget calls hide on the indicator when the parentNode position and the finalMovementTarget are close enough", () => { - viewModel.movementTarget = new Vector3(1, 1, 1.2); - viewModel.parentNode = new TransformNode( - "parentNode", - new Scene(new NullEngine()) - ); - viewModel.parentNode.position = new Vector3(1, 1, 1); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() - ); - systemUnderTest["setupAvatarAnimations"](); - - systemUnderTest["onReachMovementTarget"](); - - expect(movementIndicatorMock.hide).toHaveBeenCalledTimes(1); - }); - - test("onReachMovementTarget resets the movementTarget to null", () => { - viewModel.movementTarget = new Vector3(1, 1, 1.2); - viewModel.parentNode = new TransformNode( - "parentNode", - new Scene(new NullEngine()) - ); - viewModel.parentNode.position = new Vector3(1, 1, 1); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() - ); - systemUnderTest["setupAvatarAnimations"](); - - systemUnderTest["onReachMovementTarget"](); - - expect(viewModel.movementTarget.Value).toBeNull(); - }); - test("onReachMovementTarget calls navigation.Crowd.agentTeleport with the parentNode position", () => { - viewModel.movementTarget = new Vector3(1, 1, 1.2); - viewModel.parentNode = new TransformNode( - "parentNode", - new Scene(new NullEngine()) - ); - viewModel.parentNode.position = new Vector3(42, 43, 44); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() + test("loadAvatarAsync gets idleAnimation from loading results", async () => { + scenePresenterMock.Scene.getTransformNodeByName.mockReturnValue( + new TransformNode("AvatarParentNode", new Scene(new NullEngine())) ); - systemUnderTest["setupAvatarAnimations"](); + const mockAnimationGroup = mockDeep(); + mockAnimationGroup.name = "IdleAnimation"; + const mockLoadingResult = mockDeep(); + // @ts-ignore + mockLoadingResult.animationGroups = [mockAnimationGroup]; + scenePresenterMock.loadGLTFModel.mockResolvedValue(mockLoadingResult); - systemUnderTest["onReachMovementTarget"](); + await systemUnderTest["loadAvatarAsync"](); - expect(navigationMock.Crowd.agentTeleport).toHaveBeenCalledTimes(1); - expect(navigationMock.Crowd.agentTeleport).toHaveBeenCalledWith( - viewModel.agentIndex, - viewModel.parentNode.position - ); + expect(systemUnderTest["idleAnimation"]).toBe(mockAnimationGroup); }); - }); - - describe("debug", () => { - test("debug_displayVelocity calls MeshBuilder.CreateDashedLines", async () => { - navigationMock.Crowd.addAgent = jest.fn().mockReturnValue(42); - //@ts-ignore - navigationMock.IsReady = Promise.resolve(); + test("loadAvatarAsync gets walkAnimation from loading results", async () => { scenePresenterMock.Scene.getTransformNodeByName.mockReturnValue( new TransformNode("AvatarParentNode", new Scene(new NullEngine())) ); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() - ); - - setupMockedMesh(); - - navigationMock.Crowd.getAgentPosition = jest - .fn() - .mockReturnValue(new Vector3(1, 2, 3)); - navigationMock.Crowd.getAgentVelocity = jest - .fn() - .mockReturnValue(new Vector3(1, 2, 3)); - - MeshBuilder.CreateDashedLines = jest.fn(); + const mockAnimationGroup = mockDeep(); + mockAnimationGroup.name = "WalkCycle"; + const mockLoadingResult = mockDeep(); + // @ts-ignore + mockLoadingResult.animationGroups = [mockAnimationGroup]; + scenePresenterMock.loadGLTFModel.mockResolvedValue(mockLoadingResult); - await systemUnderTest.asyncSetup(); - systemUnderTest["viewModel"].parentNode.position = new Vector3(0, 0, 0); // reset position to unsnapped position + await systemUnderTest["loadAvatarAsync"](); - systemUnderTest["debug_displayVelocity"]( - systemUnderTest["viewModel"], - systemUnderTest["scenePresenter"], - new Vector3(1, 2, 3) - ); - - expect(MeshBuilder.CreateDashedLines).toHaveBeenCalledTimes(1); + expect(systemUnderTest["walkAnimation"]).toBe(mockAnimationGroup); }); - test("debug_displayVelocity calls logger.log", async () => { - navigationMock.Crowd.addAgent = jest.fn().mockReturnValue(42); - //@ts-ignore - navigationMock.IsReady = Promise.resolve(); - - const parentNode = new TransformNode( - "AvatarParentNode", - new Scene(new NullEngine()) - ); + test("loadAvatarAsync gets interactionAnimation from loading results", async () => { scenePresenterMock.Scene.getTransformNodeByName.mockReturnValue( - parentNode - ); - scenePresenterMock.Scene.getAnimationGroupByName.mockReturnValue( - mockDeep() + new TransformNode("AvatarParentNode", new Scene(new NullEngine())) ); + const mockAnimationGroup = mockDeep(); + mockAnimationGroup.name = "Interact"; + const mockLoadingResult = mockDeep(); + // @ts-ignore + mockLoadingResult.animationGroups = [mockAnimationGroup]; + scenePresenterMock.loadGLTFModel.mockResolvedValue(mockLoadingResult); - setupMockedMesh(); + await systemUnderTest["loadAvatarAsync"](); - navigationMock.Crowd.getAgentPosition = jest - .fn() - .mockReturnValue(new Vector3(1, 2, 3)); - navigationMock.Crowd.getAgentVelocity = jest - .fn() - .mockReturnValue(new Vector3(1, 2, 3)); - - MeshBuilder.CreateDashedLines = jest.fn(); + expect(systemUnderTest["interactionAnimation"]).toBe(mockAnimationGroup); + }); + }); - const loggerMock = jest.spyOn(Logger.prototype, "log"); + describe("movement indicator", () => { + test("onMovementTargetChanged calls movementIndicator.display when movement target is set", () => { + systemUnderTest["onMovementTargetChanged"](new Vector3(1, 2, 3)); - await systemUnderTest.asyncSetup(); - systemUnderTest["viewModel"].parentNode.position = new Vector3(0, 0, 0); // reset position to unsnapped position + expect(movementIndicatorMock.display).toHaveBeenCalledTimes(1); + }); - systemUnderTest["debug_displayVelocity"]( - systemUnderTest["viewModel"], - systemUnderTest["scenePresenter"], - new Vector3(1, 2, 3) - ); + test("onMovementTargetChanged calls movementIndicator.hide when movement target is set to null", () => { + systemUnderTest["onMovementTargetChanged"](null); - expect(loggerMock).toHaveBeenCalledTimes(1); + expect(movementIndicatorMock.hide).toHaveBeenCalledTimes(1); }); }); });