Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import PivotGrid from 'devextreme-testcafe-models/pivotGrid';
import { Selector } from 'testcafe';
import url from '../../helpers/getPageUrl';
import { createWidget } from '../../helpers/createWidget';
import { a11yCheck } from '../../helpers/accessibility/utils';

fixture.disablePageReloads`PivotGrid - Accessibility`
.page(url(__dirname, '../container.html'));

const PIVOT_GRID_SELECTOR = '#container';

const createConfig = () => ({
width: 1000,
allowSorting: true,
allowSortingBySummary: true,
allowFiltering: true,
allowExpandAll: true,
showBorders: true,
fieldPanel: {
visible: true,
},
fieldChooser: {
enabled: true,
height: 500,
},
dataSource: {
fields: [{
dataField: 'country',
area: 'filter',
}, {
caption: 'Region',
width: 120,
dataField: 'region',
area: 'row',
}, {
caption: 'City',
dataField: 'city',
width: 150,
area: 'row',
}, {
caption: 'Country',
dataField: 'country',
area: 'column',
}, {
dataField: 'date',
dataType: 'date',
area: 'column',
}, {
groupName: 'date',
groupInterval: 'year',
expanded: true,
area: 'column',
}, {
caption: 'Sales',
dataField: 'amount',
dataType: 'number',
summaryType: 'sum',
area: 'data',
}],
store: [{
region: 'Africa',
country: 'Egypt',
city: 'Cairo',
amount: 500,
date: new Date('2015-05-26'),
}, {
region: 'South America',
country: 'Argentina',
city: 'Buenos Aires',
amount: 780,
date: new Date('2015-05-07'),
}],
},
});

test('grid with field panel', async (t) => {
await a11yCheck(t, {}, PIVOT_GRID_SELECTOR);
}).before(async () => createWidget('dxPivotGrid', createConfig()));

test('field chooser popup', async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);

await t.click(pivotGrid.getFieldChooserButton());

await a11yCheck(t);
}).before(async () => createWidget('dxPivotGrid', createConfig()));

test('header filter popup', async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);
const filterIcon = pivotGrid
.getColumnHeaderArea()
.getHeaderFilterIcon()
.element;

await t.click(filterIcon);

await a11yCheck(t);
}).before(async () => createWidget('dxPivotGrid', createConfig()));

test('cell context menu', async (t) => {
const columnHeaderCell = Selector('.dx-pivotgrid-horizontal-headers td[tabindex]');

await t.rightClick(columnHeaderCell);

await a11yCheck(t);
}).before(async () => createWidget('dxPivotGrid', createConfig()));
81 changes: 81 additions & 0 deletions e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,32 @@ const createConfig = () => ({
? Selector(`.dx-pivotgridfieldchooser .dx-area-fields[group="${area}"]`)
: Selector(`.dx-pivotgrid-fields-area[group="${area}"]`));

test(`${testTitlePrefix}: Fields should be exposed as menu items of an area menubar`, async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);

if (isFieldChooser) {
await t.click(pivotGrid.getFieldChooserButton());
}

const rowAreaContainer = getAreaFieldsContainer('row');
const menubar = rowAreaContainer.find('[role="menubar"]');
const firstField = getField(pivotGrid, 'row', 0);

await t
.expect(menubar.count)
.eql(1, 'the area has a single menubar')
.expect(menubar.getAttribute('aria-label'))
.eql('Row Fields', 'the menubar is labelled with the area name')
.expect(menubar.getAttribute('aria-description'))
.ok('the menubar describes the available keyboard interactions');

await t
.expect(firstField.getAttribute('role'))
.eql('menuitem', 'a field is exposed as a menu item')
.expect(firstField.getAttribute('aria-label'))
.eql('Field: Region, Sort order: ascending', 'the field label includes the name and the sorting state');
}).before(async () => createWidget('dxPivotGrid', createConfig()));

['filter', 'data', 'column', 'row'].forEach((area) => {
test(`${testTitlePrefix}: Fields in ${area} area should form a single tab stop`, async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);
Expand Down Expand Up @@ -381,3 +407,58 @@ test('FieldChooser: Should traverse fields in all areas by tab', async (t) => {
.expect(dataFirstField.focused)
.ok('first field in data area is focused');
}).before(async () => createWidget('dxPivotGrid', createConfig()));

// TestCafe's pressKey does not support function keys (F1-F12), so the Shift+F10
// shortcut is reproduced by dispatching the keydown event on the focused field.
const SHIFT_F10_KEYDOWN = {
key: 'F10', code: 'F10', shiftKey: true, bubbles: true, cancelable: true,
};

test('PivotGrid: Should open the context menu by Shift+F10 on a field', async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);
const firstField = pivotGrid.getRowHeaderArea().getField(0);
const contextMenuItem = Selector('.dx-context-menu .dx-menu-item-text').withText('Show Field Chooser');

await t
.click(firstField)
.expect(firstField.focused)
.ok('field is focused after click')
.dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN)
.expect(contextMenuItem.visible)
.ok('the field context menu is shown after Shift+F10');
}).before(async () => createWidget('dxPivotGrid', createConfig()));

test('PivotGrid: Field should have focus after the context menu is closed', async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);
const firstField = pivotGrid.getRowHeaderArea().getField(0);
const contextMenuItem = Selector('.dx-context-menu .dx-menu-item-text').withText('Show Field Chooser');

await t
.click(firstField)
.dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN)
.expect(contextMenuItem.visible)
.ok('the field context menu is shown after Shift+F10');

await t
.pressKey('esc')
.expect(firstField.focused)
.ok('the field is focused after the context menu is closed');
}).before(async () => createWidget('dxPivotGrid', createConfig()));

