<commit_before>/**
 * @author mrdoob / http://mrdoob.com/
 */

import { EventDispatcher } from '../../core/EventDispatcher.js';
import { Group } from '../../objects/Group.js';
import { Matrix4 } from '../../math/Matrix4.js';
import { Vector2 } from '../../math/Vector2.js';
import { Vector3 } from '../../math/Vector3.js';
import { Vector4 } from '../../math/Vector4.js';
import { Quaternion } from '../../math/Quaternion.js';
import { ArrayCamera } from '../../cameras/ArrayCamera.js';
import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
import { WebGLAnimation } from '../webgl/WebGLAnimation.js';
import { setProjectionFromUnion } from './WebVRUtils.js';

function WebVRManager( renderer ) {

	var renderWidth, renderHeight;
	var scope = this;

	var device = null;
	var frameData = null;

	var poseTarget = null;

	var controllers = [];
	var standingMatrix = new Matrix4();
	var standingMatrixInverse = new Matrix4();

	var framebufferScaleFactor = 1.0;

	var referenceSpaceType = 'local-floor';

	if ( typeof window !== 'undefined' && 'VRFrameData' in window ) {

		frameData = new window.VRFrameData();
		window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false );

	}

	var matrixWorldInverse = new Matrix4();
	var tempQuaternion = new Quaternion();
	var tempPosition = new Vector3();

	var cameraL = new PerspectiveCamera();
	cameraL.viewport = new Vector4();
	cameraL.layers.enable( 1 );

	var cameraR = new PerspectiveCamera();
	cameraR.viewport = new Vector4();
	cameraR.layers.enable( 2 );

	var cameraVR = new ArrayCamera( [ cameraL, cameraR ] );
	cameraVR.layers.enable( 1 );
	cameraVR.layers.enable( 2 );

	//

	function isPresenting() {

		return device !== null && device.isPresenting === true;

	}

	var currentSize = new Vector2(), currentPixelRatio;

	function onVRDisplayPresentChange() {

		if ( isPresenting() ) {

			var eyeParameters = device.getEyeParameters( 'left' );
			renderWidth = 2 * eyeParameters.renderWidth * framebufferScaleFactor;
			renderHeight = eyeParameters.renderHeight * framebufferScaleFactor;

			currentPixelRatio = renderer.getPixelRatio();
			renderer.getSize( currentSize );

			renderer.setDrawingBufferSize( renderWidth, renderHeight, 1 );

			cameraL.viewport.set( 0, 0, renderWidth / 2, renderHeight );
			cameraR.viewport.set( renderWidth / 2, 0, renderWidth / 2, renderHeight );

			animation.start();

			scope.dispatchEvent( { type: 'sessionstart' } );

		} else {

			if ( scope.enabled ) {

				renderer.setDrawingBufferSize( currentSize.width, currentSize.height, currentPixelRatio );

			}

			animation.stop();

			scope.dispatchEvent( { type: 'sessionend' } );

		}

	}

	//

	var triggers = [];

	function findGamepad( id ) {

		var gamepads = navigator.getGamepads && navigator.getGamepads();

		for ( var i = 0, j = 0, l = gamepads.length; i < l; i ++ ) {

			var gamepad = gamepads[ i ];

			if ( gamepad && ( gamepad.id === 'Daydream Controller' ||
				gamepad.id === 'Gear VR Controller' || gamepad.id === 'Oculus Go Controller' ||
				gamepad.id === 'OpenVR Gamepad' || gamepad.id.startsWith( 'Oculus Touch' ) ||
				gamepad.id.startsWith( 'Spatial Controller' ) ) ) {

				if ( j === id ) return gamepad;

				j ++;

			}

		}

	}

	function updateControllers() {

		for ( var i = 0; i < controllers.length; i ++ ) {

			var controller = controllers[ i ];

			var gamepad = findGamepad( i );

			if ( gamepad !== undefined && gamepad.pose !== undefined ) {

				if ( gamepad.pose === null ) return;

				// Pose

				var pose = gamepad.pose;

				if ( pose.hasPosition === false ) controller.position.set( 0.2, - 0.6, - 0.05 );

				if ( pose.position !== null ) controller.position.fromArray( pose.position );
				if ( pose.orientation !== null ) controller.quaternion.fromArray( pose.orientation );
				controller.matrix.compose( controller.position, controller.quaternion, controller.scale );
				controller.matrix.premultiply( standingMatrix );
				controller.matrix.decompose( controller.position, controller.quaternion, controller.scale );
				controller.matrixWorldNeedsUpdate = true;
				controller.visible = true;

				// Trigger

				var buttonId = gamepad.id === 'Daydream Controller' ? 0 : 1;

				if ( triggers[ i ] === undefined ) triggers[ i ] = false;

				if ( triggers[ i ] !== gamepad.buttons[ buttonId ].pressed ) {

					triggers[ i ] = gamepad.buttons[ buttonId ].pressed;

					if ( triggers[ i ] === true ) {

						controller.dispatchEvent( { type: 'selectstart' } );

					} else {

						controller.dispatchEvent( { type: 'selectend' } );
						controller.dispatchEvent( { type: 'select' } );

					}

				}

			} else {

				controller.visible = false;

			}

		}

	}

	function updateViewportFromBounds( viewport, bounds ) {

		if ( bounds !== null && bounds.length === 4 ) {

			viewport.set( bounds[ 0 ] * renderWidth, bounds[ 1 ] * renderHeight, bounds[ 2 ] * renderWidth, bounds[ 3 ] * renderHeight );

		}

	}

	//

	this.enabled = false;

	this.getController = function ( id ) {

		var controller = controllers[ id ];

		if ( controller === undefined ) {

			controller = new Group();
			controller.matrixAutoUpdate = false;
			controller.visible = false;

			controllers[ id ] = controller;

		}

		return controller;

	};

	this.getDevice = function () {

		return device;

	};

	this.setDevice = function ( value ) {

		if ( value !== undefined ) device = value;

		animation.setContext( value );

	};

	this.setFramebufferScaleFactor = function ( value ) {

		framebufferScaleFactor = value;

	};

	this.setReferenceSpaceType = function ( value ) {

		referenceSpaceType = value;

	};

	this.setPoseTarget = function ( object ) {

		if ( object !== undefined ) poseTarget = object;

	};

	this.getCamera = function ( camera ) {

		var userHeight = referenceSpaceType === 'local-floor' ? 1.6 : 0;

		if ( isPresenting() === false ) {

			camera.position.set( 0, userHeight, 0 );
			camera.rotation.set( 0, 0, 0 );

			return camera;

		}

		device.depthNear = camera.near;
		device.depthFar = camera.far;

		device.getFrameData( frameData );

		//

		if ( referenceSpaceType === 'local-floor' ) {

			var stageParameters = device.stageParameters;

			if ( stageParameters ) {

				standingMatrix.fromArray( stageParameters.sittingToStandingTransform );

			} else {

				standingMatrix.makeTranslation( 0, userHeight, 0 );

			}

		}

		var pose = frameData.pose;
		var poseObject = poseTarget !== null ? poseTarget : camera;

		// We want to manipulate poseObject by its position and quaternion components since users may rely on them.
		poseObject.matrix.copy( standingMatrix );
		poseObject.matrix.decompose( poseObject.position, poseObject.quaternion, poseObject.scale );

		if ( pose.orientation !== null ) {

			tempQuaternion.fromArray( pose.orientation );
			poseObject.quaternion.multiply( tempQuaternion );

		}

		if ( pose.position !== null ) {

			tempQuaternion.setFromRotationMatrix( standingMatrix );
			tempPosition.fromArray( pose.position );
			tempPosition.applyQuaternion( tempQuaternion );
			poseObject.position.add( tempPosition );

		}

		poseObject.updateMatrixWorld();

		//

		cameraL.near = camera.near;
		cameraR.near = camera.near;

		cameraL.far = camera.far;
		cameraR.far = camera.far;

		cameraL.matrixWorldInverse.fromArray( frameData.leftViewMatrix );
		cameraR.matrixWorldInverse.fromArray( frameData.rightViewMatrix );

		// TODO (mrdoob) Double check this code

		standingMatrixInverse.getInverse( standingMatrix );

		if ( referenceSpaceType === 'local-floor' ) {

			cameraL.matrixWorldInverse.multiply( standingMatrixInverse );
			cameraR.matrixWorldInverse.multiply( standingMatrixInverse );

		}

		var parent = poseObject.parent;

		if ( parent !== null ) {

			matrixWorldInverse.getInverse( parent.matrixWorld );

			cameraL.matrixWorldInverse.multiply( matrixWorldInverse );
			cameraR.matrixWorldInverse.multiply( matrixWorldInverse );

		}

		// envMap and Mirror needs camera.matrixWorld

		cameraL.matrixWorld.getInverse( cameraL.matrixWorldInverse );
		cameraR.matrixWorld.getInverse( cameraR.matrixWorldInverse );

		cameraL.projectionMatrix.fromArray( frameData.leftProjectionMatrix );
		cameraR.projectionMatrix.fromArray( frameData.rightProjectionMatrix );

		setProjectionFromUnion( cameraVR, cameraL, cameraR );

		//

		var layers = device.getLayers();

		if ( layers.length ) {

			var layer = layers[ 0 ];

			updateViewportFromBounds( cameraL.viewport, layer.leftBounds );
			updateViewportFromBounds( cameraR.viewport, layer.rightBounds );

		}

		updateControllers();

		return cameraVR;

	};

	this.getStandingMatrix = function () {

		return standingMatrix;

	};

	this.isPresenting = isPresenting;

	// Animation Loop

	var animation = new WebGLAnimation();

	this.setAnimationLoop = function ( callback ) {

		animation.setAnimationLoop( callback );

		if ( isPresenting() ) animation.start();

	};

	this.submitFrame = function () {

		if ( isPresenting() ) device.submitFrame();

	};

	this.dispose = function () {

		if ( typeof window !== 'undefined' ) {

			window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange );

		}

	};

	// DEPRECATED

	this.setFrameOfReferenceType = function () {

		console.warn( 'THREE.WebVRManager: setFrameOfReferenceType() has been deprecated.' );

	};

}

Object.assign( WebVRManager.prototype, EventDispatcher.prototype );

export { WebVRManager };
<commit_msg>Added Vive Focus controller support for WebVR<commit_after>/**
 * @author mrdoob / http://mrdoob.com/
 */

