Skip to content

Commit

Permalink
feat(data-table): add clear selection imperitive method (uber#5162)
Browse files Browse the repository at this point in the history
* feat(data-table): add clear selection imperitive method

* test(vrt): update visual snapshots for 37f4bdc (uber#5163)

* feat(data-table): add clear selection imperitive method

* fix(e2e): improve test reliability

* chore(e2e): fix lint/tsc

Co-authored-by: UberOpenSourceBot <[email protected]>
  • Loading branch information
chasestarr and UberOpenSourceBot authored Oct 4, 2022
1 parent ef9cd57 commit 5dd8468
Show file tree
Hide file tree
Showing 22 changed files with 328 additions and 227 deletions.
183 changes: 98 additions & 85 deletions src/data-table/__tests__/data-table-batch-action.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,10 @@ LICENSE file in the root directory of this source tree.

import { expect, test } from '@playwright/test';
import { mount, analyzeAccessibility } from '../../test/integration';
import { getTable, getCellContentsAtColumnIndex, matchArrayElements } from './utilities';
import { getTableLocator, getCellContentsAtColumnIndex } from './utilities';

const COLUMN_COUNT = 2;

function getCheckboxes(parent) {
return parent.$$('label[data-baseweb="checkbox"]');
}

async function clickCheckboxAtRowIndex(parent, index) {
const checkboxes = await getCheckboxes(parent);
await checkboxes[index].click();
}

function wait(ms) {
return new Promise((res) => setTimeout(res, ms));
}

async function getCheckboxValues(element) {
await wait(50); // briefly wait to give table state chance to update
return element.$$eval('label[data-baseweb="checkbox"] input', (elements) =>
elements.map((el) => String(el.checked))
);
}

test.describe('data-table batch-actions', () => {
test('passes basic a11y tests', async ({ page }) => {
await mount(page, 'data-table--columns');
Expand All @@ -43,122 +23,155 @@ test.describe('data-table batch-actions', () => {

test('renders checkboxes if batch actions are provided', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
const checkboxes = await getCheckboxes(table);
expect(checkboxes.length).toBe(6);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
expect(checkboxes).toHaveCount(6);
});

test('checks row on selection', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 1);
const actual = await getCheckboxValues(table);
const expected = ['true', 'true', 'false', 'false', 'false', 'false'];
expect(matchArrayElements(actual, expected)).toBe(true);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(1).click();
await expect(checkboxes.nth(0)).toBeChecked();
await expect(checkboxes.nth(1)).toBeChecked();
await expect(checkboxes.nth(2)).not.toBeChecked();
await expect(checkboxes.nth(3)).not.toBeChecked();
await expect(checkboxes.nth(4)).not.toBeChecked();
await expect(checkboxes.nth(5)).not.toBeChecked();
});

test('unchecks row on second selection', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 1);
await clickCheckboxAtRowIndex(table, 1);
const actual = await getCheckboxValues(table);
const expected = ['false', 'false', 'false', 'false', 'false', 'false'];
expect(matchArrayElements(actual, expected)).toBe(true);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(1).click();
await checkboxes.nth(1).click();
await expect(checkboxes.nth(0)).not.toBeChecked();
await expect(checkboxes.nth(1)).not.toBeChecked();
await expect(checkboxes.nth(2)).not.toBeChecked();
await expect(checkboxes.nth(3)).not.toBeChecked();
await expect(checkboxes.nth(4)).not.toBeChecked();
await expect(checkboxes.nth(5)).not.toBeChecked();
});

test('checks all rows on header selection', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 0);
const actual = await getCheckboxValues(table);
const expected = ['true', 'true', 'true', 'true', 'true', 'true'];
expect(matchArrayElements(actual, expected)).toBe(true);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(0).click();
await expect(checkboxes.nth(0)).toBeChecked();
await expect(checkboxes.nth(1)).toBeChecked();
await expect(checkboxes.nth(2)).toBeChecked();
await expect(checkboxes.nth(3)).toBeChecked();
await expect(checkboxes.nth(4)).toBeChecked();
await expect(checkboxes.nth(5)).toBeChecked();
});

test('unchecks all rows on second header selection', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 0);
await clickCheckboxAtRowIndex(table, 0);
const actual = await getCheckboxValues(table);
const expected = ['false', 'false', 'false', 'false', 'false', 'false'];
expect(matchArrayElements(actual, expected)).toBe(true);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(0).click();
await checkboxes.nth(0).click();
await expect(checkboxes.nth(0)).not.toBeChecked();
await expect(checkboxes.nth(1)).not.toBeChecked();
await expect(checkboxes.nth(2)).not.toBeChecked();
await expect(checkboxes.nth(3)).not.toBeChecked();
await expect(checkboxes.nth(4)).not.toBeChecked();
await expect(checkboxes.nth(5)).not.toBeChecked();
});

