() => {
        component.stickyEndColumns = ['column-4', 'column-6'];
        fixture.detectChanges();
        flushMicrotasks();

        headerRows.forEach(row => {
          let cells = getHeaderCells(row);
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5]);
          expectStickyStyles(cells[3], '1', {right: '20px'});
          expectStickyBorderClass(cells[3], {right: true});
          expectNoStickyStyles([cells[0], cells[1], cells[2], cells[4]]);
        });
        dataRows.forEach(row => {
          let cells = getCells(row);
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5]);
          expectStickyStyles(cells[3], '1', {right: '20px'});
          expectStickyBorderClass(cells[3], {right: true});
          expectNoStickyStyles([cells[0], cells[1], cells[2], cells[4]]);
        });
        footerRows.forEach(row => {
          let cells = getFooterCells(row);
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5]);
          expectStickyStyles(cells[3], '1', {right: '20px'});
          expectStickyBorderClass(cells[3], {right: true});
          expectNoStickyStyles([cells[0], cells[1], cells[2], cells[4]]);
        });
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({
          sizes: [
            getCells(dataRows[0])[5].getBoundingClientRect().width,
            null,
            getCells(dataRows[0])[3].getBoundingClientRect().width,
          ]
        });

        component.stickyEndColumns = [];
        fixture.detectChanges();
        flushMicrotasks();
        headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row)));
        dataRows.forEach(row => expectNoStickyStyles(getCells(row)));
        footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row)));
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});
      }


row => {
          let cells = getHeaderCells(row);
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5]);
          expectStickyStyles(cells[3], '1', {right: '20px'});
          expectStickyBorderClass(cells[3], {right: true});
          expectNoStickyStyles([cells[0], cells[1], cells[2], cells[4]]);
        }


row => {
          let cells = getCells(row);
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5]);
          expectStickyStyles(cells[3], '1', {right: '20px'});
          expectStickyBorderClass(cells[3], {right: true});
          expectNoStickyStyles([cells[0], cells[1], cells[2], cells[4]]);
        }


row => {
          let cells = getFooterCells(row);
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5]);
          expectStickyStyles(cells[3], '1', {right: '20px'});
          expectStickyBorderClass(cells[3], {right: true});
          expectNoStickyStyles([cells[0], cells[1], cells[2], cells[4]]);
        }


() => {
        component.dir = 'rtl';
        component.stickyStartColumns = ['column-1', 'column-2'];
        component.stickyEndColumns = ['column-5', 'column-6'];
        fixture.detectChanges();
        flushMicrotasks();

        const firstColumnWidth = getHeaderCells(headerRows[0])[0].getBoundingClientRect().width;
        const lastColumnWidth = getHeaderCells(headerRows[0])[5].getBoundingClientRect().width;

        let headerCells = getHeaderCells(headerRows[0]);
        expectStickyStyles(headerCells[0], '1', {right: '0px'});
        expectStickyBorderClass(headerCells[0]);
        expectStickyStyles(headerCells[1], '1', {right: `${firstColumnWidth}px`});
        expectStickyBorderClass(headerCells[1], {right: true});
        expectStickyStyles(headerCells[4], '1', {left: `${lastColumnWidth}px`});
        expectStickyBorderClass(headerCells[4], {left: true});
        expectStickyStyles(headerCells[5], '1', {left: '0px'});
        expectStickyBorderClass(headerCells[5]);

        dataRows.forEach(row => {
          let cells = getCells(row);
          expectStickyStyles(cells[0], '1', {right: '0px'});
          expectStickyBorderClass(cells[0]);
          expectStickyStyles(cells[1], '1', {right: `${firstColumnWidth}px`});
          expectStickyBorderClass(cells[1], {right: true});
          expectStickyStyles(cells[4], '1', {left: `${lastColumnWidth}px`});
          expectStickyBorderClass(cells[4], {left: true});
          expectStickyStyles(cells[5], '1', {left: '0px'});
          expectStickyBorderClass(cells[5]);
        });

        let footerCells = getFooterCells(footerRows[0]);
        expectStickyStyles(footerCells[0], '1', {right: '0px'});
        expectStickyBorderClass(footerCells[0]);
        expectStickyStyles(footerCells[1], '1', {right: `${firstColumnWidth}px`});
        expectStickyBorderClass(footerCells[1], {right: true});
        expectStickyStyles(footerCells[4], '1', {left: `${lastColumnWidth}px`});
        expectStickyBorderClass(footerCells[4], {left: true});
        expectStickyStyles(footerCells[5], '1', {left: '0px'});
        expectStickyBorderClass(footerCells[5]);
      }


row => {
          let cells = getCells(row);
          expectStickyStyles(cells[0], '1', {right: '0px'});
          expectStickyBorderClass(cells[0]);
          expectStickyStyles(cells[1], '1', {right: `${firstColumnWidth}px`});
          expectStickyBorderClass(cells[1], {right: true});
          expectStickyStyles(cells[4], '1', {left: `${lastColumnWidth}px`});
          expectStickyBorderClass(cells[4], {left: true});
          expectStickyStyles(cells[5], '1', {left: '0px'});
          expectStickyBorderClass(cells[5]);
        }


() => {
        component.stickyHeaders = ['header-1'];
        component.stickyFooters = ['footer-3'];
        component.stickyStartColumns = ['column-1'];
        component.stickyEndColumns = ['column-6'];
        fixture.detectChanges();
        flushMicrotasks();

        let headerCells = getHeaderCells(headerRows[0]);
        expectStickyStyles(headerRows[0], '100', {top: '0px'});
        expectStickyBorderClass(headerRows[0], {top: true});
        expectStickyStyles(headerCells[0], '1', {left: '0px'});
        expectStickyBorderClass(headerCells[0], {left: true});
        expectStickyStyles(headerCells[5], '1', {right: '0px'});
        expectStickyBorderClass(headerCells[5], {right: true});
        expectNoStickyStyles([headerCells[1], headerCells[2], headerCells[3], headerCells[4]]);
        expectNoStickyStyles([headerRows[1], headerRows[2]]);

        dataRows.forEach(row => {
          let cells = getCells(row);
          expectStickyStyles(cells[0], '1', {left: '0px'});
          expectStickyBorderClass(cells[0], {left: true});
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5], {right: true});
          expectNoStickyStyles([cells[1], cells[2], cells[3], cells[4]]);
        });

        let footerCells = getFooterCells(footerRows[0]);
        expectStickyStyles(footerRows[2], '10', {bottom: '0px'});
        expectStickyBorderClass(footerRows[2], {bottom: true});
        expectStickyStyles(footerCells[0], '1', {left: '0px'});
        expectStickyBorderClass(footerCells[0], {left: true});
        expectStickyStyles(footerCells[5], '1', {right: '0px'});
        expectStickyBorderClass(footerCells[5], {right: true});
        expectNoStickyStyles([footerCells[1], footerCells[2], footerCells[3], footerCells[4]]);
        expectNoStickyStyles([footerRows[0], footerRows[1]]);

        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual({
          sizes: [headerRows[0].getBoundingClientRect().height],
          offsets: [0],
          elements: [[headerRows[0]]],
        });
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual({
          sizes: [footerRows[2].getBoundingClientRect().height],
          offsets: [0],
          elements: [[footerRows[2]]],
        });
        expect(component.mostRecentStickyColumnsUpdate).toEqual({
          sizes: [getCells(dataRows[0])[0].getBoundingClientRect().width]
        });
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({
          sizes: [getCells(dataRows[0])[5].getBoundingClientRect().width]
        });

        component.stickyHeaders = [];
        component.stickyFooters = [];
        component.stickyStartColumns = [];
        component.stickyEndColumns = [];
        fixture.detectChanges();
        flushMicrotasks();

        headerRows.forEach(row => expectNoStickyStyles([row, ...getHeaderCells(row)]));
        dataRows.forEach(row => expectNoStickyStyles([row, ...getCells(row)]));
        footerRows.forEach(row => expectNoStickyStyles([row, ...getFooterCells(row)]));

        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});
      }


row => {
          let cells = getCells(row);
          expectStickyStyles(cells[0], '1', {left: '0px'});
          expectStickyBorderClass(cells[0], {left: true});
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5], {right: true});
          expectNoStickyStyles([cells[1], cells[2], cells[3], cells[4]]);
        }


row => expectNoStickyStyles([row, ...getHeaderCells(row)])


row => expectNoStickyStyles([row, ...getCells(row)])


row => expectNoStickyStyles([row, ...getFooterCells(row)])


() => {
        setupTableTestApp(StickyNativeLayoutCdkTableApp);

        headerRows = getHeaderRows(tableElement);
        footerRows = getFooterRows(tableElement);
        dataRows = getRows(tableElement);
      }


() => {
        component.stickyHeaders = ['header-1', 'header-3'];
        fixture.detectChanges();
        flushMicrotasks();

        getHeaderCells(headerRows[0]).forEach(cell => {
          expectStickyStyles(cell, '100', {top: '0px'});
          expectStickyBorderClass(cell);
        });
        const firstHeaderHeight = headerRows[0].getBoundingClientRect().height;
        getHeaderCells(headerRows[2]).forEach(cell => {
          expectStickyStyles(cell, '100', {top: firstHeaderHeight + 'px'});
          expectStickyBorderClass(cell, {top: true});
        });
        expectNoStickyStyles(getHeaderCells(headerRows[1]));
        expectNoStickyStyles(headerRows);  // No sticky styles on rows for native table
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual({
          sizes: [
            headerRows[0].getBoundingClientRect().height,
            undefined,
            headerRows[2].getBoundingClientRect().height,
          ],
          offsets: [
            0,
            undefined,
            headerRows[0].getBoundingClientRect().height,
          ],
          elements: [getHeaderCells(headerRows[0]), undefined, getHeaderCells(headerRows[2])],
        });
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});

        component.stickyHeaders = [];
        fixture.detectChanges();
        flushMicrotasks();
        expectNoStickyStyles(headerRows);  // No sticky styles on rows for native table
        headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row)));
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});
      }