import { EventDispatcher } from '../../core/EventDispatcher.js';
import { Group } from '../../objects/Group.js';
import { Matrix4 } from '../../math/Matrix4.js';
import { Vector2 } from '../../math/Vector2.js';
import { Vector3 } from '../../math/Vector3.js';
import { Vector4 } from '../../math/Vector4.js';
import { Quaternion } from '../../math/Quaternion.js';
import { ArrayCamera } from '../../cameras/ArrayCamera.js';
import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
import { WebGLAnimation } from '../webgl/WebGLAnimation.js';
import { setProjectionFromUnion } from './WebVRUtils.js';

function WebVRManager( renderer ) {

	var renderWidth, renderHeight;
	var scope = this;

	var device = null;
	var frameData = null;

	var poseTarget = null;

	var controllers = [];
	var standingMatrix = new Matrix4();
	var standingMatrixInverse = new Matrix4();

	var framebufferScaleFactor = 1.0;

	var referenceSpaceType = 'local-floor';

	if ( typeof window !== 'undefined' && 'VRFrameData' in window ) {

		frameData = new window.VRFrameData();
		window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false );

	}

	var matrixWorldInverse = new Matrix4();
	var tempQuaternion = new Quaternion();
	var tempPosition = new Vector3();

	var cameraL = new PerspectiveCamera();
	cameraL.viewport = new Vector4();
	cameraL.layers.enable( 1 );

	var cameraR = new PerspectiveCamera();
	cameraR.viewport = new Vector4();
	cameraR.layers.enable( 2 );

	var cameraVR = new ArrayCamera( [ cameraL, cameraR ] );
	cameraVR.layers.enable( 1 );
	cameraVR.layers.enable( 2 );

	//

	function isPresenting() {

		return device !== null && device.isPresenting === true;

	}

	var currentSize = new Vector2(), currentPixelRatio;

	function onVRDisplayPresentChange() {

		if ( isPresenting() ) {

			var eyeParameters = device.getEyeParameters( 'left' );
			renderWidth = 2 * eyeParameters.renderWidth * framebufferScaleFactor;
			renderHeight = eyeParameters.renderHeight * framebufferScaleFactor;

			currentPixelRatio = renderer.getPixelRatio();
			renderer.getSize( currentSize );

			renderer.setDrawingBufferSize( renderWidth, renderHeight, 1 );

			cameraL.viewport.set( 0, 0, renderWidth / 2, renderHeight );
			cameraR.viewport.set( renderWidth / 2, 0, renderWidth / 2, renderHeight );

			animation.start();

			scope.dispatchEvent( { type: 'sessionstart' } );

		} else {

			if ( scope.enabled ) {

				renderer.setDrawingBufferSize( currentSize.width, currentSize.height, currentPixelRatio );

			}

			animation.stop();

			scope.dispatchEvent( { type: 'sessionend' } );

		}

	}

	//

	var triggers = [];

	function findGamepad( id ) {

		var gamepads = navigator.getGamepads && navigator.getGamepads();

		for ( var i = 0, j = 0, l = gamepads.length; i < l; i ++ ) {

			var gamepad = gamepads[ i ];

			if ( gamepad && ( gamepad.id === 'Daydream Controller' ||
				gamepad.id === 'Gear VR Controller' || gamepad.id === 'Oculus Go Controller' ||
				gamepad.id === 'OpenVR Gamepad' || gamepad.id.startsWith( 'Oculus Touch' ) ||
				gamepad.id.startsWith( 'HTC Vive Focus' ) ||
				gamepad.id.startsWith( 'Spatial Controller' ) ) ) {

				if ( j === id ) return gamepad;

				j ++;

			}

		}

	}

	function updateControllers() {

		for ( var i = 0; i < controllers.length; i ++ ) {

			var controller = controllers[ i ];

			var gamepad = findGamepad( i );

			if ( gamepad !== undefined && gamepad.pose !== undefined ) {

				if ( gamepad.pose === null ) return;

				// Pose

				var pose = gamepad.pose;

				if ( pose.hasPosition === false ) controller.position.set( 0.2, - 0.6, - 0.05 );

				if ( pose.position !== null ) controller.position.fromArray( pose.position );
				if ( pose.orientation !== null ) controller.quaternion.fromArray( pose.orientation );
				controller.matrix.compose( controller.position, controller.quaternion, controller.scale );
				controller.matrix.premultiply( standingMatrix );
				controller.matrix.decompose( controller.position, controller.quaternion, controller.scale );
				controller.matrixWorldNeedsUpdate = true;
				controller.visible = true;

				// Trigger

				var buttonId = gamepad.id === 'Daydream Controller' ? 0 : 1;

				if ( triggers[ i ] === undefined ) triggers[ i ] = false;

				if ( triggers[ i ] !== gamepad.buttons[ buttonId ].pressed ) {

					triggers[ i ] = gamepad.buttons[ buttonId ].pressed;

					if ( triggers[ i ] === true ) {

						controller.dispatchEvent( { type: 'selectstart' } );

					} else {

						controller.dispatchEvent( { type: 'selectend' } );
						controller.dispatchEvent( { type: 'select' } );

					}

				}

			} else {

				controller.visible = false;

			}

		}

	}

	function updateViewportFromBounds( viewport, bounds ) {

		if ( bounds !== null && bounds.length === 4 ) {

			viewport.set( bounds[ 0 ] * renderWidth, bounds[ 1 ] * renderHeight, bounds[ 2 ] * renderWidth, bounds[ 3 ] * renderHeight );

		}

	}

	//

	this.enabled = false;

	this.getController = function ( id ) {

		var controller = controllers[ id ];

		if ( controller === undefined ) {

			controller = new Group();
			controller.matrixAutoUpdate = false;
			controller.visible = false;

			controllers[ id ] = controller;

		}

		return controller;

	};

	this.getDevice = function () {

		return device;

	};

	this.setDevice = function ( value ) {

		if ( value !== undefined ) device = value;

		animation.setContext( value );

	};

	this.setFramebufferScaleFactor = function ( value ) {

		framebufferScaleFactor = value;

	};

	this.setReferenceSpaceType = function ( value ) {

		referenceSpaceType = value;

	};

	this.setPoseTarget = function ( object ) {

		if ( object !== undefined ) poseTarget = object;

	};

	this.getCamera = function ( camera ) {

		var userHeight = referenceSpaceType === 'local-floor' ? 1.6 : 0;

		if ( isPresenting() === false ) {

			camera.position.set( 0, userHeight, 0 );
			camera.rotation.set( 0, 0, 0 );

			return camera;

		}

		device.depthNear = camera.near;
		device.depthFar = camera.far;

		device.getFrameData( frameData );

		//

		if ( referenceSpaceType === 'local-floor' ) {

			var stageParameters = device.stageParameters;

			if ( stageParameters ) {

				standingMatrix.fromArray( stageParameters.sittingToStandingTransform );

			} else {

				standingMatrix.makeTranslation( 0, userHeight, 0 );

			}

		}

		var pose = frameData.pose;
		var poseObject = poseTarget !== null ? poseTarget : camera;

		// We want to manipulate poseObject by its position and quaternion components since users may rely on them.
		poseObject.matrix.copy( standingMatrix );
		poseObject.matrix.decompose( poseObject.position, poseObject.quaternion, poseObject.scale );

		if ( pose.orientation !== null ) {

			tempQuaternion.fromArray( pose.orientation );
			poseObject.quaternion.multiply( tempQuaternion );

		}

		if ( pose.position !== null ) {

			tempQuaternion.setFromRotationMatrix( standingMatrix );
			tempPosition.fromArray( pose.position );
			tempPosition.applyQuaternion( tempQuaternion );
			poseObject.position.add( tempPosition );

		}

		poseObject.updateMatrixWorld();

		//

		cameraL.near = camera.near;
		cameraR.near = camera.near;

		cameraL.far = camera.far;
		cameraR.far = camera.far;

		cameraL.matrixWorldInverse.fromArray( frameData.leftViewMatrix );
		cameraR.matrixWorldInverse.fromArray( frameData.rightViewMatrix );

		// TODO (mrdoob) Double check this code

		standingMatrixInverse.getInverse( standingMatrix );

		if ( referenceSpaceType === 'local-floor' ) {

			cameraL.matrixWorldInverse.multiply( standingMatrixInverse );
			cameraR.matrixWorldInverse.multiply( standingMatrixInverse );

		}

		var parent = poseObject.parent;

		if ( parent !== null ) {

			matrixWorldInverse.getInverse( parent.matrixWorld );

			cameraL.matrixWorldInverse.multiply( matrixWorldInverse );
			cameraR.matrixWorldInverse.multiply( matrixWorldInverse );

		}

		// envMap and Mirror needs camera.matrixWorld

		cameraL.matrixWorld.getInverse( cameraL.matrixWorldInverse );
		cameraR.matrixWorld.getInverse( cameraR.matrixWorldInverse );

		cameraL.projectionMatrix.fromArray( frameData.leftProjectionMatrix );
		cameraR.projectionMatrix.fromArray( frameData.rightProjectionMatrix );

		setProjectionFromUnion( cameraVR, cameraL, cameraR );

		//

		var layers = device.getLayers();

		if ( layers.length ) {

			var layer = layers[ 0 ];

			updateViewportFromBounds( cameraL.viewport, layer.leftBounds );
			updateViewportFromBounds( cameraR.viewport, layer.rightBounds );

		}

		updateControllers();

		return cameraVR;

	};

	this.getStandingMatrix = function () {

		return standingMatrix;

	};

	this.isPresenting = isPresenting;

	// Animation Loop

	var animation = new WebGLAnimation();

	this.setAnimationLoop = function ( callback ) {

		animation.setAnimationLoop( callback );

		if ( isPresenting() ) animation.start();

	};

	this.submitFrame = function () {

		if ( isPresenting() ) device.submitFrame();

	};

	this.dispose = function () {

		if ( typeof window !== 'undefined' ) {

			window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange );

		}

	};

	// DEPRECATED

	this.setFrameOfReferenceType = function () {

		console.warn( 'THREE.WebVRManager: setFrameOfReferenceType() has been deprecated.' );

	};

}

Object.assign( WebVRManager.prototype, EventDispatcher.prototype );

export { WebVRManager };
<|endoftext|>


<commit_before>/**
 * @author mrdoob / http://mrdoob.com/
 */

import { Group } from '../../objects/Group.js';
import { Vector4 } from '../../math/Vector4.js';
import { ArrayCamera } from '../../cameras/ArrayCamera.js';
import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
import { WebGLAnimation } from '../webgl/WebGLAnimation.js';
import { setProjectionFromUnion } from './WebVRUtils.js';

