import { Cartesian2 } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Cartographic } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { CullingVolume } from "../../Source/Cesium.js";
import { defined } from "../../Source/Cesium.js";
import { getAbsoluteUri } from "../../Source/Cesium.js";
import { getJsonFromTypedArray } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { HeadingPitchRoll } from "../../Source/Cesium.js";
import { Intersect } from "../../Source/Cesium.js";
import { JulianDate } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { Matrix4 } from "../../Source/Cesium.js";
import { PerspectiveFrustum } from "../../Source/Cesium.js";
import { PrimitiveType } from "../../Source/Cesium.js";
import { Ray } from "../../Source/Cesium.js";
import { RequestScheduler } from "../../Source/Cesium.js";
import { Resource } from "../../Source/Cesium.js";
import { ResourceCache } from "../../Source/Cesium.js";
import { Transforms } from "../../Source/Cesium.js";
import { ClearCommand } from "../../Source/Cesium.js";
import { ContextLimits } from "../../Source/Cesium.js";
import { Camera } from "../../Source/Cesium.js";
import { Cesium3DTile } from "../../Source/Cesium.js";
import { Cesium3DTileColorBlendMode } from "../../Source/Cesium.js";
import { Cesium3DTileContentState } from "../../Source/Cesium.js";
import { Cesium3DTilePass } from "../../Source/Cesium.js";
import { Cesium3DTilePassState } from "../../Source/Cesium.js";
import { Cesium3DTileRefine } from "../../Source/Cesium.js";
import { Cesium3DTileset } from "../../Source/Cesium.js";
import { Cesium3DTileStyle } from "../../Source/Cesium.js";
import { ClippingPlane } from "../../Source/Cesium.js";
import { ClippingPlaneCollection } from "../../Source/Cesium.js";
import { CullFace } from "../../Source/Cesium.js";
import Cesium3DTilesTester from "../Cesium3DTilesTester.js";
import createScene from "../createScene.js";
import generateJsonBuffer from "../generateJsonBuffer.js";
import pollToPromise from "../pollToPromise.js";
import { when } from "../../Source/Cesium.js";