cell => {
          expectStickyStyles(cell, '100', {top: '0px'});
          expectStickyBorderClass(cell);
        }


cell => {
          expectStickyStyles(cell, '100', {top: firstHeaderHeight + 'px'});
          expectStickyBorderClass(cell, {top: true});
        }


() => {
        component.stickyFooters = ['footer-1', 'footer-3'];
        fixture.detectChanges();
        flushMicrotasks();

        getFooterCells(footerRows[2]).forEach(cell => {
          expectStickyStyles(cell, '10', {bottom: '0px'});
          expectStickyBorderClass(cell);
        });
        const thirdFooterHeight = footerRows[2].getBoundingClientRect().height;
        getFooterCells(footerRows[0]).forEach(cell => {
          expectStickyStyles(cell, '10', {bottom: thirdFooterHeight + 'px'});
          expectStickyBorderClass(cell, {bottom: true});
        });
        expectNoStickyStyles(getFooterCells(footerRows[1]));
        expectNoStickyStyles(footerRows);  // No sticky styles on rows for native table
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual({
          sizes: [
            footerRows[2].getBoundingClientRect().height,
            undefined,
            footerRows[0].getBoundingClientRect().height,
          ],
          offsets: [
            0,
            undefined,
            footerRows[2].getBoundingClientRect().height,
          ],
          elements: [getFooterCells(footerRows[2]), undefined, getFooterCells(footerRows[0])],
        });
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});

        component.stickyFooters = [];
        fixture.detectChanges();
        flushMicrotasks();
        expectNoStickyStyles(footerRows);  // No sticky styles on rows for native table
        footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row)));
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});
      }


cell => {
          expectStickyStyles(cell, '10', {bottom: '0px'});
          expectStickyBorderClass(cell);
        }


cell => {
          expectStickyStyles(cell, '10', {bottom: thirdFooterHeight + 'px'});
          expectStickyBorderClass(cell, {bottom: true});
        }


() => {
        const tfoot = tableElement.querySelector('tfoot');
        component.stickyFooters = ['footer-1'];
        fixture.detectChanges();
        flushMicrotasks();
        expectNoStickyStyles([tfoot]);

        component.stickyFooters = ['footer-1', 'footer-2', 'footer-3'];
        fixture.detectChanges();
        flushMicrotasks();
        expectStickyStyles(tfoot, '10', {bottom: '0px'});
        expectStickyBorderClass(tfoot);

        component.stickyFooters = ['footer-1', 'footer-2'];
        fixture.detectChanges();
        flushMicrotasks();
        expectNoStickyStyles([tfoot]);
      }


() => {
        component.stickyStartColumns = ['column-1', 'column-3'];
        fixture.detectChanges();
        flushMicrotasks();

        headerRows.forEach(row => {
          let cells = getHeaderCells(row);
          expectStickyStyles(cells[0], '1', {left: '0px'});
          expectStickyBorderClass(cells[0]);
          expectStickyStyles(cells[2], '1', {left: '20px'});
          expectStickyBorderClass(cells[2], {left: true});
          expectNoStickyStyles([cells[1], cells[3], cells[4], cells[5]]);
        });
        dataRows.forEach(row => {
          let cells = getCells(row);
          expectStickyStyles(cells[0], '1', {left: '0px'});
          expectStickyBorderClass(cells[0]);
          expectStickyStyles(cells[2], '1', {left: '20px'});
          expectStickyBorderClass(cells[2], {left: true});
          expectNoStickyStyles([cells[1], cells[3], cells[4], cells[5]]);
        });
        footerRows.forEach(row => {
          let cells = getFooterCells(row);
          expectStickyStyles(cells[0], '1', {left: '0px'});
          expectStickyBorderClass(cells[0]);
          expectStickyStyles(cells[2], '1', {left: '20px'});
          expectStickyBorderClass(cells[2], {left: true});
          expectNoStickyStyles([cells[1], cells[3], cells[4], cells[5]]);
        });
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({
          sizes: [
            getCells(dataRows[0])[0].getBoundingClientRect().width,
            null,
            getCells(dataRows[0])[2].getBoundingClientRect().width,
          ],
        });
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});

        component.stickyStartColumns = [];
        fixture.detectChanges();
        flushMicrotasks();
        headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row)));
        dataRows.forEach(row => expectNoStickyStyles(getCells(row)));
        footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row)));
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});
      }


() => {
        component.stickyHeaders = ['header-1'];
        component.stickyFooters = ['footer-3'];
        component.stickyStartColumns = ['column-1'];
        component.stickyEndColumns = ['column-6'];
        fixture.detectChanges();
        flushMicrotasks();

        const headerCells = getHeaderCells(headerRows[0]);
        expectStickyStyles(headerCells[0], '101', {top: '0px', left: '0px'});
        expectStickyBorderClass(headerCells[0], {top: true, left: true});
        expectStickyStyles(headerCells[1], '100', {top: '0px'});
        expectStickyBorderClass(headerCells[1], {top: true});
        expectStickyStyles(headerCells[2], '100', {top: '0px'});
        expectStickyBorderClass(headerCells[2], {top: true});
        expectStickyStyles(headerCells[3], '100', {top: '0px'});
        expectStickyBorderClass(headerCells[3], {top: true});
        expectStickyStyles(headerCells[4], '100', {top: '0px'});
        expectStickyBorderClass(headerCells[4], {top: true});
        expectStickyStyles(headerCells[5], '101', {top: '0px', right: '0px'});
        expectStickyBorderClass(headerCells[5], {top: true, right: true});
        expectNoStickyStyles(headerRows);

        dataRows.forEach(row => {
          let cells = getCells(row);
          expectStickyStyles(cells[0], '1', {left: '0px'});
          expectStickyBorderClass(cells[0], {left: true});
          expectStickyStyles(cells[5], '1', {right: '0px'});
          expectStickyBorderClass(cells[5], {right: true});
          expectNoStickyStyles([cells[1], cells[2], cells[3], cells[4]]);
        });

        const footerCells = getFooterCells(footerRows[2]);
        expectStickyStyles(footerCells[0], '11', {bottom: '0px', left: '0px'});
        expectStickyBorderClass(footerCells[0], {bottom: true, left: true});
        expectStickyStyles(footerCells[1], '10', {bottom: '0px'});
        expectStickyBorderClass(footerCells[1], {bottom: true});
        expectStickyStyles(footerCells[2], '10', {bottom: '0px'});
        expectStickyBorderClass(footerCells[2], {bottom: true});
        expectStickyStyles(footerCells[3], '10', {bottom: '0px'});
        expectStickyBorderClass(footerCells[3], {bottom: true});
        expectStickyStyles(footerCells[4], '10', {bottom: '0px'});
        expectStickyBorderClass(footerCells[4], {bottom: true});
        expectStickyStyles(footerCells[5], '11', {bottom: '0px', right: '0px'});
        expectStickyBorderClass(footerCells[5], {bottom: true, right: true});
        expectNoStickyStyles(footerRows);

        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual({
          sizes: [headerRows[0].getBoundingClientRect().height],
          offsets: [0],
          elements: [getHeaderCells(headerRows[0])],
        });
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual({
          sizes: [footerRows[2].getBoundingClientRect().height],
          offsets: [0],
          elements: [getFooterCells(footerRows[2])],
        });
        expect(component.mostRecentStickyColumnsUpdate).toEqual({
          sizes: [getCells(dataRows[0])[0].getBoundingClientRect().width]
        });
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({
          sizes: [getCells(dataRows[0])[5].getBoundingClientRect().width]
        });

        component.stickyHeaders = [];
        component.stickyFooters = [];
        component.stickyStartColumns = [];
        component.stickyEndColumns = [];
        fixture.detectChanges();
        flushMicrotasks();

        headerRows.forEach(row => expectNoStickyStyles([row, ...getHeaderCells(row)]));
        dataRows.forEach(row => expectNoStickyStyles([row, ...getCells(row)]));
        footerRows.forEach(row => expectNoStickyStyles([row, ...getFooterCells(row)]));

        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyFooterRowsUpdate).toEqual(
            {sizes: [], offsets: [], elements: []});
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});
      }