function WebXRManager( renderer ) {

	var gl = renderer.context;

	var device = null;
	var session = null;

	var framebufferScaleFactor = 1.0;

	var frameOfReference = null;
	var frameOfReferenceType = 'stage';

	var pose = null;

	var controllers = [];
	var inputSources = [];

	function isPresenting() {

		return session !== null && frameOfReference !== null;

	}

	//

	var cameraL = new PerspectiveCamera();
	cameraL.layers.enable( 1 );
	cameraL.viewport = new Vector4();

	var cameraR = new PerspectiveCamera();
	cameraR.layers.enable( 2 );
	cameraR.viewport = new Vector4();

	var cameraVR = new ArrayCamera( [ cameraL, cameraR ] );
	cameraVR.layers.enable( 1 );
	cameraVR.layers.enable( 2 );

	//

	this.enabled = false;

	this.getController = function ( id ) {

		var controller = controllers[ id ];

		if ( controller === undefined ) {

			controller = new Group();
			controller.matrixAutoUpdate = false;
			controller.visible = false;

			controllers[ id ] = controller;

		}

		return controller;

	};

	this.getDevice = function () {

		return device;

	};

	this.setDevice = function ( value ) {

		if ( value !== undefined ) device = value;
		if ( value instanceof XRDevice ) gl.setCompatibleXRDevice( value );

	};

	//

	function onSessionEvent( event ) {

		var controller = controllers[ inputSources.indexOf( event.inputSource ) ];
		if ( controller ) controller.dispatchEvent( { type: event.type } );

	}

	function onSessionEnd() {

		renderer.setFramebuffer( null );
		renderer.setRenderTarget( renderer.getRenderTarget() );
		animation.stop();

	}

	this.setFramebufferScaleFactor = function ( value ) {

		framebufferScaleFactor = value;

	};

	this.setFrameOfReferenceType = function ( value ) {

		frameOfReferenceType = value;

	};

	this.setSession = function ( value ) {

		session = value;

		if ( session !== null ) {

			session.addEventListener( 'select', onSessionEvent );
			session.addEventListener( 'selectstart', onSessionEvent );
			session.addEventListener( 'selectend', onSessionEvent );
			session.addEventListener( 'end', onSessionEnd );

			session.baseLayer = new XRWebGLLayer( session, gl, { framebufferScaleFactor: framebufferScaleFactor } );
			session.requestFrameOfReference( frameOfReferenceType ).then( function ( value ) {

				frameOfReference = value;

				renderer.setFramebuffer( session.baseLayer.framebuffer );

				animation.setContext( session );
				animation.start();

			} );

			//

			inputSources = session.getInputSources();

			session.addEventListener( 'inputsourceschange', function () {

				inputSources = session.getInputSources();
				console.log( inputSources );

				for ( var i = 0; i < controllers.length; i ++ ) {

					var controller = controllers[ i ];
					controller.userData.inputSource = inputSources[ i ];

				}

			} );

		}

	};

	function updateCamera( camera, parent ) {

		if ( parent === null ) {

			camera.matrixWorld.copy( camera.matrix );

		} else {

			camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix );

		}

		camera.matrixWorldInverse.getInverse( camera.matrixWorld );

	}

	this.getCamera = function ( camera ) {

		if ( isPresenting() ) {

			var parent = camera.parent;
			var cameras = cameraVR.cameras;

			updateCamera( cameraVR, parent );

			for ( var i = 0; i < cameras.length; i ++ ) {

				updateCamera( cameras[ i ], parent );

			}

			// update camera and its children

			camera.matrixWorld.copy( cameraVR.matrixWorld );

			var children = camera.children;

			for ( var i = 0, l = children.length; i < l; i ++ ) {

				children[ i ].updateMatrixWorld( true );

			}

			setProjectionFromUnion( cameraVR, cameraL, cameraR );

			return cameraVR;

		}

		return camera;

	};

	this.isPresenting = isPresenting;

	// Animation Loop

	var onAnimationFrameCallback = null;

	function onAnimationFrame( time, frame ) {

		pose = frame.getDevicePose( frameOfReference );

		if ( pose !== null ) {

			var layer = session.baseLayer;
			var views = frame.views;

			for ( var i = 0; i < views.length; i ++ ) {

				var view = views[ i ];
				var viewport = layer.getViewport( view );
				var viewMatrix = pose.getViewMatrix( view );

				var camera = cameraVR.cameras[ i ];
				camera.matrix.fromArray( viewMatrix ).getInverse( camera.matrix );
				camera.projectionMatrix.fromArray( view.projectionMatrix );
				camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );

				if ( i === 0 ) {

					cameraVR.matrix.copy( camera.matrix );

				}

			}

		}

		//

		for ( var i = 0; i < controllers.length; i ++ ) {

			var controller = controllers[ i ];

			var inputSource = inputSources[ i ];

			if ( inputSource ) {

				var inputPose = frame.getInputPose( inputSource, frameOfReference );

				if ( inputPose !== null ) {

					if ( 'targetRay' in inputPose ) {

						controller.matrix.elements = inputPose.targetRay.transformMatrix;

					} else if ( 'pointerMatrix' in inputPose ) {

						// DEPRECATED

						controller.matrix.elements = inputPose.pointerMatrix;

					}

					controller.matrix.decompose( controller.position, controller.rotation, controller.scale );
					controller.visible = true;

					continue;

				}

			}

			controller.visible = false;

		}

		if ( onAnimationFrameCallback ) onAnimationFrameCallback( time );

	}

	var animation = new WebGLAnimation();
	animation.setAnimationLoop( onAnimationFrame );

	this.setAnimationLoop = function ( callback ) {

		onAnimationFrameCallback = callback;

	};

	this.dispose = function () {};

	// DEPRECATED

	this.getStandingMatrix = function () {

		console.warn( 'THREE.WebXRManager: getStandingMatrix() is no longer needed.' );
		return new THREE.Matrix4();

	};

	this.submitFrame = function () {};

}

export { WebXRManager };
<commit_msg>WebXRManager: Documented hack.<commit_after>/**
 * @author mrdoob / http://mrdoob.com/
 */

import { Group } from '../../objects/Group.js';
import { Vector4 } from '../../math/Vector4.js';
import { ArrayCamera } from '../../cameras/ArrayCamera.js';
import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
import { WebGLAnimation } from '../webgl/WebGLAnimation.js';
import { setProjectionFromUnion } from './WebVRUtils.js';

function WebXRManager( renderer ) {

	var gl = renderer.context;

	var device = null;
	var session = null;

	var framebufferScaleFactor = 1.0;

	var frameOfReference = null;
	var frameOfReferenceType = 'stage';

	var pose = null;

	var controllers = [];
	var inputSources = [];

	function isPresenting() {

		return session !== null && frameOfReference !== null;

	}

	//

	var cameraL = new PerspectiveCamera();
	cameraL.layers.enable( 1 );
	cameraL.viewport = new Vector4();

	var cameraR = new PerspectiveCamera();
	cameraR.layers.enable( 2 );
	cameraR.viewport = new Vector4();

	var cameraVR = new ArrayCamera( [ cameraL, cameraR ] );
	cameraVR.layers.enable( 1 );
	cameraVR.layers.enable( 2 );

	//

	this.enabled = false;

	this.getController = function ( id ) {

		var controller = controllers[ id ];

		if ( controller === undefined ) {

			controller = new Group();
			controller.matrixAutoUpdate = false;
			controller.visible = false;

			controllers[ id ] = controller;

		}

		return controller;

	};

	this.getDevice = function () {

		return device;

	};

	this.setDevice = function ( value ) {

		if ( value !== undefined ) device = value;
		if ( value instanceof XRDevice ) gl.setCompatibleXRDevice( value );

	};

	//

	function onSessionEvent( event ) {

		var controller = controllers[ inputSources.indexOf( event.inputSource ) ];
		if ( controller ) controller.dispatchEvent( { type: event.type } );

	}

	function onSessionEnd() {

		renderer.setFramebuffer( null );
		renderer.setRenderTarget( renderer.getRenderTarget() ); // Hack #15830
		animation.stop();

	}

	this.setFramebufferScaleFactor = function ( value ) {

		framebufferScaleFactor = value;

	};

	this.setFrameOfReferenceType = function ( value ) {

		frameOfReferenceType = value;

	};

	this.setSession = function ( value ) {

		session = value;

		if ( session !== null ) {

			session.addEventListener( 'select', onSessionEvent );
			session.addEventListener( 'selectstart', onSessionEvent );
			session.addEventListener( 'selectend', onSessionEvent );
			session.addEventListener( 'end', onSessionEnd );

			session.baseLayer = new XRWebGLLayer( session, gl, { framebufferScaleFactor: framebufferScaleFactor } );
			session.requestFrameOfReference( frameOfReferenceType ).then( function ( value ) {

				frameOfReference = value;

				renderer.setFramebuffer( session.baseLayer.framebuffer );

				animation.setContext( session );
				animation.start();

			} );

			//

			inputSources = session.getInputSources();

			session.addEventListener( 'inputsourceschange', function () {

				inputSources = session.getInputSources();
				console.log( inputSources );

				for ( var i = 0; i < controllers.length; i ++ ) {

					var controller = controllers[ i ];
					controller.userData.inputSource = inputSources[ i ];

				}

			} );

		}

	};

	function updateCamera( camera, parent ) {

		if ( parent === null ) {

			camera.matrixWorld.copy( camera.matrix );

		} else {

			camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix );

		}

		camera.matrixWorldInverse.getInverse( camera.matrixWorld );

	}

	this.getCamera = function ( camera ) {

		if ( isPresenting() ) {

			var parent = camera.parent;
			var cameras = cameraVR.cameras;

			updateCamera( cameraVR, parent );

			for ( var i = 0; i < cameras.length; i ++ ) {

				updateCamera( cameras[ i ], parent );

			}

			// update camera and its children

			camera.matrixWorld.copy( cameraVR.matrixWorld );

			var children = camera.children;

			for ( var i = 0, l = children.length; i < l; i ++ ) {

				children[ i ].updateMatrixWorld( true );

			}

			setProjectionFromUnion( cameraVR, cameraL, cameraR );

			return cameraVR;

		}

		return camera;

	};

	this.isPresenting = isPresenting;

	// Animation Loop

	var onAnimationFrameCallback = null;

	function onAnimationFrame( time, frame ) {

		pose = frame.getDevicePose( frameOfReference );

		if ( pose !== null ) {

			var layer = session.baseLayer;
			var views = frame.views;

			for ( var i = 0; i < views.length; i ++ ) {

				var view = views[ i ];
				var viewport = layer.getViewport( view );
				var viewMatrix = pose.getViewMatrix( view );

				var camera = cameraVR.cameras[ i ];
				camera.matrix.fromArray( viewMatrix ).getInverse( camera.matrix );
				camera.projectionMatrix.fromArray( view.projectionMatrix );
				camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );

				if ( i === 0 ) {

					cameraVR.matrix.copy( camera.matrix );

				}

			}

		}

		//

		for ( var i = 0; i < controllers.length; i ++ ) {

			var controller = controllers[ i ];

			var inputSource = inputSources[ i ];

			if ( inputSource ) {

				var inputPose = frame.getInputPose( inputSource, frameOfReference );

				if ( inputPose !== null ) {

					if ( 'targetRay' in inputPose ) {

						controller.matrix.elements = inputPose.targetRay.transformMatrix;

					} else if ( 'pointerMatrix' in inputPose ) {

						// DEPRECATED

						controller.matrix.elements = inputPose.pointerMatrix;

					}

					controller.matrix.decompose( controller.position, controller.rotation, controller.scale );
					controller.visible = true;

					continue;

				}

			}

			controller.visible = false;

		}

		if ( onAnimationFrameCallback ) onAnimationFrameCallback( time );

	}

	var animation = new WebGLAnimation();
	animation.setAnimationLoop( onAnimationFrame );

	this.setAnimationLoop = function ( callback ) {

		onAnimationFrameCallback = callback;

	};

	this.dispose = function () {};

	// DEPRECATED

	this.getStandingMatrix = function () {

		console.warn( 'THREE.WebXRManager: getStandingMatrix() is no longer needed.' );
		return new THREE.Matrix4();

	};

	this.submitFrame = function () {};

}