test('unchecks all after row select, then header selection', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 1);
await clickCheckboxAtRowIndex(table, 0);
const actual = await getCheckboxValues(table);
const expected = ['false', 'false', 'false', 'false', 'false', 'false'];
expect(matchArrayElements(actual, expected)).toBe(true);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(1).click();
await checkboxes.nth(0).click();
await expect(checkboxes.nth(0)).not.toBeChecked();
await expect(checkboxes.nth(1)).not.toBeChecked();
await expect(checkboxes.nth(2)).not.toBeChecked();
await expect(checkboxes.nth(3)).not.toBeChecked();
await expect(checkboxes.nth(4)).not.toBeChecked();
await expect(checkboxes.nth(5)).not.toBeChecked();
});

test('does not check header if no rows in table', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 0);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(0).click();

const button = await page.$('button[aria-label="Approve"]');
const button = page.locator('button[aria-label="Approve"]');
await button.click();

const actual = await getCheckboxValues(table);
const expected = ['false'];
expect(matchArrayElements(actual, expected)).toBe(true);
await expect(checkboxes).toHaveCount(1);
await expect(checkboxes.nth(0)).not.toBeChecked();
});

test('calls onSelectionChange on selection changes', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 1);
await clickCheckboxAtRowIndex(table, 2);
await clickCheckboxAtRowIndex(table, 3);
const count = await page.$eval('#selection-change-count', (el) => el.textContent);
expect(count).toBe('selection change count: 3');
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(1).click();
await checkboxes.nth(2).click();
await checkboxes.nth(3).click();
const count = page.locator('#selection-change-count');
await expect(count).toHaveText('selection change count: 3');
});

test('avoids sort on header check', async ({ page }) => {
const index = 0;
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');

const before = await getCellContentsAtColumnIndex(page, COLUMN_COUNT, index);
expect(matchArrayElements(before, ['1', '2', '3', '4', '5'])).toBe(true);
expect(before).toEqual(['1', '2', '3', '4', '5']);

await clickCheckboxAtRowIndex(table, 0);
await checkboxes.nth(0).click();
const after = await getCellContentsAtColumnIndex(page, COLUMN_COUNT, index);
expect(matchArrayElements(after, ['1', '2', '3', '4', '5'])).toBe(true);
expect(after).toEqual(['1', '2', '3', '4', '5']);
});

test('calls batch action onClick with selected rows', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 1);
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(1).click();

const button = await page.$('button[aria-label="Approve"]');
const button = page.locator('button[aria-label="Approve"]');
await button.click();

const actual = await getCheckboxValues(table);
const expected = ['false', 'false', 'false', 'false', 'false'];
expect(matchArrayElements(actual, expected)).toBe(true);
expect(checkboxes).toHaveCount(5);
await expect(checkboxes.nth(0)).not.toBeChecked();
await expect(checkboxes.nth(1)).not.toBeChecked();
await expect(checkboxes.nth(2)).not.toBeChecked();
await expect(checkboxes.nth(3)).not.toBeChecked();
await expect(checkboxes.nth(4)).not.toBeChecked();
});

