import equals from "./equals.js"; import { Cartesian2 } from "../Source/Cesium.js"; import { defaultValue } from "../Source/Cesium.js"; import { defined } from "../Source/Cesium.js"; import { DeveloperError } from "../Source/Cesium.js"; import { FeatureDetection } from "../Source/Cesium.js"; import { Math as CesiumMath } from "../Source/Cesium.js"; import { PrimitiveType } from "../Source/Cesium.js"; import { RuntimeError } from "../Source/Cesium.js"; import { Buffer } from "../Source/Cesium.js"; import { BufferUsage } from "../Source/Cesium.js"; import { ClearCommand } from "../Source/Cesium.js"; import { DrawCommand } from "../Source/Cesium.js"; import { ShaderProgram } from "../Source/Cesium.js"; import { VertexArray } from "../Source/Cesium.js"; function createMissingFunctionMessageFunction( item, actualPrototype, expectedInterfacePrototype ) { return function () { return ( "Expected function '" + item + "' to exist on " + actualPrototype.constructor.name + " because it should implement interface " + expectedInterfacePrototype.constructor.name + "." ); }; } function makeThrowFunction(debug, Type, name) { if (debug) { return function (util, customEqualityTesters) { return { compare: function (actual, expected) { // based on the built-in Jasmine toThrow matcher var result = false; var exception; if (typeof actual !== "function") { throw new Error("Actual is not a function"); } try { actual(); } catch (e) { exception = e; } if (exception) { result = exception instanceof Type; } var message; if (result) { message = [ "Expected function not to throw " + name + " , but it threw", exception.message || exception, ].join(" "); } else { message = "Expected function to throw " + name + "."; } return { pass: result, message: message, }; }, }; }; } return function () { return { compare: function (actual, expected) { return { pass: true }; }, negativeCompare: function (actual, expected) { return { pass: true }; }, }; }; } function createDefaultMatchers(debug) { return { toBeGreaterThanOrEqualTo: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return { pass: actual >= expected }; }, }; }, toBeLessThanOrEqualTo: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return { pass: actual <= expected }; }, }; }, toBeBetween: function (util, customEqualityTesters) { return { compare: function (actual, lower, upper) { if (lower > upper) { var tmp = upper; upper = lower; lower = tmp; } return { pass: actual >= lower && actual <= upper }; }, }; }, toStartWith: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return { pass: actual.slice(0, expected.length) === expected }; }, }; }, toEndWith: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return { pass: actual.slice(-expected.length) === expected }; }, }; }, toEqual: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return { pass: equals(util, customEqualityTesters, actual, expected), }; }, }; }, toEqualEpsilon: function (util, customEqualityTesters) { return { compare: function (actual, expected, epsilon) { function equalityTester(a, b) { if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) { return false; } for (var i = 0; i < a.length; ++i) { if (!equalityTester(a[i], b[i])) { return false; } } return true; } var to_run; if (defined(a)) { if (typeof a.equalsEpsilon === "function") { return a.equalsEpsilon(b, epsilon); } else if (a instanceof Object) { // Check if the current object has a static function named 'equalsEpsilon' to_run = Object.getPrototypeOf(a).constructor.equalsEpsilon; if (typeof to_run === "function") { return to_run(a, b, epsilon); } } } if (defined(b)) { if (typeof b.equalsEpsilon === "function") { return b.equalsEpsilon(a, epsilon); } else if (b instanceof Object) { // Check if the current object has a static function named 'equalsEpsilon' to_run = Object.getPrototypeOf(b).constructor.equalsEpsilon; if (typeof to_run === "function") { return to_run(b, a, epsilon); } } } if (typeof a === "number" || typeof b === "number") { return Math.abs(a - b) <= epsilon; } return undefined; } var result = equals(util, [equalityTester], actual, expected); return { pass: result }; }, }; }, toConformToInterface: function (util, customEqualityTesters) { return { compare: function (actual, expectedInterface) { // All function properties on the prototype should also exist on the actual's prototype. var actualPrototype = actual.prototype; var expectedInterfacePrototype = expectedInterface.prototype; for (var item in expectedInterfacePrototype) { if ( expectedInterfacePrototype.hasOwnProperty(item) && typeof expectedInterfacePrototype[item] === "function" && !actualPrototype.hasOwnProperty(item) ) { return { pass: false, message: createMissingFunctionMessageFunction( item, actualPrototype, expectedInterfacePrototype ), }; } } return { pass: true }; }, }; }, toBeInstanceOf: function (util, customEqualityTesters) { return { compare: function (actual, expectedConstructor) { var result = {}; if (expectedConstructor === String) { result.pass = typeof actual === "string" || actual instanceof String; } else if (expectedConstructor === Number) { result.pass = typeof actual === "number" || actual instanceof Number; } else if (expectedConstructor === Function) { result.pass = typeof actual === "function" || actual instanceof Function; } else if (expectedConstructor === Object) { result.pass = actual !== null && typeof actual === "object"; } else if (expectedConstructor === Boolean) { result.pass = typeof actual === "boolean"; } else { result.pass = actual instanceof expectedConstructor; } result.message = "Expected " + Object.prototype.toString.call(actual) + " to be instance of " + expectedConstructor.name + ", but was instance of " + (actual && actual.constructor.name); return result; }, }; }, toRender: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return renderEquals( util, customEqualityTesters, actual, expected, true ); }, }; }, notToRender: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return renderEquals( util, customEqualityTesters, actual, expected, false ); }, }; }, toRenderAndCall: function (util, customEqualityTesters) { return { compare: function (actual, expected) { var actualRgba = renderAndReadPixels(actual); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(actualRgba); } return { pass: true, }; }, }; }, toRenderPixelCountAndCall: function (util, customEqualityTesters) { return { compare: function (actual, expected) { var actualRgba = renderAndReadPixels(actual); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(countRenderedPixels(actualRgba)); } return { pass: true, }; }, }; }, toPickPrimitive: function (util, customEqualityTesters) { return { compare: function (actual, expected, x, y, width, height) { return pickPrimitiveEquals(actual, expected, x, y, width, height); }, }; }, notToPick: function (util, customEqualityTesters) { return { compare: function (actual, x, y, width, height) { return pickPrimitiveEquals(actual, undefined, x, y, width, height); }, }; }, toDrillPickPrimitive: function (util, customEqualityTesters) { return { compare: function (actual, expected, x, y, width, height) { return drillPickPrimitiveEquals(actual, 1, x, y, width, height); }, }; }, notToDrillPick: function (util, customEqualityTesters) { return { compare: function (actual, x, y, width, height) { return drillPickPrimitiveEquals(actual, 0, x, y, width, height); }, }; }, toPickAndCall: function (util, customEqualityTesters) { return { compare: function (actual, expected, args) { var scene = actual; var result = scene.pick(defaultValue(args, new Cartesian2(0, 0))); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(result); } return { pass: true, }; }, }; }, toDrillPickAndCall: function (util, customEqualityTesters) { return { compare: function (actual, expected, limit) { var scene = actual; var pickedObjects = scene.drillPick(new Cartesian2(0, 0), limit); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(pickedObjects); } return { pass: true, }; }, }; }, toPickFromRayAndCall: function (util, customEqualityTesters) { return { compare: function (actual, expected, ray, objectsToExclude, width) { var scene = actual; var result = scene.pickFromRay(ray, objectsToExclude, width); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(result); } return { pass: true, }; }, }; }, toDrillPickFromRayAndCall: function (util, customEqualityTesters) { return { compare: function ( actual, expected, ray, limit, objectsToExclude, width ) { var scene = actual; var results = scene.drillPickFromRay( ray, limit, objectsToExclude, width ); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(results); } return { pass: true, }; }, }; }, toSampleHeightAndCall: function (util, customEqualityTesters) { return { compare: function ( actual, expected, position, objectsToExclude, width ) { var scene = actual; var results = scene.sampleHeight(position, objectsToExclude, width); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(results); } return { pass: true, }; }, }; }, toClampToHeightAndCall: function (util, customEqualityTesters) { return { compare: function ( actual, expected, cartesian, objectsToExclude, width ) { var scene = actual; var results = scene.clampToHeight(cartesian, objectsToExclude, width); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(results); } return { pass: true, }; }, }; }, toPickPositionAndCall: function (util, customEqualityTesters) { return { compare: function (actual, expected, x, y) { var scene = actual; var canvas = scene.canvas; x = defaultValue(x, canvas.clientWidth / 2); y = defaultValue(y, canvas.clientHeight / 2); var result = scene.pickPosition(new Cartesian2(x, y)); var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(result); } return { pass: true, }; }, }; }, toReadPixels: function (util, customEqualityTesters) { return { compare: function (actual, expected) { var context; var framebuffer; var epsilon = 0; var options = actual; if (defined(options.context)) { // options were passed to to a framebuffer context = options.context; framebuffer = options.framebuffer; epsilon = defaultValue(options.epsilon, epsilon); } else { context = options; } var rgba = context.readPixels({ framebuffer: framebuffer, }); var pass = true; var message; var webglStub = !!window.webglStub; if (!webglStub) { if ( !CesiumMath.equalsEpsilon(rgba[0], expected[0], 0, epsilon) || !CesiumMath.equalsEpsilon(rgba[1], expected[1], 0, epsilon) || !CesiumMath.equalsEpsilon(rgba[2], expected[2], 0, epsilon) || !CesiumMath.equalsEpsilon(rgba[3], expected[3], 0, epsilon) ) { pass = false; if (epsilon === 0) { message = "Expected context to render " + expected + ", but rendered: " + rgba; } else { message = "Expected context to render " + expected + " with epsilon = " + epsilon + ", but rendered: " + rgba; } } } return { pass: pass, message: message, }; }, }; }, notToReadPixels: function (util, customEqualityTesters) { return { compare: function (actual, expected) { var context = actual; var rgba = context.readPixels(); var pass = true; var message; var webglStub = !!window.webglStub; if (!webglStub) { if ( rgba[0] === expected[0] && rgba[1] === expected[1] && rgba[2] === expected[2] && rgba[3] === expected[3] ) { pass = false; message = "Expected context not to render " + expected + ", but rendered: " + rgba; } } return { pass: pass, message: message, }; }, }; }, contextToRenderAndCall: function (util, customEqualityTesters) { return { compare: function (actual, expected) { var actualRgba = contextRenderAndReadPixels(actual).color; var webglStub = !!window.webglStub; if (!webglStub) { // The callback may have expectations that fail, which still makes the // spec fail, as we desired, even though this matcher sets pass to true. var callback = expected; callback(actualRgba); } return { pass: true, }; }, }; }, contextToRender: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return expectContextToRender(actual, expected, true); }, }; }, notContextToRender: function (util, customEqualityTesters) { return { compare: function (actual, expected) { return expectContextToRender(actual, expected, false); }, }; }, toBeImageOrImageBitmap: function (util, customEqualityTesters) { return { compare: function (actual) { if (typeof createImageBitmap !== "function") { return { pass: actual instanceof Image, }; } return { pass: actual instanceof ImageBitmap || actual instanceof Image, }; }, }; }, toThrowDeveloperError: makeThrowFunction( debug, DeveloperError, "DeveloperError" ), toThrowRuntimeError: makeThrowFunction(true, RuntimeError, "RuntimeError"), toThrowSyntaxError: makeThrowFunction(true, SyntaxError, "SyntaxError"), }; } function countRenderedPixels(rgba) { var pixelCount = rgba.length / 4; var count = 0; for (var i = 0; i < pixelCount; i++) { var index = i * 4; if ( rgba[index] !== 0 || rgba[index + 1] !== 0 || rgba[index + 2] !== 0 || rgba[index + 3] !== 255 ) { count++; } } return count; } function renderAndReadPixels(options) { var scene; if (defined(options.scene)) { // options were passed to render the scene at a given time or prime shadow map scene = options.scene; var time = options.time; scene.initializeFrame(); if (defined(options.primeShadowMap)) { scene.render(time); // Computes shadow near/far for next frame } scene.render(time); } else { scene = options; scene.initializeFrame(); scene.render(); } return scene.context.readPixels(); } function isTypedArray(o) { return FeatureDetection.typedArrayTypes.some(function (type) { return o instanceof type; }); } function typedArrayToArray(array) { if (isTypedArray(array)) { return Array.prototype.slice.call(array, 0); } return array; } function renderEquals( util, customEqualityTesters, actual, expected, expectEqual ) { var actualRgba = renderAndReadPixels(actual); // When the WebGL stub is used, all WebGL function calls are noops so // the expectation is not verified. This allows running all the WebGL // tests, to exercise as much Cesium code as possible, even if the system // doesn't have a WebGL implementation or a reliable one. if (!!window.webglStub) { return { pass: true, }; } var eq = equals(util, customEqualityTesters, actualRgba, expected); var pass = expectEqual ? eq : !eq; var message; if (!pass) { message = "Expected " + (expectEqual ? "" : "not ") + "to render [" + typedArrayToArray(expected) + "], but actually rendered [" + typedArrayToArray(actualRgba) + "]."; } return { pass: pass, message: message, }; } function pickPrimitiveEquals(actual, expected, x, y, width, height) { var scene = actual; var windowPosition = new Cartesian2(x, y); var result = scene.pick(windowPosition, width, height); if (!!window.webglStub) { return { pass: true, }; } var pass = true; var message; if (defined(expected)) { pass = result.primitive === expected; } else { pass = !defined(result); } if (!pass) { message = "Expected to pick " + expected + ", but picked: " + result; } return { pass: pass, message: message, }; } function drillPickPrimitiveEquals(actual, expected, x, y, width, height) { var scene = actual; var windowPosition = new Cartesian2(x, y); var result = scene.drillPick(windowPosition, undefined, width, height); if (!!window.webglStub) { return { pass: true, }; } var pass = true; var message; if (defined(expected)) { pass = result.length === expected; } else { pass = !defined(result); } if (!pass) { message = "Expected to pick " + expected + ", but picked: " + result; } return { pass: pass, message: message, }; } function contextRenderAndReadPixels(options) { var context = options.context; var vs = options.vertexShader; var fs = options.fragmentShader; var sp = options.shaderProgram; var uniformMap = options.uniformMap; var modelMatrix = options.modelMatrix; var depth = defaultValue(options.depth, 0.0); var clear = defaultValue(options.clear, true); var clearColor; if (!defined(context)) { throw new DeveloperError("options.context is required."); } if (!defined(fs) && !defined(sp)) { throw new DeveloperError( "options.fragmentShader or options.shaderProgram is required." ); } if (defined(fs) && defined(sp)) { throw new DeveloperError( "Both options.fragmentShader and options.shaderProgram can not be used at the same time." ); } if (defined(vs) && defined(sp)) { throw new DeveloperError( "Both options.vertexShader and options.shaderProgram can not be used at the same time." ); } if (!defined(sp)) { if (!defined(vs)) { vs = "attribute vec4 position; void main() { gl_PointSize = 1.0; gl_Position = position; }"; } sp = ShaderProgram.fromCache({ context: context, vertexShaderSource: vs, fragmentShaderSource: fs, attributeLocations: { position: 0, }, }); } var va = new VertexArray({ context: context, attributes: [ { index: 0, vertexBuffer: Buffer.createVertexBuffer({ context: context, typedArray: new Float32Array([0.0, 0.0, depth, 1.0]), usage: BufferUsage.STATIC_DRAW, }), componentsPerAttribute: 4, }, ], }); if (clear) { ClearCommand.ALL.execute(context); clearColor = context.readPixels(); } var command = new DrawCommand({ primitiveType: PrimitiveType.POINTS, shaderProgram: sp, vertexArray: va, uniformMap: uniformMap, modelMatrix: modelMatrix, }); command.execute(context); var rgba = context.readPixels(); sp = sp.destroy(); va = va.destroy(); return { color: rgba, clearColor: clearColor, }; } function expectContextToRender(actual, expected, expectEqual) { var options = actual; var context = options.context; var clear = defaultValue(options.clear, true); var epsilon = defaultValue(options.epsilon, 0); if (!defined(expected)) { expected = [255, 255, 255, 255]; } var webglStub = !!window.webglStub; var output = contextRenderAndReadPixels(options); if (clear) { var clearedRgba = output.clearColor; if (!webglStub) { var expectedAlpha = context.options.webgl.alpha ? 0 : 255; if ( clearedRgba[0] !== 0 || clearedRgba[1] !== 0 || clearedRgba[2] !== 0 || clearedRgba[3] !== expectedAlpha ) { return { pass: false, message: "After clearing the framebuffer, expected context to render [0, 0, 0, " + expectedAlpha + "], but rendered: " + clearedRgba, }; } } } var rgba = output.color; if (!webglStub) { if (expectEqual) { if ( !CesiumMath.equalsEpsilon(rgba[0], expected[0], 0, epsilon) || !CesiumMath.equalsEpsilon(rgba[1], expected[1], 0, epsilon) || !CesiumMath.equalsEpsilon(rgba[2], expected[2], 0, epsilon) || !CesiumMath.equalsEpsilon(rgba[3], expected[3], 0, epsilon) ) { return { pass: false, message: "Expected context to render " + expected + ", but rendered: " + rgba, }; } } else if ( CesiumMath.equalsEpsilon(rgba[0], expected[0], 0, epsilon) && CesiumMath.equalsEpsilon(rgba[1], expected[1], 0, epsilon) && CesiumMath.equalsEpsilon(rgba[2], expected[2], 0, epsilon) && CesiumMath.equalsEpsilon(rgba[3], expected[3], 0, epsilon) ) { return { pass: false, message: "Expected context not to render " + expected + ", but rendered: " + rgba, }; } } return { pass: true, }; } function addDefaultMatchers(debug) { return function () { this.addMatchers(createDefaultMatchers(debug)); }; } export default addDefaultMatchers;