export { WebXRManager };
<|endoftext|>


<commit_before>// Wrapper around Apple's Instruments app
"use strict";

var spawn = require('child_process').spawn
  , exec = require('child_process').exec
  , logger = require('../logger').get('appium')
  , fs = require('fs')
  , _ = require('underscore')
  , net = require('net')
  , uuid = require('uuid-js')
  , path = require('path')
  , codes = require('../app/uiauto/lib/status.js').codes;

var Instruments = function(app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) {
  this.app = app;
  this.udid = udid;
  this.bootstrap = bootstrap;
  this.template = template;
  this.withoutDelay = withoutDelay;
  this.webSocket = webSocket;
  this.commandQueue = [];
  this.curCommand = null;
  this.resultHandler = this.defaultResultHandler;
  this.readyHandler = this.defaultReadyHandler;
  this.exitHandler = this.defaultExitHandler;
  this.hasConnected = false;
  this.traceDir = null;
  this.proc = null;
  this.debugMode = false;
  this.onReceiveCommand = null;
  this.guid = uuid.create();
  this.bufferedData = "";
  this.instrumentsPath = "";
  this.eventRouter = {
    'cmd': this.commandHandler
  };
  this.socketServer = null;
  if (typeof sock === "undefined") {
    sock = '/tmp/instruments_sock';
  }
  if (typeof cb !== "undefined") {
    this.readyHandler = cb;
  }
  if (typeof exitCb !== "undefined") {
    this.exitHandler = exitCb;
  }

  this.startSocketServer(sock);

};

/* INITIALIZATION */

Instruments.prototype.startSocketServer = function(sock) {
  // remove socket if it currently exists
  try {
    fs.unlinkSync(sock);
  } catch (Exception) {}

  var onSocketNeverConnect = function() {
    logger.error("Instruments socket client never checked in; timing out".red);
    if (this.proc !== null) {
      this.proc.kill('SIGTERM');
    }
    this.exitHandler(1);
  };

  var socketConnectTimeout = setTimeout(_.bind(onSocketNeverConnect, this), 300000);

  this.socketServer = net.createServer({allowHalfOpen: true}, _.bind(function(conn) {
    if (!this.hasConnected) {
      this.hasConnected = true;
      this.debug("Instruments is ready to receive commands");
      clearTimeout(socketConnectTimeout);
      this.readyHandler(this);
      this.readyHandler = this.defaultReadyHandler;
    }
    conn.setEncoding('utf8'); // get strings from sockets rather than buffers

    conn.on('data', _.bind(function(data) {
      // when data comes in, route it according to the "event" property
      this.debug("Socket data received (" + data.length + " bytes)");
      this.bufferedData += data;
    }, this));

    this.currentSocket = conn;
    //this.debug("Socket Connected");

    conn.on('close', _.bind(function() {
        //this.debug("Socket Completely closed");
        this.currentSocket = null;
    }, this));

    conn.on('end', _.bind(function() {
      //this.debug("Socket closed by other side");
      var data = this.bufferedData;
      this.bufferedData = "";
      try {
        data = JSON.parse(data);
      } catch (e) {
        logger.error("Couldn't parse JSON data from socket, maybe buffer issue?");
        logger.error(data);
        data = {
          event: 'cmd'
          , result: {
            status: codes.UnknownError
            , value: "Error parsing socket data from instruments"
          }
        };
      }
      if (!_.has(data, 'event')) {
        logger.error("Socket data came in witout event, it was:");
        logger.error(JSON.stringify(data));
      } else if (!_.has(this.eventRouter, data.event)) {
        logger.error("Socket is asking for event '" + data.event +
                    "' which doesn't exist");
      } else {
        this.debug("Socket data being routed for '" + data.event + "' event");
        _.bind(this.eventRouter[data.event], this)(data, conn);
      }
    }, this));

  }, this));

  this.socketServer.on('close', _.bind(function() {
    this.debug("Instruments socket server closed");
  }, this));

  exec('xcrun -find instruments', _.bind(function (error, stdout) {
    this.instrumentsPath = stdout.trim();
    logger.info("instruments is: " + this.instrumentsPath);

    this.socketServer.listen(sock, _.bind(function() {
      this.debug("Instruments socket server started at " + sock);
      this.launch();
    }, this));
  }, this));
};

Instruments.prototype.launch = function() {
  var self = this;
  // prepare temp dir
  var tmpDir = '/tmp/' + this.guid;
  fs.mkdir(tmpDir, function(e) {
    if (!e || (e && e.code === 'EEXIST')) {
      self.proc = self.spawnInstruments(tmpDir);
      self.proc.stdout.on('data', function(data) {
        self.outputStreamHandler(data);
      });
      self.proc.stderr.on('data', function(data) {
        self.errorStreamHandler(data);
      });

      self.proc.on('exit', function(code) {
        self.debug("Instruments exited with code " + code);
        self.exitCode = code;
        self.exitHandler(self.exitCode, self.traceDir);
        self.proc.stdin.end();
        self.proc = null;
        self.exitHandler = self.defaultExitHandler;
        self.resultHandler = self.defaultResultHandler;
        self.onReceiveCommand = null;
        if (self.currentSocket) {
          self.debug("Socket closed forcibly due to exit");
          self.currentSocket.end();
          self.currentSocket.destroy(); // close this
          self.socketServer.close();
        }
      });
    } else {
      throw e;
    }
  });
};

Instruments.prototype.spawnInstruments = function(tmpDir) {
  var args = ["-t", this.template];
  if (this.udid) {
    args = args.concat(["-w", this.udid]);
    logger.info("Attempting to run app on real device with UDID " + this.udid);
  }
  args = args.concat([this.app]);
  args = args.concat(["-e", "UIASCRIPT", this.bootstrap]);
  args = args.concat(["-e", "UIARESULTSPATH", tmpDir]);
  var env = _.clone(process.env);
  if (this.withoutDelay && !this.udid) {
    env.DYLD_INSERT_LIBRARIES = path.resolve(__dirname, "../build/iwd/InstrumentsShim.dylib");
    env.LIB_PATH = path.resolve(__dirname, "../build/iwd");
  }
  logger.info("Spawning instruments with command: " + this.instrumentsPath +
              " " + args.join(" "));
  logger.info("And extra without-delay env: " + JSON.stringify({
    DYLD_INSERT_LIBRARIES: env.DYLD_INSERT_LIBRARIES
    , LIB_PATH: env.LIB_PATH
  }));
  return spawn(this.instrumentsPath, args, {env: env});
};

/* COMMAND LIFECYCLE */

Instruments.prototype.commandHandler = function(data, c) {
  var hasResult = typeof data.result !== "undefined";
  if (hasResult && !this.curCommand) {
    logger.info("Got a result when we weren't expecting one! Ignoring it");
    logger.info("Result was: " + JSON.stringify(data.result));
  } else if (!hasResult && this.curCommand) {
    logger.info("Instruments didn't send a result even though we were expecting one");
    hasResult = true;
    data.result = false;
  }

  if (hasResult && this.curCommand) {
    if (data.result) {
      this.debug("Got result from instruments: " + JSON.stringify(data.result));
    } else {
      this.debug("Got null result from instruments");
    }
    this.curCommand.cb(data.result);
    this.curCommand = null;
  }

  this.waitForCommand(_.bind(function() {
    this.curCommand = this.commandQueue.shift();
    this.onReceiveCommand = null;
    this.debug("Sending command to instruments: " + this.curCommand.cmd);
    c.write(JSON.stringify({nextCommand: this.curCommand.cmd}));
    c.end();
    //this.debug("Closing our half of the connection");
  }, this));
};

Instruments.prototype.waitForCommand = function(cb) {
  if (this.commandQueue.length) {
    cb();
  } else {
    this.onReceiveCommand = cb;
  }
};

Instruments.prototype.sendCommand = function(cmd, cb) {
  this.commandQueue.push({cmd: cmd, cb: cb});
  if (this.onReceiveCommand) {
    this.onReceiveCommand();
  }
};

/* PROCESS MANAGEMENT */

Instruments.prototype.shutdown = function() {
  this.proc.kill();
};

Instruments.prototype.doExit = function() {
  console.log("Calling exit handler");
};

/* INSTRUMENTS STREAM MANIPULATION*/

Instruments.prototype.clearBufferChars = function(output) {
  // Instruments output is buffered, so for each log output we also output
  // a stream of very many ****. This function strips those out so all we
  // get is the log output we care about
  var re = /(\n|^)\*+\n?/g;
  output = output.toString();
  output = output.replace(re, "");
  return output;
};

Instruments.prototype.outputStreamHandler = function(output) {
  output = this.clearBufferChars(output);
  this.lookForShutdownInfo(output);
  this.resultHandler(output);
};

Instruments.prototype.errorStreamHandler = function(output) {
  logger.info(("[INST STDERR] " + output).yellow);
  if (this.webSocket) {
    var re = /Call to onAlert returned 'YES'/;
    var match = re.test(output);
    if (match) {
      logger.info("Emiting alert message...");
      this.webSocket.sockets.emit('alert', {message: output});
    }
  }
};