() => {
    function createTestComponentWithTrackyByTable(trackByStrategy: string) {
      fixture = createComponent(TrackByCdkTableApp);

      component = fixture.componentInstance;
      component.trackByStrategy = trackByStrategy;

      tableElement = fixture.nativeElement.querySelector('cdk-table');
      fixture.detectChanges();

      // Each row receives an attribute 'initialIndex' the element's original place
      getRows(tableElement).forEach((row: Element, index: number) => {
        row.setAttribute('initialIndex', index.toString());
      });

      // Prove that the attributes match their indicies
      const initialRows = getRows(tableElement);
      expect(initialRows[0].getAttribute('initialIndex')).toBe('0');
      expect(initialRows[1].getAttribute('initialIndex')).toBe('1');
      expect(initialRows[2].getAttribute('initialIndex')).toBe('2');
    }

    // Swap first two elements, remove the third, add new data
    function mutateData() {
      // Swap first and second data in data array
      const copiedData = component.dataSource.data.slice();
      const temp = copiedData[0];
      copiedData[0] = copiedData[1];
      copiedData[1] = temp;

      // Remove the third element
      copiedData.splice(2, 1);

      // Add new data
      component.dataSource.data = copiedData;
      component.dataSource.addData();
    }

    it('should add/remove/move rows with reference-based trackBy', () => {
      createTestComponentWithTrackyByTable('reference');
      mutateData();

      // Expect that the first and second rows were swapped and that the last row is new
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe('1');
      expect(changedRows[1].getAttribute('initialIndex')).toBe('0');
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    });

    it('should add/remove/move rows with changed references without property-based trackBy', () => {
      createTestComponentWithTrackyByTable('reference');
      mutateData();

      // Change each item reference to show that the trackby is not checking the item properties.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect that all the rows are considered new since their references are all different
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe(null);
      expect(changedRows[1].getAttribute('initialIndex')).toBe(null);
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    });

    it('should add/remove/move rows with changed references with property-based trackBy', () => {
      createTestComponentWithTrackyByTable('propertyA');
      mutateData();

      // Change each item reference to show that the trackby is checking the item properties.
      // Otherwise this would cause them all to be removed/added.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect that the first and second rows were swapped and that the last row is new
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe('1');
      expect(changedRows[1].getAttribute('initialIndex')).toBe('0');
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    });

    it('should add/remove/move rows with changed references with index-based trackBy', () => {
      createTestComponentWithTrackyByTable('index');
      mutateData();

      // Change each item reference to show that the trackby is checking the index.
      // Otherwise this would cause them all to be removed/added.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect first two to be the same since they were swapped but indicies are consistent.
      // The third element was removed and caught by the table so it was removed before another
      // item was added, so it is without an initial index.
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe('0');
      expect(changedRows[1].getAttribute('initialIndex')).toBe('1');
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    });

    it('should change row implicit data even when trackBy finds no changes', () => {
      createTestComponentWithTrackyByTable('index');
      const firstRow = getRows(tableElement)[0];
      expect(firstRow.textContent!.trim()).toBe('a_1 b_1');
      expect(firstRow.getAttribute('initialIndex')).toBe('0');
      mutateData();

      // Change each item reference to show that the trackby is checking the index.
      // Otherwise this would cause them all to be removed/added.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect the rows were given the right implicit data even though the rows were not moved.
      fixture.detectChanges();
      expect(firstRow.textContent!.trim()).toBe('a_2 b_2');
      expect(firstRow.getAttribute('initialIndex')).toBe('0');
    });
  }


(row: Element, index: number) => {
        row.setAttribute('initialIndex', index.toString());
      }


() => {
      createTestComponentWithTrackyByTable('reference');
      mutateData();

      // Expect that the first and second rows were swapped and that the last row is new
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe('1');
      expect(changedRows[1].getAttribute('initialIndex')).toBe('0');
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    }


() => {
      createTestComponentWithTrackyByTable('reference');
      mutateData();

      // Change each item reference to show that the trackby is not checking the item properties.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect that all the rows are considered new since their references are all different
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe(null);
      expect(changedRows[1].getAttribute('initialIndex')).toBe(null);
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    }


(item: TestData) => ({a: item.a, b: item.b, c: item.c})


() => {
      createTestComponentWithTrackyByTable('propertyA');
      mutateData();

      // Change each item reference to show that the trackby is checking the item properties.
      // Otherwise this would cause them all to be removed/added.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect that the first and second rows were swapped and that the last row is new
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe('1');
      expect(changedRows[1].getAttribute('initialIndex')).toBe('0');
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    }


() => {
      createTestComponentWithTrackyByTable('index');
      mutateData();

      // Change each item reference to show that the trackby is checking the index.
      // Otherwise this would cause them all to be removed/added.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect first two to be the same since they were swapped but indicies are consistent.
      // The third element was removed and caught by the table so it was removed before another
      // item was added, so it is without an initial index.
      const changedRows = getRows(tableElement);
      expect(changedRows.length).toBe(3);
      expect(changedRows[0].getAttribute('initialIndex')).toBe('0');
      expect(changedRows[1].getAttribute('initialIndex')).toBe('1');
      expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
    }


() => {
      createTestComponentWithTrackyByTable('index');
      const firstRow = getRows(tableElement)[0];
      expect(firstRow.textContent!.trim()).toBe('a_1 b_1');
      expect(firstRow.getAttribute('initialIndex')).toBe('0');
      mutateData();

      // Change each item reference to show that the trackby is checking the index.
      // Otherwise this would cause them all to be removed/added.
      component.dataSource.data =
          component.dataSource.data.map((item: TestData) => ({a: item.a, b: item.b, c: item.c}));

      // Expect the rows were given the right implicit data even though the rows were not moved.
      fixture.detectChanges();
      expect(firstRow.textContent!.trim()).toBe('a_2 b_2');
      expect(firstRow.getAttribute('initialIndex')).toBe('0');
    }


() => {
    setupTableTestApp(DynamicDataSourceCdkTableApp);

    // Expect that the component has no data source and the table element reflects empty data.
    expect(component.dataSource).toBeUndefined();
    expectTableToMatchContent(tableElement, [
      ['Column A']
    ]);

    // Add a data source that has initialized data. Expect that the table shows this data.
    const dynamicDataSource = new FakeDataSource();
    component.dataSource = dynamicDataSource;
    fixture.detectChanges();
    expect(dynamicDataSource.isConnected).toBe(true);

    const data = component.dataSource.data;
    expectTableToMatchContent(tableElement, [
      ['Column A'],
      [data[0].a],
      [data[1].a],
      [data[2].a],
    ]);

    // Remove the data source and check to make sure the table is empty again.
    component.dataSource = undefined;
    fixture.detectChanges();

    // Expect that the old data source has been disconnected.
    expect(dynamicDataSource.isConnected).toBe(false);
    expectTableToMatchContent(tableElement, [
      ['Column A']
    ]);

    // Reconnect a data source and check that the table is populated
    const newDynamicDataSource = new FakeDataSource();
    component.dataSource = newDynamicDataSource;
    fixture.detectChanges();
    expect(newDynamicDataSource.isConnected).toBe(true);

    const newData = component.dataSource.data;
    expectTableToMatchContent(tableElement, [
      ['Column A'],
      [newData[0].a],
      [newData[1].a],
      [newData[2].a],
    ]);
  }


() => {
    setupTableTestApp(RowContextCdkTableApp);

    let rowElements = tableElement.querySelectorAll('cdk-row');

    // Rows should not have any context classes
    for (let i = 0; i < rowElements.length; i++) {
      expect(rowElements[i].classList.contains('custom-row-class-first')).toBe(false);
      expect(rowElements[i].classList.contains('custom-row-class-last')).toBe(false);
      expect(rowElements[i].classList.contains('custom-row-class-even')).toBe(false);
      expect(rowElements[i].classList.contains('custom-row-class-odd')).toBe(false);
    }

    // Enable all the context classes
    component.enableRowContextClasses = true;
    fixture.detectChanges();

    expect(rowElements[0].classList.contains('custom-row-class-first')).toBe(true);
    expect(rowElements[0].classList.contains('custom-row-class-last')).toBe(false);
    expect(rowElements[0].classList.contains('custom-row-class-even')).toBe(true);
    expect(rowElements[0].classList.contains('custom-row-class-odd')).toBe(false);

    expect(rowElements[1].classList.contains('custom-row-class-first')).toBe(false);
    expect(rowElements[1].classList.contains('custom-row-class-last')).toBe(false);
    expect(rowElements[1].classList.contains('custom-row-class-even')).toBe(false);
    expect(rowElements[1].classList.contains('custom-row-class-odd')).toBe(true);

    expect(rowElements[2].classList.contains('custom-row-class-first')).toBe(false);
    expect(rowElements[2].classList.contains('custom-row-class-last')).toBe(true);
    expect(rowElements[2].classList.contains('custom-row-class-even')).toBe(true);
    expect(rowElements[2].classList.contains('custom-row-class-odd')).toBe(false);
  }


() => {
    setupTableTestApp(RowContextCdkTableApp);

    const rowElements = fixture.nativeElement.querySelectorAll('cdk-row');

    for (let i = 0; i < rowElements.length; i++) {
      // Cells should not have any context classes
      const cellElements = rowElements[i].querySelectorAll('cdk-cell');
      for (let j = 0; j < cellElements.length; j++) {
        expect(cellElements[j].classList.contains('custom-cell-class-first')).toBe(false);
        expect(cellElements[j].classList.contains('custom-cell-class-last')).toBe(false);
        expect(cellElements[j].classList.contains('custom-cell-class-even')).toBe(false);
        expect(cellElements[j].classList.contains('custom-cell-class-odd')).toBe(false);
      }
    }

    // Enable the context classes
    component.enableCellContextClasses = true;
    fixture.detectChanges();

    let cellElement = rowElements[0].querySelectorAll('cdk-cell')[0];
    expect(cellElement.classList.contains('custom-cell-class-first')).toBe(true);
    expect(cellElement.classList.contains('custom-cell-class-last')).toBe(false);
    expect(cellElement.classList.contains('custom-cell-class-even')).toBe(true);
    expect(cellElement.classList.contains('custom-cell-class-odd')).toBe(false);

    cellElement = rowElements[1].querySelectorAll('cdk-cell')[0];
    expect(cellElement.classList.contains('custom-cell-class-first')).toBe(false);
    expect(cellElement.classList.contains('custom-cell-class-last')).toBe(false);
    expect(cellElement.classList.contains('custom-cell-class-even')).toBe(false);
    expect(cellElement.classList.contains('custom-cell-class-odd')).toBe(true);

    cellElement = rowElements[2].querySelectorAll('cdk-cell')[0];
    expect(cellElement.classList.contains('custom-cell-class-first')).toBe(false);
    expect(cellElement.classList.contains('custom-cell-class-last')).toBe(true);
    expect(cellElement.classList.contains('custom-cell-class-even')).toBe(true);
    expect(cellElement.classList.contains('custom-cell-class-odd')).toBe(false);
  }