test('batch action clearSelection clears selected rows', async ({ page }) => {
await mount(page, 'data-table--batch-action');
const table = await getTable(page);
await clickCheckboxAtRowIndex(table, 1);

const beforeActual = await getCheckboxValues(table);
const beforeExpected = ['true', 'true', 'false', 'false', 'false', 'false'];
expect(matchArrayElements(beforeActual, beforeExpected)).toBe(true);

const button = await page.$('button[aria-label="Flag"]');
const table = await getTableLocator(page);
const checkboxes = table.locator('label[data-baseweb="checkbox"]');
await checkboxes.nth(1).click();

await expect(checkboxes.nth(0)).toBeChecked();
await expect(checkboxes.nth(1)).toBeChecked();
await expect(checkboxes.nth(2)).not.toBeChecked();
await expect(checkboxes.nth(3)).not.toBeChecked();
await expect(checkboxes.nth(4)).not.toBeChecked();
await expect(checkboxes.nth(5)).not.toBeChecked();

const button = page.locator('button[aria-label="Flag"]');
await button.click();

const afterActual = await getCheckboxValues(table);
const afterExpected = ['false', 'false', 'false', 'false', 'false', 'false'];
expect(matchArrayElements(afterActual, afterExpected)).toBe(true);
await expect(checkboxes.nth(0)).not.toBeChecked();
await expect(checkboxes.nth(1)).not.toBeChecked();
await expect(checkboxes.nth(2)).not.toBeChecked();
await expect(checkboxes.nth(3)).not.toBeChecked();
await expect(checkboxes.nth(4)).not.toBeChecked();
await expect(checkboxes.nth(5)).not.toBeChecked();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/

import { expect, test } from '@playwright/test';
import { mount } from '../../test/integration';
import { getCellContentsAtColumnIndex } from './utilities';

const COLUMN_COUNT = 2;

test.describe('data-table imperative clear selection', () => {
test('clears selection outside of batch action', async ({ page }) => {
await mount(page, 'data-table--imperative-clear-selection');
const batchActionRemoveButton = page.locator('text=Remove selected rows');
const rowActionRemoveButton = page.locator('button[title="Remove row"]');
const firstRowCheckbox = page.locator('[data-baseweb="checkbox"]').nth(1);

await expect(batchActionRemoveButton).toBeHidden();
await expect(rowActionRemoveButton).toBeHidden();
const initialActual = await getCellContentsAtColumnIndex(page, COLUMN_COUNT, 0);
const initialExpected = ['1', '2', '3', '4', '5'];
expect(initialActual).toEqual(initialExpected);

await firstRowCheckbox.click();
await batchActionRemoveButton.click();
await expect(batchActionRemoveButton).toBeHidden();
const batchActual = await getCellContentsAtColumnIndex(page, COLUMN_COUNT, 0);
expect(batchActual.slice(0, 4)).toEqual(initialExpected.slice(1));

await firstRowCheckbox.click();
await expect(batchActionRemoveButton).toBeVisible();
await rowActionRemoveButton.click();
const rowActual = await getCellContentsAtColumnIndex(page, COLUMN_COUNT, 0);
expect(rowActual.slice(0, 3)).toEqual(initialExpected.slice(2));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import * as React from 'react';

import Check from '../../icon/check';

import NumericalColumn from '../column-numerical';
import { StatefulDataTable } from '../stateful-data-table';

type RowData = number;

const columns = [
NumericalColumn({
title: 'row-id',
mapDataToValue: (data: RowData) => data,
}),

NumericalColumn({
title: 'filler',
mapDataToValue: (data: RowData) => data,
}),
];

export function Scenario() {
const controlRef = React.useRef(null);
const [rows, setRows] = React.useState([
{ id: 1, data: 1 },
{ id: 2, data: 2 },
{ id: 3, data: 3 },
{ id: 4, data: 4 },
{ id: 5, data: 5 },
]);

function removeRows(ids) {
const nextRows = rows.filter((row) => !ids.includes(row.id));
setRows(nextRows);
}

const batchActions = [
{
label: 'Remove selected rows',
onClick: ({ selection, clearSelection }) => {
removeRows(selection.map((r) => r.id));
clearSelection();
},
},
];

const rowActions = [
{
label: 'Remove row',
onClick: ({ row }) => {
removeRows([row.id]);
if (controlRef.current) {
console.log('here');
controlRef.current.clearSelection();
}
},
renderIcon: function RenderCheck({ size }) {
return <Check size={size} />;
},
},
];

return (
<div>
<div style={{ height: '400px', width: '900px' }}>
<StatefulDataTable
batchActions={batchActions}
rowActions={rowActions}
controlRef={controlRef}
columns={columns}
rows={rows}
/>
</div>
</div>
);
}
Loading

0 comments on commit 5dd8468

Please sign in to comment.