obj2gltf/specs/lib/loadObjSpec.js

712 lines
25 KiB
JavaScript

"use strict";
const Cesium = require("cesium");
const path = require("path");
const loadObj = require("../../lib/loadObj");
const obj2gltf = require("../../lib/obj2gltf");
const Cartesian3 = Cesium.Cartesian3;
const CesiumMath = Cesium.Math;
const clone = Cesium.clone;
const RuntimeError = Cesium.RuntimeError;
const objPath = "specs/data/box/box.obj";
const objRotatedUrl = "specs/data/box-rotated/box-rotated.obj";
const objNormalsPath = "specs/data/box-normals/box-normals.obj";
const objUvsPath = "specs/data/box-uvs/box-uvs.obj";
const objPositionsOnlyPath =
"specs/data/box-positions-only/box-positions-only.obj";
const objNegativeIndicesPath =
"specs/data/box-negative-indices/box-negative-indices.obj";
const objTrianglesPath = "specs/data/box-triangles/box-triangles.obj";
const objObjectsPath = "specs/data/box-objects/box-objects.obj";
const objGroupsPath = "specs/data/box-groups/box-groups.obj";
const objObjectsGroupsPath =
"specs/data/box-objects-groups/box-objects-groups.obj";
const objObjectsGroupsMaterialsPath =
"specs/data/box-objects-groups-materials/box-objects-groups-materials.obj";
const objObjectsGroupsMaterialsPath2 =
"specs/data/box-objects-groups-materials-2/box-objects-groups-materials-2.obj";
const objUsemtlPath = "specs/data/box-usemtl/box-usemtl.obj";
const objNoMaterialsPath = "specs/data/box-no-materials/box-no-materials.obj";
const objMultipleMaterialsPath =
"specs/data/box-multiple-materials/box-multiple-materials.obj";
const objUncleanedPath = "specs/data/box-uncleaned/box-uncleaned.obj";
const objMtllibPath = "specs/data/box-mtllib/box-mtllib.obj";
const objMtllibSpacesPath = "specs/data/box-mtllib-spaces/box mtllib.obj";
const objMissingMtllibPath =
"specs/data/box-missing-mtllib/box-missing-mtllib.obj";
const objMissingUsemtlPath =
"specs/data/box-missing-usemtl/box-missing-usemtl.obj";
const objUnnamedMaterialPath =
"specs/data/box-unnamed-material/box-unnamed-material.obj";
const objExternalResourcesPath =
"specs/data/box-external-resources/box-external-resources.obj";
const objResourcesInRootPath =
"specs/data/box-resources-in-root/box-resources-in-root.obj";
const objExternalResourcesInRootPath =
"specs/data/box-external-resources-in-root/box-external-resources-in-root.obj";
const objTexturedPath = "specs/data/box-textured/box-textured.obj";
const objMissingTexturePath =
"specs/data/box-missing-texture/box-missing-texture.obj";
const objSubdirectoriesPath = "specs/data/box-subdirectories/box-textured.obj";
const objWindowsPaths = "specs/data/box-windows-paths/box-windows-paths.obj";
const objInvalidContentsPath = "specs/data/box/box.mtl";
const objConcavePath = "specs/data/concave/concave.obj";
const objUnnormalizedPath = "specs/data/box-unnormalized/box-unnormalized.obj";
const objMixedAttributesPath =
"specs/data/box-mixed-attributes/box-mixed-attributes.obj";
const objMissingAttributesPath =
"specs/data/box-missing-attributes/box-missing-attributes.obj";
const objIncompletePositionsPath =
"specs/data/box-incomplete-attributes/box-incomplete-positions.obj";
const objIncompleteNormalsPath =
"specs/data/box-incomplete-attributes/box-incomplete-normals.obj";
const objIncompleteUvsPath =
"specs/data/box-incomplete-attributes/box-incomplete-uvs.obj";
const objIncorrectWindingOrderPath =
"specs/data/box-incorrect-winding-order/box-incorrect-winding-order.obj";
const objWithTabs = "specs/data/box-with-tabs/box-with-tabs.obj";
const objInvalidPath = "invalid.obj";
function getMeshes(data) {
let meshes = [];
const nodes = data.nodes;
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
meshes = meshes.concat(nodes[i].meshes);
}
return meshes;
}
function getPrimitives(data) {
let primitives = [];
const nodes = data.nodes;
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const meshes = nodes[i].meshes;
const meshesLength = meshes.length;
for (let j = 0; j < meshesLength; ++j) {
primitives = primitives.concat(meshes[j].primitives);
}
}
return primitives;
}
let options;
describe("loadObj", () => {
beforeEach(() => {
options = clone(obj2gltf.defaults);
options.overridingTextures = {};
options.logger = () => {};
});
it("loads obj with positions, normals, and uvs", async () => {
const data = await loadObj(objPath, options);
const materials = data.materials;
const nodes = data.nodes;
const name = data.name;
const meshes = getMeshes(data);
const primitives = getPrimitives(data);
expect(name).toBe("box");
expect(materials.length).toBe(1);
expect(nodes.length).toBe(1);
expect(meshes.length).toBe(1);
expect(primitives.length).toBe(1);
const node = nodes[0];
const mesh = meshes[0];
const primitive = primitives[0];
expect(node.name).toBe("Cube");
expect(mesh.name).toBe("Cube-Mesh");
expect(primitive.positions.length / 3).toBe(24);
expect(primitive.normals.length / 3).toBe(24);
expect(primitive.uvs.length / 2).toBe(24);
expect(primitive.indices.length).toBe(36);
expect(primitive.material).toBe("Material");
});
it("loads obj with normals", async () => {
const data = await loadObj(objNormalsPath, options);
const primitive = getPrimitives(data)[0];
expect(primitive.positions.length / 3).toBe(24);
expect(primitive.normals.length / 3).toBe(24);
expect(primitive.uvs.length / 2).toBe(0);
});
it("normalizes normals", async () => {
const data = await loadObj(objUnnormalizedPath, options);
const scratchNormal = new Cesium.Cartesian3();
const primitive = getPrimitives(data)[0];
const normals = primitive.normals;
const normalsLength = normals.length / 3;
for (let i = 0; i < normalsLength; ++i) {
const normalX = normals.get(i * 3);
const normalY = normals.get(i * 3 + 1);
const normalZ = normals.get(i * 3 + 2);
const normal = Cartesian3.fromElements(
normalX,
normalY,
normalZ,
scratchNormal,
);
expect(
CesiumMath.equalsEpsilon(
Cartesian3.magnitude(normal),
1.0,
CesiumMath.EPSILON5,
),
).toBe(true);
}
});
it("loads obj with uvs", async () => {
const data = await loadObj(objUvsPath, options);
const primitive = getPrimitives(data)[0];
expect(primitive.positions.length / 3).toBe(20);
expect(primitive.normals.length / 3).toBe(0);
expect(primitive.uvs.length / 2).toBe(20);
});
it("loads obj with negative indices", async () => {
const results = [
await loadObj(objPositionsOnlyPath, options),
await loadObj(objNegativeIndicesPath, options),
];
const positionsReference = getPrimitives(
results[0],
)[0].positions.toFloatBuffer();
const positions = getPrimitives(results[1])[0].positions.toFloatBuffer();
expect(positions).toEqual(positionsReference);
});
it("loads obj with triangle faces", async () => {
const data = await loadObj(objTrianglesPath, options);
const primitive = getPrimitives(data)[0];
expect(primitive.positions.length / 3).toBe(24);
expect(primitive.indices.length).toBe(36);
});
it("loads obj with objects", async () => {
const data = await loadObj(objObjectsPath, options);
const nodes = data.nodes;
expect(nodes.length).toBe(3);
expect(nodes[0].name).toBe("CubeBlue");
expect(nodes[1].name).toBe("CubeGreen");
expect(nodes[2].name).toBe("CubeRed");
const primitives = getPrimitives(data);
expect(primitives.length).toBe(3);
expect(primitives[0].material).toBe("Blue");
expect(primitives[1].material).toBe("Green");
expect(primitives[2].material).toBe("Red");
});
it("loads obj with groups", async () => {
const data = await loadObj(objGroupsPath, options);
const nodes = data.nodes;
expect(nodes.length).toBe(3);
expect(nodes[0].name).toBe("CubeBlue");
expect(nodes[1].name).toBe("CubeGreen");
expect(nodes[2].name).toBe("CubeRed");
const primitives = getPrimitives(data);
expect(primitives.length).toBe(3);
expect(primitives[0].material).toBe("Blue");
expect(primitives[1].material).toBe("Green");
expect(primitives[2].material).toBe("Red");
});
it("loads obj with objects and groups", async () => {
const data = await loadObj(objObjectsGroupsPath, options);
const nodes = data.nodes;
expect(nodes.length).toBe(3);
expect(nodes[0].name).toBe("CubeBlue");
expect(nodes[1].name).toBe("CubeGreen");
expect(nodes[2].name).toBe("CubeRed");
const meshes = getMeshes(data);
expect(meshes.length).toBe(3);
expect(meshes[0].name).toBe("CubeBlue_CubeBlue_Blue");
expect(meshes[1].name).toBe("CubeGreen_CubeGreen_Green");
expect(meshes[2].name).toBe("CubeRed_CubeRed_Red");
const primitives = getPrimitives(data);
expect(primitives.length).toBe(3);
expect(primitives[0].material).toBe("Blue");
expect(primitives[1].material).toBe("Green");
expect(primitives[2].material).toBe("Red");
});
function loadsObjWithObjectsGroupsAndMaterials(data) {
const nodes = data.nodes;
expect(nodes.length).toBe(1);
expect(nodes[0].name).toBe("Cube");
const meshes = getMeshes(data);
expect(meshes.length).toBe(3);
expect(meshes[0].name).toBe("Blue");
expect(meshes[1].name).toBe("Green");
expect(meshes[2].name).toBe("Red");
const primitives = getPrimitives(data);
expect(primitives.length).toBe(6);
expect(primitives[0].material).toBe("Blue");
expect(primitives[1].material).toBe("Green");
expect(primitives[2].material).toBe("Green");
expect(primitives[3].material).toBe("Red");
expect(primitives[4].material).toBe("Red");
expect(primitives[5].material).toBe("Blue");
}
it("loads obj with objects, groups, and materials", async () => {
const data = await loadObj(objObjectsGroupsMaterialsPath, options);
loadsObjWithObjectsGroupsAndMaterials(data);
});
it("loads obj with objects, groups, and materials (2)", async () => {
// The usemtl lines are placed in an unordered fashion but
// should produce the same result as the previous test
const data = await loadObj(objObjectsGroupsMaterialsPath2, options);
loadsObjWithObjectsGroupsAndMaterials(data);
});
it("loads obj with concave face containing 5 vertices", async () => {
const data = await loadObj(objConcavePath, options);
const primitive = getPrimitives(data)[0];
expect(primitive.positions.length / 3).toBe(30);
expect(primitive.indices.length).toBe(48);
});
it("loads obj with usemtl only", async () => {
const data = await loadObj(objUsemtlPath, options);
const nodes = data.nodes;
expect(nodes.length).toBe(1);
expect(nodes[0].name).toBe("Node"); // default name
const meshes = getMeshes(data);
expect(meshes.length).toBe(1);
expect(meshes[0].name).toBe("Node-Mesh");
const primitives = getPrimitives(data);
expect(primitives.length).toBe(3);
expect(primitives[0].material).toBe("Blue");
expect(primitives[1].material).toBe("Green");
expect(primitives[2].material).toBe("Red");
});
it("loads obj with no materials", async () => {
const data = await loadObj(objNoMaterialsPath, options);
const nodes = data.nodes;
expect(nodes.length).toBe(1);
expect(nodes[0].name).toBe("Node"); // default name
const primitives = getPrimitives(data);
expect(primitives.length).toBe(1);
});
it("loads obj with multiple materials", async () => {
// The usemtl markers are interleaved, but should condense to just three primitives
const data = await loadObj(objMultipleMaterialsPath, options);
const nodes = data.nodes;
expect(nodes.length).toBe(1);
const primitives = getPrimitives(data);
expect(primitives.length).toBe(3);
expect(primitives[0].indices.length).toBe(12);
expect(primitives[1].indices.length).toBe(12);
expect(primitives[2].indices.length).toBe(12);
expect(primitives[0].material).toBe("Red");
expect(primitives[1].material).toBe("Green");
expect(primitives[2].material).toBe("Blue");
for (let i = 0; i < 3; ++i) {
const indices = primitives[i].indices;
for (let j = 0; j < indices.length; ++j) {
expect(indices.get(j)).toBeLessThan(8);
}
}
});
it("loads obj uncleaned", async () => {
// Obj with extraneous o, g, and usemtl lines
// Also tests handling of o and g lines with the same names
const data = await loadObj(objUncleanedPath, options);
const nodes = data.nodes;
const meshes = getMeshes(data);
const primitives = getPrimitives(data);
expect(nodes.length).toBe(1);
expect(meshes.length).toBe(1);
expect(primitives.length).toBe(1);
expect(nodes[0].name).toBe("Cube");
expect(meshes[0].name).toBe("Cube_1");
});
it("loads obj with multiple mtllibs", async () => {
const data = await loadObj(objMtllibPath, options);
const materials = data.materials;
expect(materials.length).toBe(3);
// .mtl files are loaded in an arbitrary order, so sort for testing purposes
materials.sort((a, b) => {
return a.name.localeCompare(b.name);
});
expect(materials[0].name).toBe("Blue");
expect(materials[0].pbrMetallicRoughness.baseColorFactor).toEqual([
0.0, 0.0, 0.64, 1.0,
]);
expect(materials[1].name).toBe("Green");
expect(materials[1].pbrMetallicRoughness.baseColorFactor).toEqual([
0.0, 0.64, 0.0, 1.0,
]);
expect(materials[2].name).toBe("Red");
expect(materials[2].pbrMetallicRoughness.baseColorFactor).toEqual([
0.64, 0.0, 0.0, 1.0,
]);
});
it("loads obj with mtllib paths with spaces", async () => {
const data = await loadObj(objMtllibSpacesPath, options);
const materials = data.materials;
expect(materials.length).toBe(3);
// .mtl files are loaded in an arbitrary order, so sort for testing purposes
materials.sort((a, b) => {
return a.name.localeCompare(b.name);
});
expect(materials[0].name).toBe("Blue");
expect(materials[0].pbrMetallicRoughness.baseColorFactor).toEqual([
0.0, 0.0, 0.64, 1.0,
]);
expect(materials[1].name).toBe("Green");
expect(materials[1].pbrMetallicRoughness.baseColorFactor).toEqual([
0.0, 0.64, 0.0, 1.0,
]);
expect(materials[2].name).toBe("Red");
expect(materials[2].pbrMetallicRoughness.baseColorFactor).toEqual([
0.64, 0.0, 0.0, 1.0,
]);
});
it("loads obj with missing mtllib", async () => {
const spy = jasmine.createSpy("logger");
options.logger = spy;
const data = await loadObj(objMissingMtllibPath, options);
expect(data.materials.length).toBe(0);
expect(spy.calls.argsFor(0)[0].indexOf("ENOENT") >= 0).toBe(true);
expect(spy.calls.argsFor(0)[0].indexOf(path.resolve("/box.mtl")) >= 0).toBe(
true,
);
expect(
spy.calls
.argsFor(1)[0]
.indexOf(
"Attempting to read the material file from within the obj directory instead.",
) >= 0,
).toBe(true);
expect(spy.calls.argsFor(2)[0].indexOf("ENOENT") >= 0).toBe(true);
expect(
spy.calls.argsFor(3)[0].indexOf("Could not read material file") >= 0,
).toBe(true);
});
it("loads obj with missing usemtl", async () => {
const data = await loadObj(objMissingUsemtlPath, options);
expect(data.materials.length).toBe(1);
expect(data.nodes[0].meshes[0].primitives[0].material).toBe("Material");
});
it("loads obj with unnamed material", async () => {
const data = await loadObj(objUnnamedMaterialPath, options);
expect(data.materials.length).toBe(1);
expect(data.nodes[0].meshes[0].primitives[0].material).toBe("");
});
it("loads .mtl outside of the obj directory", async () => {
const data = await loadObj(objExternalResourcesPath, options);
const materials = data.materials;
expect(materials.length).toBe(2);
// .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material
const materialTextured =
materials[0].name === "MaterialTextured" ? materials[0] : materials[1];
const baseColorTexture =
materialTextured.pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.source).toBeDefined();
expect(baseColorTexture.name).toEqual("cesium");
});
it("does not load .mtl outside of the obj directory when secure is true", async () => {
const spy = jasmine.createSpy("logger");
options.logger = spy;
options.secure = true;
const data = await loadObj(objExternalResourcesPath, options);
expect(data.materials.length).toBe(1); // obj references 2 materials, one of which is outside the input directory
expect(
spy.calls
.argsFor(0)[0]
.indexOf(
"The material file is outside of the obj directory and the secure flag is true. Attempting to read the material file from within the obj directory instead.",
) >= 0,
).toBe(true);
expect(spy.calls.argsFor(1)[0].indexOf("ENOENT") >= 0).toBe(true);
expect(
spy.calls.argsFor(2)[0].indexOf("Could not read material file") >= 0,
).toBe(true);
});
it("loads .mtl from root directory when the .mtl path does not exist", async () => {
const data = await loadObj(objResourcesInRootPath, options);
const baseColorTexture =
data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.name).toBe("cesium");
expect(baseColorTexture.source).toBeDefined();
});
it("loads .mtl from root directory when the .mtl path is outside of the obj directory and secure is true", async () => {
options.secure = true;
const data = await loadObj(objExternalResourcesInRootPath, options);
const materials = data.materials;
expect(materials.length).toBe(2);
// .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material
const materialTextured =
materials[0].name === "MaterialTextured" ? materials[0] : materials[1];
const baseColorTexture =
materialTextured.pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.source).toBeDefined();
expect(baseColorTexture.name).toEqual("cesium");
});
it("loads obj with texture", async () => {
const data = await loadObj(objTexturedPath, options);
const baseColorTexture =
data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.name).toBe("cesium");
expect(baseColorTexture.source).toBeDefined();
});
it("loads obj with missing texture", async () => {
const spy = jasmine.createSpy("logger");
options.logger = spy;
const data = await loadObj(objMissingTexturePath, options);
const baseColorTexture =
data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture).toBeUndefined();
expect(spy.calls.argsFor(0)[0].indexOf("ENOENT") >= 0).toBe(true);
expect(
spy.calls.argsFor(0)[0].indexOf(path.resolve("/cesium.png")) >= 0,
).toBe(true);
expect(
spy.calls
.argsFor(1)[0]
.indexOf(
"Attempting to read the texture file from within the obj directory instead.",
) >= 0,
).toBe(true);
expect(spy.calls.argsFor(2)[0].indexOf("ENOENT") >= 0).toBe(true);
expect(
spy.calls.argsFor(3)[0].indexOf("Could not read texture file") >= 0,
).toBe(true);
});
it("loads obj with subdirectories", async () => {
const data = await loadObj(objSubdirectoriesPath, options);
const baseColorTexture =
data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.name).toBe("cesium");
expect(baseColorTexture.source).toBeDefined();
});
it("loads obj with windows paths", async () => {
const data = await loadObj(objWindowsPaths, options);
const baseColorTexture =
data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.name).toBe("cesium");
expect(baseColorTexture.source).toBeDefined();
});
it("loads an obj where coordinates are separated by tabs", async () => {
/**
* We know Tinkercad to produce files with coordinates separated by tabs.
*/
const data = await loadObj(objWithTabs, options);
const primitive = getPrimitives(data)[0];
expect(primitive.positions.length / 3).toBe(24);
expect(primitive.normals.length / 3).toBe(24);
expect(primitive.uvs.length / 2).toBe(24);
});
it("separates faces that don't use the same attributes as other faces in the primitive", async () => {
const data = await loadObj(objMixedAttributesPath, options);
const primitives = getPrimitives(data);
expect(primitives.length).toBe(4);
expect(primitives[0].indices.length).toBe(18); // 6 faces
expect(primitives[1].indices.length).toBe(6); // 2 faces
expect(primitives[2].indices.length).toBe(6); // 2 faces
expect(primitives[3].indices.length).toBe(6); // 2 faces
});
function getFirstPosition(data) {
const primitive = getPrimitives(data)[0];
return new Cartesian3(
primitive.positions.get(0),
primitive.positions.get(1),
primitive.positions.get(2),
);
}
function getFirstNormal(data) {
const primitive = getPrimitives(data)[0];
return new Cartesian3(
primitive.normals.get(0),
primitive.normals.get(1),
primitive.normals.get(2),
);
}
async function checkAxisConversion(
inputUpAxis,
outputUpAxis,
position,
normal,
) {
const sameAxis = inputUpAxis === outputUpAxis;
options.inputUpAxis = inputUpAxis;
options.outputUpAxis = outputUpAxis;
const data = await loadObj(objRotatedUrl, options);
const rotatedPosition = getFirstPosition(data);
const rotatedNormal = getFirstNormal(data);
if (sameAxis) {
expect(rotatedPosition).toEqual(position);
expect(rotatedNormal).toEqual(normal);
} else {
expect(rotatedPosition).not.toEqual(position);
expect(rotatedNormal).not.toEqual(normal);
}
}
it("performs up axis conversion", async () => {
const data = await loadObj(objRotatedUrl, options);
const position = getFirstPosition(data);
const normal = getFirstNormal(data);
const axes = ["X", "Y", "Z"];
const axesLength = axes.length;
for (let i = 0; i < axesLength; ++i) {
for (let j = 0; j < axesLength; ++j) {
await checkAxisConversion(axes[i], axes[j], position, normal);
}
}
});
it("ignores missing normals and uvs", async () => {
const data = await loadObj(objMissingAttributesPath, options);
const primitive = getPrimitives(data)[0];
expect(primitive.positions.length).toBeGreaterThan(0);
expect(primitive.normals.length).toBe(0);
expect(primitive.uvs.length).toBe(0);
});
async function loadAndGetIndices(objPath, options) {
const data = await loadObj(objPath, options);
const primitive = getPrimitives(data)[0];
const indices = primitive.indices;
return new Uint16Array(indices.toUint16Buffer().buffer);
}
it("applies triangle winding order sanitization", async () => {
options.triangleWindingOrderSanitization = false;
const indicesIncorrect = await loadAndGetIndices(
objIncorrectWindingOrderPath,
options,
);
options.triangleWindingOrderSanitization = true;
const indicesCorrect = await loadAndGetIndices(
objIncorrectWindingOrderPath,
options,
);
expect(indicesIncorrect[0]).toBe(0);
expect(indicesIncorrect[2]).toBe(2);
expect(indicesIncorrect[1]).toBe(1);
expect(indicesCorrect[0]).toBe(0);
expect(indicesCorrect[2]).toBe(1);
expect(indicesCorrect[1]).toBe(2);
});
it("throws when position index is out of bounds", async () => {
let thrownError;
try {
await loadObj(objIncompletePositionsPath, options);
} catch (e) {
thrownError = e;
}
expect(thrownError).toEqual(
new RuntimeError("Position index 1 is out of bounds"),
);
});
it("throws when normal index is out of bounds", async () => {
let thrownError;
try {
await loadObj(objIncompleteNormalsPath, options);
} catch (e) {
thrownError = e;
}
expect(thrownError).toEqual(
new RuntimeError("Normal index 1 is out of bounds"),
);
});
it("throws when uv index is out of bounds", async () => {
let thrownError;
try {
await loadObj(objIncompleteUvsPath, options);
} catch (e) {
thrownError = e;
}
expect(thrownError).toEqual(
new RuntimeError("UV index 1 is out of bounds"),
);
});
it("throws when file has invalid contents", async () => {
let thrownError;
try {
await loadObj(objInvalidContentsPath, options);
} catch (e) {
thrownError = e;
}
expect(thrownError).toEqual(
new RuntimeError(
`${objInvalidContentsPath} does not have any geometry data`,
),
);
});
it("throw when reading invalid file", async () => {
let thrownError;
try {
await loadObj(objInvalidPath, options);
} catch (e) {
thrownError = e;
}
expect(
thrownError.message.startsWith("ENOENT: no such file or directory"),
).toBe(true);
});
});