Instruments.prototype.lookForShutdownInfo = function(output) {
  var re = /Instruments Trace Complete.+Output : ([^\)]+)\)/;
  var match = re.exec(output);
  if (match) {
    this.traceDir = match[1];
  }
};

/* DEFAULT HANDLERS */

Instruments.prototype.setResultHandler = function(handler) {
  this.resultHandler = handler;
};

Instruments.prototype.defaultResultHandler = function(output) {
  // if we have multiple log lines, indent non-first ones
  if (output !== "") {
    output = output.replace(/\n/m, "\n       ");
    logger.info(("[INST] " + output).green);
  }
};

Instruments.prototype.defaultReadyHandler = function() {
  logger.info("Instruments is ready and waiting!");
};

Instruments.prototype.defaultExitHandler = function(code, traceDir) {
  logger.info("Instruments exited with code " + code + " and trace dir " + traceDir);
};

/* MISC */

Instruments.prototype.setDebug = function(debug) {
  if (typeof debug === "undefined") {
    debug = true;
  }
  this.debugMode = debug;
};

Instruments.prototype.debug = function(msg) {
  logger.info(("[INSTSERVER] " + msg).grey);
};

/* NODE EXPORTS */

module.exports = function(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) {
  return new Instruments(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb);
};

<commit_msg>Fix typo<commit_after>// Wrapper around Apple's Instruments app
"use strict";

var spawn = require('child_process').spawn
  , exec = require('child_process').exec
  , logger = require('../logger').get('appium')
  , fs = require('fs')
  , _ = require('underscore')
  , net = require('net')
  , uuid = require('uuid-js')
  , path = require('path')
  , codes = require('../app/uiauto/lib/status.js').codes;

var Instruments = function(app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) {
  this.app = app;
  this.udid = udid;
  this.bootstrap = bootstrap;
  this.template = template;
  this.withoutDelay = withoutDelay;
  this.webSocket = webSocket;
  this.commandQueue = [];
  this.curCommand = null;
  this.resultHandler = this.defaultResultHandler;
  this.readyHandler = this.defaultReadyHandler;
  this.exitHandler = this.defaultExitHandler;
  this.hasConnected = false;
  this.traceDir = null;
  this.proc = null;
  this.debugMode = false;
  this.onReceiveCommand = null;
  this.guid = uuid.create();
  this.bufferedData = "";
  this.instrumentsPath = "";
  this.eventRouter = {
    'cmd': this.commandHandler
  };
  this.socketServer = null;
  if (typeof sock === "undefined") {
    sock = '/tmp/instruments_sock';
  }
  if (typeof cb !== "undefined") {
    this.readyHandler = cb;
  }
  if (typeof exitCb !== "undefined") {
    this.exitHandler = exitCb;
  }

  this.startSocketServer(sock);

};

/* INITIALIZATION */

Instruments.prototype.startSocketServer = function(sock) {
  // remove socket if it currently exists
  try {
    fs.unlinkSync(sock);
  } catch (Exception) {}

  var onSocketNeverConnect = function() {
    logger.error("Instruments socket client never checked in; timing out".red);
    if (this.proc !== null) {
      this.proc.kill('SIGTERM');
    }
    this.exitHandler(1);
  };

  var socketConnectTimeout = setTimeout(_.bind(onSocketNeverConnect, this), 300000);

  this.socketServer = net.createServer({allowHalfOpen: true}, _.bind(function(conn) {
    if (!this.hasConnected) {
      this.hasConnected = true;
      this.debug("Instruments is ready to receive commands");
      clearTimeout(socketConnectTimeout);
      this.readyHandler(this);
      this.readyHandler = this.defaultReadyHandler;
    }
    conn.setEncoding('utf8'); // get strings from sockets rather than buffers

    conn.on('data', _.bind(function(data) {
      // when data comes in, route it according to the "event" property
      this.debug("Socket data received (" + data.length + " bytes)");
      this.bufferedData += data;
    }, this));

    this.currentSocket = conn;
    //this.debug("Socket Connected");

    conn.on('close', _.bind(function() {
        //this.debug("Socket Completely closed");
        this.currentSocket = null;
    }, this));

    conn.on('end', _.bind(function() {
      //this.debug("Socket closed by other side");
      var data = this.bufferedData;
      this.bufferedData = "";
      try {
        data = JSON.parse(data);
      } catch (e) {
        logger.error("Couldn't parse JSON data from socket, maybe buffer issue?");
        logger.error(data);
        data = {
          event: 'cmd'
          , result: {
            status: codes.UnknownError
            , value: "Error parsing socket data from instruments"
          }
        };
      }
      if (!_.has(data, 'event')) {
        logger.error("Socket data came in without event, it was:");
        logger.error(JSON.stringify(data));
      } else if (!_.has(this.eventRouter, data.event)) {
        logger.error("Socket is asking for event '" + data.event +
                    "' which doesn't exist");
      } else {
        this.debug("Socket data being routed for '" + data.event + "' event");
        _.bind(this.eventRouter[data.event], this)(data, conn);
      }
    }, this));

  }, this));

  this.socketServer.on('close', _.bind(function() {
    this.debug("Instruments socket server closed");
  }, this));

  exec('xcrun -find instruments', _.bind(function (error, stdout) {
    this.instrumentsPath = stdout.trim();
    logger.info("instruments is: " + this.instrumentsPath);

    this.socketServer.listen(sock, _.bind(function() {
      this.debug("Instruments socket server started at " + sock);
      this.launch();
    }, this));
  }, this));
};

Instruments.prototype.launch = function() {
  var self = this;
  // prepare temp dir
  var tmpDir = '/tmp/' + this.guid;
  fs.mkdir(tmpDir, function(e) {
    if (!e || (e && e.code === 'EEXIST')) {
      self.proc = self.spawnInstruments(tmpDir);
      self.proc.stdout.on('data', function(data) {
        self.outputStreamHandler(data);
      });
      self.proc.stderr.on('data', function(data) {
        self.errorStreamHandler(data);
      });

      self.proc.on('exit', function(code) {
        self.debug("Instruments exited with code " + code);
        self.exitCode = code;
        self.exitHandler(self.exitCode, self.traceDir);
        self.proc.stdin.end();
        self.proc = null;
        self.exitHandler = self.defaultExitHandler;
        self.resultHandler = self.defaultResultHandler;
        self.onReceiveCommand = null;
        if (self.currentSocket) {
          self.debug("Socket closed forcibly due to exit");
          self.currentSocket.end();
          self.currentSocket.destroy(); // close this
          self.socketServer.close();
        }
      });
    } else {
      throw e;
    }
  });
};

Instruments.prototype.spawnInstruments = function(tmpDir) {
  var args = ["-t", this.template];
  if (this.udid) {
    args = args.concat(["-w", this.udid]);
    logger.info("Attempting to run app on real device with UDID " + this.udid);
  }
  args = args.concat([this.app]);
  args = args.concat(["-e", "UIASCRIPT", this.bootstrap]);
  args = args.concat(["-e", "UIARESULTSPATH", tmpDir]);
  var env = _.clone(process.env);
  if (this.withoutDelay && !this.udid) {
    env.DYLD_INSERT_LIBRARIES = path.resolve(__dirname, "../build/iwd/InstrumentsShim.dylib");
    env.LIB_PATH = path.resolve(__dirname, "../build/iwd");
  }
  logger.info("Spawning instruments with command: " + this.instrumentsPath +
              " " + args.join(" "));
  logger.info("And extra without-delay env: " + JSON.stringify({
    DYLD_INSERT_LIBRARIES: env.DYLD_INSERT_LIBRARIES
    , LIB_PATH: env.LIB_PATH
  }));
  return spawn(this.instrumentsPath, args, {env: env});
};

/* COMMAND LIFECYCLE */

Instruments.prototype.commandHandler = function(data, c) {
  var hasResult = typeof data.result !== "undefined";
  if (hasResult && !this.curCommand) {
    logger.info("Got a result when we weren't expecting one! Ignoring it");
    logger.info("Result was: " + JSON.stringify(data.result));
  } else if (!hasResult && this.curCommand) {
    logger.info("Instruments didn't send a result even though we were expecting one");
    hasResult = true;
    data.result = false;
  }

  if (hasResult && this.curCommand) {
    if (data.result) {
      this.debug("Got result from instruments: " + JSON.stringify(data.result));
    } else {
      this.debug("Got null result from instruments");
    }
    this.curCommand.cb(data.result);
    this.curCommand = null;
  }

  this.waitForCommand(_.bind(function() {
    this.curCommand = this.commandQueue.shift();
    this.onReceiveCommand = null;
    this.debug("Sending command to instruments: " + this.curCommand.cmd);
    c.write(JSON.stringify({nextCommand: this.curCommand.cmd}));
    c.end();
    //this.debug("Closing our half of the connection");
  }, this));
};

Instruments.prototype.waitForCommand = function(cb) {
  if (this.commandQueue.length) {
    cb();
  } else {
    this.onReceiveCommand = cb;
  }
};

Instruments.prototype.sendCommand = function(cmd, cb) {
  this.commandQueue.push({cmd: cmd, cb: cb});
  if (this.onReceiveCommand) {
    this.onReceiveCommand();
  }
};

/* PROCESS MANAGEMENT */

Instruments.prototype.shutdown = function() {
  this.proc.kill();
};

Instruments.prototype.doExit = function() {
  console.log("Calling exit handler");
};

/* INSTRUMENTS STREAM MANIPULATION*/

Instruments.prototype.clearBufferChars = function(output) {
  // Instruments output is buffered, so for each log output we also output
  // a stream of very many ****. This function strips those out so all we
  // get is the log output we care about
  var re = /(\n|^)\*+\n?/g;
  output = output.toString();
  output = output.replace(re, "");
  return output;
};

Instruments.prototype.outputStreamHandler = function(output) {
  output = this.clearBufferChars(output);
  this.lookForShutdownInfo(output);
  this.resultHandler(output);
};

Instruments.prototype.errorStreamHandler = function(output) {
  logger.info(("[INST STDERR] " + output).yellow);
  if (this.webSocket) {
    var re = /Call to onAlert returned 'YES'/;
    var match = re.test(output);
    if (match) {
      logger.info("Emiting alert message...");
      this.webSocket.sockets.emit('alert', {message: output});
    }
  }
};

Instruments.prototype.lookForShutdownInfo = function(output) {
  var re = /Instruments Trace Complete.+Output : ([^\)]+)\)/;
  var match = re.exec(output);
  if (match) {
    this.traceDir = match[1];
  }
};

