text: /**
   * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS-IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  import * as fakeTimers from '@sinonjs/fake-timers';
  import {AmpDocSingle} from '../../src/service/ampdoc-impl';
  import {LayoutPriority} from '../../src/layout';
  import {Resource, ResourceState} from '../../src/service/resource';
  import {ResourcesImpl} from '../../src/service/resources-impl';
  import {Services} from '../../src/services';
  import {Signals} from '../../src/utils/signals';
  import {VisibilityState} from '../../src/visibility-state';
  import {layoutRectLtwh} from '../../src/layout-rect';
  import {loadPromise} from '../../src/event-helper';
  
  /*eslint "google-camelcase/google-camelcase": 0*/
  describes.realWin('Resources', {amp: true}, (env) => {
    let window, document;
    let clock;
    let resources;
  
    beforeEach(() => {
      window = env.win;
      document = window.document;
      delete window.requestIdleCallback;
      delete window.cancelIdleCallback;
      clock = fakeTimers.withGlobal(window).install();
      resources = new ResourcesImpl(new AmpDocSingle(window));
      resources.isRuntimeOn_ = false;
    });
  
    afterEach(() => {
      resources.pass_.cancel();
    });
  
    function createResource(opts) {
      const {
        id,
        isBuilt,
        isBuilding,
        state,
        isDisplayed,
        isFixed,
        isInViewport,
        prerenderAllowed,
        renderOutsideViewport,
        idleRenderOutsideViewport,
        layoutPriority,
        taskId,
      } = {
        id: '1',
        isBuilt: false,
        isBuilding: false,
        state: ResourceState.NOT_BUILT,
        isDisplayed: true,
        isFixed: false,
        isInViewport: true,
        prerenderAllowed: false,
        renderOutsideViewport: false,
        idleRenderOutsideViewport: false,
        layoutPriority: LayoutPriority.CONTENT,
        taskId: 'resource#P',
        ...opts,
      };
  
      const element = document.createElement('amp-el');
      element.V1 = () => false;
      element.isBuilt = () => isBuilt;
      element.isBuilding = () => isBuilding;
  
      const resource = new Resource(id, element, resources);
      env.sandbox.stub(resource, 'getState').returns(state);
      env.sandbox.stub(resource, 'isDisplayed').returns(isDisplayed);
      env.sandbox.stub(resource, 'isFixed').returns(isFixed);
      env.sandbox.stub(resource, 'isInViewport').returns(isInViewport);
      env.sandbox.stub(resource, 'prerenderAllowed').returns(prerenderAllowed);
      env.sandbox
        .stub(resource, 'renderOutsideViewport')
        .returns(renderOutsideViewport);
      env.sandbox
        .stub(resource, 'idleRenderOutsideViewport')
        .returns(idleRenderOutsideViewport);
      env.sandbox.stub(resource, 'getLayoutPriority').returns(layoutPriority);
      env.sandbox.stub(resource, 'getTaskId').returns(taskId);
  
      env.sandbox.stub(resource, 'layoutScheduled');
      env.sandbox.stub(resource, 'startLayout');
  
      return resource;
    }
  
    it('should calculate correct calcTaskScore', () => {
      const viewportRect = layoutRectLtwh(0, 100, 300, 400);
      env.sandbox.stub(resources.viewport_, 'getRect').returns(viewportRect);
  
      // Task 1 is right in the middle of the viewport and priority 0
      const task_in_viewport_p0 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 200, 300, 100);
          },
          isFixed() {
            return false;
          },
        },
        priority: 0,
      };
      // Task 2 is in the viewport and priority 1
      const task_in_viewport_p1 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 200, 300, 100);
          },
          isFixed() {
            return false;
          },
        },
        priority: 1,
      };
      // Task 3 is above viewport and priority 0
      const task_above_viewport_p0 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 0, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 0,
      };
      // Task 4 is above viewport and priority 1
      const task_above_viewport_p1 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 0, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 1,
      };
      // Task 5 is below viewport and priority 0
      const task_below_viewport_p0 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 600, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 0,
      };
      // Task 6 is below viewport and priority 1
      const task_below_viewport_p1 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 600, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 1,
      };
      // Task 7 is fixed right in the middle of the viewport and priority 0
      const task_fixed_in_viewport_p0 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 200, 300, 100);
          },
          isFixed() {
            return false;
          },
        },
        priority: 0,
      };
      // Task 8 is fixed in the viewport and priority 1
      const task_fixed_in_viewport_p1 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 200, 300, 100);
          },
          isFixed() {
            return false;
          },
        },
        priority: 1,
      };
      // Task 9 is fixed above viewport and priority 0
      const task_fixed_above_viewport_p0 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 0, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 0,
      };
      // Task 10 is fixed above viewport and priority 1
      const task_fixed_above_viewport_p1 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 0, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 1,
      };
      // Task 11 is fixed below viewport and priority 0
      const task_fixed_below_viewport_p0 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 600, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 0,
      };
      // Task 12 is fixed below viewport and priority 1
      const task_fixed_below_viewport_p1 = {
        resource: {
          getLayoutBox() {
            return layoutRectLtwh(0, 600, 300, 50);
          },
          isFixed() {
            return false;
          },
        },
        priority: 1,
      };
  
      // 0 for in viewport
      expect(resources.calcTaskScore_(task_in_viewport_p0)).to.equal(0);
      expect(resources.calcTaskScore_(task_in_viewport_p1)).to.equal(10);
  
      // +2 for "one viewport away" * 2 because dir is opposite
      expect(resources.calcTaskScore_(task_above_viewport_p0)).to.equal(2);
      expect(resources.calcTaskScore_(task_above_viewport_p1)).to.equal(12);
  
      // +1 for "one viewport away" * 1 because dir is the same
      expect(resources.calcTaskScore_(task_below_viewport_p0)).to.equal(1);
      expect(resources.calcTaskScore_(task_below_viewport_p1)).to.equal(11);
  
      // 0 for fixed in viewport
      expect(resources.calcTaskScore_(task_fixed_in_viewport_p0)).to.equal(0);
      expect(resources.calcTaskScore_(task_fixed_in_viewport_p1)).to.equal(10);
  
      // +2 for fixed "one viewport away", * 2 because dir is opposite
      expect(resources.calcTaskScore_(task_fixed_above_viewport_p0)).to.equal(2);
      expect(resources.calcTaskScore_(task_fixed_above_viewport_p1)).to.equal(12);
  
      // +1 for fixed "one viewport away" * 1 because dir is the same
      expect(resources.calcTaskScore_(task_fixed_below_viewport_p0)).to.equal(1);
      expect(resources.calcTaskScore_(task_fixed_below_viewport_p1)).to.equal(11);
    });
  
    it('should calculate correct calcTaskTimeout', () => {
      // Task 1 is priority 0
      const task_p0 = {
        priority: 0,
        startTime: 0,
      };
      // Task 2 is priority 1
      const task_p1 = {
        priority: 1,
        startTime: 0,
      };
  
      // Empty pool
      expect(resources.calcTaskTimeout_(task_p0)).to.equal(0);
      expect(resources.calcTaskTimeout_(task_p1)).to.equal(0);
  
      // Idle render penalty after first visible
      resources.firstVisibleTime_ = 0;
      expect(resources.calcTaskTimeout_(task_p0)).to.equal(0);
      expect(resources.calcTaskTimeout_(task_p1)).to.equal(1000);
  
      // Hight priority task in pool
      resources.exec_.tasks_ = [task_p0];
      expect(resources.calcTaskTimeout_(task_p0)).to.equal(0);
      expect(resources.calcTaskTimeout_(task_p1)).to.equal(1000);
  
      clock.tick(100);
      expect(resources.calcTaskTimeout_(task_p1)).to.equal(900);
  
      clock.tick(1000);
      expect(resources.calcTaskTimeout_(task_p1)).to.equal(0);
  
      // Lower priority task in pool
      resources.exec_.tasks_ = [task_p1];
      expect(resources.calcTaskTimeout_(task_p0)).to.equal(0);
      expect(resources.calcTaskTimeout_(task_p1)).to.equal(0);
  
      // Multiple tasks in queue - maximum is picked
      task_p0.startTime = 1100;
      resources.exec_.tasks_ = [task_p0, task_p1];
      expect(resources.calcTaskTimeout_(task_p0)).to.equal(0);
      expect(resources.calcTaskTimeout_(task_p1)).to.equal(1000);
    });
  
    it(
      'should not schedule non-prerenderable resource when' +
        ' document is in prerender',
      () => {
        const resource = createResource({
          state: ResourceState.READY_FOR_LAYOUT,
        });
        resources.visible_ = false;
        env.sandbox
          .stub(resources.ampdoc, 'getVisibilityState')
          .returns(VisibilityState.PRERENDER);
        resources.scheduleLayoutOrPreload(resource, true);
        expect(resources.queue_.getSize()).to.equal(0);
      }
    );
  
    it('should schedule prerenderable resource when document is in prerender', () => {
      const resource = createResource({
        state: ResourceState.READY_FOR_LAYOUT,
        prerenderAllowed: true,
        renderOutsideViewport: true,
        layoutPriority: LayoutPriority.METADATA,
      });
      resources.visible_ = false;
      env.sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.PRERENDER);
      resources.scheduleLayoutOrPreload(resource, true);
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].forceOutsideViewport).to.be.false;
    });
  
    it('should not schedule prerenderable resource when document is hidden', () => {
      const resource = createResource({
        state: ResourceState.READY_FOR_LAYOUT,
        prerenderAllowed: true,
        renderOutsideViewport: true,
        layoutPriority: LayoutPriority.METADATA,
      });
      resources.visible_ = false;
      env.sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.HIDDEN);
      resources.scheduleLayoutOrPreload(resource, true);
      expect(resources.queue_.getSize()).to.equal(0);
    });
  
    it(
      'should not schedule non-renderOutsideViewport resource when' +
        ' resource is not visible',
      () => {
        const resource = createResource({
          state: ResourceState.READY_FOR_LAYOUT,
          isInViewport: false,
          prerenderAllowed: true,
        });
        resources.scheduleLayoutOrPreload(resource, true);
        expect(resources.queue_.getSize()).to.equal(0);
      }
    );
  
    it(
      'should force schedule non-renderOutsideViewport resource when' +
        ' resource is not visible',
      () => {
        const resource = createResource({
          state: ResourceState.READY_FOR_LAYOUT,
          isInViewport: false,
          prerenderAllowed: true,
          layoutPriority: LayoutPriority.METADATA,
          taskId: 'resource#L',
        });
        resources.scheduleLayoutOrPreload(
          resource,
          true,
          0,
          /* ignoreQuota */ true
        );
        expect(resources.queue_.getSize()).to.equal(1);
        expect(resources.queue_.tasks_[0].forceOutsideViewport).to.be.true;
      }
    );
  
    it(
      'should schedule renderOutsideViewport resource when' +
        ' resource is not visible',
      () => {
        const resource = createResource({
          state: ResourceState.READY_FOR_LAYOUT,
          prerenderAllowed: true,
          renderOutsideViewport: true,
          layoutPriority: LayoutPriority.METADATA,
          taskId: 'resource#L',
        });
        resources.scheduleLayoutOrPreload(resource, true);
        expect(resources.queue_.getSize()).to.equal(1);
        expect(resources.queue_.tasks_[0].forceOutsideViewport).to.be.false;
      }
    );
  
    it(
      'should schedule idleRenderOutsideViewport resource when' +
        ' resource is not visible',
      () => {
        const resource = createResource({
          state: ResourceState.READY_FOR_LAYOUT,
          isInViewport: false,
          prerenderAllowed: true,
          idleRenderOutsideViewport: true,
          layoutPriority: LayoutPriority.METADATA,
          taskId: 'resource#L',
        });
        resources.scheduleLayoutOrPreload(resource, true);
        expect(resources.queue_.getSize()).to.equal(1);
        expect(resources.queue_.tasks_[0].forceOutsideViewport).to.be.false;
      }
    );
  
    it('should update priority and schedule pass', () => {
      const element = document.createElement('div');
      element.isBuilt = () => true;
      element.getLayoutPriority = () => LayoutPriority.ADS;
      const resource = new Resource(1, element, resources);
      resources.pass_.cancel();
      expect(resource.getLayoutPriority()).to.equal(LayoutPriority.ADS);
  
      resources.updateLayoutPriority(element, LayoutPriority.METADATA);
      expect(resource.getLayoutPriority()).to.equal(LayoutPriority.METADATA);
      expect(resources.pass_.isPending()).to.be.true;
    });
  
    it('should update priority and update tasks', () => {
      resources.pass_.cancel();
  
      // Target element.
      const element = document.createElement('div');
      element.isBuilt = () => true;
      element.getLayoutPriority = () => LayoutPriority.ADS;
      const resource = new Resource(1, element, resources);
      resources.schedule_(resource, 'L', 0, 0, () => {});
      const task = resources.queue_.tasks_[0];
      expect(task.priority).to.equal(LayoutPriority.ADS);
  
      // Another element.
      const element2 = document.createElement('div');
      element2.isBuilt = () => true;
      element2.getLayoutPriority = () => LayoutPriority.ADS;
      const resource2 = new Resource(2, element2, resources);
      resources.schedule_(resource2, 'L', 0, 0, () => {});
      const task2 = resources.queue_.tasks_[1];
      expect(task2.priority).to.equal(LayoutPriority.ADS);
  
      resources.updateLayoutPriority(element, LayoutPriority.METADATA);
      expect(resource.getLayoutPriority()).to.equal(LayoutPriority.METADATA);
      expect(resources.pass_.isPending()).to.be.true;
      expect(task.priority).to.equal(LayoutPriority.METADATA);
      // The other task is not updated.
      expect(task2.priority).to.equal(LayoutPriority.ADS);
    });
  });
  
  describes.fakeWin(
    'Resources startup',
    {
      win: {
        readyState: 'loading',
      },
      amp: true,
    },
    (env) => {
      let win;
      let clock;
      let resources;
      let schedulePassStub;
      let sandbox;
  
      beforeEach(() => {
        win = env.win;
        sandbox = env.sandbox;
        clock = sandbox.useFakeTimers();
        resources = Services.resourcesForDoc(win.document.body);
        resources.relayoutAll_ = false;
        schedulePassStub = sandbox.stub(resources, 'schedulePass');
      });
  
      it('should run a full reload pass on window.onload', () => {
        expect(resources.relayoutAll_).to.be.false;
        expect(schedulePassStub).to.not.be.called;
        win.readyState = 'complete';
        win.eventListeners.fire({type: 'load'});
        win.document.eventListeners.fire({type: 'readystatechange'});
        return resources.ampdoc
          .whenReady()
          .then(() => {
            return loadPromise(win);
          })
          .then(() => {
            expect(resources.relayoutAll_).to.be.true;
            expect(schedulePassStub).to.have.been.called;
          });
      });
  
      it('should run a full reload pass on fonts timeout', () => {
        win.readyState = 'complete';
        win.document.eventListeners.fire({type: 'readystatechange'});
        let basePassCount = 0;
        return resources.ampdoc
          .whenReady()
          .then(() => {
            expect(resources.relayoutAll_).to.be.false;
            basePassCount = schedulePassStub.callCount;
            clock.tick(3100);
          })
          .then(() => {
            expect(resources.relayoutAll_).to.be.true;
            expect(schedulePassStub).to.have.callCount(basePassCount + 1);
          });
      });
  
      it('should run a full reload pass on document.fonts.ready', () => {
        win.readyState = 'interactive';
        win.document.eventListeners.fire({type: 'readystatechange'});
        win.document.fonts.status = 'loading';
        let basePassCount = 0;
        return resources.ampdoc
          .whenReady()
          .then(() => {})
          .then(() => {
            // This is the regular remeasure on doc-ready.
            expect(resources.relayoutAll_).to.be.true;
            resources.relayoutAll_ = false;
            basePassCount = schedulePassStub.callCount;
            return win.document.fonts.ready;
          })
          .then(() => {
            // Wait one micro task.
            return Promise.resolve();
          })
          .then(() => {
            expect(resources.relayoutAll_).to.be.true;
            // Remeasure on doc-ready and fonts-ready.
            expect(schedulePassStub).to.have.callCount(basePassCount + 1);
          });
      });
  
      it('should not remeasure if fonts load before doc-ready', () => {
        win.readyState = 'interactive';
        win.document.eventListeners.fire({type: 'readystatechange'});
        win.document.fonts.status = 'loaded';
        let basePassCount = 0;
        return resources.ampdoc
          .whenReady()
          .then(() => {})
          .then(() => {
            // This is the regular remeasure on doc-ready.
            expect(resources.relayoutAll_).to.be.true;
            resources.relayoutAll_ = false;
            basePassCount = schedulePassStub.callCount;
            return win.document.fonts.ready;
          })
          .then(() => {
            // Wait one micro task.
            return Promise.resolve();
          })
          .then(() => {
            expect(resources.relayoutAll_).to.be.false;
            // Only remeasure on doc-ready.
            expect(schedulePassStub).to.have.callCount(basePassCount);
          });
      });
  
      it('should run a full reload when a new element is connected', () => {
        expect(resources.relayoutAll_).to.be.false;
        expect(schedulePassStub).to.not.be.called;
        const el = win.document.createElement('amp-img');
        el.isBuilt = () => {
          return true;
        };
        el.isUpgraded = () => {
          return true;
        };
        el.isRelayoutNeeded = () => {
          return true;
        };
        el.updateLayoutBox = () => {};
        win.document.body.appendChild(el);
        resources.add(el);
        expect(resources.relayoutAll_).to.be.false;
        clock.tick(1000);
        expect(resources.relayoutAll_).to.be.true;
      });
    }
  );
  
  describes.realWin('Resources discoverWork', {amp: true}, (env) => {
    function createElement(rect) {
      const element = env.win.document.createElement('amp-test');
      element.classList.add('i-amphtml-element');
      element.V1 = () => false;
      element.signals = () => new Signals();
      element.whenBuilt = () => Promise.resolve();
      element.isBuilt = () => true;
      element.buildInternal = () => Promise.resolve();
      element.isUpgraded = () => true;
      element.updateLayoutBox = () => {};
      element.getPlaceholder = () => null;
      element.getLayoutPriority = () => LayoutPriority.CONTENT;
      element.getLayout = () => 'fixed';
  
      element.idleRenderOutsideViewport = () => true;
      element.isInViewport = () => false;
      element.getAttribute = () => null;
      element.hasAttribute = () => false;
      element.getBoundingClientRect = () => rect;
      element.layoutCallback = () => Promise.resolve();
      element.viewportCallback = sandbox.spy();
      element.prerenderAllowed = () => true;
      element.renderOutsideViewport = () => true;
      element.isRelayoutNeeded = () => true;
      element.pauseCallback = () => {};
      element.unlayoutCallback = () => true;
      element.unlayoutOnPause = () => true;
      element.togglePlaceholder = () => sandbox.spy();
      element.fakeComputedStyle = {
        marginTop: '0px',
        marginRight: '0px',
        marginBottom: '0px',
        marginLeft: '0px',
      };
      env.win.document.body.appendChild(element);
      return element;
    }
  
    function createResource(id, rect) {
      const resource = new Resource(id, createElement(rect), resources);
      resource.state_ = ResourceState.READY_FOR_LAYOUT;
      resource.layoutBox_ = rect;
      return resource;
    }
  
    let viewportMock;
    let resources;
    let resource1, resource2;
    let sandbox;
  
    beforeEach(() => {
      sandbox = env.sandbox;
  
      const viewer = Services.viewerForDoc(env.ampdoc);
      sandbox.stub(viewer, 'isRuntimeOn').returns(true);
      resources = new ResourcesImpl(env.ampdoc);
      resources.remeasurePass_.schedule = () => {};
      resources.pass_.schedule = () => {};
      viewportMock = sandbox.mock(resources.viewport_);
  
      sandbox.stub(env.win, 'getComputedStyle').callsFake((el) => {
        return el.fakeComputedStyle
          ? el.fakeComputedStyle
          : window.getComputedStyle(el);
      });
  
      resource1 = createResource(1, layoutRectLtwh(10, 10, 100, 100));
      resource2 = createResource(2, layoutRectLtwh(10, 1010, 100, 100));
      resources.resources_ = [resource1, resource2];
      resources.vsync_ = {
        mutate: (callback) => callback(),
        measure: (callback) => callback(),
        measurePromise: (callback) => Promise.resolve(callback()),
      };
    });
  
    afterEach(() => {
      viewportMock.verify();
    });
  
    it('should set ready-scan signal on first ready pass after amp init', () => {
      resources.isRuntimeOn_ = true;
      resources.documentReady_ = true;
      resources.firstPassAfterDocumentReady_ = true;
      sandbox.stub(resources.visibilityStateMachine_, 'setState');
      resources.doPass();
      expect(resources.ampdoc.signals().get('ready-scan')).to.be.null;
      resources.ampInitComplete();
      resources.doPass();
      resources.isRuntimeOn_ = false;
      expect(resources.ampdoc.signals().get('ready-scan')).to.be.ok;
    });
  
    it('should measure unbuilt elements', () => {
      resources.visible_ = true;
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.VISIBLE);
      viewportMock.expects('getRect').returns(layoutRectLtwh(0, 0, 300, 400));
      resource1.isBuilt = () => false;
      expect(resource1.hasBeenMeasured()).to.be.false;
      resource1.isBuilt = () => false;
  
      resources.discoverWork_();
  
      expect(resource1.hasBeenMeasured()).to.be.true;
    });
  
    describe('intersect-resources', () => {
      beforeEach(() => {
        // Enable "intersect-resources" experiment.
        resources.intersectionObserver_ = {};
        resource1.intersect_ = resource2.intersect_ = true;
      });
  
      it('should not force relayout after build', () => {
        resources.relayoutAll_ = false;
  
        // Unmeasured elements.
        env.sandbox.stub(resource1, 'hasBeenMeasured').returns(false);
  
        // Measured elements that need relayout.
        env.sandbox.stub(resource2, 'hasBeenMeasured').returns(true);
        env.sandbox
          .stub(resource2, 'getState')
          .returns(ResourceState.NOT_LAID_OUT);
  
        resources.discoverWork_();
      });
  
      it('should invalidate premeasurements after resize event', () => {
        resource1.premeasure({});
        expect(resource1.hasBeenPremeasured()).true;
        expect(resource1.isMeasureRequested()).false;
        resources.viewport_.changeObservable_.fire({relayoutAll_: true});
        expect(resource1.hasBeenPremeasured()).false;
        expect(resource1.isMeasureRequested()).true;
      });
  
      it('should schedule a pass after resize event', () => {
        const schedulePassStub = sandbox.stub(resources, 'schedulePass');
        resources.viewport_.changeObservable_.fire({relayoutAll_: false});
        expect(schedulePassStub).calledOnce;
      });
    });
  
    it('should render two screens when visible', () => {
      resources.visible_ = true;
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.VISIBLE);
      viewportMock.expects('getRect').returns(layoutRectLtwh(0, 0, 300, 400));
  
      resources.discoverWork_();
  
      expect(resources.queue_.getSize()).to.equal(2);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
      expect(resources.queue_.tasks_[1].resource).to.equal(resource2);
    });
  
    it('should NOT rerender anything', () => {
      resource1.state_ = ResourceState.LAYOUT_COMPLETE;
      resource2.state_ = ResourceState.LAYOUT_COMPLETE;
      resources.visible_ = true;
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.VISIBLE);
      viewportMock.expects('getRect').returns(layoutRectLtwh(0, 0, 300, 400));
  
      resources.discoverWork_();
  
      expect(resources.queue_.getSize()).to.equal(0);
    });
  
    it('should re-render from requested position', () => {
      resource1.state_ = ResourceState.LAYOUT_COMPLETE;
      resource2.state_ = ResourceState.LAYOUT_COMPLETE;
      resource1.hasBeenMeasured = () => true;
      resource2.hasBeenMeasured = () => true;
      resource1.element.getBoundingClientRect = () =>
        layoutRectLtwh(10, 10, 100, 101);
      resource2.element.getBoundingClientRect = () =>
        layoutRectLtwh(10, 1010, 100, 101);
      resources.visible_ = true;
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.VISIBLE);
      resources.relayoutAll_ = false;
      resources.relayoutTop_ = 1000;
      viewportMock.expects('getRect').returns(layoutRectLtwh(0, 0, 300, 400));
  
      resources.discoverWork_();
  
      expect(resources.relayoutTop_).to.equal(-1);
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource2);
      expect(resource1.state_).to.equal(ResourceState.LAYOUT_COMPLETE);
      expect(resource2.state_).to.equal(ResourceState.LAYOUT_SCHEDULED);
    });
  
    it('should prerender only one screen in visibilityState=prerender', () => {
      resources.visible_ = false;
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.PRERENDER);
      viewportMock.expects('getRect').returns(layoutRectLtwh(0, 0, 300, 1009));
  
      resources.discoverWork_();
  
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
    });
  
    // TODO(dvoytenko, #12476): Make this test work with sinon 4.0.
    it.skip('should remeasure when requested and scheduled unloads', () => {
      resource1.state_ = ResourceState.LAYOUT_COMPLETE;
      resource2.state_ = ResourceState.LAYOUT_COMPLETE;
      resources.visible_ = true;
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.VISIBLE);
      viewportMock.expects('getRect').returns(layoutRectLtwh(0, 0, 300, 400));
  
      const resource1MeasureStub = sandbox
        .stub(resource1, 'measure')
        .callsFake(resource1.measure.bind(resource1));
      const resource1UnloadStub = sandbox.stub(resource1, 'unload');
      const resource2MeasureStub = sandbox
        .stub(resource2, 'measure')
        .callsFake(resource2.measure.bind(resource2));
      const resource2UnloadStub = sandbox.stub(resource2, 'unload');
  
      // 1st pass: measure for the first time.
      resources.discoverWork_();
      expect(resource1MeasureStub).to.be.calledOnce;
      expect(resource1UnloadStub).to.have.not.been.called;
      expect(resource2MeasureStub).to.be.calledOnce;
      expect(resource2UnloadStub).to.have.not.been.called;
  
      // 2nd pass: do not remeasure anything.
      resources.discoverWork_();
      expect(resource1MeasureStub).to.be.calledOnce;
      expect(resource1UnloadStub).to.have.not.been.called;
      expect(resource2MeasureStub).to.be.calledOnce;
      expect(resource2UnloadStub).to.have.not.been.called;
  
      // 3rd pass: request remeasures and an unload.
      resource1.requestMeasure();
      resource2.requestMeasure();
      expect(resource1.isMeasureRequested()).to.be.true;
      expect(resource2.isMeasureRequested()).to.be.true;
      resource2.element.getBoundingClientRect = () => layoutRectLtwh(0, 0, 0, 0); // Equiv to display:none.
      resources.discoverWork_();
      expect(resource1MeasureStub).to.have.callCount(2);
      expect(resource1UnloadStub).to.have.not.been.called;
      expect(resource2MeasureStub).to.have.callCount(2);
      expect(resource2UnloadStub).to.be.calledOnce;
    });
  
    it('should eject stale tasks when element unloaded', () => {
      const pendingResource = createResource(5, layoutRectLtwh(0, 0, 0, 0));
      pendingResource.state_ = ResourceState.NOT_BUILT;
      resources.pendingBuildResources_ = [pendingResource];
      resources.visible_ = true;
      // Don't resolve layout - immulating DOM being removed and load
      // promise not resolving.
      resource2.layoutCallback = new Promise((unusedResolve) => {});
      resource2.unlayoutCallback = () => true;
      resource2.prerenderAllowed = () => false;
  
      resource1.layoutCallback = new Promise((unusedResolve) => {});
      resource1.unlayoutCallback = () => true;
  
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.VISIBLE);
      viewportMock
        .expects('getRect')
        .returns(layoutRectLtwh(0, 0, 300, 400))
        .atLeast(1);
  
      resources.discoverWork_();
      expect(resources.queue_.getSize()).to.equal(2);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
      expect(resources.queue_.tasks_[1].resource).to.equal(resource2);
      expect(resources.pendingBuildResources_.length).to.equal(1);
  
      resources.work_();
      expect(resources.exec_.getSize()).to.equal(2);
  
      // Remove unloaded resources from exec queue.
      resource2.abortController_ = null;
      resource2.unload();
      resources.cleanupTasks_(resource2);
      expect(resources.exec_.getSize()).to.equal(1);
  
      // Shouldn't remove tasks if the resource is not unloaded.
      resources.cleanupTasks_(resource1);
      expect(resources.exec_.getSize()).to.equal(1);
  
      // Can successfully reschedules unloaded elements.
      resources.discoverWork_();
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource2);
  
      // Removes them even from scheduling queue.
      resource2.abortController_ = null;
      resource2.unload();
      resources.cleanupTasks_(resource2, /* opt_removePending */ true);
      expect(resources.queue_.getSize()).to.equal(0);
      expect(resources.pendingBuildResources_.length).to.equal(1);
  
      const pendingElement = {'__AMP__RESOURCE': pendingResource};
      resources.remove(pendingElement);
      expect(resources.pendingBuildResources_.length).to.equal(0);
    });
  
    it('should schedule resource for execution', () => {
      resources.scheduleLayoutOrPreload(resource1, true);
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
  
      const measureSpy = sandbox.spy(resource1, 'measure');
      resources.work_();
      expect(resources.exec_.getSize()).to.equal(1);
      expect(measureSpy).to.be.calledOnce;
      expect(resource1.getState()).to.equal(ResourceState.LAYOUT_SCHEDULED);
    });
  
    it('should record layout schedule time on the resource element', () => {
      resources.scheduleLayoutOrPreload(resource1, true);
  
      resources.work_();
      expect(resource1.getState()).to.equal(ResourceState.LAYOUT_SCHEDULED);
      expect(resource1.element.layoutScheduleTime).to.be.greaterThan(0);
    });
  
    describe('intersect-resources', () => {
      beforeEach(() => {
        // Enables the "intersect-resources" experiment.
        resources.intersectionObserver_ = {};
      });
  
      it('should not remeasure before layout', () => {
        sandbox.stub(resource1, 'hasBeenPremeasured').returns(false);
        sandbox.stub(resource1, 'measure');
  
        resources.scheduleLayoutOrPreload(resource1, /* layout */ true);
        resources.work_();
  
        expect(resources.exec_.getSize()).to.equal(1);
        expect(resource1.measure).to.not.be.called;
        expect(resource1.getState()).to.equal(ResourceState.LAYOUT_SCHEDULED);
      });
  
      it('should check premeasured rect before layout', () => {
        sandbox.stub(resource1, 'hasBeenMeasured').returns(true);
        sandbox.stub(resource1, 'hasBeenPremeasured').returns(true);
        sandbox.stub(resource1, 'isDisplayed').returns(true);
        resource1.isDisplayed
          .withArgs(/* usePremeasuredRect */ true)
          .returns(false);
        sandbox.spy(resource1, 'layoutCanceled');
  
        resources.scheduleLayoutOrPreload(resource1, /* layout */ true);
        resources.work_();
  
        expect(resources.exec_.getSize()).to.equal(0);
        expect(resource1.isDisplayed).to.be.calledWith(
          /* usePremeasuredRect */ true
        );
        expect(resource1.layoutCanceled).to.be.calledOnce;
        expect(resource1.getState()).to.equal(ResourceState.READY_FOR_LAYOUT);
      });
    });
  
    it('should not schedule resource execution outside viewport', () => {
      resources.scheduleLayoutOrPreload(resource1, true);
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
  
      const measureSpy = sandbox.spy(resource1, 'measure');
      const layoutCanceledSpy = sandbox.spy(resource1, 'layoutCanceled');
      sandbox.stub(resource1, 'isInViewport').returns(false);
      sandbox.stub(resource1, 'renderOutsideViewport').returns(false);
      sandbox.stub(resource1, 'idleRenderOutsideViewport').returns(false);
      resources.work_();
      expect(resources.exec_.getSize()).to.equal(0);
      expect(measureSpy).to.be.calledOnce;
      expect(layoutCanceledSpy).to.be.calledOnce;
      expect(resource1.getState()).to.equal(ResourceState.READY_FOR_LAYOUT);
    });
  
    it('should force schedule resource execution outside viewport', () => {
      resources.scheduleLayoutOrPreload(
        resource1,
        true,
        0,
        /* ignoreQuota */ true
      );
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
  
      const measureSpy = sandbox.spy(resource1, 'measure');
      sandbox.stub(resource1, 'isInViewport').returns(false);
      sandbox.stub(resource1, 'renderOutsideViewport').returns(false);
      sandbox.stub(resource1, 'idleRenderOutsideViewport').returns(false);
      resources.work_();
      expect(resources.exec_.getSize()).to.equal(1);
      expect(measureSpy).to.be.calledOnce;
      expect(resource1.getState()).to.equal(ResourceState.LAYOUT_SCHEDULED);
    });
  
    it('should schedule resource prerender when doc in prerender mode', () => {
      resources.scheduleLayoutOrPreload(resource1, true);
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
  
      resources.visible_ = false;
      sandbox.stub(resources.ampdoc, 'getVisibilityState').returns('prerender');
      sandbox.stub(resource1, 'isInViewport').returns(true);
      sandbox.stub(resource1, 'prerenderAllowed').returns(true);
  
      const measureSpy = sandbox.spy(resource1, 'measure');
      const layoutCanceledSpy = sandbox.spy(resource1, 'layoutCanceled');
      resources.work_();
      expect(resources.exec_.getSize()).to.equal(1);
      expect(measureSpy).to.be.calledOnce;
      expect(layoutCanceledSpy).to.not.be.called;
      expect(resource1.getState()).to.equal(ResourceState.LAYOUT_SCHEDULED);
    });
  
    it('should not schedule resource prerender', () => {
      resources.scheduleLayoutOrPreload(resource1, true);
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
  
      resources.visible_ = false;
      sandbox.stub(resources.ampdoc, 'getVisibilityState').returns('prerender');
      sandbox.stub(resource1, 'isInViewport').returns(true);
      sandbox.stub(resource1, 'prerenderAllowed').returns(false);
  
      const measureSpy = sandbox.spy(resource1, 'measure');
      const layoutCanceledSpy = sandbox.spy(resource1, 'layoutCanceled');
      resources.work_();
      expect(resources.exec_.getSize()).to.equal(0);
      expect(measureSpy).to.be.calledOnce;
      expect(layoutCanceledSpy).to.be.calledOnce;
      expect(resource1.getState()).to.equal(ResourceState.READY_FOR_LAYOUT);
    });
  
    it('should schedule resource execution when doc is hidden', () => {
      resources.scheduleLayoutOrPreload(resource1, true);
      expect(resources.queue_.getSize()).to.equal(1);
      expect(resources.queue_.tasks_[0].resource).to.equal(resource1);
  
      resources.visible_ = false;
      sandbox.stub(resources.ampdoc, 'getVisibilityState').returns('hidden');
      sandbox.stub(resource1, 'isInViewport').returns(true);
      sandbox.stub(resource1, 'prerenderAllowed').returns(true);
  
      const measureSpy = sandbox.spy(resource1, 'measure');
      const layoutCanceledSpy = sandbox.spy(resource1, 'layoutCanceled');
      resources.work_();
      expect(resources.exec_.getSize()).to.equal(0);
      expect(measureSpy).to.be.calledOnce;
      expect(layoutCanceledSpy).to.be.calledOnce;
    });
  
    // TODO (#16156): this test results in too many calls to getRect on Safari
    // TODO(jridgewell, #21546): fix this flaky test. Originally configured with:
    // it.configure().skipSafari().run(
    it.skip('should update inViewport before scheduling layouts', () => {
      resources.visible_ = true;
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.VISIBLE);
      viewportMock.expects('getRect').returns(layoutRectLtwh(0, 0, 300, 400));
      const setInViewport = sandbox.spy(resource1, 'setInViewport');
      const schedule = sandbox.spy(resources, 'scheduleLayoutOrPreload');
  
      resources.discoverWork_();
  
      expect(setInViewport).to.have.been.calledBefore(schedule);
      expect(setInViewport).to.have.been.calledWith(true);
    });
  
    it('should build resource when not built', () => {
      const buildResourceSpy = sandbox.spy(resources, 'buildResourceUnsafe_');
      sandbox.stub(resources, 'schedule_');
      resources.documentReady_ = true;
      resource1.element.isBuilt = sandbox
        .stub()
        .onFirstCall()
        .returns(true)
        .onSecondCall()
        .returns(false);
      resource2.element.idleRenderOutsideViewport = () => false;
      resource1.state_ = ResourceState.NOT_BUILT;
      resource1.build = sandbox.spy();
  
      resources.discoverWork_();
  
      expect(resource1.build).to.be.calledOnce;
      expect(buildResourceSpy).calledWithExactly(
        resource1,
        /* ignoreQuota */ false
      );
    });
  
    it('should build resource when not built and before doc ready', () => {
      const buildResourceSpy = sandbox.spy(resources, 'buildResourceUnsafe_');
      sandbox.stub(resources, 'schedule_');
      resources.documentReady_ = false;
      sandbox.stub(resource1.element, 'nextSibling').returns({});
      resource1.element.isBuilt = sandbox
        .stub()
        .onFirstCall()
        .returns(false)
        .onSecondCall()
        .returns(true);
      resource2.element.idleRenderOutsideViewport = () => false;
      resource1.state_ = ResourceState.NOT_BUILT;
      resource1.build = sandbox.spy();
  
      resources.discoverWork_();
  
      expect(resource1.build).to.be.calledOnce;
      expect(buildResourceSpy).calledWithExactly(resource1);
    });
  
    it('should NOT build non-prerenderable resources in prerender', () => {
      sandbox
        .stub(resources.ampdoc, 'getVisibilityState')
        .returns(VisibilityState.PRERENDER);
      sandbox.stub(resources, 'schedule_');
      resources.documentReady_ = true;
  
      resource1.element.isBuilt = () => false;
      resource1.prerenderAllowed = () => false;
      resource1.state_ = ResourceState.NOT_BUILT;
      resource1.build = sandbox.spy();
      resource2.element.idleRenderOutsideViewport = () => false;
  
      resources.discoverWork_();
  
      expect(resource1.build).to.not.be.called;
    });
  
    it('should NOT build when quota reached', () => {
      sandbox.stub(resources.ampdoc, 'hasBeenVisible').returns(false);
      sandbox.stub(resources, 'schedule_');
      resources.documentReady_ = true;
      resources.buildAttemptsCount_ = 21; // quota is 20
  
      resource1.element.isBuilt = () => false;
      resource1.element.idleRenderOutsideViewport = () => true;
      resource1.prerenderAllowed = () => true;
      resource1.isBuildRenderBlocking = () => false;
      resource1.state_ = ResourceState.NOT_BUILT;
      resource1.build = sandbox.spy();
  
      resources.buildOrScheduleBuildForResource_(resource1);
      expect(resource1.build).to.not.be.called;
    });
  
    it('should build render blocking resource even if quota is reached', () => {
      sandbox.stub(resources.ampdoc, 'hasBeenVisible').returns(false);
      sandbox.stub(resources, 'schedule_');
      resources.documentReady_ = true;
      resources.buildAttemptsCount_ = 21; // quota is 20
  
      resource1.element.isBuilt = () => false;
      resource1.element.idleRenderOutsideViewport = () => true;
      resource1.prerenderAllowed = () => true;
      resource1.isBuildRenderBlocking = () => true;
      resource1.state_ = ResourceState.NOT_BUILT;
      resource1.build = sandbox.spy();
  
      resources.buildOrScheduleBuildForResource_(resource1);
      expect(resource1.build).to.be.called;
    });
  
    it('should layout resource if outside viewport but idle', () => {
      const schedulePassStub = sandbox.stub(resources, 'schedulePass');
      resources.documentReady_ = true;
      sandbox.stub(resource1.element, 'nextSibling').returns({});
      resource1.element.isBuilt = () => true;
      resource1.element.renderOutsideViewport = () => false;
      resource1.element.idleRenderOutsideViewport = () => true;
      resource2.element.renderOutsideViewport = () => false;
      resource2.element.idleRenderOutsideViewport = () => false;
      resource1.state_ = ResourceState.READY_FOR_LAYOUT;
  
      resources.discoverWork_();
  
      expect(schedulePassStub).to.be.calledOnce;
    });
  
    it('should force build resources during discoverWork layout phase', () => {
      const buildResourceSpy = sandbox.spy(resources, 'buildResourceUnsafe_');
      sandbox.stub(resources, 'schedule_');
      resources.documentReady_ = true;
      // Emulates a resource not building.
      resource1.element.isBuilt = sandbox.stub().returns(false);
      resource2.element.idleRenderOutsideViewport = () => false;
      resource1.state_ = ResourceState.NOT_BUILT;
      resource1.build = sandbox.spy();
  
      resources.discoverWork_();
  
      expect(resource1.build).to.be.calledTwice;
      // discoverWork_ phase 1 build.
      expect(buildResourceSpy).calledWithExactly(
        resource1,
        /* ignoreQuota */ false
      );
      // discoverWork_ phase 4 layout grants build.
      expect(buildResourceSpy).calledWithExactly(
        resource1,
        /* ignoreQuota */ true
      );
    });
  
    describe('onNextPass', () => {
      it('should only run callbacks once.', () => {
        resources.isRuntimeOn_ = true;
        resources.documentReady_ = true;
        resources.firstPassAfterDocumentReady_ = true;
  
        const passCallback = sandbox.spy();
        resources.onNextPass(passCallback);
  
        resources.doPass();
        expect(passCallback).to.be.calledOnce;
  
        resources.doPass();
        expect(passCallback).to.be.calledOnce;
      });
    });
  });
  
  describes.realWin(
    'Resources contentHeight',
    {
      amp: {
        runtimeOn: true,
      },
    },
    (env) => {
      let win;
      let resources;
      let viewerSendMessageStub, viewportContentHeightChangedStub;
      let sandbox;
  
      beforeEach(() => {
        win = env.win;
        sandbox = env.sandbox;
        resources = win.__AMP_SERVICES.resources.obj;
        viewerSendMessageStub = sandbox.stub(resources.viewer_, 'sendMessage');
        viewportContentHeightChangedStub = sandbox.stub(
          resources.viewport_,
          'contentHeightChanged'
        );
        sandbox.stub(resources.vsync_, 'run').callsFake((task) => {
          task.measure({});
        });
      });
  
      it('should measure initial contentHeight', () => {
        const contentHeight = resources.viewport_.getContentHeight();
        expect(resources.maybeChangeHeight_).to.equal(false);
        expect(resources.documentReady_).to.equal(true);
        expect(resources.contentHeight_).to.equal(contentHeight);
      });
  
      it('should only send contentHeight to the viewer once amp finishes init', () => {
        resources.firstPassAfterDocumentReady_ = false;
        resources.documentReady_ = false;
        resources.ampInitialized_ = false;
        resources.doPass();
        expect(viewerSendMessageStub).not.called;
  
        resources.firstPassAfterDocumentReady_ = true;
        resources.documentReady_ = true;
        resources.ampInitialized_ = true;
        resources.doPass();
        expect(viewerSendMessageStub).calledWithExactly(
          'documentHeight',
          {height: 0},
          true
        );
      });
  
      it('should send contentHeight to viewer if height was changed', () => {
        sandbox.stub(resources.viewport_, 'getContentHeight').returns(200);
        resources.maybeChangeHeight_ = true;
  
        resources.doPass();
  
        expect(resources.maybeChangeHeight_).to.equal(false);
        expect(resources.contentHeight_).to.equal(200);
        expect(viewerSendMessageStub).to.be.calledOnce;
        expect(viewerSendMessageStub.lastCall.args[0]).to.equal('documentHeight');
        expect(viewerSendMessageStub.lastCall.args[1].height).to.equal(200);
        expect(viewerSendMessageStub.lastCall.args[2]).to.equal(true);
        expect(viewportContentHeightChangedStub).to.be.calledOnce;
      });
  
      it('should not send contentHeight to viewer if height is not changed', () => {
        const contentHeight = resources.viewport_.getContentHeight();
        resources.maybeChangeHeight_ = true;
  
        resources.doPass();
  
        expect(resources.maybeChangeHeight_).to.equal(false);
        expect(resources.contentHeight_).to.equal(contentHeight);
        expect(viewerSendMessageStub).to.not.be.called;
        expect(viewportContentHeightChangedStub).to.not.be.called;
      });
  
      it('should send contentHeight to viewer if viewport resizes', () => {
        sandbox.stub(resources.viewport_, 'getContentHeight').returns(200);
        resources.viewport_.changed_(/* relayoutAll */ true, /* velocity */ 0);
        resources.doPass();
  
        expect(resources.maybeChangeHeight_).to.equal(false);
        expect(resources.contentHeight_).to.equal(200);
        expect(viewerSendMessageStub).to.be.calledOnce;
        expect(viewerSendMessageStub.lastCall.args[0]).to.equal('documentHeight');
        expect(viewerSendMessageStub.lastCall.args[1].height).to.equal(200);
        expect(viewerSendMessageStub.lastCall.args[2]).to.equal(true);
        expect(viewportContentHeightChangedStub).to.be.calledOnce;
      });
    }
  );
  
  describes.fakeWin('Resources.add/upgrade/remove', {amp: true}, (env) => {
    let resources;
    let parent;
    let parentResource;
    let child1;
    let resource1;
    let child2;
    let resource2;
    let sandbox;
  
    function createElement() {
      const signals = new Signals();
      const element = {
        ownerDocument: {defaultView: window},
        tagName: 'amp-test',
        hasAttribute() {
          return false;
        },
        isBuilding() {
          return false;
        },
        isBuilt() {
          return true;
        },
        isUpgraded() {
          return true;
        },
        reconstructWhenReparented() {
          return true;
        },
        pauseCallback() {},
        resumeCallback() {},
        updateLayoutBox() {},
        getBoundingClientRect() {
          return layoutRectLtwh(0, 0, 0, 0);
        },
        signals() {
          return signals;
        },
      };
      element.buildInternal = sandbox.stub().returns(Promise.resolve());
      return element;
    }
  
    function createElementWithResource(id) {
      const element = createElement();
      const resource = new Resource(id, element, resources);
      resource.state_ = ResourceState.NOT_BUILT;
      resource.element['__AMP__RESOURCE'] = resource;
      return [element, resource];
    }
  
    function stubBuild(resource) {
      const origBuild = resource.build;
      sandbox.stub(resource, 'build').callsFake(() => {
        resource.buildPromise = origBuild.call(resource);
        return resource.buildPromise;
      });
      return resource;
    }
  
    beforeEach(() => {
      sandbox = env.sandbox;
      const infPromise = new Promise(() => {});
      sandbox.stub(env.ampdoc, 'whenReady').returns(infPromise);
      resources = new ResourcesImpl(env.ampdoc);
      resources.isBuildOn_ = true;
      resources.pendingBuildResources_ = [];
      parent = createElementWithResource(1)[0];
      parentResource = parent['__AMP__RESOURCE'];
      child1 = createElementWithResource(2)[0];
      resource1 = child1['__AMP__RESOURCE'];
      child2 = createElementWithResource(3)[0];
      resource2 = child2['__AMP__RESOURCE'];
    });
  
    it('should enforce that viewport is ready for first add', () => {
      const ensureViewportReady = sandbox.stub(
        resources.viewport_,
        'ensureReadyForElements'
      );
      resources.add(child1);
      expect(ensureViewportReady).to.be.calledOnce;
  
      // Second `add` is ignored.
      resources.add(child2);
      expect(ensureViewportReady).to.be.calledOnce;
    });
  
    it('should build elements immediately if the document is ready', () => {
      const schedulePassStub = sandbox.stub(resources, 'schedulePass');
      child1.isBuilt = () => false;
      child2.isBuilt = () => false;
      resources.documentReady_ = false;
      resources.add(child1);
      resources.upgraded(child1);
      expect(child1.buildInternal).to.not.be.called;
      resources.documentReady_ = true;
      resources.add(child2);
      const resource2 = stubBuild(Resource.forElementOptional(child2));
      resources.upgraded(child2);
      expect(child2.buildInternal).to.be.calledOnce;
      expect(schedulePassStub).to.not.be.called;
      return resource2.buildPromise.then(() => {
        expect(schedulePassStub).to.be.calledOnce;
      });
    });
  
    // TODO(jridgewell, #15748): Fails on Safari 11.1.0.
    it.configure().skipSafari(
      'should not schedule pass when immediate build fails',
      () => {
        const schedulePassStub = sandbox.stub(resources, 'schedulePass');
        child1.isBuilt = () => false;
        const child1BuildSpy = sandbox.spy();
        child1.buildInternal = () => {
          // Emulate an error happening during an element build.
          child1BuildSpy();
          return Promise.reject(new Error('child1-build-error'));
        };
        resources.documentReady_ = true;
        resources.add(child1);
        const resource1 = stubBuild(Resource.forElementOptional(child1));
        resources.upgraded(child1);
        expect(resources.get()).to.contain(resource1);
        return resource1.buildPromise.then(
          () => {
            throw new Error('must have failed');
          },
          () => {
            expect(child1BuildSpy).to.be.calledOnce;
            expect(schedulePassStub).to.not.be.called;
            expect(resources.get()).to.not.contain(resource1);
          }
        );
      }
    );
  
    // TODO(amphtml, #15748): Fails on Safari 11.1.0.
    it.configure().skipSafari(
      'should add element to pending build when document is not ready',
      () => {
        child1.isBuilt = () => false;
        child2.isBuilt = () => false;
        resources.buildReadyResources_ = sandbox.spy();
        resources.documentReady_ = false;
        resources.add(child1);
        resources.upgraded(child1);
        expect(child1.buildInternal.called).to.be.false;
        expect(resources.pendingBuildResources_.length).to.be.equal(1);
        resources.add(child2);
        resources.upgraded(child2);
        expect(child2.buildInternal.called).to.be.false;
        expect(resources.pendingBuildResources_.length).to.be.equal(2);
        expect(resources.buildReadyResources_.calledTwice).to.be.true;
        const resource1 = Resource.forElementOptional(child1);
        const resource2 = Resource.forElementOptional(child2);
        expect(resources.get()).to.contain(resource1);
        expect(resources.get()).to.contain(resource2);
        expect(resource1.isBuilding()).to.be.false;
        expect(resource2.isBuilding()).to.be.false;
      }
    );
  
    it('should observe element after adding it', () => {
      // Enables the 'intersect-resources' experiment.
      const observer = (resources.intersectionObserver_ = {
        observe: env.sandbox.spy(),
      });
      // Avoid creating a new Resource, which is tricky to spy on.
      env.sandbox.stub(child1, 'reconstructWhenReparented').returns(false);
      env.sandbox.stub(resource1, 'getState').returns(ResourceState.NOT_LAID_OUT);
      env.sandbox.spy(resource1, 'requestMeasure');
  
      resources.add(child1);
  
      expect(resource1.requestMeasure).to.be.calledOnce;
      expect(observer.observe).to.be.calledOnceWith(child1);
    });
  
    describe('buildReadyResources_', () => {
      let schedulePassStub;
  
      beforeEach(() => {
        schedulePassStub = sandbox.stub(resources, 'schedulePass');
        resources.isBuildOn_ = true;
        resources.documentReady_ = false;
        resource1 = stubBuild(resource1);
        resource2 = stubBuild(resource2);
        parentResource = stubBuild(parentResource);
        resources.resources_ = [resource1, resource2];
      });
  
      // TODO(amphtml, #15748): Fails on Safari 11.1.0.
      it.configure().skipSafari(
        'should build ready resources and remove them from pending',
        () => {
          resources.pendingBuildResources_ = [resource1, resource2];
          resources.buildReadyResources_();
          expect(child1.buildInternal.called).to.be.false;
          expect(child2.buildInternal.called).to.be.false;
          expect(resources.pendingBuildResources_.length).to.be.equal(2);
          expect(resources.schedulePass.called).to.be.false;
  
          child1.nextSibling = child2;
          resources.buildReadyResources_();
          expect(child1.buildInternal.called).to.be.true;
          expect(child2.buildInternal.called).to.be.false;
          expect(resources.pendingBuildResources_.length).to.be.equal(1);
          expect(resources.pendingBuildResources_[0]).to.be.equal(resource2);
          expect(resource1.isBuilding()).to.be.true;
          expect(resource2.isBuilding()).to.be.false;
          return resource1.buildPromise
            .then(() => {
              expect(resources.schedulePass.calledOnce).to.be.true;
  
              child2.parentNode = parent;
              parent.nextSibling = true;
              resources.buildReadyResources_();
              expect(child1.buildInternal).to.be.calledOnce;
              expect(child2.buildInternal.called).to.be.true;
              expect(resources.pendingBuildResources_.length).to.be.equal(0);
              expect(resource2.isBuilding()).to.be.true;
              return resource2.buildPromise;
            })
            .then(() => {
              expect(resources.get()).to.contain(resource1);
              expect(resources.get()).to.contain(resource2);
              expect(resource1.isBuilding()).to.be.false;
              expect(resource2.isBuilding()).to.be.false;
              expect(resources.schedulePass.calledTwice).to.be.true;
            });
        }
      );
  
      it('should NOT build past the root node when pending', () => {
        resources.pendingBuildResources_ = [resource1];
        resources.buildReadyResources_();
        expect(child1.buildInternal.called).to.be.false;
        expect(resources.pendingBuildResources_.length).to.be.equal(1);
        expect(resources.schedulePass.called).to.be.false;
  
        child1.parentNode = parent;
        parent.nextSibling = true;
        sandbox.stub(resources.ampdoc, 'getRootNode').returns(parent);
        resources.buildReadyResources_();
        expect(child1.buildInternal.called).to.be.false;
        expect(resources.pendingBuildResources_.length).to.be.equal(1);
        expect(resources.schedulePass.called).to.be.false;
      });
  
      it('should not try to build resources already being built', () => {
        resources.pendingBuildResources_ = [resource1, resource2];
        resources.buildReadyResources_();
        expect(child1.buildInternal.called).to.be.false;
        expect(child2.buildInternal.called).to.be.false;
        expect(resources.pendingBuildResources_.length).to.be.equal(2);
  
        const newChild = createElementWithResource(3)[0];
        newChild.nextSibling = true;
        const newResource = newChild['__AMP__RESOURCE'];
        const child1BuildSpy = sandbox.spy();
        child1.nextSibling = child2;
        child1.buildInternal = () => {
          // Simulate parent elements adding children elements to simulate
          // the infinite loop of building pending resources and make sure
          // that we're handling it well.
          child1BuildSpy();
          resources.pendingBuildResources_.push(newResource);
          resources.buildReadyResources_();
          return Promise.resolve();
        };
        resources.buildReadyResources_();
        expect(child1BuildSpy.called).to.be.true;
        expect(child2.buildInternal.called).to.be.false;
        expect(newChild.buildInternal.called).to.be.true;
        expect(resources.pendingBuildResources_.length).to.be.equal(1);
        expect(resources.pendingBuildResources_[0]).to.be.equal(resource2);
  
        child2.parentNode = parent;
        parent.nextSibling = true;
        resources.buildReadyResources_();
        expect(child1BuildSpy.calledTwice).to.be.false;
        expect(child2.buildInternal.called).to.be.true;
        expect(newChild.buildInternal.calledTwice).to.be.false;
        expect(resources.pendingBuildResources_.length).to.be.equal(0);
      });
  
      // TODO(amphtml, #15748): Fails on Safari 11.1.0.
      it.configure().skipSafari(
        'should build everything pending when document is ready',
        () => {
          resources.documentReady_ = true;
          resources.pendingBuildResources_ = [
            parentResource,
            resource1,
            resource2,
          ];
          const child1BuildSpy = sandbox.spy();
          child1.buildInternal = () => {
            // Emulate an error happening during an element build.
            child1BuildSpy();
            return Promise.reject(new Error('child1-build-error'));
          };
          resources.buildReadyResources_();
          expect(child1BuildSpy.called).to.be.true;
          expect(child2.buildInternal.called).to.be.true;
          expect(parent.buildInternal.called).to.be.true;
          expect(resources.pendingBuildResources_.length).to.be.equal(0);
          return Promise.all([
            parentResource.buildPromise,
            resource2.buildPromise,
            resource1.buildPromise.then(
              () => {
                throw new Error('must have failed');
              },
              () => {
                // Ignore error.
              }
            ),
          ]).then(() => {
            expect(schedulePassStub).to.be.calledTwice;
            // Failed build.
            expect(resources.get()).to.not.contain(resource1);
            expect(resource1.isBuilding()).to.be.false;
            // Successful build.
            expect(resources.get()).to.contain(resource2);
            expect(resource2.isBuilding()).to.be.false;
          });
        }
      );
  
      // TODO(amphtml, #15748): Fails on Safari 11.1.0.
      it.configure().skipSafari(
        'should not schedule pass if all builds failed',
        () => {
          resources.documentReady_ = true;
          resources.pendingBuildResources_ = [resource1];
          const child1BuildSpy = sandbox.spy();
          child1.buildInternal = () => {
            // Emulate an error happening during an element build.
            child1BuildSpy();
            return Promise.reject(new Error('child1-build-error'));
          };
          resources.buildReadyResources_();
          expect(child1BuildSpy.called).to.be.true;
          expect(resources.pendingBuildResources_.length).to.be.equal(0);
          return resource1.buildPromise.then(
            () => {
              throw new Error('must have failed');
            },
            () => {
              expect(schedulePassStub).to.not.be.called;
              expect(resources.get()).to.not.contain(resource1);
              expect(resource1.isBuilding()).to.be.false;
            }
          );
        }
      );
    });
  
    describe('remove', () => {
      // TODO(amphtml, #15748): Fails on Safari 11.1.0.
      it.configure().skipSafari('should remove resource and pause', () => {
        child1.isBuilt = () => true;
        resources.add(child1);
        const resource = child1['__AMP__RESOURCE'];
        const pauseOnRemoveStub = sandbox.stub(resource, 'pauseOnRemove');
        const disconnectStub = sandbox.stub(resource, 'disconnect');
        resources.remove(child1);
        expect(resources.get()).to.not.contain(resource);
        expect(pauseOnRemoveStub).to.be.calledOnce;
        expect(disconnectStub).to.not.be.called;
      });
    });
  
    it("remove should remove all of a resource's tasks", () => {
      const states = [
        ResourceState.NOT_LAID_OUT,
        ResourceState.READY_FOR_LAYOUT,
        ResourceState.LAYOUT_SCHEDULED,
      ];
  
      states.forEach((state) => {
        resource1.state_ = state;
        resources.exec_.enqueue({id: 1, resource: resource1});
        resources.queue_.enqueue({id: 1, resource: resource1});
  
        expect(resources.exec_.getSize()).to.equal(1);
        expect(resources.queue_.getSize()).to.equal(1);
        resources.remove(child1);
        expect(resources.exec_.getSize()).to.equal(0);
        expect(resources.queue_.getSize()).to.equal(0);
      });
    });
  
    describe('reparent', () => {
      let scheduleBuildStub;
      let resource;
  
      beforeEach(() => {
        scheduleBuildStub = sandbox.stub(
          resources,
          'buildOrScheduleBuildForResource_'
        );
        child1.isBuilt = () => true;
        resources.add(child1);
        resources.upgraded(child1);
        resource = Resource.forElementOptional(child1);
        resources.remove(child1);
      });
  
      // TODO(amphtml, #15748): Fails on Safari 11.1.0.
      it.configure().skipSafari('should keep reference to the resource', () => {
        expect(resource).to.not.be.null;
        expect(Resource.forElementOptional(child1)).to.equal(resource);
        expect(resources.get()).to.not.contain(resource);
        expect(scheduleBuildStub).to.be.calledOnce;
        expect(resource.isMeasureRequested()).to.be.false;
      });
    });
  });
  