import { Cartesian2 } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Cartesian4 } from "../../Source/Cesium.js";
import { CesiumTerrainProvider } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { combine } from "../../Source/Cesium.js";
import { Credit } from "../../Source/Cesium.js";
import { defaultValue } from "../../Source/Cesium.js";
import { defined } from "../../Source/Cesium.js";
import { DistanceDisplayCondition } from "../../Source/Cesium.js";
import { Ellipsoid } from "../../Source/Cesium.js";
import { Event } from "../../Source/Cesium.js";
import { FeatureDetection } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { JulianDate } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { Matrix3 } from "../../Source/Cesium.js";
import { Matrix4 } from "../../Source/Cesium.js";
import { PerspectiveFrustum } from "../../Source/Cesium.js";
import { PrimitiveType } from "../../Source/Cesium.js";
import { Resource } from "../../Source/Cesium.js";
import { Transforms } from "../../Source/Cesium.js";
import { WebGLConstants } from "../../Source/Cesium.js";
import { Pass } from "../../Source/Cesium.js";
import { RenderState } from "../../Source/Cesium.js";
import { ShaderSource } from "../../Source/Cesium.js";
import { Axis } from "../../Source/Cesium.js";
import { ClippingPlane } from "../../Source/Cesium.js";
import { ClippingPlaneCollection } from "../../Source/Cesium.js";
import { ColorBlendMode } from "../../Source/Cesium.js";
import { DracoLoader } from "../../Source/Cesium.js";
import { HeightReference } from "../../Source/Cesium.js";
import { Model } from "../../Source/Cesium.js";
import { ModelAnimationLoop } from "../../Source/Cesium.js";
import { DepthFunction } from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import { when } from "../../Source/Cesium.js";
import ModelOutlineLoader from "../../Source/Scene/ModelOutlineLoader.js";

