grafana/e2e-playwright/panels-suite/table-kitchenSink.spec.ts

467 lines
20 KiB
TypeScript

import { Page, Locator } from '@playwright/test';
import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e';
import { getCell, getCellHeight, getColumnIdx } from './table-utils';
const DASHBOARD_UID = 'dcb9f5e9-8066-4397-889e-864b99555dbb';
test.use({ viewport: { width: 2000, height: 1080 } });
// helper utils
const waitForTableLoad = async (loc: Page | Locator) => {
await expect(loc.locator('.rdg')).toBeVisible();
};
const disableAllTextWrap = async (loc: Page | Locator, selectors: E2ESelectorGroups) => {
// disable text wrapping for all of the columns, since long text with links in them can push the links off the screen.
const wrapTextToggle = loc.getByLabel('Wrap text');
const count = await wrapTextToggle.count();
for (let i = 0; i < count; i++) {
const toggle = wrapTextToggle.nth(i);
if ((await toggle.getAttribute('checked')) !== null) {
await toggle.click({ force: true });
}
}
};
test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] }, () => {
test('Tests word wrap, hover overflow, max cell height, and cell inspect', async ({
gotoDashboardPage,
selectors,
page,
}) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
// to avoid a race condition when counting up , wait for react-data-grid to finish rendering.
await waitForTableLoad(page);
const longTextColIdx = await getColumnIdx(page, 'Long Text');
// text wrapping is enabled by default on this panel.
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100);
// set a max row height, watch the height decrease, then clear it to continue.
const maxRowHeightInput = page.getByLabel('Max row height').last();
await maxRowHeightInput.fill('80');
await expect(async () => {
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
}).toPass();
await maxRowHeightInput.clear();
// toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks.
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.group('panel-options-override-12'))
.getByLabel('Wrap text')
.click({ force: true });
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// test that hover overflow works.
const loremIpsumCell = await getCell(page, 1, longTextColIdx);
await loremIpsumCell.scrollIntoViewIfNeeded();
await loremIpsumCell.hover();
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100);
await (await getCell(page, 1, longTextColIdx + 1)).hover();
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// enable cell inspect, confirm that hover no longer triggers.
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Cell options Cell value inspect'))
.first()
.getByRole('switch', { name: 'Cell value inspect' })
.click({ force: true });
await loremIpsumCell.hover();
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// click cell inspect, check that cell inspection pops open in the side as we'd expect.
await loremIpsumCell.getByLabel('Inspect value').click();
const loremIpsumText = await loremIpsumCell.textContent();
expect(loremIpsumText).toBeDefined();
await expect(page.getByRole('dialog').getByText(loremIpsumText!)).toBeVisible();
});
test('Tests visibility and display name via overrides', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
const stateOverride = dashboardPage.getByGrafanaSelector(
selectors.components.OptionsGroup.group('panel-options-override-11')
);
// confirm that "State" column is hidden by default.
expect(page.getByRole('row').nth(0)).not.toContainText('State');
// toggle the "State" column visibility via the override we set up in the kitchen sink panel.
const hideStateColumnSwitch = stateOverride.locator('label').last();
await hideStateColumnSwitch.click();
expect(page.getByRole('row').nth(0)).toContainText('State');
// now change the display name of the "State" column.
const displayNameInput = stateOverride.locator('input[value="State"]').last();
await displayNameInput.fill('State (renamed)');
await displayNameInput.press('Enter');
expect(page.getByRole('row').nth(0)).toContainText('State (renamed)');
// toggle the "State" column visibility again to hide it again. this confirms that we avoid bugs related to
// array lengths between the fields array and the column widths array.
await hideStateColumnSwitch.click();
expect(page.getByRole('row').nth(0)).not.toContainText('State');
// since the previous assertion is just for the absence of text, let's also confirm that the table is
// actually still on the page and that an error has not been throw.
await waitForTableLoad(page);
});
// we test niche cases for sorting, filtering, pagination, etc. in a unit tests already.
// we mainly want to test the happiest paths for these in e2es as well to check for integration
// issues, but the unit tests can confirm that the internal logic works as expected much more quickly and thoroughly.
// hashtag testing pyramid.
test('Tests sorting by column', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
// click the "State" column header to sort it.
const stateColumnHeader = await getCell(page, 0, 1);
await stateColumnHeader.getByText('Info').click();
await expect(stateColumnHeader).toHaveAttribute('aria-sort', 'ascending');
expect(getCell(page, 1, 1)).resolves.toContainText('down'); // down or down fast
await stateColumnHeader.getByText('Info').click();
await expect(stateColumnHeader).toHaveAttribute('aria-sort', 'descending');
expect(getCell(page, 1, 1)).resolves.toContainText('up'); // up or up fast
await stateColumnHeader.getByText('Info').click();
await expect(stateColumnHeader).not.toHaveAttribute('aria-sort');
});
test('Tests filtering within a column', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
await waitForTableLoad(page);
const infoColumnIdx = await getColumnIdx(page, 'Info');
const stateColumnHeader = page.getByRole('columnheader').nth(infoColumnIdx);
// get the first value in the "State" column, filter it out, then check that it went away.
const firstStateValue = (await (await getCell(page, 1, infoColumnIdx)).textContent())!;
await stateColumnHeader.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.HeaderButton).click();
const filterContainer = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Visualization.TableNG.Filters.Container
);
await expect(filterContainer).toBeVisible();
// select all, then click the first value to unselect it, filtering it out.
await filterContainer.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.SelectAll).click();
await filterContainer.getByTitle(firstStateValue, { exact: true }).locator('label').click();
await filterContainer.getByRole('button', { name: 'Ok' }).click();
// make sure the filter container closed when we clicked "Ok".
await expect(filterContainer).not.toBeVisible();
// did it actually filter out our value?
await expect(getCell(page, 1, infoColumnIdx)).resolves.not.toHaveText(firstStateValue);
});
test('Tests pagination, row height adjustment', async ({ gotoDashboardPage, selectors, page }) => {
const rowRe = /([\d]+) - ([\d]+) of ([\d]+) rows/;
const getRowStatus = async (page: Page | Locator) => {
const text = (await page.getByText(rowRe).textContent()) ?? '';
const match = text.match(rowRe);
return {
start: parseInt(match?.[1] ?? '0', 10),
end: parseInt(match?.[2] ?? '0', 10),
total: parseInt(match?.[3] ?? '0', 10),
};
};
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
await page
.getByLabel(selectors.components.PanelEditor.OptionsPane.fieldLabel(`Enable pagination`), { exact: true })
.click();
// because of text wrapping, we're guaranteed to only be showing a single row when we enable pagination.
await expect(page.getByText(/([\d]+) - ([\d]+) of ([\d]+) rows/)).toBeVisible();
await disableAllTextWrap(page, selectors);
// any number of rows that is not "1" is allowed here, we don't want to police the exact number of rows that
// are rendered since there are tons of factors which could effect this. we do want to grab this number for comparison
// in a second, though.
const smallRowStatus = await getRowStatus(page);
expect(smallRowStatus.end).toBeGreaterThan(1);
expect(page.getByRole('grid').getByRole('row')).toHaveCount(smallRowStatus.end + 2); // +2 for header and footer rows
// change cell height to Large
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Cell height'))
.locator('input')
.last()
.click();
const largeRowStatus = await getRowStatus(page);
expect(largeRowStatus.end).toBeLessThan(smallRowStatus.end);
expect(page.getByRole('grid').getByRole('row')).toHaveCount(largeRowStatus.end + 2); // +2 for header and footer rows
// click a page over with the directional nav
await page.getByLabel('next page').click();
const nextPageStatus = await getRowStatus(page);
expect(nextPageStatus.start).toBe(largeRowStatus.end + 1);
expect(nextPageStatus.end).toBe(largeRowStatus.end * 2);
expect(nextPageStatus.total).toBe(largeRowStatus.total);
// click a page number
await page.getByTestId('data-testid panel content').getByRole('navigation').getByText('4', { exact: true }).click();
const fourthPageStatus = await getRowStatus(page);
expect(fourthPageStatus.start).toBe(largeRowStatus.end * 3 + 1);
expect(fourthPageStatus.end).toBe(largeRowStatus.end * 4);
expect(fourthPageStatus.total).toBe(largeRowStatus.total);
});
test.skip('Tests DataLinks (single and multi) and actions', async ({ gotoDashboardPage, selectors, page }) => {
const addDataLink = async (title: string, url: string) => {
await dashboardPage
.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Data links and actions Data links')
)
.locator('button')
.filter({ hasText: 'Add link' })
.click();
// DataLinks dialog has popped open - fill it in and add a global datalink.
await expect(page.getByRole('dialog')).toBeVisible();
await page.getByRole('dialog').locator('#link-title').fill(title);
await page.getByRole('dialog').locator('#data-link-input [contenteditable="true"]').focus();
await page.getByRole('dialog').locator('#data-link-input [contenteditable="true"]').fill(url);
await page.getByRole('dialog').locator('#data-link-input [contenteditable="true"]').blur();
await page.getByRole('dialog').locator('button[aria-disabled="false"]').filter({ hasText: 'Save' }).click();
await expect(page.getByRole('dialog')).not.toBeVisible();
};
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
await disableAllTextWrap(page, selectors);
const infoColumnIdx = await getColumnIdx(page, 'Info');
const pillColIdx = await getColumnIdx(page, 'Pills');
const dataLinkColIdx = await getColumnIdx(page, 'Data Link');
// Info column has a single DataLink by default.
const infoCell = await getCell(page, 1, infoColumnIdx);
await expect(infoCell.locator('a')).toBeVisible();
expect(infoCell.locator('a')).toHaveAttribute('href');
expect(infoCell.locator('a')).not.toHaveAttribute('aria-haspopup');
// now, add a DataLink to the whole table
await addDataLink('Test link', 'https://grafana.com');
// add a DataLink to the whole table, all cells will now have a single link.
const colCount = await page.getByRole('row').nth(1).getByRole('gridcell').count();
for (let colIdx = 0; colIdx < colCount; colIdx++) {
// - pills column currently does not support DataLinks.
// - we don't apply DataLinks to the DataLinks column itself, since they're rendered inside.
if (colIdx === pillColIdx || colIdx === dataLinkColIdx) {
continue;
}
const cell = await getCell(page, 1, colIdx);
await expect(cell.locator('a')).toBeVisible();
expect(cell.locator('a')).toHaveAttribute('href');
expect(cell.locator('a')).not.toHaveAttribute('aria-haspopup', 'menu');
}
const headerContainer = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer);
// add another data link. now we'll check that the multi-link popups work.
await addDataLink('Another test link', 'https://grafana.com/foo');
// loop thru the columns, click the links, observe that the tooltip appears, and close the tooltip.
for (let colIdx = 0; colIdx < colCount; colIdx++) {
const cell = await getCell(page, 1, colIdx);
if (colIdx === infoColumnIdx) {
// the Info column should still have its single link.
expect(cell.locator('a')).not.toHaveAttribute('aria-haspopup', 'menu');
continue;
}
// - pills column currently does not support DataLinks.
// - we don't apply DataLinks to the DataLinks column itself, since they're rendered inside.
if (colIdx === pillColIdx || colIdx === dataLinkColIdx) {
continue;
}
await cell.locator('a').click({ force: true });
await expect(page.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).toBeVisible();
await headerContainer.click(); // convenient just to click the header to close the tooltip.
await expect(page.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeVisible();
}
// add an Action to the whole table and check that the action button is added to the tooltip.
// TODO -- saving for another day.
});
test('Tests tooltip interactions', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
const firstCaret = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Caret)
.first();
// test hovering over and blurring the caret, and whether the tooltip appears and disappears as expected.
await firstCaret.hover();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
).toBeVisible();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')).hover();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
).not.toBeVisible();
// when a pinned tooltip is open, clicking outside of it should close it.
await firstCaret.click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
).toBeVisible();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
).not.toBeVisible();
// when a pinned tooltip is open, clicking inside of it should NOT close it.
await firstCaret.click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
).toBeVisible();
const tooltip = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper
);
await tooltip.click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
).toBeVisible();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
).not.toBeVisible();
});
test('Styling overrides with styling from field', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).toBeVisible();
await waitForTableLoad(page);
const infoColumnIdx = await getColumnIdx(page, 'Info');
const dataLinkColumnIdx = await getColumnIdx(page, 'Data Link');
const stateColumnHeader = page.getByRole('columnheader').nth(infoColumnIdx);
// filter to only "Up," which we have a style override on.
await stateColumnHeader.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.HeaderButton).click();
const filterContainer = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Visualization.TableNG.Filters.Container
);
await expect(filterContainer).toBeVisible();
await filterContainer.getByTitle('up', { exact: true }).locator('label').click();
await filterContainer.getByRole('button', { name: 'Ok' }).click();
const cell = await getCell(page, 1, dataLinkColumnIdx);
await expect(cell).toBeVisible();
await expect(cell).toHaveCSS('text-decoration', /line-through/);
// now filter out "up," and confirm that the style override isn't present.
await stateColumnHeader.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.HeaderButton).click();
await expect(filterContainer).toBeVisible();
// select all, then click the first value to unselect it, filtering it out.
await filterContainer.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.SelectAll).click();
await filterContainer.getByTitle('up', { exact: true }).locator('label').click();
await filterContainer.getByRole('button', { name: 'Ok' }).click();
await expect(cell).toBeVisible();
await expect(cell).not.toHaveCSS('text-decoration', /line-through/);
});
test('Empty Table panel', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '3' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
).not.toBeVisible();
});
});