(index: number, _rowData: TestData) => index == 1


(_index: number, rowData: TestData) => rowData.c == 'c_3'


(index: number, item: TestData) => {
    switch (this.trackByStrategy) {
      case 'reference': return item;
      case 'propertyA': return item.a;
      case 'index': return index;
    }
  }


() => {
      this.displayedColumns = ['column_a'];
    }


columnDef => this.table.addColumnDef(columnDef)


rowDef => this.table.addRowDef(rowDef)


(i: number) => i === 0


row => {
    actualTableContent.push(getHeaderCells(row));
  }


row => getCells(row)


row => {
    actualTableContent.push(getFooterCells(row));
  }


row => row.map(cell => cell.textContent!.trim())


cell => cell.textContent!.trim()


(row, rowIndex) => {
    const expectedRow = expected[rowIndex];

    // Make sure the number of cells match
    if (row.length !== expectedRow.length) {
      missedExpectations.push(`Expected ${expectedRow.length} cells in row but got ${row.length}`);
      fail(missedExpectations.join('\n'));
    }

    row.forEach((actualCell, cellIndex) => {
      const expectedCell = expectedRow ? expectedRow[cellIndex] : null;
      checkCellContent(actualCell, expectedCell);
    });
  }


(actualCell, cellIndex) => {
      const expectedCell = expectedRow ? expectedRow[cellIndex] : null;
      checkCellContent(actualCell, expectedCell);
    }


class FakeDataSource extends DataSource<TestData> {
  isConnected = false;

  get data() {
    return this._dataChange.getValue();
  }
  set data(data: TestData[]) {
    this._dataChange.next(data);
  }
  _dataChange = new BehaviorSubject<TestData[]>([]);

  constructor() {
    super();
    for (let i = 0; i < 3; i++) {
      this.addData();
    }
  }

  connect(collectionViewer: CollectionViewer) {
    this.isConnected = true;
    return combineLatest([this._dataChange, collectionViewer.viewChange])
      .pipe(map(data => data[0]));
  }

  disconnect() {
    this.isConnected = false;
  }

  addData() {
    const nextIndex = this.data.length + 1;

    let copiedData = this.data.slice();
    copiedData.push({
      a: `a_${nextIndex}`,
      b: `b_${nextIndex}`,
      c: `c_${nextIndex}`
    });

    this.data = copiedData;
  }
}


class BooleanDataSource extends DataSource<boolean> {
  _dataChange = new BehaviorSubject<boolean[]>([false, true, false, true]);

  connect(): Observable<boolean[]> {
    return this._dataChange;
  }

  disconnect() {}
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource"
               (contentChanged)="contentChangedCount = contentChangedCount + 1">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
        <cdk-footer-cell *cdkFooterCellDef> Footer A</cdk-footer-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}} </cdk-cell>
        <cdk-footer-cell *cdkFooterCellDef> Footer B</cdk-footer-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_c">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.c}} </cdk-cell>
        <cdk-footer-cell *cdkFooterCellDef> Footer C</cdk-footer-cell>
      </ng-container>

      <cdk-header-row class="customHeaderRowClass"
                      *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row class="customRowClass"
               *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
      <cdk-footer-row class="customFooterRowClass"
                      *cdkFooterRowDef="columnsToRender"></cdk-footer-row>

      <div *cdkNoDataRow>No data</div>
    </cdk-table>
  `
})
class SimpleCdkTableApp {
  dataSource: FakeDataSource | undefined = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b', 'column_c'];
  contentChangedCount = 0;

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_c">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
    </cdk-table>
  `
})
class CdkTableWithDifferentDataInputsApp {
  dataSource: DataSource<TestData> | Observable<TestData[]> | TestData[] | any = null;
  columnsToRender = ['column_a', 'column_b', 'column_c'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef></cdk-header-cell>
        <cdk-cell *cdkCellDef="let data"> {{data}} </cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="['column_a']"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: ['column_a']"></cdk-row>
    </cdk-table>
  `
})
class BooleanRowCdkTableApp {
  dataSource = new BooleanDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource"
               (contentChanged)="contentChangedCount = contentChangedCount + 1">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef></cdk-header-cell>
        <cdk-cell *cdkCellDef="let data"> {{data}} </cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="['column_a']"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: ['column_a']"></cdk-row>
    </cdk-table>
  `
})
class NullDataCdkTableApp {
  dataSource = observableOf(null);
  contentChangedCount = 0;
}


@Component({
  template: `
    <cdk-table [dataSource]="[]">
      <ng-container cdkColumnDef="first-header">
         first-header
      </ng-container>

      <ng-container cdkColumnDef="second-header">
         second-header
      </ng-container>

      <ng-container cdkColumnDef="first-footer">
         first-footer
      </ng-container>

      <ng-container cdkColumnDef="second-footer">
         second-footer
      </ng-container>

    </cdk-table>
  `
})
class MultipleHeaderFooterRowsCdkTableApp {
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource" [multiTemplateDataRows]="multiTemplateDataRows">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_c">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="index1Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> index_1_special_row</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="c3Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> c3_special_row</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="index">
        <cdk-header-cell *cdkHeaderCellDef> Index</cdk-header-cell>
        <cdk-cell *cdkCellDef="let index = index"> {{index}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="dataIndex">
        <cdk-header-cell *cdkHeaderCellDef> Data Index</cdk-header-cell>
        <cdk-cell *cdkCellDef="let dataIndex = dataIndex"> {{dataIndex}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="renderIndex">
        <cdk-header-cell *cdkHeaderCellDef> Render Index</cdk-header-cell>
        <cdk-cell *cdkCellDef="let renderIndex = renderIndex"> {{renderIndex}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
      <cdk-row *cdkRowDef="let row; columns: columnsForIsIndex1Row; when: isIndex1"></cdk-row>
      <cdk-row *cdkRowDef="let row; columns: columnsForHasC3Row; when: hasC3"></cdk-row>
    </cdk-table>
  `
})
class WhenRowCdkTableApp {
  multiTemplateDataRows = false;
  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b', 'column_c'];
  columnsForIsIndex1Row = ['index1Column'];
  columnsForHasC3Row = ['c3Column'];
  isIndex1 = (index: number, _rowData: TestData) => index == 1;
  hasC3 = (_index: number, rowData: TestData) => rowData.c == 'c_3';

  constructor() {
    this.dataSource.addData();
  }

  @ViewChild(CdkTable) table: CdkTable<TestData>;

  showIndexColumns() {
    const indexColumns = ['index', 'dataIndex', 'renderIndex'];
    this.columnsToRender = indexColumns;
    this.columnsForIsIndex1Row = indexColumns;
    this.columnsForHasC3Row = indexColumns;
  }
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource" multiTemplateDataRows>
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_c">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="index1Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> index_1_special_row</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="c3Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> c3_special_row</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="index">
        <cdk-header-cell *cdkHeaderCellDef> Index</cdk-header-cell>
        <cdk-cell *cdkCellDef="let index = index"> {{index}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="dataIndex">
        <cdk-header-cell *cdkHeaderCellDef> Data Index</cdk-header-cell>
        <cdk-cell *cdkCellDef="let dataIndex = dataIndex"> {{dataIndex}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="renderIndex">
        <cdk-header-cell *cdkHeaderCellDef> Render Index</cdk-header-cell>
        <cdk-cell *cdkCellDef="let renderIndex = renderIndex"> {{renderIndex}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
      <cdk-row *cdkRowDef="let row; columns: columnsForIsIndex1Row; when: isIndex1"></cdk-row>
      <cdk-row *cdkRowDef="let row; columns: columnsForHasC3Row; when: hasC3"></cdk-row>
    </cdk-table>
  `
})
class CoercedMultiTemplateDataRows extends WhenRowCdkTableApp {
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_c">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="index1Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> index_1_special_row </cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="c3Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> c3_special_row </cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: ['index1Column']; when: isIndex1"></cdk-row>
      <cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
    </cdk-table>
  `
})
class WhenRowWithoutDefaultCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b', 'column_c'];
  isIndex1 = (index: number, _rowData: TestData) => index == 1;
  hasC3 = (_index: number, rowData: TestData) => rowData.c == 'c_3';

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_c">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="index1Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> index_1_special_row </cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="c3Column">
        <cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
        <cdk-cell *cdkCellDef> c3_special_row </cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
      <cdk-row *cdkRowDef="let row; columns: ['index1Column']"></cdk-row>
      <cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
    </cdk-table>
  `
})
class WhenRowMultipleDefaultsCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b', 'column_c'];
  hasC3 = (_index: number, rowData: TestData) => rowData.c == 'c_3';

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
    </cdk-table>
  `
})
class DynamicDataSourceCdkTableApp {
  dataSource: FakeDataSource | undefined;
  columnsToRender = ['column_a'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource" [trackBy]="trackBy">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
    </cdk-table>
  `
})
class TrackByCdkTableApp {
  trackByStrategy: 'reference' | 'propertyA' | 'index' = 'reference';

  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;