test('FieldChooser: Shift+F10 on a popup field should not open the grid context menu', async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);
const fieldChooser = pivotGrid.getFieldChooser();
const contextMenu = Selector('.dx-context-menu .dx-menu-item-text');

await t.click(pivotGrid.getFieldChooserButton());

const firstField = fieldChooser.getRowAreaItem(0);

// Parity with the mouse: right-click on the popup fields does not show the
// grid context menu either.
await t
.click(firstField)
.dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN)
.expect(contextMenu.exists)
.notOk('the grid context menu is not shown for a field chooser popup field');
}).before(async () => createWidget('dxPivotGrid', createConfig()));
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, expect, it } from '@jest/globals';

import { describeDataCellsWithHeaders } from './data_cell_description';

const createSection = (tag: 'thead' | 'tbody', rowsHtml: string): HTMLTableSectionElement => {
const table = document.createElement('table');
table.innerHTML = `<${tag}>${rowsHtml}</${tag}>`;
return table.querySelector(tag) as HTMLTableSectionElement;
};

const getDescribingCells = (cell: HTMLTableCellElement | null): (Element | null)[] => (cell?.getAttribute('aria-describedby') ?? '')
.split(' ')
.filter((id) => !!id)
.map((id) => document.getElementById(id));

describe('describeDataCellsWithHeaders', () => {
it('should reference the column header path including colspan groups', () => {
const columns = createSection('thead', `
<tr><td id="group" colspan="2" aria-colindex="1">2015</td><td id="total" rowspan="2" aria-colindex="3">Grand Total</td></tr>
<tr><td id="q1" aria-colindex="1">Q1</td><td id="q2" aria-colindex="2">Q2</td></tr>
`);
const data = createSection('tbody', `
<tr><td aria-colindex="1" aria-rowindex="1">10</td><td aria-colindex="2" aria-rowindex="1">20</td><td aria-colindex="3" aria-rowindex="1">30</td></tr>
`);
document.body.append(columns, data);

describeDataCellsWithHeaders(columns, undefined, data);

const cells = data.querySelectorAll('td');
expect(getDescribingCells(cells[0])).toEqual([
document.getElementById('group'),
document.getElementById('q1'),
]);
expect(getDescribingCells(cells[1])).toEqual([
document.getElementById('group'),
document.getElementById('q2'),
]);
expect(getDescribingCells(cells[2])).toEqual([
document.getElementById('total'),
]);
});

it('should reference the row header path including rowspan groups', () => {
const rows = createSection('tbody', `
<tr><td id="africa" rowspan="2" aria-rowindex="1">Africa</td><td id="cairo" aria-rowindex="1">Cairo</td></tr>
<tr><td id="luxor" aria-rowindex="2">Luxor</td></tr>
`);
const data = createSection('tbody', `
<tr><td aria-colindex="1" aria-rowindex="1">10</td></tr>
<tr><td aria-colindex="1" aria-rowindex="2">20</td></tr>
`);
document.body.append(rows, data);

describeDataCellsWithHeaders(undefined, rows, data);

const cells = data.querySelectorAll('td');
expect(getDescribingCells(cells[0])).toEqual([
document.getElementById('africa'),
document.getElementById('cairo'),
]);
expect(getDescribingCells(cells[1])).toEqual([
document.getElementById('africa'),
document.getElementById('luxor'),
]);
});

it('should combine row and column paths in the row-then-column order', () => {
const columns = createSection('thead', '<tr><td id="c" aria-colindex="1">2015</td></tr>');
const rows = createSection('tbody', '<tr><td id="r" aria-rowindex="1">Africa</td></tr>');
const data = createSection('tbody', '<tr><td aria-colindex="1" aria-rowindex="1">10</td></tr>');
document.body.append(columns, rows, data);

describeDataCellsWithHeaders(columns, rows, data);

expect(getDescribingCells(data.querySelector('td'))).toEqual([
document.getElementById('r'),
document.getElementById('c'),
]);
});

it('should respect the virtual scrolling index offset', () => {
const columns = createSection('thead', '<tr><td id="c5" aria-colindex="5">2015</td><td id="c6" aria-colindex="6">2016</td></tr>');
const data = createSection('tbody', '<tr><td aria-colindex="5" aria-rowindex="1">10</td><td aria-colindex="6" aria-rowindex="1">20</td></tr>');
document.body.append(columns, data);

describeDataCellsWithHeaders(columns, undefined, data);

const cells = data.querySelectorAll('td');
expect(getDescribingCells(cells[0])).toEqual([document.getElementById('c5')]);
expect(getDescribingCells(cells[1])).toEqual([document.getElementById('c6')]);
});

it('should reuse existing header cell ids and keep generated ones stable across runs', () => {
const columns = createSection('thead', '<tr><td id="existing" aria-colindex="1">2015</td><td aria-colindex="2">2016</td></tr>');
const data = createSection('tbody', '<tr><td aria-colindex="1" aria-rowindex="1">10</td><td aria-colindex="2" aria-rowindex="1">20</td></tr>');
document.body.append(columns, data);

describeDataCellsWithHeaders(columns, undefined, data);
const generatedId = (columns.querySelectorAll('td')[1]).id;
describeDataCellsWithHeaders(columns, undefined, data);

expect((columns.querySelectorAll('td')[0]).id).toBe('existing');
expect((columns.querySelectorAll('td')[1]).id).toBe(generatedId);
expect(generatedId).toMatch(/^dx-/);
});

it('should remove a stale description when no header chains match', () => {
const data = createSection('tbody', '<tr><td aria-colindex="1" aria-rowindex="1" aria-describedby="stale">10</td></tr>');
document.body.append(data);

describeDataCellsWithHeaders(undefined, undefined, data);

expect(data.querySelector('td')?.hasAttribute('aria-describedby')).toBe(false);
});
});
Loading
Loading