/* DEFAULT HANDLERS */

Instruments.prototype.setResultHandler = function(handler) {
  this.resultHandler = handler;
};

Instruments.prototype.defaultResultHandler = function(output) {
  // if we have multiple log lines, indent non-first ones
  if (output !== "") {
    output = output.replace(/\n/m, "\n       ");
    logger.info(("[INST] " + output).green);
  }
};

Instruments.prototype.defaultReadyHandler = function() {
  logger.info("Instruments is ready and waiting!");
};

Instruments.prototype.defaultExitHandler = function(code, traceDir) {
  logger.info("Instruments exited with code " + code + " and trace dir " + traceDir);
};

/* MISC */

Instruments.prototype.setDebug = function(debug) {
  if (typeof debug === "undefined") {
    debug = true;
  }
  this.debugMode = debug;
};

Instruments.prototype.debug = function(msg) {
  logger.info(("[INSTSERVER] " + msg).grey);
};

/* NODE EXPORTS */

module.exports = function(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) {
  return new Instruments(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb);
};

<|endoftext|>


<commit_before>/**
 * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * An audience action flow will show a dialog prompt to a reader, asking them
 * to complete an action for potentially free, additional metered entitlements.
 *
 * An action flow can potentially assign a new user token so if we complete one,
 * we should:
 * 1) Store the new user token
 * 2) Clear any existing entitlements stored on the page
 * 3) Re-fetch entitlements which may potentially provide access to the page
 * 4) Close the prompt that initiated the flow.
 */

import {ActivityIframeView} from '../ui/activity-iframe-view';
import {
  AlreadySubscribedResponse,
  CompleteAudienceActionResponse,
  EntitlementsResponse,
} from '../proto/api_messages';
import {AutoPromptType} from '../api/basic-subscriptions';
import {Constants} from '../utils/constants';
import {ProductType} from '../api/subscriptions';
import {SWG_I18N_STRINGS} from '../i18n/swg-strings';
import {Toast} from '../ui/toast';
import {feArgs, feUrl} from './services';
import {msg} from '../utils/i18n';
import {parseUrl} from '../utils/url';

/**
 * @typedef {{
 *  action: (string|undefined),
 *  fallback: (function()|undefined),
 *  autoPromptType: (AutoPromptType|undefined)
 * }}
 */
export let AudienceActionParams;

const actionToIframeMapping = {
  'TYPE_REGISTRATION_WALL': '/regwalliframe',
  'TYPE_NEWSLETTER_SIGNUP': '/newsletteriframe',
};

const autopromptTypeToProductTypeMapping = {
  [AutoPromptType.SUBSCRIPTION]: ProductType.SUBSCRIPTION,
  [AutoPromptType.SUBSCRIPTION_LARGE]: ProductType.SUBSCRIPTION,
  [AutoPromptType.CONTRIBUTION]: ProductType.UI_CONTRIBUTION,
  [AutoPromptType.CONTRIBUTION_LARGE]: ProductType.UI_CONTRIBUTION,
};

const DEFAULT_PRODUCT_TYPE = ProductType.SUBSCRIPTION;

/**
 * The flow to initiate and manage handling an audience action.
 */
export class AudienceActionFlow {
  /**
   * @param {!./deps.DepsDef} deps
   * @param {!AudienceActionParams} params
   */
  constructor(deps, params) {
    /** @private @const {!./deps.DepsDef} */
    this.deps_ = deps;

    /** @private @const {!AudienceActionParams} */
    this.params_ = params;

    /** @private @const {!ProductType} */
    this.productType_ = params.autoPromptType
      ? autopromptTypeToProductTypeMapping[params.autoPromptType]
      : DEFAULT_PRODUCT_TYPE;

    /** @private @const {!../components/dialog-manager.DialogManager} */
    this.dialogManager_ = deps.dialogManager();

    /** @private @const {!./entitlements-manager.EntitlementsManager} */
    this.entitlementsManager_ = deps.entitlementsManager();

    /** @private @const {?./client-config-manager.ClientConfigManager} */
    this.clientConfigManager_ = deps.clientConfigManager();

    /** @private @const {!Promise<?ActivityIframeView>} */
    this.activityIframeViewPromise_ = deps
      .storage()
      .get(Constants.USER_TOKEN, true)
      .then((token) => {
        return new ActivityIframeView(
          deps.win(),
          deps.activities(),
          this.getUrl_(deps.pageConfig(), deps.win(), token),
          feArgs({
            'supportsEventManager': true,
            'productType': this.productType_,
          }),
          /* shouldFadeBody */ true
        );
      });

    /** @private {?ActivityIframeView} */
    this.activityIframeView_ = null;
  }

  /**
   * Starts the flow for the suggested audience action.
   * @public
   * @return {!Promise}
   */
  start() {
    if (!this.activityIframeViewPromise_) {
      return Promise.resolve();
    }

    return this.activityIframeViewPromise_.then((activityIframeView) => {
      if (!activityIframeView) {
        return Promise.resolve();
      }

      activityIframeView.on(CompleteAudienceActionResponse, (response) =>
        this.handleCompleteAudienceActionResponse_(response)
      );

      activityIframeView.on(
        AlreadySubscribedResponse,
        this.handleLinkRequest_.bind(this)
      );

      const {fallback} = this.params_;
      if (fallback) {
        /**
         * For a subscription publication, we need to show
         * what would have been the original prompt if the
         * user indicated they do not want to complete an action.
         */
        activityIframeView.onCancel(fallback);
      }

      this.activityIframeView_ = activityIframeView;
      return this.dialogManager_.openView(activityIframeView);
    });
  }

  /**
   * Builds a URL to access the appropriate iframe path
   * @param {!../model/page-config.PageConfig} pageConfig
   * @param {!Window} win
   * @param {?string} userToken
   * @private
   * @return {string}
   */
  getUrl_(pageConfig, win, userToken) {
    const path = actionToIframeMapping[this.params_.action];

    return feUrl(path, {
      'publicationId': pageConfig.getPublicationId(),
      'origin': parseUrl(win.location.href).origin,
      'sut': userToken ? userToken : '',
    });
  }

  /**
   * On a successful response from the dialog, we should:
   * 1) Store the updated user token
   * 2) Clear existing entitlements from the page
   * 3) Re-fetch entitlements which may potentially provide access to the page
   * @param {CompleteAudienceActionResponse} response
   * @private
   */
  handleCompleteAudienceActionResponse_(response) {
    this.dialogManager_.completeView(this.activityIframeView_);
    this.entitlementsManager_.clear();
    const userToken = response.getSwgUserToken();
    if (userToken) {
      this.deps_.storage().set(Constants.USER_TOKEN, userToken, true);
    }
    if (response.getActionCompleted()) {
      this.showSignedInToast_();
    } else {
      this.showAlreadyOptedInToast_();
    }
    this.entitlementsManager_.getEntitlements();
  }

  /** @private */
  showSignedInToast_() {
    // Show 'Signed in as abc@gmail.com' toast on the pub page.
    new Toast(
      this.deps_,
      feUrl('/toastiframe', {
        flavor: 'basic',
      })
    ).open();
  }

  /** @private */
  showAlreadyOptedInToast_() {
    const lang = this.clientConfigManager_.getLanguage();
    let customText = '';
    if (this.params_.action === 'TYPE_REGISTRATION_WALL') {
      customText = msg(
        SWG_I18N_STRINGS.REGWALL_ALREADY_REGISTERED_LANG_MAP,
        lang
      );
    } else if (this.params_.action === 'TYPE_NEWSLETTER_SIGNUP') {
      customText = msg(
        SWG_I18N_STRINGS.NEWSLETTER_ALREADY_SIGNED_UP_LANG_MAP,
        lang
      );
    }
    new Toast(
      this.deps_,
      feUrl('/toastiframe', {
        flavor: 'custom',
        customText,
      })
    ).open();
  }

  /**
   * @param {AlreadySubscribedResponse} response
   * @private
   */
  handleLinkRequest_(response) {
    if (response.getSubscriberOrMember()) {
      this.deps_.callbacks().triggerLoginRequest({linkRequested: false});
    }
  }

  /**
   * Shows the toast of 'no entitlement found' on activity iFrame view.
   * @public
   */
  showNoEntitlementFoundToast() {
    if (this.activityIframeView_) {
      this.activityIframeView_.execute(new EntitlementsResponse());
    }
  }
}
<commit_msg>Remove unnecessary code<commit_after>/**
 * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * An audience action flow will show a dialog prompt to a reader, asking them
 * to complete an action for potentially free, additional metered entitlements.
 *
 * An action flow can potentially assign a new user token so if we complete one,
 * we should:
 * 1) Store the new user token
 * 2) Clear any existing entitlements stored on the page
 * 3) Re-fetch entitlements which may potentially provide access to the page
 * 4) Close the prompt that initiated the flow.
 */

import {ActivityIframeView} from '../ui/activity-iframe-view';
import {
  AlreadySubscribedResponse,
  CompleteAudienceActionResponse,
  EntitlementsResponse,
} from '../proto/api_messages';
import {AutoPromptType} from '../api/basic-subscriptions';
import {Constants} from '../utils/constants';
import {ProductType} from '../api/subscriptions';
import {SWG_I18N_STRINGS} from '../i18n/swg-strings';
import {Toast} from '../ui/toast';
import {feArgs, feUrl} from './services';
import {msg} from '../utils/i18n';
import {parseUrl} from '../utils/url';

/**
 * @typedef {{
 *  action: (string|undefined),
 *  fallback: (function()|undefined),
 *  autoPromptType: (AutoPromptType|undefined)
 * }}
 */
export let AudienceActionParams;

const actionToIframeMapping = {
  'TYPE_REGISTRATION_WALL': '/regwalliframe',
  'TYPE_NEWSLETTER_SIGNUP': '/newsletteriframe',
};

const autopromptTypeToProductTypeMapping = {
  [AutoPromptType.SUBSCRIPTION]: ProductType.SUBSCRIPTION,
  [AutoPromptType.SUBSCRIPTION_LARGE]: ProductType.SUBSCRIPTION,
  [AutoPromptType.CONTRIBUTION]: ProductType.UI_CONTRIBUTION,
  [AutoPromptType.CONTRIBUTION_LARGE]: ProductType.UI_CONTRIBUTION,
};

const DEFAULT_PRODUCT_TYPE = ProductType.SUBSCRIPTION;

/**
 * The flow to initiate and manage handling an audience action.
 */