  trackBy = (index: number, item: TestData) => {
    switch (this.trackByStrategy) {
      case 'reference': return item;
      case 'propertyA': return item.a;
      case 'index': return index;
    }
  }
}


class StickyPositioningListenerTest implements StickyPositioningListener {
  mostRecentStickyColumnsUpdate?: StickyUpdate;
  mostRecentStickyEndColumnsUpdate?: StickyUpdate;
  mostRecentStickyHeaderRowsUpdate?: StickyUpdate;
  mostRecentStickyFooterRowsUpdate?: StickyUpdate;

  stickyColumnsUpdated(update: StickyUpdate) {
    this.mostRecentStickyColumnsUpdate = update;
  }

  stickyEndColumnsUpdated(update: StickyUpdate) {
    this.mostRecentStickyEndColumnsUpdate = update;
  }

  stickyHeaderRowsUpdated(update: StickyUpdate) {
    this.mostRecentStickyHeaderRowsUpdate = update;
  }

  stickyFooterRowsUpdated(update: StickyUpdate) {
    this.mostRecentStickyFooterRowsUpdate = update;
  }
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource" [dir]="dir">
      <ng-container [cdkColumnDef]="column" *ngFor="let column of columns"
                    [sticky]="isStuck(stickyStartColumns, column)"
                    [stickyEnd]="isStuck(stickyEndColumns, column)">
        <cdk-header-cell *cdkHeaderCellDef> Header {{column}} </cdk-header-cell>
        <cdk-cell *cdkCellDef>{{column}}</cdk-cell>
        <cdk-footer-cell *cdkFooterCellDef> Footer {{column}} </cdk-footer-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columns; sticky: isStuck(stickyHeaders, 'header-1')">
      </cdk-header-row>
      <cdk-header-row *cdkHeaderRowDef="columns; sticky: isStuck(stickyHeaders, 'header-2')">
      </cdk-header-row>
      <cdk-header-row *cdkHeaderRowDef="columns; sticky: isStuck(stickyHeaders, 'header-3')">
      </cdk-header-row>

      <cdk-row *cdkRowDef="let row; columns: columns"></cdk-row>

      <cdk-footer-row *cdkFooterRowDef="columns; sticky: isStuck(stickyFooters, 'footer-1')">
      </cdk-footer-row>
      <cdk-footer-row *cdkFooterRowDef="columns; sticky: isStuck(stickyFooters, 'footer-2')">
      </cdk-footer-row>
      <cdk-footer-row *cdkFooterRowDef="columns; sticky: isStuck(stickyFooters, 'footer-3')">
      </cdk-footer-row>
    </cdk-table>
  `,
  styles: [`
    .cdk-header-cell, .cdk-cell, .cdk-footer-cell {
      display: block;
      width: 20px;
    }
  `],
  providers: [
    {provide: STICKY_POSITIONING_LISTENER, useExisting: StickyFlexLayoutCdkTableApp},
  ]
})
class StickyFlexLayoutCdkTableApp extends StickyPositioningListenerTest {
  dataSource: FakeDataSource = new FakeDataSource();
  columns = ['column-1', 'column-2', 'column-3', 'column-4', 'column-5', 'column-6'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;

  dir = 'ltr';
  stickyHeaders: string[] = [];
  stickyFooters: string[] = [];
  stickyStartColumns: string[] = [];
  stickyEndColumns: string[] = [];

  isStuck(list: string[], id: string) {
    return list.indexOf(id) != -1;
  }
}


@Component({
  template: `
    <table cdk-table [dataSource]="dataSource">
      <ng-container [cdkColumnDef]="column" *ngFor="let column of columns"
                    [sticky]="isStuck(stickyStartColumns, column)"
                    [stickyEnd]="isStuck(stickyEndColumns, column)">
        <th cdk-header-cell *cdkHeaderCellDef> Header {{column}} </th>
        <td cdk-cell *cdkCellDef="let row"> {{column}} </td>
        <td cdk-footer-cell *cdkFooterCellDef> Footer {{column}} </td>
      </ng-container>

      <tr cdk-header-row *cdkHeaderRowDef="columns; sticky: isStuck(stickyHeaders, 'header-1')">
      </tr>
      <tr cdk-header-row *cdkHeaderRowDef="columns; sticky: isStuck(stickyHeaders, 'header-2')">
      </tr>
      <tr cdk-header-row *cdkHeaderRowDef="columns; sticky: isStuck(stickyHeaders, 'header-3')">
      </tr>

      <tr cdk-row *cdkRowDef="let row; columns: columns"></tr>

      <tr cdk-footer-row *cdkFooterRowDef="columns; sticky: isStuck(stickyFooters, 'footer-1')">
      </tr>
      <tr cdk-footer-row *cdkFooterRowDef="columns; sticky: isStuck(stickyFooters, 'footer-2')">
      </tr>
      <tr cdk-footer-row *cdkFooterRowDef="columns; sticky: isStuck(stickyFooters, 'footer-3')">
      </tr>
    </table>
  `,
  styles: [`
    .cdk-header-cell, .cdk-cell, .cdk-footer-cell {
      display: block;
      width: 20px;
      box-sizing: border-box;
    }
  `],
  providers: [
    {provide: STICKY_POSITIONING_LISTENER, useExisting: StickyNativeLayoutCdkTableApp},
  ]
})
class StickyNativeLayoutCdkTableApp extends StickyPositioningListenerTest {
  dataSource: FakeDataSource = new FakeDataSource();
  columns = ['column-1', 'column-2', 'column-3', 'column-4', 'column-5', 'column-6'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;

  stickyHeaders: string[] = [];
  stickyFooters: string[] = [];
  stickyStartColumns: string[] = [];
  stickyEndColumns: string[] = [];

  isStuck(list: string[], id: string) {
    return list.indexOf(id) != -1;
  }
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container [cdkColumnDef]="column" *ngFor="let column of dynamicColumns">
        <cdk-header-cell *cdkHeaderCellDef> {{column}} </cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{column}} </cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="dynamicColumns"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: dynamicColumns;"></cdk-row>
    </cdk-table>
  `
})
class DynamicColumnDefinitionsCdkTableApp {
  dynamicColumns: any[] = [];
  dataSource: FakeDataSource = new FakeDataSource();

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource" role="treegrid">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
    </cdk-table>
  `
})
class CustomRoleCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender = ['column_a'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container [cdkColumnDef]="columnsToRender[0]">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
    </cdk-table>
  `
})
class CrazyColumnNameCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender = ['crazy-column-NAME-1!@#$%^-_&*()2'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="['column_a']"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: ['column_a']"></cdk-row>
    </cdk-table>
  `
})
class DuplicateColumnDefNameCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="['column_a']"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: ['column_a']"></cdk-row>
    </cdk-table>
  `
})
class MissingColumnDefCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_b">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="displayedColumns"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: displayedColumns"></cdk-row>
    </cdk-table>
  `
})
class MissingColumnDefAfterRenderCdkTableApp implements AfterViewInit {
  dataSource: FakeDataSource|null = null;
  displayedColumns: string[] = [];

  ngAfterViewInit() {
    setTimeout(() => {
      this.displayedColumns = ['column_a'];
    }, 0);
  }
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>
    </cdk-table>
  `
})
class MissingAllRowDefsCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
        <cdk-footer-cell *cdkFooterCellDef> Footer A </cdk-footer-cell>
      </ng-container>

      <cdk-row *cdkRowDef="let row; columns: ['column_a']"></cdk-row>
      <cdk-footer-row *cdkFooterRowDef="['column_a']"></cdk-footer-row>
    </cdk-table>
  `
})
class MissingHeaderRowDefCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
        <cdk-footer-cell *cdkFooterCellDef> Footer A </cdk-footer-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="['column_a']"></cdk-header-row>
      <cdk-footer-row *cdkFooterRowDef="['column_a']"></cdk-footer-row>
    </cdk-table>
  `
})
class MissingRowDefCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
        <cdk-footer-cell *cdkFooterCellDef> Footer A </cdk-footer-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="['column_a']"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: ['column_a']"></cdk-row>
    </cdk-table>
  `
})
class MissingFooterRowDefCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="undefinedColumns"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: undefinedColumns"></cdk-row>
    </cdk-table>
  `
})
class UndefinedColumnsCdkTableApp {
  undefinedColumns: string[];
  dataSource: FakeDataSource = new FakeDataSource();
}


