From 63653bb341e532a802001b616d78685534924197 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 23 Nov 2019 16:52:20 +0100 Subject: [PATCH 1/2] feat(table): add test harness Adds a test harness for `MatTable`, as well as the related row and cell directives. --- src/material/config.bzl | 1 + src/material/table/testing/BUILD.bazel | 49 ++++ src/material/table/testing/cell-harness.ts | 74 ++++++ src/material/table/testing/index.ts | 9 + src/material/table/testing/public-api.ts | 12 + src/material/table/testing/row-harness.ts | 92 +++++++ src/material/table/testing/shared.spec.ts | 240 ++++++++++++++++++ .../table/testing/table-harness-filters.ts | 18 ++ .../table/testing/table-harness.spec.ts | 7 + src/material/table/testing/table-harness.ts | 94 +++++++ test/karma-system-config.js | 2 + .../material/table/testing.d.ts | 75 ++++++ 12 files changed, 673 insertions(+) create mode 100644 src/material/table/testing/BUILD.bazel create mode 100644 src/material/table/testing/cell-harness.ts create mode 100644 src/material/table/testing/index.ts create mode 100644 src/material/table/testing/public-api.ts create mode 100644 src/material/table/testing/row-harness.ts create mode 100644 src/material/table/testing/shared.spec.ts create mode 100644 src/material/table/testing/table-harness-filters.ts create mode 100644 src/material/table/testing/table-harness.spec.ts create mode 100644 src/material/table/testing/table-harness.ts create mode 100644 tools/public_api_guard/material/table/testing.d.ts diff --git a/src/material/config.bzl b/src/material/config.bzl index d266e25bf9a7..600eed672c95 100644 --- a/src/material/config.bzl +++ b/src/material/config.bzl @@ -49,6 +49,7 @@ entryPoints = [ "sort/testing", "stepper", "table", + "table/testing", "tabs", "tabs/testing", "toolbar", diff --git a/src/material/table/testing/BUILD.bazel b/src/material/table/testing/BUILD.bazel new file mode 100644 index 000000000000..07d382859971 --- /dev/null +++ b/src/material/table/testing/BUILD.bazel @@ -0,0 +1,49 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library") + +ts_library( + name = "testing", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + module_name = "@angular/material/table/testing", + deps = [ + "//src/cdk/testing", + ], +) + +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), +) + +ng_test_library( + name = "harness_tests_lib", + srcs = ["shared.spec.ts"], + deps = [ + ":testing", + "//src/cdk/testing", + "//src/cdk/testing/testbed", + "//src/material/table", + ], +) + +ng_test_library( + name = "unit_tests_lib", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["shared.spec.ts"], + ), + deps = [ + ":harness_tests_lib", + ":testing", + "//src/material/table", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_tests_lib"], +) diff --git a/src/material/table/testing/cell-harness.ts b/src/material/table/testing/cell-harness.ts new file mode 100644 index 000000000000..96dd78927213 --- /dev/null +++ b/src/material/table/testing/cell-harness.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {CellHarnessFilters} from './table-harness-filters'; + +/** Harness for interacting with a standard Angular Material table cell. */ +export class MatCellHarness extends ComponentHarness { + static hostSelector = '.mat-cell'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a cell with specific attributes. + */ + static with(options: CellHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatCellHarness, options) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + } + + /** Gets a promise for the cell's text. */ + async getText(): Promise { + return (await this.host()).text(); + } + + /** Gets the name of the column. */ + async getColumnName(): Promise { + const host = await this.host(); + const classAttribute = await host.getAttribute('class'); + + if (classAttribute) { + const prefix = 'mat-column-'; + const name = classAttribute.split(' ').map(c => c.trim()).find(c => c.startsWith(prefix)); + + if (name) { + return name.split(prefix)[1]; + } + } + + throw Error('Could not determine column name of cell.'); + } +} + +/** Harness for interacting with a standard Angular Material table header cell. */ +export class MatHeaderCellHarness extends MatCellHarness { + static hostSelector = '.mat-header-cell'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a header cell with specific attributes + */ + static with(options: CellHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatHeaderCellHarness, options) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + } +} + +/** Harness for interacting with a standard Angular Material table footer cell. */ +export class MatFooterCellHarness extends MatCellHarness { + static hostSelector = '.mat-footer-cell'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a footer cell with specific attributes + */ + static with(options: CellHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatFooterCellHarness, options) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + } +} diff --git a/src/material/table/testing/index.ts b/src/material/table/testing/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material/table/testing/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './public-api'; diff --git a/src/material/table/testing/public-api.ts b/src/material/table/testing/public-api.ts new file mode 100644 index 000000000000..1a5ecd8070f2 --- /dev/null +++ b/src/material/table/testing/public-api.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './table-harness'; +export * from './row-harness'; +export * from './cell-harness'; +export * from './table-harness-filters'; diff --git a/src/material/table/testing/row-harness.ts b/src/material/table/testing/row-harness.ts new file mode 100644 index 000000000000..79c92c94c51a --- /dev/null +++ b/src/material/table/testing/row-harness.ts @@ -0,0 +1,92 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {RowHarnessFilters, CellHarnessFilters} from './table-harness-filters'; +import {MatCellHarness, MatHeaderCellHarness, MatFooterCellHarness} from './cell-harness'; + +/** Data extracted from the cells in a table row. */ +export type MatRowHarnessData = { + columnName: string; + text: string; +}[]; + +/** Harness for interacting with a standard Angular Material table row. */ +export class MatRowHarness extends ComponentHarness { + static hostSelector = '.mat-row'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a row with specific attributes. + */ + static with(options: RowHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatRowHarness, options); + } + + /** Gets all cells of the table row. */ + async getCells(filter: CellHarnessFilters = {}): Promise { + return this.locatorForAll(MatCellHarness.with(filter))(); + } + + /** Gets the data of the cells in the footer row. */ + async getData(filter: CellHarnessFilters = {}): Promise { + return getCellData(await this.getCells(filter)); + } +} + +/** Harness for interacting with a standard Angular Material table header row. */ +export class MatHeaderRowHarness extends ComponentHarness { + static hostSelector = '.mat-header-row'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a header row with specific attributes. + */ + static with(options: RowHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatHeaderRowHarness, options); + } + + /** Gets all cells of the table header row. */ + async getCells(filter: CellHarnessFilters = {}): Promise { + return this.locatorForAll(MatHeaderCellHarness.with(filter))(); + } + + /** Gets the data of the cells in the footer row. */ + async getData(filter: CellHarnessFilters = {}): Promise { + return getCellData(await this.getCells(filter)); + } +} + + +/** Harness for interacting with a standard Angular Material table footer row. */ +export class MatFooterRowHarness extends ComponentHarness { + static hostSelector = '.mat-footer-row'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a footer row with specific attributes. + */ + static with(options: RowHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatFooterRowHarness, options); + } + + /** Gets all cells of the table footer row. */ + async getCells(filter: CellHarnessFilters = {}): Promise { + return this.locatorForAll(MatFooterCellHarness.with(filter))(); + } + + /** Gets the data of the cells in the footer row. */ + async getData(filter: CellHarnessFilters = {}): Promise { + return getCellData(await this.getCells(filter)); + } +} + +/** Extracts the data from the cells in a row. */ +async function getCellData(cells: MatCellHarness[]): Promise { + return Promise.all(cells.map(async cell => ({ + text: await cell.getText(), + columnName: await cell.getColumnName() + }))); +} diff --git a/src/material/table/testing/shared.spec.ts b/src/material/table/testing/shared.spec.ts new file mode 100644 index 000000000000..a7fb9dedb8c2 --- /dev/null +++ b/src/material/table/testing/shared.spec.ts @@ -0,0 +1,240 @@ +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatTableModule} from '@angular/material/table'; +import {MatTableHarness} from './table-harness'; + +/** Shared tests to run on both the original and MDC-based table. */ +export function runHarnessTests( + tableModule: typeof MatTableModule, + tableHarness: typeof MatTableHarness) { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [tableModule], + declarations: [TableHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(TableHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should load harness for a table', async () => { + const tables = await loader.getAllHarnesses(tableHarness); + expect(tables.length).toBe(1); + }); + + it('should get the different kinds of rows in the table', async () => { + const table = await loader.getHarness(tableHarness); + const headerRows = await table.getHeaderRows(); + const footerRows = await table.getFooterRows(); + const rows = await table.getRows(); + expect(headerRows.length).toBe(1); + expect(footerRows.length).toBe(1); + expect(rows.length).toBe(10); + }); + + it('should get cells inside a row', async () => { + const table = await loader.getHarness(tableHarness); + const headerRows = await table.getHeaderRows(); + const footerRows = await table.getFooterRows(); + const rows = await table.getRows(); + const headerCells = (await Promise.all(headerRows.map(row => row.getCells()))) + .map(row => row.length); + const footerCells = (await Promise.all(footerRows.map(row => row.getCells()))) + .map(row => row.length); + const cells = (await Promise.all(rows.map(row => row.getCells()))) + .map(row => row.length); + + expect(headerCells).toEqual([4]); + expect(cells).toEqual([4, 4, 4, 4, 4, 4, 4, 4, 4, 4]); + expect(footerCells).toEqual([4]); + }); + + it('should be able to get the text of a cell', async () => { + const table = await loader.getHarness(tableHarness); + const secondRow = (await table.getRows())[1]; + const cells = await secondRow.getCells(); + const cellTexts = await Promise.all(cells.map(cell => cell.getText())); + expect(cellTexts).toEqual(['2', 'Helium', '4.0026', 'He']); + }); + + it('should be able to get the column name of a cell', async () => { + const table = await loader.getHarness(tableHarness); + const fifthRow = (await table.getRows())[1]; + const cells = await fifthRow.getCells(); + const cellColumnNames = await Promise.all(cells.map(cell => cell.getColumnName())); + expect(cellColumnNames).toEqual(['position', 'name', 'weight', 'symbol']); + }); + + it('should be able to filter cells by text', async () => { + const table = await loader.getHarness(tableHarness); + const firstRow = (await table.getRows())[0]; + const cells = await firstRow.getCells({text: '1.0079'}); + const cellTexts = await Promise.all(cells.map(cell => cell.getText())); + expect(cellTexts).toEqual(['1.0079']); + }); + + it('should be able to filter cells by regex', async () => { + const table = await loader.getHarness(tableHarness); + const firstRow = (await table.getRows())[0]; + const cells = await firstRow.getCells({text: /^H/}); + const cellTexts = await Promise.all(cells.map(cell => cell.getText())); + expect(cellTexts).toEqual(['Hydrogen', 'H']); + }); + + it('should be able to get the table data organized by columns', async () => { + const table = await loader.getHarness(tableHarness); + const data = await table.getColumnsData(); + + expect(data).toEqual({ + position: { + headerText: 'No.', + footerText: 'Number of the element', + text: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] + }, + name: { + headerText: 'Name', + footerText: 'Name of the element', + text: [ + 'Hydrogen', 'Helium', 'Lithium', 'Beryllium', 'Boron', + 'Carbon', 'Nitrogen', 'Oxygen', 'Fluorine', 'Neon' + ] + }, + weight: { + headerText: 'Weight', + footerText: 'Weight of the element', + text: [ + '1.0079', '4.0026', '6.941', '9.0122', '10.811', + '12.0107', '14.0067', '15.9994', '18.9984', '20.1797' + ] + }, + symbol: { + headerText: 'Symbol', + footerText: 'Symbol of the element', + text: ['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne'] + } + }); + }); + + it('should be able to get the table data organized by rows', async () => { + const table = await loader.getHarness(tableHarness); + const data = await table.getRowsData(); + + expect(data).toEqual([ + [ + {text: '1', columnName: 'position'}, + {text: 'Hydrogen', columnName: 'name'}, + {text: '1.0079', columnName: 'weight'}, + {text: 'H', columnName: 'symbol'} + ], + [ + {text: '2', columnName: 'position'}, + {text: 'Helium', columnName: 'name'}, + {text: '4.0026', columnName: 'weight'}, + {text: 'He', columnName: 'symbol'} + ], + [ + {text: '3', columnName: 'position'}, + {text: 'Lithium', columnName: 'name'}, + {text: '6.941', columnName: 'weight'}, + {text: 'Li', columnName: 'symbol'} + ], + [ + {text: '4', columnName: 'position'}, + {text: 'Beryllium', columnName: 'name'}, + {text: '9.0122', columnName: 'weight'}, + {text: 'Be', columnName: 'symbol'} + ], + [ + {text: '5', columnName: 'position'}, + {text: 'Boron', columnName: 'name'}, + {text: '10.811', columnName: 'weight'}, + {text: 'B', columnName: 'symbol'} + ], + [ + {text: '6', columnName: 'position'}, + {text: 'Carbon', columnName: 'name'}, + {text: '12.0107', columnName: 'weight'}, + {text: 'C', columnName: 'symbol'} + ], + [ + {text: '7', columnName: 'position'}, + {text: 'Nitrogen', columnName: 'name'}, + {text: '14.0067', columnName: 'weight'}, + {text: 'N', columnName: 'symbol'} + ], + [ + {text: '8', columnName: 'position'}, + {text: 'Oxygen', columnName: 'name'}, + {text: '15.9994', columnName: 'weight'}, + {text: 'O', columnName: 'symbol'} + ], + [ + {text: '9', columnName: 'position'}, + {text: 'Fluorine', columnName: 'name'}, + {text: '18.9984', columnName: 'weight'}, + {text: 'F', columnName: 'symbol'} + ], + [ + {text: '10', columnName: 'position'}, + {text: 'Neon', columnName: 'name'}, + {text: '20.1797', columnName: 'weight'}, + {text: 'Ne', columnName: 'symbol'} + ] + ]); + }); +} + +@Component({ + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No.{{element.position}}Number of the elementName{{element.name}}Name of the elementWeight{{element.weight}}Weight of the elementSymbol{{element.symbol}}Symbol of the element
+ ` +}) +class TableHarnessTest { + displayedColumns: string[] = ['position', 'name', 'weight', 'symbol']; + dataSource = [ + {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'}, + {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}, + {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'}, + {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'}, + {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'}, + {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'}, + {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'}, + {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'}, + {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'}, + {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'}, + ]; +} diff --git a/src/material/table/testing/table-harness-filters.ts b/src/material/table/testing/table-harness-filters.ts new file mode 100644 index 000000000000..5071ba204991 --- /dev/null +++ b/src/material/table/testing/table-harness-filters.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {BaseHarnessFilters} from '@angular/cdk/testing'; + +export interface CellHarnessFilters extends BaseHarnessFilters { + text?: string | RegExp; +} + +export interface RowHarnessFilters extends BaseHarnessFilters { +} + +export interface TableHarnessFilters extends BaseHarnessFilters { +} diff --git a/src/material/table/testing/table-harness.spec.ts b/src/material/table/testing/table-harness.spec.ts new file mode 100644 index 000000000000..3c48afddbf06 --- /dev/null +++ b/src/material/table/testing/table-harness.spec.ts @@ -0,0 +1,7 @@ +import {MatTableModule} from '@angular/material/table'; +import {runHarnessTests} from '@angular/material/table/testing/shared.spec'; +import {MatTableHarness} from './table-harness'; + +describe('Non-MDC-based MatTableHarness', () => { + runHarnessTests(MatTableModule, MatTableHarness); +}); diff --git a/src/material/table/testing/table-harness.ts b/src/material/table/testing/table-harness.ts new file mode 100644 index 000000000000..b7be706f5286 --- /dev/null +++ b/src/material/table/testing/table-harness.ts @@ -0,0 +1,94 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import {TableHarnessFilters, RowHarnessFilters} from './table-harness-filters'; +import {MatRowHarness, MatHeaderRowHarness, MatFooterRowHarness} from './row-harness'; + +/** Data extracted from a table organized by columns. */ +export interface MatTableHarnessColumnsData { + [columnName: string]: { + text: string[]; + headerText: string; + footerText: string; + }; +} + +/** Data extracted from a table organized by rows. */ +export type MatTableHarnessRowsData = { + columnName: string; + text: string; +}[][]; + +/** Harness for interacting with a standard mat-table in tests. */ +export class MatTableHarness extends ComponentHarness { + static hostSelector = '.mat-table'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a radio-button with + * specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: TableHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatTableHarness, options); + } + + /** Gets all of the header rows in a table. */ + async getHeaderRows(filter: RowHarnessFilters = {}): Promise { + return this.locatorForAll(MatHeaderRowHarness.with(filter))(); + } + + /** Gets all of the regular data rows in a table. */ + async getRows(filter: RowHarnessFilters = {}): Promise { + return this.locatorForAll(MatRowHarness.with(filter))(); + } + + /** Gets all of the footer rows in a table. */ + async getFooterRows(filter: RowHarnessFilters = {}): Promise { + return this.locatorForAll(MatFooterRowHarness.with(filter))(); + } + + /** Gets the data inside the entire table organized by rows. */ + async getRowsData(): Promise { + const rows = await this.getRows(); + return Promise.all(rows.map(row => row.getData())); + } + + /** Gets the data inside the entire table organized by columns. */ + async getColumnsData(): Promise { + // Tables can have multiple header rows, but we consider the first one as the "main" row. + const headerRow = (await this.getHeaderRows())[0]; + const footerRow = (await this.getFooterRows())[0]; + const dataRows = await this.getRows(); + + const headerData = headerRow ? await headerRow.getData() : []; + const footerData = footerRow ? await footerRow.getData() : []; + const rowsData = await Promise.all(dataRows.map(row => row.getData())); + const data: MatTableHarnessColumnsData = {}; + + rowsData.forEach(cells => { + cells.forEach(cell => { + if (!data[cell.columnName]) { + const headerCell = headerData.find(header => header.columnName === cell.columnName); + const footerCell = footerData.find(footer => footer.columnName === cell.columnName); + + data[cell.columnName] = { + headerText: headerCell ? headerCell.text : '', + footerText: footerCell ? footerCell.text : '', + text: [] + }; + } + + data[cell.columnName].text.push(cell.text); + }); + }); + + return data; + } +} diff --git a/test/karma-system-config.js b/test/karma-system-config.js index d8359d3344e4..728a02f6256f 100644 --- a/test/karma-system-config.js +++ b/test/karma-system-config.js @@ -163,6 +163,8 @@ System.config({ '@angular/material/sort/testing/shared.spec': 'dist/packages/material/sort/testing/shared.spec.js', '@angular/material/stepper': 'dist/packages/material/stepper/index.js', '@angular/material/table': 'dist/packages/material/table/index.js', + '@angular/material/table/testing': 'dist/packages/material/table/testing/index.js', + '@angular/material/table/testing/shared.spec': 'dist/packages/material/table/testing/shared.spec.js', '@angular/material/tabs': 'dist/packages/material/tabs/index.js', '@angular/material/tabs/testing': 'dist/packages/material/tabs/testing/index.js', '@angular/material/tabs/testing/shared.spec': 'dist/packages/material/tabs/testing/shared.spec.js', diff --git a/tools/public_api_guard/material/table/testing.d.ts b/tools/public_api_guard/material/table/testing.d.ts new file mode 100644 index 000000000000..e0c23f5472bc --- /dev/null +++ b/tools/public_api_guard/material/table/testing.d.ts @@ -0,0 +1,75 @@ +export interface CellHarnessFilters extends BaseHarnessFilters { + text?: string | RegExp; +} + +export declare class MatCellHarness extends ComponentHarness { + getColumnName(): Promise; + getText(): Promise; + static hostSelector: string; + static with(options?: CellHarnessFilters): HarnessPredicate; +} + +export declare class MatFooterCellHarness extends MatCellHarness { + static hostSelector: string; + static with(options?: CellHarnessFilters): HarnessPredicate; +} + +export declare class MatFooterRowHarness extends ComponentHarness { + getCells(filter?: CellHarnessFilters): Promise; + getData(filter?: CellHarnessFilters): Promise; + static hostSelector: string; + static with(options?: RowHarnessFilters): HarnessPredicate; +} + +export declare class MatHeaderCellHarness extends MatCellHarness { + static hostSelector: string; + static with(options?: CellHarnessFilters): HarnessPredicate; +} + +export declare class MatHeaderRowHarness extends ComponentHarness { + getCells(filter?: CellHarnessFilters): Promise; + getData(filter?: CellHarnessFilters): Promise; + static hostSelector: string; + static with(options?: RowHarnessFilters): HarnessPredicate; +} + +export declare class MatRowHarness extends ComponentHarness { + getCells(filter?: CellHarnessFilters): Promise; + getData(filter?: CellHarnessFilters): Promise; + static hostSelector: string; + static with(options?: RowHarnessFilters): HarnessPredicate; +} + +export declare type MatRowHarnessData = { + columnName: string; + text: string; +}[]; + +export declare class MatTableHarness extends ComponentHarness { + getColumnsData(): Promise; + getFooterRows(filter?: RowHarnessFilters): Promise; + getHeaderRows(filter?: RowHarnessFilters): Promise; + getRows(filter?: RowHarnessFilters): Promise; + getRowsData(): Promise; + static hostSelector: string; + static with(options?: TableHarnessFilters): HarnessPredicate; +} + +export interface MatTableHarnessColumnsData { + [columnName: string]: { + text: string[]; + headerText: string; + footerText: string; + }; +} + +export declare type MatTableHarnessRowsData = { + columnName: string; + text: string; +}[][]; + +export interface RowHarnessFilters extends BaseHarnessFilters { +} + +export interface TableHarnessFilters extends BaseHarnessFilters { +} From a6f4c745d35fa4c4ec1192516fb1eb1becfe6456 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 19 Dec 2019 22:59:47 +0100 Subject: [PATCH 2/2] feat(table): add test harness Adds a test harness for `MatTable`, as well as the related row and cell directives. --- src/material/table/testing/cell-harness.ts | 48 ++++++--- src/material/table/testing/row-harness.ts | 58 +++++----- src/material/table/testing/shared.spec.ts | 100 +++++------------- .../table/testing/table-harness-filters.ts | 4 + src/material/table/testing/table-harness.ts | 86 +++++++++------ .../material/table/testing.d.ts | 26 ++--- 6 files changed, 152 insertions(+), 170 deletions(-) diff --git a/src/material/table/testing/cell-harness.ts b/src/material/table/testing/cell-harness.ts index 96dd78927213..a446eadf0c1e 100644 --- a/src/material/table/testing/cell-harness.ts +++ b/src/material/table/testing/cell-harness.ts @@ -6,28 +6,33 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import { + ComponentHarness, + HarnessPredicate, + ComponentHarnessConstructor, +} from '@angular/cdk/testing'; import {CellHarnessFilters} from './table-harness-filters'; /** Harness for interacting with a standard Angular Material table cell. */ export class MatCellHarness extends ComponentHarness { + /** The selector for the host element of a `MatCellHarness` instance. */ static hostSelector = '.mat-cell'; /** - * Gets a `HarnessPredicate` that can be used to search for a cell with specific attributes. + * Gets a `HarnessPredicate` that can be used to search for a table cell with specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. */ static with(options: CellHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatCellHarness, options) - .addOption('text', options.text, - (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + return getCellPredicate(MatCellHarness, options); } - /** Gets a promise for the cell's text. */ + /** Gets the cell's text. */ async getText(): Promise { return (await this.host()).text(); } - /** Gets the name of the column. */ + /** Gets the name of the column that the cell belongs to. */ async getColumnName(): Promise { const host = await this.host(); const classAttribute = await host.getAttribute('class'); @@ -47,28 +52,41 @@ export class MatCellHarness extends ComponentHarness { /** Harness for interacting with a standard Angular Material table header cell. */ export class MatHeaderCellHarness extends MatCellHarness { + /** The selector for the host element of a `MatHeaderCellHarness` instance. */ static hostSelector = '.mat-header-cell'; /** - * Gets a `HarnessPredicate` that can be used to search for a header cell with specific attributes + * Gets a `HarnessPredicate` that can be used to search for + * a table header cell with specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. */ static with(options: CellHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatHeaderCellHarness, options) - .addOption('text', options.text, - (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + return getCellPredicate(MatHeaderCellHarness, options); } } /** Harness for interacting with a standard Angular Material table footer cell. */ export class MatFooterCellHarness extends MatCellHarness { + /** The selector for the host element of a `MatFooterCellHarness` instance. */ static hostSelector = '.mat-footer-cell'; /** - * Gets a `HarnessPredicate` that can be used to search for a footer cell with specific attributes + * Gets a `HarnessPredicate` that can be used to search for + * a table footer cell with specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. */ static with(options: CellHarnessFilters = {}): HarnessPredicate { - return new HarnessPredicate(MatFooterCellHarness, options) - .addOption('text', options.text, - (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + return getCellPredicate(MatFooterCellHarness, options); } } + + +function getCellPredicate( + type: ComponentHarnessConstructor, + options: CellHarnessFilters): HarnessPredicate { + return new HarnessPredicate(type, options) + .addOption('text', options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); +} diff --git a/src/material/table/testing/row-harness.ts b/src/material/table/testing/row-harness.ts index 79c92c94c51a..88c03f6d1f7f 100644 --- a/src/material/table/testing/row-harness.ts +++ b/src/material/table/testing/row-harness.ts @@ -10,83 +10,87 @@ import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; import {RowHarnessFilters, CellHarnessFilters} from './table-harness-filters'; import {MatCellHarness, MatHeaderCellHarness, MatFooterCellHarness} from './cell-harness'; -/** Data extracted from the cells in a table row. */ -export type MatRowHarnessData = { - columnName: string; - text: string; -}[]; - /** Harness for interacting with a standard Angular Material table row. */ export class MatRowHarness extends ComponentHarness { + /** The selector for the host element of a `MatRowHarness` instance. */ static hostSelector = '.mat-row'; /** - * Gets a `HarnessPredicate` that can be used to search for a row with specific attributes. + * Gets a `HarnessPredicate` that can be used to search for a table row with specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. */ static with(options: RowHarnessFilters = {}): HarnessPredicate { return new HarnessPredicate(MatRowHarness, options); } - /** Gets all cells of the table row. */ + /** Gets a list of `MatCellHarness` for all cells in the row. */ async getCells(filter: CellHarnessFilters = {}): Promise { return this.locatorForAll(MatCellHarness.with(filter))(); } - /** Gets the data of the cells in the footer row. */ - async getData(filter: CellHarnessFilters = {}): Promise { - return getCellData(await this.getCells(filter)); + /** Gets the text of the cells in the row. */ + async getCellTextByIndex(filter: CellHarnessFilters = {}): Promise { + return getCellTextByIndex(this, filter); } } /** Harness for interacting with a standard Angular Material table header row. */ export class MatHeaderRowHarness extends ComponentHarness { + /** The selector for the host element of a `MatHeaderRowHarness` instance. */ static hostSelector = '.mat-header-row'; /** - * Gets a `HarnessPredicate` that can be used to search for a header row with specific attributes. + * Gets a `HarnessPredicate` that can be used to search for + * a table header row with specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. */ static with(options: RowHarnessFilters = {}): HarnessPredicate { return new HarnessPredicate(MatHeaderRowHarness, options); } - /** Gets all cells of the table header row. */ + /** Gets a list of `MatHeaderCellHarness` for all cells in the row. */ async getCells(filter: CellHarnessFilters = {}): Promise { return this.locatorForAll(MatHeaderCellHarness.with(filter))(); } - /** Gets the data of the cells in the footer row. */ - async getData(filter: CellHarnessFilters = {}): Promise { - return getCellData(await this.getCells(filter)); + /** Gets the text of the cells in the header row. */ + async getCellTextByIndex(filter: CellHarnessFilters = {}): Promise { + return getCellTextByIndex(this, filter); } } /** Harness for interacting with a standard Angular Material table footer row. */ export class MatFooterRowHarness extends ComponentHarness { + /** The selector for the host element of a `MatFooterRowHarness` instance. */ static hostSelector = '.mat-footer-row'; /** - * Gets a `HarnessPredicate` that can be used to search for a footer row with specific attributes. + * Gets a `HarnessPredicate` that can be used to search for + * a table footer row cell with specific attributes. + * @param options Options for narrowing the search + * @return a `HarnessPredicate` configured with the given options. */ static with(options: RowHarnessFilters = {}): HarnessPredicate { return new HarnessPredicate(MatFooterRowHarness, options); } - /** Gets all cells of the table footer row. */ + /** Gets a list of `MatFooterCellHarness` for all cells in the row. */ async getCells(filter: CellHarnessFilters = {}): Promise { return this.locatorForAll(MatFooterCellHarness.with(filter))(); } - /** Gets the data of the cells in the footer row. */ - async getData(filter: CellHarnessFilters = {}): Promise { - return getCellData(await this.getCells(filter)); + /** Gets the text of the cells in the footer row. */ + async getCellTextByIndex(filter: CellHarnessFilters = {}): Promise { + return getCellTextByIndex(this, filter); } } -/** Extracts the data from the cells in a row. */ -async function getCellData(cells: MatCellHarness[]): Promise { - return Promise.all(cells.map(async cell => ({ - text: await cell.getText(), - columnName: await cell.getColumnName() - }))); +async function getCellTextByIndex(harness: { + getCells: (filter?: CellHarnessFilters) => Promise +}, filter: CellHarnessFilters): Promise { + const cells = await harness.getCells(filter); + return Promise.all(cells.map(cell => cell.getText())); } diff --git a/src/material/table/testing/shared.spec.ts b/src/material/table/testing/shared.spec.ts index a7fb9dedb8c2..8ac81fa76787 100644 --- a/src/material/table/testing/shared.spec.ts +++ b/src/material/table/testing/shared.spec.ts @@ -87,105 +87,55 @@ export function runHarnessTests( expect(cellTexts).toEqual(['Hydrogen', 'H']); }); - it('should be able to get the table data organized by columns', async () => { + it('should be able to get the table text organized by columns', async () => { const table = await loader.getHarness(tableHarness); - const data = await table.getColumnsData(); + const text = await table.getCellTextByColumnName(); - expect(data).toEqual({ + expect(text).toEqual({ position: { - headerText: 'No.', - footerText: 'Number of the element', + headerText: ['No.'], + footerText: ['Number of the element'], text: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] }, name: { - headerText: 'Name', - footerText: 'Name of the element', + headerText: ['Name'], + footerText: ['Name of the element'], text: [ 'Hydrogen', 'Helium', 'Lithium', 'Beryllium', 'Boron', 'Carbon', 'Nitrogen', 'Oxygen', 'Fluorine', 'Neon' ] }, weight: { - headerText: 'Weight', - footerText: 'Weight of the element', + headerText: ['Weight'], + footerText: ['Weight of the element'], text: [ '1.0079', '4.0026', '6.941', '9.0122', '10.811', '12.0107', '14.0067', '15.9994', '18.9984', '20.1797' ] }, symbol: { - headerText: 'Symbol', - footerText: 'Symbol of the element', + headerText: ['Symbol'], + footerText: ['Symbol of the element'], text: ['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne'] } }); }); - it('should be able to get the table data organized by rows', async () => { + it('should be able to get the table text organized by rows', async () => { const table = await loader.getHarness(tableHarness); - const data = await table.getRowsData(); - - expect(data).toEqual([ - [ - {text: '1', columnName: 'position'}, - {text: 'Hydrogen', columnName: 'name'}, - {text: '1.0079', columnName: 'weight'}, - {text: 'H', columnName: 'symbol'} - ], - [ - {text: '2', columnName: 'position'}, - {text: 'Helium', columnName: 'name'}, - {text: '4.0026', columnName: 'weight'}, - {text: 'He', columnName: 'symbol'} - ], - [ - {text: '3', columnName: 'position'}, - {text: 'Lithium', columnName: 'name'}, - {text: '6.941', columnName: 'weight'}, - {text: 'Li', columnName: 'symbol'} - ], - [ - {text: '4', columnName: 'position'}, - {text: 'Beryllium', columnName: 'name'}, - {text: '9.0122', columnName: 'weight'}, - {text: 'Be', columnName: 'symbol'} - ], - [ - {text: '5', columnName: 'position'}, - {text: 'Boron', columnName: 'name'}, - {text: '10.811', columnName: 'weight'}, - {text: 'B', columnName: 'symbol'} - ], - [ - {text: '6', columnName: 'position'}, - {text: 'Carbon', columnName: 'name'}, - {text: '12.0107', columnName: 'weight'}, - {text: 'C', columnName: 'symbol'} - ], - [ - {text: '7', columnName: 'position'}, - {text: 'Nitrogen', columnName: 'name'}, - {text: '14.0067', columnName: 'weight'}, - {text: 'N', columnName: 'symbol'} - ], - [ - {text: '8', columnName: 'position'}, - {text: 'Oxygen', columnName: 'name'}, - {text: '15.9994', columnName: 'weight'}, - {text: 'O', columnName: 'symbol'} - ], - [ - {text: '9', columnName: 'position'}, - {text: 'Fluorine', columnName: 'name'}, - {text: '18.9984', columnName: 'weight'}, - {text: 'F', columnName: 'symbol'} - ], - [ - {text: '10', columnName: 'position'}, - {text: 'Neon', columnName: 'name'}, - {text: '20.1797', columnName: 'weight'}, - {text: 'Ne', columnName: 'symbol'} - ] + const text = await table.getCellTextByIndex(); + + expect(text).toEqual([ + ['1', 'Hydrogen', '1.0079', 'H'], + ['2', 'Helium', '4.0026', 'He'], + ['3', 'Lithium', '6.941', 'Li'], + ['4', 'Beryllium', '9.0122', 'Be'], + ['5', 'Boron', '10.811', 'B'], + ['6', 'Carbon', '12.0107', 'C'], + ['7', 'Nitrogen', '14.0067', 'N'], + ['8', 'Oxygen', '15.9994', 'O'], + ['9', 'Fluorine', '18.9984', 'F'], + ['10', 'Neon', '20.1797', 'Ne'] ]); }); } diff --git a/src/material/table/testing/table-harness-filters.ts b/src/material/table/testing/table-harness-filters.ts index 5071ba204991..78a16d701087 100644 --- a/src/material/table/testing/table-harness-filters.ts +++ b/src/material/table/testing/table-harness-filters.ts @@ -7,12 +7,16 @@ */ import {BaseHarnessFilters} from '@angular/cdk/testing'; +/** A set of criteria that can be used to filter a list of cell harness instances. */ export interface CellHarnessFilters extends BaseHarnessFilters { + /** Only find instances whose text matches the given value. */ text?: string | RegExp; } +/** A set of criteria that can be used to filter a list of row harness instances. */ export interface RowHarnessFilters extends BaseHarnessFilters { } +/** A set of criteria that can be used to filter a list of table harness instances. */ export interface TableHarnessFilters extends BaseHarnessFilters { } diff --git a/src/material/table/testing/table-harness.ts b/src/material/table/testing/table-harness.ts index b7be706f5286..dcd5ff64f16c 100644 --- a/src/material/table/testing/table-harness.ts +++ b/src/material/table/testing/table-harness.ts @@ -10,28 +10,22 @@ import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; import {TableHarnessFilters, RowHarnessFilters} from './table-harness-filters'; import {MatRowHarness, MatHeaderRowHarness, MatFooterRowHarness} from './row-harness'; -/** Data extracted from a table organized by columns. */ -export interface MatTableHarnessColumnsData { +/** Text extracted from a table organized by columns. */ +export interface MatTableHarnessColumnsText { [columnName: string]: { text: string[]; - headerText: string; - footerText: string; + headerText: string[]; + footerText: string[]; }; } -/** Data extracted from a table organized by rows. */ -export type MatTableHarnessRowsData = { - columnName: string; - text: string; -}[][]; - /** Harness for interacting with a standard mat-table in tests. */ export class MatTableHarness extends ComponentHarness { + /** The selector for the host element of a `MatTableHarness` instance. */ static hostSelector = '.mat-table'; /** - * Gets a `HarnessPredicate` that can be used to search for a radio-button with - * specific attributes. + * Gets a `HarnessPredicate` that can be used to search for a table with specific attributes. * @param options Options for narrowing the search * @return a `HarnessPredicate` configured with the given options. */ @@ -54,41 +48,63 @@ export class MatTableHarness extends ComponentHarness { return this.locatorForAll(MatFooterRowHarness.with(filter))(); } - /** Gets the data inside the entire table organized by rows. */ - async getRowsData(): Promise { + /** Gets the text inside the entire table organized by rows. */ + async getCellTextByIndex(): Promise { const rows = await this.getRows(); - return Promise.all(rows.map(row => row.getData())); + return Promise.all(rows.map(row => row.getCellTextByIndex())); } - /** Gets the data inside the entire table organized by columns. */ - async getColumnsData(): Promise { - // Tables can have multiple header rows, but we consider the first one as the "main" row. - const headerRow = (await this.getHeaderRows())[0]; - const footerRow = (await this.getFooterRows())[0]; - const dataRows = await this.getRows(); + /** Gets the text inside the entire table organized by columns. */ + async getCellTextByColumnName(): Promise { + const [headerRows, footerRows, dataRows] = await Promise.all([ + this.getHeaderRows(), + this.getFooterRows(), + this.getRows() + ]); - const headerData = headerRow ? await headerRow.getData() : []; - const footerData = footerRow ? await footerRow.getData() : []; - const rowsData = await Promise.all(dataRows.map(row => row.getData())); - const data: MatTableHarnessColumnsData = {}; + const text: MatTableHarnessColumnsText = {}; + const [headerData, footerData, rowsData] = await Promise.all([ + Promise.all(headerRows.map(row => getRowData(row))), + Promise.all(footerRows.map(row => getRowData(row))), + Promise.all(dataRows.map(row => getRowData(row))), + ]); rowsData.forEach(cells => { - cells.forEach(cell => { - if (!data[cell.columnName]) { - const headerCell = headerData.find(header => header.columnName === cell.columnName); - const footerCell = footerData.find(footer => footer.columnName === cell.columnName); - - data[cell.columnName] = { - headerText: headerCell ? headerCell.text : '', - footerText: footerCell ? footerCell.text : '', + cells.forEach(([columnName, cellText]) => { + if (!text[columnName]) { + text[columnName] = { + headerText: getCellTextsByColumn(headerData, columnName), + footerText: getCellTextsByColumn(footerData, columnName), text: [] }; } - data[cell.columnName].text.push(cell.text); + text[columnName].text.push(cellText); }); }); - return data; + return text; } } + +/** Utility to extract the column names and text from all of the cells in a row. */ +async function getRowData(row: MatRowHarness | MatHeaderRowHarness | MatFooterRowHarness) { + const cells = await row.getCells(); + return Promise.all(cells.map(cell => Promise.all([cell.getColumnName(), cell.getText()]))); +} + + +/** Extracts the text of cells only under a particular column. */ +function getCellTextsByColumn(rowsData: [string, string][][], column: string): string[] { + const columnTexts: string[] = []; + + rowsData.forEach(cells => { + cells.forEach(([columnName, cellText]) => { + if (columnName === column) { + columnTexts.push(cellText); + } + }); + }); + + return columnTexts; +} diff --git a/tools/public_api_guard/material/table/testing.d.ts b/tools/public_api_guard/material/table/testing.d.ts index e0c23f5472bc..de7736e82333 100644 --- a/tools/public_api_guard/material/table/testing.d.ts +++ b/tools/public_api_guard/material/table/testing.d.ts @@ -15,8 +15,8 @@ export declare class MatFooterCellHarness extends MatCellHarness { } export declare class MatFooterRowHarness extends ComponentHarness { + getCellTextByIndex(filter?: CellHarnessFilters): Promise; getCells(filter?: CellHarnessFilters): Promise; - getData(filter?: CellHarnessFilters): Promise; static hostSelector: string; static with(options?: RowHarnessFilters): HarnessPredicate; } @@ -27,47 +27,37 @@ export declare class MatHeaderCellHarness extends MatCellHarness { } export declare class MatHeaderRowHarness extends ComponentHarness { + getCellTextByIndex(filter?: CellHarnessFilters): Promise; getCells(filter?: CellHarnessFilters): Promise; - getData(filter?: CellHarnessFilters): Promise; static hostSelector: string; static with(options?: RowHarnessFilters): HarnessPredicate; } export declare class MatRowHarness extends ComponentHarness { + getCellTextByIndex(filter?: CellHarnessFilters): Promise; getCells(filter?: CellHarnessFilters): Promise; - getData(filter?: CellHarnessFilters): Promise; static hostSelector: string; static with(options?: RowHarnessFilters): HarnessPredicate; } -export declare type MatRowHarnessData = { - columnName: string; - text: string; -}[]; - export declare class MatTableHarness extends ComponentHarness { - getColumnsData(): Promise; + getCellTextByColumnName(): Promise; + getCellTextByIndex(): Promise; getFooterRows(filter?: RowHarnessFilters): Promise; getHeaderRows(filter?: RowHarnessFilters): Promise; getRows(filter?: RowHarnessFilters): Promise; - getRowsData(): Promise; static hostSelector: string; static with(options?: TableHarnessFilters): HarnessPredicate; } -export interface MatTableHarnessColumnsData { +export interface MatTableHarnessColumnsText { [columnName: string]: { text: string[]; - headerText: string; - footerText: string; + headerText: string[]; + footerText: string[]; }; } -export declare type MatTableHarnessRowsData = { - columnName: string; - text: string; -}[][]; - export interface RowHarnessFilters extends BaseHarnessFilters { }