mirror of https://github.com/grafana/grafana.git
382 lines
16 KiB
TypeScript
382 lines
16 KiB
TypeScript
import { Page, Locator } from '@playwright/test';
|
|
|
|
import { test, expect } from '@grafana/plugin-e2e';
|
|
|
|
const DASHBOARD_UID = 'dcb9f5e9-8066-4397-889e-864b99555dbb';
|
|
|
|
test.use({
|
|
viewport: { width: 2000, height: 1080 },
|
|
featureToggles: {
|
|
tableNextGen: true,
|
|
},
|
|
});
|
|
|
|
// helper utils
|
|
const waitForTableLoad = async (loc: Page | Locator) => {
|
|
await expect(loc.locator('.rdg')).toBeVisible();
|
|
};
|
|
|
|
const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) =>
|
|
loc
|
|
.getByRole('row')
|
|
.nth(rowIdx)
|
|
.getByRole(rowIdx === 0 ? 'columnheader' : 'gridcell')
|
|
.nth(colIdx);
|
|
|
|
const getCellHeight = async (loc: Page | Locator, rowIdx: number, colIdx: number) => {
|
|
const cell = await getCell(loc, rowIdx, colIdx);
|
|
return (await cell.boundingBox())?.height ?? 0;
|
|
};
|
|
|
|
const getColumnIdx = async (loc: Page | Locator, columnName: string) => {
|
|
// find the index of the column "Long text." The kitchen sink table will change over time, but
|
|
// we can just find the column programatically and use it throughout the test.
|
|
let result = -1;
|
|
const colCount = await loc.getByRole('columnheader').count();
|
|
for (let colIdx = 0; colIdx < colCount; colIdx++) {
|
|
const cell = await getCell(loc, 0, colIdx);
|
|
if ((await cell.textContent()) === columnName) {
|
|
result = colIdx;
|
|
break;
|
|
}
|
|
}
|
|
if (result === -1) {
|
|
throw new Error(`Could not find the "${columnName}" column in the table`);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
test.describe(
|
|
'Panels test: Table - Kitchen Sink',
|
|
{
|
|
tag: ['@panels'],
|
|
},
|
|
() => {
|
|
test('Tests word wrap, hover overflow, 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);
|
|
|
|
// toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks.
|
|
await dashboardPage
|
|
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text'))
|
|
.last()
|
|
.click();
|
|
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()
|
|
.locator('label[for="custom.inspect"]')
|
|
.click();
|
|
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();
|
|
|
|
// confirm that "State" column is hidden by default.
|
|
expect(page.getByRole('row').nth(0)).not.toContainText('State');
|
|
|
|
// toggle the "State" column visibility and test that it appears before re-hiding it.
|
|
// FIXME this selector is utterly godawful, but there's no way to give testIds or aria-labels or anything to
|
|
// the panel editor builder. we should fix that to make e2e's easier to write for our team.
|
|
const hideStateColumnSwitch = page.locator('[id="Override 12"]').locator('label').last();
|
|
await hideStateColumnSwitch.click();
|
|
expect(page.getByRole('row').nth(0)).toContainText('State');
|
|
|
|
// now change the display name of the "State" column.
|
|
// FIXME it would be good to have a better selector here too.
|
|
const displayNameInput = page.locator('[id="Override 12"]').locator('input[value="State"]').last();
|
|
await displayNameInput.fill('State (renamed)');
|
|
await displayNameInput.press('Enter');
|
|
expect(page.getByRole('row').nth(0)).toContainText('State (renamed)');
|
|
});
|
|
|
|
// 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();
|
|
|
|
// disable text wrap and see the number of rows.
|
|
await dashboardPage
|
|
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text'))
|
|
.last()
|
|
.click();
|
|
|
|
// 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 + 1);
|
|
|
|
// 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 + 1);
|
|
|
|
// 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('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();
|
|
|
|
// disable text wrapping for this test to make it easier to click the links, the long lorem ipsum
|
|
// can push the links off the screen.
|
|
await dashboardPage
|
|
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text'))
|
|
.last()
|
|
.click();
|
|
|
|
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('Empty Table panel', async ({ gotoDashboardPage, selectors }) => {
|
|
const dashboardPage = await gotoDashboardPage({
|
|
uid: DASHBOARD_UID,
|
|
queryParams: new URLSearchParams({ editPanel: '2' }),
|
|
});
|
|
|
|
await expect(
|
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage)
|
|
).toBeVisible();
|
|
await expect(
|
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
|
|
).not.toBeVisible();
|
|
});
|
|
}
|
|
);
|