export class AudienceActionFlow {
  /**
   * @param {!./deps.DepsDef} deps
   * @param {!AudienceActionParams} params
   */
  constructor(deps, params) {
    /** @private @const {!./deps.DepsDef} */
    this.deps_ = deps;

    /** @private @const {!AudienceActionParams} */
    this.params_ = params;

    /** @private @const {!ProductType} */
    this.productType_ = params.autoPromptType
      ? autopromptTypeToProductTypeMapping[params.autoPromptType]
      : DEFAULT_PRODUCT_TYPE;

    /** @private @const {!../components/dialog-manager.DialogManager} */
    this.dialogManager_ = deps.dialogManager();

    /** @private @const {!./entitlements-manager.EntitlementsManager} */
    this.entitlementsManager_ = deps.entitlementsManager();

    /** @private @const {?./client-config-manager.ClientConfigManager} */
    this.clientConfigManager_ = deps.clientConfigManager();

    /** @private @const {!Promise<?ActivityIframeView>} */
    this.activityIframeViewPromise_ = deps
      .storage()
      .get(Constants.USER_TOKEN, true)
      .then((token) => {
        return new ActivityIframeView(
          deps.win(),
          deps.activities(),
          this.getUrl_(deps.pageConfig(), deps.win(), token),
          feArgs({
            'supportsEventManager': true,
            'productType': this.productType_,
          }),
          /* shouldFadeBody */ true
        );
      });

    /** @private {?ActivityIframeView} */
    this.activityIframeView_ = null;
  }

  /**
   * Starts the flow for the suggested audience action.
   * @public
   * @return {!Promise}
   */
  start() {
    return this.activityIframeViewPromise_.then((activityIframeView) => {
      if (!activityIframeView) {
        return Promise.resolve();
      }

      activityIframeView.on(CompleteAudienceActionResponse, (response) =>
        this.handleCompleteAudienceActionResponse_(response)
      );

      activityIframeView.on(
        AlreadySubscribedResponse,
        this.handleLinkRequest_.bind(this)
      );

      const {fallback} = this.params_;
      if (fallback) {
        /**
         * For a subscription publication, we need to show
         * what would have been the original prompt if the
         * user indicated they do not want to complete an action.
         */
        activityIframeView.onCancel(fallback);
      }

      this.activityIframeView_ = activityIframeView;
      return this.dialogManager_.openView(activityIframeView);
    });
  }

  /**
   * Builds a URL to access the appropriate iframe path
   * @param {!../model/page-config.PageConfig} pageConfig
   * @param {!Window} win
   * @param {?string} userToken
   * @private
   * @return {string}
   */
  getUrl_(pageConfig, win, userToken) {
    const path = actionToIframeMapping[this.params_.action];

    return feUrl(path, {
      'publicationId': pageConfig.getPublicationId(),
      'origin': parseUrl(win.location.href).origin,
      'sut': userToken ? userToken : '',
    });
  }

  /**
   * On a successful response from the dialog, we should:
   * 1) Store the updated user token
   * 2) Clear existing entitlements from the page
   * 3) Re-fetch entitlements which may potentially provide access to the page
   * @param {CompleteAudienceActionResponse} response
   * @private
   */
  handleCompleteAudienceActionResponse_(response) {
    this.dialogManager_.completeView(this.activityIframeView_);
    this.entitlementsManager_.clear();
    const userToken = response.getSwgUserToken();
    if (userToken) {
      this.deps_.storage().set(Constants.USER_TOKEN, userToken, true);
    }
    if (response.getActionCompleted()) {
      this.showSignedInToast_();
    } else {
      this.showAlreadyOptedInToast_();
    }
    this.entitlementsManager_.getEntitlements();
  }

  /** @private */
  showSignedInToast_() {
    // Show 'Signed in as abc@gmail.com' toast on the pub page.
    new Toast(
      this.deps_,
      feUrl('/toastiframe', {
        flavor: 'basic',
      })
    ).open();
  }

  /** @private */
  showAlreadyOptedInToast_() {
    const lang = this.clientConfigManager_.getLanguage();
    let customText = '';
    if (this.params_.action === 'TYPE_REGISTRATION_WALL') {
      customText = msg(
        SWG_I18N_STRINGS.REGWALL_ALREADY_REGISTERED_LANG_MAP,
        lang
      );
    } else if (this.params_.action === 'TYPE_NEWSLETTER_SIGNUP') {
      customText = msg(
        SWG_I18N_STRINGS.NEWSLETTER_ALREADY_SIGNED_UP_LANG_MAP,
        lang
      );
    }
    new Toast(
      this.deps_,
      feUrl('/toastiframe', {
        flavor: 'custom',
        customText,
      })
    ).open();
  }

  /**
   * @param {AlreadySubscribedResponse} response
   * @private
   */
  handleLinkRequest_(response) {
    if (response.getSubscriberOrMember()) {
      this.deps_.callbacks().triggerLoginRequest({linkRequested: false});
    }
  }

  /**
   * Shows the toast of 'no entitlement found' on activity iFrame view.
   * @public
   */
  showNoEntitlementFoundToast() {
    if (this.activityIframeView_) {
      this.activityIframeView_.execute(new EntitlementsResponse());
    }
  }
}
<|endoftext|>