@Component({
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="column_a">
        <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
        <cdk-cell *cdkCellDef="let row; let first = first;
                               let last = last; let even = even; let odd = odd"
                  [ngClass]="{
                    'custom-cell-class-first': enableCellContextClasses && first,
                    'custom-cell-class-last': enableCellContextClasses && last,
                    'custom-cell-class-even': enableCellContextClasses && even,
                    'custom-cell-class-odd': enableCellContextClasses && odd
                  }">
          {{row.a}}
        </cdk-cell>
      </ng-container>
      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: columnsToRender;
                           let first = first; let last = last; let even = even; let odd = odd"
               [ngClass]="{
                 'custom-row-class-first': enableRowContextClasses && first,
                 'custom-row-class-last': enableRowContextClasses && last,
                 'custom-row-class-even': enableRowContextClasses && even,
                 'custom-row-class-odd': enableRowContextClasses && odd
               }">
      </cdk-row>
    </cdk-table>
  `
})
class RowContextCdkTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender = ['column_a'];
  enableRowContextClasses = false;
  enableCellContextClasses = false;
}


@Component({
  selector: 'wrapper-table',
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container cdkColumnDef="content_column_a">
        <cdk-header-cell *cdkHeaderCellDef> Content Column A </cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
      </ng-container>
      <ng-container cdkColumnDef="content_column_b">
        <cdk-header-cell *cdkHeaderCellDef> Content Column B </cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}} </cdk-cell>
      </ng-container>

      <cdk-row *cdkRowDef="let row; columns: columns"></cdk-row>
      <ng-template cdkNoDataRow>No data</ng-template>
    </cdk-table>
  `
})
class WrapperCdkTableApp<T> implements AfterContentInit {
  @ContentChildren(CdkColumnDef) columnDefs: QueryList<CdkColumnDef>;
  @ContentChild(CdkHeaderRowDef) headerRowDef: CdkHeaderRowDef;
  @ContentChildren(CdkRowDef) rowDefs: QueryList<CdkRowDef<T>>;
  @ContentChild(CdkNoDataRow) noDataRow: CdkNoDataRow;

  @ViewChild(CdkTable, {static: true}) table: CdkTable<T>;

  @Input() columns: string[];
  @Input() dataSource: DataSource<T>;

  ngAfterContentInit() {
    // Register the content's column, row, and header row definitions.
    this.columnDefs.forEach(columnDef => this.table.addColumnDef(columnDef));
    this.rowDefs.forEach(rowDef => this.table.addRowDef(rowDef));
    this.table.addHeaderRowDef(this.headerRowDef);
    this.table.setNoDataRow(this.noDataRow);
  }
}


@Component({
  template: `
    <wrapper-table [dataSource]="dataSource" [columns]="columnsToRender">
      <ng-container cdkColumnDef="injected_column_a">
        <cdk-header-cell *cdkHeaderCellDef> Injected Column A </cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
      </ng-container>
      <ng-container cdkColumnDef="injected_column_b">
        <cdk-header-cell *cdkHeaderCellDef> Injected Column B </cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.b}} </cdk-cell>
      </ng-container>

      <!-- Only used for the 'when' row, the first row -->
      <ng-container cdkColumnDef="special_column">
        <cdk-cell *cdkCellDef="let row"> injected row with when predicate </cdk-cell>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
      <cdk-row class="first-row" *cdkRowDef="let row; columns: ['special_column']; when: firstRow">
      </cdk-row>
    </wrapper-table>
  `
})
class OuterTableApp {
  dataSource: FakeDataSource = new FakeDataSource();
  columnsToRender =
      ['content_column_a', 'content_column_b', 'injected_column_a', 'injected_column_b'];

  firstRow = (i: number) => i === 0;
}


@Component({
  template: `

      <ng-container cdkColumnDef="column_a">
         Column A
         {{row.a}}
      </ng-container>

      <ng-container cdkColumnDef="column_b">
         Column B
         {{row.b}}
      </ng-container>

      <ng-container cdkColumnDef="column_c">
         Column C
         {{row.c}}
      </ng-container>

        No data

  `
})
class NativeHtmlTableApp {
  dataSource: FakeDataSource | undefined = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b', 'column_c'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `

      <ng-container cdkColumnDef="column_a">
         Column A
        {{row.a}}
      </ng-container>

      <ng-container cdkColumnDef="column_b">
         Column B

            <ng-container cdkColumnDef="column_a">
               Column A
               {{row.a}}
            </ng-container>

            <ng-container cdkColumnDef="column_b">
               Column B
               {{row.b}}
            </ng-container>

            <ng-container cdkColumnDef="column_c">
               Column C
               {{row.c}}
            </ng-container>

      </ng-container>

      <ng-container cdkColumnDef="column_c">
         Column C
        {{row.c}}
      </ng-container>

  `
})
class NestedHtmlTableApp {
  dataSource: FakeDataSource | undefined = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b', 'column_c'];
}


@Component({
  template: `

      <ng-container cdkColumnDef="column_a">
         Column A
         {{row.a}}
      </ng-container>

      <ng-container cdkColumnDef="column_b">
         Column B
         {{row.b}}
      </ng-container>

      <ng-container cdkColumnDef="column_c">
         Column C
         {{row.c}}
      </ng-container>

  `
})
class NativeTableWithNoHeaderOrFooterRows {
  dataSource: FakeDataSource | undefined = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b', 'column_c'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `

Very important data

      <ng-container cdkColumnDef="column_a">
         Column A
         {{row.a}}
      </ng-container>

  `
})
class NativeHtmlTableWithCaptionApp {
  dataSource: FakeDataSource | undefined = new FakeDataSource();
  columnsToRender = ['column_a'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  template: `

      <colgroup>
        <col>
      </colgroup>
      <col>
      <ng-container cdkColumnDef="column_a">
         Column A
         {{row.a}}
      </ng-container>
      <ng-container cdkColumnDef="column_b">
         Column B
         {{row.b}}
      </ng-container>

  `
})
class NativeHtmlTableWithColgroupAndCol {
  dataSource: FakeDataSource | undefined = new FakeDataSource();
  columnsToRender = ['column_a', 'column_b'];

  @ViewChild(CdkTable) table: CdkTable<TestData>;
}


@Component({
  // Note that we need the `ngSwitch` below in order to surface the issue we're testing for.
  template: `
    <cdk-table [dataSource]="dataSource">
      <ng-container [ngSwitch]="true">
        <ng-container cdkColumnDef="column_a">
          <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
          <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
        </ng-container>
      </ng-container>

