cesium/Specs/addDefaultMatchers.js

1042 lines
28 KiB
JavaScript

import {
Cartesian2,
defined,
DeveloperError,
FeatureDetection,
PrimitiveType,
Buffer,
BufferUsage,
ClearCommand,
DrawCommand,
ShaderProgram,
VertexArray,
Math as CesiumMath,
} from "@cesium/engine";
import equals from "./equals.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 makeAsyncThrowFunction(debug, Type, name) {
if (debug) {
return function (util) {
return {
compare: function (actualPromise, message) {
// based on the built-in Jasmine toBeRejectedWithError async-matcher
if (!defined(actualPromise) || !defined(actualPromise.then)) {
throw new Error("Expected function to be called on a promise.");
}
return actualPromise
.then(() => {
return {
pass: false,
message:
"Expected a promise to be rejected but it was resolved.",
};
})
.catch((e) => {
let result = e instanceof Type || e.name === name;
if (defined(message)) {
if (typeof message === "string") {
result = result && e.message === message;
} else {
// if the expected message is a regular expression check it against the error message
// this matches how the builtin .toRejectWithError(Error, /message/) works
// https://github.com/jasmine/jasmine/blob/main/src/core/matchers/toThrowError.js
result = result && message.test(e.message);
}
}
return {
pass: result,
message: result
? `Expected a promise to be rejected with ${name}.`
: `Expected a promise to be rejected with ${
defined(message) ? `${name}: ${message}` : name
}, but it was rejected with ${e}`,
};
});
},
};
};
}
return function () {
return {
compare: function (actualPromise) {
return Promise.resolve(actualPromise)
.then(() => {
return { pass: true };
})
.catch((e) => {
// Ignore any error
return { pass: true };
});
},
negativeCompare: function (actualPromise) {
return Promise.resolve(actualPromise)
.then(() => {
return { pass: true };
})
.catch((e) => {
// Ignore any error
return { pass: true };
});
},
};
};
}
function makeThrowFunction(debug, Type, name) {
if (debug) {
return function (util) {
return {
compare: function (actual, message) {
// based on the built-in Jasmine toThrow matcher
let result = false;
let 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 || exception.name === name;
}
if (defined(message)) {
if (typeof message === "string") {
result = result && exception.message === message;
} else {
// if the expected message is a regular expression check it against the error message
// this matches how the builtin .toRejectWithError(Error, /message/) works
// https://github.com/jasmine/jasmine/blob/main/src/core/matchers/toThrowError.js
result = result && message.test(exception.message);
}
}
let testMessage;
if (result) {
testMessage = `Expected function not to throw ${name} , but it threw ${exception.message || exception}`;
} else {
testMessage = defined(message)
? `Expected to throw with ${name}: ${message}, but it was thrown with ${exception}`
: `Expected function to throw with ${name}.`;
}
return {
pass: result,
message: testMessage,
};
},
};
};
}
return function () {
return {
compare: function (actual, expected) {
return { pass: true };
},
negativeCompare: function (actual, expected) {
return { pass: true };
},
};
};
}
function createDefaultMatchers(debug) {
return {
toBeBetween: function (util) {
return {
compare: function (actual, lower, upper) {
if (lower > upper) {
const tmp = upper;
upper = lower;
lower = tmp;
}
return { pass: actual >= lower && actual <= upper };
},
};
},
toStartWith: function (util) {
return {
compare: function (actual, expected) {
return { pass: actual.slice(0, expected.length) === expected };
},
};
},
toEndWith: function (util) {
return {
compare: function (actual, expected) {
return { pass: actual.slice(-expected.length) === expected };
},
};
},
toEqual: function (util) {
return {
compare: function (actual, expected) {
return {
pass: equals(util, actual, expected),
};
},
};
},
toEqualEpsilon: function (util) {
return {
compare: function (actual, expected, epsilon) {
function equalityTester(a, b) {
a = typedArrayToArray(a);
b = typedArrayToArray(b);
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; ++i) {
if (!equalityTester(a[i], b[i])) {
return false;
}
}
return true;
}
let 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;
}
if (defined(a) && defined(b)) {
const keys = Object.keys(a);
for (let i = 0; i < keys.length; i++) {
if (!b.hasOwnProperty(keys[i])) {
return false;
}
const aVal = a[keys[i]];
const bVal = b[keys[i]];
if (!equalityTester(aVal, bVal)) {
return false;
}
}
return true;
}
return equals(util, a, b);
}
const result = equalityTester(actual, expected);
return { pass: result };
},
};
},
toConformToInterface: function (util) {
return {
compare: function (actual, expectedInterface) {
// All function properties on the prototype should also exist on the actual's prototype.
const actualPrototype = actual.prototype;
const expectedInterfacePrototype = expectedInterface.prototype;
for (const 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 };
},
};
},
toRender: function (util) {
return {
compare: function (actual, expected) {
return renderEquals(util, actual, expected, true);
},
};
},
notToRender: function (util) {
return {
compare: function (actual, expected) {
return renderEquals(util, actual, expected, false);
},
};
},
toRenderAndCall: function (util) {
return {
compare: function (actual, expected) {
const actualRgba = renderAndReadPixels(actual);
const 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.
const callback = expected;
callback(actualRgba);
}
return {
pass: true,
};
},
};
},
toRenderPixelCountAndCall: function (util) {
return {
compare: function (actual, expected) {
const actualRgba = renderAndReadPixels(actual);
const 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.
const callback = expected;
callback(countRenderedPixels(actualRgba));
}
return {
pass: true,
};
},
};
},
toPickPrimitive: function (util) {
return {
compare: function (actual, expected, x, y, width, height) {
return pickPrimitiveEquals(actual, expected, x, y, width, height);
},
};
},
notToPick: function (util) {
return {
compare: function (actual, x, y, width, height) {
return pickPrimitiveEquals(actual, undefined, x, y, width, height);
},
};
},
toDrillPickPrimitive: function (util) {
return {
compare: function (actual, expected, x, y, width, height) {
return drillPickPrimitiveEquals(actual, 1, x, y, width, height);
},
};
},
notToDrillPick: function (util) {
return {
compare: function (actual, x, y, width, height) {
return drillPickPrimitiveEquals(actual, 0, x, y, width, height);
},
};
},
toPickAndCall: function (util) {
return {
compare: function (actual, expected, args) {
const scene = actual;
const result = scene.pick(args ?? new Cartesian2(0, 0));
const 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.
const callback = expected;
callback(result);
}
return {
pass: true,
};
},
};
},
toPickVoxelAndCall: function (util) {
return {
compare: function (actual, expected, args) {
const scene = actual;
const result = scene.pickVoxel(args ?? new Cartesian2(0, 0));
const 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.
const callback = expected;
callback(result);
}
return {
pass: true,
};
},
};
},
toDrillPickAndCall: function (util) {
return {
compare: function (actual, expected, limit) {
const scene = actual;
const pickedObjects = scene.drillPick(new Cartesian2(0, 0), limit);
const 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.
const callback = expected;
callback(pickedObjects);
}
return {
pass: true,
};
},
};
},
toPickFromRayAndCall: function (util) {
return {
compare: function (actual, expected, ray, objectsToExclude, width) {
const scene = actual;
const result = scene.pickFromRay(ray, objectsToExclude, width);
const 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.
const callback = expected;
callback(result);
}
return {
pass: true,
};
},
};
},
toDrillPickFromRayAndCall: function (util) {
return {
compare: function (
actual,
expected,
ray,
limit,
objectsToExclude,
width,
) {
const scene = actual;
const results = scene.drillPickFromRay(
ray,
limit,
objectsToExclude,
width,
);
const 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.
const callback = expected;
callback(results);
}
return {
pass: true,
};
},
};
},
toSampleHeightAndCall: function (util) {
return {
compare: function (
actual,
expected,
position,
objectsToExclude,
width,
) {
const scene = actual;
const results = scene.sampleHeight(position, objectsToExclude, width);
const 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.
const callback = expected;
callback(results);
}
return {
pass: true,
};
},
};
},
toClampToHeightAndCall: function (util) {
return {
compare: function (
actual,
expected,
cartesian,
objectsToExclude,
width,
) {
const scene = actual;
const results = scene.clampToHeight(
cartesian,
objectsToExclude,
width,
);
const 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.
const callback = expected;
callback(results);
}
return {
pass: true,
};
},
};
},
toPickPositionAndCall: function (util) {
return {
compare: function (actual, expected, x, y) {
const scene = actual;
const canvas = scene.canvas;
x = x ?? canvas.clientWidth / 2;
y = y ?? canvas.clientHeight / 2;
const result = scene.pickPosition(new Cartesian2(x, y));
const 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.
const callback = expected;
callback(result);
}
return {
pass: true,
};
},
};
},
toReadPixels: function (util) {
return {
compare: function (actual, expected) {
let context;
let framebuffer;
let epsilon = 0;
const options = actual;
if (defined(options.context)) {
// options were passed to to a framebuffer
context = options.context;
framebuffer = options.framebuffer;
epsilon = options.epsilon ?? epsilon;
} else {
context = options;
}
const rgba = context.readPixels({
framebuffer: framebuffer,
});
let pass = true;
let message;
const 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) {
return {
compare: function (actual, expected) {
const context = actual;
const rgba = context.readPixels();
let pass = true;
let message;
const 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) {
return {
compare: function (actual, expected) {
const actualRgba = contextRenderAndReadPixels(actual).color;
const 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.
const callback = expected;
callback(actualRgba);
}
return {
pass: true,
};
},
};
},
contextToRender: function (util) {
return {
compare: function (actual, expected) {
return expectContextToRender(actual, expected, true);
},
};
},
notContextToRender: function (util) {
return {
compare: function (actual, expected) {
return expectContextToRender(actual, expected, false);
},
};
},
toBeImageOrImageBitmap: function (util) {
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",
),
};
}
function createDefaultAsyncMatchers(debug) {
return {
toBeRejectedWithDeveloperError: makeAsyncThrowFunction(
debug,
DeveloperError,
"DeveloperError",
),
};
}
function countRenderedPixels(rgba) {
const pixelCount = rgba.length / 4;
let count = 0;
for (let i = 0; i < pixelCount; i++) {
const 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) {
let scene;
if (defined(options.scene)) {
// options were passed to render the scene at a given time or prime shadow map
scene = options.scene;
const 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, actual, expected, expectEqual) {
const 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,
};
}
const eq = equals(util, actualRgba, expected);
const pass = expectEqual ? eq : !eq;
let 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) {
const scene = actual;
const windowPosition = new Cartesian2(x, y);
const result = scene.pick(windowPosition, width, height);
if (!!window.webglStub) {
return {
pass: true,
};
}
let pass = true;
let 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) {
const scene = actual;
const windowPosition = new Cartesian2(x, y);
const result = scene.drillPick(windowPosition, undefined, width, height);
if (!!window.webglStub) {
return {
pass: true,
};
}
let pass = true;
let 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) {
const context = options.context;
let vs = options.vertexShader;
const fs = options.fragmentShader;
let sp = options.shaderProgram;
const uniformMap = options.uniformMap;
const modelMatrix = options.modelMatrix;
const depth = options.depth ?? 0.0;
const clear = options.clear ?? true;
let 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 =
"in vec4 position; void main() { gl_PointSize = 1.0; gl_Position = position; }";
}
sp = ShaderProgram.fromCache({
context: context,
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: {
position: 0,
},
});
}
let 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();
}
const command = new DrawCommand({
primitiveType: PrimitiveType.POINTS,
shaderProgram: sp,
vertexArray: va,
uniformMap: uniformMap,
modelMatrix: modelMatrix,
});
command.execute(context);
const rgba = context.readPixels();
sp = sp.destroy();
va = va.destroy();
return {
color: rgba,
clearColor: clearColor,
};
}
function expectContextToRender(actual, expected, expectEqual) {
const options = actual;
const context = options.context;
const clear = options.clear ?? true;
const epsilon = options.epsilon ?? 0;
if (!defined(expected)) {
expected = [255, 255, 255, 255];
}
const webglStub = !!window.webglStub;
const output = contextRenderAndReadPixels(options);
if (clear) {
const clearedRgba = output.clearColor;
if (!webglStub) {
const 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}`,
};
}
}
}
const 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));
this.addAsyncMatchers(createDefaultAsyncMatchers(debug));
};
}
export default addDefaultMatchers;