<commit_before>/**
 * Copyright 2020 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ActivityPort} from '../components/activities';
import {AnalyticsEvent} from '../proto/api_messages';
import {ClientEventManager} from './client-event-manager';
import {ConfiguredRuntime} from './runtime';
import {
  IFRAME_BOX_SHADOW,
  MINIMIZED_IFRAME_SIZE,
  MeterToastApi,
} from './meter-toast-api';
import {PageConfig} from '../model/page-config';
import {
  ToastCloseRequest,
  ViewSubscriptionsResponse,
} from '../proto/api_messages';
import {getStyle} from '../utils/style';

describes.realWin('MeterToastApi', {}, (env) => {
  let win;
  let runtime;
  let activitiesMock;
  let callbacksMock;
  let eventManagerMock;
  let pageConfig;
  let messageMap;
  let meterToastApi;
  let port;
  let dialogManagerMock;
  const productId = 'pub1:label1';

  beforeEach(() => {
    win = env.win;
    sandbox.stub(win, 'matchMedia').returns({
      'matches': true,
      'addListener': (callback) => callback,
    });
    messageMap = {};
    pageConfig = new PageConfig(productId);
    runtime = new ConfiguredRuntime(win, pageConfig);
    activitiesMock = sandbox.mock(runtime.activities());
    callbacksMock = sandbox.mock(runtime.callbacks());
    dialogManagerMock = sandbox.mock(runtime.dialogManager());
    const eventManager = new ClientEventManager(Promise.resolve());
    eventManagerMock = sandbox.mock(eventManager);
    sandbox.stub(runtime, 'eventManager').callsFake(() => eventManager);
    meterToastApi = new MeterToastApi(runtime, {'isClosable': true});
    sandbox.stub(meterToastApi, 'isMobile_').returns(true);
    port = new ActivityPort();
    port.onResizeRequest = () => {};
    port.whenReady = () => Promise.resolve();
    port.acceptResult = () => Promise.resolve();
    sandbox.stub(port, 'on').callsFake((ctor, callback) => {
      const messageType = new ctor();
      const messageLabel = messageType.label();
      messageMap[messageLabel] = callback;
    });
    sandbox.stub(self.console, 'warn');
  });

  afterEach(() => {
    activitiesMock.verify();
    callbacksMock.verify();
    dialogManagerMock.verify();
    eventManagerMock.verify();
    self.console.warn.restore();
  });

  it('should start the flow correctly without native subscribe request', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.IMPRESSION_METER_TOAST);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.EVENT_OFFERED_METER);
    await meterToastApi.start();
    const errorMessage =
      '[swg.js]: `setOnNativeSubscribeRequest` has not been ' +
      'set before starting the metering flow, so users will not be able to ' +
      'subscribe from the metering dialog directly. Please call ' +
      '`setOnNativeSubscribeRequest` with a subscription flow callback before ' +
      'starting metering.';
    expect(self.console.warn).to.be.calledWithExactly(errorMessage);
  });

  it('should start the flow correctly with native subscribe request', async () => {
    runtime.callbacks().setOnSubscribeRequest(() => {});
    callbacksMock.expects('triggerFlowStarted').once();
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    meterToastApi = new MeterToastApi(runtime, {'isClosable': true});
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.IMPRESSION_METER_TOAST);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.EVENT_OFFERED_METER);
    await meterToastApi.start();
  });

  it('should activate native subscribe request', async () => {
    const nativeStub = sandbox.stub(
      runtime.callbacks(),
      'triggerSubscribeRequest'
    );
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    // Native message.
    const viewSubscriptionsResponse = new ViewSubscriptionsResponse();
    viewSubscriptionsResponse.setNative(true);
    const messageCallback = messageMap[viewSubscriptionsResponse.label()];
    messageCallback(viewSubscriptionsResponse);
    expect(nativeStub).to.be.calledOnce.calledWithExactly();
    // event listeners should be removed.
    const messageStub = sandbox.stub(port, 'execute');
    await win.dispatchEvent(new Event('click'));
    expect(messageStub).to.not.be.called;
    expect(onConsumeCallbackFake).to.not.be.called;
  });

  it('should close iframe on click', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('click'));
    // next three should have no effect
    await win.dispatchEvent(new Event('touchstart'));
    await win.dispatchEvent(new Event('mousedown'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('should not close iframe on scroll events on mobile', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      )
      .never();
    await win.dispatchEvent(new Event('scroll'));
    expect(messageStub).to.not.be.called;
    expect(onConsumeCallbackFake).to.not.be.called;
  });

  it('should close iframe on long scroll events on desktop', async () => {
    meterToastApi.isMobile_.restore();
    sandbox.stub(meterToastApi, 'isMobile_').returns(false);
    sandbox.stub(win, 'setTimeout').callsFake((callback) => callback());
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('scroll'));
    expect(onConsumeCallbackFake).to.not.be.called;
    win.pageYOffset = 10;
    await win.dispatchEvent(new Event('scroll'));
    expect(onConsumeCallbackFake).to.not.be.called;
    win.pageYOffset = 200;
    await win.dispatchEvent(new Event('scroll'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('should close iframe on touchstart', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('touchstart'));
    // next three should have no effect
    await win.dispatchEvent(new Event('scroll'));
    await win.dispatchEvent(new Event('click'));
    await win.dispatchEvent(new Event('mousedown'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('should close iframe on mousedown', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('mousedown'));
    // next three should have no effect
    await win.dispatchEvent(new Event('scroll'));
    await win.dispatchEvent(new Event('touchstart'));
    await win.dispatchEvent(new Event('click'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('removeCloseEventListener should remove all event listeners', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    await meterToastApi.start();
    meterToastApi.removeCloseEventListener();
    await win.dispatchEvent(new Event('click'));
    await win.dispatchEvent(new Event('scroll'));
    await win.dispatchEvent(new Event('touchstart'));
    await win.dispatchEvent(new Event('mousedown'));
    expect(messageStub).to.not.be.called;
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('visible');
  });

  it('should update desktop UI for loading screen', async () => {
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    await meterToastApi.start();

    const element = runtime
      .dialogManager()
      .getDialog()
      .getLoadingView()
      .getElement();
    expect(getStyle(element, 'width')).to.equal(MINIMIZED_IFRAME_SIZE);
    expect(getStyle(element, 'margin')).to.equal('auto');
  });

  it('should update box shadow for iframe on mobile', async () => {
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    await meterToastApi.start();
    const element = runtime.dialogManager().getDialog().getElement();
    expect(getStyle(element, 'box-shadow')).to.equal(IFRAME_BOX_SHADOW);
  });

  it('isMobile_ works as expected', async () => {
    let minWin = {
      navigator: {
        userAgent: 'Inception 1.4 (iPhone; iPhone OS 4.3.1; en_US)',
      },
    };
    let minRuntime = new ConfiguredRuntime(
      Object.assign({}, win, minWin),
      pageConfig
    );
    meterToastApi = new MeterToastApi(minRuntime);
    expect(meterToastApi.isMobile_()).to.be.true;

    minWin = {
      navigator: {
        userAgent:
          'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1',
      },
    };
    minRuntime = new ConfiguredRuntime(
      Object.assign({}, win, minWin),
      pageConfig
    );
    meterToastApi = new MeterToastApi(minRuntime);
    expect(meterToastApi.isMobile_()).to.be.false;
  });
});
<commit_msg>Add REVIEW to pageYOffset<commit_after>/**
 * Copyright 2020 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ActivityPort} from '../components/activities';
import {AnalyticsEvent} from '../proto/api_messages';
import {ClientEventManager} from './client-event-manager';
import {ConfiguredRuntime} from './runtime';
import {
  IFRAME_BOX_SHADOW,
  MINIMIZED_IFRAME_SIZE,
  MeterToastApi,
} from './meter-toast-api';
import {PageConfig} from '../model/page-config';
import {
  ToastCloseRequest,
  ViewSubscriptionsResponse,
} from '../proto/api_messages';
import {getStyle} from '../utils/style';

describes.realWin('MeterToastApi', {}, (env) => {
  let win;
  let runtime;
  let activitiesMock;
  let callbacksMock;
  let eventManagerMock;
  let pageConfig;
  let messageMap;
  let meterToastApi;
  let port;
  let dialogManagerMock;
  const productId = 'pub1:label1';

  beforeEach(() => {
    win = env.win;
    sandbox.stub(win, 'matchMedia').returns({
      'matches': true,
      'addListener': (callback) => callback,
    });
    messageMap = {};
    pageConfig = new PageConfig(productId);
    runtime = new ConfiguredRuntime(win, pageConfig);
    activitiesMock = sandbox.mock(runtime.activities());
    callbacksMock = sandbox.mock(runtime.callbacks());
    dialogManagerMock = sandbox.mock(runtime.dialogManager());
    const eventManager = new ClientEventManager(Promise.resolve());
    eventManagerMock = sandbox.mock(eventManager);
    sandbox.stub(runtime, 'eventManager').callsFake(() => eventManager);
    meterToastApi = new MeterToastApi(runtime, {'isClosable': true});
    sandbox.stub(meterToastApi, 'isMobile_').returns(true);
    port = new ActivityPort();
    port.onResizeRequest = () => {};
    port.whenReady = () => Promise.resolve();
    port.acceptResult = () => Promise.resolve();
    sandbox.stub(port, 'on').callsFake((ctor, callback) => {
      const messageType = new ctor();
      const messageLabel = messageType.label();
      messageMap[messageLabel] = callback;
    });
    sandbox.stub(self.console, 'warn');
  });

  afterEach(() => {
    activitiesMock.verify();
    callbacksMock.verify();
    dialogManagerMock.verify();
    eventManagerMock.verify();
    self.console.warn.restore();
  });

  it('should start the flow correctly without native subscribe request', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.IMPRESSION_METER_TOAST);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.EVENT_OFFERED_METER);
    await meterToastApi.start();
    const errorMessage =
      '[swg.js]: `setOnNativeSubscribeRequest` has not been ' +
      'set before starting the metering flow, so users will not be able to ' +
      'subscribe from the metering dialog directly. Please call ' +
      '`setOnNativeSubscribeRequest` with a subscription flow callback before ' +
      'starting metering.';
    expect(self.console.warn).to.be.calledWithExactly(errorMessage);
  });

  it('should start the flow correctly with native subscribe request', async () => {
    runtime.callbacks().setOnSubscribeRequest(() => {});
    callbacksMock.expects('triggerFlowStarted').once();
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    meterToastApi = new MeterToastApi(runtime, {'isClosable': true});
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.IMPRESSION_METER_TOAST);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(AnalyticsEvent.EVENT_OFFERED_METER);
    await meterToastApi.start();
  });

  it('should activate native subscribe request', async () => {
    const nativeStub = sandbox.stub(
      runtime.callbacks(),
      'triggerSubscribeRequest'
    );
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    // Native message.
    const viewSubscriptionsResponse = new ViewSubscriptionsResponse();
    viewSubscriptionsResponse.setNative(true);
    const messageCallback = messageMap[viewSubscriptionsResponse.label()];
    messageCallback(viewSubscriptionsResponse);
    expect(nativeStub).to.be.calledOnce.calledWithExactly();
    // event listeners should be removed.
    const messageStub = sandbox.stub(port, 'execute');
    await win.dispatchEvent(new Event('click'));
    expect(messageStub).to.not.be.called;
    expect(onConsumeCallbackFake).to.not.be.called;
  });

  it('should close iframe on click', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('click'));
    // next three should have no effect
    await win.dispatchEvent(new Event('touchstart'));
    await win.dispatchEvent(new Event('mousedown'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('should not close iframe on scroll events on mobile', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      )
      .never();
    await win.dispatchEvent(new Event('scroll'));
    expect(messageStub).to.not.be.called;
    expect(onConsumeCallbackFake).to.not.be.called;
  });

  it('should close iframe on long scroll events on desktop', async () => {
    meterToastApi.isMobile_.restore();
    sandbox.stub(meterToastApi, 'isMobile_').returns(false);
    sandbox.stub(win, 'setTimeout').callsFake((callback) => callback());
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('scroll'));
    expect(onConsumeCallbackFake).to.not.be.called;
    win./*REVIEW*/pageYOffset = 10;
    await win.dispatchEvent(new Event('scroll'));
    expect(onConsumeCallbackFake).to.not.be.called;
    win./*REVIEW*/pageYOffset = 200;
    await win.dispatchEvent(new Event('scroll'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('should close iframe on touchstart', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('touchstart'));
    // next three should have no effect
    await win.dispatchEvent(new Event('scroll'));
    await win.dispatchEvent(new Event('click'));
    await win.dispatchEvent(new Event('mousedown'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('should close iframe on mousedown', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    const onConsumeCallbackFake = sandbox.fake();
    meterToastApi.setOnConsumeCallback(onConsumeCallbackFake);
    await meterToastApi.start();
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('hidden');
    const toastCloseRequest = new ToastCloseRequest();
    toastCloseRequest.setClose(true);
    eventManagerMock
      .expects('logSwgEvent')
      .withExactArgs(
        AnalyticsEvent.ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION,
        true
      );
    await win.dispatchEvent(new Event('mousedown'));
    // next three should have no effect
    await win.dispatchEvent(new Event('scroll'));
    await win.dispatchEvent(new Event('touchstart'));
    await win.dispatchEvent(new Event('click'));
    expect(messageStub).to.be.calledOnce.calledWith(toastCloseRequest);
    expect(onConsumeCallbackFake).to.be.calledOnce;
  });

  it('removeCloseEventListener should remove all event listeners', async () => {
    callbacksMock.expects('triggerFlowStarted').once();
    const messageStub = sandbox.stub(port, 'execute');
    activitiesMock.expects('openIframe').returns(Promise.resolve(port));
    await meterToastApi.start();
    meterToastApi.removeCloseEventListener();
    await win.dispatchEvent(new Event('click'));
    await win.dispatchEvent(new Event('scroll'));
    await win.dispatchEvent(new Event('touchstart'));
    await win.dispatchEvent(new Event('mousedown'));
    expect(messageStub).to.not.be.called;
    const $body = win.document.body;
    expect($body.style.overflow).to.equal('visible');
  });

  it('should update desktop UI for loading screen', async () => {
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    await meterToastApi.start();

    const element = runtime
      .dialogManager()
      .getDialog()
      .getLoadingView()
      .getElement();
    expect(getStyle(element, 'width')).to.equal(MINIMIZED_IFRAME_SIZE);
    expect(getStyle(element, 'margin')).to.equal('auto');
  });

  it('should update box shadow for iframe on mobile', async () => {
    const iframeArgs = meterToastApi.activityPorts_.addDefaultArguments({
      isClosable: true,
      hasSubscriptionCallback: runtime
        .callbacks()
        .hasSubscribeRequestCallback(),
    });
    activitiesMock
      .expects('openIframe')
      .withExactArgs(
        sandbox.match((arg) => arg.tagName == 'IFRAME'),
        '$frontend$/swg/_/ui/v1/metertoastiframe?_=_',
        iframeArgs
      )
      .returns(Promise.resolve(port));
    await meterToastApi.start();
    const element = runtime.dialogManager().getDialog().getElement();
    expect(getStyle(element, 'box-shadow')).to.equal(IFRAME_BOX_SHADOW);
  });

  it('isMobile_ works as expected', async () => {
    let minWin = {
      navigator: {
        userAgent: 'Inception 1.4 (iPhone; iPhone OS 4.3.1; en_US)',
      },
    };
    let minRuntime = new ConfiguredRuntime(
      Object.assign({}, win, minWin),
      pageConfig
    );
    meterToastApi = new MeterToastApi(minRuntime);
    expect(meterToastApi.isMobile_()).to.be.true;

    minWin = {
      navigator: {
        userAgent:
          'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1',
      },
    };
    minRuntime = new ConfiguredRuntime(
      Object.assign({}, win, minWin),
      pageConfig
    );
    meterToastApi = new MeterToastApi(minRuntime);
    expect(meterToastApi.isMobile_()).to.be.false;
  });
});
<|endoftext|>