describe(
  "Scene/Model",
  function () {
    var boxUrl = "./Data/Models/Box/CesiumBoxTest.gltf";
    var boxNoTechniqueUrl = "./Data/Models/Box/CesiumBoxTest-NoTechnique.gltf";
    var boxNoIndicesUrl = "./Data/Models/Box-NoIndices/box-noindices.gltf";
    var texturedBoxUrl =
      "./Data/Models/Box-Textured/CesiumTexturedBoxTest.gltf";
    var texturedBoxSeparateUrl =
      "./Data/Models/Box-Textured-Separate/CesiumTexturedBoxTest.gltf";
    var texturedBoxBasePathUrl =
      "./Data/Models/Box-Textured-BasePath/CesiumTexturedBoxTest.gltf";
    var texturedBoxKTX2Url =
      "./Data/Models/Box-Textured-KTX2-Basis/CesiumTexturedBoxTest.gltf";
    var texturedBoxKTX2MipmapUrl =
      "./Data/Models/Box-Textured-KTX2-Mipmap/CheckerboardTexturedBoxTest.gltf";
    var texturedBoxCustomUrl =
      "./Data/Models/Box-Textured-Custom/CesiumTexturedBoxTest.gltf";
    var texturedBoxKhrBinaryUrl =
      "./Data/Models/Box-Textured-Binary/CesiumTexturedBoxTest.glb";
    var texturedBoxTextureTransformUrl =
      "./Data/Models/Box-Texture-Transform/CesiumTexturedBoxTest.gltf";
    var texturedBoxWebpUrl =
      "./Data/Models/Box-Textured-Webp/CesiumBoxWebp.gltf";
    var boxRtcUrl = "./Data/Models/Box-RTC/Box.gltf";
    var boxEcefUrl = "./Data/Models/Box-ECEF/ecef.gltf";
    var boxWithUnusedMaterial = "./Data/Models/BoxWithUnusedMaterial/Box.gltf";
    var boxArticulationsUrl =
      "./Data/Models/Box-Articulations/Box-Articulations.gltf";

    var cesiumAirUrl = "./Data/Models/CesiumAir/Cesium_Air.gltf";
    var cesiumAir_0_8Url = "./Data/Models/CesiumAir/Cesium_Air_0_8.gltf";
    var animBoxesUrl = "./Data/Models/anim-test-1-boxes/anim-test-1-boxes.gltf";
    var riggedFigureUrl =
      "./Data/Models/rigged-figure-test/rigged-figure-test.gltf";
    var riggedSimpleUrl = "./Data/Models/rigged-simple/rigged-simple.gltf";
    var boxConstantUrl = "./Data/Models/MaterialsCommon/BoxConstant.gltf";
    var boxLambertUrl = "./Data/Models/MaterialsCommon/BoxLambert.gltf";
    var boxBlinnUrl = "./Data/Models/MaterialsCommon/BoxBlinn.gltf";
    var boxPhongUrl = "./Data/Models/MaterialsCommon/BoxPhong.gltf";
    var boxNoLightUrl = "./Data/Models/MaterialsCommon/BoxNoLight.gltf";
    var boxAmbientLightUrl =
      "./Data/Models/MaterialsCommon/BoxAmbientLight.gltf";
    var boxDirectionalLightUrl =
      "./Data/Models/MaterialsCommon/BoxDirectionalLight.gltf";
    var boxPointLightUrl = "./Data/Models/MaterialsCommon/BoxPointLight.gltf";
    var boxSpotLightUrl = "./Data/Models/MaterialsCommon/BoxSpotLight.gltf";
    var boxTransparentUrl = "./Data/Models/MaterialsCommon/BoxTransparent.gltf";
    var boxColorUrl = "./Data/Models/Box-Color/Box-Color.gltf";
    var boxUint32Indices =
      "./Data/Models/Box-Uint32Indices/Box-Uint32Indices.gltf";
    var boxQuantizedUrl =
      "./Data/Models/WEB3DQuantizedAttributes/Box-Quantized.gltf";
    var boxColorQuantizedUrl =
      "./Data/Models/WEB3DQuantizedAttributes/Box-Color-Quantized.gltf";
    var boxScalarQuantizedUrl =
      "./Data/Models/WEB3DQuantizedAttributes/Box-Scalar-Quantized.gltf";
    var milkTruckQuantizedUrl =
      "./Data/Models/WEB3DQuantizedAttributes/CesiumMilkTruck-Quantized.gltf";
    var milkTruckQuantizedMismatchUrl =
      "./Data/Models/WEB3DQuantizedAttributes/CesiumMilkTruck-Mismatch-Quantized.gltf";
    var duckQuantizedUrl =
      "./Data/Models/WEB3DQuantizedAttributes/Duck-Quantized.gltf";
    var riggedSimpleQuantizedUrl =
      "./Data/Models/WEB3DQuantizedAttributes/RiggedSimple-Quantized.gltf";
    var CesiumManUrl = "./Data/Models/MaterialsCommon/Cesium_Man.gltf";
    var interpolationTestUrl =
      "./Data/Models/InterpolationTest/InterpolationTest.glb";

    var boomBoxUrl = "./Data/Models/PBR/BoomBox/BoomBox.gltf";
    var boomBoxPbrSpecularGlossinessUrl =
      "./Data/Models/PBR/BoomBoxSpecularGlossiness/BoomBox.gltf";
    var boomBoxPbrSpecularGlossinessDefaultsUrl =
      "./Data/Models/PBR/BoomBoxSpecularGlossiness/BoomBox-Default.gltf";
    var boomBoxPbrSpecularGlossinessNoTextureUrl =
      "./Data/Models/PBR/BoomBoxSpecularGlossiness/BoomBox-NoTexture.gltf";
    var boxPbrUrl = "./Data/Models/PBR/Box/Box.gltf";
    var boxPbrUnlitUrl = "./Data/Models/PBR/BoxUnlit/BoxUnlit.gltf";
    var boxAnimatedPbrUrl = "./Data/Models/PBR/BoxAnimated/BoxAnimated.gltf";
    var boxInterleavedPbrUrl =
      "./Data/Models/PBR/BoxInterleaved/BoxInterleaved.gltf";
    var riggedSimplePbrUrl = "./Data/Models/PBR/RiggedSimple/RiggedSimple.gltf";
    var animatedMorphCubeUrl =
      "./Data/Models/PBR/AnimatedMorphCube/AnimatedMorphCube.gltf";
    var twoSidedPlaneUrl = "./Data/Models/PBR/TwoSidedPlane/TwoSidedPlane.gltf";
    var vertexColorTestUrl =
      "./Data/Models/PBR/VertexColorTest/VertexColorTest.gltf";
    var emissiveUrl = "./Data/Models/PBR/BoxEmissive/BoxEmissive.gltf";
    var dracoCompressedModelUrl =
      "./Data/Models/DracoCompression/CesiumMilkTruck/CesiumMilkTruck.gltf";
    var dracoCompressedModelWithAnimationUrl =
      "./Data/Models/DracoCompression/CesiumMan/CesiumMan.gltf";
    var dracoCompressedModelWithLinesUrl =
      "./Data/Models/DracoCompression/BoxWithLines/BoxWithLines.gltf";
    var dracoBoxVertexColorsRGBUrl =
      "./Data/Models/DracoCompression/BoxVertexColorsDracoRGB.gltf";
    var dracoBoxVertexColorsRGBAUrl =
      "./Data/Models/DracoCompression/BoxVertexColorsDracoRGBA.gltf";
    var multiUvTestUrl = "./Data/Models/MultiUVTest/MultiUVTest.glb";

    var boxGltf2Url = "./Data/Models/Box-Gltf-2/Box.gltf";
    var boxGltf2WithTechniquesUrl =
      "./Data/Models/Box-Gltf-2-Techniques/Box.gltf";

    var boxBackFaceCullingUrl =
      "./Data/Models/Box-Back-Face-Culling/Box-Back-Face-Culling.gltf";

    var texturedBoxModel;
    var cesiumAirModel;
    var animBoxesModel;
    var riggedFigureModel;

    var scene;
    var primitives;

    beforeAll(function () {
      scene = createScene();
      primitives = scene.primitives;

      var modelPromises = [];
      modelPromises.push(
        loadModel(texturedBoxUrl).then(function (model) {
          texturedBoxModel = model;
        })
      );
      modelPromises.push(
        loadModel(cesiumAirUrl, {
          minimumPixelSize: 1,
          maximumScale: 200,
          asynchronous: false,
        }).then(function (model) {
          cesiumAirModel = model;
        })
      );
      modelPromises.push(
        loadModel(animBoxesUrl, {
          scale: 2.0,
        }).then(function (model) {
          animBoxesModel = model;
        })
      );
      modelPromises.push(
        loadModel(riggedFigureUrl).then(function (model) {
          riggedFigureModel = model;
        })
      );
      modelPromises.push(FeatureDetection.supportsWebP.initialize());

      return when.all(modelPromises);
    });

    afterAll(function () {
      scene.destroyForSpecs();
    });

    beforeEach(function () {
      scene.morphTo3D(0.0);

      var camera = scene.camera;
      camera.frustum = new PerspectiveFrustum();
      camera.frustum.aspectRatio =
        scene.drawingBufferWidth / scene.drawingBufferHeight;
      camera.frustum.fov = CesiumMath.toRadians(60.0);
    });

    function addZoomTo(model) {
      model.zoomTo = function (zoom) {
        zoom = defaultValue(zoom, 4.0);

        var camera = scene.camera;
        var center = Matrix4.multiplyByPoint(
          model.modelMatrix,
          model.boundingSphere.center,
          new Cartesian3()
        );
        var r =
          zoom * Math.max(model.boundingSphere.radius, camera.frustum.near);
        camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, r));
      };
    }

    function loadModel(url, options) {
      options = combine(options, {
        modelMatrix: Transforms.eastNorthUpToFixedFrame(
          Cartesian3.fromDegrees(0.0, 0.0, 100.0)
        ),
        url: url,
        id: url,
        show: false,
      });

      var model = primitives.add(Model.fromGltf(options));
      addZoomTo(model);

      return pollToPromise(
        function () {
          // Render scene to progressively load the model
          scene.renderForSpecs();
          return model.ready;
        },
        { timeout: 10000 }
      )
        .then(function () {
          return model;
        })
        .otherwise(function () {
          return when.reject(model);
        });
    }

    function loadModelJson(gltf, options) {
      options = defaultValue(options, defaultValue.EMPTY_OBJECT);
      options = combine(options, {
        modelMatrix: Transforms.eastNorthUpToFixedFrame(
          Cartesian3.fromDegrees(0.0, 0.0, 100.0)
        ),
        gltf: gltf,
        show: defaultValue(options.show, false),
      });

      var model = primitives.add(new Model(options));
      addZoomTo(model);

      return pollToPromise(
        function () {
          // Render scene to progressively load the model
          scene.renderForSpecs();
          return model.ready;
        },
        { timeout: 10000 }
      ).then(function () {
        return model;
      });
    }

    function verifyRender(model) {
      expect(model.ready).toBe(true);

      expect({
        scene: scene,
        time: JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC")),
      }).toRenderAndCall(function (rgba) {
        expect(rgba).toEqual([0, 0, 0, 255]);
      });

      expect(scene).toRender([0, 0, 0, 255]);
      model.show = true;
      model.zoomTo();

      expect({
        scene: scene,
        time: JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC")),
      }).toRenderAndCall(function (rgba) {
        expect(rgba).not.toEqual([0, 0, 0, 255]);
      });

      model.show = false;
    }

    it("fromGltf throws without options", function () {
      expect(function () {
        Model.fromGltf();
      }).toThrowDeveloperError();
    });

    it("fromGltf throws without options.url", function () {
      expect(function () {
        Model.fromGltf({});
      }).toThrowDeveloperError();
    });

    it("sets model properties", function () {
      var modelMatrix = Transforms.eastNorthUpToFixedFrame(
        Cartesian3.fromDegrees(0.0, 0.0, 100.0)
      );

      expect(texturedBoxModel.gltf).toBeDefined();
      expect(texturedBoxModel.basePath).toEqual(
        "./Data/Models/Box-Textured/CesiumTexturedBoxTest.gltf"
      );
      expect(texturedBoxModel.show).toEqual(false);
      expect(texturedBoxModel.modelMatrix).toEqual(modelMatrix);
      expect(texturedBoxModel.scale).toEqual(1.0);
      expect(texturedBoxModel.minimumPixelSize).toEqual(0.0);
      expect(texturedBoxModel.maximumScale).toBeUndefined();
      expect(texturedBoxModel.id).toEqual(texturedBoxUrl);
      expect(texturedBoxModel.allowPicking).toEqual(true);
      expect(texturedBoxModel.activeAnimations).toBeDefined();
      expect(texturedBoxModel.ready).toEqual(true);
      expect(texturedBoxModel.asynchronous).toEqual(true);
      expect(texturedBoxModel.releaseGltfJson).toEqual(false);
      expect(texturedBoxModel.cacheKey).toBeDefined();
      expect(texturedBoxModel.cacheKey).not.toContain(
        "Data/Models/Box-Textured/CesiumTexturedBoxTest.gltf"
      );
      expect(texturedBoxModel.debugShowBoundingVolume).toEqual(false);
      expect(texturedBoxModel.debugWireframe).toEqual(false);
      expect(texturedBoxModel.distanceDisplayCondition).toBeUndefined();
      expect(texturedBoxModel.silhouetteColor).toEqual(Color.RED);
      expect(texturedBoxModel.silhouetteSize).toEqual(0.0);
      expect(texturedBoxModel.color).toEqual(Color.WHITE);
      expect(texturedBoxModel.colorBlendMode).toEqual(ColorBlendMode.HIGHLIGHT);
      expect(texturedBoxModel.colorBlendAmount).toEqual(0.5);
      expect(texturedBoxModel.credit).toBeUndefined();
    });

    it("preserves query string in url", function () {
      var params = "?param1=1&param2=2";
      var url = texturedBoxUrl + params;
      var model = Model.fromGltf({
        url: url,
      });
      expect(model.basePath).toEndWith(params);
    });

    it("fromGltf takes a base path", function () {
      var url = texturedBoxBasePathUrl;
      var basePath = "./Data/Models/Box-Textured-Separate/";
      var model = Model.fromGltf({
        url: url,
        basePath: basePath,
      });
      expect(model.basePath).toEndWith(basePath);
      expect(model._cacheKey).toEndWith(basePath);
    });

    it("fromGltf takes Resource as url and basePath parameters", function () {
      spyOn(Resource._Implementations, "loadWithXhr").and.callThrough();

      var url = new Resource({
        url: texturedBoxUrl,
      });
      var basePath = new Resource({
        url: "./Data/Models/Box-Textured-Separate/",
      });
      var model = Model.fromGltf({
        url: url,
        basePath: basePath,
      });
      expect(model._resource).toEqual(basePath);
      expect(Resource._Implementations.loadWithXhr.calls.argsFor(0)[0]).toEqual(
        url.url
      );
    });

    it("fromGltf takes a credit", function () {
      var url = texturedBoxBasePathUrl;
      var model = Model.fromGltf({
        url: url,
        credit: "This is my model credit",
      });
      expect(model.credit).toBeInstanceOf(Credit);
    });

    it("renders", function () {
      verifyRender(texturedBoxModel);
    });

    it("renders in CV", function () {
      scene.morphToColumbusView(0.0);
      verifyRender(texturedBoxModel);
    });

    it("renders in 2D", function () {
      scene.morphTo2D(0.0);
      verifyRender(texturedBoxModel);
    });

    it("renders in 2D over the IDL", function () {
      return when(loadModel(texturedBoxUrl)).then(function (model) {
        model.modelMatrix = Transforms.eastNorthUpToFixedFrame(
          Cartesian3.fromDegrees(180.0, 0.0, 100.0)
        );
        scene.morphTo2D(0.0);
        verifyRender(model);
      });
    });

    it("renders RTC in 2D", function () {
      return loadModel(boxRtcUrl, {
        modelMatrix: Matrix4.IDENTITY,
        minimumPixelSize: 1,
      }).then(function (m) {
        scene.morphTo2D(0.0);
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("renders ECEF in 2D", function () {
      return loadModel(boxEcefUrl, {
        modelMatrix: Matrix4.IDENTITY,
        minimumPixelSize: undefined,
      }).then(function (m) {
        scene.morphTo2D(0.0);
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("renders RTC in CV", function () {
      return loadModel(boxRtcUrl, {
        modelMatrix: Matrix4.IDENTITY,
        minimumPixelSize: 1,
      }).then(function (m) {
        scene.morphToColumbusView(0.0);
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("renders ECEF in CV", function () {
      return loadModel(boxEcefUrl, {
        modelMatrix: Matrix4.IDENTITY,
        minimumPixelSize: undefined,
      }).then(function (m) {
        scene.morphToColumbusView(0.0);
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("does not render during morph", function () {
      var commandList = scene.frameState.commandList;
      var model = texturedBoxModel;
      model.show = true;
      model.cull = false;
      expect(model.ready).toBe(true);

      scene.renderForSpecs();
      expect(commandList.length).toBeGreaterThan(0);

      scene.morphTo2D(1.0);
      scene.renderForSpecs();
      expect(commandList.length).toBe(0);
      scene.completeMorph();
      model.show = false;
    });

    it("renders x-up model", function () {
      return Resource.fetchJson(boxEcefUrl).then(function (gltf) {
        // Model data is z-up. Edit the transform to be z-up to x-up.
        gltf.nodes.node_transform.matrix = Matrix4.pack(
          Axis.Z_UP_TO_X_UP,
          new Array(16)
        );

        return loadModelJson(gltf, {
          modelMatrix: Matrix4.IDENTITY,
          upAxis: Axis.X,
        }).then(function (m) {
          verifyRender(m);
          expect(m.upAxis).toBe(Axis.X);
          primitives.remove(m);
        });
      });
    });

    it("renders y-up model", function () {
      return Resource.fetchJson(boxEcefUrl).then(function (gltf) {
        // Model data is z-up. Edit the transform to be z-up to y-up.
        gltf.nodes.node_transform.matrix = Matrix4.pack(
          Axis.Z_UP_TO_Y_UP,
          new Array(16)
        );

        return loadModelJson(gltf, {
          modelMatrix: Matrix4.IDENTITY,
          upAxis: Axis.Y,
        }).then(function (m) {
          verifyRender(m);
          expect(m.upAxis).toBe(Axis.Y);
          primitives.remove(m);
        });
      });
    });

    it("renders z-up model", function () {
      return Resource.fetchJson(boxEcefUrl).then(function (gltf) {
        // Model data is z-up. Edit the transform to be the identity.
        gltf.nodes.node_transform.matrix = Matrix4.pack(
          Matrix4.IDENTITY,
          new Array(16)
        );

        return loadModelJson(gltf, {
          modelMatrix: Matrix4.IDENTITY,
          upAxis: Axis.Z,
        }).then(function (m) {
          verifyRender(m);
          expect(m.upAxis).toBe(Axis.Z);
          primitives.remove(m);
        });
      });
    });

    it("renders x-forward model", function () {
      return Resource.fetchJson(boxPbrUrl).then(function (gltf) {
        return loadModelJson(gltf, {
          forwardAxis: Axis.X,
        }).then(function (m) {
          verifyRender(m);
          expect(m.forwardAxis).toBe(Axis.X);
          primitives.remove(m);
        });
      });
    });

    it("renders z-forward model", function () {
      return Resource.fetchJson(boxEcefUrl).then(function (gltf) {
        return loadModelJson(gltf, {
          forwardAxis: Axis.Z,
        }).then(function (m) {
          verifyRender(m);
          expect(m.forwardAxis).toBe(Axis.Z);
          primitives.remove(m);
        });
      });
    });

    it("detects glTF 1.0 models as x-forward", function () {
      return Resource.fetchJson(boxEcefUrl).then(function (gltf) {
        return loadModelJson(gltf).then(function (m) {
          verifyRender(m);
          expect(m.forwardAxis).toBe(Axis.X);
          primitives.remove(m);
        });
      });
    });

    it("detects glTF 2.0 models as z-forward", function () {
      return Resource.fetchJson(boxPbrUrl).then(function (gltf) {
        return loadModelJson(gltf).then(function (m) {
          verifyRender(m);
          expect(m.forwardAxis).toBe(Axis.Z);
          primitives.remove(m);
        });
      });
    });

    it("resolves readyPromise", function () {
      return texturedBoxModel.readyPromise.then(function (model) {
        verifyRender(model);
      });
    });

    it("rejects readyPromise on error", function () {
      return Resource.fetchJson(boomBoxUrl).then(function (gltf) {
        gltf.images[0].uri = "invalid.png";
        var model = primitives.add(
          new Model({
            gltf: gltf,
          })
        );

        scene.renderForSpecs();

        return model.readyPromise
          .then(function (model) {
            fail("should not resolve");
          })
          .otherwise(function (error) {
            expect(model.ready).toEqual(false);
            primitives.remove(model);
          });
      });
    });

    it("renders from glTF", function () {
      // Simulate using procedural glTF as opposed to loading it from a file
      return loadModelJson(texturedBoxModel.gltf).then(function (model) {
        verifyRender(model);
        primitives.remove(model);
      });
    });

    it("applies the right render state", function () {
      spyOn(RenderState, "fromCache").and.callThrough();

      // Simulate using procedural glTF as opposed to loading it from a file
      return loadModelJson(texturedBoxModel.gltf).then(function (model) {
        var rs = {
          cull: {
            enabled: true,
          },
          depthTest: {
            enabled: true,
            func: DepthFunction.LESS_OR_EQUAL,
          },
          depthMask: true,
          blending: {
            enabled: false,
            equationRgb: WebGLConstants.FUNC_ADD,
            equationAlpha: WebGLConstants.FUNC_ADD,
            functionSourceRgb: WebGLConstants.ONE,
            functionSourceAlpha: WebGLConstants.ONE,
            functionDestinationRgb: WebGLConstants.ONE_MINUS_SRC_ALPHA,
            functionDestinationAlpha: WebGLConstants.ONE_MINUS_SRC_ALPHA,
          },
        };

        expect(RenderState.fromCache).toHaveBeenCalledWith(rs);
        primitives.remove(model);
      });
    });

    it("renders bounding volume", function () {
      texturedBoxModel.debugShowBoundingVolume = true;
      verifyRender(texturedBoxModel);
      texturedBoxModel.debugShowBoundingVolume = false;
    });

    it("renders in wireframe", function () {
      expect(scene).toRender([0, 0, 0, 255]);

      texturedBoxModel.show = true;
      texturedBoxModel.debugWireframe = true;
      texturedBoxModel.zoomTo();
      scene.renderForSpecs();

      var commands = texturedBoxModel._nodeCommands;
      var length = commands.length;
      for (var i = 0; i < length; ++i) {
        expect(commands[i].command.primitiveType).toEqual(PrimitiveType.LINES);
      }

      texturedBoxModel.show = false;
      texturedBoxModel.debugWireframe = false;
    });

    it("renders with distance display condition", function () {
      expect(scene).toRender([0, 0, 0, 255]);

      var center = Matrix4.getTranslation(
        texturedBoxModel.modelMatrix,
        new Cartesian3()
      );
      var near = 10.0;
      var far = 100.0;

      texturedBoxModel.show = true;
      texturedBoxModel.distanceDisplayCondition = new DistanceDisplayCondition(
        near,
        far
      );

      var frameState = scene.frameState;
      var commands = frameState.commandList;

      frameState.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, 0.0, far + 10.0)
      );
      frameState.camera.lookAtTransform(Matrix4.IDENTITY);
      frameState.commandList = [];
      texturedBoxModel.update(frameState);
      expect(frameState.commandList.length).toEqual(0);

      frameState.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, 0.0, (far + near) * 0.5)
      );
      frameState.camera.lookAtTransform(Matrix4.IDENTITY);
      frameState.commandList = [];
      texturedBoxModel.update(frameState);
      expect(frameState.commandList.length).toBeGreaterThan(0);

      frameState.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, 0.0, near - 1.0)
      );
      frameState.camera.lookAtTransform(Matrix4.IDENTITY);
      frameState.commandList = [];
      texturedBoxModel.update(frameState);
      expect(frameState.commandList.length).toEqual(0);

      scene.frameState.commandList = commands;
      texturedBoxModel.show = false;
      texturedBoxModel.distanceDisplayCondition = undefined;
    });

    it("renders with spherical harmonics", function () {
      if (!scene.highDynamicRangeSupported) {
        return;
      }

      return loadModel(boomBoxUrl).then(function (m) {
        m.scale = 20.0; // Source model is very small, so scale up a bit

        var L00 = new Cartesian3(
          0.692622075009195,
          0.4543516001819,
          0.36910172299235
        ); // L00, irradiance, pre-scaled base
        var L1_1 = new Cartesian3(
          0.289407068366422,
          0.16789310162658,
          0.106174907004792
        ); // L1-1, irradiance, pre-scaled base
        var L10 = new Cartesian3(
          -0.591502034778913,
          -0.28152432317119,
          0.124647554708491
        ); // L10, irradiance, pre-scaled base
        var L11 = new Cartesian3(
          0.34945458117126,
          0.163273486841657,
          -0.03095643545207
        ); // L11, irradiance, pre-scaled base
        var L2_2 = new Cartesian3(
          0.22171176447426,
          0.11771991868122,
          0.031381053430064
        ); // L2-2, irradiance, pre-scaled base
        var L2_1 = new Cartesian3(
          -0.348955284677868,
          -0.187256994042823,
          -0.026299717727617
        ); // L2-1, irradiance, pre-scaled base
        var L20 = new Cartesian3(
          0.119982671127227,
          0.076784552175028,
          0.055517838847755
        ); // L20, irradiance, pre-scaled base
        var L21 = new Cartesian3(
          -0.545546043202299,
          -0.279787444030397,
          -0.086854000285261
        ); // L21, irradiance, pre-scaled base
        var L22 = new Cartesian3(
          0.160417569726332,
          0.120896423762313,
          0.121102528320197
        ); // L22, irradiance, pre-scaled base
        m.sphericalHarmonicCoefficients = [
          L00,
          L1_1,
          L10,
          L11,
          L2_2,
          L2_1,
          L20,
          L21,
          L22,
        ];

        scene.highDynamicRange = true;
        verifyRender(m);
        primitives.remove(m);
        scene.highDynamicRange = false;
      });
    });

    it("renders with specular environment map", function () {
      if (!scene.highDynamicRangeSupported) {
        return;
      }

      return loadModel(boomBoxUrl).then(function (m) {
        m.scale = 20.0; // Source model is very small, so scale up a bit
        m.specularEnvironmentMaps =
          "./Data/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx2";

        return pollToPromise(function () {
          scene.highDynamicRange = true;
          scene.render();
          scene.highDynamicRange = false;
          return (
            defined(m._specularEnvironmentMapAtlas) &&
            m._specularEnvironmentMapAtlas.ready
          );
        }).then(function () {
          scene.highDynamicRange = true;
          verifyRender(m);
          primitives.remove(m);
          scene.highDynamicRange = false;
        });
      });
    });

    it("distanceDisplayCondition throws when ner >= far", function () {
      expect(function () {
        texturedBoxModel.distanceDisplayCondition = new DistanceDisplayCondition(
          100.0,
          10.0
        );
      }).toThrowDeveloperError();
    });

    it("getNode throws when model is not loaded", function () {
      var m = new Model();
      expect(function () {
        return m.getNode("gltf-node-name");
      }).toThrowDeveloperError();
    });

    it("getNode throws when name is not provided", function () {
      expect(function () {
        return texturedBoxModel.getNode();
      }).toThrowDeveloperError();
    });

    it("getNode returns undefined when node does not exist", function () {
      expect(
        texturedBoxModel.getNode("name-of-node-that-does-not-exist")
      ).not.toBeDefined();
    });

    it("getNode returns a node", function () {
      var node = texturedBoxModel.getNode("Mesh");
      expect(node).toBeDefined();
      expect(node.name).toEqual("Mesh");
      expect(node.id).toEqual(0);
      expect(node.show).toEqual(true);

      // Change node transform and render
      expect(texturedBoxModel._cesiumAnimationsDirty).toEqual(false);
      node.matrix = Matrix4.fromUniformScale(1.01, new Matrix4());
      expect(texturedBoxModel._cesiumAnimationsDirty).toEqual(true);

      verifyRender(texturedBoxModel);

      expect(texturedBoxModel._cesiumAnimationsDirty).toEqual(false);

      node.matrix = Matrix4.fromUniformScale(1.0, new Matrix4());
    });

    it("getMesh throws when model is not loaded", function () {
      var m = new Model();
      expect(function () {
        return m.getMesh("gltf-mesh-name");
      }).toThrowDeveloperError();
    });

    it("getMesh throws when name is not provided", function () {
      expect(function () {
        return texturedBoxModel.getMesh();
      }).toThrowDeveloperError();
    });

    it("getMesh returns undefined when mesh does not exist", function () {
      expect(
        texturedBoxModel.getMesh("name-of-mesh-that-does-not-exist")
      ).not.toBeDefined();
    });

    it("getMesh returns a mesh", function () {
      var mesh = texturedBoxModel.getMesh("Mesh");
      expect(mesh).toBeDefined();
      expect(mesh.name).toEqual("Mesh");
      expect(mesh.id).toEqual(0);
      expect(mesh.materials[0].name).toEqual("Texture");
    });

    it("getMaterial throws when model is not loaded", function () {
      var m = new Model();
      expect(function () {
        return m.getMaterial("gltf-material-name");
      }).toThrowDeveloperError();
    });

    it("getMaterial throws when name is not provided", function () {
      expect(function () {
        return texturedBoxModel.getMaterial();
      }).toThrowDeveloperError();
    });

    it("getMaterial returns undefined when mesh does not exist", function () {
      expect(
        texturedBoxModel.getMaterial("name-of-material-that-does-not-exist")
      ).not.toBeDefined();
    });

    it("getMaterial returns a material", function () {
      var material = texturedBoxModel.getMaterial("Texture");
      expect(material).toBeDefined();
      expect(material.name).toEqual("Texture");
      expect(material.id).toEqual(0);
    });

    it("ModelMaterial.setValue throws when name is not provided", function () {
      var material = texturedBoxModel.getMaterial("Texture");
      expect(function () {
        material.setValue();
      }).toThrowDeveloperError();
    });

    it("ModelMaterial.setValue sets a scalar uniform value", function () {
      var material = texturedBoxModel.getMaterial("Texture");
      material.setValue("shininess", 12.34);
      expect(material.getValue("shininess")).toEqual(12.34);
    });

    it("ModelMaterial.setValue sets a Cartesian4 uniform value", function () {
      var material = texturedBoxModel.getMaterial("Texture");
      var specular = new Cartesian4(0.25, 0.5, 0.75, 1.0);
      material.setValue("specular", specular);
      expect(material.getValue("specular")).toEqual(specular);
    });

    it("ModelMaterial.getValue throws when name is not provided", function () {
      var material = texturedBoxModel.getMaterial("Texture");
      expect(function () {
        material.getValue();
      }).toThrowDeveloperError();
    });

    it("ModelMaterial.getValue returns undefined when uniform value does not exist", function () {
      var material = texturedBoxModel.getMaterial("Texture");
      expect(
        material.getValue("name-of-parameter-that-does-not-exist")
      ).not.toBeDefined();
    });

    it("boundingSphere throws when model is not loaded", function () {
      var m = new Model();
      expect(function () {
        return m.boundingSphere;
      }).toThrowDeveloperError();
    });

    it("boundingSphere returns the bounding sphere", function () {
      var boundingSphere = texturedBoxModel.boundingSphere;
      expect(boundingSphere.center).toEqualEpsilon(
        new Cartesian3(0.0, 0.0, 0.0),
        CesiumMath.EPSILON3
      );
      expect(boundingSphere.radius).toEqualEpsilon(0.866, CesiumMath.EPSILON3);
    });

    it("boundingSphere returns the bounding sphere when scale property is set", function () {
      var originalScale = texturedBoxModel.scale;
      texturedBoxModel.scale = 10;

      var boundingSphere = texturedBoxModel.boundingSphere;
      expect(boundingSphere.center).toEqualEpsilon(
        new Cartesian3(0.0, 0.0, 0.0),
        CesiumMath.EPSILON3
      );
      expect(boundingSphere.radius).toEqualEpsilon(8.66, CesiumMath.EPSILON3);

      texturedBoxModel.scale = originalScale;
    });

    it("boundingSphere returns the bounding sphere when maximumScale is reached", function () {
      var originalScale = texturedBoxModel.scale;
      var originalMaximumScale = texturedBoxModel.maximumScale;
      texturedBoxModel.scale = 20;
      texturedBoxModel.maximumScale = 10;

      var boundingSphere = texturedBoxModel.boundingSphere;
      expect(boundingSphere.center).toEqualEpsilon(
        new Cartesian3(0.0, 0.0, 0.0),
        CesiumMath.EPSILON3
      );
      expect(boundingSphere.radius).toEqualEpsilon(8.66, CesiumMath.EPSILON3);

      texturedBoxModel.scale = originalScale;
      texturedBoxModel.maximumScale = originalMaximumScale;
    });

    it("boundingSphere returns the bounding sphere when modelMatrix has non-uniform scale", function () {
      var originalMatrix = Matrix4.clone(texturedBoxModel.modelMatrix);
      Matrix4.multiplyByScale(
        texturedBoxModel.modelMatrix,
        new Cartesian3(2, 5, 10),
        texturedBoxModel.modelMatrix
      );

      var boundingSphere = texturedBoxModel.boundingSphere;
      expect(boundingSphere.center).toEqualEpsilon(
        new Cartesian3(0.0, 0.0, 0.0),
        CesiumMath.EPSILON3
      );
      expect(boundingSphere.radius).toEqualEpsilon(8.66, CesiumMath.EPSILON3);

      texturedBoxModel.modelMatrix = originalMatrix;
    });

    it("destroys", function () {
      return loadModel(boxUrl).then(function (m) {
        expect(m.isDestroyed()).toEqual(false);
        primitives.remove(m);
        expect(m.isDestroyed()).toEqual(true);
      });
    });

    it("destroys attached ClippingPlaneCollections", function () {
      return loadModel(boxUrl).then(function (model) {
        var clippingPlanes = new ClippingPlaneCollection({
          planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
        });
        model.clippingPlanes = clippingPlanes;
        expect(model.isDestroyed()).toEqual(false);
        expect(clippingPlanes.isDestroyed()).toEqual(false);
        primitives.remove(model);
        expect(model.isDestroyed()).toEqual(true);
        expect(clippingPlanes.isDestroyed()).toEqual(true);
      });
    });

    it("throws a DeveloperError when given a ClippingPlaneCollection attached to another Model", function () {
      var clippingPlanes;
      return loadModel(boxUrl)
        .then(function (model1) {
          clippingPlanes = new ClippingPlaneCollection({
            planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
          });
          model1.clippingPlanes = clippingPlanes;

          return loadModel(boxUrl);
        })
        .then(function (model2) {
          expect(function () {
            model2.clippingPlanes = clippingPlanes;
          }).toThrowDeveloperError();
        });
    });

    it("destroys ClippingPlaneCollections that are detached", function () {
      var clippingPlanes;
      return loadModel(boxUrl).then(function (model1) {
        clippingPlanes = new ClippingPlaneCollection({
          planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
        });
        model1.clippingPlanes = clippingPlanes;
        expect(clippingPlanes.isDestroyed()).toBe(false);

        model1.clippingPlanes = undefined;
        expect(clippingPlanes.isDestroyed()).toBe(true);

        primitives.remove(model1);
      });
    });

    it("rebuilds shaders when clipping planes change state", function () {
      spyOn(Model, "_getClippingFunction").and.callThrough();

      return loadModel(boxUrl).then(function (model) {
        model.show = true;

        var clippingPlanes = new ClippingPlaneCollection({
          planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
          unionClippingRegions: false,
        });
        model.clippingPlanes = clippingPlanes;

        scene.renderForSpecs();
        expect(Model._getClippingFunction.calls.count()).toEqual(1);

        clippingPlanes.unionClippingRegions = true;

        scene.renderForSpecs();
        expect(Model._getClippingFunction.calls.count()).toEqual(2);

        primitives.remove(model);
      });
    });

    it("rebuilds shaders when color requires coloring shader code", function () {
      spyOn(Model, "_modifyShaderForColor").and.callThrough();

      return loadModel(boxUrl).then(function (model) {
        model.show = true;

        model.color = Color.LIME;

        scene.renderForSpecs();
        expect(Model._modifyShaderForColor.calls.count()).toEqual(1);

        primitives.remove(model);
      });
    });

    ///////////////////////////////////////////////////////////////////////////

    it("Throws because of invalid extension", function () {
      return Resource.fetchJson(boxUrl).then(function (gltf) {
        gltf.extensionsRequired = ["NOT_supported_extension"];
        var model = primitives.add(
          new Model({
            gltf: gltf,
          })
        );

        expect(function () {
          scene.renderForSpecs();
        }).toThrowRuntimeError();
        primitives.remove(model);
      });
    });

    it("Throws because of invalid extension", function () {
      return Resource.fetchJson(boxUrl).then(function (gltf) {
        gltf.extensionsRequired = ["CESIUM_binary_glTF"];
        var model = primitives.add(
          new Model({
            gltf: gltf,
          })
        );

        expect(function () {
          scene.renderForSpecs();
        }).toThrowRuntimeError();
        primitives.remove(model);
      });
    });

    it("Throws for EXT_texture_webp if browser does not support WebP", function () {
      var supportsWebP = FeatureDetection.supportsWebP._result;
      FeatureDetection.supportsWebP._result = false;
      return Resource.fetchJson(texturedBoxWebpUrl).then(function (gltf) {
        gltf.extensionsRequired = ["EXT_texture_webp"];
        var model = primitives.add(
          new Model({
            gltf: gltf,
          })
        );

        expect(function () {
          scene.renderForSpecs();
        }).toThrowRuntimeError();
        primitives.remove(model);
        FeatureDetection.supportsWebP._result = supportsWebP;
      });
    });

    it("loads a glTF v0.8 model", function () {
      return loadModel(cesiumAir_0_8Url, {
        minimumPixelSize: 1,
      }).then(function (m) {
        // Verify that the version has been updated
        expect(m.gltf.asset.version).toEqual("2.0");

        // Verify that rotation is converted from
        // Axis-Angle (1,0,0,0) to Quaternion (0,0,0,1)
        var rotation = m.gltf.nodes[2].rotation;
        expect(rotation).toEqual([0.0, 0.0, 0.0, 1.0]);

        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 model", function () {
      return loadModel(boxGltf2Url).then(function (m) {
        verifyRender(m);
        m.show = true;
        m.luminanceAtZenith = undefined;

        expect({
          scene: scene,
          time: JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC")),
        }).toRenderAndCall(function (rgba) {
          expect(rgba).toEqualEpsilon([179, 9, 9, 255], 5); // Red
        });

        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 model with showOutline set as false", function () {
      spyOn(ModelOutlineLoader, "outlinePrimitives");
      return loadModel(boxGltf2Url, {
        showOutline: false,
      }).then(function (m) {
        expect(ModelOutlineLoader.outlinePrimitives).not.toHaveBeenCalled();
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 model with techniques_webgl extension", function () {
      return loadModel(boxGltf2WithTechniquesUrl).then(function (m) {
        verifyRender(m);
        m.show = true;
        expect(scene).toRender([204, 0, 0, 255]); // Red
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 model with AGI_articulations extension", function () {
      return loadModel(boxArticulationsUrl).then(function (m) {
        verifyRender(m);

        m.setArticulationStage("SampleArticulation MoveX", 1.0);
        m.setArticulationStage("SampleArticulation MoveY", 2.0);
        m.setArticulationStage("SampleArticulation MoveZ", 3.0);
        m.setArticulationStage("SampleArticulation Yaw", 4.0);
        m.setArticulationStage("SampleArticulation Pitch", 5.0);
        m.setArticulationStage("SampleArticulation Roll", 6.0);
        m.setArticulationStage("SampleArticulation Size", 0.9);
        m.setArticulationStage("SampleArticulation SizeX", 0.8);
        m.setArticulationStage("SampleArticulation SizeY", 0.7);
        m.setArticulationStage("SampleArticulation SizeZ", 0.6);
        m.applyArticulations();

        var node = m.getNode("Root");
        expect(node.useMatrix).toBe(true);

        var expected = [
          0.7147690483240505,
          -0.04340611926232735,
          -0.0749741046529782,
          0,
          -0.06188330295778636,
          0.05906797312763484,
          -0.6241645867602773,
          0,
          0.03752515582279579,
          0.5366347296529127,
          0.04706410108373541,
          0,
          1,
          3,
          -2,
          1,
        ];

        expect(node.matrix).toEqualEpsilon(expected, CesiumMath.EPSILON14);

        primitives.remove(m);
      });
    });

    it("loads a glTF model with unused material", function () {
      return loadModel(boxWithUnusedMaterial).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF model that doesn't have a technique", function () {
      return loadModel(boxNoTechniqueUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF model that doesn't have indices", function () {
      return loadModel(boxNoIndicesUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("renders texturedBoxCustom (all uniform semantics)", function () {
      return loadModel(texturedBoxCustomUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("renders a model with the KHR_binary_glTF extension", function () {
      return loadModel(texturedBoxKhrBinaryUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a model with the KHR_binary_glTF extension as an ArrayBuffer using new Model", function () {
      return Resource.fetchArrayBuffer(texturedBoxKhrBinaryUrl).then(function (
        arrayBuffer
      ) {
        return loadModelJson(arrayBuffer).then(function (model) {
          verifyRender(model);
          primitives.remove(model);
        });
      });
    });

    it("loads a model with the KHR_binary_glTF extension as an Uint8Array using new Model", function () {
      return Resource.fetchArrayBuffer(texturedBoxKhrBinaryUrl).then(function (
        arrayBuffer
      ) {
        return loadModelJson(new Uint8Array(arrayBuffer)).then(function (
          model
        ) {
          verifyRender(model);
          primitives.remove(model);
        });
      });
    });

    it("Throws because of an invalid Binary glTF header - magic", function () {
      var arrayBuffer = new ArrayBuffer(20);
      expect(function () {
        return new Model({
          gltf: arrayBuffer,
        });
      }).toThrowRuntimeError();
    });

    it("Throws because of an invalid Binary glTF header - version", function () {
      var arrayBuffer = new ArrayBuffer(20);
      var bytes = new Uint8Array(arrayBuffer);
      bytes[0] = "g".charCodeAt(0);
      bytes[1] = "l".charCodeAt(0);
      bytes[2] = "T".charCodeAt(0);
      bytes[3] = "F".charCodeAt(0);

      expect(function () {
        return new Model({
          gltf: arrayBuffer,
        });
      }).toThrowRuntimeError();
    });

    it("renders a model with the CESIUM_RTC extension", function () {
      return loadModel(boxRtcUrl, {
        modelMatrix: Matrix4.IDENTITY,
        minimumPixelSize: 1,
      }).then(function (m) {
        var bs = m.boundingSphere;
        expect(
          bs.center.equalsEpsilon(
            new Cartesian3(6378137.0, 0.0, 0.0),
            CesiumMath.EPSILON14
          )
        );
        var radius = Math.sqrt(0.5 * 0.5 * 3);
        expect(bs.radius).toEqualEpsilon(radius, CesiumMath.EPSILON14);

        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("renders textured box with external resources: .glsl, .bin, and .png files", function () {
      return loadModel(texturedBoxSeparateUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("renders textured box with embedded KTX2 texture", function () {
      if (!scene.context.supportsBasis) {
        return;
      }
      return loadModel(texturedBoxKTX2Url, {
        incrementallyLoadTextures: false,
      }).then(function (m) {
        verifyRender(m);
        expect(Object.keys(m._rendererResources.textures).length).toBe(1);
        primitives.remove(m);
      });
    });

    it("renders textured box with embedded KTX2 texture with mipmap", function () {
      if (!scene.context.supportsBasis) {
        return;
      }
      var gl = scene.context._gl;
      spyOn(gl, "compressedTexImage2D").and.callThrough();
      return loadModel(texturedBoxKTX2MipmapUrl, {
        incrementallyLoadTextures: false,
      }).then(function (m) {
        verifyRender(m);
        expect(gl.compressedTexImage2D.calls.count()).toEqual(9);
        expect(Object.keys(m._rendererResources.textures).length).toBe(1);
        primitives.remove(m);
      });
    });

    it("renders textured box with KHR_texture_transform extension", function () {
      return loadModel(texturedBoxTextureTransformUrl, {
        incrementallyLoadTextures: false,
      }).then(function (m) {
        verifyRender(m);
        expect(Object.keys(m._rendererResources.textures).length).toBe(1);
        primitives.remove(m);
      });
    });

    it("renders textured box with WebP texture", function () {
      if (!FeatureDetection.supportsWebP()) {
        return;
      }
      return loadModel(texturedBoxWebpUrl, {
        incrementallyLoadTextures: false,
      }).then(function (m) {
        verifyRender(m);
        expect(Object.keys(m._rendererResources.textures).length).toBe(1);
        primitives.remove(m);
      });
    });

    ///////////////////////////////////////////////////////////////////////////

    it("loads cesiumAir", function () {
      expect(cesiumAirModel.minimumPixelSize).toEqual(1);
      expect(cesiumAirModel.maximumScale).toEqual(200);
      expect(cesiumAirModel.asynchronous).toEqual(false);
    });

    it("renders cesiumAir (has translucency)", function () {
      verifyRender(cesiumAirModel);
    });

    it("renders cesiumAir with per-node show (root)", function () {
      expect(scene).toRender([0, 0, 0, 255]);

      var commands = cesiumAirModel._nodeCommands;
      var i;
      var length;

      cesiumAirModel.show = true;
      cesiumAirModel.zoomTo();

      cesiumAirModel.getNode("Cesium_Air").show = false;
      expect(scene).toRender([0, 0, 0, 255]);

      length = commands.length;
      for (i = 0; i < length; ++i) {
        expect(commands[i].show).toEqual(false);
      }

      cesiumAirModel.getNode("Cesium_Air").show = true;
      expect(scene).notToRender([0, 0, 0, 255]);

      length = commands.length;
      for (i = 0; i < length; ++i) {
        expect(commands[i].show).toEqual(true);
      }

      cesiumAirModel.show = false;
    });

    it("renders cesiumAir with per-node show (non-root)", function () {
      cesiumAirModel.show = true;
      cesiumAirModel.zoomTo();

      var commands = cesiumAirModel._nodeCommands;
      var i;
      var length;

      var commandsPropFalse = 0;
      var commandsPropTrue = 0;

      cesiumAirModel.getNode("Prop").show = false;
      scene.renderForSpecs();

      length = commands.length;
      for (i = 0; i < length; ++i) {
        commandsPropFalse += commands[i].show ? 1 : 0;
      }

      cesiumAirModel.getNode("Prop").show = true;
      scene.renderForSpecs();

      length = commands.length;
      for (i = 0; i < length; ++i) {
        commandsPropTrue += commands[i].show ? 1 : 0;
      }

      cesiumAirModel.show = false;

      // Prop node has one mesh with two primitives
      expect(commandsPropFalse).toEqual(commandsPropTrue - 2);
    });

    it("picks cesiumAir", function () {
      if (FeatureDetection.isInternetExplorer()) {
        // Workaround IE 11.0.9.  This test fails when all tests are ran without a breakpoint here.
        return;
      }

      cesiumAirModel.show = true;
      cesiumAirModel.zoomTo();

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(cesiumAirModel);
        expect(result.id).toEqual(cesiumAirUrl);
        expect(result.node).toBeDefined();
        expect(result.mesh).toBeDefined();
      });

      cesiumAirModel.show = false;
    });

    it("cesiumAir is picked with a new pick id", function () {
      if (FeatureDetection.isInternetExplorer()) {
        // Workaround IE 11.0.9.  This test fails when all tests are ran without a breakpoint here.
        return;
      }

      var oldId = cesiumAirModel.id;
      cesiumAirModel.id = "id";
      cesiumAirModel.show = true;
      cesiumAirModel.zoomTo();

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(cesiumAirModel);
        expect(result.id).toEqual("id");
      });

      cesiumAirModel.id = oldId;
      cesiumAirModel.show = false;
    });

    it("cesiumAir is not picked (show === false)", function () {
      cesiumAirModel.zoomTo();

      expect(scene).notToPick();
    });

    ///////////////////////////////////////////////////////////////////////////

    it("renders animBoxes without animation", function () {
      verifyRender(animBoxesModel);
    });

    it("adds and removes all animations", function () {
      var animations = animBoxesModel.activeAnimations;
      expect(animations.length).toEqual(0);

      var spyAdd = jasmine.createSpy("listener");
      animations.animationAdded.addEventListener(spyAdd);
      var a = animations.addAll();
      expect(animations.length).toEqual(2);
      expect(spyAdd.calls.count()).toEqual(2);
      expect(spyAdd.calls.argsFor(0)[0]).toBe(animBoxesModel);
      expect(spyAdd.calls.argsFor(0)[1]).toBe(a[0]);
      expect(spyAdd.calls.argsFor(1)[0]).toBe(animBoxesModel);
      expect(spyAdd.calls.argsFor(1)[1]).toBe(a[1]);
      animations.animationAdded.removeEventListener(spyAdd);

      expect(animations.contains(a[0])).toEqual(true);
      expect(animations.get(0)).toEqual(a[0]);
      expect(animations.contains(a[1])).toEqual(true);
      expect(animations.get(1)).toEqual(a[1]);

      var spyRemove = jasmine.createSpy("listener");
      animations.animationRemoved.addEventListener(spyRemove);
      animations.removeAll();
      expect(animations.length).toEqual(0);
      expect(spyRemove.calls.count()).toEqual(2);
      expect(spyRemove.calls.argsFor(0)[0]).toBe(animBoxesModel);
      expect(spyRemove.calls.argsFor(0)[1]).toBe(a[0]);
      expect(spyRemove.calls.argsFor(1)[0]).toBe(animBoxesModel);
      expect(spyRemove.calls.argsFor(1)[1]).toBe(a[1]);
      animations.animationRemoved.removeEventListener(spyRemove);
    });

    it("addAll throws when model is not loaded", function () {
      var m = new Model();
      expect(function () {
        return m.activeAnimations.addAll();
      }).toThrowDeveloperError();
    });

    it("addAll throws when multiplier is less than or equal to zero.", function () {
      expect(function () {
        return animBoxesModel.activeAnimations.addAll({
          multiplier: 0.0,
        });
      }).toThrowDeveloperError();
    });

    it("adds and removes an animation", function () {
      var animations = animBoxesModel.activeAnimations;
      expect(animations.length).toEqual(0);

      var spyAdd = jasmine.createSpy("listener");
      animations.animationAdded.addEventListener(spyAdd);
      var a = animations.add({
        name: "animation_1",
      });
      expect(a).toBeDefined();
      expect(a.name).toEqual("animation_1");
      expect(a.startTime).not.toBeDefined();
      expect(a.delay).toEqual(0.0);
      expect(a.stopTime).not.toBeDefined();
      expect(a.removeOnStop).toEqual(false);
      expect(a.multiplier).toEqual(1.0);
      expect(a.reverse).toEqual(false);
      expect(a.loop).toEqual(ModelAnimationLoop.NONE);
      expect(a.start).toBeDefined();
      expect(a.update).toBeDefined();
      expect(a.stop).toBeDefined();
      expect(spyAdd).toHaveBeenCalledWith(animBoxesModel, a);
      animations.animationAdded.removeEventListener(spyAdd);

      expect(animations.contains(a)).toEqual(true);
      expect(animations.get(0)).toEqual(a);

      var spyRemove = jasmine.createSpy("listener");
      animations.animationRemoved.addEventListener(spyRemove);
      expect(animations.remove(a)).toEqual(true);
      expect(animations.remove(a)).toEqual(false);
      expect(animations.remove()).toEqual(false);
      expect(animations.contains(a)).toEqual(false);
      expect(animations.length).toEqual(0);
      expect(spyRemove).toHaveBeenCalledWith(animBoxesModel, a);
      animations.animationRemoved.removeEventListener(spyRemove);
    });

    it("adds an animation by index", function () {
      var animations = animBoxesModel.activeAnimations;
      expect(animations.length).toEqual(0);

      var spyAdd = jasmine.createSpy("listener");
      animations.animationAdded.addEventListener(spyAdd);
      var a = animations.add({
        index: 1,
      });
      expect(a).toBeDefined();
      expect(a.name).toEqual("animation_1");
      animations.remove(a);
    });

    it("add throws when name and index are not defined", function () {
      var m = new Model();
      expect(function () {
        return m.activeAnimations.add();
      }).toThrowDeveloperError();
    });

    it("add throws when index is invalid", function () {
      var m = new Model();
      expect(function () {
        return m.activeAnimations.add({
          index: -1,
        });
      }).toThrowDeveloperError();
      expect(function () {
        return m.activeAnimations.add({
          index: 2,
        });
      }).toThrowDeveloperError();
    });

    it("add throws when model is not loaded", function () {
      var m = new Model();
      expect(function () {
        return m.activeAnimations.add({
          name: "animation_1",
        });
      }).toThrowDeveloperError();
    });

    it("add throws when name is invalid.", function () {
      expect(function () {
        return animBoxesModel.activeAnimations.add({
          name: "animation-does-not-exist",
        });
      }).toThrowDeveloperError();
    });

    it("add throws when multiplier is less than or equal to zero.", function () {
      expect(function () {
        return animBoxesModel.activeAnimations.add({
          name: "animation_1",
          multiplier: 0.0,
        });
      }).toThrowDeveloperError();
    });

    it("get throws without an index", function () {
      var m = new Model();
      expect(function () {
        return m.activeAnimations.get();
      }).toThrowDeveloperError();
    });

    it("contains(undefined) returns false", function () {
      expect(animBoxesModel.activeAnimations.contains(undefined)).toEqual(
        false
      );
    });

    it("raises animation start, update, and stop events when removeOnStop is true", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        removeOnStop: true,
      });

      var spyStart = jasmine.createSpy("listener");
      a.start.addEventListener(spyStart);

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      var stopped = false;
      a.stop.addEventListener(function (model, animation) {
        stopped = true;
      });
      var spyStop = jasmine.createSpy("listener");
      a.stop.addEventListener(spyStop);

      animBoxesModel.show = true;

      return pollToPromise(
        function () {
          scene.renderForSpecs(time);
          time = JulianDate.addSeconds(time, 1.0, time, new JulianDate());
          return stopped;
        },
        { timeout: 10000 }
      ).then(function () {
        expect(spyStart).toHaveBeenCalledWith(animBoxesModel, a);

        expect(spyUpdate.calls.count()).toEqual(5);
        expect(spyUpdate.calls.argsFor(0)[0]).toBe(animBoxesModel);
        expect(spyUpdate.calls.argsFor(0)[1]).toBe(a);
        expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
          0.0,
          CesiumMath.EPSILON14
        );
        expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
          1.0,
          CesiumMath.EPSILON14
        );
        expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
          2.0,
          CesiumMath.EPSILON14
        );
        expect(spyUpdate.calls.argsFor(3)[2]).toEqualEpsilon(
          3.0,
          CesiumMath.EPSILON14
        );
        expect(spyUpdate.calls.argsFor(4)[2]).toEqualEpsilon(
          3.708, // Expect animation to have reached its final value.
          CesiumMath.EPSILON3
        );

        expect(spyStop).toHaveBeenCalledWith(animBoxesModel, a);
        expect(animations.length).toEqual(0);
        animBoxesModel.show = false;
      });
    });

    it("animates with a delay", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));

      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        delay: 1.0,
      });

      var spyStart = jasmine.createSpy("listener");
      a.start.addEventListener(spyStart);

      animBoxesModel.show = true;
      scene.renderForSpecs(time); // Does not fire start
      scene.renderForSpecs(JulianDate.addSeconds(time, 1.0, new JulianDate()));

      expect(spyStart.calls.count()).toEqual(1);

      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("animates with an explicit stopTime", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var stopTime = JulianDate.fromDate(
        new Date("January 1, 2014 12:00:01 UTC")
      );

      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        stopTime: stopTime,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      scene.renderForSpecs(time);
      scene.renderForSpecs(JulianDate.addSeconds(time, 1.0, new JulianDate()));
      scene.renderForSpecs(JulianDate.addSeconds(time, 2.0, new JulianDate()));

      expect(spyUpdate.calls.count()).toEqual(3);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        1.0,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        1.0,
        CesiumMath.EPSILON14
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("animates with a multiplier", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        multiplier: 1.5,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      scene.renderForSpecs(time);
      scene.renderForSpecs(JulianDate.addSeconds(time, 1.0, new JulianDate()));
      scene.renderForSpecs(JulianDate.addSeconds(time, 2.0, new JulianDate()));

      expect(spyUpdate.calls.count()).toEqual(3);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        1.5,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        3.0,
        CesiumMath.EPSILON14
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("finishes an animation after the stop time", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var stopTime = JulianDate.fromDate(
        new Date("January 1, 2014 12:00:01 UTC")
      );

      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        stopTime: stopTime,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      scene.renderForSpecs(time);
      scene.renderForSpecs(JulianDate.addSeconds(time, 0.5, new JulianDate())); // Midpoint of designated interval
      scene.renderForSpecs(JulianDate.addSeconds(time, 2.0, new JulianDate())); // Past designated stop time

      expect(spyUpdate.calls.count()).toEqual(3);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        0.5,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        1.0, // Expect clamping to designated stop time.
        CesiumMath.EPSILON14
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("finishes an animation after it runs off the end", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      scene.renderForSpecs(time);
      scene.renderForSpecs(JulianDate.addSeconds(time, 0.5, new JulianDate())); // Somewhere inside animation
      scene.renderForSpecs(JulianDate.addSeconds(time, 10.0, new JulianDate())); // Way past end of animation

      expect(spyUpdate.calls.count()).toEqual(3);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        0.5,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        3.708, // Expect animation to have reached its final value.
        CesiumMath.EPSILON3
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("halts an animation before the start time", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var stopTime = JulianDate.fromDate(
        new Date("January 1, 2014 12:00:01 UTC")
      );

      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        stopTime: stopTime,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      scene.renderForSpecs(time);
      scene.renderForSpecs(JulianDate.addSeconds(time, 0.5, new JulianDate())); // Midpoint of animation
      scene.renderForSpecs(JulianDate.addSeconds(time, -1.0, new JulianDate())); // Before start of animation

      expect(spyUpdate.calls.count()).toEqual(3);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        0.5,
        CesiumMath.EPSILON14
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON14
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("animates in reverse", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        reverse: true,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      scene.renderForSpecs(time);
      scene.renderForSpecs(JulianDate.addSeconds(time, 1.0, new JulianDate()));
      scene.renderForSpecs(JulianDate.addSeconds(time, 2.0, new JulianDate()));
      scene.renderForSpecs(JulianDate.addSeconds(time, 3.0, new JulianDate()));

      expect(spyUpdate.calls.count()).toEqual(4);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        3.708,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        2.708,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        1.708,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(3)[2]).toEqualEpsilon(
        0.708,
        CesiumMath.EPSILON3
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("animates with REPEAT", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        loop: ModelAnimationLoop.REPEAT,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      for (var i = 0; i < 8; ++i) {
        scene.renderForSpecs(JulianDate.addSeconds(time, i, new JulianDate()));
      }

      expect(spyUpdate.calls.count()).toEqual(8);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        1.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        2.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(3)[2]).toEqualEpsilon(
        3.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(4)[2]).toEqualEpsilon(
        0.291,
        CesiumMath.EPSILON3
      ); // Repeat with duration of ~3.7
      expect(spyUpdate.calls.argsFor(5)[2]).toEqualEpsilon(
        1.291,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(6)[2]).toEqualEpsilon(
        2.291,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(7)[2]).toEqualEpsilon(
        3.291,
        CesiumMath.EPSILON3
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("animates with MIRRORED_REPEAT", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var animations = animBoxesModel.activeAnimations;
      var a = animations.add({
        name: "animation_1",
        startTime: time,
        loop: ModelAnimationLoop.MIRRORED_REPEAT,
      });

      var spyUpdate = jasmine.createSpy("listener");
      a.update.addEventListener(spyUpdate);

      animBoxesModel.show = true;
      for (var i = 0; i < 8; ++i) {
        scene.renderForSpecs(JulianDate.addSeconds(time, i, new JulianDate()));
      }

      expect(spyUpdate.calls.count()).toEqual(8);
      expect(spyUpdate.calls.argsFor(0)[2]).toEqualEpsilon(
        0.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(1)[2]).toEqualEpsilon(
        1.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(2)[2]).toEqualEpsilon(
        2.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(3)[2]).toEqualEpsilon(
        3.0,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(4)[2]).toEqualEpsilon(
        3.416,
        CesiumMath.EPSILON3
      ); // Mirror repeat with duration of 3.6
      expect(spyUpdate.calls.argsFor(5)[2]).toEqualEpsilon(
        2.416,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(6)[2]).toEqualEpsilon(
        1.416,
        CesiumMath.EPSILON3
      );
      expect(spyUpdate.calls.argsFor(7)[2]).toEqualEpsilon(
        0.416,
        CesiumMath.EPSILON3
      );
      expect(animations.remove(a)).toEqual(true);
      animBoxesModel.show = false;
    });

    it("animates and renders", function () {
      return loadModel(animBoxesUrl, {
        scale: 2.0,
      }).then(function (m) {
        var node = m.getNode("inner_box");
        var time = JulianDate.fromDate(
          new Date("January 1, 2014 12:00:00 UTC")
        );
        var animations = m.activeAnimations;
        var a = animations.add({
          name: "animation_1",
          startTime: time,
        });

        expect(node.matrix).toEqual(Matrix4.IDENTITY);
        var previousMatrix = Matrix4.clone(node.matrix);

        m.zoomTo();

        for (var i = 1; i < 4; ++i) {
          var t = JulianDate.addSeconds(time, i, new JulianDate());
          expect({
            scene: scene,
            time: t,
          }).toRender([0, 0, 0, 255]);

          m.show = true;
          expect({
            scene: scene,
            time: t,
          }).notToRender([0, 0, 0, 255]);
          m.show = false;

          expect(node.matrix).not.toEqual(previousMatrix);
          previousMatrix = Matrix4.clone(node.matrix);
        }

        expect(animations.remove(a)).toEqual(true);
        primitives.remove(m);
      });
    });

    it("does not animate when there are no animations", function () {
      var animations = animBoxesModel.activeAnimations;
      expect(animations.length).toEqual(0);
      expect(animations.update()).toEqual(false);
    });

    it("animates a single keyframe", function () {
      return Resource.fetchJson(animBoxesUrl).then(function (gltf) {
        gltf.accessors["animAccessor_0"].count = 1;
        gltf.accessors["animAccessor_1"].count = 1;

        return loadModelJson(gltf).then(function (m) {
          m.show = true;
          var node = m.getNode("inner_box");
          var time = JulianDate.fromDate(
            new Date("January 1, 2014 12:00:00 UTC")
          );
          var animations = m.activeAnimations;
          animations.add({
            name: "animation_0",
            startTime: time,
          });

          expect(node.matrix).toEqual(Matrix4.IDENTITY);
          var previousMatrix = Matrix4.clone(node.matrix);

          for (var i = 1; i < 4; ++i) {
            var t = JulianDate.addSeconds(time, i, new JulianDate());
            scene.renderForSpecs(t);
            expect(node.matrix).toEqual(previousMatrix);
          }
          primitives.remove(m);
        });
      });
    });

    it("animates with STEP interpolation", function () {
      return loadModel(interpolationTestUrl).then(function (model) {
        var time = JulianDate.fromDate(
          new Date("January 1, 2014 12:00:00 UTC")
        );

        model.show = true;
        var animations = model.activeAnimations;
        var a = animations.add({
          name: "Step Translation",
          startTime: time,
        });

        var animatedNode = model.getNode("Cube.006");

        scene.renderForSpecs(time);
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          6.665,
          CesiumMath.EPSILON3
        );

        scene.renderForSpecs(
          JulianDate.addSeconds(time, 0.5, new JulianDate())
        );
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          10.0,
          CesiumMath.EPSILON14
        );

        scene.renderForSpecs(
          JulianDate.addSeconds(time, 1.0, new JulianDate())
        );
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          6.0,
          CesiumMath.EPSILON14
        );

        scene.renderForSpecs(
          JulianDate.addSeconds(time, 2.0, new JulianDate())
        );
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          6.0,
          CesiumMath.EPSILON14
        );

        expect(animations.remove(a)).toEqual(true);
        primitives.remove(model);
      });
    });

    it("animates with LINEAR interpolation", function () {
      return loadModel(interpolationTestUrl).then(function (model) {
        var time = JulianDate.fromDate(
          new Date("January 1, 2014 12:00:00 UTC")
        );

        model.show = true;
        var animations = model.activeAnimations;
        var a = animations.add({
          name: "Linear Translation",
          startTime: time,
        });

        var animatedNode = model.getNode("Cube.009");

        scene.renderForSpecs(time);
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          6.621,
          CesiumMath.EPSILON3
        );

        scene.renderForSpecs(
          JulianDate.addSeconds(time, 0.5, new JulianDate())
        );
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          9.2,
          CesiumMath.EPSILON3
        );

        scene.renderForSpecs(
          JulianDate.addSeconds(time, 1.0, new JulianDate())
        );
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          7.6,
          CesiumMath.EPSILON3
        );

        scene.renderForSpecs(
          JulianDate.addSeconds(time, 2.0, new JulianDate())
        );
        expect(animatedNode.matrix[13]).toEqualEpsilon(
          6.0,
          CesiumMath.EPSILON14
        );

        expect(animations.remove(a)).toEqual(true);
        primitives.remove(model);
      });
    });

    ///////////////////////////////////////////////////////////////////////////

    it("renders riggedFigure without animation", function () {
      verifyRender(riggedFigureModel);
    });

    it("renders riggedFigure with animation (skinning)", function () {
      var time = JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC"));
      var animations = riggedFigureModel.activeAnimations;
      animations.addAll({
        startTime: time,
      });

      riggedFigureModel.zoomTo();

      for (var i = 0; i < 6; ++i) {
        var t = JulianDate.addSeconds(time, 0.25 * i, new JulianDate());
        expect({
          scene: scene,
          time: t,
        }).toRender([0, 0, 0, 255]);

        riggedFigureModel.show = true;
        expect({
          scene: scene,
          time: t,
        }).notToRender([0, 0, 0, 255]);
        riggedFigureModel.show = false;
      }

      animations.removeAll();
      riggedFigureModel.show = false;
    });

    it("renders riggedSimple", function () {
      return loadModel(riggedSimpleUrl).then(function (m) {
        expect(m).toBeDefined();
        verifyRender(m);
      });
    });

    it("should load a model where WebGL shader optimizer removes an attribute (linux)", function () {
      var url = "./Data/Models/test-shader-optimize/test-shader-optimize.gltf";
      return loadModel(url).then(function (m) {
        expect(m).toBeDefined();
        primitives.remove(m);
      });
    });

    it("releaseGltfJson releases glTF JSON when constructed with fromGltf", function () {
      return loadModel(boxUrl, {
        releaseGltfJson: true,
      }).then(function (m) {
        expect(m.releaseGltfJson).toEqual(true);
        expect(m.gltf).not.toBeDefined();

        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("releaseGltfJson releases glTF JSON when constructed with Model constructor function", function () {
      return loadModelJson(texturedBoxModel.gltf, {
        releaseGltfJson: true,
        incrementallyLoadTextures: false,
        asynchronous: true,
      }).then(function (m) {
        expect(m.releaseGltfJson).toEqual(true);
        expect(m.gltf).not.toBeDefined();

        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("Models are cached with fromGltf (1/2)", function () {
      var key = "a-cache-key";

      // This cache for this model is initially empty
      var gltfCache = Model._gltfCache;
      expect(gltfCache[key]).not.toBeDefined();

      var modelRendererResourceCache =
        scene.context.cache.modelRendererResourceCache;
      expect(modelRendererResourceCache[key]).not.toBeDefined();

      // Use a custom cache key to avoid conflicting with previous tests
      var promise = loadModel(boxUrl, {
        cacheKey: key,
      });

      expect(gltfCache[key]).toBeDefined();
      expect(gltfCache[key].count).toEqual(1);
      expect(gltfCache[key].ready).toEqual(false);

      // This is a cache hit, but the JSON request is still pending.
      // In the test below, the cache hit occurs after the request completes.
      var promise2 = loadModel(boxUrl, {
        cacheKey: key,
      });

      expect(gltfCache[key].count).toEqual(2);

      return when.all([promise, promise2], function (models) {
        var m = models[0];
        var m2 = models[1];

        // Render scene to progressively load the model
        scene.renderForSpecs();

        // glTF JSON cache set ready once the JSON was downloaded
        expect(gltfCache[key].ready).toEqual(true);

        expect(modelRendererResourceCache[key]).toBeDefined();
        expect(modelRendererResourceCache[key].count).toEqual(2);
        expect(modelRendererResourceCache[key].ready).toEqual(true);

        verifyRender(m);
        verifyRender(m2);

        primitives.remove(m);
        expect(gltfCache[key].count).toEqual(1);
        expect(modelRendererResourceCache[key].count).toEqual(1);

        primitives.remove(m2);
        expect(gltfCache[key]).not.toBeDefined();
        expect(modelRendererResourceCache[key]).not.toBeDefined();
      });
    });

    it("Models are cached with fromGltf (2/2)", function () {
      var key = "a-cache-key";

      // This cache for this model is initially empty
      var gltfCache = Model._gltfCache;
      expect(gltfCache[key]).not.toBeDefined();

      // Use a custom cache key to avoid conflicting with previous tests
      var promise = loadModel(boxUrl, {
        cacheKey: key,
      });

      expect(gltfCache[key]).toBeDefined();
      expect(gltfCache[key].count).toEqual(1);
      expect(gltfCache[key].ready).toEqual(false);

      return promise.then(function (m) {
        // Render scene to progressively load the model
        scene.renderForSpecs();

        // Cache hit after JSON request completed.
        var m2;
        loadModel(boxUrl, {
          cacheKey: key,
        }).then(function (model) {
          m2 = model;
        });

        expect(gltfCache[key].ready).toEqual(true);
        expect(gltfCache[key].count).toEqual(2);

        verifyRender(m);
        verifyRender(m2);

        primitives.remove(m);
        expect(gltfCache[key].count).toEqual(1);

        primitives.remove(m2);
        expect(gltfCache[key]).not.toBeDefined();
      });
    });

    it("Cache with a custom cacheKey the Model Constructor (1/2)", function () {
      var key = "a-cache-key";

      // This cache for this model is initially empty
      var gltfCache = Model._gltfCache;
      expect(gltfCache[key]).not.toBeDefined();

      var modelRendererResourceCache =
        scene.context.cache.modelRendererResourceCache;
      expect(modelRendererResourceCache[key]).not.toBeDefined();

      var m = primitives.add(
        new Model({
          gltf: texturedBoxModel.gltf,
          modelMatrix: Transforms.eastNorthUpToFixedFrame(
            Cartesian3.fromDegrees(0.0, 0.0, 100.0)
          ),
          show: false,
          cacheKey: key,
          incrementallyLoadTextures: false,
          asynchronous: true,
        })
      );
      addZoomTo(m);

      expect(gltfCache[key]).toBeDefined();
      expect(gltfCache[key].count).toEqual(1);
      expect(gltfCache[key].ready).toEqual(true);

      return pollToPromise(
        function () {
          // Render scene to progressively load the model
          scene.renderForSpecs();

          expect(modelRendererResourceCache[key]).toBeDefined();
          expect(modelRendererResourceCache[key].count).toEqual(1);
          expect(modelRendererResourceCache[key].ready).toEqual(m.ready);

          return m.ready;
        },
        { timeout: 10000 }
      ).then(function () {
        verifyRender(m);

        primitives.remove(m);
        expect(gltfCache[key]).not.toBeDefined();
        expect(modelRendererResourceCache[key]).not.toBeDefined();
      });
    });

    it("Cache with a custom cacheKey when using the Model Constructor (2/2)", function () {
      var key = "a-cache-key";
      var key3 = "another-cache-key";

      // This cache for these keys is initially empty
      var gltfCache = Model._gltfCache;
      expect(gltfCache[key]).not.toBeDefined();
      expect(gltfCache[key3]).not.toBeDefined();

      var modelRendererResourceCache =
        scene.context.cache.modelRendererResourceCache;
      expect(modelRendererResourceCache[key]).not.toBeDefined();
      expect(modelRendererResourceCache[key3]).not.toBeDefined();

      var m = primitives.add(
        new Model({
          gltf: texturedBoxModel.gltf,
          modelMatrix: Transforms.eastNorthUpToFixedFrame(
            Cartesian3.fromDegrees(0.0, 0.0, 100.0)
          ),
          show: false,
          cacheKey: key,
          asynchronous: true,
        })
      );
      addZoomTo(m);

      expect(gltfCache[key]).toBeDefined();
      expect(gltfCache[key].count).toEqual(1);
      expect(gltfCache[key].ready).toEqual(true);

      // Should be cache hit.  Not need to provide glTF.
      var m2 = primitives.add(
        new Model({
          modelMatrix: Transforms.eastNorthUpToFixedFrame(
            Cartesian3.fromDegrees(0.0, 0.0, 100.0)
          ),
          show: false,
          cacheKey: key,
          asynchronous: true,
        })
      );
      addZoomTo(m2);

      expect(gltfCache[key].count).toEqual(2);

      // Should be cache miss.
      var m3 = primitives.add(
        new Model({
          gltf: texturedBoxModel.gltf,
          modelMatrix: Transforms.eastNorthUpToFixedFrame(
            Cartesian3.fromDegrees(0.0, 0.0, 100.0)
          ),
          show: false,
          cacheKey: key3,
          asynchronous: true,
        })
      );
      addZoomTo(m3);

      expect(gltfCache[key3]).toBeDefined();
      expect(gltfCache[key3].count).toEqual(1);
      expect(gltfCache[key3].ready).toEqual(true);

      return pollToPromise(
        function () {
          // Render scene to progressively load the model
          scene.renderForSpecs();

          if (m.ready && m2.ready && m3.ready) {
            expect(modelRendererResourceCache[key]).toBeDefined();
            expect(modelRendererResourceCache[key].count).toEqual(2);

            expect(modelRendererResourceCache[key3]).toBeDefined();
            expect(modelRendererResourceCache[key3].count).toEqual(1);

            return true;
          }

          return false;
        },
        { timeout: 10000 }
      ).then(function () {
        verifyRender(m);
        verifyRender(m2);
        verifyRender(m3);

        primitives.remove(m);
        primitives.remove(m2);
        expect(gltfCache[key]).not.toBeDefined();
        expect(modelRendererResourceCache[key]).not.toBeDefined();

        primitives.remove(m3);
        expect(gltfCache[key3]).not.toBeDefined();
        expect(modelRendererResourceCache[key3]).not.toBeDefined();
      });
    });

    it("Loads with incrementallyLoadTextures set to true", function () {
      return loadModelJson(texturedBoxModel.gltf, {
        incrementallyLoadTextures: true,
        show: true,
      }).then(function (m) {
        // Get the rendered color of the model before textures are loaded
        var loadedColor;

        m.zoomTo();
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual([0, 0, 0, 255]);
          loadedColor = rgba;
        });

        return pollToPromise(
          function () {
            // Render scene to progressively load textures
            scene.renderForSpecs();
            // Textures have finished loading
            return m.pendingTextureLoads === 0;
          },
          { timeout: 10000 }
        ).then(function () {
          expect(scene).notToRender(loadedColor);
          primitives.remove(m);
        });
      });
    });

    it("Loads with incrementallyLoadTextures set to false", function () {
      return loadModelJson(texturedBoxModel.gltf, {
        incrementallyLoadTextures: false,
        show: true,
      }).then(function (m) {
        // Get the rendered color of the model before textures are loaded
        var loadedColor;

        m.zoomTo();
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual([0, 0, 0, 255]);
          loadedColor = rgba;
        });

        return pollToPromise(
          function () {
            // Render scene to progressively load textures (they should already be loaded)
            scene.renderForSpecs();
            // Textures have finished loading
            return !defined(m._loadResources);
          },
          { timeout: 10000 }
        ).then(function () {
          expect(scene).toRender(loadedColor);
          primitives.remove(m);
        });
      });
    });

    it("loads a glTF with KHR_materials_common using the constant lighting model", function () {
      return loadModel(boxConstantUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using the lambert lighting model", function () {
      return loadModel(boxLambertUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using the blinn lighting model", function () {
      return loadModel(boxBlinnUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using the phong lighting model", function () {
      return loadModel(boxPhongUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using a black ambient/directional light", function () {
      return loadModel(boxNoLightUrl).then(function (m) {
        // Verify that we render a black model because lighting is completely off
        expect(m.ready).toBe(true);
        expect(scene).toRender([0, 0, 0, 255]);
        m.show = true;
        m.zoomTo();
        expect(scene).toRender([0, 0, 0, 255]);
        m.show = false;

        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using an ambient light", function () {
      return loadModel(boxAmbientLightUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using a directional light", function () {
      return loadModel(boxDirectionalLightUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using a point light", function () {
      return loadModel(boxPointLightUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common using a spot light", function () {
      return loadModel(boxSpotLightUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common that has skinning", function () {
      return loadModel(CesiumManUrl).then(function (m) {
        // Face CesiumMan towards the camera. See https://github.com/CesiumGS/cesium/pull/8958#issuecomment-644352798
        m.modelMatrix = Matrix4.multiply(
          m.modelMatrix,
          Axis.Y_UP_TO_X_UP,
          new Matrix4()
        );
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_materials_common that has transparency", function () {
      return loadModel(boxTransparentUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with WEB3D_quantized_attributes POSITION and NORMAL", function () {
      return loadModel(boxQuantizedUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with WEB3D_quantized_attributes and accessor.normalized", function () {
      return loadModel(boxQuantizedUrl).then(function (m) {
        verifyRender(m);
        var gltf = m.gltf;
        var accessors = gltf.accessors;
        var normalAccessor = accessors[2];
        var positionAccessor = accessors[1];
        normalAccessor.normalized = true;
        positionAccessor.normalized = true;
        var decodeMatrixArray =
          normalAccessor.extensions.WEB3D_quantized_attributes.decodeMatrix;
        var decodeMatrix = new Matrix4();
        Matrix4.unpack(decodeMatrixArray, 0, decodeMatrix);
        Matrix4.multiplyByUniformScale(decodeMatrix, 65535.0, decodeMatrix);
        Matrix4.pack(decodeMatrix, decodeMatrixArray);
        decodeMatrixArray =
          positionAccessor.extensions.WEB3D_quantized_attributes.decodeMatrix;
        Matrix4.unpack(decodeMatrixArray, 0, decodeMatrix);
        Matrix4.multiplyByUniformScale(decodeMatrix, 65535.0, decodeMatrix);
        Matrix4.pack(decodeMatrix, decodeMatrixArray);
        primitives.remove(m);
        return loadModelJson(gltf, {}).then(function (m) {
          verifyRender(m);
          primitives.remove(m);
        });
      });
    });

    it("loads a glTF with WEB3D_quantized_attributes POSITION and NORMAL where primitives with different accessors use the same shader", function () {
      return loadModel(milkTruckQuantizedUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with WEB3D_quantized_attributes POSITION, TEXCOORD and NORMAL where one primitive is quantized and the other is not, but both use the same shader", function () {
      return loadModel(milkTruckQuantizedMismatchUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with WEB3D_quantized_attributes TEXCOORD", function () {
      return loadModel(duckQuantizedUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with WEB3D_quantized_attributes JOINT and WEIGHT", function () {
      return loadModel(riggedSimpleQuantizedUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 without textures", function () {
      return loadModel(boxPbrUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with textures", function () {
      return loadModel(boomBoxUrl).then(function (m) {
        m.scale = 20.0; // Source model is very small, so scale up a bit
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with node animation", function () {
      return loadModel(boxAnimatedPbrUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with node animations set to unclamped", function () {
      return loadModel(boxAnimatedPbrUrl, {
        clampAnimations: false,
      }).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with skinning", function () {
      return loadModel(riggedSimplePbrUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with morph targets", function () {
      return loadModel(animatedMorphCubeUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with alphaMode set to OPAQUE", function () {
      return Resource.fetchJson(boxPbrUrl).then(function (gltf) {
        gltf.materials[0].alphaMode = "OPAQUE";

        return loadModelJson(gltf).then(function (m) {
          verifyRender(m);
          primitives.remove(m);
        });
      });
    });

    it("loads a glTF 2.0 with alphaMode set to MASK", function () {
      return Resource.fetchJson(boxPbrUrl).then(function (gltf) {
        gltf.materials[0].alphaMode = "MASK";
        gltf.materials[0].alphaCutoff = 0.5;

        return loadModelJson(gltf).then(function (m) {
          verifyRender(m);
          primitives.remove(m);
        });
      });
    });

    it("loads a glTF 2.0 with alphaMode set to BLEND", function () {
      return Resource.fetchJson(boxPbrUrl).then(function (gltf) {
        gltf.materials[0].alphaMode = "BLEND";

        return loadModelJson(gltf).then(function (m) {
          verifyRender(m);
          primitives.remove(m);
        });
      });
    });

    it("loads a glTF 2.0 with interleaved vertex attributes", function () {
      return loadModel(boxInterleavedPbrUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    function checkDoubleSided(model, doubleSided) {
      var camera = scene.camera;
      var center = Matrix4.multiplyByPoint(
        model.modelMatrix,
        model.boundingSphere.center,
        new Cartesian3()
      );
      var range = 4.0 * model.boundingSphere.radius;

      camera.lookAt(
        center,
        new HeadingPitchRange(0, -CesiumMath.PI_OVER_TWO, range)
      );
      expect(scene).notToRender([0, 0, 0, 255]);
      camera.lookAt(
        center,
        new HeadingPitchRange(0, CesiumMath.PI_OVER_TWO, range)
      );
      if (doubleSided) {
        expect(scene).notToRender([0, 0, 0, 255]);
      } else {
        expect(scene).toRender([0, 0, 0, 255]);
      }
    }

    function checkVertexColors(model) {
      model.zoomTo();
      // Blue plane
      scene.camera.moveLeft(0.5);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(255);
      });
      // Red plane
      scene.camera.moveRight(1.0);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toEqual(255);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });
    }

    it("loads a glTF 2.0 with doubleSided set to false", function () {
      return Resource.fetchJson(twoSidedPlaneUrl).then(function (gltf) {
        gltf.materials[0].doubleSided = false;
        return loadModelJson(gltf).then(function (m) {
          m.show = true;
          checkDoubleSided(m, false);
          primitives.remove(m);
        });
      });
    });

    it("loads a glTF 2.0 with doubleSided set to true", function () {
      return loadModel(twoSidedPlaneUrl).then(function (m) {
        m.show = true;
        checkDoubleSided(m, true);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with vertex colors", function () {
      return loadModel(vertexColorTestUrl, {
        forwardAxis: Axis.X,
      }).then(function (m) {
        m.show = true;
        checkVertexColors(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF 2.0 with an emissive texture and no normals", function () {
      return loadModel(emissiveUrl).then(function (model) {
        model.show = true;
        model.zoomTo();
        expect(scene).toRenderAndCall(function (rgba) {
          // Emissive texture is red
          expect(rgba[0]).toBeGreaterThan(20);
          expect(rgba[1]).toBeLessThan(20);
          expect(rgba[2]).toBeLessThan(20);
        });

        primitives.remove(model);
      });
    });

    function testBoxSideColors(m) {
      var rotateX = Matrix3.fromRotationX(CesiumMath.toRadians(90.0));
      var rotateY = Matrix3.fromRotationY(CesiumMath.toRadians(90.0));
      var rotateZ = Matrix3.fromRotationZ(CesiumMath.toRadians(90.0));

      // Each side of the cube should be a different color
      var oldPixelColor;

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba).not.toEqual([0, 0, 0, 255]);
        oldPixelColor = rgba;
      });

      for (var i = 0; i < 6; i++) {
        var rotate = rotateZ;
        if (i % 3 === 0) {
          rotate = rotateX;
        } else if ((i - 1) % 3 === 0) {
          rotate = rotateY;
        }
        Matrix4.multiplyByMatrix3(m.modelMatrix, rotate, m.modelMatrix);

        //eslint-disable-next-line no-loop-func
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual([0, 0, 0, 255]);
          expect(rgba).not.toEqual(oldPixelColor);
          oldPixelColor = rgba;
        });
      }
    }

    it("loads a gltf with normalized color attributes", function () {
      return loadModel(boxColorUrl).then(function (m) {
        expect(m.ready).toBe(true);
        expect(scene).toRender([0, 0, 0, 255]);
        m.show = true;
        m.zoomTo();
        testBoxSideColors(m);
        primitives.remove(m);
      });
    });

    it("loads a gltf with uint32 indices", function () {
      var context = scene.context;
      if (context._elementIndexUint) {
        return loadModel(boxUint32Indices).then(function (m) {
          verifyRender(m);
          primitives.remove(m);
        });
      }
    });

    it("throws runtime error when loading a gltf with uint32 indices if OES_element_index_uint is disabled", function () {
      var context = scene.context;
      var uint32Supported = context._elementIndexUint;
      context._elementIndexUint = false;

      var model = primitives.add(
        Model.fromGltf({
          url: boxUint32Indices,
        })
      );

      return pollToPromise(
        function () {
          // Render scene to progressively load the model
          scene.renderForSpecs();
          return model.ready;
        },
        { timeout: 10000 }
      )
        .then(function () {
          fail("should not resolve");
        })
        .otherwise(function (e) {
          expect(e).toBeDefined();
          primitives.remove(model);
          context._elementIndexUint = uint32Supported;
        });
    });

    it("loads a gltf with WEB3D_quantized_attributes COLOR", function () {
      return loadModel(boxColorQuantizedUrl).then(function (m) {
        expect(m.ready).toBe(true);
        expect(scene).toRender([0, 0, 0, 255]);
        m.show = true;
        m.zoomTo();
        testBoxSideColors(m);
        primitives.remove(m);
      });
    });

    it("loads a gltf with WEB3D_quantized_attributes SCALAR attribute", function () {
      return loadModel(boxScalarQuantizedUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads with custom vertex attributes, vertexShader, fragmentShader, and uniform map", function () {
      function vertexShaderLoaded(vs) {
        var renamedSource = ShaderSource.replaceMain(vs, "czm_old_main");
        var newMain =
          "attribute vec4 a_color;\n" +
          "varying vec4 v_color;\n" +
          "void main()\n" +
          "{\n" +
          "    czm_old_main();\n" +
          "    v_color = a_color;\n" +
          "}";
        return renamedSource + "\n" + newMain;
      }

      function fragmentShaderLoaded(fs) {
        fs =
          "uniform float u_value;\n" +
          "varying vec4 v_color;\n" +
          "void main()\n" +
          "{\n" +
          "    gl_FragColor = u_value * v_color;\n" +
          "}";
        return fs;
      }

      function uniformMapLoaded(uniformMap) {
        return combine(uniformMap, {
          u_value: function () {
            return 1.0;
          },
        });
      }

      var precreatedAttributes = {
        a_color: {
          index: 0, // updated in Model
          componentsPerAttribute: 4,
          value: [1.0, 1.0, 1.0, 1.0],
        },
      };

      var options = {
        show: true,
        precreatedAttributes: precreatedAttributes,
        vertexShaderLoaded: vertexShaderLoaded,
        fragmentShaderLoaded: fragmentShaderLoaded,
        uniformMapLoaded: uniformMapLoaded,
      };

      return loadModelJson(texturedBoxModel.gltf, options).then(function (
        model
      ) {
        model.zoomTo();
        expect(scene).toRender([255, 255, 255, 255]);
        primitives.remove(model);
      });
    });

    it("loads a glTF with KHR_draco_mesh_compression extension", function () {
      return loadModel(dracoCompressedModelUrl, {
        dequantizeInShader: false,
      }).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_draco_mesh_compression extension with integer attributes", function () {
      return loadModel(dracoCompressedModelWithAnimationUrl, {
        dequantizeInShader: false,
        forwardAxis: Axis.X,
      }).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with KHR_draco_mesh_compression extension and LINES attributes", function () {
      return loadModel(dracoCompressedModelWithLinesUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads multiple draco models from cache without decoding", function () {
      var initialModel;
      var decoder = DracoLoader._getDecoderTaskProcessor();
      return loadModel(dracoCompressedModelUrl)
        .then(function (m) {
          verifyRender(m);
          initialModel = m;
          spyOn(decoder, "scheduleTask");
          return loadModel(dracoCompressedModelUrl);
        })
        .then(function (m) {
          verifyRender(m);
          expect(decoder.scheduleTask).not.toHaveBeenCalled();
          primitives.remove(m);
          primitives.remove(initialModel);
        });
    });

    it("error decoding a draco compressed glTF causes model loading to fail", function () {
      var decoder = DracoLoader._getDecoderTaskProcessor();
      spyOn(decoder, "scheduleTask").and.returnValue(
        when.reject({ message: "my error" })
      );

      var model = primitives.add(
        Model.fromGltf({
          url: dracoCompressedModelUrl,
          dequantizeInShader: false,
        })
      );

      return pollToPromise(
        function () {
          scene.renderForSpecs();
          return model._state === 3; // FAILED
        },
        { timeout: 10000 }
      ).then(function () {
        model.readyPromise
          .then(function (e) {
            fail("should not resolve");
          })
          .otherwise(function (e) {
            expect(e).toBeDefined();
            expect(e.message).toEqual(
              "Failed to load model: ./Data/Models/DracoCompression/CesiumMilkTruck/CesiumMilkTruck.gltf\nmy error"
            );
            primitives.remove(model);
          });
      });
    });

    it("loads a draco compressed glTF and dequantizes in the shader", function () {
      return loadModel(dracoCompressedModelUrl, {
        dequantizeInShader: true,
      }).then(function (m) {
        verifyRender(m);

        var atrributeData = m._decodedData["0.primitive.0"].attributes;
        expect(atrributeData["POSITION"].quantization).toBeDefined();
        expect(atrributeData["TEXCOORD_0"].quantization).toBeDefined();
        expect(atrributeData["NORMAL"].quantization).toBeDefined();
        expect(atrributeData["NORMAL"].quantization.octEncoded).toBe(true);

        primitives.remove(m);
      });
    });

    it("loads a draco compressed glTF and dequantizes in the shader, skipping generic attributes", function () {
      return loadModel(dracoCompressedModelWithAnimationUrl, {
        dequantizeInShader: true,
        forwardAxis: Axis.X,
      }).then(function (m) {
        verifyRender(m);

        var atrributeData = m._decodedData["0.primitive.0"].attributes;
        expect(atrributeData["POSITION"].quantization).toBeDefined();
        expect(atrributeData["TEXCOORD_0"].quantization).toBeDefined();
        expect(atrributeData["NORMAL"].quantization).toBeDefined();
        expect(atrributeData["JOINTS_0"].quantization).toBeUndefined();

        primitives.remove(m);
      });
    });

    it("loads draco compressed glTF with RGBA per-vertex color", function () {
      return loadModel(dracoBoxVertexColorsRGBAUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads draco compressed glTF with RGB per-vertex color", function () {
      return loadModel(dracoBoxVertexColorsRGBUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("loads a glTF with multiple texCoords", function () {
      return loadModel(multiUvTestUrl).then(function (m) {
        verifyRender(m);
        primitives.remove(m);
      });
    });

    it("does not issue draw commands when ignoreCommands is true", function () {
      return loadModel(texturedBoxUrl, {
        ignoreCommands: true,
      }).then(function (m) {
        expect(m.ready).toBe(true);
        m.show = true;

        m.zoomTo();
        m.update(scene.frameState);
        expect(scene.frameState.commandList.length).toEqual(0);

        m.show = false;
        primitives.remove(m);
      });
    });

    it("does not issue draw commands when the model is out of view and cull is true", function () {
      return loadModel(texturedBoxUrl, {
        cull: true,
      }).then(function (m) {
        expect(m.ready).toBe(true);
        m.show = true;

        // Look at the model
        m.zoomTo();
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).not.toEqual(0);

        // Move the model out of view
        m.modelMatrix = Matrix4.fromTranslation(
          new Cartesian3(100000.0, 0.0, 0.0)
        );
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).toEqual(0);

        m.show = false;
        primitives.remove(m);
      });
    });

    it("issues draw commands when the model is out of view and cull is false", function () {
      return loadModel(texturedBoxUrl, {
        cull: false,
      }).then(function (m) {
        expect(m.ready).toBe(true);
        m.show = true;

        // Look at the model
        m.zoomTo();
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).not.toEqual(0);

        // Move the model out of view
        m.modelMatrix = Matrix4.fromTranslation(
          new Cartesian3(10000000000.0, 0.0, 0.0)
        );
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).not.toEqual(0);

        m.show = false;
        primitives.remove(m);
      });
    });

    it("renders with a color", function () {
      return loadModel(boxUrl).then(function (model) {
        model.show = true;
        model.zoomTo();

        // Model is originally red
        var sourceColor;
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(0);
          expect(rgba[1]).toEqual(0);
          sourceColor = rgba;
        });

        // Check MIX
        model.colorBlendMode = ColorBlendMode.MIX;
        model.color = Color.LIME;

        model.colorBlendAmount = 0.0;
        expect(scene).toRender(sourceColor);

        model.colorBlendAmount = 0.5;
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(0);
          expect(rgba[1]).toBeGreaterThan(0);
        });

        model.colorBlendAmount = 1.0;
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toEqual(0);
          expect(rgba[1]).toEqual(255);
        });

        // Check REPLACE
        model.colorBlendMode = ColorBlendMode.REPLACE;
        model.colorBlendAmount = 0.5; // Should have no effect
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toEqual(0);
          expect(rgba[1]).toEqual(255);
        });

        // Check HIGHLIGHT
        model.colorBlendMode = ColorBlendMode.HIGHLIGHT;
        model.color = Color.DARKGRAY;
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(0);
          expect(rgba[0]).toBeLessThan(255);
          expect(rgba[1]).toEqual(0);
          expect(rgba[2]).toEqual(0);
        });

        // Check alpha
        model.colorBlendMode = ColorBlendMode.REPLACE;
        model.color = Color.fromAlpha(Color.LIME, 0.5);
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toEqual(0);
          expect(rgba[1]).toBeLessThan(255);
          expect(rgba[1]).toBeGreaterThan(0);
        });

        // No commands are issued when the alpha is 0.0
        model.color = Color.fromAlpha(Color.LIME, 0.0);
        scene.renderForSpecs();
        var commands = scene.frameState.commandList;
        expect(commands.length).toBe(0);

        primitives.remove(model);
      });
    });

    it("renders with the KHR_materials_unlit extension", function () {
      return loadModel(boxPbrUnlitUrl).then(function (model) {
        model.show = true;
        model.zoomTo();
        // We expect to see the base color when unlit
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toEqual(0);
          expect(rgba[1]).toEqual(255);
          expect(rgba[2]).toEqual(0);
        });

        primitives.remove(model);
      });
    });

    it("renders with the KHR_materials_pbrSpecularGlossiness extension", function () {
      return loadModel(boomBoxPbrSpecularGlossinessUrl).then(function (model) {
        model.show = false;
        model.zoomTo(2.0);

        var originalColor;
        expect(scene).toRenderAndCall(function (rgba) {
          originalColor = rgba;
        });

        model.show = true;

        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual(originalColor);
        });

        primitives.remove(model);
      });
    });

    it("renders with the KHR_materials_pbrSpecularGlossiness extension with defaults", function () {
      return loadModel(boomBoxPbrSpecularGlossinessDefaultsUrl).then(function (
        model
      ) {
        model.show = false;
        model.zoomTo(2.0);

        var originalColor;
        expect(scene).toRenderAndCall(function (rgba) {
          originalColor = rgba;
        });

        model.show = true;

        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual(originalColor);
        });

        primitives.remove(model);
      });
    });

    it("renders with the KHR_materials_pbrSpecularGlossiness extension when no textures are found", function () {
      return loadModel(boomBoxPbrSpecularGlossinessNoTextureUrl).then(function (
        model
      ) {
        model.show = false;
        model.zoomTo(2.0);

        var originalColor;
        expect(scene).toRenderAndCall(function (rgba) {
          originalColor = rgba;
        });

        model.show = true;

        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual(originalColor);
        });

        primitives.remove(model);
      });
    });

    it("silhouetteSupported", function () {
      expect(Model.silhouetteSupported(scene)).toBe(true);
      scene.context._stencilBits = 0;
      expect(Model.silhouetteSupported(scene)).toBe(false);
      scene.context._stencilBits = 8;
    });

    it("renders with a silhouette", function () {
      return loadModel(boxUrl).then(function (model) {
        model.show = true;
        model.zoomTo();

        var commands = scene.frameState.commandList;

        // No silhouette
        model.silhouetteSize = 0.0;
        scene.renderForSpecs();
        expect(commands.length).toBe(1);
        expect(commands[0].renderState.stencilTest.enabled).toBe(false);
        expect(commands[0].pass).toBe(Pass.OPAQUE);

        // Opaque silhouette
        model.silhouetteSize = 1.0;
        scene.renderForSpecs();
        expect(commands.length).toBe(2);
        expect(commands[0].renderState.stencilTest.enabled).toBe(true);
        expect(commands[0].pass).toBe(Pass.OPAQUE);
        expect(commands[1].renderState.stencilTest.enabled).toBe(true);
        expect(commands[1].pass).toBe(Pass.OPAQUE);

        // Translucent silhouette
        model.silhouetteColor = Color.fromAlpha(Color.GREEN, 0.5);
        scene.renderForSpecs();
        expect(commands.length).toBe(2);
        expect(commands[0].renderState.stencilTest.enabled).toBe(true);
        expect(commands[0].pass).toBe(Pass.OPAQUE);
        expect(commands[1].renderState.stencilTest.enabled).toBe(true);
        expect(commands[1].pass).toBe(Pass.TRANSLUCENT);

        // Invisible silhouette. The model is rendered normally.
        model.silhouetteColor = Color.fromAlpha(Color.GREEN, 0.0);
        scene.renderForSpecs();
        expect(commands.length).toBe(1);
        expect(commands[0].renderState.stencilTest.enabled).toBe(false);
        expect(commands[0].pass).toBe(Pass.OPAQUE);

        // Invisible model with no silhouette. No commands.
        model.color = Color.fromAlpha(Color.WHITE, 0.0);
        model.silhouetteColor = Color.GREEN;
        model.silhouetteSize = 0.0;
        scene.renderForSpecs();
        expect(commands.length).toBe(0);

        // Invisible model with silhouette. Model command is stencil-only.
        model.silhouetteSize = 1.0;
        scene.renderForSpecs();
        expect(commands.length).toBe(2);
        expect(commands[0].renderState.colorMask).toEqual({
          red: false,
          green: false,
          blue: false,
          alpha: false,
        });
        expect(commands[0].renderState.depthMask).toEqual(false);
        expect(commands[0].renderState.stencilTest.enabled).toBe(true);
        expect(commands[0].pass).toBe(Pass.OPAQUE);
        expect(commands[1].renderState.stencilTest.enabled).toBe(true);
        expect(commands[1].pass).toBe(Pass.OPAQUE);

        // Translucent model with opaque silhouette. Silhouette is placed in the translucent pass.
        model.color = Color.fromAlpha(Color.WHITE, 0.5);
        scene.renderForSpecs();
        expect(commands.length).toBe(2);
        expect(commands[0].renderState.stencilTest.enabled).toBe(true);
        expect(commands[0].pass).toBe(Pass.TRANSLUCENT);
        expect(commands[1].renderState.stencilTest.enabled).toBe(true);
        expect(commands[1].pass).toBe(Pass.TRANSLUCENT);

        // Model with translucent commands with silhouette
        model.color = Color.WHITE;
        model._nodeCommands[0].command.pass = Pass.TRANSLUCENT;
        scene.renderForSpecs();
        expect(commands.length).toBe(2);
        expect(commands[0].renderState.stencilTest.enabled).toBe(true);
        expect(commands[0].pass).toBe(Pass.TRANSLUCENT);
        expect(commands[1].renderState.stencilTest.enabled).toBe(true);
        expect(commands[1].pass).toBe(Pass.TRANSLUCENT);
        model._nodeCommands[0].command.pass = Pass.OPAQUE; // Revert change

        // Translucent model with translucent silhouette.
        model.color = Color.fromAlpha(Color.WHITE, 0.5);
        model.silhouetteColor = Color.fromAlpha(Color.GREEN, 0.5);
        scene.renderForSpecs();
        expect(commands.length).toBe(2);
        expect(commands[0].renderState.stencilTest.enabled).toBe(true);
        expect(commands[0].pass).toBe(Pass.TRANSLUCENT);
        expect(commands[1].renderState.stencilTest.enabled).toBe(true);
        expect(commands[1].pass).toBe(Pass.TRANSLUCENT);

        model.color = Color.WHITE;
        model.silhouetteColor = Color.GREEN;

        // Load a second model
        return loadModel(boxUrl).then(function (model2) {
          model2.show = true;
          model2.silhouetteSize = 1.0;
          scene.renderForSpecs();
          expect(commands.length).toBe(4);
          expect(commands[0].renderState.stencilTest.enabled).toBe(true);
          expect(commands[0].pass).toBe(Pass.OPAQUE);
          expect(commands[1].renderState.stencilTest.enabled).toBe(true);
          expect(commands[1].pass).toBe(Pass.OPAQUE);
          expect(commands[2].renderState.stencilTest.enabled).toBe(true);
          expect(commands[2].pass).toBe(Pass.OPAQUE);
          expect(commands[3].renderState.stencilTest.enabled).toBe(true);
          expect(commands[3].pass).toBe(Pass.OPAQUE);

          var reference1 = commands[0].renderState.stencilTest.reference;
          var reference2 = commands[2].renderState.stencilTest.reference;
          expect(reference2).toEqual(reference1 + 1);

          primitives.remove(model);
          primitives.remove(model2);
        });
      });
    });

    it("renders with imageBaseLightingFactor", function () {
      return loadModel(boxPbrUrl).then(function (model) {
        model.show = true;
        model.zoomTo();
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual([0, 0, 0, 255]);
          model.imageBasedLightingFactor = new Cartesian2(0.0, 0.0);
          expect(scene).notToRender(rgba);

          primitives.remove(model);
        });
      });
    });

    it("renders with lightColor", function () {
      return loadModel(boxPbrUrl).then(function (model) {
        model.show = true;
        model.zoomTo();

        var sceneArgs = {
          scene: scene,
          time: JulianDate.fromDate(new Date("January 1, 2014 23:00:00 UTC")),
        };

        expect(sceneArgs).toRenderAndCall(function (rgba) {
          expect(rgba).toEqualEpsilon([131, 9, 9, 255], 5);
        });

        model.imageBasedLightingFactor = new Cartesian2(0.0, 0.0);
        expect(sceneArgs).toRenderAndCall(function (rgba) {
          expect(rgba).toEqualEpsilon([102, 9, 9, 255], 5);
        });

        model.lightColor = new Cartesian3(5.0, 5.0, 5.0);
        expect(sceneArgs).toRenderAndCall(function (rgba) {
          expect(rgba).toEqualEpsilon([164, 16, 16, 255], 5);
        });

        primitives.remove(model);
      });
    });

    it("Updates clipping planes when clipping planes are enabled", function () {
      return loadModel(boxUrl).then(function (model) {
        model.show = true;
        model.zoomTo();

        var gl = scene.frameState.context._gl;
        spyOn(gl, "texImage2D").and.callThrough();

        scene.renderForSpecs();
        var callsBeforeClipping = gl.texImage2D.calls.count();

        model.clippingPlanes = new ClippingPlaneCollection({
          planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
        });

        model.update(scene.frameState);
        scene.renderForSpecs();
        // When clipping planes are created, we expect two calls to texImage2D
        // (one for initial creation, and one for copying the data in)
        // because clipping planes is stored inside a texture.
        expect(gl.texImage2D.calls.count() - callsBeforeClipping).toEqual(2);

        primitives.remove(model);
      });
    });

    it("Clipping planes selectively disable rendering", function () {
      return loadModel(boxUrl).then(function (model) {
        model.show = true;
        model.zoomTo();

        var modelColor;
        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          modelColor = rgba;
        });

        var plane = new ClippingPlane(Cartesian3.UNIT_X, 0.0);
        model.clippingPlanes = new ClippingPlaneCollection({
          planes: [plane],
        });

        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual(modelColor);
        });

        plane.distance = 10.0;
        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).toEqual(modelColor);
        });

        primitives.remove(model);
      });
    });

    it("Clipping planes apply edge styling", function () {
      return loadModel(boxUrl).then(function (model) {
        model.show = true;
        model.zoomTo();

        var modelColor;
        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          modelColor = rgba;
        });

        var plane = new ClippingPlane(Cartesian3.UNIT_X, 0.0);
        model.clippingPlanes = new ClippingPlaneCollection({
          planes: [plane],
          edgeWidth: 5.0,
          edgeColor: Color.BLUE,
        });

        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual(modelColor);
        });

        plane.distance = 5.0;
        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).toEqual([0, 0, 255, 255]);
        });

        primitives.remove(model);
      });
    });

    it("Clipping planes union regions", function () {
      return loadModel(boxUrl).then(function (model) {
        model.show = true;
        model.zoomTo();

        var modelColor;
        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          modelColor = rgba;
        });

        model.clippingPlanes = new ClippingPlaneCollection({
          planes: [
            new ClippingPlane(Cartesian3.UNIT_Z, 5.0),
            new ClippingPlane(Cartesian3.UNIT_X, 0.0),
          ],
          unionClippingRegions: true,
        });

        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual(modelColor);
        });

        model.clippingPlanes.unionClippingRegions = false;
        model.update(scene.frameState);
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).toEqual(modelColor);
        });

        primitives.remove(model);
      });
    });

    it("gets triangle count", function () {
      expect(texturedBoxModel.trianglesLength).toBe(12);
      expect(cesiumAirModel.trianglesLength).toBe(5984);
    });

    it("gets memory usage", function () {
      // Texture is originally 211*211 but is scaled up to 256*256 to support its minification filter and then is mipmapped
      var expectedTextureMemory = Math.floor(256 * 256 * 4 * (4 / 3));
      var expectedGeometryMemory = 840;
      var options = {
        cacheKey: "memory-usage-test",
        incrementallyLoadTextures: false,
      };
      return loadModel(texturedBoxUrl, options).then(function (model) {
        // The first model owns the resources
        expect(model.geometryByteLength).toBe(expectedGeometryMemory);
        expect(model.texturesByteLength).toBe(expectedTextureMemory);
        expect(model.cachedGeometryByteLength).toBe(0);
        expect(model.cachedTexturesByteLength).toBe(0);

        return loadModel(texturedBoxUrl, options).then(function (model) {
          // The second model is sharing the resources, so its memory usage is reported as 0
          expect(model.geometryByteLength).toBe(0);
          expect(model.texturesByteLength).toBe(0);
          expect(model.cachedGeometryByteLength).toBe(expectedGeometryMemory);
          expect(model.cachedTexturesByteLength).toBe(expectedTextureMemory);
        });
      });
    });

    it("renders box when back face culling is disabled", function () {
      return loadModel(boxBackFaceCullingUrl).then(function (model) {
        expect(model.ready).toBe(true);
        model.show = true;

        // Look at the model
        model.zoomTo();

        expect({
          scene: scene,
          time: JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC")),
        }).toRenderAndCall(function (rgba) {
          expect(rgba).toEqual([0, 0, 0, 255]);
        });

        model.backFaceCulling = false;

        expect({
          scene: scene,
          time: JulianDate.fromDate(new Date("January 1, 2014 12:00:00 UTC")),
        }).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual([0, 0, 0, 255]);
        });

        primitives.remove(model);
      });
    });

    describe("height referenced model", function () {
      function createMockGlobe() {
        var globe = {
          callback: undefined,
          removedCallback: false,
          ellipsoid: Ellipsoid.WGS84,
          update: function () {},
          render: function () {},
          getHeight: function () {
            return 0.0;
          },
          _surface: {
            tileProvider: {
              ready: true,
            },
            _tileLoadQueueHigh: [],
            _tileLoadQueueMedium: [],
            _tileLoadQueueLow: [],
            _debug: {
              tilesWaitingForChildren: 0,
            },
          },
          imageryLayersUpdatedEvent: new Event(),
          destroy: function () {},
        };

        globe.beginFrame = function () {};

        globe.endFrame = function () {};

        globe.terrainProviderChanged = new Event();
        Object.defineProperties(globe, {
          terrainProvider: {
            set: function (value) {
              this.terrainProviderChanged.raiseEvent(value);
            },
          },
        });

        globe._surface.updateHeight = function (position, callback) {
          globe.callback = callback;
          return function () {
            globe.removedCallback = true;
            globe.callback = undefined;
          };
        };

        return globe;
      }

      it("explicitly constructs a model with height reference", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          scene: scene,
        }).then(function (model) {
          expect(model.heightReference).toEqual(
            HeightReference.CLAMP_TO_GROUND
          );
          primitives.remove(model);
        });
      });

      it("set model height reference property", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          scene: scene,
        }).then(function (model) {
          model.heightReference = HeightReference.CLAMP_TO_GROUND;
          expect(model.heightReference).toEqual(
            HeightReference.CLAMP_TO_GROUND
          );
          primitives.remove(model);
        });
      });

      it("creating with a height reference creates a height update callback", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
        }).then(function (model) {
          expect(scene.globe.callback).toBeDefined();
          primitives.remove(model);
        });
      });

      it("set height reference property creates a height update callback", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
          show: true,
        }).then(function (model) {
          model.heightReference = HeightReference.CLAMP_TO_GROUND;
          expect(model.heightReference).toEqual(
            HeightReference.CLAMP_TO_GROUND
          );
          scene.renderForSpecs();
          expect(scene.globe.callback).toBeDefined();
          primitives.remove(model);
        });
      });

      it("updates the callback when the height reference changes", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
          show: true,
        }).then(function (model) {
          expect(scene.globe.callback).toBeDefined();

          model.heightReference = HeightReference.RELATIVE_TO_GROUND;
          scene.renderForSpecs();
          expect(scene.globe.removedCallback).toEqual(true);
          expect(scene.globe.callback).toBeDefined();

          scene.globe.removedCallback = false;
          model.heightReference = HeightReference.NONE;
          scene.renderForSpecs();
          expect(scene.globe.removedCallback).toEqual(true);
          expect(scene.globe.callback).not.toBeDefined();

          primitives.remove(model);
        });
      });

      it("changing the position updates the callback", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
          show: true,
        }).then(function (model) {
          expect(scene.globe.callback).toBeDefined();

          var matrix = Matrix4.clone(model.modelMatrix);
          var position = Cartesian3.fromDegrees(-73.0, 40.0);
          matrix[12] = position.x;
          matrix[13] = position.y;
          matrix[14] = position.z;

          model.modelMatrix = matrix;
          scene.renderForSpecs();

          expect(scene.globe.removedCallback).toEqual(true);
          expect(scene.globe.callback).toBeDefined();

          primitives.remove(model);
        });
      });

      it("callback updates the position", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
          show: true,
        }).then(function (model) {
          expect(scene.globe.callback).toBeDefined();
          scene.renderForSpecs();

          scene.globe.callback(Cartesian3.fromDegrees(-72.0, 40.0, 100.0));
          var matrix = model._clampedModelMatrix;
          var clampedPosition = new Cartesian3(
            matrix[12],
            matrix[13],
            matrix[14]
          );
          var cartographic = scene.globe.ellipsoid.cartesianToCartographic(
            clampedPosition
          );
          expect(cartographic.height).toEqualEpsilon(
            100.0,
            CesiumMath.EPSILON9
          );

          primitives.remove(model);
        });
      });

      it("removes the callback on destroy", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
          show: true,
        }).then(function (model) {
          expect(scene.globe.callback).toBeDefined();
          scene.renderForSpecs();

          primitives.remove(model);
          scene.renderForSpecs();
          expect(scene.globe.callback).toBeUndefined();
        });
      });

      it("changing the terrain provider", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
        }).then(function (model) {
          expect(model._heightChanged).toBe(false);

          var terrainProvider = new CesiumTerrainProvider({
            url: "made/up/url",
            requestVertexNormals: true,
          });

          scene.terrainProvider = terrainProvider;

          expect(model._heightChanged).toBe(true);
        });
      });

      it("height reference without a scene rejects", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          show: true,
        }).otherwise(function (error) {
          expect(error.message).toEqual(
            "Height reference is not supported without a scene and globe."
          );
        });
      });

      it("changing height reference without a scene throws DeveloperError", function () {
        scene.globe = createMockGlobe();
        return loadModelJson(texturedBoxModel.gltf, {
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          show: true,
        }).then(function (model) {
          model.heightReference = HeightReference.CLAMP_TO_GROUND;

          expect(function () {
            return scene.renderForSpecs();
          }).toThrowDeveloperError();

          primitives.remove(model);
        });
      });

      it("height reference without a globe rejects", function () {
        scene.globe = undefined;
        return loadModelJson(texturedBoxModel.gltf, {
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          scene: scene,
          show: true,
        }).otherwise(function (error) {
          expect(error.message).toEqual(
            "Height reference is not supported without a scene and globe."
          );
        });
      });

      it("changing height reference without a globe throws DeveloperError", function () {
        scene.globe = undefined;
        return loadModelJson(texturedBoxModel.gltf, {
          position: Cartesian3.fromDegrees(-72.0, 40.0),
          show: true,
        }).then(function (model) {
          model.heightReference = HeightReference.CLAMP_TO_GROUND;

          expect(function () {
            return scene.renderForSpecs();
          }).toThrowDeveloperError();

          primitives.remove(model);
        });
      });
    });
  },
  "WebGL"
);