      <cdk-header-row *cdkHeaderRowDef="['column_a']"></cdk-header-row>
      <cdk-row *cdkRowDef="let row; columns: ['column_a']"></cdk-row>
    </cdk-table>
  `
})
class TableWithIndirectDescendantDefs {
  dataSource = new FakeDataSource();
}


interface PositionDirections {
      [key: string]: string | undefined;
      top?: string;
      bottom?: string;
      left?: string;
      right?: string;
    }


interface TestData {
  a: string;
  b: string;
  c: string;
}


connect(collectionViewer: CollectionViewer) {
    this.isConnected = true;
    return combineLatest([this._dataChange, collectionViewer.viewChange])
      .pipe(map(data => data[0]));
  }


disconnect() {
    this.isConnected = false;
  }


addData() {
    const nextIndex = this.data.length + 1;

    let copiedData = this.data.slice();
    copiedData.push({
      a: `a_${nextIndex}`,
      b: `b_${nextIndex}`,
      c: `c_${nextIndex}`
    });

    this.data = copiedData;
  }


connect(): Observable<boolean[]> {
    return this._dataChange;
  }


disconnect() {}


showIndexColumns() {
    const indexColumns = ['index', 'dataIndex', 'renderIndex'];
    this.columnsToRender = indexColumns;
    this.columnsForIsIndex1Row = indexColumns;
    this.columnsForHasC3Row = indexColumns;
  }


stickyColumnsUpdated(update: StickyUpdate) {
    this.mostRecentStickyColumnsUpdate = update;
  }


stickyEndColumnsUpdated(update: StickyUpdate) {
    this.mostRecentStickyEndColumnsUpdate = update;
  }


stickyHeaderRowsUpdated(update: StickyUpdate) {
    this.mostRecentStickyHeaderRowsUpdate = update;
  }


stickyFooterRowsUpdated(update: StickyUpdate) {
    this.mostRecentStickyFooterRowsUpdate = update;
  }


isStuck(list: string[], id: string) {
    return list.indexOf(id) != -1;
  }


ngAfterViewInit() {
    setTimeout(() => {
      this.displayedColumns = ['column_a'];
    }, 0);
  }


ngAfterContentInit() {
    // Register the content's column, row, and header row definitions.
    this.columnDefs.forEach(columnDef => this.table.addColumnDef(columnDef));
    this.rowDefs.forEach(rowDef => this.table.addRowDef(rowDef));
    this.table.addHeaderRowDef(this.headerRowDef);
    this.table.setNoDataRow(this.noDataRow);
  }


(
  orderId: string,
  dispatchingState: DispatchingState
) => {
  // context
  const api = React.useContext(ApiContext);
  const getServerTime = useContextGetSeverTime();
  // state
  const [tick, setTick] = React.useState(0);
  const [timestamp, setTimestmap] = React.useState<Date>();
  const [elapsed, setElapsed] = React.useState<number>();
  // side effects
  // setting up interval to keep elapsed time updated
  React.useEffect(() => {
    const interval = setInterval(() => setTick((value) => value + 5), 5000);
    return () => clearInterval(interval);
  }, []);
  // observing dispatchingState
  React.useEffect(() => {
    return api
      .order()
      .observeOrderDispatchingStateTimestamp(orderId, dispatchingState, (change) => {
        if (change) {
          setTimestmap((change.timestamp as firebase.firestore.Timestamp).toDate());
        }
      });
  }, [api, orderId, dispatchingState]);
  // calculating elapsed time every tick or when timestamp changes
  React.useEffect(() => {
    if (!timestamp) return;
    const now = (getServerTime ? getServerTime() : new Date()).getTime();
    const diff = now - timestamp.getTime();
    setElapsed(diff / 1000);
  }, [tick, timestamp]);
  // result
  return elapsed;
}


() => {
    const interval = setInterval(() => setTick((value) => value + 5), 5000);
    return () => clearInterval(interval);
  }


() => setTick((value) => value + 5)


(value) => value + 5


() => {
    return api
      .order()
      .observeOrderDispatchingStateTimestamp(orderId, dispatchingState, (change) => {
        if (change) {
          setTimestmap((change.timestamp as firebase.firestore.Timestamp).toDate());
        }
      });
  }


(change) => {
        if (change) {
          setTimestmap((change.timestamp as firebase.firestore.Timestamp).toDate());
        }
      }


() => {
    if (!timestamp) return;
    const now = (getServerTime ? getServerTime() : new Date()).getTime();
    const diff = now - timestamp.getTime();
    setElapsed(diff / 1000);
  }


function clear() {
    reset();
    onSearch({ searchText: "" });
  }


({
  className,
  onSearch,
  variant = "outline",
  shadow = false,
  inputClassName,
  ...rest
}) => {
  const {
    register,
    handleSubmit,
    watch,
    reset,

    formState: { errors },
  } = useForm<SearchValue>({
    defaultValues: {
      searchText: "",
    },
  });
  const searchText = watch("searchText");
  const { t } = useTranslation();

  useEffect(() => {
    if (!searchText) {
      onSearch({ searchText: "" });
    }
  }, [searchText]);

  const rootClassName = cn(
    classes.root,
    {
      [classes.normal]: variant === "normal",
      [classes.solid]: variant === "solid",
      [classes.outline]: variant === "outline",
    },
    {
      [classes.shadow]: shadow,
    },
    inputClassName
  );

  function clear() {
    reset();
    onSearch({ searchText: "" });
  }

  return (
    <form
      noValidate
      role="search"
      className={cn("w-full flex items-center relative", className)}
      onSubmit={handleSubmit(onSearch)}
    >
      <label htmlFor="search" className="sr-only">
        {t("form:input-label-search")}
      </label>
      <button className="outline-none absolute start-1 focus:outline-none active:outline-none p-2 text-body">
        <SearchIcon className="w-5 h-5" />
      </button>
      <input
        type="text"
        id="search"
        {...register("searchText")}
        className={rootClassName}
        placeholder={t("form:input-placeholder-search")}
        aria-label="Search"
        autoComplete="off"
        {...rest}
      />
      {errors.searchText && <p>{errors.searchText.message}</p>}
      {!!searchText && (
        <button
          type="button"
          onClick={clear}
          className="outline-none absolute end-1 focus:outline-none active:outline-none p-2 text-body"
        >
          <CloseIcon className="w-5 h-5" />
        </button>


() => {
    if (!searchText) {
      onSearch({ searchText: "" });
    }
  }


type SearchProps = {
  className?: string;
  shadow?: boolean;
  variant?: "normal" | "solid" | "outline";
  inputClassName?: string;
  onSearch: (data: SearchValue) => void;
};


type SearchValue = {
  searchText: string;
};


cn("w-full flex items-center relative", className)


handleSubmit(onSearch)


t("form:input-label-search")


t("form:input-placeholder-search")


() => {
    const eventEmitter = new EventEmitter();

    const exitPromise = new Promise<void>((resolve) => {
        eventEmitter.on("exit", () => resolve());
    });

    const onExit = () => {
        eventEmitter.emit("exit");
    };

    return {
        exitPromise,
        onExit,
    };
}


(resolve) => {
        eventEmitter.on("exit", () => resolve());
    }


() => {
        eventEmitter.emit("exit");
    }


() => {
    const results: Array<Result> = [];
    const collectResult = (result: Result) => {
        results.push(result);
    };

    return { results, collectResult };
}


(result: Result) => {
        results.push(result);
    }


async () => {
        const resultHandler = buildResultHandler();

        const consumerStrategy = new PriorityConsumerStrategy(logger,5);

        const consumers = [
            new TestCustomConsumer(logger,"A", resultHandler.collectResult, consumerStrategy),
            new TestCustomConsumer(logger,"B", resultHandler.collectResult, consumerStrategy),
            new TestCustomConsumer(logger,"C", resultHandler.collectResult, consumerStrategy),
        ];

        const exitHandler = buildExitHandler();

        const broker = new Broker(logger, consumers, 3, 3, 500, exitHandler.onExit);
        const producer = new CustomProducer(broker);

        for (let i = 0; i < 9; i++) {
            const message: Message = {
                data: `data_${i}`,
                priority: i,
                ttl: 5,
                predicateRunCount: 0,
            };

            producer.produce(JSON.stringify(message));
        }

        await exitHandler.exitPromise;

        assert.deepStrictEqual(
            resultHandler.results,
            [
                { consumerId: "B", data: "DATA_5" },
                { consumerId: "C", data: "DATA_6" },
                { consumerId: "C", data: "DATA_7" },
                { consumerId: "C", data: "DATA_8" },
                { consumerId: "B", data: "DATA_4" },
                { consumerId: "C", data: "DATA_3" },
                { consumerId: "A", data: "DATA_2" },
                { consumerId: "B", data: "DATA_1" },
                { consumerId: "C", data: "DATA_0" }
            ],
        );
    }


async () => {
        const resultHandler = buildResultHandler();

        const consumerStrategy = new TtlConsumerStrategy(logger, 0, 4);

        const consumers = [
            new TestCustomConsumer(logger,"A", resultHandler.collectResult, consumerStrategy),
            new TestCustomConsumer(logger,"B", resultHandler.collectResult, consumerStrategy),
            new TestCustomConsumer(logger,"C", resultHandler.collectResult, consumerStrategy),
        ];

        const exitHandler = buildExitHandler();

        const broker = new Broker(logger, consumers, 3, 3, 500, exitHandler.onExit);
        const producer = new CustomProducer(broker);

        for (let i = 0; i < 9; i++) {
            const message: Message = {
                data: `data_${i}`,
                priority: 0,
                ttl: 9 - i,
                predicateRunCount: 0,
            };

            producer.produce(JSON.stringify(message));
        }

        await exitHandler.exitPromise;

        assert.deepStrictEqual(
            resultHandler.results,
            [
                { consumerId: "C", data: "DATA_6" },
                { consumerId: "C", data: "DATA_7" },
                { consumerId: "C", data: "DATA_8" },
                { consumerId: "B", data: "DATA_5" },
                { consumerId: "A", data: "DATA_4" },
                { consumerId: "B", data: "DATA_3" },
                { consumerId: "C", data: "DATA_2" },
                { consumerId: "A", data: "DATA_1" },
                { consumerId: "B", data: "DATA_0" }
            ],
        );
    }


async () => {
        const resultHandler = buildResultHandler();

        const consumerStrategy = new PredicateConsumerStrategy(logger, "data_0", 3);

        const consumers = [
            new TestCustomConsumer(logger,"A", resultHandler.collectResult, consumerStrategy),
            new TestCustomConsumer(logger,"B", resultHandler.collectResult, consumerStrategy),
        ];

        const exitHandler = buildExitHandler();

        const broker = new Broker(logger, consumers, 3, 3, 500, exitHandler.onExit);
        const producer = new CustomProducer(broker);

        for (let i = 0; i < 3; i++) {
            const message: Message = {
                data: `data_${i}`,
                priority: 0,
                ttl: 0,
                predicateRunCount: 0,
            };

            producer.produce(JSON.stringify(message));
        }

        await exitHandler.exitPromise;

        assert.deepStrictEqual(
            resultHandler.results,
            [
                { consumerId: "A", data: "DATA_1" },
                { consumerId: "A", data: "DATA_2" },
                { consumerId: "B", data: "DATA_0" }
            ],
        );
    }


() => mockImagePlacerUtil


(num: number) => parseFloat(num.toFixed(5))


(
  props: Partial<ImagePlacerProps> = defaultProps,
  imageSourceWidth?: number,
  imageSourceHeight?: number,
): SetupInfo => {
  const onDragStart = jest.fn();
  const onDragMove = jest.fn();
  const onWheel = jest.fn();
  const onZoomChange = jest.fn();
  const onSaveImage = jest.fn();

  let wrapper = shallow(
    <ImagePlacer
      {...defaultComponentProps}
      onZoomChange={onZoomChange}
      onImageActions={onSaveImage}
      {...props}
    />,
  );

  const instance = wrapper.instance() as ImagePlacer;

  if (imageSourceWidth && imageSourceHeight) {
    /* force image load */
    instance.onImageLoad({} as any, imageSourceWidth, imageSourceHeight);
  }
  return {
    wrapper,
    instance,
    onDragStart,
    onDragMove,
    onWheel,
    onZoomChange,
    onSaveImage,
  };
}


() => {
    it('should show image if loads successfully', () => {
      const { wrapper } = setup();
      const img = wrapper.find(ImagePlacerImage);
      expect(img).toHaveLength(1);
      expect(img.props().src).toEqual('some-src');
    });

    it('should set error state if image load fails', () => {
      const { wrapper } = setup();
      const img = wrapper.find(ImagePlacerImage);
      img.props().onError('some-error');
      expect(wrapper.state('errorMessage')).toEqual('some-error');
    });

    it('should show image at zoomed to fit view when image loads', () => {
      const { instance } = setup();
      jest.spyOn(instance, 'zoomToFit');
      instance.onImageLoad({} as any, smallSize, smallSize);
      expect(instance.zoomToFit).toHaveBeenCalled();
    });

    it('should provide ImagePlacerAPI when image loads successfully', done => {
      setup({
        ...defaultProps,
        onImageActions: (api: ImageActions) => {
          expect(api).toHaveProperty('toCanvas');
          expect(api).toHaveProperty('toDataURL');
          expect(api).toHaveProperty('toFile');
          done();
        },
      });
    });

    it('should set image size state when image loads with no constraints', async () => {
      const { instance } = setup(
        {
          ...defaultProps,
          useConstraints: false,
        },
        smallSize,
        mediumSize,
      );
      expect(instance.state.imageWidth).toEqual(smallSize);
      expect(instance.state.imageHeight).toEqual(mediumSize);
    });

    it('should set image size state when image loads with constraints', async () => {
      const { instance } = setup(defaultProps, smallSize, mediumSize);
      const { containerWidth, containerHeight } = instance.props;
      expect(instance.state.imageWidth).toEqual(containerWidth);
      expect(instance.state.imageHeight).toEqual(
        containerHeight * smallToMediumRatio,
      );
    });
  }


() => {
      const { wrapper } = setup();
      const img = wrapper.find(ImagePlacerImage);
      expect(img).toHaveLength(1);
      expect(img.props().src).toEqual('some-src');
    }


() => {
      const { wrapper } = setup();
      const img = wrapper.find(ImagePlacerImage);
      img.props().onError('some-error');
      expect(wrapper.state('errorMessage')).toEqual('some-error');
    }


() => {
      const { instance } = setup();
      jest.spyOn(instance, 'zoomToFit');
      instance.onImageLoad({} as any, smallSize, smallSize);
      expect(instance.zoomToFit).toHaveBeenCalled();
    }


done => {
      setup({
        ...defaultProps,
        onImageActions: (api: ImageActions) => {
          expect(api).toHaveProperty('toCanvas');
          expect(api).toHaveProperty('toDataURL');
          expect(api).toHaveProperty('toFile');
          done();
        },
      });
    }


(api: ImageActions) => {
          expect(api).toHaveProperty('toCanvas');
          expect(api).toHaveProperty('toDataURL');
          expect(api).toHaveProperty('toFile');
          done();
        }


async () => {
      const { instance } = setup(
        {
          ...defaultProps,
          useConstraints: false,
        },
        smallSize,
        mediumSize,
      );
      expect(instance.state.imageWidth).toEqual(smallSize);
      expect(instance.state.imageHeight).toEqual(mediumSize);
    }


async () => {
      const { instance } = setup(defaultProps, smallSize, mediumSize);
      const { containerWidth, containerHeight } = instance.props;
      expect(instance.state.imageWidth).toEqual(containerWidth);
      expect(instance.state.imageHeight).toEqual(
        containerHeight * smallToMediumRatio,
      );
    }


() => {
      it('should be equal to container size plus margins', () => {
        const { instance } = setup();
        const {
          containerRectWithMargins,
          props: { margin, containerWidth, containerHeight },
        } = instance;
        const doubleMargin = margin * 2;
        expect(containerRectWithMargins.width).toEqual(
          containerWidth + doubleMargin,
        );
        expect(containerRectWithMargins.height).toEqual(
          containerHeight + doubleMargin,
        );
      });
    }


() => {
        const { instance } = setup();
        const {
          containerRectWithMargins,
          props: { margin, containerWidth, containerHeight },
        } = instance;
        const doubleMargin = margin * 2;
        expect(containerRectWithMargins.width).toEqual(
          containerWidth + doubleMargin,
        );
        expect(containerRectWithMargins.height).toEqual(
          containerHeight + doubleMargin,
        );
      }


() => {
      it('should be equal to container size minus margins', () => {
        const { instance } = setup();
        const {
          visibleBounds,
          props: { containerWidth, containerHeight },
        } = instance;
        expect(visibleBounds.width).toEqual(containerWidth);
        expect(visibleBounds.height).toEqual(containerHeight);
      });
    }


() => {
        const { instance } = setup();
        const {
          visibleBounds,
          props: { containerWidth, containerHeight },
        } = instance;
        expect(visibleBounds.width).toEqual(containerWidth);
        expect(visibleBounds.height).toEqual(containerHeight);
      }


() => {
      it('should scale and move image correctly when zooming with no constraints', () => {
        const { instance } = setup(
          {
            ...defaultProps,
            useConstraints: false,
          },
          smallSize,
          mediumSize,
        );
        for (let zoom = 0; zoom <= zoomSteps; zoom++) {
          const currentZoom = zoom / zoomSteps;
          const center = instance.imageBounds.center;
          instance.setZoom(currentZoom);
          const imageBounds = instance.imageBounds.map(round);
          expect(imageBounds.width).toEqual(
            round(smallSize * (1 + currentZoom)),
          );
          expect(imageBounds.height).toEqual(
            round(mediumSize * (1 + currentZoom)),
          );
          expect(imageBounds.left).toEqual(
            round(center.x - imageBounds.width * 0.5),
          );
          expect(imageBounds.top).toEqual(
            round(center.y - imageBounds.height * 0.5),
          );
        }
      });

      it('should scale and move image correctly when zooming with constraints', () => {
        const { instance } = setup(defaultProps, smallSize, mediumSize);
        const {
          props: { containerWidth, containerHeight },
        } = instance;
        for (let zoom = 0; zoom <= zoomSteps; zoom++) {
          const currentZoom = zoom / zoomSteps;
          const center = instance.imageBounds.center;
          instance.setZoom(currentZoom);
          const imageBounds = instance.imageBounds.map(round);
          expect(imageBounds.width).toEqual(
            round(containerWidth * (1 + currentZoom)),
          );
          expect(imageBounds.height).toEqual(
            round(containerHeight * (1 + currentZoom) * smallToMediumRatio),
          );
          expect(imageBounds.left).toEqual(
            round(center.x - imageBounds.width * 0.5),
          );
          expect(imageBounds.top).toEqual(
            round(center.y - imageBounds.height * 0.5),
          );
        }
      });
    }


() => {
        const { instance } = setup(
          {
            ...defaultProps,
            useConstraints: false,
          },
          smallSize,
          mediumSize,
        );
        for (let zoom = 0; zoom <= zoomSteps; zoom++) {
          const currentZoom = zoom / zoomSteps;
          const center = instance.imageBounds.center;
          instance.setZoom(currentZoom);
          const imageBounds = instance.imageBounds.map(round);
          expect(imageBounds.width).toEqual(
            round(smallSize * (1 + currentZoom)),
          );
          expect(imageBounds.height).toEqual(
            round(mediumSize * (1 + currentZoom)),
          );
          expect(imageBounds.left).toEqual(
            round(center.x - imageBounds.width * 0.5),
          );
          expect(imageBounds.top).toEqual(
            round(center.y - imageBounds.height * 0.5),
          );
        }
      }


() => {
        const { instance } = setup(defaultProps, smallSize, mediumSize);
        const {
          props: { containerWidth, containerHeight },
        } = instance;
        for (let zoom = 0; zoom <= zoomSteps; zoom++) {
          const currentZoom = zoom / zoomSteps;
          const center = instance.imageBounds.center;
          instance.setZoom(currentZoom);
          const imageBounds = instance.imageBounds.map(round);
          expect(imageBounds.width).toEqual(
            round(containerWidth * (1 + currentZoom)),
          );
          expect(imageBounds.height).toEqual(
            round(containerHeight * (1 + currentZoom) * smallToMediumRatio),
          );
          expect(imageBounds.left).toEqual(
            round(center.x - imageBounds.width * 0.5),
          );
          expect(imageBounds.top).toEqual(
            round(center.y - imageBounds.height * 0.5),
          );
        }
      }


() => {
      it('should zero imageSize, origin, and zoom when reset', async () => {
        const zoom = 0.5;
        const { instance } = setup(
          {
            ...defaultProps,
            zoom,
          },
          smallSize,
          smallSize,
        );
        instance.state.originX = 50;
        instance.state.originX = 60;
        instance.reset();
        expect(instance.imageBounds.width).toEqual(smallSize);
        expect(instance.imageBounds.height).toEqual(smallSize);
        expect(instance.state.zoom).toEqual(0);
        expect(instance.state.originX).toEqual(0);
        expect(instance.state.originY).toEqual(0);
      });
    }


async () => {
        const zoom = 0.5;
        const { instance } = setup(
          {
            ...defaultProps,
            zoom,
          },
          smallSize,
          smallSize,
        );
        instance.state.originX = 50;
        instance.state.originX = 60;
        instance.reset();
        expect(instance.imageBounds.width).toEqual(smallSize);
        expect(instance.imageBounds.height).toEqual(smallSize);
        expect(instance.state.zoom).toEqual(0);
        expect(instance.state.originX).toEqual(0);
        expect(instance.state.originY).toEqual(0);
      }


() => {
      describe('Using Constraints', () => {
        it('should snap left/right edges', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x += 1) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          });
        });

        it('should snap top/bottom edges', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          });
        });
      });

      describe('Not Using Constraints', () => {
        it('should snap left edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.left).toEqual(visibleBounds.left);
            }
          });
        });

        it('should snap top edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.top).toEqual(visibleBounds.top);
            }
          });
        });

        it('should snap bottom edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let y = -largeSize; y <= largeSize; y++) {
              instance.state.originY = y;
              instance.applyConstraints();
              expect(instance.imageBounds.bottom).toEqual(visibleBounds.bottom);
            }
          });
        });

        it('should snap right edge', () => {
          imageSizes.forEach(imageSize => {
            const { instance } = setup(defaultProps, imageSize, imageSize);
            const { visibleBounds } = instance;
            for (let x = -largeSize; x <= largeSize; x++) {
              instance.state.originX = x;
              instance.applyConstraints();
              expect(instance.imageBounds.right).toEqual(visibleBounds.right);
            }
          });
        });
      });
    }