() => {
        const filename = join(testDir, "TestFirstJUnit.java");
        let test = new Test("firstTest", filename, new JUnitLibrary(new JavaHandler()), "TestFirstJUnit");
        writeFileSync(filename, "class TestFirstJUnit {\n\n}", "utf-8");
        test.appendTestToFile();
        const shouldBe =
            "import org.junit.*;\n\n" +
            "class TestFirstJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid firstTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, 'utf8'), shouldBe);
    }


() => {
        const filename = join(testDir, "TestSecondJUnit.java");
        let test = new Test("firstTest", filename, new JUnitLibrary(new JavaHandler()), "TestSecondJUnit");
        writeFileSync(filename, "", "utf-8");
        test.appendTestToFile();
        const shouldBe =
            "import org.junit.*;\n\n" +
            "class TestSecondJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid firstTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, 'utf8'), shouldBe);
    }


() => {
        const filename = join(testDir, "TestThirdJUnit.java");
        let test = new Test("thirdTest", filename, new JUnitLibrary(new JavaHandler()), "TestThirdJUnit");
        writeFileSync(filename,
            "import org.junit.*;\n\n" +
            "class TestThirdJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid firstTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}",
            { encoding: "utf8" });
        test.appendTestToFile();

        const shouldBe = "import org.junit.*;\n\n" +
            "class TestThirdJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid firstTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "\t@Test\n" +
            "\tvoid thirdTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    }


() => {
        const filename = join(testDir, "TestTenthJUnit.java");
        let test = new Test("tenthTest", filename, new JUnitLibrary(new JavaHandler()), "TestTenthJUnit");
        writeFileSync(filename,
            "package mypackage;\n" +
            "class TestTenthJUnit {\n\n" +
            "}", { encoding: "utf-8" });
        test.appendTestToFile();
        const shouldBe = "package mypackage;\n" +
            "import org.junit.*;\n\n" +
            "class TestTenthJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid tenthTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    }


() => {
        const filename = join(testDir, "TestNinethJUnit.java");
        let test = new Test("tenthTest", filename, new JUnitLibrary(new JavaHandler()), "TestNinethJUnit");
        writeFileSync(filename,
            "package mypackage;\n" +
            "class TestNinethJUnit {\n\n" +
            "}", { encoding: "utf-8" });
        test.appendTestToFile();
        const shouldBe = "package mypackage;\n" +
            "import org.junit.*;\n\n" +
            "class TestNinethJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid tenthTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    }


() => {
        const filename = join(testDir, "TestEleventhJUnit.java");
        let test = new Test("thirdTest", filename, new JUnitLibrary(new JavaHandler()), "TestEleventhJUnit");
        writeFileSync(filename,
            "import org.java.junit.*;\n\n" +
            "class TestEleventhJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid firstTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}", { encoding: "utf-8" });
        test.appendTestToFile();
        const shouldBe = "import org.java.junit.*;\n\n" +
            "class TestEleventhJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid firstTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "\t@Test\n" +
            "\tvoid thirdTest() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    }


() => {
    const testDir = join(__filename, "..", "..", "..", "test_env");

    before((done) => {
        createDir(testDir).then(async () => {
            createFile(join(testDir, "TestFifthJUnit.java"));
            createFile(join(testDir, "TestSixthJUnit.java"));
            done();
        });
    });

    test("Should do the same with one test", () => {
        const filename = join(testDir, "TestFifthJUnit.java");
        writeFileSync(filename, "", { encoding: "utf8" });

        const testList = new TestList(filename, new JUnitLibrary(new JavaHandler()), "TestFifthJUnit");
        testList.addTest("test");
        testList.addTestsToFile();

        const shouldBe =
            "import org.junit.*;\n\n" +
            "class TestFifthJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid test() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    });

    test("Should also work with multiple tests", () => {
        const filename = join(testDir, "TestSixthJUnit.java");
        writeFileSync(filename, "", { encoding: "utf8" });

        const testList = new TestList(filename, new JUnitLibrary(new JavaHandler()), "TestSixthJUnit");
        testList.addTest("test1");
        testList.addTest("test2");
        testList.addTest("test3");
        testList.addTestsToFile();

        const shouldBe =
            "import org.junit.*;\n\n" +
            "class TestSixthJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid test1() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "\t@Test\n" +
            "\tvoid test2() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "\t@Test\n" +
            "\tvoid test3() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    });
}


(done) => {
        createDir(testDir).then(async () => {
            createFile(join(testDir, "TestFifthJUnit.java"));
            createFile(join(testDir, "TestSixthJUnit.java"));
            done();
        });
    }


async () => {
            createFile(join(testDir, "TestFifthJUnit.java"));
            createFile(join(testDir, "TestSixthJUnit.java"));
            done();
        }


() => {
        const filename = join(testDir, "TestFifthJUnit.java");
        writeFileSync(filename, "", { encoding: "utf8" });

        const testList = new TestList(filename, new JUnitLibrary(new JavaHandler()), "TestFifthJUnit");
        testList.addTest("test");
        testList.addTestsToFile();

        const shouldBe =
            "import org.junit.*;\n\n" +
            "class TestFifthJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid test() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    }


() => {
        const filename = join(testDir, "TestSixthJUnit.java");
        writeFileSync(filename, "", { encoding: "utf8" });

        const testList = new TestList(filename, new JUnitLibrary(new JavaHandler()), "TestSixthJUnit");
        testList.addTest("test1");
        testList.addTest("test2");
        testList.addTest("test3");
        testList.addTestsToFile();

        const shouldBe =
            "import org.junit.*;\n\n" +
            "class TestSixthJUnit {\n\n" +
            "\t@Test\n" +
            "\tvoid test1() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "\t@Test\n" +
            "\tvoid test2() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "\t@Test\n" +
            "\tvoid test3() {\n" +
            "\t\t// TODO\n" +
            "\t}\n\n" +
            "}";

        assert.strictEqual(readFileSync(filename, "utf-8"), shouldBe);
    }


/**
 * Cannot be a title case string validator.
 *
 * @export
 * @class CannotBeTitleCaseValidator
 * @extends {Validator}
 */
export class CannotBeTitleCaseValidator extends Validator
{
    // #region Constructors (1)

    /**
     * Creates an instance of CannotBeTitleCaseValidator.
     * @param {(string | null | undefined)} message - The custom validation message.
     * @param {(ValidationLevel | null | undefined)} validationLevel - The custom validation level.
     * @param {(string | null | undefined)} validationContext - The custom validation context.
     * @param {(number | null | undefined)} validationPriority - The custom validation priority.
     */
    constructor(message: string | null | undefined, validationLevel: ValidationLevel | null | undefined, validationContext: string | null | undefined, validationPriority: number | null | undefined)
    {
        super(message, validationLevel, validationContext, validationPriority);
    }

    // #endregion

    // #region Public Methods (1)

    /**
     * Validates the specified value.
     *
     * @param {*} value
     * @returns {boolean} - True if the value is valid; false otherwise.
     */
    public isValid(value: any): boolean
    {
        if (isNull(value))
        {
            return true;
        }
        else if (typeof value === "string")
        {
            if (isNullOrWhiteSpace(value))
            {
                return true;
            }
            else
            {
                return value !== toTitleCase(<string>value);
            }
        }
        else
        {
            return true;
        }
    }

    // #endregion

    // #region Protected Methods (1)

    /**
     * Gets the default message.
     *
     * @protected
     * @returns {string} - The default message.
     */
    protected getDefaultMessage(): string
    {
        return "Value cannot be title case.";
    }

    // #endregion
}


// #endregion
// #region Public Methods (1)
/**
     * Validates the specified value.
     *
     * @param {*} value
     * @returns {boolean} - True if the value is valid; false otherwise.
     */
public isValid(value: any): boolean
    {
        if (isNull(value))
        {
            return true;
        }
        else if (typeof value === "string")
        {
            if (isNullOrWhiteSpace(value))
            {
                return true;
            }
            else
            {
                return value !== toTitleCase(<string>value);
            }
        }
        else
        {
            return true;
        }
    }


// #endregion
// #region Protected Methods (1)
/**
     * Gets the default message.
     *
     * @protected
     * @returns {string} - The default message.
     */
protected getDefaultMessage(): string
    {
        return "Value cannot be title case.";
    }


function decodeChapterImages(input: string) {
    const key = CryptoJS.enc.Utf8.parse("123456781234567G")
    const iv = CryptoJS.enc.Utf8.parse("ABCDEF1G34123412")
    let decryptedStr = CryptoJS.AES.decrypt(
        input, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
    ).toString(CryptoJS.enc.Utf8)
    return JSON.parse(decryptedStr)
}


function parseComicHTML(html: string, referer: string) {
    let chapterImagesMatch = /chapterImages = "([^"]+)"/.exec(html)
    let prevChapterDataMatch = /prevChapterData = ([^;]*)/.exec(html)
    let nextChapterDataMatch = /nextChapterData = ([^;]*)/.exec(html)
    let chapterPathMatch = /chapterPath = "([^"]*)"/.exec(html)

    let chapterImages = chapterImagesMatch ? decodeChapterImages(chapterImagesMatch[1]) : undefined
    let prevChapterData = prevChapterDataMatch ? JSON.parse(prevChapterDataMatch[1]) : undefined
    let nextChapterData = nextChapterDataMatch ? JSON.parse(nextChapterDataMatch[1]) : undefined
    let chapterPath = chapterPathMatch ? chapterPathMatch[1] : undefined

    let root = HTMLParser.parse(html)
    let chapterTitleDOM = root.querySelector('.head_title h2')
    let chapterTitle = chapterTitleDOM.text

    console.log(chapterImages)

    const uriFromItem = (item: string) => {
        if (item.startsWith("http")) {
            if (/eshanyao/.exec(item) != null)
                return item
            else
                return `https://mhimg.eshanyao.com/showImage.php?url=${encodeURI(item)}`
        } else {
            return `https://mhimg.eshanyao.com/${chapterPath}${item}`
        }
    }

    let imageSources = chapterImages.map((item: string) => ({
        uri: uriFromItem(item),
        headers: {
            'User-Agent': userAgent,
            Referer: referer
        }
    }))

    return {
        prevChapter: prevChapterData.id as string | null,
        nextChapter: nextChapterData.id as string | null,
        title: chapterTitle,
        sources: imageSources
    }
}


function parseMangaHTML(html: string, meta: MangaMeta, referer: string) {
    let root = HTMLParser.parse(html)

    let cover = root.querySelector(".comic_i_img img")
    let coverSource = {
        uri: cover.attributes['src'],
        headers: {
            'User-Agent': userAgent,
            Referer: referer
        }
    }
    let title = cover.attributes['alt']

    let entries = root.querySelectorAll('#chapter-list-1 li a')

    let chapters = entries.map(item => {
        console.log(item.attributes['href'])
        let hrefMatch = /\/manhua\/([^\/]+)\/(\d+)/.exec(item.attributes['href'])
        let manga = hrefMatch && hrefMatch[1]
        let chapterId = hrefMatch && hrefMatch[2]
        let title = item.attributes['title']

        console.log(chapterId)

        return {
            mangaNo: manga as string,
            chapterNo: chapterId as string,
            title: title
        }
    })

    return {
        meta: {
            ...meta,
            coverSource: coverSource,
            title: title,
        },
        chapters: chapters
    }
}


function parseSearchResult(html: string, referer: string) {
    let root = HTMLParser.parse(html)
    let entries = root.querySelectorAll('.list-comic a.image-link')

    let resultCount = parseInt(root.querySelector('.comi_num em').text)

    let results = entries.map(item => {
        let title = item.attributes['title']
        let mangaIdMatch = /\/manhua\/([^\/]*)\//.exec(item.attributes['href'])
        let mangaId = mangaIdMatch && mangaIdMatch[1]
        let coverElement = item.querySelector('img')
        let coverSource = {
            uri: coverElement.attributes['src'],
            headers: {
                'User-Agent': userAgent,
                Referer: referer
            }
        }

        return {
            title: title,
            id: mangaId as string,
            coverSource: coverSource
        }
    })

    return {
        totalCount: resultCount,
        results: results
    }
}


(item: string) => {
        if (item.startsWith("http")) {
            if (/eshanyao/.exec(item) != null)
                return item
            else
                return `https://mhimg.eshanyao.com/showImage.php?url=${encodeURI(item)}`
        } else {
            return `https://mhimg.eshanyao.com/${chapterPath}${item}`
        }
    }


(item: string) => ({
        uri: uriFromItem(item),
        headers: {
            'User-Agent': userAgent,
            Referer: referer
        }
    })


item => {
        console.log(item.attributes['href'])
        let hrefMatch = /\/manhua\/([^\/]+)\/(\d+)/.exec(item.attributes['href'])
        let manga = hrefMatch && hrefMatch[1]
        let chapterId = hrefMatch && hrefMatch[2]
        let title = item.attributes['title']

        console.log(chapterId)

        return {
            mangaNo: manga as string,
            chapterNo: chapterId as string,
            title: title
        }
    }


item => {
        let title = item.attributes['title']
        let mangaIdMatch = /\/manhua\/([^\/]*)\//.exec(item.attributes['href'])
        let mangaId = mangaIdMatch && mangaIdMatch[1]
        let coverElement = item.querySelector('img')
        let coverSource = {
            uri: coverElement.attributes['src'],
            headers: {
                'User-Agent': userAgent,
                Referer: referer
            }
        }

        return {
            title: title,
            id: mangaId as string,
            coverSource: coverSource
        }
    }


res => res.text().then(html => {
            let parseResult = parseComicHTML(html, pageUrl)
            this.prevChapter = parseResult.prevChapter
            this.nextChapter = parseResult.nextChapter
            callback({
                meta: {
                    title: parseResult.title,
                    mangaNo: this.mangaId,
                    chapterNo: this.chapterId
                },
                pages: parseResult.sources.map((item: any) => ({ source: item }))
            })
        })


html => {
            let parseResult = parseComicHTML(html, pageUrl)
            this.prevChapter = parseResult.prevChapter
            this.nextChapter = parseResult.nextChapter
            callback({
                meta: {
                    title: parseResult.title,
                    mangaNo: this.mangaId,
                    chapterNo: this.chapterId
                },
                pages: parseResult.sources.map((item: any) => ({ source: item }))
            })
        }


(item: any) => ({ source: item })


res => res.text().then(html => {
                let parseResult = parseSearchResult(html, searchUrl)
                this.totalResults = parseResult.totalCount
                callback(parseResult.results)
            })


html => {
                let parseResult = parseSearchResult(html, searchUrl)
                this.totalResults = parseResult.totalCount
                callback(parseResult.results)
            }


item => item.id == entry.id


item => item.id != entry.id


res => res.text().then(html => callback(parseMangaHTML(html, meta, mangaUrl)))


html => callback(parseMangaHTML(html, meta, mangaUrl))


class ManhuaduiChapterFeeder implements ChapterFeeder {
    nextChapter: string | null = null
    prevChapter: string | null = null
    chapterId: string
    mangaId: string

    constructor(meta: ChapterMeta) {
        this.chapterId = meta.chapterNo
        this.mangaId = meta.mangaNo
    }

    current(callback: (chapter?: ChapterInfo) => void) {
        const pageUrl = `https://www.manhuadui.com/manhua/${this.mangaId}/${this.chapterId}.html`
        console.log(`fetching ${pageUrl}`)
        fetch(pageUrl, {
            headers: {
                'User-Agent': userAgent
            }
        }).then(res => res.text().then(html => {
            let parseResult = parseComicHTML(html, pageUrl)
            this.prevChapter = parseResult.prevChapter
            this.nextChapter = parseResult.nextChapter
            callback({
                meta: {
                    title: parseResult.title,
                    mangaNo: this.mangaId,
                    chapterNo: this.chapterId
                },
                pages: parseResult.sources.map((item: any) => ({ source: item }))
            })
        }))
    }

    prev(callback: (chapter?: ChapterInfo) => void) {
        if (!this.prevChapter)
            return callback()

        this.chapterId = this.prevChapter
        this.current(callback)
    }

    next(callback: (chapter?: ChapterInfo) => void) {
        if (!this.nextChapter)
            return callback()

        this.chapterId = this.nextChapter
        this.current(callback)
    }
}


class ManhuaduiSearchResultFeeder implements SearchResultFeeder {
    totalResults: number = 0
    currentPage: number = 0

    constructor(private queryStr: string) { }

    more(callback: (result: MangaMeta[]) => void) {
        if (this.currentPage != 0 && this.currentPage * 36 >= this.totalResults)
            return callback([])

        this.currentPage += 1

        let searchUrl = `https://www.manhuadui.com/search/?keywords=${this.queryStr}&page=${this.currentPage}`
        fetch(searchUrl, { headers: { 'User-Agent': userAgent } })
            .then(res => res.text().then(html => {
                let parseResult = parseSearchResult(html, searchUrl)
                this.totalResults = parseResult.totalCount
                callback(parseResult.results)
            }))
    }
}


export default class ManhuaduiAPI implements MangaAPI {
    name: string = "manhuadui"

    getFavorite(entries: MangaMeta[]) {
        return entries
    }

    addFavorite(entries: MangaMeta[], entry: MangaMeta) {
        if (entries.find(item => item.id == entry.id))
            return entries
        return entries.concat([entry])
    }

    removeFavorite(entries: MangaMeta[], entry: MangaMeta) {
        return entries.filter(item => item.id != entry.id)
    }

    getManga(meta: MangaMeta, callback: (info: MangaInfo) => void) {
        let mangaUrl = `https://www.manhuadui.com/manhua/${meta.id}/`

        fetch(mangaUrl, { headers: { 'User-Agent': userAgent } })
            .then(res => res.text().then(html => callback(parseMangaHTML(html, meta, mangaUrl))))
    }

    search(str: string, callback: (feeder: SearchResultFeeder) => void) {
        callback(new ManhuaduiSearchResultFeeder(str))
    }

    getChapterFeeder(chapter: ChapterMeta, callback: (feeder: ChapterFeeder) => void) {
        callback(new ManhuaduiChapterFeeder(chapter))
    }
}


current(callback: (chapter?: ChapterInfo) => void) {
        const pageUrl = `https://www.manhuadui.com/manhua/${this.mangaId}/${this.chapterId}.html`
        console.log(`fetching ${pageUrl}`)
        fetch(pageUrl, {
            headers: {
                'User-Agent': userAgent
            }
        }).then(res => res.text().then(html => {
            let parseResult = parseComicHTML(html, pageUrl)
            this.prevChapter = parseResult.prevChapter
            this.nextChapter = parseResult.nextChapter
            callback({
                meta: {
                    title: parseResult.title,
                    mangaNo: this.mangaId,
                    chapterNo: this.chapterId
                },
                pages: parseResult.sources.map((item: any) => ({ source: item }))
            })
        }))
    }


prev(callback: (chapter?: ChapterInfo) => void) {
        if (!this.prevChapter)
            return callback()

        this.chapterId = this.prevChapter
        this.current(callback)
    }


next(callback: (chapter?: ChapterInfo) => void) {
        if (!this.nextChapter)
            return callback()

        this.chapterId = this.nextChapter
        this.current(callback)
    }


more(callback: (result: MangaMeta[]) => void) {
        if (this.currentPage != 0 && this.currentPage * 36 >= this.totalResults)
            return callback([])

        this.currentPage += 1

        let searchUrl = `https://www.manhuadui.com/search/?keywords=${this.queryStr}&page=${this.currentPage}`
        fetch(searchUrl, { headers: { 'User-Agent': userAgent } })
            .then(res => res.text().then(html => {
                let parseResult = parseSearchResult(html, searchUrl)
                this.totalResults = parseResult.totalCount
                callback(parseResult.results)
            }))
    }


getFavorite(entries: MangaMeta[]) {
        return entries
    }


addFavorite(entries: MangaMeta[], entry: MangaMeta) {
        if (entries.find(item => item.id == entry.id))
            return entries
        return entries.concat([entry])
    }


removeFavorite(entries: MangaMeta[], entry: MangaMeta) {
        return entries.filter(item => item.id != entry.id)
    }


getManga(meta: MangaMeta, callback: (info: MangaInfo) => void) {
        let mangaUrl = `https://www.manhuadui.com/manhua/${meta.id}/`

        fetch(mangaUrl, { headers: { 'User-Agent': userAgent } })
            .then(res => res.text().then(html => callback(parseMangaHTML(html, meta, mangaUrl))))
    }


search(str: string, callback: (feeder: SearchResultFeeder) => void) {
        callback(new ManhuaduiSearchResultFeeder(str))
    }


getChapterFeeder(chapter: ChapterMeta, callback: (feeder: ChapterFeeder) => void) {
        callback(new ManhuaduiChapterFeeder(chapter))
    }


config => {
    store.commit(SHOW_LOADING, true, { root: true });
    return config;
  }


error => {
    store.commit(SHOW_LOADING, false, { root: true });
    return error;
  }


@NgModule({
    imports: [ArtemisExerciseScoresChartModule, ArtemisSharedModule, FontAwesomeModule, RouterModule.forChild(routes), NgxChartsModule],
    declarations: [CourseStatisticsComponent],
})
export class CourseStatisticsModule {}


export enum OrderState {
  NEW = 'NEW',
  APPROVED = 'APPROVED',
  REJECTED = 'REJECTED',
}


function getPageName(id: PageDefinition['id']): string {
        // NOTE: assumes the page is always found
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const page = filteredPagesMap[id];
        if (page.isDrillDown) {
            // NOTE: assumes the drilldown page always have a parent set
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            return `${getPageName(page.parent!)} > ${page.name}`;
        }
        return page.name;
    }


({
    allowDrillDownPages = false,
    name,
    onChange = noop,
    value
}) => {
    const filteredPagesMap = useSelector((state: ReduxState) => {
        const pagesMap = createPagesMap(state.pages);
        return allowDrillDownPages ? pagesMap : pickBy(pagesMap, page => !page.isDrillDown);
    });
    const [pageId, setPageId] = useState(value);

    function getPageName(id: PageDefinition['id']): string {
        // NOTE: assumes the page is always found
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const page = filteredPagesMap[id];
        if (page.isDrillDown) {
            // NOTE: assumes the drilldown page always have a parent set
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            return `${getPageName(page.parent!)} > ${page.name}`;
        }
        return page.name;
    }

    const handleInputChange: DropdownProps['onChange'] = (event, field) => {
        const fieldValue = field.value as string;
        setPageId(fieldValue);
        onChange(event, { name, value: fieldValue });
    };

    const pagesOptions = map(filteredPagesMap, page => ({
        text: getPageName(page.id),
        value: page.id,
        key: page.id
    }));
    pagesOptions.sort((a, b) => a.text.localeCompare(b.text));
    const defaultValue = pagesOptions[0].value;

    return (
        <Form.Dropdown
            name="pageId"
            search
            selection
            value={pageId || defaultValue}


(state: ReduxState) => {
        const pagesMap = createPagesMap(state.pages);
        return allowDrillDownPages ? pagesMap : pickBy(pagesMap, page => !page.isDrillDown);
    }


page => !page.isDrillDown


(event, field) => {
        const fieldValue = field.value as string;
        setPageId(fieldValue);
        onChange(event, { name, value: fieldValue });
    }


page => ({
        text: getPageName(page.id),
        value: page.id,
        key: page.id
    })


(a, b) => a.text.localeCompare(b.text)


type PageFilterProps = Pick<Stage.Types.CustomConfigurationComponentProps<string>, 'name' | 'onChange' | 'value'> & {
    allowDrillDownPages?: boolean;
};


(loadPeristedState = true) => {
  const autoSaved = getAutoSavedOperatorData();

  const initialState: EditorReducerState = {
    operator: getDefaultOperator(),
    operatorModified: false,
    uploads: [],
    formErrors: {},
    sectionStatus: {
      metadata: EDITOR_STATUS.empty,
      'owned-crds': EDITOR_STATUS.empty,
      'required-crds': EDITOR_STATUS.empty,
      deployments: EDITOR_STATUS.empty,
      permissions: EDITOR_STATUS.empty,
      'cluster-permissions': EDITOR_STATUS.empty,
      'install-modes': EDITOR_STATUS.empty
    }
  };

  if (loadPeristedState && autoSaved) {
    initialState.operator = autoSaved.editorState.operator || initialState.operator;
    initialState.uploads = autoSaved.editorState.uploads || initialState.uploads;
  }

  return initialState;
}


(state: EditorReducerState = getInitialState(), action) => {
  switch (action.type) {
    case reduxConstants.SET_EDITOR_SECTION_STATUS:
      return {
        ...state,
        sectionStatus: {
          ...state.sectionStatus,
          [action.section]: action.status
        }
      };

    case reduxConstants.SET_EDITOR_ALL_SECTIONS_STATUS:
      return {
        ...state,
        sectionStatus: {
          ...state.sectionStatus,
          ...action.status
        }
      };

    case reduxConstants.RESET_EDITOR_OPERATOR:
      return {
        ...getInitialState(false)
      };

    case reduxConstants.SET_EDITOR_OPERATOR:
      // save operator on change
      return {
        ...state,
        operator: action.operator,
        operatorModified: true
      };

    case reduxConstants.SET_EDITOR_UPLOADS:
      return {
        ...state,
        uploads: action.uploads
      };

    case reduxConstants.SET_EDITOR_FORM_ERRORS:
      return {
        ...state,
        formErrors: action.formErrors
      };

    default:
      return state;
  }
}


export interface EditorReducerState {
  operator: Operator
  operatorModified: boolean
  uploads: UploadMetadata[],
  formErrors: any,
  sectionStatus: Record<keyof ISectionFields, EDITOR_STATUS>
}


async (resolve: (result: string) => void, reject) => {
      const filter = input.value.substring(0, input.selectionStart);
      const history = getHistorySuggestions(filter);

      const historySuggestions: Suggestion[] = [];

      if ((!history[0] || !history[0].canSuggest) && filter.trim() !== '') {
        historySuggestions.unshift({
          primaryText: filter,
          secondaryText: store.dictionary.searchInGoogle,
          favicon: icons.search,
          isSearch: true,
        });
        if (isURL(filter)) {
          historySuggestions.unshift({
            primaryText: filter,
            secondaryText: store.dictionary.openWebsite,
            favicon: icons.page,
          });
        }
      }

      for (const item of history) {
        if (!item.isSearch) {
          historySuggestions.push({
            primaryText: item.url,
            secondaryText: item.title,
            favicon: store.faviconsStore.favicons[item.favicon],
            canSuggest: item.canSuggest,
          });
        } else {
          historySuggestions.push({
            primaryText: item.url,
            secondaryText: store.dictionary.searchInGoogle,
            favicon: icons.search,
            canSuggest: item.canSuggest,
          });
        }
      }

      let suggestions: Suggestion[] =
        input.value === ''
          ? []
          : historySuggestions.concat(searchSuggestions).slice(0, 6);

      for (let i = 0; i < suggestions.length; i++) {
        suggestions[i].id = i;
      }

      this.suggestions = suggestions;

      if (historySuggestions.length > 0 && historySuggestions[0].canSuggest) {
        resolve(historySuggestions[0].primaryText);
      }

      const searchData = await getSearchSuggestions(filter);

      if (input.value.substring(0, input.selectionStart) === filter) {
        searchSuggestions = [];
        for (const item of searchData) {
          searchSuggestions.push({
            primaryText: item,
            favicon: icons.search,
            isSearch: true,
          });
        }

        suggestions =
          input.value === ''
            ? []
            : historySuggestions.concat(searchSuggestions).slice(0, 6);

        for (let i = 0; i < suggestions.length; i++) {
          suggestions[i].id = i;
        }

        this.suggestions = suggestions;
      }
    }


export class SuggestionsStore {
  @observable
  public suggestions: Suggestion[] = [];

  @observable
  public selected = 0;

  public load(input: HTMLInputElement) {
    return new Promise(async (resolve: (result: string) => void, reject) => {
      const filter = input.value.substring(0, input.selectionStart);
      const history = getHistorySuggestions(filter);

      const historySuggestions: Suggestion[] = [];

      if ((!history[0] || !history[0].canSuggest) && filter.trim() !== '') {
        historySuggestions.unshift({
          primaryText: filter,
          secondaryText: store.dictionary.searchInGoogle,
          favicon: icons.search,
          isSearch: true,
        });
        if (isURL(filter)) {
          historySuggestions.unshift({
            primaryText: filter,
            secondaryText: store.dictionary.openWebsite,
            favicon: icons.page,
          });
        }
      }

      for (const item of history) {
        if (!item.isSearch) {
          historySuggestions.push({
            primaryText: item.url,
            secondaryText: item.title,
            favicon: store.faviconsStore.favicons[item.favicon],
            canSuggest: item.canSuggest,
          });
        } else {
          historySuggestions.push({
            primaryText: item.url,
            secondaryText: store.dictionary.searchInGoogle,
            favicon: icons.search,
            canSuggest: item.canSuggest,
          });
        }
      }

      let suggestions: Suggestion[] =
        input.value === ''
          ? []
          : historySuggestions.concat(searchSuggestions).slice(0, 6);

      for (let i = 0; i < suggestions.length; i++) {
        suggestions[i].id = i;
      }

      this.suggestions = suggestions;

      if (historySuggestions.length > 0 && historySuggestions[0].canSuggest) {
        resolve(historySuggestions[0].primaryText);
      }

      const searchData = await getSearchSuggestions(filter);

      if (input.value.substring(0, input.selectionStart) === filter) {
        searchSuggestions = [];
        for (const item of searchData) {
          searchSuggestions.push({
            primaryText: item,
            favicon: icons.search,
            isSearch: true,
          });
        }

        suggestions =
          input.value === ''
            ? []
            : historySuggestions.concat(searchSuggestions).slice(0, 6);

        for (let i = 0; i < suggestions.length; i++) {
          suggestions[i].id = i;
        }

        this.suggestions = suggestions;
      }
    });
  }
}


public load(input: HTMLInputElement) {
    return new Promise(async (resolve: (result: string) => void, reject) => {
      const filter = input.value.substring(0, input.selectionStart);
      const history = getHistorySuggestions(filter);

      const historySuggestions: Suggestion[] = [];

      if ((!history[0] || !history[0].canSuggest) && filter.trim() !== '') {
        historySuggestions.unshift({
          primaryText: filter,
          secondaryText: store.dictionary.searchInGoogle,
          favicon: icons.search,
          isSearch: true,
        });
        if (isURL(filter)) {
          historySuggestions.unshift({
            primaryText: filter,
            secondaryText: store.dictionary.openWebsite,
            favicon: icons.page,
          });
        }
      }

      for (const item of history) {
        if (!item.isSearch) {
          historySuggestions.push({
            primaryText: item.url,
            secondaryText: item.title,
            favicon: store.faviconsStore.favicons[item.favicon],
            canSuggest: item.canSuggest,
          });
        } else {
          historySuggestions.push({
            primaryText: item.url,
            secondaryText: store.dictionary.searchInGoogle,
            favicon: icons.search,
            canSuggest: item.canSuggest,
          });
        }
      }

      let suggestions: Suggestion[] =
        input.value === ''
          ? []
          : historySuggestions.concat(searchSuggestions).slice(0, 6);

      for (let i = 0; i < suggestions.length; i++) {
        suggestions[i].id = i;
      }

      this.suggestions = suggestions;

      if (historySuggestions.length > 0 && historySuggestions[0].canSuggest) {
        resolve(historySuggestions[0].primaryText);
      }

      const searchData = await getSearchSuggestions(filter);

      if (input.value.substring(0, input.selectionStart) === filter) {
        searchSuggestions = [];
        for (const item of searchData) {
          searchSuggestions.push({
            primaryText: item,
            favicon: icons.search,
            isSearch: true,
          });
        }

        suggestions =
          input.value === ''
            ? []
            : historySuggestions.concat(searchSuggestions).slice(0, 6);

        for (let i = 0; i < suggestions.length; i++) {
          suggestions[i].id = i;
        }

        this.suggestions = suggestions;
      }
    });
  }


function createComponent<T>(
      componentType: Type<T>, declarations: any[] = []): ComponentFixture<T> {
    TestBed.configureTestingModule({
      imports: [CdkTableModule, BidiModule],
      declarations: [componentType, ...declarations],
    }).compileComponents();

    return TestBed.createComponent<T>(componentType);
  }


function setupTableTestApp(componentType: Type<any>, declarations: any[] = []) {
    fixture = createComponent(componentType, declarations);
    component = fixture.componentInstance;
    fixture.detectChanges();

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


function addInitialIndexAttribute() {
        // 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');
      }


function expectNoStickyStyles(elements: any[]) {
      elements.forEach(element => {
        expect(element.classList.contains('cdk-table-sticky')).toBe(false);
        expectStickyBorderClass(element);
        expect(element.style.position).toBe('');
        expect(element.style.zIndex || '0').toBe('0');
        ['top', 'bottom', 'left', 'right'].forEach(d => {
          expect(element.style[d] || 'unset')
            .withContext(`Expected ${d} to be unset`).toBe('unset');
        });
      });
    }


function expectStickyStyles(element: any, zIndex: string, directions: PositionDirections = {}) {
      expect(element.style.position).toContain('sticky');
      expect(element.style.zIndex)
        .withContext(`Expected zIndex to be ${zIndex}`).toBe(zIndex);

      ['top', 'bottom', 'left', 'right'].forEach(d => {
        const directionValue = directions[d];

        if (!directionValue) {
          // If no expected position for this direction, must either be unset or empty string
          // If no expected position for this direction, must either be unset or empty string
          expect(element.style[d] || 'unset')
            .withContext(`Expected ${d} to be unset`).toBe('unset');
          return;
        }

        const expectationMessage = `Expected direction ${d} to be ${directionValue}`;

        // If the direction contains `px`, we parse the number to be able to avoid deviations
        // caused by individual browsers.
        if (directionValue.includes('px')) {
          expect(Math.round(parseInt(element.style[d])))
            .withContext(expectationMessage).toBe(Math.round(parseInt(directionValue)));
        } else {
          expect(element.style[d])
            .withContext(expectationMessage).toBe(directionValue);
        }
      });
    }


function expectStickyBorderClass(element: any, directions: {[key: string]: boolean} = {}) {
      ['top', 'bottom', 'left', 'right'].forEach(d => {
        const stickyClass = `cdk-table-sticky-border-elem-${d}`;

        if (directions[d]) {
          expect(element.classList).toContain(stickyClass);
        } else {
          expect(element.classList).not.toContain(stickyClass);
        }
      });
    }


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();
    }


function getElements(element: Element, query: string): HTMLElement[] {
  return [].slice.call(element.querySelectorAll(query));
}


function getHeaderRows(tableElement: Element): HTMLElement[] {
  return [].slice.call(tableElement.querySelectorAll('.cdk-header-row'))!;
}


function getFooterRows(tableElement: Element): HTMLElement[] {
  return [].slice.call(tableElement.querySelectorAll('.cdk-footer-row'))!;
}


function getRows(tableElement: Element): HTMLElement[] {
  return getElements(tableElement, '.cdk-row');
}


function getCells(row: Element): HTMLElement[] {
  if (!row) {
    return [];
  }

  let cells = getElements(row, 'cdk-cell');
  if (!cells.length) {
    cells = getElements(row, 'td.cdk-cell');
  }

  return cells;
}


function getHeaderCells(headerRow: Element): HTMLElement[] {
  let cells = getElements(headerRow, 'cdk-header-cell');
  if (!cells.length) {
    cells = getElements(headerRow, 'th.cdk-header-cell');
  }

  return cells;
}


function getFooterCells(footerRow: Element): HTMLElement[] {
  let cells = getElements(footerRow, 'cdk-footer-cell');
  if (!cells.length) {
    cells = getElements(footerRow, 'td.cdk-footer-cell');
  }

  return cells;
}


function getActualTableContent(tableElement: Element): string[][] {
  let actualTableContent: Element[][] = [];
  getHeaderRows(tableElement).forEach(row => {
    actualTableContent.push(getHeaderCells(row));
  });

  // Check data row cells
  const rows = getRows(tableElement).map(row => getCells(row));
  actualTableContent = actualTableContent.concat(rows);

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

  // Convert the nodes into their text content;
  return actualTableContent.map(row => row.map(cell => cell.textContent!.trim()));
}


export function expectTableToMatchContent(tableElement: Element, expected: any[]) {
  const missedExpectations: string[] = [];
  function checkCellContent(actualCell: string, expectedCell: string) {
    if (actualCell !== expectedCell) {
      missedExpectations.push(`Expected cell contents to be ${expectedCell} but was ${actualCell}`);
    }
  }

  const actual = getActualTableContent(tableElement);

  // Make sure the number of rows match
  if (actual.length !== expected.length) {
    missedExpectations.push(`Expected ${expected.length} total rows but got ${actual.length}`);
    fail(missedExpectations.join('\n'));
  }

  actual.forEach((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);
    });
  });

  if (missedExpectations.length) {
    fail(missedExpectations.join('\n'));
  }
}


function checkCellContent(actualCell: string, expectedCell: string) {
    if (actualCell !== expectedCell) {
      missedExpectations.push(`Expected cell contents to be ${expectedCell} but was ${actualCell}`);
    }
  }


() => {
      setupTableTestApp(SimpleCdkTableApp);

      component = fixture.componentInstance as SimpleCdkTableApp;
      dataSource = component.dataSource;
      table = component.table;

      fixture.detectChanges();
    }


() => {
      it('with a connected data source', () => {
        expect(table.dataSource).toBe(dataSource);
        expect(dataSource.isConnected).toBe(true);
        expect(component.contentChangedCount).toBe(1);
      });

      it('with a rendered header with the right number of header cells', () => {
        const header = getHeaderRows(tableElement)[0];

        expect(header).toBeTruthy();
        expect(header.classList).toContain('customHeaderRowClass');
        expect(getHeaderCells(header).length).toBe(component.columnsToRender.length);
      });

      it('with rendered rows with right number of row cells', () => {
        const rows = getRows(tableElement);

        expect(rows.length).toBe(dataSource.data.length);
        rows.forEach(row => {
          expect(row.classList).toContain('customRowClass');
          expect(getCells(row).length).toBe(component.columnsToRender.length);
        });
      });

      it('with column class names provided to header and data row cells', () => {
        const header = getHeaderRows(tableElement)[0];
        getHeaderCells(header).forEach((headerCell, index) => {
          expect(headerCell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
        });

        getRows(tableElement).forEach(row => {
          getCells(row).forEach((cell, index) => {
            expect(cell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
          });
        });
      });

      it('with the right accessibility roles', () => {
        expect(tableElement.getAttribute('role')).toBe('table');

        expect(getHeaderRows(tableElement)[0].getAttribute('role')).toBe('row');
        const header = getHeaderRows(tableElement)[0];
        getHeaderCells(header).forEach(cell => {
          expect(cell.getAttribute('role')).toBe('columnheader');
        });

        getRows(tableElement).forEach(row => {
          expect(row.getAttribute('role')).toBe('row');
          getCells(row).forEach(cell => {
            expect(cell.getAttribute('role')).toBe('cell');
          });
        });
      });
    }


() => {
        expect(table.dataSource).toBe(dataSource);
        expect(dataSource.isConnected).toBe(true);
        expect(component.contentChangedCount).toBe(1);
      }


() => {
        const header = getHeaderRows(tableElement)[0];

        expect(header).toBeTruthy();
        expect(header.classList).toContain('customHeaderRowClass');
        expect(getHeaderCells(header).length).toBe(component.columnsToRender.length);
      }


() => {
        const rows = getRows(tableElement);

        expect(rows.length).toBe(dataSource.data.length);
        rows.forEach(row => {
          expect(row.classList).toContain('customRowClass');
          expect(getCells(row).length).toBe(component.columnsToRender.length);
        });
      }


row => {
          expect(row.classList).toContain('customRowClass');
          expect(getCells(row).length).toBe(component.columnsToRender.length);
        }


() => {
        const header = getHeaderRows(tableElement)[0];
        getHeaderCells(header).forEach((headerCell, index) => {
          expect(headerCell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
        });

        getRows(tableElement).forEach(row => {
          getCells(row).forEach((cell, index) => {
            expect(cell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
          });
        });
      }


(headerCell, index) => {
          expect(headerCell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
        }


row => {
          getCells(row).forEach((cell, index) => {
            expect(cell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
          });
        }


(cell, index) => {
            expect(cell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
          }


() => {
        expect(tableElement.getAttribute('role')).toBe('table');

        expect(getHeaderRows(tableElement)[0].getAttribute('role')).toBe('row');
        const header = getHeaderRows(tableElement)[0];
        getHeaderCells(header).forEach(cell => {
          expect(cell.getAttribute('role')).toBe('columnheader');
        });

        getRows(tableElement).forEach(row => {
          expect(row.getAttribute('role')).toBe('row');
          getCells(row).forEach(cell => {
            expect(cell.getAttribute('role')).toBe('cell');
          });
        });
      }


cell => {
          expect(cell.getAttribute('role')).toBe('columnheader');
        }


row => {
          expect(row.getAttribute('role')).toBe('row');
          getCells(row).forEach(cell => {
            expect(cell.getAttribute('role')).toBe('cell');
          });
        }


cell => {
            expect(cell.getAttribute('role')).toBe('cell');
          }


() => {
      expect(dataSource.isConnected).toBe(true);

      fixture.destroy();
      expect(dataSource.isConnected).toBe(false);
    }


() => {
      dataSource.addData();
      fixture.detectChanges();

      // Expect it to have emitted once on init, and once again for the data changes.
      expect(component.contentChangedCount).toBe(2);

      expect(getRows(tableElement).length).toBe(dataSource.data.length);

      // Check that the number of cells is correct
      getRows(tableElement).forEach(row => {
        expect(getCells(row).length).toBe(component.columnsToRender.length);
      });
    }


row => {
        expect(getCells(row).length).toBe(component.columnsToRender.length);
      }


() => {
      // Note: we cast the assertions here to booleans, because they may
      // contain circular objects which will throw Jasmine into an infinite
      // when its tries to stringify them to show a test failure.
      expect(!!CdkCellOutlet.mostRecentCellOutlet).toBe(true);

      fixture.destroy();

      expect(!!CdkCellOutlet.mostRecentCellOutlet).toBe(false);
    }


() => {
      function addInitialIndexAttribute() {
        // 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');
      }

      it('when the data is heterogeneous', () => {
        addInitialIndexAttribute();

        // 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();

        // 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('when the data contains multiple occurrences of the same object instance', () => {
        const obj = {value: true};
        component.dataSource!.data = [obj, obj, obj];
        addInitialIndexAttribute();

        const copiedData = component.dataSource!.data.slice();

        // Remove the third element and add a new different obj in the beginning.
        copiedData.splice(2, 1);
        copiedData.unshift({value: false});

        // Add new data
        component.dataSource!.data = copiedData;

        // Expect that two of the three rows still have an initial index. Not as concerned about
        // the order they are in, but more important that there was no unnecessary removes/inserts.
        const changedRows = getRows(tableElement);
        expect(changedRows.length).toBe(3);
        let numInitialRows = 0;
        changedRows.forEach(row => {
          if (row.getAttribute('initialIndex') !== null) {
            numInitialRows++;
          }
        });
        expect(numInitialRows).toBe(2);
      });
    }


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


() => {
        addInitialIndexAttribute();

        // 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();

        // 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);
      }


() => {
        const obj = {value: true};
        component.dataSource!.data = [obj, obj, obj];
        addInitialIndexAttribute();

        const copiedData = component.dataSource!.data.slice();

        // Remove the third element and add a new different obj in the beginning.
        copiedData.splice(2, 1);
        copiedData.unshift({value: false});

        // Add new data
        component.dataSource!.data = copiedData;

        // Expect that two of the three rows still have an initial index. Not as concerned about
        // the order they are in, but more important that there was no unnecessary removes/inserts.
        const changedRows = getRows(tableElement);
        expect(changedRows.length).toBe(3);
        let numInitialRows = 0;
        changedRows.forEach(row => {
          if (row.getAttribute('initialIndex') !== null) {
            numInitialRows++;
          }
        });
        expect(numInitialRows).toBe(2);
      }


row => {
          if (row.getAttribute('initialIndex') !== null) {
            numInitialRows++;
          }
        }


() => {
      const rowOutlet = fixture.componentInstance.table._rowOutlet.viewContainer;
      const headerPlaceholder = fixture.componentInstance.table._headerRowOutlet.viewContainer;

      spyOn(rowOutlet, 'clear').and.callThrough();
      spyOn(headerPlaceholder, 'clear').and.callThrough();

      fixture.destroy();

      expect(rowOutlet.clear).toHaveBeenCalled();
      expect(headerPlaceholder.clear).toHaveBeenCalled();
    }


() => {
      const initialDataLength = dataSource.data.length;
      expect(dataSource.data.length).toBe(3);

      let data = dataSource.data;
      expectTableToMatchContent(tableElement, [
        ['Column A', 'Column B', 'Column C'],
        [data[0].a, data[0].b, data[0].c],
        [data[1].a, data[1].b, data[1].c],
        [data[2].a, data[2].b, data[2].c],
        ['Footer A', 'Footer B', 'Footer C'],
      ]);

      // Add data to the table and recreate what the rendered output should be.
      dataSource.addData();
      expect(dataSource.data.length).toBe(initialDataLength + 1); // Make sure data was added
      fixture.detectChanges();

      data = dataSource.data;
      expectTableToMatchContent(tableElement, [
        ['Column A', 'Column B', 'Column C'],
        [data[0].a, data[0].b, data[0].c],
        [data[1].a, data[1].b, data[1].c],
        [data[2].a, data[2].b, data[2].c],
        [data[3].a, data[3].b, data[3].c],
        ['Footer A', 'Footer B', 'Footer C'],
      ]);
    }


() => {
      expect(dataSource.data.length).toBe(3);

      let data = dataSource.data;
      expectTableToMatchContent(tableElement, [
        ['Column A', 'Column B', 'Column C'],
        [data[0].a, data[0].b, data[0].c],
        [data[1].a, data[1].b, data[1].c],
        [data[2].a, data[2].b, data[2].c],
        ['Footer A', 'Footer B', 'Footer C'],
      ]);

      // Remove column_a and swap column_b/column_c.
      component.columnsToRender = ['column_c', 'column_b'];
      fixture.detectChanges();

      let changedTableContent = [['Column C', 'Column B']];
      dataSource.data.forEach(rowData => changedTableContent.push([rowData.c, rowData.b]));

      data = dataSource.data;
      expectTableToMatchContent(tableElement, [
        ['Column C', 'Column B'],
        [data[0].c, data[0].b],
        [data[1].c, data[1].b],
        [data[2].c, data[2].b],
        ['Footer C', 'Footer B'],
      ]);
    }


rowData => changedTableContent.push([rowData.c, rowData.b])


() => {
      expect(tableElement.textContent!.trim()).not.toContain('No data');

      const originalData = dataSource.data;
      dataSource.data = [];
      fixture.detectChanges();

      expect(tableElement.textContent!.trim()).toContain('No data');

      dataSource.data = originalData;
      fixture.detectChanges();

      // Expect it to have emitted once on init, once when empty, and again with original data.
      expect(component.contentChangedCount).toBe(3);

      expect(tableElement.textContent!.trim()).not.toContain('No data');
    }


() => {
      fixture.destroy();
      fixture = TestBed.createComponent(SimpleCdkTableApp);
      fixture.componentInstance.dataSource.data = [];
      fixture.detectChanges();
      tableElement = fixture.nativeElement.querySelector('.cdk-table');
      expect(tableElement.textContent!.trim()).toContain('No data');
      expect(component.contentChangedCount).toBe(1);
    }


() => {
    setupTableTestApp(NullDataCdkTableApp);
    fixture.detectChanges();

    expect(getRows(tableElement).length).toBe(0);

    // Emits that the data rows are changed even when the result is empty.
    expect(component.contentChangedCount).toBe(1);
  }


() => {
    setupTableTestApp(MultipleHeaderFooterRowsCdkTableApp);
    fixture.detectChanges();

    expectTableToMatchContent(tableElement, [
      ['first-header'],
      ['second-header'],
      ['first-footer'],
      ['second-footer'],
    ]);
  }


() => {
    setupTableTestApp(NativeHtmlTableWithCaptionApp);
    fixture.detectChanges();

    const caption = tableElement.querySelector('caption');

    expect(caption).toBeTruthy();
    expect(tableElement.firstElementChild).toBe(caption);
  }


() => {
    setupTableTestApp(NativeHtmlTableWithColgroupAndCol);
    fixture.detectChanges();

    const colgroupsAndCols = Array.from(tableElement.querySelectorAll('colgroup, col'));

    expect(colgroupsAndCols.length).toBe(3);
    expect(colgroupsAndCols[0].childNodes[0]).toBe(colgroupsAndCols[1]);
    expect(colgroupsAndCols[2].parentNode!.nodeName.toLowerCase()).toBe('table');
    expect(colgroupsAndCols.map(e => e.nodeName.toLowerCase())).toEqual([
      'colgroup',
      'col',
      'col',
    ]);
  }


e => e.nodeName.toLowerCase()


() => {
    let baseData: TestData[] = [
      {a: 'a_1', b: 'b_1', c: 'c_1'},
      {a: 'a_2', b: 'b_2', c: 'c_2'},
      {a: 'a_3', b: 'b_3', c: 'c_3'},
    ];

    beforeEach(() => {
      setupTableTestApp(CdkTableWithDifferentDataInputsApp);
    });

    it('should render with data array input', () => {
      const data = baseData.slice();
      component.dataSource = data;
      fixture.detectChanges();

      const expectedRender = [
        ['Column A', 'Column B', 'Column C'],
        ['a_1', 'b_1', 'c_1'],
        ['a_2', 'b_2', 'c_2'],
        ['a_3', 'b_3', 'c_3'],
      ];
      expectTableToMatchContent(tableElement, expectedRender);

      // Push data to the array but neglect to tell the table, should be no change
      data.push({a: 'a_4', b: 'b_4', c: 'c_4'});

      expectTableToMatchContent(tableElement, expectedRender);

      // Notify table of the change, expect another row
      component.table.renderRows();
      fixture.detectChanges();

      expectedRender.push(['a_4', 'b_4', 'c_4']);
      expectTableToMatchContent(tableElement, expectedRender);

      // Remove a row and expect the change in rows
      data.pop();
      component.table.renderRows();

      expectedRender.pop();
      expectTableToMatchContent(tableElement, expectedRender);

      // Remove the data input entirely and expect no rows - just header.
      component.dataSource = null;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, [expectedRender[0]]);

      // Add back the data to verify that it renders rows
      component.dataSource = data;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, expectedRender);
    });

    it('should render with data stream input', () => {
      const data = baseData.slice();
      const stream = new BehaviorSubject<TestData[]>(data);
      component.dataSource = stream;
      fixture.detectChanges();

      const expectedRender = [
        ['Column A', 'Column B', 'Column C'],
        ['a_1', 'b_1', 'c_1'],
        ['a_2', 'b_2', 'c_2'],
        ['a_3', 'b_3', 'c_3'],
      ];
      expectTableToMatchContent(tableElement, expectedRender);

      // Push data to the array and emit the data array on the stream
      data.push({a: 'a_4', b: 'b_4', c: 'c_4'});
      stream.next(data);
      fixture.detectChanges();

      expectedRender.push(['a_4', 'b_4', 'c_4']);
      expectTableToMatchContent(tableElement, expectedRender);

      // Push data to the array but rather than emitting, call renderRows.
      data.push({a: 'a_5', b: 'b_5', c: 'c_5'});
      component.table.renderRows();
      fixture.detectChanges();

      expectedRender.push(['a_5', 'b_5', 'c_5']);
      expectTableToMatchContent(tableElement, expectedRender);

      // Remove a row and expect the change in rows
      data.pop();
      expectedRender.pop();
      stream.next(data);

      expectTableToMatchContent(tableElement, expectedRender);

      // Remove the data input entirely and expect no rows - just header.
      component.dataSource = null;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, [expectedRender[0]]);

      // Add back the data to verify that it renders rows
      component.dataSource = stream;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, expectedRender);
    });

    it('should throw an error if the data source is not valid', () => {
      component.dataSource = {invalid: 'dataSource'};

      expect(() => fixture.detectChanges()).toThrowError(getTableUnknownDataSourceError().message);
    });

    it('should throw an error if the data source is not valid', () => {
      component.dataSource = undefined;
      fixture.detectChanges();

      // Expect the table to render just the header, no rows
      expectTableToMatchContent(tableElement, [
        ['Column A', 'Column B', 'Column C']
      ]);
    });
  }


() => {
      setupTableTestApp(CdkTableWithDifferentDataInputsApp);
    }


() => {
      const data = baseData.slice();
      component.dataSource = data;
      fixture.detectChanges();

      const expectedRender = [
        ['Column A', 'Column B', 'Column C'],
        ['a_1', 'b_1', 'c_1'],
        ['a_2', 'b_2', 'c_2'],
        ['a_3', 'b_3', 'c_3'],
      ];
      expectTableToMatchContent(tableElement, expectedRender);

      // Push data to the array but neglect to tell the table, should be no change
      data.push({a: 'a_4', b: 'b_4', c: 'c_4'});

      expectTableToMatchContent(tableElement, expectedRender);

      // Notify table of the change, expect another row
      component.table.renderRows();
      fixture.detectChanges();

      expectedRender.push(['a_4', 'b_4', 'c_4']);
      expectTableToMatchContent(tableElement, expectedRender);

      // Remove a row and expect the change in rows
      data.pop();
      component.table.renderRows();

      expectedRender.pop();
      expectTableToMatchContent(tableElement, expectedRender);

      // Remove the data input entirely and expect no rows - just header.
      component.dataSource = null;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, [expectedRender[0]]);

      // Add back the data to verify that it renders rows
      component.dataSource = data;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, expectedRender);
    }


() => {
      const data = baseData.slice();
      const stream = new BehaviorSubject<TestData[]>(data);
      component.dataSource = stream;
      fixture.detectChanges();

      const expectedRender = [
        ['Column A', 'Column B', 'Column C'],
        ['a_1', 'b_1', 'c_1'],
        ['a_2', 'b_2', 'c_2'],
        ['a_3', 'b_3', 'c_3'],
      ];
      expectTableToMatchContent(tableElement, expectedRender);

      // Push data to the array and emit the data array on the stream
      data.push({a: 'a_4', b: 'b_4', c: 'c_4'});
      stream.next(data);
      fixture.detectChanges();

      expectedRender.push(['a_4', 'b_4', 'c_4']);
      expectTableToMatchContent(tableElement, expectedRender);

      // Push data to the array but rather than emitting, call renderRows.
      data.push({a: 'a_5', b: 'b_5', c: 'c_5'});
      component.table.renderRows();
      fixture.detectChanges();

      expectedRender.push(['a_5', 'b_5', 'c_5']);
      expectTableToMatchContent(tableElement, expectedRender);

      // Remove a row and expect the change in rows
      data.pop();
      expectedRender.pop();
      stream.next(data);

      expectTableToMatchContent(tableElement, expectedRender);

      // Remove the data input entirely and expect no rows - just header.
      component.dataSource = null;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, [expectedRender[0]]);

      // Add back the data to verify that it renders rows
      component.dataSource = stream;
      fixture.detectChanges();

      expectTableToMatchContent(tableElement, expectedRender);
    }


() => {
      component.dataSource = {invalid: 'dataSource'};

      expect(() => fixture.detectChanges()).toThrowError(getTableUnknownDataSourceError().message);
    }


() => {
      component.dataSource = undefined;
      fixture.detectChanges();

      // Expect the table to render just the header, no rows
      expectTableToMatchContent(tableElement, [
        ['Column A', 'Column B', 'Column C']
      ]);
    }


() => {
    it('should be able to render without a header row def', () => {
      setupTableTestApp(MissingHeaderRowDefCdkTableApp);
      expectTableToMatchContent(tableElement, [
        ['a_1'],  // Data rows
        ['a_2'],
        ['a_3'],
        ['Footer A'],  // Footer row
      ]);
    });

    it('should be able to render without a data row def', () => {
      setupTableTestApp(MissingRowDefCdkTableApp);
      expectTableToMatchContent(tableElement, [
        ['Column A'],  // Header row
        ['Footer A'],  // Footer row
      ]);
    });

    it('should be able to render without a footer row def', () => {
      setupTableTestApp(MissingFooterRowDefCdkTableApp);
      expectTableToMatchContent(tableElement, [
        ['Column A'],  // Header row
        ['a_1'],  // Data rows
        ['a_2'],
        ['a_3'],
      ]);
    });
  }


() => {
      setupTableTestApp(MissingHeaderRowDefCdkTableApp);
      expectTableToMatchContent(tableElement, [
        ['a_1'],  // Data rows
        ['a_2'],
        ['a_3'],
        ['Footer A'],  // Footer row
      ]);
    }


() => {
      setupTableTestApp(MissingRowDefCdkTableApp);
      expectTableToMatchContent(tableElement, [
        ['Column A'],  // Header row
        ['Footer A'],  // Footer row
      ]);
    }


() => {
      setupTableTestApp(MissingFooterRowDefCdkTableApp);
      expectTableToMatchContent(tableElement, [
        ['Column A'],  // Header row
        ['a_1'],  // Data rows
        ['a_2'],
        ['a_3'],
      ]);
    }


() => {
    const thisFixture = createComponent(NativeHtmlTableApp);
    const thisTableElement = thisFixture.nativeElement.querySelector('table');
    thisFixture.detectChanges();

    expectTableToMatchContent(thisTableElement, [
      ['Column A', 'Column B', 'Column C'],
      ['a_1', 'b_1', 'c_1'],
      ['a_2', 'b_2', 'c_2'],
      ['a_3', 'b_3', 'c_3'],
    ]);
  }


() => {
    const thisFixture = createComponent(NestedHtmlTableApp);
    thisFixture.detectChanges();
    const outerTable = thisFixture.nativeElement.querySelector('table');
    const innerTable = outerTable.querySelector('table');
    const outerRows = Array.from<HTMLTableRowElement>(outerTable.querySelector('tbody').rows);
    const innerRows = Array.from<HTMLTableRowElement>(innerTable.querySelector('tbody').rows);

    expect(outerTable).toBeTruthy();
    expect(outerRows.map(row => row.cells.length)).toEqual([3, 3, 3]);

    expect(innerTable).toBeTruthy();
    expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3]);
  }


row => row.cells.length


() => {
    const thisFixture = createComponent(NativeHtmlTableApp);
    thisFixture.detectChanges();

    // Assert that the data is inside the tbody specifically.
    const tbody = thisFixture.nativeElement.querySelector('tbody');
    const dataSource = thisFixture.componentInstance.dataSource!;
    const originalData = dataSource.data;

    expect(tbody.textContent!.trim()).not.toContain('No data');

    dataSource.data = [];
    thisFixture.detectChanges();

    expect(tbody.textContent!.trim()).toContain('No data');

    dataSource.data = originalData;
    thisFixture.detectChanges();

    expect(tbody.textContent!.trim()).not.toContain('No data');
  }


() => {
    const thisFixture = createComponent(NativeHtmlTableApp);
    const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table');
    thisFixture.detectChanges();

    const rowGroups = Array.from(thisTableElement.querySelectorAll('thead, tbody, tfoot'));
    expect(rowGroups.length)
      .withContext('Expected table to have a thead, tbody, and tfoot').toBe(3);
    for (const group of rowGroups) {
      expect(group.getAttribute('role'))
        .withContext('Expected thead, tbody, and tfoot to have role="rowgroup"').toBe('rowgroup');
    }
  }


() => {
    const thisFixture = createComponent(NativeTableWithNoHeaderOrFooterRows);
    const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table');
    thisFixture.detectChanges();

    const rowGroups: HTMLElement[] = Array.from(thisTableElement.querySelectorAll('thead, tfoot'));
    expect(rowGroups.length)
      .withContext('Expected table to have a thead and tfoot').toBe(2);
    for (const group of rowGroups) {
      expect(group.style.display)
        .withContext('Expected thead and tfoot to be `display: none`').toBe('none');
    }
  }


() => {
    setupTableTestApp(BooleanRowCdkTableApp);
    expectTableToMatchContent(tableElement, [
      [''], // Header row
      ['false'], // Data rows
      ['true'],
      ['false'],
      ['true'],
    ]);
  }


() => {
    setupTableTestApp(CrazyColumnNameCdkTableApp);
    // Column was named 'crazy-column-NAME-1!@#$%^-_&*()2'
    const header = getHeaderRows(tableElement)[0];
    expect(getHeaderCells(header)[0].classList)
        .toContain('cdk-column-crazy-column-NAME-1-------_----2');
  }


() => {
    setupTableTestApp(CustomRoleCdkTableApp);
    expect(tableElement.getAttribute('role')).toBe('treegrid');

    expect(getHeaderRows(tableElement)[0].getAttribute('role')).toBe('row');
    const header = getHeaderRows(tableElement)[0];
    getHeaderCells(header).forEach(cell => {
      expect(cell.getAttribute('role')).toBe('columnheader');
    });

    getRows(tableElement).forEach(row => {
      expect(row.getAttribute('role')).toBe('row');
      getCells(row).forEach(cell => {
        expect(cell.getAttribute('role')).toBe('gridcell');
      });
    });
  }


cell => {
      expect(cell.getAttribute('role')).toBe('columnheader');
    }


row => {
      expect(row.getAttribute('role')).toBe('row');
      getCells(row).forEach(cell => {
        expect(cell.getAttribute('role')).toBe('gridcell');
      });
    }


cell => {
        expect(cell.getAttribute('role')).toBe('gridcell');
      }


() => {
    expect(() => createComponent(DuplicateColumnDefNameCdkTableApp).detectChanges())
        .toThrowError(getTableDuplicateColumnNameError('column_a').message);
  }


() => createComponent(DuplicateColumnDefNameCdkTableApp).detectChanges()


() => {
    expect(() => createComponent(MissingColumnDefCdkTableApp).detectChanges())
        .toThrowError(getTableUnknownColumnError('column_a').message);
  }


() => createComponent(MissingColumnDefCdkTableApp).detectChanges()


() => {
    expect(() => createComponent(TableWithIndirectDescendantDefs).detectChanges()).not.toThrow();
  }


() => createComponent(TableWithIndirectDescendantDefs).detectChanges()


() => {
       const columnDefinitionMissingAfterRenderFixture =
           createComponent(MissingColumnDefAfterRenderCdkTableApp);
       expect(() => {
         columnDefinitionMissingAfterRenderFixture.detectChanges();
         flush();
         columnDefinitionMissingAfterRenderFixture.detectChanges();
       }).toThrowError(getTableUnknownColumnError('column_a').message);
     }


() => {
         columnDefinitionMissingAfterRenderFixture.detectChanges();
         flush();
         columnDefinitionMissingAfterRenderFixture.detectChanges();
       }


() => {
    expect(() => createComponent(MissingAllRowDefsCdkTableApp).detectChanges())
        .toThrowError(getTableMissingRowDefsError().message);
  }


() => createComponent(MissingAllRowDefsCdkTableApp).detectChanges()


() => {
    setupTableTestApp(UndefinedColumnsCdkTableApp);

    // Header should be empty since there are no columns to display.
    const headerRow = getHeaderRows(tableElement)[0];
    expect(headerRow.textContent).toBe('');

    // Rows should be empty since there are no columns to display.
    const rows = getRows(tableElement);
    expect(rows[0].textContent).toBe('');
    expect(rows[1].textContent).toBe('');
    expect(rows[2].textContent).toBe('');
  }


() => {
    setupTableTestApp(DynamicColumnDefinitionsCdkTableApp);

    // Add a new column and expect it to show up in the table
    let columnA = 'columnA';
    component.dynamicColumns.push(columnA);
    fixture.detectChanges();
    expectTableToMatchContent(tableElement, [
      [columnA], // Header row
      [columnA], // Data rows
      [columnA],
      [columnA],
    ]);

    // Add another new column and expect it to show up in the table
    let columnB = 'columnB';
    component.dynamicColumns.push(columnB);
    fixture.detectChanges();
    expectTableToMatchContent(tableElement, [
      [columnA, columnB], // Header row
      [columnA, columnB], // Data rows
      [columnA, columnB],
      [columnA, columnB],
    ]);

    // Remove column A expect only column B to be rendered
    component.dynamicColumns.shift();
    fixture.detectChanges();
    expectTableToMatchContent(tableElement, [
      [columnB], // Header row
      [columnB], // Data rows
      [columnB],
      [columnB],
    ]);
  }


() => {
    setupTableTestApp(OuterTableApp, [WrapperCdkTableApp]);

    // The first two columns were defined in the wrapped table component as content children,
    // while the injected columns were provided to the wrapped table from the outer component.
    // A special row was provided with a when predicate that shows the single column with text.
    // The header row was defined by the outer component.
    expectTableToMatchContent(tableElement, [
      ['Content Column A', 'Content Column B', 'Injected Column A', 'Injected Column B'],
      ['injected row with when predicate'],
      ['a_2', 'b_2', 'a_2', 'b_2'],
      ['a_3', 'b_3', 'a_3', 'b_3']
    ]);
  }


() => {
    setupTableTestApp(OuterTableApp, [WrapperCdkTableApp]);

    fixture.componentInstance.dataSource.data = [];
    fixture.detectChanges();

    expect(tableElement.textContent).toContain('No data');
  }


() => {
    it('should be able to display different row templates based on the row data', () => {
      setupTableTestApp(WhenRowCdkTableApp);
      let data = component.dataSource.data;
      expectTableToMatchContent(tableElement, [
        ['Column A', 'Column B', 'Column C'],
        [data[0].a, data[0].b, data[0].c],
        ['index_1_special_row'],
        ['c3_special_row'],
        [data[3].a, data[3].b, data[3].c],
      ]);
    });

    it('should error if there is row data that does not have a matching row template',
       fakeAsync(() => {
         const whenRowWithoutDefaultFixture = createComponent(WhenRowWithoutDefaultCdkTableApp);
         const data = whenRowWithoutDefaultFixture.componentInstance.dataSource.data;
         expect(() => {
           try {
             whenRowWithoutDefaultFixture.detectChanges();
             flush();
           } catch {
             flush();
           }
         }).toThrowError(getTableMissingMatchingRowDefError(data[0]).message);
       }));

    it('should fail when multiple rows match data without multiTemplateDataRows', fakeAsync(() => {
      let whenFixture = createComponent(WhenRowMultipleDefaultsCdkTableApp);
      expect(() => {
        whenFixture.detectChanges();
        flush();
      }).toThrowError(getTableMultipleDefaultRowDefsError().message);
    }));

    describe('with multiTemplateDataRows', () => {
      it('should be able to render multiple rows per data object', () => {
        setupTableTestApp(WhenRowCdkTableApp);
        component.multiTemplateDataRows = true;
        fixture.detectChanges();

        const data = component.dataSource.data;
        expectTableToMatchContent(tableElement, [
          ['Column A', 'Column B', 'Column C'],
          [data[0].a, data[0].b, data[0].c],
          [data[1].a, data[1].b, data[1].c],
          ['index_1_special_row'],
          [data[2].a, data[2].b, data[2].c],
          ['c3_special_row'],
          [data[3].a, data[3].b, data[3].c],
        ]);
      });

      it('should have the correct data and row indicies', () => {
        setupTableTestApp(WhenRowCdkTableApp);
        component.multiTemplateDataRows = true;
        component.showIndexColumns();
        fixture.detectChanges();

        expectTableToMatchContent(tableElement, [
          ['Index', 'Data Index', 'Render Index'],
          ['', '0', '0'],
          ['', '1', '1'],
          ['', '1', '2'],
          ['', '2', '3'],
          ['', '2', '4'],
          ['', '3', '5'],
        ]);
      });

      it('should have the correct data and row indicies when data contains multiple instances of ' +
             'the same object instance',
         () => {
           setupTableTestApp(WhenRowCdkTableApp);
           component.multiTemplateDataRows = true;
           component.showIndexColumns();

           const obj = {value: true};
           component.dataSource.data = [obj, obj, obj, obj];
           fixture.detectChanges();

           expectTableToMatchContent(tableElement, [
             ['Index', 'Data Index', 'Render Index'],
             ['', '0', '0'],
             ['', '1', '1'],
             ['', '1', '2'],
             ['', '2', '3'],
             ['', '3', '4'],
           ]);

           // Push unique data on the front and add another obj to the array
           component.dataSource.data = [{value: false}, obj, obj, obj, obj, obj];
           fixture.detectChanges();

           expectTableToMatchContent(tableElement, [
             ['Index', 'Data Index', 'Render Index'],
             ['', '0', '0'],
             ['', '1', '1'],
             ['', '1', '2'],
             ['', '2', '3'],
             ['', '3', '4'],
             ['', '4', '5'],
             ['', '5', '6'],
           ]);
         });

      it('should not throw when multiTemplateDataRows is coerced from a static value', () => {
        expect(() => {
          setupTableTestApp(CoercedMultiTemplateDataRows);
          fixture.detectChanges();
        }).not.toThrow();
      });

    });
  }


() => {
      setupTableTestApp(WhenRowCdkTableApp);
      let data = component.dataSource.data;
      expectTableToMatchContent(tableElement, [
        ['Column A', 'Column B', 'Column C'],
        [data[0].a, data[0].b, data[0].c],
        ['index_1_special_row'],
        ['c3_special_row'],
        [data[3].a, data[3].b, data[3].c],
      ]);
    }


() => {
         const whenRowWithoutDefaultFixture = createComponent(WhenRowWithoutDefaultCdkTableApp);
         const data = whenRowWithoutDefaultFixture.componentInstance.dataSource.data;
         expect(() => {
           try {
             whenRowWithoutDefaultFixture.detectChanges();
             flush();
           } catch {
             flush();
           }
         }).toThrowError(getTableMissingMatchingRowDefError(data[0]).message);
       }


() => {
           try {
             whenRowWithoutDefaultFixture.detectChanges();
             flush();
           } catch {
             flush();
           }
         }


() => {
      let whenFixture = createComponent(WhenRowMultipleDefaultsCdkTableApp);
      expect(() => {
        whenFixture.detectChanges();
        flush();
      }).toThrowError(getTableMultipleDefaultRowDefsError().message);
    }


() => {
        whenFixture.detectChanges();
        flush();
      }


() => {
      it('should be able to render multiple rows per data object', () => {
        setupTableTestApp(WhenRowCdkTableApp);
        component.multiTemplateDataRows = true;
        fixture.detectChanges();

        const data = component.dataSource.data;
        expectTableToMatchContent(tableElement, [
          ['Column A', 'Column B', 'Column C'],
          [data[0].a, data[0].b, data[0].c],
          [data[1].a, data[1].b, data[1].c],
          ['index_1_special_row'],
          [data[2].a, data[2].b, data[2].c],
          ['c3_special_row'],
          [data[3].a, data[3].b, data[3].c],
        ]);
      });

      it('should have the correct data and row indicies', () => {
        setupTableTestApp(WhenRowCdkTableApp);
        component.multiTemplateDataRows = true;
        component.showIndexColumns();
        fixture.detectChanges();

        expectTableToMatchContent(tableElement, [
          ['Index', 'Data Index', 'Render Index'],
          ['', '0', '0'],
          ['', '1', '1'],
          ['', '1', '2'],
          ['', '2', '3'],
          ['', '2', '4'],
          ['', '3', '5'],
        ]);
      });

      it('should have the correct data and row indicies when data contains multiple instances of ' +
             'the same object instance',
         () => {
           setupTableTestApp(WhenRowCdkTableApp);
           component.multiTemplateDataRows = true;
           component.showIndexColumns();

           const obj = {value: true};
           component.dataSource.data = [obj, obj, obj, obj];
           fixture.detectChanges();

           expectTableToMatchContent(tableElement, [
             ['Index', 'Data Index', 'Render Index'],
             ['', '0', '0'],
             ['', '1', '1'],
             ['', '1', '2'],
             ['', '2', '3'],
             ['', '3', '4'],
           ]);

           // Push unique data on the front and add another obj to the array
           component.dataSource.data = [{value: false}, obj, obj, obj, obj, obj];
           fixture.detectChanges();

           expectTableToMatchContent(tableElement, [
             ['Index', 'Data Index', 'Render Index'],
             ['', '0', '0'],
             ['', '1', '1'],
             ['', '1', '2'],
             ['', '2', '3'],
             ['', '3', '4'],
             ['', '4', '5'],
             ['', '5', '6'],
           ]);
         });

      it('should not throw when multiTemplateDataRows is coerced from a static value', () => {
        expect(() => {
          setupTableTestApp(CoercedMultiTemplateDataRows);
          fixture.detectChanges();
        }).not.toThrow();
      });

    }


() => {
        setupTableTestApp(WhenRowCdkTableApp);
        component.multiTemplateDataRows = true;
        fixture.detectChanges();

        const data = component.dataSource.data;
        expectTableToMatchContent(tableElement, [
          ['Column A', 'Column B', 'Column C'],
          [data[0].a, data[0].b, data[0].c],
          [data[1].a, data[1].b, data[1].c],
          ['index_1_special_row'],
          [data[2].a, data[2].b, data[2].c],
          ['c3_special_row'],
          [data[3].a, data[3].b, data[3].c],
        ]);
      }


() => {
        setupTableTestApp(WhenRowCdkTableApp);
        component.multiTemplateDataRows = true;
        component.showIndexColumns();
        fixture.detectChanges();

        expectTableToMatchContent(tableElement, [
          ['Index', 'Data Index', 'Render Index'],
          ['', '0', '0'],
          ['', '1', '1'],
          ['', '1', '2'],
          ['', '2', '3'],
          ['', '2', '4'],
          ['', '3', '5'],
        ]);
      }


() => {
           setupTableTestApp(WhenRowCdkTableApp);
           component.multiTemplateDataRows = true;
           component.showIndexColumns();

           const obj = {value: true};
           component.dataSource.data = [obj, obj, obj, obj];
           fixture.detectChanges();

           expectTableToMatchContent(tableElement, [
             ['Index', 'Data Index', 'Render Index'],
             ['', '0', '0'],
             ['', '1', '1'],
             ['', '1', '2'],
             ['', '2', '3'],
             ['', '3', '4'],
           ]);

           // Push unique data on the front and add another obj to the array
           component.dataSource.data = [{value: false}, obj, obj, obj, obj, obj];
           fixture.detectChanges();

           expectTableToMatchContent(tableElement, [
             ['Index', 'Data Index', 'Render Index'],
             ['', '0', '0'],
             ['', '1', '1'],
             ['', '1', '2'],
             ['', '2', '3'],
             ['', '3', '4'],
             ['', '4', '5'],
             ['', '5', '6'],
           ]);
         }


() => {
        expect(() => {
          setupTableTestApp(CoercedMultiTemplateDataRows);
          fixture.detectChanges();
        }).not.toThrow();
      }


() => {
          setupTableTestApp(CoercedMultiTemplateDataRows);
          fixture.detectChanges();
        }


element => {
        expect(element.classList.contains('cdk-table-sticky')).toBe(false);
        expectStickyBorderClass(element);
        expect(element.style.position).toBe('');
        expect(element.style.zIndex || '0').toBe('0');
        ['top', 'bottom', 'left', 'right'].forEach(d => {
          expect(element.style[d] || 'unset')
            .withContext(`Expected ${d} to be unset`).toBe('unset');
        });
      }


d => {
          expect(element.style[d] || 'unset')
            .withContext(`Expected ${d} to be unset`).toBe('unset');
        }


d => {
        const directionValue = directions[d];

        if (!directionValue) {
          // If no expected position for this direction, must either be unset or empty string
          // If no expected position for this direction, must either be unset or empty string
          expect(element.style[d] || 'unset')
            .withContext(`Expected ${d} to be unset`).toBe('unset');
          return;
        }

        const expectationMessage = `Expected direction ${d} to be ${directionValue}`;

        // If the direction contains `px`, we parse the number to be able to avoid deviations
        // caused by individual browsers.
        if (directionValue.includes('px')) {
          expect(Math.round(parseInt(element.style[d])))
            .withContext(expectationMessage).toBe(Math.round(parseInt(directionValue)));
        } else {
          expect(element.style[d])
            .withContext(expectationMessage).toBe(directionValue);
        }
      }


d => {
        const stickyClass = `cdk-table-sticky-border-elem-${d}`;

        if (directions[d]) {
          expect(element.classList).toContain(stickyClass);
        } else {
          expect(element.classList).not.toContain(stickyClass);
        }
      }


() => {
        setupTableTestApp(StickyFlexLayoutCdkTableApp);

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


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

        expectStickyStyles(headerRows[0], '100', {top: '0px'});
        expectStickyBorderClass(headerRows[0]);
        expectNoStickyStyles([headerRows[1]]);
        expectStickyStyles(
            headerRows[2], '100', {top: headerRows[0].getBoundingClientRect().height + 'px'});
        expectStickyBorderClass(headerRows[2], {top: true});
        expect(component.mostRecentStickyHeaderRowsUpdate).toEqual({
          sizes: [
            headerRows[0].getBoundingClientRect().height,
            undefined,
            headerRows[2].getBoundingClientRect().height,
          ],
          offsets: [
            0,
            undefined,
            headerRows[0].getBoundingClientRect().height,
          ],
          elements: [[headerRows[0]], undefined, [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);
        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.stickyFooters = ['footer-1', 'footer-3'];
        fixture.detectChanges();
        flushMicrotasks();

        expectStickyStyles(
            footerRows[0], '10', {bottom: footerRows[1].getBoundingClientRect().height + 'px'});
        expectStickyBorderClass(footerRows[0], {bottom: true});
        expectNoStickyStyles([footerRows[1]]);
        expectStickyStyles(footerRows[2], '10', {bottom: '0px'});
        expectStickyBorderClass(footerRows[2]);
        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[0].getBoundingClientRect().height],
          elements: [[footerRows[2]], undefined, [footerRows[0]]],
        });
        expect(component.mostRecentStickyColumnsUpdate).toEqual({sizes: []});
        expect(component.mostRecentStickyEndColumnsUpdate).toEqual({sizes: []});

        component.stickyFooters = [];
        fixture.detectChanges();
        flushMicrotasks();
        expectNoStickyStyles(footerRows);
        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.stickyFooters = ['footer-3'];
        fixture.detectChanges();
        flushMicrotasks();

        expectStickyStyles(footerRows[2], '10', {bottom: '0px'});
        expectStickyBorderClass(footerRows[2], {bottom: true});
        expectNoStickyStyles([footerRows[0], footerRows[1]]);
      }


() => {
        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: []});
      }


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]]);
        }


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]]);
        }


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]]);
        }


row => expectNoStickyStyles(getHeaderCells(row))


row => expectNoStickyStyles(getCells(row))


row => expectNoStickyStyles(getFooterCells(row))