describe(
  "Scene/Cesium3DTileset",
  function () {
    // It's not easily possible to mock the most detailed pick functions
    // so don't run those tests when using the WebGL stub
    var webglStub = !!window.webglStub;

    var scene;
    var centerLongitude = -1.31968;
    var centerLatitude = 0.698874;
    var options;

    // Parent tile with content and four child tiles with content
    var tilesetUrl = "Data/Cesium3DTiles/Tilesets/Tileset/tileset.json";

    // Parent tile with no content and four child tiles with content
    var tilesetEmptyRootUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetEmptyRoot/tileset.json";

    // Tileset with 3 levels of uniform subdivision
    var tilesetUniform =
      "Data/Cesium3DTiles/Tilesets/TilesetUniform/tileset.json";

    var tilesetReplacement1Url =
      "Data/Cesium3DTiles/Tilesets/TilesetReplacement1/tileset.json";
    var tilesetReplacement2Url =
      "Data/Cesium3DTiles/Tilesets/TilesetReplacement2/tileset.json";
    var tilesetReplacement3Url =
      "Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset.json";

    // 3 level tree with mix of additive and replacement refinement
    var tilesetRefinementMix =
      "Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/tileset.json";

    // tileset.json : root content points to tiles2.json
    // tiles2.json: root with b3dm content, three children with b3dm content, one child points to tiles3.json
    // tiles3.json: root with b3dm content
    var tilesetOfTilesetsUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json";

    var withoutBatchTableUrl =
      "Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/tileset.json";
    var withBatchTableUrl =
      "Data/Cesium3DTiles/Batched/BatchedWithBatchTable/tileset.json";
    var noBatchIdsUrl =
      "Data/Cesium3DTiles/Batched/BatchedNoBatchIds/tileset.json";

    var withBatchTableHierarchyUrl =
      "Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json";

    var withTransformBoxUrl =
      "Data/Cesium3DTiles/Batched/BatchedWithTransformBox/tileset.json";
    var withTransformSphereUrl =
      "Data/Cesium3DTiles/Batched/BatchedWithTransformSphere/tileset.json";
    var withTransformRegionUrl =
      "Data/Cesium3DTiles/Batched/BatchedWithTransformRegion/tileset.json";
    var withBoundingSphereUrl =
      "Data/Cesium3DTiles/Batched/BatchedWithBoundingSphere/tileset.json";

    var compositeUrl = "Data/Cesium3DTiles/Composite/Composite/tileset.json";
    var instancedUrl =
      "Data/Cesium3DTiles/Instanced/InstancedWithBatchTable/tileset.json";
    var instancedRedMaterialUrl =
      "Data/Cesium3DTiles/Instanced/InstancedRedMaterial/tileset.json";
    var instancedAnimationUrl =
      "Data/Cesium3DTiles/Instanced/InstancedAnimated/tileset.json";

    var gltfContentUrl = "Data/Cesium3DTiles/GltfContent/glTF/tileset.json";

    // 1 tile where each feature is a different source color
    var colorsUrl = "Data/Cesium3DTiles/Batched/BatchedColors/tileset.json";

    // 1 tile where each feature has a reddish texture
    var texturedUrl = "Data/Cesium3DTiles/Batched/BatchedTextured/tileset.json";

    // 1 tile with translucent features
    var translucentUrl =
      "Data/Cesium3DTiles/Batched/BatchedTranslucent/tileset.json";

    // 1 tile with opaque and translucent features
    var translucentOpaqueMixUrl =
      "Data/Cesium3DTiles/Batched/BatchedTranslucentOpaqueMix/tileset.json";

    // Root tile is transformed from local space to wgs84, child tile is rotated, scaled, and translated locally
    var tilesetWithTransformsUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetWithTransforms/tileset.json";

    // Root tile with 4 b3dm children and 1 pnts child with a viewer request volume
    var tilesetWithViewerRequestVolumeUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetWithViewerRequestVolume/tileset.json";

    // Parent tile with content and four child tiles with content with viewer request volume for each child
    var tilesetReplacementWithViewerRequestVolumeUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetReplacementWithViewerRequestVolume/tileset.json";

    var tilesetWithExternalResourcesUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetWithExternalResources/tileset.json";
    var tilesetUrlWithContentUri =
      "Data/Cesium3DTiles/Batched/BatchedWithContentDataUri/tileset.json";

    var tilesetSubtreeExpirationUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/tileset.json";
    var tilesetSubtreeUrl =
      "Data/Cesium3DTiles/Tilesets/TilesetSubtreeExpiration/subtree.json";
    var batchedExpirationUrl =
      "Data/Cesium3DTiles/Batched/BatchedExpiration/tileset.json";
    var batchedColorsB3dmUrl =
      "Data/Cesium3DTiles/Batched/BatchedColors/batchedColors.b3dm";
    var batchedVertexColorsUrl =
      "Data/Cesium3DTiles/Batched/BatchedWithVertexColors/tileset.json";
    var batchedAnimationUrl =
      "Data/Cesium3DTiles/Batched/BatchedAnimated/tileset.json";

    var styleUrl = "Data/Cesium3DTiles/Style/style.json";

    var pointCloudUrl =
      "Data/Cesium3DTiles/PointCloud/PointCloudRGB/tileset.json";
    var pointCloudBatchedUrl =
      "Data/Cesium3DTiles/PointCloud/PointCloudBatched/tileset.json";

    function endsWith(string, suffix) {
      var slicePoint = string.length - suffix.length;
      return string.slice(slicePoint) === suffix;
    }

    beforeAll(function () {
      scene = createScene();
    });

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

    beforeEach(function () {
      RequestScheduler.clearForSpecs();
      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);

      viewAllTiles();

      options = {
        cullRequestsWhileMoving: false,
      };
    });

    afterEach(function () {
      scene.primitives.removeAll();
      ResourceCache.clearForSpecs();
    });

    function setZoom(distance) {
      // Bird's eye view
      var center = Cartesian3.fromRadians(centerLongitude, centerLatitude);
      scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, distance));
    }

    function viewAllTiles() {
      setZoom(15.0);
    }

    function viewRootOnly() {
      setZoom(100.0);
    }

    function viewNothing() {
      setZoom(200.0);
    }

    function viewSky() {
      var center = Cartesian3.fromRadians(centerLongitude, centerLatitude, 100);
      scene.camera.lookAt(center, new HeadingPitchRange(0.0, 1.57, 10.0));
    }

    function viewBottomLeft() {
      viewAllTiles();
      scene.camera.moveLeft(200.0);
      scene.camera.moveDown(200.0);
    }

    function viewInstances() {
      setZoom(30.0);
    }

    function viewPointCloud() {
      setZoom(5.0);
    }

    function viewGltfContent() {
      setZoom(100.0);
    }

    function isSelected(tileset, tile) {
      return tileset._selectedTiles.indexOf(tile) > -1;
    }

    it("throws with undefined url", function () {
      expect(function () {
        return new Cesium3DTileset();
      }).toThrowDeveloperError();
    });

    it("rejects readyPromise with invalid tileset JSON fiile", function () {
      spyOn(Resource._Implementations, "loadWithXhr").and.callFake(function (
        url,
        responseType,
        method,
        data,
        headers,
        deferred,
        overrideMimeType
      ) {
        deferred.reject();
      });

      options.url = "invalid.json";
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      return tileset.readyPromise
        .then(function () {
          fail("should not resolve");
        })
        .otherwise(function (error) {
          expect(tileset.ready).toEqual(false);
        });
    });

    it("loads json with static loadJson method", function () {
      var tilesetJson = {
        asset: {
          version: 2.0,
        },
      };

      var uri = "data:text/plain;base64," + btoa(JSON.stringify(tilesetJson));

      Cesium3DTileset.loadJson(uri)
        .then(function (result) {
          expect(result).toEqual(tilesetJson);
        })
        .otherwise(function (error) {
          fail("should not fail");
        });
    });

    it("static method loadJson is used in Cesium3DTileset constructor", function () {
      var path = "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json";

      var originalLoadJson = Cesium3DTileset.loadJson;

      // override loadJson and replace incorrect url with correct url
      Cesium3DTileset.loadJson = function (tilesetUrl) {
        return originalLoadJson(path);
      };

      // setup tileset with invalid url (overridden loadJson should replace invalid url with correct url)
      var tileset = new Cesium3DTileset({
        url: "invalid.json",
      });

      // restore original version
      Cesium3DTileset.loadJson = originalLoadJson;

      return tileset.readyPromise
        .then(function () {
          expect(tileset.ready).toEqual(true);
        })
        .otherwise(function (error) {
          fail("should not fail");
        });
    });

    it("Constructor works with promise to resource", function () {
      var resource = new Resource({
        url: "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json",
      });

      // setup tileset with invalid url (overridden loadJson should replace invalid url with correct url)
      var tileset = new Cesium3DTileset({
        url: when.resolve(resource),
      });

      return tileset.readyPromise
        .then(function () {
          expect(tileset.ready).toEqual(true);
        })
        .otherwise(function (error) {
          fail("should not fail");
        });
    });

    it("Constructor works with file resource", function () {
      var resource = new Resource({
        url: "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json",
      });

      // setup tileset with invalid url (overridden loadJson should replace invalid url with correct url)
      var tileset = new Cesium3DTileset({
        url: resource,
      });

      return tileset.readyPromise
        .then(function () {
          expect(tileset.ready).toEqual(true);
        })
        .otherwise(function (error) {
          fail("should not fail");
        });
    });

    it("rejects readyPromise with invalid tileset version", function () {
      var tilesetJson = {
        asset: {
          version: 2.0,
        },
      };

      var uri = "data:text/plain;base64," + btoa(JSON.stringify(tilesetJson));

      options.url = uri;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      return tileset.readyPromise
        .then(function () {
          fail("should not resolve");
        })
        .otherwise(function (error) {
          expect(tileset.ready).toEqual(false);
        });
    });

    it("rejects readyPromise with unsupported extension", function () {
      var tilesetJson = {
        asset: {
          version: 1.0,
        },
        extensionsUsed: ["unsupported_extension"],
        extensionsRequired: ["unsupported_extension"],
      };

      var uri = "data:text/plain;base64," + btoa(JSON.stringify(tilesetJson));

      options.url = uri;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      return tileset.readyPromise
        .then(function () {
          fail("should not resolve");
        })
        .otherwise(function (error) {
          expect(tileset.ready).toEqual(false);
        });
    });

    it("url and tilesetUrl set up correctly given tileset JSON filepath", function () {
      var path = "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json";
      var tileset = new Cesium3DTileset({
        url: path,
      });
      expect(tileset.resource.url).toEqual(path);
    });

    it("url and tilesetUrl set up correctly given path with query string", function () {
      var path = "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json";
      var param = "?param1=1&param2=2";
      var tileset = new Cesium3DTileset({
        url: path + param,
      });
      expect(tileset.resource.url).toEqual(path + param);
    });

    it("resolves readyPromise", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        return tileset.readyPromise.then(function (tileset) {
          expect(tileset.ready).toEqual(true);
        });
      });
    });

    it("loads tileset JSON file", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var asset = tileset.asset;
        expect(asset).toBeDefined();
        expect(asset.version).toEqual("1.0");
        expect(asset.tilesetVersion).toEqual("1.2.3");

        var properties = tileset.properties;
        expect(properties).toBeDefined();
        expect(properties.id).toBeDefined();
        expect(properties.id.minimum).toEqual(0);
        expect(properties.id.maximum).toEqual(9);

        expect(tileset._geometricError).toEqual(240.0);
        expect(tileset.root).toBeDefined();
        expect(tileset.resource.url).toEqual(tilesetUrl);
      });
    });

    it("loads tileset with extras", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        expect(tileset.extras).toEqual({ name: "Sample Tileset" });
        expect(tileset.root.extras).toBeUndefined();

        var length = tileset.root.children.length;
        var taggedChildren = 0;
        for (var i = 0; i < length; ++i) {
          if (defined(tileset.root.children[i].extras)) {
            expect(tileset.root.children[i].extras).toEqual({
              id: "Special Tile",
            });
            ++taggedChildren;
          }
        }

        expect(taggedChildren).toEqual(1);
      });
    });

    it("gets root tile", function () {
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      expect(function () {
        return tileset.root;
      }).toThrowDeveloperError();
      return tileset.readyPromise.then(function () {
        expect(tileset.root).toBeDefined();
      });
    });

    it("hasExtension returns true if the tileset JSON file uses the specified extension", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        withBatchTableHierarchyUrl
      ).then(function (tileset) {
        expect(tileset.hasExtension("3DTILES_batch_table_hierarchy")).toBe(
          true
        );
        expect(tileset.hasExtension("3DTILES_nonexistant_extension")).toBe(
          false
        );
      });
    });

    it("passes version in query string to tiles", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        expect(tileset.root.content._resource.url).toEqual(
          getAbsoluteUri(
            tilesetUrl.replace("tileset.json", "parent.b3dm?v=1.2.3")
          )
        );
      });
    });

    it("passes version in query string to all external resources", function () {
      // Spy on loadWithXhr so we can verify requested urls
      spyOn(Resource._Implementations, "loadWithXhr").and.callThrough();

      var queryParams = "?a=1&b=boy";
      var queryParamsWithVersion = "?a=1&b=boy&v=1.2.3";
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithExternalResourcesUrl + queryParams
      ).then(function (tileset) {
        var calls = Resource._Implementations.loadWithXhr.calls.all();
        var callsLength = calls.length;
        for (var i = 0; i < callsLength; ++i) {
          var url = calls[0].args[0];
          if (url.indexOf(tilesetWithExternalResourcesUrl) >= 0) {
            var query = url.slice(url.indexOf("?"));
            if (url.indexOf("tileset.json") >= 0) {
              // The initial tileset.json does not have a tileset version parameter
              expect(query).toBe(queryParams);
            } else {
              expect(query).toBe(queryParamsWithVersion);
            }
          }
        }
      });
    });

    it("throws when getting asset and tileset is not ready", function () {
      var tileset = new Cesium3DTileset({
        url: tilesetUrl,
      });
      expect(function () {
        return tileset.asset;
      }).toThrowDeveloperError();
    });

    it("throws when getting extensions and tileset is not ready", function () {
      var tileset = new Cesium3DTileset({
        url: tilesetUrl,
      });
      expect(function () {
        return tileset.extensions;
      }).toThrowDeveloperError();
    });

    it("throws when getting properties and tileset is not ready", function () {
      var tileset = new Cesium3DTileset({
        url: tilesetUrl,
      });
      expect(function () {
        return tileset.properties;
      }).toThrowDeveloperError();
    });

    it("throws when getting extras and tileset is not ready", function () {
      var tileset = new Cesium3DTileset({
        url: tilesetUrl,
      });
      expect(function () {
        return tileset.extras;
      }).toThrowDeveloperError();
    });

    it("requests tile with invalid magic", function () {
      var invalidMagicBuffer = Cesium3DTilesTester.generateBatchedTileBuffer({
        magic: [120, 120, 120, 120],
      });
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      return tileset.readyPromise.then(function (tileset) {
        // Start spying after the tileset json has been loaded
        spyOn(Resource._Implementations, "loadWithXhr").and.callFake(function (
          url,
          responseType,
          method,
          data,
          headers,
          deferred,
          overrideMimeType
        ) {
          deferred.resolve(invalidMagicBuffer);
        });
        scene.renderForSpecs(); // Request root
        var root = tileset.root;
        return root.contentReadyPromise
          .then(function () {
            fail("should not resolve");
          })
          .otherwise(function (error) {
            expect(error.message).toBe("Invalid tile content.");
            expect(root._contentState).toEqual(Cesium3DTileContentState.FAILED);
          });
      });
    });

    it("handles failed tile requests", function () {
      viewRootOnly();
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      return tileset.readyPromise.then(function (tileset) {
        // Start spying after the tileset json has been loaded
        spyOn(Resource._Implementations, "loadWithXhr").and.callFake(function (
          url,
          responseType,
          method,
          data,
          headers,
          deferred,
          overrideMimeType
        ) {
          deferred.reject();
        });
        scene.renderForSpecs(); // Request root
        var root = tileset.root;
        return root.contentReadyPromise
          .then(function () {
            fail("should not resolve");
          })
          .otherwise(function (error) {
            expect(root._contentState).toEqual(Cesium3DTileContentState.FAILED);
            var statistics = tileset.statistics;
            expect(statistics.numberOfAttemptedRequests).toBe(0);
            expect(statistics.numberOfPendingRequests).toBe(0);
            expect(statistics.numberOfTilesProcessing).toBe(0);
            expect(statistics.numberOfTilesWithContentReady).toBe(0);
          });
      });
    });

    it("handles failed tile processing", function () {
      viewRootOnly();
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      return tileset.readyPromise.then(function (tileset) {
        // Start spying after the tileset json has been loaded
        spyOn(Resource._Implementations, "loadWithXhr").and.callFake(function (
          url,
          responseType,
          method,
          data,
          headers,
          deferred,
          overrideMimeType
        ) {
          deferred.resolve(
            Cesium3DTilesTester.generateBatchedTileBuffer({
              version: 0, // Invalid version
            })
          );
        });
        scene.renderForSpecs(); // Request root
        var root = tileset.root;
        return root.contentReadyPromise
          .then(function () {
            fail("should not resolve");
          })
          .otherwise(function (error) {
            expect(root._contentState).toEqual(Cesium3DTileContentState.FAILED);
            var statistics = tileset.statistics;
            expect(statistics.numberOfAttemptedRequests).toBe(0);
            expect(statistics.numberOfPendingRequests).toBe(0);
            expect(statistics.numberOfTilesProcessing).toBe(0);
            expect(statistics.numberOfTilesWithContentReady).toBe(0);
          });
      });
    });

    it("renders tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(5);
      });
    });

    function checkAnimation(url) {
      return Cesium3DTilesTester.loadTileset(scene, url).then(function (
        tileset
      ) {
        var renderOptions = {
          scene: scene,
          time: new JulianDate(271.828),
        };

        expect(renderOptions).toRenderAndCall(function (rgba) {
          var commandList = scene.frameState.commandList;
          var modelMatrix1 = Matrix4.clone(commandList[0].modelMatrix);
          // Check that the scene changes after .5 seconds. (it animates)
          renderOptions.time.secondsOfDay += 0.5;
          expect(renderOptions).toRenderAndCall(function (rgba) {
            var modelMatrix2 = Matrix4.clone(commandList[0].modelMatrix);
            expect(modelMatrix1).not.toEqual(modelMatrix2);
          });
        });
      });
    }
    it("animates instanced tileset", function () {
      return checkAnimation(instancedAnimationUrl);
    });

    it("animates batched tileset", function () {
      return checkAnimation(batchedAnimationUrl);
    });

    it("renders tileset in CV", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        scene.morphToColumbusView(0.0);
        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(5);
      });
    });

    it("renders tileset in 2D", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        scene.morphTo2D(0.0);
        tileset.maximumScreenSpaceError = 3;
        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(10);
      });
    });

    it("does not render during morph", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var commandList = scene.frameState.commandList;
        scene.renderForSpecs();
        expect(commandList.length).toBeGreaterThan(0);
        scene.morphToColumbusView(1.0);
        scene.renderForSpecs();
        expect(commandList.length).toBe(0);
      });
    });

    it("renders tileset with empty root tile", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetEmptyRootUrl).then(
        function (tileset) {
          var statistics = tileset._statistics;
          expect(statistics.visited).toEqual(5);
          expect(statistics.numberOfCommands).toEqual(4); // Empty tile doesn't issue a command
        }
      );
    });

    it("verify statistics", function () {
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      // Verify initial values
      var statistics = tileset._statistics;
      expect(statistics.visited).toEqual(0);
      expect(statistics.numberOfCommands).toEqual(0);
      expect(statistics.numberOfPendingRequests).toEqual(0);
      expect(statistics.numberOfTilesProcessing).toEqual(0);

      return Cesium3DTilesTester.waitForReady(scene, tileset).then(function () {
        // Check that root and children are requested
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(0);
        expect(statistics.numberOfPendingRequests).toEqual(5);
        expect(statistics.numberOfTilesProcessing).toEqual(0);

        // Wait for all tiles to load and check that they are all visited and rendered
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(statistics.visited).toEqual(5);
            expect(statistics.numberOfCommands).toEqual(5);
            expect(statistics.numberOfPendingRequests).toEqual(0);
            expect(statistics.numberOfTilesProcessing).toEqual(0);
          }
        );
      });
    });

    function checkPointAndFeatureCounts(tileset, features, points, triangles) {
      var statistics = tileset._statistics;

      expect(statistics.numberOfFeaturesSelected).toEqual(0);
      expect(statistics.numberOfFeaturesLoaded).toEqual(0);
      expect(statistics.numberOfPointsSelected).toEqual(0);
      expect(statistics.numberOfPointsLoaded).toEqual(0);
      expect(statistics.numberOfTrianglesSelected).toEqual(0);

      return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
        function () {
          expect(statistics.numberOfFeaturesSelected).toEqual(features);
          expect(statistics.numberOfFeaturesLoaded).toEqual(features);
          expect(statistics.numberOfPointsSelected).toEqual(points);
          expect(statistics.numberOfPointsLoaded).toEqual(points);
          expect(statistics.numberOfTrianglesSelected).toEqual(triangles);

          viewNothing();
          scene.renderForSpecs();

          expect(statistics.numberOfFeaturesSelected).toEqual(0);
          expect(statistics.numberOfFeaturesLoaded).toEqual(features);
          expect(statistics.numberOfPointsSelected).toEqual(0);
          expect(statistics.numberOfPointsLoaded).toEqual(points);
          expect(statistics.numberOfTrianglesSelected).toEqual(0);

          tileset.trimLoadedTiles();
          scene.renderForSpecs();

          expect(statistics.numberOfFeaturesSelected).toEqual(0);
          expect(statistics.numberOfFeaturesLoaded).toEqual(0);
          expect(statistics.numberOfPointsSelected).toEqual(0);
          expect(statistics.numberOfPointsLoaded).toEqual(0);
          expect(statistics.numberOfTrianglesSelected).toEqual(0);
        }
      );
    }

    it("verify batched features statistics", function () {
      options.url = withBatchTableUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 10, 0, 120);
    });

    it("verify no batch table features statistics", function () {
      options.url = noBatchIdsUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 0, 0, 120);
    });

    it("verify instanced features statistics", function () {
      options.url = instancedRedMaterialUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 25, 0, 12);
    });

    it("verify composite features statistics", function () {
      options.url = compositeUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 35, 0, 132);
    });

    it("verify tileset of tilesets features statistics", function () {
      options.url = tilesetOfTilesetsUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 50, 0, 600);
    });

    it("verify points statistics", function () {
      viewPointCloud();

      options.url = pointCloudUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 0, 1000, 0);
    });

    it("verify triangle statistics", function () {
      options.url = tilesetEmptyRootUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 40, 0, 480);
    });

    it("verify batched points statistics", function () {
      viewPointCloud();

      options.url = pointCloudBatchedUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));

      return checkPointAndFeatureCounts(tileset, 8, 1000, 0);
    });

    it("verify memory usage statistics", function () {
      // Calculations in Batched3DModel3DTileContentSpec, minus uvs
      var singleTileGeometryMemory = 7440;
      var singleTileTextureMemory = 0;
      var singleTileBatchTextureMemory = 40;
      var singleTilePickTextureMemory = 40;
      var tilesLength = 5;

      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;

        // No tiles loaded
        expect(statistics.geometryByteLength).toEqual(0);
        expect(statistics.texturesByteLength).toEqual(0);
        expect(statistics.batchTableByteLength).toEqual(0);

        viewRootOnly();
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            // Root tile loaded
            expect(statistics.geometryByteLength).toEqual(
              singleTileGeometryMemory
            );
            expect(statistics.texturesByteLength).toEqual(
              singleTileTextureMemory
            );
            expect(statistics.batchTableByteLength).toEqual(0);

            viewAllTiles();
            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function () {
                // All tiles loaded
                expect(statistics.geometryByteLength).toEqual(
                  singleTileGeometryMemory * tilesLength
                );
                expect(statistics.texturesByteLength).toEqual(
                  singleTileTextureMemory * tilesLength
                );
                expect(statistics.batchTableByteLength).toEqual(0);

                // One feature colored, the batch table memory is now higher
                tileset.root.content.getFeature(0).color = Color.RED;
                scene.renderForSpecs();
                expect(statistics.geometryByteLength).toEqual(
                  singleTileGeometryMemory * tilesLength
                );
                expect(statistics.texturesByteLength).toEqual(
                  singleTileTextureMemory * tilesLength
                );
                expect(statistics.batchTableByteLength).toEqual(
                  singleTileBatchTextureMemory
                );

                // All tiles picked, the texture memory is now higher
                scene.pickForSpecs();
                expect(statistics.geometryByteLength).toEqual(
                  singleTileGeometryMemory * tilesLength
                );
                expect(statistics.texturesByteLength).toEqual(
                  singleTileTextureMemory * tilesLength
                );
                expect(statistics.batchTableByteLength).toEqual(
                  singleTileBatchTextureMemory +
                    singleTilePickTextureMemory * tilesLength
                );

                // Tiles are still in memory when zoomed out
                viewNothing();
                scene.renderForSpecs();
                expect(statistics.geometryByteLength).toEqual(
                  singleTileGeometryMemory * tilesLength
                );
                expect(statistics.texturesByteLength).toEqual(
                  singleTileTextureMemory * tilesLength
                );
                expect(statistics.batchTableByteLength).toEqual(
                  singleTileBatchTextureMemory +
                    singleTilePickTextureMemory * tilesLength
                );

                // Trim loaded tiles, expect the memory statistics to be 0
                tileset.trimLoadedTiles();
                scene.renderForSpecs();
                expect(statistics.geometryByteLength).toEqual(0);
                expect(statistics.texturesByteLength).toEqual(0);
                expect(statistics.batchTableByteLength).toEqual(0);
              }
            );
          }
        );
      });
    });

    it("verify memory usage statistics for shared resources", function () {
      // Six tiles total:
      // * Two b3dm tiles - no shared resources
      // * Two i3dm tiles with embedded glTF - no shared resources
      // * Two i3dm tiles with external glTF - shared resources
      // Expect to see some saving with memory usage since two of the tiles share resources
      // All tiles reference the same external texture but texture caching is not supported yet
      // TODO : tweak test when #5051 is in

      var b3dmGeometryMemory = 840; // Only one box in the tile, unlike most other test tiles
      var i3dmGeometryMemory = 840;

      // Texture is 128x128 RGBA bytes, not mipmapped
      var texturesByteLength = 65536;

      var expectedGeometryMemory =
        b3dmGeometryMemory * 2 + i3dmGeometryMemory * 3;
      var expectedTextureMemory = texturesByteLength * 5;

      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithExternalResourcesUrl
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        expect(statistics.geometryByteLength).toBe(expectedGeometryMemory);
        expect(statistics.texturesByteLength).toBe(expectedTextureMemory);
      });
    });

    it("does not process tileset when screen space error is not met", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(5);

        // Set zoom far enough away to not meet sse
        viewNothing();
        scene.renderForSpecs();
        expect(statistics.visited).toEqual(0);
        expect(statistics.numberOfCommands).toEqual(0);
      });
    });

    it("does not select tiles when outside of view frustum", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(5);

        viewSky();

        scene.renderForSpecs();
        expect(statistics.visited).toEqual(0);
        expect(statistics.numberOfCommands).toEqual(0);
        expect(
          tileset.root.visibility(
            scene.frameState,
            CullingVolume.MASK_INDETERMINATE
          )
        ).toEqual(CullingVolume.MASK_OUTSIDE);
      });
    });

    it("does not load additive tiles that are out of view", function () {
      viewBottomLeft();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;
        expect(statistics.numberOfTilesWithContentReady).toEqual(2);
      });
    });

    it("culls with content box", function () {
      // Root tile has a content box that is half the extents of its box
      // Expect to cull root tile and three child tiles
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(5);

        viewBottomLeft();
        scene.renderForSpecs();
        expect(statistics.visited).toEqual(2); // Visits root, but does not render it
        expect(statistics.numberOfCommands).toEqual(1);
        expect(tileset._selectedTiles[0]).not.toBe(tileset.root);

        // Set contents box to undefined, and now root won't be culled
        tileset.root._contentBoundingVolume = undefined;
        scene.renderForSpecs();
        expect(statistics.visited).toEqual(2);
        expect(statistics.numberOfCommands).toEqual(2);
      });
    });

    function findTileByUri(tiles, uri) {
      var length = tiles.length;
      for (var i = 0; i < length; ++i) {
        var tile = tiles[i];
        var contentHeader = tile._header.content;
        if (defined(contentHeader)) {
          if (contentHeader.uri.indexOf(uri) >= 0) {
            return tile;
          }
        }
      }
      return undefined;
    }

    it("selects children in front to back order", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        // After moving the camera left by 1.0 and down by 0.5, the distance from the camera should be in the order:
        // 1. lower left
        // 2. upper left
        // 3. lower right
        // 4. upper right

        scene.camera.moveLeft(1.0);
        scene.camera.moveDown(0.5);
        scene.renderForSpecs();

        var root = tileset.root;
        var llTile = findTileByUri(root.children, "ll.b3dm");
        var lrTile = findTileByUri(root.children, "lr.b3dm");
        var urTile = findTileByUri(root.children, "ur.b3dm");
        var ulTile = findTileByUri(root.children, "ul.b3dm");

        var selectedTiles = tileset._selectedTiles;
        expect(selectedTiles[0]).toBe(root);
        expect(selectedTiles[1]).toBe(llTile);
        expect(selectedTiles[2]).toBe(ulTile);
        expect(selectedTiles[3]).toBe(lrTile);
        expect(selectedTiles[4]).toBe(urTile);
      });
    });

    function testDynamicScreenSpaceError(url, distance) {
      return Cesium3DTilesTester.loadTileset(scene, url).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;

        // Horizon view, only root is visible
        var center = Cartesian3.fromRadians(centerLongitude, centerLatitude);
        scene.camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, distance));

        // Set dynamic SSE to false (default)
        tileset.dynamicScreenSpaceError = false;
        scene.renderForSpecs();
        expect(statistics.visited).toEqual(1);
        expect(statistics.numberOfCommands).toEqual(1);

        // Set dynamic SSE to true, now the root is not rendered
        tileset.dynamicScreenSpaceError = true;
        tileset.dynamicScreenSpaceErrorDensity = 1.0;
        tileset.dynamicScreenSpaceErrorFactor = 10.0;
        scene.renderForSpecs();
        expect(statistics.visited).toEqual(0);
        expect(statistics.numberOfCommands).toEqual(0);
      });
    }

    function numberOfChildrenWithoutContent(tile) {
      var children = tile.children;
      var length = children.length;
      var count = 0;
      for (var i = 0; i < length; ++i) {
        var child = children[i];
        if (!child.contentReady) {
          ++count;
        }
      }
      return count;
    }

    // Adjust distances for each test because the dynamic SSE takes the
    // bounding volume height into account, which differs for each bounding volume.
    it("uses dynamic screen space error for tileset with region", function () {
      return testDynamicScreenSpaceError(withTransformRegionUrl, 103.0);
    });

    it("uses dynamic screen space error for tileset with bounding sphere", function () {
      return testDynamicScreenSpaceError(withBoundingSphereUrl, 137.0);
    });

    it("uses dynamic screen space error for local tileset with box", function () {
      return testDynamicScreenSpaceError(withTransformBoxUrl, 103.0);
    });

    it("uses dynamic screen space error for local tileset with sphere", function () {
      return testDynamicScreenSpaceError(withTransformSphereUrl, 144.0);
    });

    it("additive refinement - selects root when sse is met", function () {
      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        // Meets screen space error, only root tile is rendered
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(1);
        expect(statistics.numberOfCommands).toEqual(1);
      });
    });

    it("additive refinement - selects all tiles when sse is not met", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        // Does not meet screen space error, all tiles are visible
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(5);
      });
    });

    it("additive refinement - use parent's geometric error on child's box for early refinement", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5);
        expect(statistics.numberOfCommands).toEqual(5);

        // Both right tiles don't meet the SSE anymore
        scene.camera.moveLeft(50.0);
        scene.renderForSpecs();
        expect(statistics.visited).toEqual(3);
        expect(statistics.numberOfCommands).toEqual(3);
      });
    });

    it("additive refinement - selects tile when inside viewer request volume", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithViewerRequestVolumeUrl
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        // Force root tile to always not meet SSE since this is just checking the request volume
        tileset.maximumScreenSpaceError = 0.0;

        // Renders all 5 tiles
        setZoom(20.0);
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(5);

        // No longer renders the tile with a request volume
        setZoom(1500.0);
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(4);
      });
    });

    it("replacement refinement - selects root when sse is met", function () {
      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.root.refine = Cesium3DTileRefine.REPLACE;

        // Meets screen space error, only root tile is rendered
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(1);
        expect(statistics.numberOfCommands).toEqual(1);
      });
    });

    it("replacement refinement - selects children when sse is not met", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.root.refine = Cesium3DTileRefine.REPLACE;

        // Does not meet screen space error, child tiles replace root tile
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(5); // Visits root, but does not render it
        expect(statistics.numberOfCommands).toEqual(4);
      });
    });

    it("replacement refinement - selects root when sse is not met and children are not ready", function () {
      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var root = tileset.root;
        root.refine = Cesium3DTileRefine.REPLACE;

        // Set zoom to start loading child tiles
        viewAllTiles();
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        // LOD skipping visits all visible
        expect(statistics.visited).toEqual(5);
        // no stencil clear command because only the root tile
        expect(statistics.numberOfCommands).toEqual(1);
        expect(statistics.numberOfPendingRequests).toEqual(4);
        expect(numberOfChildrenWithoutContent(root)).toEqual(4);
      });
    });

    it("replacement refinement - selects tile when inside viewer request volume", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithViewerRequestVolumeUrl,
        {
          skipLevelOfDetail: false,
        }
      ).then(function (tileset) {
        var statistics = tileset._statistics;

        var root = tileset.root;
        root.refine = Cesium3DTileRefine.REPLACE;
        root.hasEmptyContent = false; // mock content
        tileset.maximumScreenSpaceError = 0.0; // Force root tile to always not meet SSE since this is just checking the request volume

        // Renders all 5 tiles
        setZoom(20.0);
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(5);
        expect(isSelected(tileset, root)).toBe(false);

        // No longer renders the tile with a request volume
        setZoom(1500.0);
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(4);
        expect(isSelected(tileset, root)).toBe(true); // one child is no longer selected. root is chosen instead
      });
    });

    it("replacement refinement - selects upwards when traversal stops at empty tile", function () {
      // No children have content, but all grandchildren have content
      //
      //          C
      //      E       E
      //    C   C   C   C
      //
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement1Url
      ).then(function (tileset) {
        tileset.root.geometricError = 90;
        setZoom(80);
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.selected).toEqual(1);
        expect(statistics.visited).toEqual(3);
        expect(isSelected(tileset, tileset.root)).toBe(true);
      });
    });

    it("replacement refinement - selects root when sse is not met and subtree is not refinable (1)", function () {
      // No children have content, but all grandchildren have content
      //
      //          C
      //      E       E
      //    C   C   C   C
      //
      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement1Url
      ).then(function (tileset) {
        tileset.skipLevelOfDetail = false;
        viewAllTiles();
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        var root = tileset.root;

        // Even though root's children are loaded, the grandchildren need to be loaded before it becomes refinable
        expect(numberOfChildrenWithoutContent(root)).toEqual(0); // Children are loaded
        expect(statistics.numberOfCommands).toEqual(1); // No stencil or backface commands; no mixed content
        expect(statistics.numberOfPendingRequests).toEqual(4); // Loading grandchildren

        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            scene.renderForSpecs();
            expect(statistics.numberOfCommands).toEqual(4); // Render children
          }
        );
      });
    });

    it("replacement refinement - selects root when sse is not met and subtree is not refinable (2)", function () {
      // Check that the root is refinable once its child is loaded
      //
      //          C
      //          E
      //        C   E
      //            C (smaller geometric error)
      //

      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement2Url
      ).then(function (tileset) {
        tileset.skipLevelOfDetail = false;
        var statistics = tileset._statistics;
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(statistics.numberOfCommands).toEqual(1);

            setZoom(5.0); // Zoom into the last tile, when it is ready the root is refinable
            scene.renderForSpecs();

            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function () {
                expect(statistics.numberOfCommands).toEqual(2); // Renders two content tiles
              }
            );
          }
        );
      });
    });

    it("replacement refinement - selects root when sse is not met and subtree is not refinable (3)", function () {
      // Check that the root is refinable once its child is loaded
      //
      //          C
      //          T (external tileset ref)
      //          E (root of external tileset)
      //     C  C  C  C
      //

      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement3Url
      ).then(function (tileset) {
        tileset.skipLevelOfDetail = false;
        var statistics = tileset._statistics;
        var root = tileset.root;
        expect(statistics.numberOfCommands).toEqual(1);

        viewAllTiles();
        scene.renderForSpecs();
        return root.children[0].contentReadyPromise.then(function () {
          // The external tileset json is loaded, but the external tileset isn't.
          scene.renderForSpecs();
          expect(statistics.numberOfCommands).toEqual(1); // root
          expect(statistics.numberOfPendingRequests).toEqual(4); // Loading child content tiles

          return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
            function () {
              expect(isSelected(tileset, root)).toEqual(false);
              expect(statistics.numberOfCommands).toEqual(4); // Render child content tiles
            }
          );
        });
      });
    });

    it("replacement refinement - refines if descendant is empty leaf tile", function () {
      // Check that the root is refinable once its children with content are loaded
      //
      //          C
      //     C  C  C  E
      //
      viewAllTiles();
      var originalLoadJson = Cesium3DTileset.loadJson;
      spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
        return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
          tilesetJson.root.refine = "REPLACE";
          tilesetJson.root.children[3].content = undefined;
          return tilesetJson;
        });
      });

      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.skipLevelOfDetail = false;
        var statistics = tileset._statistics;
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(3);
      });
    });

    it("replacement and additive refinement", function () {
      //          A
      //      A       R (not rendered)
      //    R   A   R   A
      //
      return Cesium3DTilesTester.loadTileset(scene, tilesetRefinementMix).then(
        function (tileset) {
          var statistics = tileset._statistics;
          expect(statistics.visited).toEqual(7);
          expect(statistics.numberOfCommands).toEqual(6);
        }
      );
    });

    describe("children bound union optimization", function () {
      it("does not select visible tiles with invisible children", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetReplacementWithViewerRequestVolumeUrl
        ).then(function (tileset) {
          var center = Cartesian3.fromRadians(
            centerLongitude,
            centerLatitude,
            22.0
          );
          scene.camera.lookAt(center, new HeadingPitchRange(0.0, 1.57, 1.0));

          var root = tileset.root;
          var childRoot = root.children[0];

          scene.renderForSpecs();

          expect(
            childRoot.visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);

          expect(
            childRoot.children[0].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[1].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[2].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[3].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).toEqual(CullingVolume.MASK_OUTSIDE);

          expect(tileset._selectedTiles.length).toEqual(0);
          expect(isSelected(tileset, childRoot)).toBe(false);
        });
      });

      it("does not select external tileset whose root has invisible children", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetOfTilesetsUrl
        ).then(function (tileset) {
          var center = Cartesian3.fromRadians(
            centerLongitude,
            centerLatitude,
            50.0
          );
          scene.camera.lookAt(center, new HeadingPitchRange(0.0, 1.57, 1.0));
          var root = tileset.root;
          var externalRoot = root.children[0];
          externalRoot.refine = Cesium3DTileRefine.REPLACE;
          scene.renderForSpecs();

          expect(isSelected(tileset, root)).toBe(false);
          expect(isSelected(tileset, externalRoot)).toBe(false);
          expect(root._visible).toBe(false);
          expect(externalRoot._visible).toBe(false);
          expect(tileset.statistics.numberOfTilesCulledWithChildrenUnion).toBe(
            1
          );
        });
      });

      it("does not select visible tiles not meeting SSE with visible children", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetReplacementWithViewerRequestVolumeUrl
        ).then(function (tileset) {
          var root = tileset.root;
          var childRoot = root.children[0];
          childRoot.geometricError = 240;

          scene.renderForSpecs();

          expect(
            childRoot.visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);

          expect(
            childRoot.children[0].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[1].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[2].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[3].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);

          expect(isSelected(tileset, childRoot)).toBe(false);
        });
      });

      it("does select visible tiles meeting SSE with visible children", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetReplacementWithViewerRequestVolumeUrl
        ).then(function (tileset) {
          var root = tileset.root;
          var childRoot = root.children[0];

          childRoot.geometricError = 0; // child root should meet SSE and children should not be drawn
          scene.renderForSpecs();
          // wait for load because geometric error has changed
          return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
            function (tileset) {
              expect(
                childRoot.visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);

              expect(
                childRoot.children[0].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);
              expect(
                childRoot.children[1].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);
              expect(
                childRoot.children[2].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);
              expect(
                childRoot.children[3].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);

              expect(isSelected(tileset, childRoot)).toBe(true);
            }
          );
        });
      });

      it("does select visible tiles with visible children failing request volumes", function () {
        viewRootOnly();
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetReplacementWithViewerRequestVolumeUrl,
          {
            cullWithChildrenBounds: false,
          }
        ).then(function (tileset) {
          var root = tileset.root;
          var childRoot = root.children[0];

          expect(
            childRoot.visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);

          expect(
            childRoot.children[0].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[1].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[2].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);
          expect(
            childRoot.children[3].visibility(
              scene.frameState,
              CullingVolume.MASK_INDETERMINATE
            )
          ).not.toEqual(CullingVolume.MASK_OUTSIDE);

          expect(tileset._selectedTiles.length).toEqual(1);
          expect(isSelected(tileset, childRoot)).toBe(true);
        });
      });

      it("does select visible tiles with visible children passing request volumes", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetReplacementWithViewerRequestVolumeUrl
        ).then(function (tileset) {
          var root = tileset.root;
          var childRoot = root.children[0];
          childRoot.geometricError = 0;

          // wait for load because geometric error has changed
          return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
            function (tileset) {
              expect(
                childRoot.visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);

              expect(
                childRoot.children[0].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);
              expect(
                childRoot.children[1].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);
              expect(
                childRoot.children[2].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);
              expect(
                childRoot.children[3].visibility(
                  scene.frameState,
                  CullingVolume.MASK_INDETERMINATE
                )
              ).not.toEqual(CullingVolume.MASK_OUTSIDE);

              expect(tileset._selectedTiles.length).toEqual(1);
              expect(isSelected(tileset, childRoot)).toBe(true);

              childRoot.geometricError = 200;
              scene.renderForSpecs();
              expect(tileset._selectedTiles.length).toEqual(4);
              expect(isSelected(tileset, childRoot)).toBe(false);
            }
          );
        });
      });
    });

    it("loads tileset with external tileset JSON file", function () {
      // Set view so that no tiles are loaded initially
      viewNothing();

      return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(
        function (tileset) {
          // Root points to an external tileset JSON file and has no children until it is requested
          var root = tileset.root;
          expect(root.children.length).toEqual(0);

          // Set view so that root's content is requested
          viewRootOnly();
          scene.renderForSpecs();
          return root.contentReadyPromise.then(function () {
            expect(root.hasTilesetContent).toEqual(true);

            // Root has one child now, the root of the external tileset
            expect(root.children.length).toEqual(1);

            // Check that headers are equal
            var subtreeRoot = root.children[0];
            expect(root.refine).toEqual(subtreeRoot.refine);
            expect(root.contentBoundingVolume.boundingVolume).toEqual(
              subtreeRoot.contentBoundingVolume.boundingVolume
            );

            // Check that subtree root has 4 children
            expect(subtreeRoot.hasTilesetContent).toEqual(false);
            expect(subtreeRoot.children.length).toEqual(4);
          });
        }
      );
    });

    it("preserves query string with external tileset JSON file", function () {
      // Set view so that no tiles are loaded initially
      viewNothing();

      //Spy on loadWithXhr so we can verify requested urls
      spyOn(Resource._Implementations, "loadWithXhr").and.callThrough();

      var queryParams = "a=1&b=boy";
      var expectedUrl =
        "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json?" +
        queryParams;
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetOfTilesetsUrl + "?" + queryParams
      )
        .then(function (tileset) {
          //Make sure tileset JSON file was requested with query parameters
          expect(
            Resource._Implementations.loadWithXhr.calls.argsFor(0)[0]
          ).toEqual(expectedUrl);

          Resource._Implementations.loadWithXhr.calls.reset();

          // Set view so that root's content is requested
          viewRootOnly();
          scene.renderForSpecs();

          return tileset.root.contentReadyPromise;
        })
        .then(function () {
          //Make sure tileset2.json was requested with query parameters and does not use parent tilesetVersion
          expectedUrl = getAbsoluteUri(
            "Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json?v=1.2.3&" +
              queryParams
          );
          expect(
            Resource._Implementations.loadWithXhr.calls.argsFor(0)[0]
          ).toEqual(expectedUrl);
        });
    });

    it("renders tileset with external tileset JSON file", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(
        function (tileset) {
          var statistics = tileset._statistics;
          expect(statistics.visited).toEqual(7); // Visits two tiles with tileset content, five tiles with b3dm content
          expect(statistics.numberOfCommands).toEqual(5); // Render the five tiles with b3dm content
        }
      );
    });

    it("always visits external tileset root", function () {
      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(
        function (tileset) {
          var statistics = tileset._statistics;
          expect(statistics.visited).toEqual(2); // Visits external tileset tile, and external tileset root
          expect(statistics.numberOfCommands).toEqual(1); // Renders external tileset root
        }
      );
    });

    it("set tile color", function () {
      return Cesium3DTilesTester.loadTileset(scene, noBatchIdsUrl).then(
        function (tileset) {
          // Get initial color
          var color;
          Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
            color = rgba;
          });

          // Check for color
          tileset.root.color = Color.RED;
          Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
            expect(rgba).not.toEqual(color);
          });
        }
      );
    });

    it("debugFreezeFrame", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        viewRootOnly();
        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(1);
        expect(statistics.numberOfCommands).toEqual(1);

        tileset.debugFreezeFrame = true;
        viewAllTiles();
        scene.renderForSpecs();
        expect(statistics.visited).toEqual(0); // selectTiles returns early, so no tiles are visited
        expect(statistics.numberOfCommands).toEqual(1); // root tile is still in selectedTiles list
      });
    });

    function checkDebugColorizeTiles(url) {
      CesiumMath.setRandomNumberSeed(0);
      return Cesium3DTilesTester.loadTileset(scene, url).then(function (
        tileset
      ) {
        // Get initial color
        var color;
        Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
          color = rgba;
        });

        // Check for debug color
        tileset.debugColorizeTiles = true;
        Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
          expect(rgba).not.toEqual(color);
        });

        // Check for original color
        tileset.debugColorizeTiles = false;
        Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
          expect(rgba).toEqual(color);
        });
      });
    }

    it("debugColorizeTiles for b3dm with batch table", function () {
      return checkDebugColorizeTiles(withBatchTableUrl);
    });

    it("debugColorizeTiles for b3dm without batch table", function () {
      return checkDebugColorizeTiles(noBatchIdsUrl);
    });

    it("debugColorizeTiles for i3dm", function () {
      viewInstances();
      return checkDebugColorizeTiles(instancedUrl);
    });

    it("debugColorizeTiles for cmpt", function () {
      return checkDebugColorizeTiles(compositeUrl);
    });

    it("debugColorizeTiles for pnts with batch table", function () {
      viewPointCloud();
      return checkDebugColorizeTiles(pointCloudBatchedUrl);
    });

    it("debugColorizeTiles for pnts without batch table", function () {
      viewPointCloud();
      return checkDebugColorizeTiles(pointCloudUrl);
    });

    it("debugColorizeTiles for glTF", function () {
      viewGltfContent();
      return checkDebugColorizeTiles(gltfContentUrl);
    });

    it("debugWireframe", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        viewRootOnly();
        tileset.debugWireframe = true;
        scene.renderForSpecs();
        var commands = scene.frameState.commandList;
        var length = commands.length;
        var i;
        for (i = 0; i < length; ++i) {
          expect(commands[i].primitiveType).toEqual(PrimitiveType.LINES);
        }

        tileset.debugWireframe = false;
        scene.renderForSpecs();
        commands = scene.frameState.commandList;
        for (i = 0; i < length; ++i) {
          expect(commands[i].primitiveType).toEqual(PrimitiveType.TRIANGLES);
        }
      });
    });

    it("debugShowBoundingVolume", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        viewRootOnly();
        tileset.debugShowBoundingVolume = true;
        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(1);
        expect(statistics.numberOfCommands).toEqual(2); // Tile command + bounding volume command

        tileset.debugShowBoundingVolume = false;
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(1);
      });
    });

    it("debugShowContentBoundingVolume", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        viewRootOnly();
        tileset.debugShowContentBoundingVolume = true;
        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(1);
        expect(statistics.numberOfCommands).toEqual(2); // Tile command + bounding volume command

        tileset.debugShowContentBoundingVolume = false;
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(1);
      });
    });

    it("debugShowViewerRequestVolume", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithViewerRequestVolumeUrl
      ).then(function (tileset) {
        tileset.debugShowViewerRequestVolume = true;
        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(6); // 1 empty root tile + 4 b3dm tiles + 1 pnts tile
        expect(statistics.numberOfCommands).toEqual(6); // 5 tile commands + viewer request volume command

        tileset.debugShowViewerRequestVolume = false;
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(5);
      });
    });

    it("show tile debug labels with regions", function () {
      // tilesetUrl has bounding regions
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.debugShowGeometricError = true;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).toBeDefined();
        expect(tileset._tileDebugLabels.length).toEqual(5);

        var root = tileset.root;
        expect(tileset._tileDebugLabels._labels[0].text).toEqual(
          "Geometric error: " + root.geometricError
        );
        expect(tileset._tileDebugLabels._labels[1].text).toEqual(
          "Geometric error: " + root.children[0].geometricError
        );
        expect(tileset._tileDebugLabels._labels[2].text).toEqual(
          "Geometric error: " + root.children[1].geometricError
        );
        expect(tileset._tileDebugLabels._labels[3].text).toEqual(
          "Geometric error: " + root.children[2].geometricError
        );
        expect(tileset._tileDebugLabels._labels[4].text).toEqual(
          "Geometric error: " + root.children[3].geometricError
        );

        tileset.debugShowGeometricError = false;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).not.toBeDefined();
      });
    });

    it("show tile debug labels with boxes", function () {
      // tilesetWithTransformsUrl has bounding boxes
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithTransformsUrl
      ).then(function (tileset) {
        tileset.debugShowGeometricError = true;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).toBeDefined();
        expect(tileset._tileDebugLabels.length).toEqual(2);

        var root = tileset.root;
        expect(tileset._tileDebugLabels._labels[0].text).toEqual(
          "Geometric error: " + root.geometricError
        );
        expect(tileset._tileDebugLabels._labels[1].text).toEqual(
          "Geometric error: " + root.children[0].geometricError
        );

        tileset.debugShowGeometricError = false;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).not.toBeDefined();
      });
    });

    it("show tile debug labels with bounding spheres", function () {
      // tilesetWithViewerRequestVolumeUrl has bounding sphere
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithViewerRequestVolumeUrl
      ).then(function (tileset) {
        tileset.debugShowGeometricError = true;
        scene.renderForSpecs();

        var length = tileset._selectedTiles.length;
        expect(tileset._tileDebugLabels).toBeDefined();
        expect(tileset._tileDebugLabels.length).toEqual(length);

        for (var i = 0; i < length; ++i) {
          expect(tileset._tileDebugLabels._labels[i].text).toEqual(
            "Geometric error: " + tileset._selectedTiles[i].geometricError
          );
        }

        tileset.debugShowGeometricError = false;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).not.toBeDefined();
      });
    });

    it("show tile debug labels with rendering statistics", function () {
      // tilesetUrl has bounding regions
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.debugShowRenderingStatistics = true;
        viewRootOnly();
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).toBeDefined();
        expect(tileset._tileDebugLabels.length).toEqual(1);

        var content = tileset.root.content;
        var expected =
          "Commands: " +
          tileset.root.commandsLength +
          "\n" +
          "Triangles: " +
          content.trianglesLength +
          "\n" +
          "Features: " +
          content.featuresLength;

        expect(tileset._tileDebugLabels._labels[0].text).toEqual(expected);

        tileset.debugShowRenderingStatistics = false;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).not.toBeDefined();
      });
    });

    it("show tile debug labels with memory usage", function () {
      // tilesetUrl has bounding regions
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.debugShowMemoryUsage = true;
        viewRootOnly();
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).toBeDefined();
        expect(tileset._tileDebugLabels.length).toEqual(1);

        var expected = "Texture Memory: 0\n" + "Geometry Memory: 0.007";

        expect(tileset._tileDebugLabels._labels[0].text).toEqual(expected);

        tileset.debugShowMemoryUsage = false;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).not.toBeDefined();
      });
    });

    it("show tile debug labels with all statistics", function () {
      // tilesetUrl has bounding regions
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.debugShowGeometricError = true;
        tileset.debugShowRenderingStatistics = true;
        tileset.debugShowMemoryUsage = true;
        tileset.debugShowUrl = true;
        viewRootOnly();
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).toBeDefined();

        var expected =
          "Geometric error: 70\n" +
          "Commands: 1\n" +
          "Triangles: 120\n" +
          "Features: 10\n" +
          "Texture Memory: 0\n" +
          "Geometry Memory: 0.007\n" +
          "Url: parent.b3dm";
        expect(tileset._tileDebugLabels._labels[0].text).toEqual(expected);

        tileset.debugShowGeometricError = false;
        tileset.debugShowRenderingStatistics = false;
        tileset.debugShowMemoryUsage = false;
        tileset.debugShowUrl = false;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).not.toBeDefined();
      });
    });

    it("show only picked tile debug label with all stats", function () {
      // tilesetUrl has bounding regions
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.debugShowGeometricError = true;
        tileset.debugShowRenderingStatistics = true;
        tileset.debugShowMemoryUsage = true;
        tileset.debugShowUrl = true;
        tileset.debugPickedTileLabelOnly = true;

        var scratchPosition = new Cartesian3(1.0, 1.0, 1.0);
        tileset.debugPickedTile = tileset.root;
        tileset.debugPickPosition = scratchPosition;

        scene.renderForSpecs();
        expect(tileset._tileDebugLabels).toBeDefined();

        var expected =
          "Geometric error: 70\n" +
          "Commands: 1\n" +
          "Triangles: 120\n" +
          "Features: 10\n" +
          "Texture Memory: 0\n" +
          "Geometry Memory: 0.007\n" +
          "Url: parent.b3dm";
        expect(tileset._tileDebugLabels.get(0).text).toEqual(expected);
        expect(tileset._tileDebugLabels.get(0).position).toEqual(
          scratchPosition
        );

        tileset.debugPickedTile = undefined;
        scene.renderForSpecs();
        expect(tileset._tileDebugLabels.length).toEqual(0);
      });
    });

    it("does not request tiles when picking", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        viewRootOnly();
        scene.pickForSpecs();
        expect(tileset._statistics.numberOfPendingRequests).toEqual(0);
        scene.renderForSpecs();
        expect(tileset._statistics.numberOfPendingRequests).toEqual(1);
      });
    });

    it("does not process tiles when picking", function () {
      var spy = spyOn(Cesium3DTile.prototype, "process").and.callThrough();

      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        viewRootOnly();
        scene.renderForSpecs(); // Request root
        expect(tileset._statistics.numberOfPendingRequests).toEqual(1);
        return tileset.root.contentReadyToProcessPromise.then(function () {
          scene.pickForSpecs();
          expect(spy).not.toHaveBeenCalled();
          scene.renderForSpecs();
          expect(spy).toHaveBeenCalled();
        });
      });
    });

    it("does not request tiles when the request scheduler is full", function () {
      viewRootOnly(); // Root tiles are loaded initially
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl, {
        skipLevelOfDetail: false,
      }).then(function (tileset) {
        // Try to load 4 children. Only 3 requests will go through, 1 will be attempted.
        var oldMaximumRequestsPerServer =
          RequestScheduler.maximumRequestsPerServer;
        RequestScheduler.maximumRequestsPerServer = 3;

        viewAllTiles();
        scene.renderForSpecs();

        expect(tileset._statistics.numberOfPendingRequests).toEqual(3);
        expect(tileset._statistics.numberOfAttemptedRequests).toEqual(1);

        RequestScheduler.maximumRequestsPerServer = oldMaximumRequestsPerServer;
      });
    });

    it("load progress events are raised", function () {
      // [numberOfPendingRequests, numberOfTilesProcessing]
      var results = [
        [1, 0],
        [0, 1],
        [0, 0],
      ];
      var spyUpdate = jasmine.createSpy("listener");

      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.loadProgress.addEventListener(spyUpdate);
        viewRootOnly();
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(spyUpdate.calls.count()).toEqual(3);
            expect(spyUpdate.calls.allArgs()).toEqual(results);
          }
        );
      });
    });

    it("tilesLoaded", function () {
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      expect(tileset.tilesLoaded).toBe(false);
      tileset.readyPromise.then(function () {
        expect(tileset.tilesLoaded).toBe(false);
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(tileset.tilesLoaded).toBe(true);
          }
        );
      });
    });

    it("all tiles loaded event is raised", function () {
      // Called first when only the root is visible and it becomes loaded, and then again when
      // the rest of the tileset is visible and all tiles are loaded.
      var spyUpdate1 = jasmine.createSpy("listener");
      var spyUpdate2 = jasmine.createSpy("listener");
      viewRootOnly();
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      tileset.allTilesLoaded.addEventListener(spyUpdate1);
      tileset.initialTilesLoaded.addEventListener(spyUpdate2);
      return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
        function () {
          viewAllTiles();
          return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
            function () {
              expect(spyUpdate1.calls.count()).toEqual(2);
              expect(spyUpdate2.calls.count()).toEqual(1);
            }
          );
        }
      );
    });

    it("tile visible event is raised", function () {
      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var spyUpdate = jasmine.createSpy("listener");
        tileset.tileVisible.addEventListener(spyUpdate);
        scene.renderForSpecs();
        expect(
          tileset.root.visibility(
            scene.frameState,
            CullingVolume.MASK_INDETERMINATE
          )
        ).not.toEqual(CullingVolume.MASK_OUTSIDE);
        expect(spyUpdate.calls.count()).toEqual(1);
        expect(spyUpdate.calls.argsFor(0)[0]).toBe(tileset.root);
      });
    });

    it("tile load event is raised", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var spyUpdate = jasmine.createSpy("listener");
        tileset.tileLoad.addEventListener(spyUpdate);
        tileset.maximumMemoryUsage = 0;
        viewRootOnly();
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            // Root is loaded
            expect(spyUpdate.calls.count()).toEqual(1);
            expect(spyUpdate.calls.argsFor(0)[0]).toBe(tileset.root);
            spyUpdate.calls.reset();

            // Unload from cache
            viewNothing();
            scene.renderForSpecs();
            expect(tileset.statistics.numberOfTilesWithContentReady).toEqual(0);

            // Look at root again
            viewRootOnly();
            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function () {
                expect(spyUpdate.calls.count()).toEqual(1);
                expect(spyUpdate.calls.argsFor(0)[0]).toBe(tileset.root);
              }
            );
          }
        );
      });
    });

    it("tile failed event is raised", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        spyOn(Resource._Implementations, "loadWithXhr").and.callFake(function (
          url,
          responseType,
          method,
          data,
          headers,
          deferred,
          overrideMimeType
        ) {
          deferred.reject("404");
        });
        var spyUpdate = jasmine.createSpy("listener");
        tileset.tileFailed.addEventListener(spyUpdate);
        tileset.maximumMemoryUsage = 0;
        viewRootOnly();
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(spyUpdate.calls.count()).toEqual(1);

            var arg = spyUpdate.calls.argsFor(0)[0];
            expect(arg).toBeDefined();
            expect(arg.url).toContain("parent.b3dm");
            expect(arg.message).toBeDefined();
          }
        );
      });
    });

    it("destroys", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var root = tileset.root;
        expect(tileset.isDestroyed()).toEqual(false);
        scene.primitives.remove(tileset);
        expect(tileset.isDestroyed()).toEqual(true);

        // Check that all tiles are destroyed
        expect(root.isDestroyed()).toEqual(true);
        expect(root.children[0].isDestroyed()).toEqual(true);
        expect(root.children[1].isDestroyed()).toEqual(true);
        expect(root.children[2].isDestroyed()).toEqual(true);
        expect(root.children[3].isDestroyed()).toEqual(true);
      });
    });

    it("destroys before external tileset JSON file finishes loading", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(
        function (tileset) {
          var root = tileset.root;

          viewRootOnly();
          scene.renderForSpecs(); // Request external tileset JSON file

          var statistics = tileset._statistics;
          expect(statistics.numberOfPendingRequests).toEqual(1);
          scene.primitives.remove(tileset);

          return root.contentReadyPromise
            .then(function (root) {
              fail("should not resolve");
            })
            .otherwise(function (error) {
              // Expect the root to not have added any children from the external tileset JSON file
              expect(root.children.length).toEqual(0);
            });
        }
      );
    });

    it("destroys before tile finishes loading", function () {
      viewRootOnly();
      options.url = tilesetUrl;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      return tileset.readyPromise.then(function (tileset) {
        var root = tileset.root;
        scene.renderForSpecs(); // Request root
        scene.primitives.remove(tileset);

        return root.contentReadyPromise
          .then(function (content) {
            fail("should not resolve");
          })
          .otherwise(function (error) {
            expect(root._contentState).toBe(Cesium3DTileContentState.FAILED);
          });
      });
    });

    it("renders with imageBaseLightingFactor", function () {
      var renderOptions = {
        scene: scene,
        time: new JulianDate(2457522.154792),
      };
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          expect(renderOptions).toRenderAndCall(function (rgba) {
            expect(rgba).not.toEqual([0, 0, 0, 255]);
            tileset.imageBasedLightingFactor = new Cartesian2(0.0, 0.0);
            expect(renderOptions).notToRender(rgba);
          });
        }
      );
    });

    it("renders with lightColor", function () {
      var renderOptions = {
        scene: scene,
        time: new JulianDate(2457522.154792),
      };
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          expect(renderOptions).toRenderAndCall(function (rgba) {
            expect(rgba).not.toEqual([0, 0, 0, 255]);
            tileset.imageBasedLightingFactor = new Cartesian2(0.0, 0.0);
            expect(renderOptions).toRenderAndCall(function (rgba2) {
              expect(rgba2).not.toEqual(rgba);
              tileset.lightColor = new Cartesian3(5.0, 5.0, 5.0);
              expect(renderOptions).notToRender(rgba2);
            });
          });
        }
      );
    });

    function testBackFaceCulling(url, setViewOptions) {
      var renderOptions = {
        scene: scene,
        time: new JulianDate(2457522.154792),
      };
      return Cesium3DTilesTester.loadTileset(scene, url).then(function (
        tileset
      ) {
        scene.camera.setView(setViewOptions);
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba).toEqual([0, 0, 0, 255]);
          tileset.backFaceCulling = false;
          expect(renderOptions).toRenderAndCall(function (rgba2) {
            expect(rgba2).not.toEqual(rgba);
          });
        });
      });
    }

    it("renders b3dm tileset when back face culling is disabled", function () {
      var setViewOptions = {
        destination: new Cartesian3(
          1215012.6853779217,
          -4736313.101374343,
          4081603.4657718465
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179584,
          -0.49999825387267993,
          6.283185307179586
        ),
        endTransform: Matrix4.IDENTITY,
      };

      return testBackFaceCulling(withoutBatchTableUrl, setViewOptions);
    });

    it("renders glTF tileset when back face culling is disabled", function () {
      var setViewOptions = {
        destination: new Cartesian3(
          1215012.6853779217,
          -4736313.101374343,
          4081603.4657718465
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179584,
          -0.49999825387267993,
          6.283185307179586
        ),
        endTransform: Matrix4.IDENTITY,
      };

      return testBackFaceCulling(gltfContentUrl, setViewOptions);
    });

    it("renders i3dm tileset when back face culling is disabled", function () {
      var setViewOptions = {
        destination: new Cartesian3(
          1215015.8599828142,
          -4736324.65638894,
          4081609.967056947
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179585,
          -0.5000006393986758,
          6.283185307179586
        ),
        endTransform: Matrix4.IDENTITY,
      };

      return testBackFaceCulling(instancedUrl, setViewOptions);
    });

    ///////////////////////////////////////////////////////////////////////////
    // Styling tests

    it("applies show style to a tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          var hideStyle = new Cesium3DTileStyle({ show: "false" });
          tileset.style = hideStyle;
          expect(tileset.style).toBe(hideStyle);
          expect(scene).toRender([0, 0, 0, 255]);

          tileset.style = new Cesium3DTileStyle({ show: "true" });
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    it("applies show style to a tileset without features", function () {
      return Cesium3DTilesTester.loadTileset(scene, noBatchIdsUrl).then(
        function (tileset) {
          var hideStyle = new Cesium3DTileStyle({ show: "false" });
          tileset.style = hideStyle;
          expect(tileset.style).toBe(hideStyle);
          expect(scene).toRender([0, 0, 0, 255]);

          tileset.style = new Cesium3DTileStyle({ show: "true" });
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    it("applies style with complex show expression to a tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          // Each feature in the b3dm file has an id property from 0 to 9
          // ${id} >= 10 will always evaluate to false
          tileset.style = new Cesium3DTileStyle({ show: "${id} >= 50 * 2" });
          expect(scene).toRender([0, 0, 0, 255]);

          // ${id} < 10 will always evaluate to true
          tileset.style = new Cesium3DTileStyle({ show: "${id} < 200 / 2" });
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    it("applies show style to a tileset with a composite tile", function () {
      return Cesium3DTilesTester.loadTileset(scene, compositeUrl).then(
        function (tileset) {
          tileset.style = new Cesium3DTileStyle({ show: "false" });
          expect(scene).toRender([0, 0, 0, 255]);

          tileset.style = new Cesium3DTileStyle({ show: "true" });
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    it("applies show style to a tileset with glTF content", function () {
      return Cesium3DTilesTester.loadTileset(scene, gltfContentUrl).then(
        function (tileset) {
          viewGltfContent();
          var hideStyle = new Cesium3DTileStyle({ show: "false" });
          tileset.style = hideStyle;
          expect(tileset.style).toBe(hideStyle);
          expect(scene).toRender([0, 0, 0, 255]);

          tileset.style = new Cesium3DTileStyle({ show: "true" });
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    function expectColorStyle(tileset) {
      var color;
      expect(scene).toRenderAndCall(function (rgba) {
        color = rgba;
      });

      tileset.style = new Cesium3DTileStyle({ color: 'color("blue")' });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toBeGreaterThan(0);
        expect(rgba[3]).toEqual(255);
      });

      // set color to transparent
      tileset.style = new Cesium3DTileStyle({ color: 'color("blue", 0.0)' });
      expect(scene).toRender([0, 0, 0, 255]);

      tileset.style = new Cesium3DTileStyle({ color: 'color("cyan")' });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toEqual(0);
        expect(rgba[1]).toBeGreaterThan(0);
        expect(rgba[2]).toBeGreaterThan(0);
        expect(rgba[3]).toEqual(255);
      });

      // Remove style
      tileset.style = undefined;
      expect(scene).toRender(color);
    }

    it("applies color style to a tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          expectColorStyle(tileset);
        }
      );
    });

    it("applies color style to a tileset with translucent tiles", function () {
      return Cesium3DTilesTester.loadTileset(scene, translucentUrl).then(
        function (tileset) {
          expectColorStyle(tileset);
        }
      );
    });

    it("applies color style to a tileset with translucent and opaque tiles", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        translucentOpaqueMixUrl
      ).then(function (tileset) {
        expectColorStyle(tileset);
      });
    });

    it("applies color style to tileset without features", function () {
      return Cesium3DTilesTester.loadTileset(scene, noBatchIdsUrl).then(
        function (tileset) {
          expectColorStyle(tileset);
        }
      );
    });

    it("applies color style to tileset with glTF content", function () {
      return Cesium3DTilesTester.loadTileset(scene, gltfContentUrl).then(
        function (tileset) {
          viewGltfContent();
          expectColorStyle(tileset);
        }
      );
    });

    it("applies style when feature properties change", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          // Initially, all feature ids are less than 10
          tileset.style = new Cesium3DTileStyle({ show: "${id} < 10" });
          expect(scene).notToRender([0, 0, 0, 255]);

          // Change feature ids so the show expression will evaluate to false
          var content = tileset.root.content;
          var length = content.featuresLength;
          var i;
          var feature;
          for (i = 0; i < length; ++i) {
            feature = content.getFeature(i);
            feature.setProperty("id", feature.getProperty("id") + 10);
          }
          expect(scene).toRender([0, 0, 0, 255]);

          // Change ids back
          for (i = 0; i < length; ++i) {
            feature = content.getFeature(i);
            feature.setProperty("id", feature.getProperty("id") - 10);
          }
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    it("applies style when tile is selected after new style is applied", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          var feature = tileset.root.content.getFeature(0);
          tileset.style = new Cesium3DTileStyle({ color: 'color("red")' });
          scene.renderForSpecs();
          expect(feature.color).toEqual(Color.RED);

          tileset.style = new Cesium3DTileStyle({ color: 'color("blue")' });
          scene.renderForSpecs();
          expect(feature.color).toEqual(Color.BLUE);

          viewNothing();
          tileset.style = new Cesium3DTileStyle({ color: 'color("lime")' });
          scene.renderForSpecs();
          expect(feature.color).toEqual(Color.BLUE); // Hasn't been selected yet

          viewAllTiles();
          scene.renderForSpecs();
          expect(feature.color).toEqual(Color.LIME);

          // Feature's show property is preserved if the style hasn't changed and the feature is newly selected
          feature.show = false;
          scene.renderForSpecs();
          expect(feature.show).toBe(false);
          viewNothing();
          scene.renderForSpecs();
          expect(feature.show).toBe(false);
          viewAllTiles();
          expect(feature.show).toBe(false);
        }
      );
    });

    it("does not reapply style during pick pass", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          tileset.style = new Cesium3DTileStyle({ color: 'color("red")' });
          scene.renderForSpecs();
          expect(
            tileset._statisticsPerPass[Cesium3DTilePass.RENDER]
              .numberOfTilesStyled
          ).toBe(1);
          scene.pickForSpecs();
          expect(
            tileset._statisticsPerPass[Cesium3DTilePass.PICK]
              .numberOfTilesStyled
          ).toBe(0);
        }
      );
    });

    it("applies style with complex color expression to a tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          // Each feature in the b3dm file has an id property from 0 to 9
          // ${id} >= 10 will always evaluate to false
          tileset.style = new Cesium3DTileStyle({
            color: '(${id} >= 50 * 2) ? color("red") : color("blue")',
          });
          expect(scene).toRenderAndCall(function (rgba) {
            expect(rgba[0]).toEqual(0);
            expect(rgba[1]).toEqual(0);
            expect(rgba[2]).toBeGreaterThan(0);
            expect(rgba[3]).toEqual(255);
          });

          // ${id} < 10 will always evaluate to true
          tileset.style = new Cesium3DTileStyle({
            color: '(${id} < 50 * 2) ? color("red") : color("blue")',
          });
          expect(scene).toRenderAndCall(function (rgba) {
            expect(rgba[0]).toBeGreaterThan(0);
            expect(rgba[1]).toEqual(0);
            expect(rgba[2]).toEqual(0);
            expect(rgba[3]).toEqual(255);
          });
        }
      );
    });

    it("applies conditional color style to a tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          // ${id} < 10 will always evaluate to true
          tileset.style = new Cesium3DTileStyle({
            color: {
              conditions: [
                ["${id} < 10", 'color("red")'],
                ["true", 'color("blue")'],
              ],
            },
          });
          expect(scene).toRenderAndCall(function (rgba) {
            expect(rgba[0]).toBeGreaterThan(0);
            expect(rgba[1]).toEqual(0);
            expect(rgba[2]).toEqual(0);
            expect(rgba[3]).toEqual(255);
          });

          // ${id}>= 10 will always evaluate to false
          tileset.style = new Cesium3DTileStyle({
            color: {
              conditions: [
                ["${id} >= 10", 'color("red")'],
                ["true", 'color("blue")'],
              ],
            },
          });
          expect(scene).toRenderAndCall(function (rgba) {
            expect(rgba[0]).toEqual(0);
            expect(rgba[1]).toEqual(0);
            expect(rgba[2]).toBeGreaterThan(0);
            expect(rgba[3]).toEqual(255);
          });
        }
      );
    });

    it("handle else case when applying conditional color style to a tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          tileset.style = new Cesium3DTileStyle({
            color: {
              conditions: [["${id} > 0", 'color("black")']],
            },
          });
          scene.renderForSpecs();
          expect(tileset.root.content.getFeature(0).color).toEqual(Color.WHITE);
          expect(tileset.root.content.getFeature(1).color).toEqual(Color.BLACK);
        }
      );
    });

    it("handle else case when applying conditional show to a tileset", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          tileset.style = new Cesium3DTileStyle({
            show: {
              conditions: [["${id} > 0", "true"]],
            },
          });
          scene.renderForSpecs();
          expect(tileset.root.content.getFeature(0).show).toBe(true);
          expect(tileset.root.content.getFeature(1).show).toBe(true);

          tileset.style = new Cesium3DTileStyle({
            show: {
              conditions: [["${id} > 0", "false"]],
            },
          });
          scene.renderForSpecs();
          expect(tileset.root.content.getFeature(0).show).toBe(true);
          expect(tileset.root.content.getFeature(1).show).toBe(false);
        }
      );
    });

    it("loads style from uri", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          // ${id} < 10 will always evaluate to true
          tileset.style = new Cesium3DTileStyle(styleUrl);
          return tileset.style.readyPromise
            .then(function (style) {
              expect(scene).toRenderAndCall(function (rgba) {
                expect(rgba[0]).toBeGreaterThan(0);
                expect(rgba[1]).toEqual(0);
                expect(rgba[2]).toEqual(0);
                expect(rgba[3]).toEqual(255);
              });
            })
            .otherwise(function (error) {
              expect(error).not.toBeDefined();
            });
        }
      );
    });

    it("applies custom style to a tileset", function () {
      var style = new Cesium3DTileStyle();
      style.show = {
        evaluate: function (feature) {
          return this._value;
        },
        _value: false,
      };
      style.color = {
        evaluateColor: function (feature, result) {
          return Color.clone(Color.WHITE, result);
        },
      };

      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          tileset.style = style;
          expect(tileset.style).toBe(style);
          expect(scene).toRender([0, 0, 0, 255]);

          style.show._value = true;
          tileset.makeStyleDirty();
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    it("doesn't re-evaluate style during the next update", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          tileset.show = false;
          tileset.preloadWhenHidden = true;
          tileset.style = new Cesium3DTileStyle({ color: 'color("red")' });
          scene.renderForSpecs();

          var statistics = tileset._statisticsPerPass[Cesium3DTilePass.PRELOAD];
          expect(statistics.numberOfTilesStyled).toBe(1);

          scene.renderForSpecs();
          expect(statistics.numberOfTilesStyled).toBe(0);
        }
      );
    });

    it("doesn't re-evaluate style if the style being set is the same as the active style", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          var style = new Cesium3DTileStyle({ color: 'color("red")' });
          tileset.style = style;
          scene.renderForSpecs();

          var statistics = tileset._statisticsPerPass[Cesium3DTilePass.RENDER];
          expect(statistics.numberOfTilesStyled).toBe(1);

          tileset.style = style;
          scene.renderForSpecs();
          expect(statistics.numberOfTilesStyled).toBe(0);
        }
      );
    });

    function testColorBlendMode(url) {
      return Cesium3DTilesTester.loadTileset(scene, url).then(function (
        tileset
      ) {
        tileset.luminanceAtZenith = undefined;

        // Check that the feature is red
        var sourceRed;
        var sourceGreen;
        var renderOptions = {
          scene: scene,
          time: new JulianDate(2457522.154792),
        };
        expect(renderOptions).toRenderAndCall(function (rgba) {
          sourceRed = rgba[0];
          sourceGreen = rgba[1];
        });

        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(200);
          expect(rgba[1]).toBeLessThan(25);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Use HIGHLIGHT blending
        tileset.colorBlendMode = Cesium3DTileColorBlendMode.HIGHLIGHT;

        // Style with dark yellow. Expect the red channel to be darker than before.
        tileset.style = new Cesium3DTileStyle({
          color: "rgb(128, 128, 0)",
        });
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(100);
          expect(rgba[0]).toBeLessThan(sourceRed);
          expect(rgba[1]).toBeLessThan(25);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Style with yellow + alpha. Expect the red channel to be darker than before.
        tileset.style = new Cesium3DTileStyle({
          color: "rgba(255, 255, 0, 0.5)",
        });
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(100);
          expect(rgba[0]).toBeLessThan(sourceRed);
          expect(rgba[1]).toBeLessThan(25);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Use REPLACE blending
        tileset.colorBlendMode = Cesium3DTileColorBlendMode.REPLACE;

        // Style with dark yellow. Expect the red and green channels to be roughly dark yellow.
        tileset.style = new Cesium3DTileStyle({
          color: "rgb(128, 128, 0)",
        });
        var replaceRed;
        var replaceGreen;
        expect(renderOptions).toRenderAndCall(function (rgba) {
          replaceRed = rgba[0];
          replaceGreen = rgba[1];
          expect(rgba[0]).toBeGreaterThan(100);
          expect(rgba[0]).toBeLessThan(255);
          expect(rgba[1]).toBeGreaterThan(100);
          expect(rgba[1]).toBeLessThan(255);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Style with yellow + alpha. Expect the red and green channels to be a shade of yellow.
        tileset.style = new Cesium3DTileStyle({
          color: "rgba(255, 255, 0, 0.5)",
        });
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(100);
          expect(rgba[0]).toBeLessThan(255);
          expect(rgba[1]).toBeGreaterThan(100);
          expect(rgba[1]).toBeLessThan(255);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Use MIX blending
        tileset.colorBlendMode = Cesium3DTileColorBlendMode.MIX;
        tileset.colorBlendAmount = 0.5;

        // Style with dark yellow. Expect color to be a mix of the source and style colors.
        tileset.style = new Cesium3DTileStyle({
          color: "rgb(128, 128, 0)",
        });
        var mixRed;
        var mixGreen;
        expect(renderOptions).toRenderAndCall(function (rgba) {
          mixRed = rgba[0];
          mixGreen = rgba[1];
          expect(rgba[0]).toBeGreaterThan(replaceRed);
          expect(rgba[0]).toBeLessThan(sourceRed);
          expect(rgba[1]).toBeGreaterThan(sourceGreen);
          expect(rgba[1]).toBeLessThan(replaceGreen);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Set colorBlendAmount to 0.25. Expect color to be closer to the source color.
        tileset.colorBlendAmount = 0.25;
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(mixRed);
          expect(rgba[0]).toBeLessThan(sourceRed);
          expect(rgba[1]).toBeGreaterThan(0);
          expect(rgba[1]).toBeLessThan(mixGreen);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Set colorBlendAmount to 0.0. Expect color to equal the source color
        tileset.colorBlendAmount = 0.0;
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toEqual(sourceRed);
          expect(rgba[1]).toBeLessThan(25);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Set colorBlendAmount to 1.0. Expect color to equal the style color
        tileset.colorBlendAmount = 1.0;
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toEqual(replaceRed);
          expect(rgba[1]).toEqual(replaceGreen);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });

        // Style with yellow + alpha. Expect color to be a mix of the source and style colors.
        tileset.colorBlendAmount = 0.5;
        tileset.style = new Cesium3DTileStyle({
          color: "rgba(255, 255, 0, 0.5)",
        });
        expect(renderOptions).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(0);
          expect(rgba[1]).toBeGreaterThan(0);
          expect(rgba[2]).toBeLessThan(25);
          expect(rgba[3]).toEqual(255);
        });
      });
    }

    it("sets colorBlendMode", function () {
      return testColorBlendMode(colorsUrl);
    });

    it("sets colorBlendMode when vertex texture fetch is not supported", function () {
      // Disable VTF
      var maximumVertexTextureImageUnits =
        ContextLimits.maximumVertexTextureImageUnits;
      ContextLimits._maximumVertexTextureImageUnits = 0;
      return testColorBlendMode(colorsUrl).then(function () {
        // Re-enable VTF
        ContextLimits._maximumVertexTextureImageUnits = maximumVertexTextureImageUnits;
      });
    });

    it("sets colorBlendMode for textured tileset", function () {
      return testColorBlendMode(texturedUrl);
    });

    it("sets colorBlendMode for instanced tileset", function () {
      viewInstances();
      return testColorBlendMode(instancedRedMaterialUrl);
    });

    it("sets colorBlendMode for vertex color tileset", function () {
      return testColorBlendMode(batchedVertexColorsUrl);
    });

    ///////////////////////////////////////////////////////////////////////////
    // Cache replacement tests

    it("Unload all cached tiles not required to meet SSE using maximumMemoryUsage", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.maximumMemoryUsage = 0;

        // Render parent and four children (using additive refinement)
        viewAllTiles();
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.numberOfCommands).toEqual(5);
        expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles
        expect(tileset.totalMemoryUsageInBytes).toEqual(37200); // Specific to this tileset

        // Zoom out so only root tile is needed to meet SSE.  This unloads
        // the four children since the maximum memory usage is zero.
        viewRootOnly();
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(1);
        expect(statistics.numberOfTilesWithContentReady).toEqual(1);
        expect(tileset.totalMemoryUsageInBytes).toEqual(7440); // Specific to this tileset

        // Zoom back in so all four children are re-requested.
        viewAllTiles();

        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(statistics.numberOfCommands).toEqual(5);
            expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles
            expect(tileset.totalMemoryUsageInBytes).toEqual(37200); // Specific to this tileset
          }
        );
      });
    });

    it("Unload some cached tiles not required to meet SSE using maximumMemoryUsage", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.maximumMemoryUsage = 0.025; // Just enough memory to allow 3 tiles to remain
        // Render parent and four children (using additive refinement)
        viewAllTiles();
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.numberOfCommands).toEqual(5);
        expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles

        // Zoom out so only root tile is needed to meet SSE.  This unloads
        // two of the four children so three tiles are still loaded (the
        // root and two children) since the maximum memory usage is sufficient.
        viewRootOnly();
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(1);
        expect(statistics.numberOfTilesWithContentReady).toEqual(3);

        // Zoom back in so the two children are re-requested.
        viewAllTiles();

        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(statistics.numberOfCommands).toEqual(5);
            expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles
          }
        );
      });
    });

    it("Unloads cached tiles outside of the view frustum using maximumMemoryUsage", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.maximumMemoryUsage = 0;

        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.numberOfCommands).toEqual(5);
        expect(statistics.numberOfTilesWithContentReady).toEqual(5);

        viewSky();

        // All tiles are unloaded
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toEqual(0);
        expect(statistics.numberOfTilesWithContentReady).toEqual(0);

        // Reset camera so all tiles are reloaded
        viewAllTiles();

        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(statistics.numberOfCommands).toEqual(5);
            expect(statistics.numberOfTilesWithContentReady).toEqual(5);
          }
        );
      });
    });

    it("Unloads cached tiles in a tileset with external tileset JSON file using maximumMemoryUsage", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(
        function (tileset) {
          var statistics = tileset._statistics;
          var cacheList = tileset._cache._list;

          tileset.maximumMemoryUsage = 0.02;

          scene.renderForSpecs();
          expect(statistics.numberOfCommands).toEqual(5);
          expect(statistics.numberOfTilesWithContentReady).toEqual(5);
          expect(cacheList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel.

          // Zoom out so only root tile is needed to meet SSE.  This unloads
          // all tiles except the root and one of the b3dm children
          viewRootOnly();
          scene.renderForSpecs();

          expect(statistics.numberOfCommands).toEqual(1);
          expect(statistics.numberOfTilesWithContentReady).toEqual(2);
          expect(cacheList.length - 1).toEqual(2);

          // Reset camera so all tiles are reloaded
          viewAllTiles();

          return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
            function () {
              expect(statistics.numberOfCommands).toEqual(5);
              expect(statistics.numberOfTilesWithContentReady).toEqual(5);

              expect(cacheList.length - 1).toEqual(5);
            }
          );
        }
      );
    });

    it("Unloads cached tiles in a tileset with empty tiles using maximumMemoryUsage", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetEmptyRootUrl).then(
        function (tileset) {
          var statistics = tileset._statistics;

          tileset.maximumMemoryUsage = 0.02;

          scene.renderForSpecs();
          expect(statistics.numberOfCommands).toEqual(4);
          expect(statistics.numberOfTilesWithContentReady).toEqual(4); // 4 children with b3dm content (does not include empty root)

          viewSky();

          // Unload tiles to meet cache size
          scene.renderForSpecs();
          expect(statistics.numberOfCommands).toEqual(0);
          expect(statistics.numberOfTilesWithContentReady).toEqual(2); // 2 children with b3dm content (does not include empty root)

          // Reset camera so all tiles are reloaded
          viewAllTiles();

          return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
            function () {
              expect(statistics.numberOfCommands).toEqual(4);
              expect(statistics.numberOfTilesWithContentReady).toEqual(4);
            }
          );
        }
      );
    });

    it("Unload cached tiles when a tileset uses replacement refinement using maximumMemoryUsage", function () {
      // No children have content, but all grandchildren have content
      //
      //          C
      //      E       E
      //    C   C   C   C
      //
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement1Url
      ).then(function (tileset) {
        tileset.maximumMemoryUsage = 0; // Only root needs to be visible

        // Render parent and four children (using additive refinement)
        viewAllTiles();
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.numberOfCommands).toEqual(4); // 4 grandchildren. Root is replaced.
        expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Root + four grandchildren (does not include empty children)

        // Zoom out so only root tile is needed to meet SSE.  This unloads
        // all grandchildren since the max number of loaded tiles is one.
        viewRootOnly();
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(1);
        expect(statistics.numberOfTilesWithContentReady).toEqual(1);

        // Zoom back in so the four children are re-requested.
        viewAllTiles();

        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function () {
            expect(statistics.numberOfCommands).toEqual(4);
            expect(statistics.numberOfTilesWithContentReady).toEqual(5);
          }
        );
      });
    });

    it("Explicitly unloads cached tiles with trimLoadedTiles", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.maximumMemoryUsage = 0.05;

        // Render parent and four children (using additive refinement)
        viewAllTiles();
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.numberOfCommands).toEqual(5);
        expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles

        // Zoom out so only root tile is needed to meet SSE.  The children
        // are not unloaded since max number of loaded tiles is five.
        viewRootOnly();
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(1);
        expect(statistics.numberOfTilesWithContentReady).toEqual(5);

        tileset.trimLoadedTiles();
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(1);
        expect(statistics.numberOfTilesWithContentReady).toEqual(1);
      });
    });

    it("tileUnload event is raised", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        tileset.maximumMemoryUsage = 0;

        // Render parent and four children (using additive refinement)
        viewAllTiles();
        scene.renderForSpecs();

        var statistics = tileset._statistics;
        expect(statistics.numberOfCommands).toEqual(5);
        expect(statistics.numberOfTilesWithContentReady).toEqual(5); // Five loaded tiles

        // Zoom out so only root tile is needed to meet SSE.  All the
        // children are unloaded since max number of loaded tiles is one.
        viewRootOnly();
        var spyUpdate = jasmine.createSpy("listener");
        tileset.tileUnload.addEventListener(spyUpdate);
        scene.renderForSpecs();

        expect(
          tileset.root.visibility(
            scene.frameState,
            CullingVolume.MASK_INDETERMINATE
          )
        ).not.toEqual(CullingVolume.MASK_OUTSIDE);
        expect(spyUpdate.calls.count()).toEqual(4);
        expect(spyUpdate.calls.argsFor(0)[0]).toBe(tileset.root.children[0]);
        expect(spyUpdate.calls.argsFor(1)[0]).toBe(tileset.root.children[1]);
        expect(spyUpdate.calls.argsFor(2)[0]).toBe(tileset.root.children[2]);
        expect(spyUpdate.calls.argsFor(3)[0]).toBe(tileset.root.children[3]);
      });
    });

    it("maximumMemoryUsage throws when negative", function () {
      var tileset = new Cesium3DTileset({
        url: tilesetUrl,
      });
      expect(function () {
        tileset.maximumMemoryUsage = -1;
      }).toThrowDeveloperError();
    });

    it("maximumScreenSpaceError throws when negative", function () {
      var tileset = new Cesium3DTileset({
        url: tilesetUrl,
      });
      expect(function () {
        tileset.maximumScreenSpaceError = -1;
      }).toThrowDeveloperError();
    });

    it("propagates tile transform down the tree", function () {
      var b3dmCommands = 1;
      // when instancing is supported, there is a single draw command,
      // else each instance is a separate command.
      var i3dmCommands = scene.context.instancedArrays ? 1 : 25;
      var totalCommands = b3dmCommands + i3dmCommands;
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithTransformsUrl
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        var root = tileset.root;
        var rootTransform = Matrix4.unpack(root._header.transform);

        var child = root.children[0];
        var childTransform = Matrix4.unpack(child._header.transform);
        var computedTransform = Matrix4.multiply(
          rootTransform,
          childTransform,
          new Matrix4()
        );

        expect(statistics.numberOfCommands).toBe(totalCommands);
        expect(root.computedTransform).toEqual(rootTransform);
        expect(child.computedTransform).toEqual(computedTransform);

        // Set the tileset's modelMatrix
        var tilesetTransform = Matrix4.fromTranslation(
          new Cartesian3(0.0, 1.0, 0.0)
        );
        tileset.modelMatrix = tilesetTransform;
        computedTransform = Matrix4.multiply(
          tilesetTransform,
          computedTransform,
          computedTransform
        );
        scene.renderForSpecs();
        expect(child.computedTransform).toEqual(computedTransform);

        // Set the modelMatrix somewhere off screen
        tileset.modelMatrix = Matrix4.fromTranslation(
          new Cartesian3(0.0, 100000.0, 0.0)
        );
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toBe(0);

        // Now bring it back
        tileset.modelMatrix = Matrix4.IDENTITY;
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toBe(totalCommands);

        // Do the same steps for a tile transform
        child.transform = Matrix4.fromTranslation(
          new Cartesian3(0.0, 100000.0, 0.0)
        );
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toBe(1);
        child.transform = Matrix4.IDENTITY;
        scene.renderForSpecs();
        expect(statistics.numberOfCommands).toBe(totalCommands);
      });
    });

    var skipLevelOfDetailOptions = {
      skipLevelOfDetail: true,
    };

    it("does not mark tileset as refining when tiles have selection depth 0", function () {
      viewRootOnly();
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetUrl,
        skipLevelOfDetailOptions
      ).then(function (tileset) {
        viewAllTiles();
        scene.renderForSpecs();
        var statistics = tileset._statistics;
        expect(statistics.numberOfTilesWithContentReady).toEqual(1);
        expect(tileset._selectedTiles[0]._selectionDepth).toEqual(0);
        expect(tileset._hasMixedContent).toBe(false);

        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function (tileset) {
            expect(statistics.numberOfTilesWithContentReady).toEqual(5);
            expect(tileset._hasMixedContent).toBe(false);
          }
        );
      });
    });

    it("marks tileset as mixed when tiles have nonzero selection depth", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement3Url,
        skipLevelOfDetailOptions
      ).then(function (tileset) {
        var statistics = tileset._statistics;

        tileset.root.children[0].children[0].children[0].unloadContent();
        tileset.root.children[0].children[0].children[1].unloadContent();
        tileset.root.children[0].children[0].children[2].unloadContent();
        statistics.numberOfTilesWithContentReady -= 3;

        scene.renderForSpecs();

        expect(tileset._hasMixedContent).toBe(true);
        expect(statistics.numberOfTilesWithContentReady).toEqual(2);
        expect(
          tileset.root.children[0].children[0].children[3]._selectionDepth
        ).toEqual(1);
        expect(tileset.root._selectionDepth).toEqual(0);

        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function (tileset) {
            expect(statistics.numberOfTilesWithContentReady).toEqual(5);
            expect(tileset._hasMixedContent).toBe(false);
          }
        );
      });
    });

    it("adds stencil clear command first when unresolved", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement3Url,
        skipLevelOfDetailOptions
      ).then(function (tileset) {
        tileset.root.children[0].children[0].children[0].unloadContent();
        tileset.root.children[0].children[0].children[1].unloadContent();
        tileset.root.children[0].children[0].children[2].unloadContent();

        scene.renderForSpecs();
        var commandList = scene.frameState.commandList;
        expect(commandList[0]).toBeInstanceOf(ClearCommand);
        expect(commandList[0].stencil).toBe(0);
      });
    });

    it("creates duplicate backface commands", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement3Url,
        skipLevelOfDetailOptions
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        var root = tileset.root;

        tileset.root.children[0].children[0].children[0].unloadContent();
        tileset.root.children[0].children[0].children[1].unloadContent();
        tileset.root.children[0].children[0].children[2].unloadContent();

        scene.renderForSpecs();

        // 2 for root tile, 1 for child, 1 for stencil clear
        // Tiles that are marked as finalResolution, including leaves, do not create back face commands
        expect(statistics.numberOfCommands).toEqual(4);
        expect(isSelected(tileset, root)).toBe(true);
        expect(root._finalResolution).toBe(false);
        expect(
          isSelected(tileset, root.children[0].children[0].children[3])
        ).toBe(true);
        expect(root.children[0].children[0].children[3]._finalResolution).toBe(
          true
        );
        expect(tileset._hasMixedContent).toBe(true);

        var commandList = scene.frameState.commandList;
        var rs = commandList[1].renderState;
        expect(rs.cull.enabled).toBe(true);
        expect(rs.cull.face).toBe(CullFace.FRONT);
        expect(rs.polygonOffset.enabled).toBe(true);
      });
    });

    it("does not create duplicate backface commands if no selected descendants", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement3Url,
        skipLevelOfDetailOptions
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        var root = tileset.root;

        tileset.root.children[0].children[0].children[0].unloadContent();
        tileset.root.children[0].children[0].children[1].unloadContent();
        tileset.root.children[0].children[0].children[2].unloadContent();
        tileset.root.children[0].children[0].children[3].unloadContent();

        scene.renderForSpecs();

        // 2 for root tile, 1 for child, 1 for stencil clear
        expect(statistics.numberOfCommands).toEqual(1);
        expect(isSelected(tileset, root)).toBe(true);
        expect(root._finalResolution).toBe(true);
        expect(
          isSelected(tileset, root.children[0].children[0].children[0])
        ).toBe(false);
        expect(
          isSelected(tileset, root.children[0].children[0].children[1])
        ).toBe(false);
        expect(
          isSelected(tileset, root.children[0].children[0].children[2])
        ).toBe(false);
        expect(
          isSelected(tileset, root.children[0].children[0].children[3])
        ).toBe(false);
        expect(tileset._hasMixedContent).toBe(false);
      });
    });

    it("does not add commands or stencil clear command with no selected tiles", function () {
      options.url = tilesetUrl;
      options.skipLevelOfDetail = true;
      var tileset = scene.primitives.add(new Cesium3DTileset(options));
      scene.renderForSpecs();
      var statistics = tileset._statistics;
      expect(tileset._selectedTiles.length).toEqual(0);
      expect(statistics.numberOfCommands).toEqual(0);
    });

    it("does not add stencil clear command or backface commands when fully resolved", function () {
      viewAllTiles();
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetReplacement3Url,
        skipLevelOfDetailOptions
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        expect(statistics.numberOfCommands).toEqual(
          tileset._selectedTiles.length
        );

        var commandList = scene.frameState.commandList;
        var length = commandList.length;
        for (var i = 0; i < length; ++i) {
          var command = commandList[i];
          expect(command).not.toBeInstanceOf(ClearCommand);
          expect(command.renderState.cull.face).not.toBe(CullFace.FRONT);
        }
      });
    });

    it("loadSiblings", function () {
      viewBottomLeft();
      return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url, {
        skipLevelOfDetail: true,
        loadSiblings: false,
        foveatedTimeDelay: 0,
      }).then(function (tileset) {
        var statistics = tileset._statistics;
        expect(statistics.numberOfTilesWithContentReady).toBe(2);
        tileset.loadSiblings = true;
        scene.renderForSpecs();
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function (tileset) {
            expect(statistics.numberOfTilesWithContentReady).toBe(5);
          }
        );
      });
    });

    it("immediatelyLoadDesiredLevelOfDetail", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl, {
        skipLevelOfDetail: true,
        immediatelyLoadDesiredLevelOfDetail: true,
      }).then(function (tileset) {
        var root = tileset.root;
        var child = findTileByUri(root.children, "ll.b3dm");
        tileset.root.refine = Cesium3DTileRefine.REPLACE;
        tileset._allTilesAdditive = false;
        viewBottomLeft();
        return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
          function (tileset) {
            expect(isSelected(tileset, child));
            expect(!isSelected(tileset, root));
            expect(root.contentUnloaded).toBe(true);
            // Renders child while parent loads
            viewRootOnly();
            scene.renderForSpecs();
            expect(isSelected(tileset, child));
            expect(!isSelected(tileset, root));
            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function (tileset) {
                expect(!isSelected(tileset, child));
                expect(isSelected(tileset, root));
              }
            );
          }
        );
      });
    });

    it("selects children if no ancestors available", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetOfTilesetsUrl,
        skipLevelOfDetailOptions
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        var parent = tileset.root.children[0];
        var child = parent.children[3].children[0];
        parent.refine = Cesium3DTileRefine.REPLACE;
        parent.unloadContent();

        viewBottomLeft();
        scene.renderForSpecs();

        expect(child.contentReady).toBe(true);
        expect(parent.contentReady).toBe(false);
        expect(isSelected(tileset, child)).toBe(true);
        expect(isSelected(tileset, parent)).toBe(false);
        expect(statistics.numberOfCommands).toEqual(1);
      });
    });

    it("tile expires", function () {
      return Cesium3DTilesTester.loadTileset(scene, batchedExpirationUrl).then(
        function (tileset) {
          // Intercept the request and load content that produces more draw commands, to simulate fetching new content after the original expires
          spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
            function (
              url,
              responseType,
              method,
              data,
              headers,
              deferred,
              overrideMimeType
            ) {
              Resource._DefaultImplementations.loadWithXhr(
                batchedColorsB3dmUrl,
                responseType,
                method,
                data,
                headers,
                deferred,
                overrideMimeType
              );
            }
          );
          var tile = tileset.root;
          var statistics = tileset._statistics;
          var expiredContent;
          tileset.style = new Cesium3DTileStyle({
            color: 'color("red")',
          });

          // Check that expireDuration and expireDate are correctly set
          var expireDate = JulianDate.addSeconds(
            JulianDate.now(),
            5.0,
            new JulianDate()
          );
          expect(
            JulianDate.secondsDifference(tile.expireDate, expireDate)
          ).toEqualEpsilon(0.0, CesiumMath.EPSILON1);
          expect(tile.expireDuration).toBe(5.0);
          expect(tile.contentExpired).toBe(false);
          expect(tile.contentReady).toBe(true);
          expect(tile.contentAvailable).toBe(true);
          expect(tile._expiredContent).toBeUndefined();

          // Check statistics
          expect(statistics.numberOfCommands).toBe(1);
          expect(statistics.numberOfTilesTotal).toBe(1);

          // Trigger expiration to happen next frame
          tile.expireDate = JulianDate.addSeconds(
            JulianDate.now(),
            -1.0,
            new JulianDate()
          );

          // Stays in the expired state until the request goes through
          var originalMaxmimumRequests = RequestScheduler.maximumRequests;
          RequestScheduler.maximumRequests = 0; // Artificially limit Request Scheduler so the request won't go through
          scene.renderForSpecs();
          RequestScheduler.maximumRequests = originalMaxmimumRequests;
          expiredContent = tile._expiredContent;
          expect(tile.contentExpired).toBe(true);
          expect(tile.contentAvailable).toBe(true); // Expired content now exists
          expect(expiredContent).toBeDefined();

          // Expired content renders while new content loads in
          expect(statistics.numberOfCommands).toBe(1);
          expect(statistics.numberOfTilesTotal).toBe(1);

          // Request goes through, now in the LOADING state
          scene.renderForSpecs();
          expect(tile.contentExpired).toBe(false);
          expect(tile.contentReady).toBe(false);
          expect(tile.contentAvailable).toBe(true);
          expect(tile._contentState).toBe(Cesium3DTileContentState.LOADING);
          expect(tile._expiredContent).toBeDefined(); // Still holds onto expired content until the content state is READY

          // Check that url contains a query param with the timestamp
          var url = Resource._Implementations.loadWithXhr.calls.first().args[0];
          expect(url.indexOf("expired=") >= 0).toBe(true);

          // statistics are still the same
          expect(statistics.numberOfCommands).toBe(1);
          expect(statistics.numberOfTilesTotal).toBe(1);

          return pollToPromise(function () {
            expect(statistics.numberOfCommands).toBe(1); // Still renders expired content
            scene.renderForSpecs();
            return tile.contentReady;
          }).then(function () {
            scene.renderForSpecs();

            // Expired content is destroyed
            expect(tile._expiredContent).toBeUndefined();
            expect(expiredContent.isDestroyed()).toBe(true);

            // Expect the style to be reapplied
            expect(tile.content.getFeature(0).color).toEqual(Color.RED);

            // statistics for new content
            expect(statistics.numberOfCommands).toBe(10);
            expect(statistics.numberOfTilesTotal).toBe(1);
          });
        }
      );
    });

    function modifySubtreeBuffer(arrayBuffer) {
      var uint8Array = new Uint8Array(arrayBuffer);
      var json = getJsonFromTypedArray(uint8Array);
      json.root.children.splice(0, 1);

      return generateJsonBuffer(json).buffer;
    }

    it("tile with tileset content expires", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetSubtreeExpirationUrl
      ).then(function (tileset) {
        // Intercept the request and load a subtree with one less child. Still want to make an actual request to simulate
        // real use cases instead of immediately returning a pre-created array buffer.
        spyOn(Resource._Implementations, "loadWithXhr").and.callFake(function (
          url,
          responseType,
          method,
          data,
          headers,
          deferred,
          overrideMimeType
        ) {
          var newDeferred = when.defer();
          Resource._DefaultImplementations.loadWithXhr(
            tilesetSubtreeUrl,
            responseType,
            method,
            data,
            headers,
            newDeferred,
            overrideMimeType
          );
          newDeferred.promise.then(function (arrayBuffer) {
            deferred.resolve(modifySubtreeBuffer(arrayBuffer));
          });
        });

        var subtreeRoot = tileset.root.children[0];
        var subtreeChildren = subtreeRoot.children[0].children;
        var childrenLength = subtreeChildren.length;
        var statistics = tileset._statistics;

        // Check statistics
        expect(statistics.numberOfCommands).toBe(5);
        expect(statistics.numberOfTilesTotal).toBe(7);
        expect(statistics.numberOfTilesWithContentReady).toBe(5);

        // Trigger expiration to happen next frame
        subtreeRoot.expireDate = JulianDate.addSeconds(
          JulianDate.now(),
          -1.0,
          new JulianDate()
        );

        // Listen to tile unload events
        var spyUpdate = jasmine.createSpy("listener");
        tileset.tileUnload.addEventListener(spyUpdate);

        // Tiles in the subtree are removed from the cache and destroyed.
        scene.renderForSpecs(); // Becomes expired
        scene.renderForSpecs(); // Makes request
        expect(subtreeRoot.children).toEqual([]);
        for (var i = 0; i < childrenLength; ++i) {
          expect(subtreeChildren[0].isDestroyed()).toBe(true);
        }
        expect(spyUpdate.calls.count()).toEqual(4);

        // Remove the spy so new tiles load in normally
        Resource._Implementations.loadWithXhr =
          Resource._DefaultImplementations.loadWithXhr;

        // Wait for the new tileset content to come in with one less leaf
        return pollToPromise(function () {
          scene.renderForSpecs();
          return subtreeRoot.contentReady && tileset.tilesLoaded;
        }).then(function () {
          scene.renderForSpecs();
          expect(statistics.numberOfCommands).toBe(4);
          expect(statistics.numberOfTilesTotal).toBe(6);
          expect(statistics.numberOfTilesWithContentReady).toBe(4);
        });
      });
    });

    it("tile expires and request fails", function () {
      return Cesium3DTilesTester.loadTileset(scene, batchedExpirationUrl).then(
        function (tileset) {
          spyOn(Resource._Implementations, "loadWithXhr").and.callFake(
            function (
              url,
              responseType,
              method,
              data,
              headers,
              deferred,
              overrideMimeType
            ) {
              deferred.reject();
            }
          );
          var tile = tileset.root;
          var statistics = tileset._statistics;

          // Trigger expiration to happen next frame
          tile.expireDate = JulianDate.addSeconds(
            JulianDate.now(),
            -1.0,
            new JulianDate()
          );

          // After update the tile is expired
          scene.renderForSpecs();

          // Make request (it will fail)
          scene.renderForSpecs();

          // Render scene
          scene.renderForSpecs();
          expect(tile._contentState).toBe(Cesium3DTileContentState.FAILED);
          expect(statistics.numberOfCommands).toBe(0);
          expect(statistics.numberOfTilesTotal).toBe(1);
        }
      );
    });

    it("tile expiration date", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var tile = tileset.root;

        // Trigger expiration to happen next frame
        tile.expireDate = JulianDate.addSeconds(
          JulianDate.now(),
          -1.0,
          new JulianDate()
        );

        // Stays in the expired state until the request goes through
        var originalMaxmimumRequests = RequestScheduler.maximumRequests;
        RequestScheduler.maximumRequests = 0; // Artificially limit Request Scheduler so the request won't go through
        scene.renderForSpecs();
        RequestScheduler.maximumRequests = originalMaxmimumRequests;

        expect(tile.contentExpired).toBe(true);

        return pollToPromise(function () {
          scene.renderForSpecs();
          return tile.contentReady;
        }).then(function () {
          scene.renderForSpecs();
          expect(tile._expiredContent).toBeUndefined();
          expect(tile.expireDate).toBeUndefined();
        });
      });
    });

    it("supports content data URIs", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetUrlWithContentUri
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        expect(statistics.visited).toEqual(1);
        expect(statistics.numberOfCommands).toEqual(1);
      });
    });

    it("destroys attached ClippingPlaneCollections and ClippingPlaneCollections that have been detached", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var clippingPlaneCollection1 = new ClippingPlaneCollection({
          planes: [new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0)],
        });
        expect(clippingPlaneCollection1.owner).not.toBeDefined();

        tileset.clippingPlanes = clippingPlaneCollection1;
        var clippingPlaneCollection2 = new ClippingPlaneCollection({
          planes: [new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0)],
        });

        tileset.clippingPlanes = clippingPlaneCollection2;
        expect(clippingPlaneCollection1.isDestroyed()).toBe(true);

        scene.primitives.remove(tileset);
        expect(clippingPlaneCollection2.isDestroyed()).toBe(true);
      });
    });

    it("throws a DeveloperError when given a ClippingPlaneCollection attached to another Tileset", function () {
      var clippingPlanes;
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl)
        .then(function (tileset1) {
          clippingPlanes = new ClippingPlaneCollection({
            planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
          });
          tileset1.clippingPlanes = clippingPlanes;

          return Cesium3DTilesTester.loadTileset(scene, tilesetUrl);
        })
        .then(function (tileset2) {
          expect(function () {
            tileset2.clippingPlanes = clippingPlanes;
          }).toThrowDeveloperError();
        });
    });

    it("clipping planes cull hidden tiles", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var visibility = tileset.root.visibility(
          scene.frameState,
          CullingVolume.MASK_INSIDE
        );

        expect(visibility).not.toBe(CullingVolume.MASK_OUTSIDE);

        var plane = new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0);
        tileset.clippingPlanes = new ClippingPlaneCollection({
          planes: [plane],
        });

        visibility = tileset.root.visibility(
          scene.frameState,
          CullingVolume.MASK_INSIDE
        );

        expect(visibility).toBe(CullingVolume.MASK_OUTSIDE);

        plane.distance = 0.0;
        visibility = tileset.root.visibility(
          scene.frameState,
          CullingVolume.MASK_INSIDE
        );

        expect(visibility).not.toBe(CullingVolume.MASK_OUTSIDE);
      });
    });

    it("clipping planes cull hidden content", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var visibility = tileset.root.contentVisibility(scene.frameState);

        expect(visibility).not.toBe(Intersect.OUTSIDE);

        var plane = new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0);
        tileset.clippingPlanes = new ClippingPlaneCollection({
          planes: [plane],
        });

        visibility = tileset.root.contentVisibility(scene.frameState);

        expect(visibility).toBe(Intersect.OUTSIDE);

        plane.distance = 0.0;
        visibility = tileset.root.contentVisibility(scene.frameState);

        expect(visibility).not.toBe(Intersect.OUTSIDE);
      });
    });

    it("clipping planes cull tiles completely inside clipping region", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function (
        tileset
      ) {
        var statistics = tileset._statistics;
        var root = tileset.root;

        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(5);

        tileset.update(scene.frameState);

        var radius = 287.0736139905632;

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

        tileset.update(scene.frameState);
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(5);
        expect(root._isClipped).toBe(false);

        plane.distance = -1;

        tileset.update(scene.frameState);
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(3);
        expect(root._isClipped).toBe(true);

        plane.distance = -radius;

        tileset.update(scene.frameState);
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(0);
        expect(root._isClipped).toBe(true);
      });
    });

    it("clipping planes cull tiles completely inside clipping region for i3dm", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        tilesetWithExternalResourcesUrl
      ).then(function (tileset) {
        var statistics = tileset._statistics;
        var root = tileset.root;

        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(6);

        tileset.update(scene.frameState);

        var radius = 142.19001637409772;

        var plane = new ClippingPlane(Cartesian3.UNIT_Z, radius);
        tileset.clippingPlanes = new ClippingPlaneCollection({
          planes: [plane],
        });

        tileset.update(scene.frameState);
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(6);
        expect(root._isClipped).toBe(false);

        plane.distance = 0;

        tileset.update(scene.frameState);
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(6);
        expect(root._isClipped).toBe(true);

        plane.distance = -radius;

        tileset.update(scene.frameState);
        scene.renderForSpecs();

        expect(statistics.numberOfCommands).toEqual(0);
        expect(root._isClipped).toBe(true);
      });
    });

    it("clippingPlanesOriginMatrix has correct orientation", function () {
      return Cesium3DTilesTester.loadTileset(scene, withTransformBoxUrl).then(
        function (tileset) {
          // The bounding volume of this tileset puts it under the surface, so no
          // east-north-up should be applied. Check that it matches the orientation
          // of the original transform.
          var offsetMatrix = tileset.clippingPlanesOriginMatrix;

          expect(
            Matrix4.equals(offsetMatrix, tileset.root.computedTransform)
          ).toBe(true);

          return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(
            function (tileset) {
              // The bounding volume of this tileset puts it on the surface,
              //  so we want to apply east-north-up as our best guess.
              offsetMatrix = tileset.clippingPlanesOriginMatrix;
              // The clipping plane matrix is not the same as the original because we applied east-north-up.
              expect(
                Matrix4.equals(offsetMatrix, tileset.root.computedTransform)
              ).toBe(false);

              // But they have the same translation.
              var clippingPlanesOrigin = Matrix4.getTranslation(
                offsetMatrix,
                new Cartesian3()
              );
              expect(
                Cartesian3.equals(
                  tileset.root.boundingSphere.center,
                  clippingPlanesOrigin
                )
              ).toBe(true);
            }
          );
        }
      );
    });

    it("clippingPlanesOriginMatrix matches root tile bounding sphere", function () {
      return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(
        function (tileset) {
          var offsetMatrix = Matrix4.clone(
            tileset.clippingPlanesOriginMatrix,
            new Matrix4()
          );
          var boundingSphereEastNorthUp = Transforms.eastNorthUpToFixedFrame(
            tileset.root.boundingSphere.center
          );
          expect(Matrix4.equals(offsetMatrix, boundingSphereEastNorthUp)).toBe(
            true
          );

          // Changing the model matrix should change the clipping planes matrix
          tileset.modelMatrix = Matrix4.fromTranslation(
            new Cartesian3(100, 0, 0)
          );
          scene.renderForSpecs();
          expect(
            Matrix4.equals(offsetMatrix, tileset.clippingPlanesOriginMatrix)
          ).toBe(false);

          boundingSphereEastNorthUp = Transforms.eastNorthUpToFixedFrame(
            tileset.root.boundingSphere.center
          );
          offsetMatrix = tileset.clippingPlanesOriginMatrix;
          expect(offsetMatrix).toEqualEpsilon(
            boundingSphereEastNorthUp,
            CesiumMath.EPSILON3
          );
        }
      );
    });

    describe("updateForPass", function () {
      it("updates for pass", function () {
        viewAllTiles();
        var passCamera = Camera.clone(scene.camera);
        var passCullingVolume = passCamera.frustum.computeCullingVolume(
          passCamera.positionWC,
          passCamera.directionWC,
          passCamera.upWC
        );
        viewNothing(); // Main camera views nothing, pass camera views all tiles

        var preloadFlightPassState = new Cesium3DTilePassState({
          pass: Cesium3DTilePass.PRELOAD_FLIGHT,
          camera: passCamera,
          cullingVolume: passCullingVolume,
        });

        return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(
          function (tileset) {
            expect(tileset.statistics.selected).toBe(0);
            tileset.updateForPass(scene.frameState, preloadFlightPassState);
            expect(tileset._requestedTiles.length).toBe(5);
          }
        );
      });

      it("uses custom command list", function () {
        var passCommandList = [];

        var renderPassState = new Cesium3DTilePassState({
          pass: Cesium3DTilePass.RENDER,
          commandList: passCommandList,
        });

        viewAllTiles();

        return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(
          function (tileset) {
            tileset.updateForPass(scene.frameState, renderPassState);
            expect(passCommandList.length).toBe(5);
          }
        );
      });

      it("throws if frameState is undefined", function () {
        var tileset = new Cesium3DTileset({
          url: tilesetUrl,
        });

        expect(function () {
          tileset.updateForPass();
        }).toThrowDeveloperError();
      });

      it("throws if tilesetPassState is undefined", function () {
        var tileset = new Cesium3DTileset({
          url: tilesetUrl,
        });

        expect(function () {
          tileset.updateForPass(scene.frameState);
        }).toThrowDeveloperError();
      });
    });

    function sampleHeightMostDetailed(cartographics, objectsToExclude) {
      var result;
      var completed = false;
      scene
        .sampleHeightMostDetailed(cartographics, objectsToExclude)
        .then(function (pickResult) {
          result = pickResult;
          completed = true;
        });
      return pollToPromise(function () {
        // Scene requires manual updates in the tests to move along the promise
        scene.render();
        return completed;
      }).then(function () {
        return result;
      });
    }

    function drillPickFromRayMostDetailed(ray, limit, objectsToExclude) {
      var result;
      var completed = false;
      scene
        .drillPickFromRayMostDetailed(ray, limit, objectsToExclude)
        .then(function (pickResult) {
          result = pickResult;
          completed = true;
        });
      return pollToPromise(function () {
        // Scene requires manual updates in the tests to move along the promise
        scene.render();
        return completed;
      }).then(function () {
        return result;
      });
    }

    describe("most detailed height queries", function () {
      it("tileset is offscreen", function () {
        if (webglStub) {
          return;
        }

        viewNothing();

        // Tileset uses replacement refinement so only one tile should be loaded and selected during most detailed picking
        var centerCartographic = new Cartographic(
          -1.3196799798348215,
          0.6988740001506679,
          2.4683731133709323
        );
        var cartographics = [centerCartographic];

        return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
          function (tileset) {
            return sampleHeightMostDetailed(cartographics).then(function () {
              expect(centerCartographic.height).toEqualEpsilon(
                2.47,
                CesiumMath.EPSILON1
              );
              var statisticsMostDetailedPick =
                tileset._statisticsPerPass[Cesium3DTilePass.MOST_DETAILED_PICK];
              var statisticsRender =
                tileset._statisticsPerPass[Cesium3DTilePass.RENDER];
              expect(statisticsMostDetailedPick.numberOfCommands).toBe(1);
              expect(
                statisticsMostDetailedPick.numberOfTilesWithContentReady
              ).toBe(1);
              expect(statisticsMostDetailedPick.selected).toBe(1);
              expect(statisticsMostDetailedPick.visited).toBeGreaterThan(1);
              expect(statisticsMostDetailedPick.numberOfTilesTotal).toBe(21);
              expect(statisticsRender.selected).toBe(0);
            });
          }
        );
      });

      it("tileset is onscreen", function () {
        if (webglStub) {
          return;
        }

        viewAllTiles();

        // Tileset uses replacement refinement so only one tile should be loaded and selected during most detailed picking
        var centerCartographic = new Cartographic(
          -1.3196799798348215,
          0.6988740001506679,
          2.4683731133709323
        );
        var cartographics = [centerCartographic];

        return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
          function (tileset) {
            return sampleHeightMostDetailed(cartographics).then(function () {
              expect(centerCartographic.height).toEqualEpsilon(
                2.47,
                CesiumMath.EPSILON1
              );
              var statisticsMostDetailedPick =
                tileset._statisticsPerPass[Cesium3DTilePass.MOST_DETAILED_PICK];
              var statisticsRender =
                tileset._statisticsPerPass[Cesium3DTilePass.RENDER];
              expect(statisticsMostDetailedPick.numberOfCommands).toBe(1);
              expect(
                statisticsMostDetailedPick.numberOfTilesWithContentReady
              ).toBeGreaterThan(1);
              expect(statisticsMostDetailedPick.selected).toBe(1);
              expect(statisticsMostDetailedPick.visited).toBeGreaterThan(1);
              expect(statisticsMostDetailedPick.numberOfTilesTotal).toBe(21);
              expect(statisticsRender.selected).toBeGreaterThan(0);
            });
          }
        );
      });

      it("tileset uses additive refinement", function () {
        if (webglStub) {
          return;
        }

        viewNothing();

        var originalLoadJson = Cesium3DTileset.loadJson;
        spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
          return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
            tilesetJson.root.refine = "ADD";
            return tilesetJson;
          });
        });

        var offcenterCartographic = new Cartographic(
          -1.3196754112739246,
          0.6988705057695633,
          2.467395745774971
        );
        var cartographics = [offcenterCartographic];

        return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
          function (tileset) {
            return sampleHeightMostDetailed(cartographics).then(function () {
              var statistics =
                tileset._statisticsPerPass[Cesium3DTilePass.MOST_DETAILED_PICK];
              expect(offcenterCartographic.height).toEqualEpsilon(
                7.407,
                CesiumMath.EPSILON1
              );
              expect(statistics.numberOfCommands).toBe(3); // One for each level of the tree
              expect(
                statistics.numberOfTilesWithContentReady
              ).toBeGreaterThanOrEqualTo(3);
              expect(statistics.selected).toBe(3);
              expect(statistics.visited).toBeGreaterThan(3);
              expect(statistics.numberOfTilesTotal).toBe(21);
            });
          }
        );
      });

      it("drill picks multiple features when tileset uses additive refinement", function () {
        if (webglStub) {
          return;
        }

        viewNothing();
        var ray = new Ray(scene.camera.positionWC, scene.camera.directionWC);

        var originalLoadJson = Cesium3DTileset.loadJson;
        spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
          return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
            tilesetJson.root.refine = "ADD";
            return tilesetJson;
          });
        });

        return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
          function (tileset) {
            return drillPickFromRayMostDetailed(ray).then(function (results) {
              expect(results.length).toBe(3);
              expect(
                results[0].object.content.url.indexOf("0_0_0.b3dm") > -1
              ).toBe(true);
              expect(
                results[1].object.content.url.indexOf("1_1_1.b3dm") > -1
              ).toBe(true);
              expect(
                results[2].object.content.url.indexOf("2_4_4.b3dm") > -1
              ).toBe(true);
            });
          }
        );
      });

      it("works when tileset cache is disabled", function () {
        if (webglStub) {
          return;
        }
        viewNothing();
        var centerCartographic = new Cartographic(
          -1.3196799798348215,
          0.6988740001506679,
          2.4683731133709323
        );
        var cartographics = [centerCartographic];
        return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
          function (tileset) {
            tileset.maximumMemoryUsage = 0;
            return sampleHeightMostDetailed(cartographics).then(function () {
              expect(centerCartographic.height).toEqualEpsilon(
                2.47,
                CesiumMath.EPSILON1
              );
            });
          }
        );
      });

      it("multiple samples", function () {
        if (webglStub) {
          return;
        }

        viewNothing();

        var centerCartographic = new Cartographic(
          -1.3196799798348215,
          0.6988740001506679
        );
        var offcenterCartographic = new Cartographic(
          -1.3196754112739246,
          0.6988705057695633
        );
        var missCartographic = new Cartographic(
          -1.3196096042084076,
          0.6988703290845706
        );
        var cartographics = [
          centerCartographic,
          offcenterCartographic,
          missCartographic,
        ];

        return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
          function (tileset) {
            return sampleHeightMostDetailed(cartographics).then(function () {
              expect(centerCartographic.height).toEqualEpsilon(
                2.47,
                CesiumMath.EPSILON1
              );
              expect(offcenterCartographic.height).toEqualEpsilon(
                2.47,
                CesiumMath.EPSILON1
              );
              expect(missCartographic.height).toBeUndefined();

              var statistics =
                tileset._statisticsPerPass[Cesium3DTilePass.MOST_DETAILED_PICK];
              expect(statistics.numberOfTilesWithContentReady).toBe(2);
            });
          }
        );
      });
    });

    it("cancels out-of-view tiles", function () {
      viewNothing();

      return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
        function (tileset) {
          // Make requests
          viewAllTiles();
          scene.renderForSpecs();
          var requestedTilesInFlight = tileset._requestedTilesInFlight;
          var requestedTilesInFlightLength = requestedTilesInFlight.length;
          expect(requestedTilesInFlightLength).toBeGreaterThan(0);

          // Save off old requests
          var oldRequests = [];
          var i;
          for (i = 0; i < requestedTilesInFlightLength; i++) {
            oldRequests.push(requestedTilesInFlight[i]);
          }

          // Cancel requests
          viewNothing();
          scene.renderForSpecs();
          expect(requestedTilesInFlight.length).toBe(0);

          // Make sure old requests were marked for cancelling
          var allCancelled = true;
          var oldRequestsLength = oldRequests.length;
          for (i = 0; i < oldRequestsLength; i++) {
            var tile = oldRequests[i];
            allCancelled = allCancelled && tile._request.cancelled;
          }
          expect(allCancelled).toBe(true);
        }
      );
    });

    it("sorts requests by priority", function () {
      viewNothing();

      return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
        function (tileset) {
          // Make requests
          viewAllTiles();
          scene.renderForSpecs();
          var requestedTilesInFlight = tileset._requestedTilesInFlight;
          var requestedTilesInFlightLength = requestedTilesInFlight.length;
          expect(requestedTilesInFlightLength).toBeGreaterThan(0);

          // Verify sort
          var allSorted = true;
          var lastPriority = -Number.MAX_VALUE;
          var thisPriority;
          for (var i = 0; i < requestedTilesInFlightLength; i++) {
            thisPriority = requestedTilesInFlight[i]._priority;
            allSorted = allSorted && thisPriority >= lastPriority;
            lastPriority = thisPriority;
          }

          expect(allSorted).toBe(true);
          expect(lastPriority).not.toEqual(requestedTilesInFlight[0]._priority); // Not all the same value
        }
      );
    });

    it("defers requests when foveatedScreenSpaceError is true", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetRefinementMix).then(
        function (tileset) {
          tileset.foveatedScreenSpaceError = true;
          tileset.foveatedConeSize = 0;
          tileset.maximumScreenSpaceError = 8;
          tileset.foveatedTimeDelay = 0;

          // Position camera such that some tiles are outside the foveated cone but still on screen.
          viewAllTiles();
          scene.camera.moveLeft(205.0);
          scene.camera.moveDown(205.0);

          scene.renderForSpecs();

          // Verify deferred
          var requestedTilesInFlight = tileset._requestedTilesInFlight;
          expect(requestedTilesInFlight.length).toBe(1);
          expect(requestedTilesInFlight[0].priorityDeferred).toBe(true);
        }
      );
    });

    it("loads deferred requests only after time delay.", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetRefinementMix).then(
        function (tileset) {
          tileset.foveatedScreenSpaceError = true;
          tileset.foveatedConeSize = 0;
          tileset.maximumScreenSpaceError = 8;
          tileset.foveatedTimeDelay = 0.1;

          // Position camera such that some tiles are outside the foveated cone but still on screen.
          viewAllTiles();
          scene.camera.moveLeft(205.0);
          scene.camera.moveDown(205.0);

          scene.renderForSpecs();

          // Nothing should be loaded yet.
          expect(tileset._requestedTilesInFlight.length).toBe(0);
          // Eventually, a deferred tile should load.
          return pollToPromise(function () {
            scene.renderForSpecs();
            return tileset._requestedTilesInFlight.length !== 0;
          }).then(function () {
            expect(tileset._requestedTilesInFlight[0].priorityDeferred).toBe(
              true
            );
          });
        }
      );
    });

    it("preloads tiles", function () {
      // Flight destination
      viewAllTiles();
      scene.preloadFlightCamera = Camera.clone(scene.camera);
      scene.preloadFlightCullingVolume = scene.camera.frustum.computeCullingVolume(
        scene.camera.positionWC,
        scene.camera.directionWC,
        scene.camera.upWC
      );

      // Reset view
      viewNothing();

      return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
        function (tileset) {
          spyOn(Camera.prototype, "canPreloadFlight").and.callFake(function () {
            return true;
          });
          scene.renderForSpecs();
          expect(tileset._requestedTilesInFlight.length).toBeGreaterThan(0);
        }
      );
    });

    it("does not fetch tiles while camera is moving", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
        function (tileset) {
          tileset.cullRequestsWhileMoving = true;
          viewAllTiles();
          scene.renderForSpecs();
          expect(tileset._requestedTilesInFlight.length).toEqual(0); // Big camera delta so no fetches should occur.
        }
      );
    });

    it("does not apply cullRequestWhileMoving optimization if tileset is moving", function () {
      viewNothing();
      return Cesium3DTilesTester.loadTileset(scene, tilesetUniform).then(
        function (tileset) {
          tileset.cullRequestsWhileMoving = true;
          tileset.modelMatrix[12] += 1.0;
          viewAllTiles();
          scene.renderForSpecs();
          expect(tileset._requestedTilesInFlight.length).toEqual(2);
        }
      );
    });

    it("loads tiles when preloadWhenHidden is true", function () {
      var options = {
        show: false,
        preloadWhenHidden: true,
      };

      return Cesium3DTilesTester.loadTileset(scene, tilesetUrl, options).then(
        function (tileset) {
          var selectedLength = tileset.statistics.selected;
          expect(selectedLength).toBeGreaterThan(0);
          tileset.show = true;
          scene.renderForSpecs();
          expect(tileset.statistics.selected).toBe(selectedLength);
          expect(tileset.statistics.numberOfPendingRequests).toBe(0);
        }
      );
    });

    describe("3DTILES_implicit_tiling", function () {
      var implicitTilesetUrl =
        "Data/Cesium3DTiles/Implicit/ImplicitTileset/tileset.json";
      var implicitRootUrl =
        "Data/Cesium3DTiles/Implicit/ImplicitRootTile/tileset.json";
      var implicitChildUrl =
        "Data/Cesium3DTiles/Implicit/ImplicitChildTile/tileset.json";

      it("renders tileset", function () {
        return Cesium3DTilesTester.loadTileset(scene, implicitTilesetUrl).then(
          function (tileset) {
            var statistics = tileset._statistics;
            // root + implicit placeholder + 4 child tiles
            expect(statistics.visited).toEqual(6);
            // the implicit placeholder tile is not rendered
            expect(statistics.numberOfCommands).toEqual(5);
          }
        );
      });

      it("detects and initializes an implicit tileset in root tile", function () {
        viewNothing();
        return Cesium3DTilesTester.loadTileset(scene, implicitRootUrl).then(
          function (tileset) {
            var implicitTile = tileset.root;
            expect(
              endsWith(
                implicitTile._contentResource.url,
                "subtrees/0/0/0/0.subtree"
              )
            ).toEqual(true);
            expect(implicitTile.implicitTileset).toBeDefined();
            expect(implicitTile.implicitCoordinates).toBeDefined();
            expect(implicitTile.implicitCoordinates.level).toEqual(0);
            expect(implicitTile.implicitCoordinates.x).toEqual(0);
            expect(implicitTile.implicitCoordinates.y).toEqual(0);
            expect(implicitTile.implicitCoordinates.z).toEqual(0);
          }
        );
      });

      it("detects and initializes an implicit tileset in child tile", function () {
        viewNothing();
        return Cesium3DTilesTester.loadTileset(scene, implicitChildUrl).then(
          function (tileset) {
            var parentTile = tileset.root;
            var implicitTile = parentTile.children[0];
            expect(
              endsWith(
                implicitTile._contentResource.url,
                "subtrees/0/0/0.subtree"
              )
            ).toEqual(true);
            expect(implicitTile.implicitTileset).toBeDefined();
            expect(implicitTile.implicitCoordinates).toBeDefined();
            expect(implicitTile.implicitCoordinates.level).toEqual(0);
            expect(implicitTile.implicitCoordinates.x).toEqual(0);
            expect(implicitTile.implicitCoordinates.y).toEqual(0);
          }
        );
      });
    });

    describe("3DTILES_multiple_contents", function () {
      var multipleContentsUrl =
        "Data/Cesium3DTiles/MultipleContents/MultipleContents/tileset.json";
      var implicitMultipleContentsUrl =
        "Data/Cesium3DTiles/Implicit/ImplicitMultipleContents/tileset.json";

      it("request statistics are updated correctly on success", function () {
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            var statistics = tileset.statistics;
            expect(statistics.numberOfAttemptedRequests).toBe(0);
            expect(statistics.numberOfPendingRequests).toBe(0);
            expect(statistics.numberOfTilesProcessing).toBe(0);
            expect(statistics.numberOfTilesWithContentReady).toBe(1);
          }
        );
      });

      it("request statistics are updated for partial success", function () {
        var originalLoadJson = Cesium3DTileset.loadJson;
        spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
          return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
            var content =
              tilesetJson.root.extensions["3DTILES_multiple_contents"].content;
            var badTile = {
              uri: "nonexistent.b3dm",
            };
            content.splice(1, 0, badTile);

            return tilesetJson;
          });
        });

        viewNothing();
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            viewAllTiles();
            scene.renderForSpecs();

            var statistics = tileset.statistics;
            expect(statistics.numberOfAttemptedRequests).toBe(0);
            expect(statistics.numberOfPendingRequests).toBe(3);
            expect(statistics.numberOfTilesProcessing).toBe(0);
            expect(statistics.numberOfTilesWithContentReady).toBe(0);

            tileset.root.contentReadyToProcessPromise
              .then(function () {
                expect(statistics.numberOfAttemptedRequests).toBe(0);
                expect(statistics.numberOfPendingRequests).toBe(0);
                expect(statistics.numberOfTilesProcessing).toBe(1);
                expect(statistics.numberOfTilesWithContentReady).toBe(0);
              })
              .otherwise(fail);

            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function () {
                expect(statistics.numberOfAttemptedRequests).toBe(0);
                expect(statistics.numberOfPendingRequests).toBe(0);
                expect(statistics.numberOfTilesProcessing).toBe(0);
                expect(statistics.numberOfTilesWithContentReady).toBe(1);
              }
            );
          }
        );
      });

      it("request statistics are updated correctly if requests are not scheduled", function () {
        viewNothing();
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            var oldMaximumRequestsPerServer =
              RequestScheduler.maximumRequestsPerServer;
            RequestScheduler.maximumRequestsPerServer = 1;

            viewAllTiles();
            scene.renderForSpecs();

            var statistics = tileset.statistics;
            expect(statistics.numberOfAttemptedRequests).toBe(2);
            expect(statistics.numberOfPendingRequests).toBe(0);
            expect(statistics.numberOfTilesProcessing).toBe(0);
            expect(statistics.numberOfTilesWithContentReady).toBe(0);

            RequestScheduler.maximumRequestsPerServer = oldMaximumRequestsPerServer;
          }
        );
      });

      it("statistics update correctly if tile is canceled", function () {
        viewNothing();
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            var callCount = 0;
            tileset.tileFailed.addEventListener(function (event) {
              callCount++;
            });

            viewAllTiles();
            scene.renderForSpecs();

            var statistics = tileset.statistics;
            expect(statistics.numberOfAttemptedRequests).toBe(0);
            expect(statistics.numberOfPendingRequests).toBe(2);
            expect(statistics.numberOfTilesProcessing).toBe(0);
            expect(statistics.numberOfTilesWithContentReady).toBe(0);

            var multipleContents = tileset.root.content;
            multipleContents.cancelRequests();

            tileset.root.contentReadyToProcessPromise
              .then(function () {
                expect(statistics.numberOfAttemptedRequests).toBe(0);
                expect(statistics.numberOfPendingRequests).toBe(0);
                expect(statistics.numberOfTilesProcessing).toBe(1);
                expect(statistics.numberOfTilesWithContentReady).toBe(0);
              })
              .otherwise(fail);

            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function () {
                // Resetting content should be handled gracefully; it should
                // not trigger the tileFailed event
                expect(callCount).toBe(0);

                expect(statistics.numberOfAttemptedRequests).toBe(0);
                expect(statistics.numberOfPendingRequests).toBe(0);
                expect(statistics.numberOfTilesProcessing).toBe(0);
                expect(statistics.numberOfTilesWithContentReady).toBe(1);
              }
            );
          }
        );
      });

      it("verify multiple content statistics", function () {
        options.url = multipleContentsUrl;
        var tileset = scene.primitives.add(new Cesium3DTileset(options));

        return checkPointAndFeatureCounts(tileset, 35, 0, 132);
      });

      it("calls tileFailed for each content with errors", function () {
        var originalLoadJson = Cesium3DTileset.loadJson;
        spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
          return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
            var extension =
              tilesetJson.root.extensions["3DTILES_multiple_contents"];
            extension.content = [
              {
                uri: "nonexistent1.b3dm",
              },
              {
                uri: "nonexistent2.b3dm",
              },
              {
                uri: "nonexistent3.b3dm",
              },
            ];

            return tilesetJson;
          });
        });

        var uris = [];

        viewNothing();
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            tileset.tileFailed.addEventListener(function (event) {
              uris.push(event.url);
            });

            viewAllTiles();
            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function () {
                expect(uris.length).toBe(3);
                uris.sort();
                var expected = [
                  "nonexistent1.b3dm",
                  "nonexistent2.b3dm",
                  "nonexistent3.b3dm",
                ];

                for (var i = 0; i < expected.length; i++) {
                  expect(endsWith(uris[i], expected[i])).toBe(true);
                }
              }
            );
          }
        );
      });

      it("raises tileFailed for external tileset inside multiple contents", function () {
        var originalLoadJson = Cesium3DTileset.loadJson;
        spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
          return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
            var extension =
              tilesetJson.root.extensions["3DTILES_multiple_contents"];
            extension.content = [
              {
                uri: "external.json",
              },
            ];

            return tilesetJson;
          });
        });

        spyOn(Resource.prototype, "fetchArrayBuffer").and.callFake(function () {
          var externalTileset = {
            asset: {
              version: "1.0",
            },
            root: {},
          };
          var buffer = generateJsonBuffer(externalTileset).buffer;
          return when.resolve(buffer);
        });

        viewNothing();
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            var errorCount = 0;
            tileset.tileFailed.addEventListener(function (event) {
              errorCount++;
              expect(endsWith(event.url, "external.json")).toBe(true);
              expect(event.message).toEqual(
                "External tilesets are disallowed inside the 3DTILES_multiple_contents extension"
              );
            });

            viewAllTiles();
            scene.renderForSpecs();

            return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(
              function () {
                expect(errorCount).toBe(1);
              }
            );
          }
        );
      });

      it("debugColorizeTiles for multiple contents", function () {
        return checkDebugColorizeTiles(multipleContentsUrl);
      });

      it("debugShowUrl lists each URI", function () {
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            tileset.debugShowUrl = true;
            scene.renderForSpecs();
            expect(tileset._tileDebugLabels).toBeDefined();

            var expected = "Urls:\n- batched.b3dm\n- instanced.i3dm";
            expect(tileset._tileDebugLabels._labels[0].text).toEqual(expected);

            tileset.debugShowUrl = false;
            scene.renderForSpecs();
            expect(tileset._tileDebugLabels).not.toBeDefined();
          }
        );
      });

      it("renders tileset with multiple contents", function () {
        var b3dmCommands = 1;
        // when instancing is supported, there is a single draw command,
        // else,each instance is a separate command.
        var i3dmCommands = scene.context.instancedArrays ? 1 : 25;
        var totalCommands = b3dmCommands + i3dmCommands;
        return Cesium3DTilesTester.loadTileset(scene, multipleContentsUrl).then(
          function (tileset) {
            var statistics = tileset._statistics;
            expect(statistics.visited).toEqual(1);
            expect(statistics.numberOfCommands).toEqual(totalCommands);
          }
        );
      });

      it("renders implicit tileset with multiple contents", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitMultipleContentsUrl
        ).then(function (tileset) {
          var statistics = tileset._statistics;
          // implicit placeholder + transcoded root + 4 child tiles
          expect(statistics.visited).toEqual(6);
          // root content + 2 contents per child tile
          expect(statistics.numberOfCommands).toEqual(9);
        });
      });
    });

    describe("3DTILES_metadata", function () {
      var tilesetMetadataUrl =
        "Data/Cesium3DTiles/Metadata/TilesetMetadata/tileset.json";
      var tilesetWithExternalSchemaUrl =
        "Data/Cesium3DTiles/Metadata/ExternalSchema/tileset.json";
      var tilesetWithGroupMetadataUrl =
        "Data/Cesium3DTiles/Metadata/GroupMetadata/tileset.json";
      var tilesetWithImplicitTileMetadataUrl =
        "Data/Cesium3DTiles/Metadata/ImplicitTileMetadata/tileset.json";
      var tilesetWithExplicitTileMetadataUrl =
        "Data/Cesium3DTiles/Metadata/TileMetadata/tileset.json";

      var tilesetProperties = {
        author: "Cesium",
        date: "2021-03-23",
        centerCartographic: [
          -1.3196816996258511,
          0.6988767486400521,
          45.78600543644279,
        ],
        tileCount: 5,
      };

      it("loads tileset metadata", function () {
        return Cesium3DTilesTester.loadTileset(scene, tilesetMetadataUrl).then(
          function (tileset) {
            var metadata = tileset.metadata;
            expect(metadata).toBeDefined();

            var tilesetMetadata = metadata.tileset;
            expect(tilesetMetadata.getProperty("name")).not.toBeDefined();
            expect(tilesetMetadata.getProperty("author")).toBe(
              tilesetProperties.author
            );
            expect(tilesetMetadata.getPropertyBySemantic("DATE_ISO_8601")).toBe(
              tilesetProperties.date
            );
            expect(tilesetMetadata.getProperty("centerCartographic")).toEqual(
              Cartesian3.unpack(tilesetProperties.centerCartographic)
            );
            expect(tilesetMetadata.getProperty("tileCount")).toBe(
              tilesetProperties.tileCount
            );
          }
        );
      });

      it("loads group metadata", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetWithGroupMetadataUrl
        ).then(function (tileset) {
          var metadata = tileset.metadata;
          expect(metadata).toBeDefined();

          var groups = metadata.groups;
          expect(groups).toBeDefined();

          var group = groups.commercialDistrict;
          expect(group).toBeDefined();
          expect(group.getProperty("businessCount")).toBe(143);

          group = groups.residentialDistrict;
          expect(group).toBeDefined();
          expect(group.getProperty("population")).toBe(300000);
          expect(group.getProperty("neighborhoods")).toEqual([
            "Hillside",
            "Middletown",
            "Western Heights",
          ]);

          group = groups.port;
          expect(group).not.toBeDefined();
        });
      });

      it("can access group metadata through contents", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetWithGroupMetadataUrl
        ).then(function (tileset) {
          var metadata = tileset.metadata;
          var commercialDistrict = metadata.groups.commercialDistrict;
          var residentialDistrict = metadata.groups.residentialDistrict;

          // the parent tile in this dataset does not have a group defined,
          // but its children do.
          var parent = tileset.root;
          var group = parent.content.groupMetadata;
          expect(group).not.toBeDefined();

          var expected = {
            "ul.b3dm": residentialDistrict,
            "ll.b3dm": residentialDistrict,
            "ur.b3dm": commercialDistrict,
            "lr.b3dm": commercialDistrict,
          };

          var childrenTiles = parent.children;
          childrenTiles.forEach(function (tile) {
            var uri = tile._header.content.uri;
            expect(tile.content.groupMetadata).toBe(expected[uri]);
          });
        });
      });

      it("loads metadata with embedded schema", function () {
        return Cesium3DTilesTester.loadTileset(scene, tilesetMetadataUrl).then(
          function (tileset) {
            var schema = tileset.metadata.schema;
            expect(schema).toBeDefined();

            var classes = schema.classes;
            expect(classes.tileset).toBeDefined();
          }
        );
      });

      it("loads metadata with external schema", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetWithExternalSchemaUrl
        ).then(function (tileset) {
          var schema = tileset.metadata.schema;
          expect(schema).toBeDefined();

          var classes = schema.classes;
          expect(classes.tileset).toBeDefined();
        });
      });

      it("loads explicit tileset with tile metadata", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetWithExplicitTileMetadataUrl
        ).then(function (tileset) {
          var expected = {
            "parent.b3dm": {
              color: new Cartesian3(0.5, 0.0, 1.0),
              population: 530,
            },
            "ll.b3dm": {
              color: new Cartesian3(1.0, 1.0, 0.0),
              population: 50,
            },
            "lr.b3dm": {
              color: new Cartesian3(1.0, 0.0, 0.5),
              population: 230,
            },
            "ur.b3dm": {
              color: new Cartesian3(1.0, 0.5, 0.0),
              population: 150,
            },
            "ul.b3dm": {
              color: new Cartesian3(1.0, 0.0, 0.0),
              population: 100,
            },
          };

          var parent = tileset.root;
          var tiles = [parent].concat(parent.children);
          tiles.forEach(function (tile) {
            var uri = tile._header.content.uri;
            var expectedValues = expected[uri];
            var metadata = tile.metadata;
            expect(metadata.getProperty("color")).toEqual(expectedValues.color);
            expect(metadata.getProperty("population")).toEqual(
              expectedValues.population
            );
          });
        });
      });

      it("loads implicit tileset with tile metadata", function () {
        // this tileset is similar to other implicit tilesets, though
        // one tile is removed
        return Cesium3DTilesTester.loadTileset(
          scene,
          tilesetWithImplicitTileMetadataUrl
        ).then(function (tileset) {
          var placeholderTile = tileset.root;

          var transcodedRoot = placeholderTile.children[0];
          var subtree = transcodedRoot.implicitSubtree;
          expect(subtree).toBeDefined();

          var metadataTable = subtree.metadataTable;
          expect(metadataTable).toBeDefined();
          expect(metadataTable.count).toBe(4);
          expect(metadataTable.hasProperty("color")).toBe(true);
          expect(metadataTable.hasProperty("quadrant")).toBe(true);

          var tileCount = 4;
          var expectedQuadrants = [
            "None",
            "Southwest",
            "Northwest",
            "Northeast",
          ];
          var expectedColors = [
            new Cartesian3(255, 255, 255),
            new Cartesian3(255, 0, 0),
            new Cartesian3(0, 255, 0),
            new Cartesian3(0, 0, 255),
          ];

          var tiles = [transcodedRoot].concat(transcodedRoot.children);
          expect(tiles.length).toBe(tileCount);

          var i;
          for (i = 0; i < tileCount; i++) {
            var tile = tiles[i];
            var entityId = subtree.getEntityId(tile.implicitCoordinates);
            var metadata = tile.metadata;
            expect(metadata.getProperty("quadrant")).toBe(
              expectedQuadrants[entityId]
            );
            expect(metadata.getProperty("color")).toEqual(
              expectedColors[entityId]
            );
          }
        });
      });
    });
  },
  "WebGL"
);