E2E: migrate to playwright (#107241)

* separate playwright into its own folder

* better separation

* add login scenario, add tags

* remove ui option

* update CODEOWNERS

* add a panels suite test

* migrate queries test to playwright

* rename + add dashlist test

* add panelEdit_base

* add geomap map controls test

* add geomap-layer-types

* add geomap-spatial-operations tests

* add frontend-sandbox-panel tests

* add smoke-tests-suite

* add comment about adding datasource

* add dashboard-browse-nested

* add dashboard-browse

* add dashboard-export-json

* add dashboard-keybindings test

* remove @wip tag

* turn on screenshots and add comment for why this test fails

* add dashboard-links-without-slug test

* try adding permissions in the test as well

* add dashboard-live-streaming

* context in the test doesn't work - sad

* create dashboard-public-templating

* add dashboard-public-create and make live streaming more resilient

* add share externally test

* add dashboard-share-internally

* add share-snapshot-create test

* add dashboard-templating

* add timepicker tests

* add embedded-dashboard test

* add general_dashboards test

* add import-dashboard test

* add load-options-from-url test

* add new-constant-variable test

* add custom-variable test

* add new-datasource-variable test

* add new-interval-variable test

* add text-box-variable test

* add new-query-variable test

* add horizontal repeat test

* add panel-vertical-repeat test

* add empty-row-repeat test

* add set-options-from-ui test

* add snapshot-create test

* add templating test

* add textbox-variables test

* add cloud-plugins-suite

* add storybook verification tests

* add playwright storybook verification workflow

* add playwright browsers

* update CODEOWNERS

* test change to trigger storybook verification workflows

* try container instead

* get the version right...

* go back to installing - less chance of forgetting to update

* Basic Github Actions

Squashed commit of the following:

commit f84c650a71
Author: joshhunt <josh.hunt@grafana.com>
Date:   Tue Jul 1 13:23:46 2025 +0100

    add arg for sharding, but not using it yet

commit 7bcf0512c6
Author: joshhunt <josh.hunt@grafana.com>
Date:   Tue Jul 1 12:30:30 2025 +0100

    less newline

commit b643911882
Author: joshhunt <josh.hunt@grafana.com>
Date:   Tue Jul 1 12:24:31 2025 +0100

    less logs

commit 38f871e9c2
Author: joshhunt <josh.hunt@grafana.com>
Date:   Tue Jul 1 10:00:26 2025 +0100

    fix yaml

commit db9a773136
Author: joshhunt <josh.hunt@grafana.com>
Date:   Tue Jul 1 09:57:47 2025 +0100

    clean up files

commit c0525f41fa
Author: joshhunt <josh.hunt@grafana.com>
Date:   Tue Jul 1 09:44:56 2025 +0100

    gha workflow

commit 895bea7c52
Author: joshhunt <josh.hunt@grafana.com>
Date:   Mon Jun 30 19:33:08 2025 +0100

    working dagger

commit cea1f84437
Author: joshhunt <josh.hunt@grafana.com>
Date:   Mon Jun 30 16:17:46 2025 +0100

    wip

* shard gha

* some tidy up

* add flags for exporting results, and a gha step to merge runs

* fix shard gha

* add dashboard-duplicate-panel test

* add dashboard-outline test

* add dashboards-add-panel

* remove some commented out code

* add dashboards-title-description test

* add dashboards-remove-panel

* don't install cypress

* gha: check playwright results

* add dashboards-edit-adhoc-variables test

* fix check-jobs

* add dagger cloud token

* add dagger cloud token

* add edit-datasource-variable test

* update CODEOWNERS

* add dashboards-edit-group-by-variables (skipped for now)

* add dashboards-edit-panel-title-description test

* add dashboards-edit-transparent-bg test

* add dashboards-edit-query-variables test

* run with 8 shards

* add dashboards-edit-variables

* tidy up gha

* add dashboard-group-panels

* fix action

* try to cache the grafana build

* fix missing action becuase no checkout, use builtin continue-on-error instead

* fix missing id

* cat out.txt

* debug build cache

* fix debug build cache

* add dashboards-panel-layouts test

* tidy up

* no more debug

* fix grafana dir

* add dashboards-move-panel test

* skip some failing tests

* mark up plugins tests with @plugins tag, only run @plugins tests in drone

* Hackathon/Playwright Conversion - Various Suite (#107516)

* Playwright Migration: Various Suite tests

* skipping bad tests

* fix some tests that can fail

* fix uid

* separate user for the verify-i18n test

* build test plugins for grafana server

* properly blur input fields

* login manually

* get dashboardPage from goto

* ignore a couple of type assertions

* remove a couple of timeouts

* remove timeouts on dashboard-share-internally

* use toBeHidden

* make dashboard-share-internally more stable

* remove TEMP_DAGGER_TOKEN

* clean up visaulization-suggestions

* unskip gauge test

* unskip trace-view-scrolling

* attempt to make create variable utils stable

* unskip loki tests

* make go linter happy

* unskip edit-group-by-variables test

* unskip move panel tests

* isolate dashboard-timepicker tests with separate user

* create data source as part of smoke test

* make sure we're awaiting in dashboard-edit-adhoc-variables

* make dashboards-edit-variables test more robust

* Hackathon Playwright: Dashboards Search (#107580)

* Hackathon Playwright: Dashboards Search

* Feedback changes

* make trace-view-scrolling more stable

* add json report and bench step

* fix bench version

* move fail step to after the playwright report so we can report test failures

* fix output file name

* fix typo

* try wrap in expect.poll

* stability

* bit more tidy up

* fix dashboard-new-layouts tests

* move test-plugins to e2e-playwright

* fix go code for drone e2e run

* move loki plugin-e2e test

* make v2 dashboards work again

---------

Co-authored-by: joshhunt <josh.hunt@grafana.com>
Co-authored-by: Josh Hunt <joshhunt@users.noreply.github.com>
Co-authored-by: Collin Fingar <collin.fingar@grafana.com>
Co-authored-by: Jeff Levin <jeff@levinology.com>
This commit is contained in:
Ashley Harrison
2025-07-11 10:31:33 +01:00
committed by GitHub
co-authored by joshhunt Josh Hunt Collin Fingar Jeff Levin
parent ea0ddb3fc9
commit b6580ccb10
250 changed files with 22677 additions and 1038 deletions
@@ -0,0 +1,544 @@
import { readFileSync } from 'fs';
import { load } from 'js-yaml';
import { Page } from 'playwright-core';
import { v4 as uuidv4 } from 'uuid';
import {
test,
expect,
CreateDataSourcePageArgs,
DataSourceConfigPage,
E2ESelectorGroups,
DashboardPage,
} from '@grafana/plugin-e2e';
import { AzureQueryType } from '../../public/app/plugins/datasource/azuremonitor/dataquery.gen';
import { selectors as azMonSelectors } from '../../public/app/plugins/datasource/azuremonitor/e2e/selectors';
import {
AzureMonitorDataSourceJsonData,
AzureMonitorDataSourceSecureJsonData,
} from '../../public/app/plugins/datasource/azuremonitor/types/types';
const provisioningPath = 'provisioning/datasources/azmonitor-ds.yaml';
type AzureMonitorConfig = {
secureJsonData: AzureMonitorDataSourceSecureJsonData;
jsonData: AzureMonitorDataSourceJsonData;
};
type AzureMonitorProvision = { datasources: AzureMonitorConfig[] };
const dataSourceName = `Azure Monitor E2E Tests - ${uuidv4()}`;
const storageAcctName = 'azmonteststorage';
const logAnalyticsName = 'az-mon-test-logs';
const applicationInsightsName = 'az-mon-test-ai-a';
const rootSubscription = 'grafanalabs-datasources-dev';
async function provisionAzureMonitorDatasources(
createDataSourceConfigPage: (args: CreateDataSourcePageArgs) => Promise<DataSourceConfigPage>,
datasourceConfig: AzureMonitorConfig,
page: Page,
selectors: E2ESelectorGroups
) {
const configPage = await createDataSourceConfigPage({
type: 'grafana-azure-monitor-datasource',
name: dataSourceName,
});
const azureCloudInput = page.getByTestId(azMonSelectors.components.configEditor.azureCloud.input).locator('input');
await azureCloudInput.fill('Azure');
await azureCloudInput.press('Enter');
const tenantIdInput = page.getByTestId(azMonSelectors.components.configEditor.tenantID.input).locator('input');
await tenantIdInput.fill(datasourceConfig.jsonData.tenantId!);
const clientIdInput = page.getByTestId(azMonSelectors.components.configEditor.clientID.input).locator('input');
await clientIdInput.fill(datasourceConfig.jsonData.clientId!);
const clientSecretInput = page
.getByTestId(azMonSelectors.components.configEditor.clientSecret.input)
.locator('input');
await clientSecretInput.fill(datasourceConfig.secureJsonData.clientSecret!);
const loadSubscriptionsButton = page.getByTestId(azMonSelectors.components.configEditor.loadSubscriptions.button);
await loadSubscriptionsButton.click();
const defaultSubscriptionInput = page
.getByTestId(azMonSelectors.components.configEditor.defaultSubscription.input)
.locator('input');
await defaultSubscriptionInput.fill('datasources');
await configPage
.getByGrafanaSelector(selectors.components.Select.option)
.filter({
hasText: 'datasources',
})
.click();
await configPage.saveAndTest();
}
// TODO unskip when we've figured out how to populate the credentials in CI
test.describe.skip(
'Azure Monitor datasource',
{
tag: ['@cloud-plugins'],
},
() => {
let datasourceConfig: AzureMonitorConfig;
test.beforeAll(async () => {
// Check if we're running in CI
const CI = process.env.CI;
if (CI) {
const outputs = JSON.parse(readFileSync('outputs.json', 'utf8'));
datasourceConfig = {
jsonData: {
cloudName: 'Azure',
tenantId: outputs.tenantId,
clientId: outputs.clientId,
},
secureJsonData: { clientSecret: outputs.clientSecret },
};
} else {
const yamlContent = readFileSync(provisioningPath, 'utf8');
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const yaml = load(yamlContent) as AzureMonitorProvision;
datasourceConfig = yaml.datasources[0];
}
});
test('create dashboard, add panel for metrics, log analytics, ARG, and traces queries', async ({
createDataSourceConfigPage,
gotoDashboardPage,
selectors,
page,
}) => {
// this test can absolutely take longer than the default 30s timeout
test.setTimeout(120000);
await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors);
// Create new dashboard
const dashboardPage = await gotoDashboardPage({
timeRange: {
from: 'now-6h',
to: 'now',
zone: 'Coordinated Universal Time',
},
});
// Add metrics panel
const metricsPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
const datasourcePicker = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill(dataSourceName);
const datasourceList = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText(dataSourceName).click();
// Configure metrics query
const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button);
await resourcePickerButton.click();
await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 });
const resourceSearchInput = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.search.input);
await resourceSearchInput.fill(storageAcctName);
await expect(page.getByText(storageAcctName)).toBeVisible({ timeout: 30000 });
await page.getByText(storageAcctName).click();
const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button);
await applyButton.click();
await expect(page.getByText('microsoft.storage/storageaccounts')).toBeVisible();
const metricNameInput = page
.getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input)
.locator('input');
await metricNameInput.fill('Used capacity');
await metricNameInput.press('Enter');
// Save and go back to dashboard
metricsPanel.backToDashboard();
// Add logs panel
const logsPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
await datasourcePicker.fill(dataSourceName);
await datasourceList.getByText(dataSourceName).click();
// Switch to Logs query type
const queryTypeSelect = page.getByTestId(azMonSelectors.components.queryEditor.header.select).locator('input');
await queryTypeSelect.fill('Logs');
await queryTypeSelect.press('Enter');
// Configure logs query
await resourcePickerButton.click();
await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 });
await resourceSearchInput.fill(logAnalyticsName);
await expect(page.getByText(logAnalyticsName)).toBeVisible({ timeout: 30000 });
await page.getByText(logAnalyticsName).click();
await applyButton.click();
let codeEditor = logsPanel.getByGrafanaSelector(selectors.components.CodeEditor.container).locator('textarea');
await codeEditor.fill('AzureDiagnostics', { force: true });
const formatSelection = page
.getByTestId(azMonSelectors.components.queryEditor.logsQueryEditor.formatSelection.input)
.locator('input');
await formatSelection.fill('Time series');
await formatSelection.press('Enter');
// Save and go back to dashboard
logsPanel.backToDashboard();
// Add Azure Resource Graph panel
const resourceGraphPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
await datasourcePicker.fill(dataSourceName);
await datasourceList.getByText(dataSourceName).click();
// Switch to Azure Resource Graph query type
await queryTypeSelect.fill('Azure Resource Graph');
await queryTypeSelect.press('Enter');
// Configure Azure Resource Graph query
const subscriptionsInput = page
.getByTestId(azMonSelectors.components.queryEditor.argsQueryEditor.subscriptions.input)
.locator('input');
await subscriptionsInput.fill('datasources');
await subscriptionsInput.press('Enter');
codeEditor = resourceGraphPanel
.getByGrafanaSelector(selectors.components.CodeEditor.container)
.locator('textarea');
await codeEditor.fill(
"Resources | where resourceGroup == 'cloud-plugins-e2e-test-azmon' | project name, resourceGroup"
);
// Save and go back to dashboard
resourceGraphPanel.backToDashboard();
// Add traces panel
const tracesPanel = await dashboardPage.addPanel();
// Select Azure Monitor datasource
await datasourcePicker.fill(dataSourceName);
await datasourceList.getByText(dataSourceName).click();
// Switch to Traces query type
await queryTypeSelect.fill('Traces');
await queryTypeSelect.press('Enter');
// Configure traces query
await resourcePickerButton.click();
await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 });
await resourceSearchInput.fill(applicationInsightsName);
await expect(page.getByText(applicationInsightsName)).toBeVisible({ timeout: 30000 });
await page.getByText(applicationInsightsName).click();
await applyButton.click();
await formatSelection.fill('Trace');
await formatSelection.press('Enter');
});
test('create dashboard with template variables', async ({
createDataSourceConfigPage,
gotoDashboardPage,
page,
selectors,
}) => {
await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors);
// Create new dashboard
const dashboardPage = await gotoDashboardPage({
timeRange: {
from: 'now-6h',
to: 'now',
},
});
// Add subscription variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'subscription',
AzureQueryType.SubscriptionsQuery,
true
);
// Add resource groups variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'resourceGroups',
AzureQueryType.ResourceGroupsQuery,
false,
{
subscription: '$subscription',
}
);
// Add namespaces variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'namespaces',
AzureQueryType.NamespacesQuery,
false,
{
subscription: '$subscription',
resourceGroup: '$resourceGroups',
}
);
// Add region variable
await addAzureMonitorVariable(page, dashboardPage, selectors, 'region', AzureQueryType.LocationsQuery, false, {
subscription: '$subscription',
});
// Add resource variable
await addAzureMonitorVariable(
page,
dashboardPage,
selectors,
'resource',
AzureQueryType.ResourceNamesQuery,
false,
{
subscription: '$subscription',
resourceGroup: '$resourceGroups',
namespace: '$namespaces',
region: '$region',
}
);
// Set variable values
const subscriptionVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('subscription'))
.locator('..')
.locator('input');
await subscriptionVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.filter({ hasText: 'grafanalabs-datasources-dev' })
.click();
const resourceGroupsVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resourceGroups'))
.locator('..')
.locator('input');
await resourceGroupsVariable.fill('cloud-plugins-e2e-test-azmon');
await resourceGroupsVariable.press('ArrowDown');
await resourceGroupsVariable.press('Enter');
const namespacesVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('namespaces'))
.locator('..')
.locator('input');
await namespacesVariable.fill('microsoft.storage/storageaccounts');
await namespacesVariable.press('ArrowDown');
await namespacesVariable.press('Enter');
const regionVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('region'))
.locator('..')
.locator('input');
await regionVariable.fill('uk south');
await regionVariable.press('ArrowDown');
await regionVariable.press('Enter');
const resourceVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resource'))
.locator('..')
.locator('input');
await resourceVariable.fill(storageAcctName);
await resourceVariable.press('ArrowDown');
await resourceVariable.press('Enter');
// Add panel with template variables
const newPanel = await dashboardPage.addPanel();
const datasourcePicker = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill(dataSourceName);
const datasourceList = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText(dataSourceName).click();
const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button);
await resourcePickerButton.click();
const advancedCollapse = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.collapse);
await advancedCollapse.click();
const advancedSubscription = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.subscription.input)
.locator('input');
await advancedSubscription.fill('$subscription');
const advancedResourceGroup = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resourceGroup.input)
.locator('input');
await advancedResourceGroup.fill('$resourceGroups');
const advancedNamespace = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.namespace.input)
.locator('input');
await advancedNamespace.fill('$namespaces');
const advancedRegion = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.region.input)
.locator('input');
await advancedRegion.fill('$region');
const advancedResource = page
.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resource.input)
.locator('input');
await advancedResource.fill('$resource');
const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button);
await applyButton.click();
const metricNameInput = page
.getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input)
.locator('input');
await metricNameInput.fill('Transactions');
await metricNameInput.press('Enter');
});
// Helper function to add template variables
async function addAzureMonitorVariable(
page: Page,
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
name: string,
type: AzureQueryType,
isFirst: boolean,
options?: {
subscription?: string;
resourceGroup?: string;
namespace?: string;
resource?: string;
region?: string;
}
) {
// Navigate to variables settings
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.settingsButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Variables')).click();
if (isFirst) {
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.List.addVariableCTAV2)
.click();
} else {
await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click();
}
// Configure variable
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.clear();
await nameInput.fill(name);
const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill(dataSourceName);
const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText(dataSourceName).click();
const queryTypeInput = page
.getByTestId(azMonSelectors.components.variableEditor.queryType.input)
.locator('input');
await queryTypeInput.fill(type.replace('Azure', '').trim());
await queryTypeInput.press('Enter');
// Configure type-specific options
switch (type) {
case AzureQueryType.ResourceGroupsQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
break;
case AzureQueryType.LocationsQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
break;
case AzureQueryType.NamespacesQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
if (options?.resourceGroup) {
const resourceGroupInput = page
.getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input)
.locator('input');
await resourceGroupInput.fill(options.resourceGroup);
await resourceGroupInput.press('Enter');
}
break;
case AzureQueryType.ResourceNamesQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
if (options?.resourceGroup) {
const resourceGroupInput = page
.getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input)
.locator('input');
await resourceGroupInput.fill(options.resourceGroup);
await resourceGroupInput.press('Enter');
}
if (options?.namespace) {
const namespaceInput = page
.getByTestId(azMonSelectors.components.variableEditor.namespace.input)
.locator('input');
await namespaceInput.fill(options.namespace);
await namespaceInput.press('Enter');
}
if (options?.region) {
const regionInput = page
.getByTestId(azMonSelectors.components.variableEditor.region.input)
.locator('input');
await regionInput.fill(options.region);
await regionInput.press('Enter');
}
break;
case AzureQueryType.MetricNamesQuery:
if (options?.subscription) {
const subscriptionInput = page
.getByTestId(azMonSelectors.components.variableEditor.subscription.input)
.locator('input');
await subscriptionInput.fill(options.subscription);
await subscriptionInput.press('Enter');
}
if (options?.resourceGroup) {
const resourceGroupInput = page
.getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input)
.locator('input');
await resourceGroupInput.fill(options.resourceGroup);
await resourceGroupInput.press('Enter');
}
if (options?.namespace) {
const namespaceInput = page
.getByTestId(azMonSelectors.components.variableEditor.namespace.input)
.locator('input');
await namespaceInput.fill(options.namespace);
await namespaceInput.press('Enter');
}
if (options?.resource) {
const resourceInput = page
.getByTestId(azMonSelectors.components.variableEditor.resource.input)
.locator('input');
await resourceInput.fill(options.resource);
await resourceInput.press('Enter');
}
break;
}
// Save variable
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton)
.click();
// Go back to dashboard
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
}
}
);
@@ -0,0 +1,56 @@
import { test, expect } from '@grafana/plugin-e2e';
import testV2Dashboard from '../dashboards/TestV2Dashboard.json';
import { flows } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
test.describe(
'Dashboard panels',
{
tag: ['@dashboards'],
},
() => {
test('can duplicate a panel', async ({ dashboardPage, selectors, page }) => {
await page.goto(selectors.pages.ImportDashboard.url);
await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard));
await page.getByTestId(selectors.components.DashboardImportPage.submit).click();
await page.getByTestId(selectors.components.ImportDashboardForm.name).fill('Paste tab');
await page.getByTestId(selectors.components.ImportDashboardForm.submit).click();
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
const oldPanelTitle = 'New panel';
const panelTitle = 'Unique';
await flows.changePanelTitle(dashboardPage, selectors, oldPanelTitle, panelTitle);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.menu(panelTitle))
.click({ force: true });
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('More...')).hover();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Duplicate')).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount(
2
);
// Save, reload, and ensure duplicate has persisted
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click();
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount(
2
);
});
}
);
@@ -0,0 +1,804 @@
import { Page } from 'playwright-core';
import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e';
import testV2Dashboard from '../dashboards/TestV2Dashboard.json';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
// these tests require a larger viewport
test.use({
viewport: { width: 1280, height: 1080 },
});
test.describe(
'Grouping panels',
{
tag: ['@dashboards'],
},
() => {
// Helper functions
async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) {
await page.goto(selectors.pages.ImportDashboard.url);
await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard));
await page.getByTestId(selectors.components.DashboardImportPage.submit).click();
await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title);
await page.getByTestId(selectors.components.ImportDashboardForm.submit).click();
}
async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click();
}
async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
await page.getByText('Group into row').click();
}
async function groupIntoTab(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
await page.getByText('Group into tab').click();
}
async function ungroupPanels(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroup).click();
}
async function addPanel(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click();
}
/*
* Rows
*/
test('can group and ungroup new panels into row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into row
await groupIntoRow(page, dashboardPage, selectors);
// Verify row and panel titles
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify row and panel titles after reload
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors);
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can add and remove several rows', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Add and remove rows');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click();
await addPanel(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click();
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(5);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(5);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
.locator('..')
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
.locator('..')
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2'))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can paste a copied row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Paste row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Copy by selecting row and using copy button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Copy' }).click();
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteRow).click();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
await saveDashboard(dashboardPage, selectors);
await page.reload();
// scroll last `New panel` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
.last()
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
});
test('can duplicate a row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Duplicate row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Duplicate by selecting row and using duplicate button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
await saveDashboard(dashboardPage, selectors);
await page.reload();
// scroll last `New panel` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
.last()
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
});
test('can collapse rows', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Collapse rows');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Duplicate row
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(6);
// Collapse rows by clicking on their titles
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(0);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(0);
});
test('can convert rows into tabs when changing layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Rows to tabs');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// Duplicate row
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
// Go back to dashboard options
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
// Expand layouts section
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
.click();
// Select tabs layout
await page.getByLabel('Tabs').click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can group and ungroup new panels into row with tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into tab with row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into row with tab
await groupIntoRow(page, dashboardPage, selectors);
await groupIntoTab(page, dashboardPage, selectors);
// Verify tab and panel titles
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify tab, row and panel titles after reload
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors); // ungroup tabs
await ungroupPanels(dashboardPage, selectors); // ungroup rows
// Verify tab and row titles is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
/*
* Tabs
*/
test('can group and ungroup new panels into tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into tab');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into tab
await groupIntoTab(page, dashboardPage, selectors);
// Verify tab and panel titles
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify row and panel titles after reload
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors);
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can add and remove several tabs', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Add and remove tabs');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click();
await addPanel(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click();
await addPanel(dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute(
'aria-selected',
'true'
);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(1);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute(
'aria-selected',
'true'
);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(1);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can paste a copied tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Paste tab');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// Copy by selecting tab and using copy button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Copy' }).click();
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteTab).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can duplicate a tab', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Duplicate tab');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// Duplicate by selecting tab and using duplicate button
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('can convert tabs into rows when changing layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Tabs to rows');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// Duplicate tab twice
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
// Go back to dashboard options
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
// Expand layouts section
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
.click();
// Select rows layout
await page.getByLabel('Rows').click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab'))
).toBeVisible();
// Wait for panels to load
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first()
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
).toBeVisible();
// scroll `New row` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(9);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab'))
).toBeVisible();
// Wait for panels to load
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first()
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1'))
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2'))
).toBeVisible();
// scroll last `New panel` into view - this is at the bottom of the dashboard body
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
.last()
.scrollIntoViewIfNeeded();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(9);
});
test('can group and ungroup new panels into tab with row', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Group new panels into tab with row');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Group into tab
await groupIntoTab(page, dashboardPage, selectors);
await groupIntoRow(page, dashboardPage, selectors);
// Verify tab and panel titles
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify tab, row and panel titles after reload
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Ungroup
await ungroupPanels(dashboardPage, selectors); // ungroup rows
await ungroupPanels(dashboardPage, selectors); // ungroup tabs
// Verify tab and row titles is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
// Save dashboard and reload
await saveDashboard(dashboardPage, selectors);
await page.reload();
// Verify Row title is gone
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
}
);
@@ -0,0 +1,38 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
test.describe(
'Dashboard Outline',
{
tag: ['@dashboards'],
},
() => {
test('can use dashboard outline', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click();
// Should be able to click Variables item in outline to see add variable button
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
).toBeVisible();
// Clicking a panel should scroll that panel in view
await expect(page.getByText('Dashboard panel 48')).toBeHidden();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click();
await expect(page.getByText('Dashboard panel 48')).toBeVisible();
});
}
);
@@ -0,0 +1,44 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard panels',
{
tag: ['@dashboards'],
},
() => {
test('can add a new panel', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click();
// Check that new panel has been added
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toBeVisible();
// Check that pressing the configure button shows the panel editor
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.content)
.filter({ hasText: 'Configure' })
.click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible();
});
}
);
@@ -0,0 +1,91 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - Ad hoc variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new adhoc variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'adhoc',
name: 'VariableUnderTest',
value: 'label1',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// Select datasource for the ad hoc variable
const dataSource = 'gdev-loki';
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect)
.click();
await page.getByText(dataSource).click();
// mock the API call to get the labels
const labels = ['label1', 'label2'];
await page.route('**/resources/labels*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labels,
}),
});
});
// Select the variable in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// mock the API call to get the label values
const labelValues = ['label2Value1'];
await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labelValues,
}),
});
});
// choose the label and value
await page.getByText(labels[1]).click();
await page.getByText('=', { exact: true }).click();
await page.getByText(labelValues[0]).click();
await page.keyboard.press('Escape');
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}="${labelValues[0]}"`);
});
}
);
@@ -0,0 +1,62 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - datasource variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new datasource variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const dsType = 'cloudwatch';
const variable: Variable = {
type: 'datasource',
name: 'VariableUnderTest',
label: 'VariableUnderTest',
value: `gdev-${dsType}`,
};
// Common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect)
.click();
await page.getByText(dsType).click();
const regexFilter = 'cloud';
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.nameFilter)
.fill(regexFilter);
// Assert the variable dropdown is visible with correct label
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// Assert the variable values are correctly displayed in the panel
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`${variable.name}: ${variable.value}`);
});
}
);
@@ -0,0 +1,89 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - Group By variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new group by variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'groupby',
name: 'VariableUnderTest',
value: 'label1',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// select datasource for the group by variable
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect)
.click();
const dataSource = 'gdev-loki';
await page.getByText(dataSource).click();
// mock the API call to get the labels
const labels = ['label1', 'label2'];
await page.route('**/resources/labels*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labels,
}),
});
});
// open the variable dropdown in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).locator('> div').click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// mock the API call to get the label values
const labelValues = ['label2Value1'];
await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labelValues,
}),
});
});
// choose the label and value
await page.getByText(labels[1]).click();
await page.keyboard.press('Escape');
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}`);
});
}
);
@@ -0,0 +1,58 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph';
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can edit panel title and description', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
const oldTitle = 'No Data Points Warning';
const firstPanelTitle = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.first()
.locator('h2')
.first();
await expect(firstPanelTitle).toHaveText(oldTitle);
const newDescription = 'A description of this panel';
await flows.changePanelDescription(dashboardPage, selectors, oldTitle, newDescription);
const newTitle = 'New Panel Title';
await flows.changePanelTitle(dashboardPage, selectors, oldTitle, newTitle);
// Check that new title is reflected in panel header
const updatedPanelTitle = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.first()
.locator('h2')
.first();
await expect(updatedPanelTitle).toHaveText(newTitle);
// Reveal description tooltip and check that its value is as expected
const descriptionIcon = page.locator('[data-testid="title-items-container"] > span').first();
await descriptionIcon.click({ force: true });
// Get the tooltip ID from the aria-describedby attribute
const tooltipId = await descriptionIcon.getAttribute('aria-describedby');
const tooltip = page.locator(`[id="${tooltipId}"]`);
await expect(tooltip).toHaveText(`${newDescription}\n`);
});
}
);
@@ -0,0 +1,43 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph';
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can toggle transparent background switch', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^No Data Points Warning$/ })
.first()
.click();
const panelTitle = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title('No Data Points Warning')
);
const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/);
await page.locator('#transparent-background').click({ force: true });
const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/);
});
}
);
@@ -0,0 +1,81 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - Query variable',
{
tag: ['@dashboards'],
},
() => {
test('can add a new query variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const queryVariableOptions = ['default'];
const variable: Variable = {
type: 'query',
name: 'VariableUnderTest',
value: queryVariableOptions[0],
label: 'VariableUnderTest', // constant doesn't really need a label
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// open the modal query variable editor
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsOpenButton)
.click();
// select a core data source that just runs a query during preview
await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click();
const dataSource = 'gdev-cloudwatch';
// this will trigger an API call to get the query options
await page.getByText(dataSource).click();
// show the preview of the query results
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.previewButton)
.click();
// assert the query results are shown
const firstPreviewOption = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption)
.first();
await expect(firstPreviewOption).toBeVisible();
const previewOptionText = await firstPreviewOption.textContent();
const previewOption = previewOptionText?.trim() || '';
// close the modal
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.closeButton)
.click();
// assert the query variable values are in the variable value select
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!)
);
const nextElement = variableLabel.locator('+ *');
await expect(nextElement).toHaveText(previewOption);
// Assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${previewOption}`);
});
}
);
@@ -0,0 +1,183 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Dashboard edit - variables',
{
tag: ['@dashboards'],
},
() => {
test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'custom',
name: 'foo',
label: 'Foo',
value: 'one,two,three',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the custom variable value
const customValueInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
);
await customValueInput.fill(variable.value);
await customValueInput.blur();
// assert the dropdown for the variable is visible and has the correct values
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
const values = variable.value.split(',');
const firstValueLink = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(values[0])
);
await expect(firstValueLink).toBeVisible();
// check that variable deletion works
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await expect(variableLabel).toBeHidden();
});
test('can add a new constant variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'constant',
name: 'VariableUnderTest',
value: 'foo',
label: 'VariableUnderTest', // constant doesn't really need a label
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the constant variable value
const type = 'variable-type Value';
const fieldLabel = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel(type)
);
await expect(fieldLabel).toBeVisible();
const inputField = fieldLabel.locator('input');
await expect(inputField).toBeVisible();
await inputField.fill(variable.value);
await inputField.blur();
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`);
});
test('can add a new textbox variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'textbox',
name: 'VariableUnderTest',
value: 'foo',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the textbox variable value
const type = 'variable-type Value';
const fieldLabel = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel(type)
);
await expect(fieldLabel).toBeVisible();
const inputField = fieldLabel.locator('input');
await expect(inputField).toBeVisible();
await inputField.fill(variable.value);
await inputField.blur();
// select the variable in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`);
});
test('can add a new interval variable', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
const variable: Variable = {
type: 'interval',
name: 'VariableUnderTest',
value: '1m',
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// enable the auto option
await page.getByText('Auto option').click();
// select the variable in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click();
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
);
await expect(variableLabel).toBeVisible();
await expect(variableLabel).toContainText(variable.label!);
// assert the panel is visible and has the correct value
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
const markdownContent = panelContent.locator('.markdown-html');
await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`);
// select the variable in the dashboard and set the Auto option
const variableDropdown = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!)
);
const nextElement = variableDropdown.locator('+ *');
await expect(nextElement).toHaveText('1m');
await nextElement.click();
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Auto' }).click();
// assert the panel is visible and has the correct "Auto" value
await expect(panelContent).toBeVisible();
await expect(markdownContent).toContainText('VariableUnderTest: 10m');
});
}
);
@@ -0,0 +1,120 @@
import { Page } from 'playwright-core';
import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'ed155665/annotation-filtering';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
// these tests require a larger viewport
viewport: { width: 1920, height: 1080 },
});
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can drag and drop panels', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Move panel three to panel one position
await movePanel(dashboardPage, selectors, /^Panel three$/, /^Panel one$/);
// Verify panel three is now above panel one
const panel3Position = await getPanelPosition(dashboardPage, selectors, /^Panel three$/);
const panel1Position = await getPanelPosition(dashboardPage, selectors, /^Panel one$/);
expect(panel3Position?.y).toBeLessThan(panel1Position?.y || 0);
// Move panel two to panel three position
await movePanel(dashboardPage, selectors, /^Panel two$/, /^Panel three$/);
// Verify panel two is now above panel three
const panel2Position = await getPanelPosition(dashboardPage, selectors, /^Panel two$/);
const panel3PositionAfter = await getPanelPosition(dashboardPage, selectors, /^Panel three$/);
expect(panel2Position?.y).toBeLessThan(panel3PositionAfter?.y || 0);
});
// Note, moving a panel from a nested row to a parent row currently just deletes the panel
// This test will need to be updated once the correct behavior is implemented.
test('can move panel from nested row to parent row', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await groupIntoRow(page, dashboardPage, selectors);
const firstRowElement = dashboardPage
.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
.first();
const rowBoundingBox = await firstRowElement.boundingBox();
if (!rowBoundingBox) {
throw new Error('Row element not found');
}
const panel1Element = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel one$/ });
await panel1Element.hover();
await page.mouse.down();
await page.mouse.move(rowBoundingBox.x, rowBoundingBox.y);
await page.mouse.up();
await expect(
dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel one$/ })
).toBeHidden();
});
}
);
// Helper functions
async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
await page.getByText('Group into row').click();
}
async function getPanelPosition(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
panelTitle: string | RegExp
) {
const panel = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: panelTitle });
const boundingBox = await panel.boundingBox();
return boundingBox;
}
async function movePanel(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
sourcePanel: string | RegExp,
targetPanel: string | RegExp
) {
// Get target panel position
const targetPanelElement = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: targetPanel });
// Get source panel element
const sourcePanelElement = dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: sourcePanel });
// Perform drag and drop
await sourcePanelElement.dragTo(targetPanelElement);
}
@@ -0,0 +1,409 @@
import { Page } from 'playwright-core';
import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e';
import testV2Dashboard from '../dashboards/TestV2Dashboard.json';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
test.describe(
'Dashboard Panel Layouts',
{
tag: ['@dashboards'],
},
() => {
test('can switch to auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Switch to auto grid');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await checkAutoGridLayoutInputs(dashboardPage, selectors);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await checkAutoGridLayoutInputs(dashboardPage, selectors);
});
test('can change min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set min column width');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
// Get initial positions - standard width should have panels on different rows
const firstPanelTop = await getPanelTop(dashboardPage, selectors);
const lastPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).last();
const lastPanelBox = await lastPanel.boundingBox();
const lastPanelTop = lastPanelBox?.y || 0;
// Verify standard layout has panels on different rows
expect(lastPanelTop).toBeGreaterThan(firstPanelTop);
// Change to narrow min column width
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
.click();
await page.getByRole('option', { name: 'Narrow' }).click();
// Verify narrow layout has all panels on same row
const firstPanelTopNarrow = await getPanelTop(dashboardPage, selectors);
const lastPanelBoxNarrow = await lastPanel.boundingBox();
const lastPanelTopNarrow = lastPanelBoxNarrow?.y || 0;
expect(lastPanelTopNarrow).toBe(firstPanelTopNarrow);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth
)
).toHaveValue('Narrow');
const firstPanelTopReload = await getPanelTop(dashboardPage, selectors);
const lastPanelBoxReload = await lastPanel.boundingBox();
const lastPanelTopReload = lastPanelBoxReload?.y || 0;
expect(lastPanelTopReload).toBe(firstPanelTopReload);
});
test('can change to custom min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set custom min column width');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
.click();
await page.getByRole('option', { name: 'Custom' }).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
.fill('900');
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
.blur();
// Changing to 900 custom width should have each panel span the whole row (stacked vertically)
await verifyPanelsStackedVertically(dashboardPage, selectors);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await verifyPanelsStackedVertically(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth
)
).toHaveValue('900');
await verifyPanelsStackedVertically(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomMinColumnWidth)
.click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth
)
).toHaveValue('Standard');
});
test('can change max columns in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set max columns');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
.click();
await page.getByRole('option', { name: '1', exact: true }).click();
// Changing to 1 max column should have each panel span the whole row (stacked vertically)
await verifyPanelsStackedVertically(dashboardPage, selectors);
await saveDashboard(dashboardPage, selectors);
await page.reload();
await verifyPanelsStackedVertically(dashboardPage, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
).toHaveValue('1');
await verifyPanelsStackedVertically(dashboardPage, selectors);
});
test('can change row height in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set row height');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
.click();
await page.getByRole('option', { name: 'Short' }).click();
const shortHeight = await getPanelHeight(dashboardPage, selectors);
expect(shortHeight).toBeLessThan(regularRowHeight);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
.click();
await page.getByRole('option', { name: 'Tall' }).click();
const tallHeight = await getPanelHeight(dashboardPage, selectors);
expect(tallHeight).toBeGreaterThan(regularRowHeight);
await saveDashboard(dashboardPage, selectors);
await page.reload();
const tallHeightAfterReload = await getPanelHeight(dashboardPage, selectors);
expect(tallHeightAfterReload).toBeGreaterThan(regularRowHeight);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
).toHaveValue('Tall');
const tallHeightAfterEdit = await getPanelHeight(dashboardPage, selectors);
expect(tallHeightAfterEdit).toBeGreaterThan(regularRowHeight);
});
test('can change to custom row height in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set custom row height');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
.click();
await page.getByRole('option', { name: 'Custom' }).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight)
.fill('800');
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight)
.blur();
const customHeight = await getPanelHeight(dashboardPage, selectors);
expect(customHeight).toBeCloseTo(800, 5); // Allow some tolerance for rendering differences
expect(customHeight).toBeGreaterThan(regularRowHeight);
await saveDashboard(dashboardPage, selectors);
await page.reload();
const customHeightAfterReload = await getPanelHeight(dashboardPage, selectors);
expect(customHeightAfterReload).toBeCloseTo(800, 5);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight
)
).toHaveValue('800');
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomRowHeight)
.click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
).toHaveValue('Standard');
});
test('can change fill screen in auto grid layout', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Set fill screen');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
.click();
await page.getByLabel('Auto grid').click();
// Set narrow column width first to ensure panels fit horizontally
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
.click();
await page.getByRole('option', { name: 'Narrow' }).click();
const initialHeight = await getPanelHeight(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
.click({ force: true });
const fillScreenHeight = await getPanelHeight(dashboardPage, selectors);
expect(fillScreenHeight).toBeGreaterThan(initialHeight);
await saveDashboard(dashboardPage, selectors);
await page.reload();
const fillScreenHeightAfterReload = await getPanelHeight(dashboardPage, selectors);
expect(fillScreenHeightAfterReload).toBeGreaterThan(initialHeight);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
).toBeChecked();
const fillScreenHeightAfterEdit = await getPanelHeight(dashboardPage, selectors);
expect(fillScreenHeightAfterEdit).toBeGreaterThan(initialHeight);
});
}
);
// Helper functions
async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) {
await page.goto(selectors.pages.ImportDashboard.url);
await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard));
await page.getByTestId(selectors.components.DashboardImportPage.submit).click();
await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title);
await page.getByTestId(selectors.components.ImportDashboardForm.submit).click();
}
async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click();
}
async function checkAutoGridLayoutInputs(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
).toBeVisible();
}
async function verifyPanelsStackedVertically(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
const panels = await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).all();
let previousTop = 0;
for (const panel of panels) {
const boundingBox = await panel.boundingBox();
if (boundingBox) {
if (previousTop === 0) {
previousTop = boundingBox.y;
} else {
expect(boundingBox.y).toBeGreaterThan(previousTop);
previousTop = boundingBox.y;
}
}
}
}
async function getPanelHeight(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise<number> {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first();
const boundingBox = await panel.boundingBox();
return boundingBox?.height || 0;
}
async function getPanelTop(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise<number> {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first();
const boundingBox = await panel.boundingBox();
return boundingBox?.y || 0;
}
@@ -0,0 +1,66 @@
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
test.describe(
'Dashboard panels',
{
tag: ['@dashboards'],
},
() => {
test('can remove a panel', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Remove Panel #1
await removePanelsByTitle(dashboardPage, selectors, ['Panel #1']);
// Check that panel has been deleted
await expect(
dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel #1$/ })
).toBeHidden();
});
test('can remove several panels at once', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Remove multiple panels
await removePanelsByTitle(dashboardPage, selectors, ['Panel #1', 'Panel #2', 'Panel #3']);
// Check that panels have been deleted
await expect(
dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: /^Panel #[123]$/ })
).toBeHidden();
});
}
);
// Helper function to remove a panel by its title
async function removePanelsByTitle(dashboardPage: DashboardPage, selectors: E2ESelectorGroups, panelTitles: string[]) {
for (const panelTitle of panelTitles) {
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: panelTitle })
.click({
modifiers: ['Shift'],
});
}
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click();
}
@@ -0,0 +1,47 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
});
const PAGE_UNDER_TEST = 'ed155665/annotation-filtering';
test.describe(
'Dashboard',
{
tag: ['@dashboards'],
},
() => {
test('can change dashboard description and title', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
// Check that current dashboard title is visible in breadcrumb
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering'))
).toBeVisible();
const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input');
await expect(titleInput).toHaveValue('Annotation filtering');
await titleInput.fill('New dashboard title');
await expect(titleInput).toHaveValue('New dashboard title');
// Check that new dashboard title is reflected in breadcrumb
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('New dashboard title'))
).toBeVisible();
// Check that we can successfully change the dashboard description
const descriptionTextArea = page.locator(
'[aria-label="dashboard-options Description field property editor"] textarea'
);
await descriptionTextArea.fill('Dashboard description');
await expect(descriptionTextArea).toHaveValue('Dashboard description');
});
}
);
@@ -0,0 +1,84 @@
import { DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
const deselectPanels = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => {
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls).click({
position: { x: 0, y: 0 },
});
};
export const flows = {
deselectPanels,
async changePanelTitle(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
oldPanelTitle: string,
newPanelTitle: string
) {
await deselectPanels(dashboardPage, selectors);
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: oldPanelTitle })
.first()
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'))
.fill(newPanelTitle);
},
async changePanelDescription(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
panelTitle: string,
newDescription: string
) {
await deselectPanels(dashboardPage, selectors);
const panelTitleRegex = new RegExp(`^${panelTitle}$`);
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer)
.filter({ hasText: panelTitleRegex })
.first()
.click();
const descriptionTextArea = dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('panel-options Description'))
.locator('textarea');
await descriptionTextArea.fill(newDescription);
},
async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
.click();
},
async newEditPanelCommonVariableInputs(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
variable: Variable
) {
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.variableType(variable.type))
.click();
const variableNameInput = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.variableNameInput
);
await variableNameInput.click();
await variableNameInput.fill(variable.name);
await variableNameInput.blur();
if (variable.label) {
const variableLabelInput = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.ElementEditPane.variableLabelInput
);
await variableLabelInput.click();
await variableLabelInput.fill(variable.label);
await variableLabelInput.blur();
}
},
};
export type Variable = {
type: string;
name: string;
label?: string;
description?: string;
value: string;
};
@@ -0,0 +1,63 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Dashboard search',
{
tag: ['@dashboards-search'],
},
() => {
test.use({ viewport: { width: 1280, height: 1080 } });
test.beforeEach(async ({ page, selectors }) => {
await page.goto('/dashboards');
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible();
});
test('Search - Dashboards list', async ({ page, selectors }) => {
await toggleSearchView(page, selectors);
await assertResultsCount(page, 24);
});
test('Search - Filter by search input', async ({ page, selectors }) => {
await toggleSearchView(page, selectors);
await assertResultsCount(page, 24);
const searchInput = await page.getByTestId('input-wrapper');
await searchInput.click();
await page.keyboard.type('Datasource tests - MySQL');
await assertResultsCount(page, 2);
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('Backspace');
await page.keyboard.type('Datasource tests - MySQL (unittest)');
await assertResultsCount(page, 1);
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('Backspace');
await page.keyboard.type('- MySQL');
await assertResultsCount(page, 2);
});
}
);
async function assertResultsCount(page, length) {
const rowGroup = await page.getByRole('rowgroup');
await expect(rowGroup).toHaveCount(1);
const rows = await rowGroup.first().getByRole('row');
await expect(rows).toHaveCount(length);
}
async function toggleSearchView(page, selectors) {
const toggleButtons = await page.getByTestId(selectors.pages.Dashboards.toggleView);
await expect(toggleButtons).toHaveCount(2);
const listRadioButton = await toggleButtons.nth(1).locator('input');
await expect(listRadioButton).toBeChecked({ checked: false });
await listRadioButton.check();
await expect(listRadioButton).toBeChecked({ checked: true });
}
@@ -0,0 +1,126 @@
import { test, expect } from '@grafana/plugin-e2e';
import { makeNewDashboardRequestBody } from './utils/makeDashboard';
const NUM_ROOT_FOLDERS = 60;
const NUM_ROOT_DASHBOARDS = 60;
const NUM_NESTED_FOLDERS = 60;
const NUM_NESTED_DASHBOARDS = 60;
// TODO change this test so it doesn't conflict with the existing dashboard browse test
// probably needs a separate user
test.describe.skip(
'Dashboard browse (nested)',
{
tag: ['@dashboards'],
},
() => {
const dashboardUIDsToCleanUp: string[] = [];
const folderUIDsToCleanUp: string[] = [];
test.beforeAll(async ({ request }) => {
// Add root folders
for (let i = 0; i < NUM_ROOT_FOLDERS; i++) {
const response = await request.post('/api/folders', {
data: {
title: `Root folder ${i.toString().padStart(2, '0')}`,
},
});
const responseBody = await response.json();
folderUIDsToCleanUp.push(responseBody.uid);
}
// Add root dashboards
for (let i = 0; i < NUM_ROOT_DASHBOARDS; i++) {
const response = await request.post('/api/dashboards/db', {
data: makeNewDashboardRequestBody(`Root dashboard ${i.toString().padStart(2, '0')}`),
});
const responseBody = await response.json();
dashboardUIDsToCleanUp.push(responseBody.uid);
}
// Add folder with children
const folderResponse = await request.post('/api/folders', {
data: {
title: 'A root folder with children',
},
});
const folderResponseBody = await folderResponse.json();
const folderUid = folderResponseBody.uid;
folderUIDsToCleanUp.push(folderUid);
// Add nested folders
for (let i = 0; i < NUM_NESTED_FOLDERS; i++) {
await request.post('/api/folders', {
data: {
title: `Nested folder ${i.toString().padStart(2, '0')}`,
parentUid: folderUid,
},
headers: {
'Content-Type': 'application/json',
},
});
}
// Add nested dashboards
for (let i = 0; i < NUM_NESTED_DASHBOARDS; i++) {
await request.post('/api/dashboards/db', {
data: makeNewDashboardRequestBody(`Nested dashboard ${i.toString().padStart(2, '0')}`, folderUid),
headers: {
'Content-Type': 'application/json',
},
});
}
});
test.afterAll(async ({ request }) => {
// Clean up root dashboards
for (const dashboardUID of dashboardUIDsToCleanUp) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
// Clean up root folders (cascading delete will remove any nested folders and dashboards)
for (const folderUID of folderUIDsToCleanUp) {
await request.delete(`/api/folders/${folderUID}`, {
params: {
forceDeleteRules: false,
},
});
}
});
test('pagination works correctly for folders and root', async ({ page, selectors }) => {
// Navigate to dashboards page
await page.goto('/dashboards');
// Wait for and verify the root folder with children is visible
await expect(page.getByText('A root folder with children')).toBeVisible();
// Expand A root folder with children
await page.getByLabel('Expand folder A root folder with children').click();
await expect(page.getByText('Nested folder 00')).toBeVisible();
// Get the table body container for scrolling
const tableBody = page.getByTestId(selectors.pages.BrowseDashboards.table.body).locator('> div');
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 2100));
await expect(page.getByText('Nested folder 59')).toBeVisible();
await expect(page.getByText('Nested dashboard 00')).toBeVisible();
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 4200));
await expect(page.getByText('Nested dashboard 59')).toBeVisible();
await expect(page.getByText('Root folder 00')).toBeVisible();
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 6300));
await expect(page.getByText('Root folder 59')).toBeVisible();
await expect(page.getByText('Root dashboard 00')).toBeVisible();
// Scroll the page and check visibility of next set of items
await tableBody.evaluate((el) => el.scrollTo(0, 8400));
await expect(page.getByText('Root dashboard 59')).toBeVisible();
});
}
);
@@ -0,0 +1,102 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/TestDashboard.json';
test.describe(
'Dashboard browse',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test.beforeAll(async ({ request }) => {
// Import the test dashboard
const response = await request.post('/api/dashboards/import', {
data: {
dashboard: testDashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
const responseBody = await response.json();
dashboardUID = responseBody.uid;
});
test.afterAll(async ({ request }) => {
// Clean up the imported dashboard
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
});
test('Manage Dashboards tests', async ({ page, selectors }) => {
// Navigate to dashboards page
await page.goto('/dashboards');
// Folders and dashboards should be visible
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible();
await expect(
page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard'))
).toBeVisible();
// gdev dashboards folder is collapsed - its content should not be visible
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeHidden();
// should click a folder and see its children
await page
.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))
.getByLabel(/Expand folder/)
.click();
await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeVisible();
// Open the new folder drawer
await page.getByText('New').click();
await page.getByRole('menuitem', { name: 'New folder' }).click();
// And create a new folder
await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.nameInput).fill('My new folder');
await page
.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form)
.getByRole('button', { name: 'Create' })
.click();
// await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form).getByRole('button', { name: 'Create' }).click({ force: true });
// Verify success alert and close it
const alert = page.getByTestId(selectors.components.Alert.alertV2('success'));
await expect(alert).toBeVisible();
await alert.getByLabel('Close alert').click();
await expect(page.getByRole('heading', { name: 'My new folder' })).toBeVisible();
// Delete the folder and expect to go back to the root
await page.getByRole('button', { name: 'Folder actions' }).click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete');
await page.getByTestId(selectors.pages.ConfirmModal.delete).click();
await expect(page.getByRole('heading', { name: 'Dashboards' })).toBeVisible();
// Can collapse the gdev folder and delete the dashboard we imported
await page
.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))
.getByLabel(/Collapse folder/)
.click();
// Select the imported dashboard using checkbox
await page
.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard'))
.getByRole('checkbox')
.click({ force: true });
// Delete the selected dashboard
await page.getByRole('button', { name: 'Delete' }).click();
// Confirm deletion in modal
await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete');
await page.getByTestId(selectors.pages.ConfirmModal.delete).click();
await expect(
page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard'))
).toBeHidden();
});
}
);
@@ -0,0 +1,66 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Export as JSON',
{
tag: ['@dashboards'],
},
() => {
test('Export for internal and external use', async ({ gotoDashboardPage, page, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: 'ZqZnVvFZz',
});
// Open the export drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.Menu.exportAsJson)
.click();
await expect(page).toHaveURL(/.*shareView=export.*/);
// Export as JSON
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.container)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle)
).toBeChecked({
checked: false,
});
await expect(dashboardPage.getByGrafanaSelector(selectors.components.CodeEditor.container)).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.saveToFileButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton)
.click();
// TODO failing in CI - fix it
// let clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
// expect(clipboardContent).not.toContain('__inputs');
await dashboardPage
.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle)
.click({ force: true });
await dashboardPage
.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton)
.click();
// TODO failing in CI - fix it
// clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
// expect(clipboardContent).toContain('__inputs');
await dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton).click();
await expect(page).not.toHaveURL(/.*shareView=export.*/);
});
}
);
@@ -0,0 +1,60 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Dashboard keybindings',
{
tag: ['@dashboards'],
},
() => {
// the "should collapse and expand all rows" test requires a larger viewport
// otherwise the final panel is not visible when everything is expanded
test.use({
viewport: { width: 1280, height: 1080 },
});
test('should collapse and expand all rows', async ({ gotoDashboardPage, page, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: 'Repeating-rows-uid/repeating-rows' });
const panelContents = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content);
await expect(panelContents).toHaveCount(5);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = A, pod = Bob'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = B, pod = Bob'))
).toBeVisible();
// Collapse all rows using keyboard shortcut: d + Shift+C
await page.keyboard.press('d');
await page.keyboard.press('Shift+C');
await expect(panelContents).toHaveCount(0);
await expect(page.getByText('server = A, pod = Bob')).toBeHidden();
await expect(page.getByText('server = B, pod = Bob')).toBeHidden();
// Expand all rows using keyboard shortcut: d + Shift+E
await page.keyboard.press('d');
await page.keyboard.press('Shift+E');
await expect(panelContents).toHaveCount(6);
await expect(page.getByText('server = A, pod = Bob')).toBeVisible();
await expect(page.getByText('server = B, pod = Bob')).toBeVisible();
});
test('should open panel inspect', async ({ gotoDashboardPage, page, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: 'edediimbjhdz4b/a-tall-dashboard' });
// Find Panel #1 and press 'i' to open inspector
const panel1 = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1'));
await expect(panel1).toBeVisible();
await panel1.press('i');
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible();
// Press Escape to close inspector
await page.keyboard.press('Escape');
await expect(page.getByTestId(selectors.components.PanelInspector.Json.content)).toBeHidden();
});
}
);
@@ -0,0 +1,65 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json';
test.describe(
'Dashboard with data links that have no slug',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test.beforeAll(async ({ request }) => {
// Import the test dashboard
const response = await request.post('/api/dashboards/import', {
data: {
dashboard: testDashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
const responseBody = await response.json();
dashboardUID = responseBody.uid;
});
test.afterAll(async ({ request }) => {
// Clean up the imported dashboard
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
});
test('Should not reload if linking to same dashboard', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: dashboardUID });
const panel = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title('Data links without slug')
);
await expect(panel).toBeVisible();
const urlShouldContain = '/d/data-link-no-slug/data-link-without-slug-test';
await dashboardPage
.getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink)
.getByText(/9yy21uzzxypg/)
.click();
await expect(page.getByText(/Loading/)).toBeHidden();
await expect(page).toHaveURL(new RegExp(urlShouldContain));
await dashboardPage
.getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink)
.getByText(/dr199bpvpcru/)
.click();
await expect(page.getByText(/Loading/)).toBeHidden();
await expect(page).toHaveURL(new RegExp(urlShouldContain));
await dashboardPage
.getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink)
.getByText(/dre33fzyxcrz/)
.click();
await expect(page.getByText(/Loading/)).toBeHidden();
await expect(page).toHaveURL(new RegExp(urlShouldContain));
});
}
);
@@ -0,0 +1,48 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/DashboardLiveTest.json';
test.describe(
'Dashboard Live streaming support',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test.beforeAll(async ({ request }) => {
// Import the test dashboard
const response = await request.post('/api/dashboards/import', {
data: {
dashboard: testDashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
const responseBody = await response.json();
dashboardUID = responseBody.uid;
});
test.afterAll(async ({ request }) => {
// Clean up the imported dashboard
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
});
test('Should receive streaming data', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: dashboardUID });
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Live'))).toBeVisible();
await expect
.poll(
async () =>
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body)
.getByRole('row')
.count()
)
.toBeGreaterThan(5);
});
}
);
@@ -0,0 +1,211 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
newDashboardSharingComponent: false,
},
});
test.describe(
'Public dashboards',
{
tag: ['@dashboards'],
},
() => {
test('Create, open and disable a public dashboard', async ({ page, gotoDashboardPage, selectors, request }) => {
// Navigate to dashboard without template variables
let dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' });
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
// Create button should be disabled
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeDisabled();
// Create flow shouldn't show these elements
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton)
).toBeHidden();
// Acknowledge checkboxes
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox)
.click({ force: true });
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox)
.click({ force: true });
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox)
.click({ force: true });
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeEnabled();
// Create public dashboard
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
.click();
// These elements shouldn't be rendered after creating public dashboard
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeHidden();
// These elements should be rendered
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
.click();
// These elements should be rendered once the Settings dropdown is opened
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch)
).toBeVisible();
// Close the sharing modal
await page.getByRole('button', { name: 'Close', exact: true }).click();
// Tag indicating a dashboard is public
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.publicDashboardTag)
).toBeVisible();
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown)
.click();
// These elements should be rendered once the Settings dropdown is opened
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch)
).toBeVisible();
// Make a request to public dashboards api endpoint without authentication
let copyUrlInput = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput
);
let url = await copyUrlInput.inputValue();
let publicDashboardApiUrl = getPublicDashboardAPIUrl(url);
// Create a new context without authentication
let response = await request.get(publicDashboardApiUrl);
expect(response.status()).toBe(200);
// Close the sharing modal
await page.getByRole('button', { name: 'Close', exact: true }).click();
// Navigate to dashboard without template variables
dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' });
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
// Save url before disabling public dashboard
copyUrlInput = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput
);
url = await copyUrlInput.inputValue();
// Switch off enabling toggle
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
.click({ force: true });
// Url should be disabled
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput)
).toBeDisabled();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton)
).toBeDisabled();
// Make a request to public dashboards api endpoint without authentication
publicDashboardApiUrl = getPublicDashboardAPIUrl(url);
response = await request.get(publicDashboardApiUrl);
expect(response.status()).toBe(403);
});
}
);
const getPublicDashboardAPIUrl = (url: string): string => {
const accessToken = url.split('/').pop();
return `/api/public/dashboards/${accessToken}`;
};
@@ -0,0 +1,56 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
newDashboardSharingComponent: false,
},
});
test.describe(
'Public dashboard with template variables',
{
tag: ['@dashboards'],
},
() => {
test('Create a public dashboard with template variables shows a template variable warning', async ({
gotoDashboardPage,
selectors,
}) => {
// Navigate to dashboard with template variables
const dashboardPage = await gotoDashboardPage({
uid: 'HYaGDGIMk',
});
// Open sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select public dashboards tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click();
// Warning Alert dashboard cannot be made public because it has template variables
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert
)
).toBeVisible();
// Configuration elements for public dashboards should exist
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch)
).toBeHidden();
});
}
);
@@ -0,0 +1,191 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
scenes: true,
newDashboardSharingComponent: true,
},
});
const DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8';
const DASHBOARD_UID_2 = 'edediimbjhdz4b';
test.describe(
'Shared dashboards',
{
tag: ['@dashboards'],
},
() => {
test('Close share externally drawer', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
// Open share externally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally)
.click();
await expect(page).toHaveURL(/.*shareView=public_dashboard.*/);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container)
).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.cancelButton)
.click();
await expect(page).not.toHaveURL(/.*shareView=public_dashboard.*/);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container)
).toBeHidden();
});
test('Create and disable a shared dashboard and check API', async ({
page,
gotoDashboardPage,
selectors,
request,
}) => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID_2 });
// Open share externally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally)
.click();
// Create button should be disabled
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton
)
).toBeDisabled();
// Create flow shouldn't show these elements
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton
)
).toBeHidden();
// Acknowledge checkbox
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox
)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox)
.click({ force: true });
// Create shared dashboard
const createResponse = page.waitForResponse(
(response) =>
response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards`) &&
response.request().method() === 'POST'
);
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton
)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton)
.click();
let response = await createResponse;
let publicDashboard = await response.json();
// Test API access with the created dashboard
let apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`);
expect(apiResponse.status()).toBe(200);
// These elements shouldn't be rendered after creating public dashboard
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton
)
).toBeHidden();
// These elements should be rendered
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton
)
).toBeVisible();
// Switch off enabling toggle
const updateResponse = page.waitForResponse(
(response) =>
response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards/`) &&
response.request().method() === 'PATCH'
);
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton
)
).toBeEnabled();
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton)
.click({ force: true });
response = await updateResponse;
expect(response.status()).toBe(200);
publicDashboard = await response.json();
// Test that API access is now forbidden
apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`);
expect(apiResponse.status()).toBe(403);
});
}
);
@@ -0,0 +1,175 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
scenes: true,
newDashboardSharingComponent: true,
},
});
const DASHBOARD_UID = 'ZqZnVvFZz';
test.describe(
'Share internally',
{
tag: ['@dashboards'],
},
() => {
test.beforeEach(async ({ page }) => {
// Clear localStorage before each test
await page.evaluate(() => {
window.localStorage.removeItem('grafana.dashboard.link.shareConfiguration');
});
});
test('Create a locked time range short link', async ({ page, gotoDashboardPage, selectors }) => {
// Navigate to dashboard with specific time range
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }),
});
// Open share internally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
const createResponse = page.waitForResponse(
(response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST'
);
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally)
.click();
await expect(page).toHaveURL(/.*shareView=link.*/);
// Check that the required elements exist
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.copyUrlButton)
).toBeVisible();
// Check radio buttons
const radioButtons = dashboardPage.getByGrafanaSelector(selectors.components.RadioButton.container);
await expect(radioButtons).toHaveCount(3);
// Check localStorage is initially null
const initialConfig = await page.evaluate(() => {
return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
});
expect(initialConfig).toBeNull();
// Wait for the short URL creation
const response = await createResponse;
expect(response.status()).toBe(200);
const responseBody = await response.json();
expect(responseBody.url).toContain('goto');
});
test('Create a relative time range short link', async ({ page, gotoDashboardPage, selectors }) => {
// Navigate to dashboard with specific time range
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }),
});
// Open share internally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally)
.click();
const updateResponse = page.waitForResponse(
(response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST'
);
// Toggle the lock time range switch
const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch
);
await expect(lockTimeRangeSwitch).toBeInViewport();
await expect(async () => {
await lockTimeRangeSwitch.uncheck({ force: true });
}).toPass();
await expect(async () => {
// Check localStorage configuration
const shareConfig = await page.evaluate(() => {
const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
return config ? JSON.parse(config) : null;
});
expect(shareConfig).not.toBeNull();
expect(shareConfig.useAbsoluteTimeRange).toBe(false);
expect(shareConfig.useShortUrl).toBe(true);
expect(shareConfig.theme).toBe('current');
}).toPass();
// Wait for the API response
const response = await updateResponse;
expect(response.status()).toBe(200);
const responseBody = await response.json();
expect(responseBody.url).toContain('goto');
});
test('Share button gets configured link', async ({ page, gotoDashboardPage, selectors }) => {
// Navigate to dashboard with specific time range
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }),
});
// Open share internally drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally)
.click();
// Check localStorage is initially null
const initialConfig = await page.evaluate(() => {
return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
});
expect(initialConfig).toBeNull();
// Configure the sharing options
const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch
);
await expect(lockTimeRangeSwitch).toBeInViewport();
await expect(async () => {
await lockTimeRangeSwitch.uncheck({ force: true });
}).toPass();
const shortenUrlSwitch = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch
);
await expect(shortenUrlSwitch).toBeInViewport();
await expect(async () => {
await shortenUrlSwitch.uncheck({ force: true });
}).toPass();
// Close the drawer
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click();
await expect(page).not.toHaveURL(/.*shareView=link.*/);
await expect(async () => {
// Check that localStorage has been updated with the configuration
const finalConfig = await page.evaluate(() => {
const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration');
return config ? JSON.parse(config) : null;
});
expect(finalConfig).not.toBeNull();
expect(finalConfig.useAbsoluteTimeRange).toBe(false);
expect(finalConfig.useShortUrl).toBe(false);
expect(finalConfig.theme).toBe('current');
}).toPass();
});
}
);
@@ -0,0 +1,69 @@
import { test, expect } from '@grafana/plugin-e2e';
import { SnapshotCreateResponse } from '../../public/app/features/dashboard/services/SnapshotSrv';
test.use({
featureToggles: {
scenes: true,
newDashboardSharingComponent: true,
},
});
const DASHBOARD_UID = 'ZqZnVvFZz';
test.describe(
'Snapshots',
{
tag: ['@dashboards'],
},
() => {
test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => {
// Opening a dashboard
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
const panelsToCheck = [
'Raw Data Graph',
'Last non-null',
'min',
'Max',
'The data from graph above with seriesToColumns transform',
];
// Open the sharing drawer
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareSnapshot)
.click();
// Publish snapshot
const createSnapshotPromise = page.waitForResponse(
(response) => response.url().includes('/api/snapshots') && response.request().method() === 'POST'
);
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot)
.click();
const createResponse = await createSnapshotPromise;
expect(createResponse.status()).toBe(200);
const responseBody: SnapshotCreateResponse = await createResponse.json();
// Navigate to the snapshot URL
const snapshotUrl = getSnapshotUrl(responseBody.key);
await page.goto(snapshotUrl);
// Validate the dashboard controls are rendered
await expect(dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls)).toBeVisible();
// Validate the panels are rendered
for (const title of panelsToCheck) {
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title))).toBeVisible();
}
});
}
);
const getSnapshotUrl = (uid: string): string => {
return `/dashboard/snapshot/${uid}`;
};
@@ -0,0 +1,74 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = 'HYaGDGIMk';
test.use({
timezoneId: 'Pacific/Easter',
});
test.describe(
'Dashboard templating',
{
tag: ['@dashboards'],
},
() => {
test('Verify variable interpolation works', async ({ page, gotoDashboardPage }) => {
// Open dashboard global variables and interpolation
await gotoDashboardPage({ uid: DASHBOARD_UID });
// Get the actual timezone from the browser context (should be Pacific/Easter due to test.use)
const timeZone = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
const example = `Example: from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}`;
const expectedItems: string[] = [
'__dashboard = Templating - Global variables and interpolation',
'__dashboard.name = Templating - Global variables and interpolation',
'__dashboard.uid = HYaGDGIMk',
'__org.name = Main Org.',
'__org.id = 1',
'__user.id = 1',
'__user.login = admin',
'__user.email = admin@localhost',
`Server:raw = A'A"A,BB\\B,CCC`,
`Server:regex = (A'A"A|BB\\\\B|CCC)`,
`Server:lucene = ("A'A\\"A" OR "BB\\\\B" OR "CCC")`,
`Server:glob = {A'A"A,BB\\B,CCC}`,
`Server:pipe = A'A"A|BB\\B|CCC`,
`Server:distributed = A'A"A,Server=BB\\B,Server=CCC`,
`Server:csv = A'A"A,BB\\B,CCC`,
`Server:html = A&#39;A&quot;A, BB\\B, CCC`,
`Server:json = ["A'A\\"A","BB\\\\B","CCC"]`,
`Server:percentencode = %7BA%27A%22A%2CBB%5CB%2CCCC%7D`,
`Server:singlequote = 'A\\'A"A','BB\\B','CCC'`,
`Server:doublequote = "A'A\\"A","BB\\B","CCC"`,
`Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`,
`Server:date = NaN`,
`Server:text = All`,
`Server:queryparam = var-Server=$__all`,
`1 < 2`,
example,
];
// Get all list items from the markdown content
const listItems = page.locator('.markdown-html li');
// Verify we have the expected number of items
await expect(listItems).toHaveCount(26);
// Get all the text content from list items
const actualItems = await listItems.allTextContents();
// Compare each expected item with actual items
for (let i = 0; i < expectedItems.length; i++) {
expect(actualItems[i]).toBe(expectedItems[i]);
}
// Check link interpolation is working correctly
const exampleLink = page.locator(`a:has-text("${example}")`);
await expect(exampleLink).toHaveAttribute(
'href',
`https://example.com/?from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}`
);
});
}
);
@@ -0,0 +1,97 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = '5SdHCasdf';
const USER = 'dashboard-timepicker-test';
const PASSWORD = 'dashboard-timepicker-test';
// Separate user to isolate changes from other tests
test.use({
user: {
user: USER,
password: PASSWORD,
},
storageState: {
cookies: [],
origins: [],
},
});
test.describe(
'Dashboard timepicker',
{
tag: ['@dashboards'],
},
() => {
test('Shows the correct calendar days with custom timezone set via preferences', async ({
createUser,
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
await createUser();
// login manually for now
await page.getByTestId(selectors.pages.Login.username).fill(USER);
await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD);
await page.getByTestId(selectors.pages.Login.submit).click();
await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible();
// Set user preferences for timezone
await page.goto('/profile');
await page.getByTestId(selectors.components.TimeZonePicker.containerV2).click();
await page.getByRole('option', { name: 'Asia/Tokyo' }).click();
await page.getByTestId(selectors.components.UserProfile.preferencesSaveButton).click();
// Open dashboard with time range from 8th to end of 10th.
// Will be Tokyo time because of above preference
await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({
timezone: 'Default',
}),
});
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click();
// Assert that the calendar shows 08 and 09 and 10 as selected days
const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive');
await expect(activeTiles).toHaveCount(3);
});
test('Shows the correct calendar days with custom timezone set via time picker', async ({
createUser,
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
await createUser();
// login manually for now
await page.getByTestId(selectors.pages.Login.username).fill(USER);
await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD);
await page.getByTestId(selectors.pages.Login.submit).click();
await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible();
// Open dashboard with time range from 2022-06-08 00:00:00 to 2022-06-10 23:59:59 in Tokyo time
await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({
timezone: 'Asia/Tokyo',
}),
});
// Open dashboard with time range from 8th to end of 10th.
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click();
// Assert that the calendar shows 08 and 09 and 10 as selected days
const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive');
await expect(activeTiles).toHaveCount(3);
});
}
);
@@ -0,0 +1,26 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Embedded dashboard',
{
tag: ['@dashboards'],
},
() => {
test('open test page', async ({ page, dashboardPage, selectors }) => {
await page.goto('/dashboards/embedding-test');
// Verify pie charts are rendered
const pieChartSlices = page.locator(
`[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]`
);
await expect(pieChartSlices).toHaveCount(5);
// Verify no url sync
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click();
await page.locator('label').filter({ hasText: 'Last 1 hour' }).click();
// Verify URL remains the same (no sync)
expect(page.url()).toMatch(/\/dashboards\/embedding-test$/);
});
}
);
@@ -0,0 +1,47 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
test.describe(
'Dashboards',
{
tag: ['@dashboards'],
},
() => {
test('should restore scroll position', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
// Verify first panel is visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1'))
).toBeVisible();
// Scroll to the bottom
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
// The last panel should be visible...
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50'))
).toBeVisible();
// Then we open and close the panel editor
// Click on panel menu (it only shows on hover)
await dashboardPage
.getByGrafanaSelector(selectors.components.Panels.Panel.menu('Panel #50'))
.click({ force: true });
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click();
// Go back to dashboard
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
// The last panel should still be visible!
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50'))
).toBeVisible();
});
}
);
@@ -0,0 +1,63 @@
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/TestDashboard.json';
test.describe(
'Import Dashboards Test',
{
tag: ['@dashboards'],
},
() => {
let dashboardUID: string;
test('Ensure you can import a number of json test dashboards from a specific test directory', async ({
page,
dashboardPage,
selectors,
}) => {
await page.goto('/dashboard/import');
// Fill in the dashboard JSON and name
const textarea = dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.textarea);
await textarea.fill(JSON.stringify(testDashboard));
// Submit the JSON
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.submit).click();
// Fill in the dashboard name and submit
const nameField = dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.name);
await expect(nameField).toBeVisible();
await nameField.click();
await nameField.clear();
await nameField.fill(testDashboard.title);
await dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.submit).click();
// Wait for dashboard to load and extract UID from URL
await page.waitForURL('**/d/**');
const url = page.url();
const urlParts = url.split('/d/');
if (urlParts.length > 1) {
dashboardUID = urlParts[1].split('/')[0];
}
// Verify the dashboard title is present in the breadcrumbs
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb(testDashboard.title))
).toBeVisible();
// Verify that specific panels from the test dashboard are loaded
await expect(page.getByText('Gauge Example')).toBeVisible();
await expect(page.getByText('Stat')).toBeVisible();
await expect(page.getByText('Time series example')).toBeVisible();
});
test.afterEach(async ({ request }) => {
// Clean up the imported dashboard using API
if (dashboardUID) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
dashboardUID = '';
}
});
}
);
@@ -0,0 +1,300 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
test.describe(
'Variables - Load options from Url',
{
tag: ['@dashboards'],
},
() => {
test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
// Test first variable (A)
const firstVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A'))
.locator('input');
await expect(firstVariableInput).toBeVisible();
await firstVariableInput.click();
// Check dropdown has 10 options
const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(firstDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test second variable (AA)
const secondVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA'))
.locator('input');
await expect(secondVariableInput).toBeVisible();
await secondVariableInput.click();
// Check dropdown has 10 options
const secondDropdownOptions = dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.locator('..');
await expect(secondDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test third variable ($__all)
const thirdVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all'))
.locator('input');
await expect(thirdVariableInput).toBeVisible();
await thirdVariableInput.click();
// Check dropdown has 10 options
const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(thirdDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAC'))
).toBeVisible();
});
test('options set in url should load correct options', async ({
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({
orgId: '1',
'var-datacenter': 'B',
'var-server': 'BB',
'var-pod': 'BBB',
}),
});
// Test first variable (B) - should be selected from URL
const firstVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B'))
.locator('input');
await expect(firstVariableInput).toBeVisible();
await firstVariableInput.click();
// Check dropdown has 10 options
const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(firstDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test second variable (BB) - should be selected from URL
const secondVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB'))
.locator('input');
await expect(secondVariableInput).toBeVisible();
await secondVariableInput.click();
// Check dropdown has 10 options
const secondDropdownOptions = dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.locator('..');
await expect(secondDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Test third variable (BBB) - should be selected from URL
const thirdVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB'))
.locator('input');
await expect(thirdVariableInput).toBeVisible();
await thirdVariableInput.click();
// Check dropdown has 10 options
const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(thirdDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC'))
).toBeVisible();
});
test('options set in url that do not exist should load correct options', async ({
page,
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
// Handle uncaught exceptions that are expected
page.on('pageerror', (error) => {
if (error.message.includes("Couldn't find any field of type string in the results.")) {
// This error is expected and should not fail the test
return;
}
throw error;
});
await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({
orgId: '1',
'var-datacenter': 'X',
}),
});
// Test first variable (X) - invalid value from URL
const firstVariableInput = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X'))
.locator('input');
await expect(firstVariableInput).toBeVisible();
await firstVariableInput.click();
// Check dropdown has 10 options
const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(firstDropdownOptions).toHaveCount(10);
// Check toggle shows 'Selected (1)'
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
// Check specific options are visible (should fall back to default options)
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C'))
).toBeVisible();
// Close dropdown by clicking outside
await page.locator('body').click();
// Check that second variable shows $__all (should be 2 instances)
const allVariables = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')
);
await expect(allVariables).toHaveCount(2);
await expect(allVariables.first()).toBeVisible();
await expect(allVariables.last()).toBeVisible();
});
}
);
@@ -0,0 +1,65 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Variables - Constant',
{
tag: ['@dashboards'],
},
() => {
test('can add a new constant variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Constant" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
// Select "Constant" type from dropdown
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Constant');
await typeSelect.locator('input').press('Enter');
// Set variable name
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill('VariableUnderTest');
// Set constant value
const constantInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2
);
await constantInput.fill('pesto');
// Set label
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('Variable under test');
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click();
// Assert the variable was rendered in the markdown content
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: pesto');
// Assert the variable is not visible in the dashboard navigation (constant variables are hidden)
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test'))
).toBeHidden();
});
}
);
@@ -0,0 +1,121 @@
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
async function fillInCustomVariable(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
name: string,
label: string,
value: string
) {
// Select "Custom" type from dropdown
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Custom');
await typeSelect.locator('input').press('Enter');
// Set variable name
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill(name);
// Set label
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill(label);
// Set custom values
const customValueInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
);
await customValueInput.fill(value);
await customValueInput.blur();
}
async function assertPreviewValues(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
expectedValues: string[]
) {
for (let i = 0; i < expectedValues.length; i++) {
const previewOption = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption)
.nth(i);
await expect(previewOption).toHaveText(expectedValues[i]);
}
}
test.describe(
'Variables - Custom',
{
tag: ['@dashboards'],
},
() => {
test('can add a custom template variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Custom" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
await fillInCustomVariable(dashboardPage, selectors, 'VariableUnderTest', 'Variable under test', 'one,two,three');
await assertPreviewValues(dashboardPage, selectors, ['one', 'two', 'three']);
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one'))
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'two' }).click();
// Assert it was rendered
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: two');
});
test('can add a custom template variable with labels', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Custom" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
await fillInCustomVariable(
dashboardPage,
selectors,
'VariableUnderTest',
'Variable under test',
'One : 1,Two : 2, Three : 3'
);
await assertPreviewValues(dashboardPage, selectors, ['One', 'Two', 'Three']);
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1'))
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Two' }).click();
// Assert it was rendered (the value "2" should be displayed, not the label "Two")
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 2');
});
}
);
@@ -0,0 +1,74 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Variables - Datasource',
{
tag: ['@dashboards'],
},
() => {
test('can add a new datasource variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Datasource" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Data source');
await typeSelect.locator('input').press('Enter');
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill('VariableUnderTest');
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('Variable under test');
// If this is failing, make sure there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus"
// Or update to match available gdev datasources for testing
const datasourceSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect
);
await datasourceSelect.locator('input').fill('Prometheus');
await datasourceSelect.locator('input').press('Enter');
const previewOptions = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption
);
await expect(previewOptions.first()).toContainText('gdev-prometheus');
await expect(previewOptions.last()).toContainText('gdev-slow-prometheus');
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click();
// Change the selected variable value
await dashboardPage
.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus')
)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.Select.option)
.filter({ hasText: 'gdev-slow-prometheus' })
.click();
// Assert it was rendered
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: gdev-slow-prometheus-uid');
await expect(page.locator('.markdown-html').nth(1)).toContainText('VariableUnderTestText: gdev-slow-prometheus');
});
}
);
@@ -0,0 +1,84 @@
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
async function assertPreviewValues(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
expectedValues: string[]
) {
for (let i = 0; i < expectedValues.length; i++) {
const previewOption = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption)
.nth(i);
await expect(previewOption).toHaveText(expectedValues[i]);
}
}
test.describe(
'Variables - Interval',
{
tag: ['@dashboards'],
},
() => {
test('can add a new interval variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "Interval" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Interval');
await typeSelect.locator('input').press('Enter');
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill('VariableUnderTest');
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('Variable under test');
// Set interval values
const intervalInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput
);
await intervalInput.fill('10s,10m,60m,90m,1h30m');
await intervalInput.blur();
await assertPreviewValues(dashboardPage, selectors, ['10s', '10m', '60m', '90m', '1h30m']);
// Navigate back to the homepage and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click();
// Verify the variable label and initial value, then change the selected value
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test')
);
// Find the value element (next sibling) and verify it shows the default value
const variableValue = variableLabel.locator('+ div');
await expect(variableValue).toHaveText('10s');
// Click to open the dropdown
await variableValue.click();
// Select '1h30m' from the dropdown
await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: '1h30m' }).click();
// Assert it was rendered
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 1h30m');
});
}
);
@@ -0,0 +1,251 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
const DASHBOARD_NAME = 'Templating - Nested Template Variables';
test.describe(
'Variables - Query - Add variable',
{
tag: ['@dashboards'],
},
() => {
test('query variable should be default and default fields should be correct', async ({
page,
gotoDashboardPage,
selectors,
}) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click();
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await expect(nameInput).toBeVisible();
await expect(nameInput).toHaveAttribute('placeholder', 'Variable name');
await expect(nameInput).toHaveValue('query0');
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await expect(typeSelect).toBeVisible();
const singleValue = typeSelect.locator('div[class*="-singleValue"]');
await expect(singleValue).toHaveText('Query');
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await expect(labelInput).toBeVisible();
await expect(labelInput).toHaveAttribute('placeholder', 'Label name');
await expect(labelInput).toHaveValue('');
const descriptionInput = page.locator('[placeholder="Descriptive text"]');
await expect(descriptionInput).toBeVisible();
await expect(descriptionInput).toHaveAttribute('placeholder', 'Descriptive text');
await expect(descriptionInput).toHaveValue('');
await expect(page.locator('label').filter({ hasText: 'Hide' })).toBeVisible();
// Check datasource selector
const datasourceSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect
);
await expect(datasourceSelect).toBeVisible();
await expect(datasourceSelect).toHaveAttribute('placeholder', 'gdev-testdata');
await expect(page.locator('label').filter({ hasText: 'Refresh' })).toBeVisible();
await expect(page.locator('label').filter({ hasText: 'On dashboard load' })).toBeVisible();
const regexInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
await expect(regexInput).toBeVisible();
await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?<text>.*)-(?<value>.*)-.*/');
await expect(regexInput).toHaveValue('');
const sortSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
);
await expect(sortSelect).toBeVisible();
const sortSingleValue = sortSelect.locator('div[class*="-singleValue"]');
await expect(sortSingleValue).toHaveText('Disabled');
// Check Multi-value checkbox
const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' });
const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]');
await expect(multiValueCheckbox).toBeChecked({
checked: false,
});
// Check Include All option checkbox
const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' });
const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]');
await expect(includeAllCheckbox).toBeChecked({
checked: false,
});
// Check preview and custom all input don't exist initially
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption
)
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
)
).toBeHidden();
});
test('adding a single value query variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Click the "New" button to add a variable
await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click();
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('a label');
const descriptionInput = page.locator('[placeholder="Descriptive text"]');
await descriptionInput.fill('a description');
const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill('gdev-testdata');
const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText('gdev-testdata').click();
const queryInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput
);
await queryInput.fill('*');
const regexInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
await regexInput.fill('/.*C.*/');
await regexInput.blur();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption
)
).toBeVisible();
// Navigate back to dashboard
const submitButton = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton
);
await submitButton.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
// Check variable appears in submenu
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label'))
).toBeVisible();
const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem);
await expect(submenuItems).toHaveCount(4);
const fourthItem = submenuItems.nth(3);
const input = fourthItem.locator('input');
await input.click();
// Check dropdown has 1 option containing 'C'
const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option);
await expect(options).toHaveCount(1);
await expect(options).toContainText('C');
});
test('adding a multi value query variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click();
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('a label');
const descriptionInput = page.locator('[placeholder="Descriptive text"]');
await descriptionInput.fill('a description');
const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2);
await datasourcePicker.fill('gdev-testdata');
const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList);
await datasourceList.getByText('gdev-testdata').click();
const queryInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput
);
await queryInput.fill('*');
const regexInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
await regexInput.fill('/.*C.*/');
// Enable Multi-value
const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' });
const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]');
await multiValueCheckbox.click({ force: true });
await expect(multiValueCheckbox).toBeChecked();
// Enable Include All option
const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' });
const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]');
await includeAllCheckbox.click({ force: true });
await expect(includeAllCheckbox).toBeChecked();
const customAllInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
);
await expect(customAllInput).toHaveValue('');
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption
)
).toHaveCount(2);
// Navigate back to dashboard
const submitButton = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton
);
await submitButton.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
// Check variable appears in submenu
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label'))
).toBeVisible();
const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem);
await expect(submenuItems).toHaveCount(4);
const fourthItem = submenuItems.nth(3);
const input = fourthItem.locator('input');
await input.click();
// Check dropdown has 3 options (All + filtered results)
const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option);
await expect(options).toHaveCount(3);
// Check toggle shows Selected (1)
const toggleAll = dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions);
await expect(toggleAll).toHaveText('Selected (1)');
// Check options contain 'All' and 'C'
await expect(options.filter({ hasText: 'All' })).toBeVisible();
// need to use regex here else this will also match "Selected"
await expect(options.filter({ hasText: /^C$/ })).toBeVisible();
});
}
);
@@ -0,0 +1,56 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
test.describe(
'Variables - Text box',
{
tag: ['@dashboards'],
},
() => {
test('can add a new text box variable', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
});
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
// Create a new "text box" variable
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
const typeSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2
);
await typeSelect.locator('input').fill('Textbox');
await typeSelect.locator('input').press('Enter');
const nameInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2
);
await nameInput.fill('VariableUnderTest');
const labelInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2
);
await labelInput.fill('Variable under test');
const textBoxInput = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2
);
await textBoxInput.fill('cat-dog');
// Navigate back to the dashboard and change the selected variable value
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem);
const textInput = submenuItem.locator('input');
await textInput.fill('dog-cat');
await textInput.blur();
// Assert it was rendered
await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: dog-cat');
});
}
);
@@ -0,0 +1,132 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally';
test.describe(
'Repeating a panel horizontally',
{
tag: ['@dashboards'],
},
() => {
test('should be able to repeat a panel horizontally', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
let prevLeft = Number.NEGATIVE_INFINITY;
let prevTop: number | null = null;
const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3'];
for (const title of panelTitles) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
const boundingBox = await panel.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { x: left, y: top } = boundingBox;
expect(left).toBeGreaterThan(prevLeft);
if (prevTop !== null) {
expect(top).toBe(prevTop);
}
prevLeft = left;
prevTop = top;
}
}
});
test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3'];
// Verify all panels are initially visible
for (const title of panelTitles) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
}
// Change to only show panels 1 + 3
const horizontalVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('horizontal'))
.locator('..')
.locator('input');
await horizontalVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1'))
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3'))
.click();
// blur the dropdown
await page.locator('body').click();
// Verify positioning of shown panels
let prevLeft = Number.NEGATIVE_INFINITY;
let prevTop: number | null = null;
const panelsShown = ['Panel Title 1', 'Panel Title 3'];
const panelsNotShown = ['Panel Title 2'];
for (const title of panelsShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
const boundingBox = await panel.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { x: left, y: top } = boundingBox;
expect(left).toBeGreaterThan(prevLeft);
if (prevTop !== null) {
expect(top).toBe(prevTop);
}
prevLeft = left;
prevTop = top;
}
}
for (const title of panelsNotShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeHidden();
}
});
test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => {
// Have to manually add the queryParams to the url because they have the same name
const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-horizontal=1&var-horizontal=3` });
let prevLeft = Number.NEGATIVE_INFINITY;
let prevTop: number | null = null;
const panelsShown = ['Panel Title 1', 'Panel Title 3'];
const panelsNotShown = ['Panel Title 2'];
// Check correct panels are displayed with proper positioning
for (const title of panelsShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
const boundingBox = await panel.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { x: left, y: top } = boundingBox;
expect(left).toBeGreaterThan(prevLeft);
if (prevTop !== null) {
expect(top).toBe(prevTop);
}
prevLeft = left;
prevTop = top;
}
}
for (const title of panelsNotShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeHidden();
}
});
}
);
@@ -0,0 +1,132 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically';
test.describe(
'Repeating a panel vertically',
{
tag: ['@dashboards'],
},
() => {
test('should be able to repeat a panel vertically', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
let prevTop = Number.NEGATIVE_INFINITY;
let prevLeft: number | null = null;
const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3'];
for (const title of panelTitles) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
const boundingBox = await panel.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { x: left, y: top } = boundingBox;
expect(top).toBeGreaterThan(prevTop);
if (prevLeft !== null) {
expect(left).toBe(prevLeft);
}
prevLeft = left;
prevTop = top;
}
}
});
test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3'];
// Verify all panels are initially visible
for (const title of panelTitles) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
}
// Change to only show panels 1 + 3
const verticalVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('vertical'))
.locator('..')
.locator('input');
await verticalVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1'))
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3'))
.click();
// blur the dropdown
await page.locator('body').click();
// Verify positioning of shown panels
let prevTop = Number.NEGATIVE_INFINITY;
let prevLeft: number | null = null;
const panelsShown = ['Panel Title 1', 'Panel Title 3'];
const panelsNotShown = ['Panel Title 2'];
for (const title of panelsShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
const boundingBox = await panel.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { x: left, y: top } = boundingBox;
expect(top).toBeGreaterThan(prevTop);
if (prevLeft !== null) {
expect(left).toBe(prevLeft);
}
prevLeft = left;
prevTop = top;
}
}
for (const title of panelsNotShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeHidden();
}
});
test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => {
// Have to manually add the queryParams to the url because they have the same name
const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-vertical=1&var-vertical=3` });
let prevTop = Number.NEGATIVE_INFINITY;
let prevLeft: number | null = null;
const panelsShown = ['Panel Title 1', 'Panel Title 3'];
const panelsNotShown = ['Panel Title 2'];
// Check correct panels are displayed with proper positioning
for (const title of panelsShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeVisible();
const boundingBox = await panel.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { x: left, y: top } = boundingBox;
expect(top).toBeGreaterThan(prevTop);
if (prevLeft !== null) {
expect(left).toBe(prevLeft);
}
prevLeft = left;
prevTop = top;
}
}
for (const title of panelsNotShown) {
const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title));
await expect(panel).toBeHidden();
}
});
}
);
@@ -0,0 +1,114 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row';
test.describe(
'Repeating empty rows',
{
tag: ['@dashboards'],
},
() => {
test('should be able to repeat empty rows vertically', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
let prevTop = Number.NEGATIVE_INFINITY;
const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3'];
for (const title of rowTitles) {
const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title));
await expect(row).toBeVisible();
const boundingBox = await row.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { y: top } = boundingBox;
expect(top).toBeGreaterThan(prevTop);
prevTop = top;
}
}
});
test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3'];
// Verify all rows are initially visible
for (const title of rowTitles) {
const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title));
await expect(row).toBeVisible();
}
// Change to only show rows 1 + 3
const rowVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('row'))
.locator('..')
.locator('input');
await rowVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1'))
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3'))
.click();
// blur the dropdown
await page.locator('body').click();
// Verify positioning of shown rows
let prevTop = Number.NEGATIVE_INFINITY;
const rowsShown = ['Row title 1', 'Row title 3'];
const rowsNotShown = ['Row title 2'];
for (const title of rowsShown) {
const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title));
await expect(row).toBeVisible();
const boundingBox = await row.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { y: top } = boundingBox;
expect(top).toBeGreaterThan(prevTop);
prevTop = top;
}
}
for (const title of rowsNotShown) {
const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title));
await expect(row).toBeHidden();
}
});
test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => {
// Have to manually add the queryParams to the url because they have the same name
const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-row=1&var-row=3` });
let prevTop = Number.NEGATIVE_INFINITY;
const rowsShown = ['Row title 1', 'Row title 3'];
const rowsNotShown = ['Row title 2'];
// Check correct rows are displayed with proper positioning
for (const title of rowsShown) {
const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title));
await expect(row).toBeVisible();
const boundingBox = await row.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
const { y: top } = boundingBox;
expect(top).toBeGreaterThan(prevTop);
prevTop = top;
}
}
for (const title of rowsNotShown) {
const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title));
await expect(row).toBeHidden();
}
});
}
);
@@ -0,0 +1,280 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
test.describe(
'Variables - Set options from ui',
{
tag: ['@dashboards'],
},
() => {
test('clicking a value that is not part of dependents options should change these to All', async ({
gotoDashboardPage,
selectors,
page,
}) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({
orgId: '1',
'var-datacenter': 'A',
'var-server': 'AA',
'var-pod': 'AAA',
}),
});
const datacenterVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A'))
.locator('..')
.locator('input');
await expect(datacenterVariable).toBeVisible();
await datacenterVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
.click();
await page.locator('body').click({ position: { x: 0, y: 0 } });
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')
)
).toBeVisible();
const serverVariableAllOptions = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')
);
await expect(serverVariableAllOptions).toHaveCount(2);
const firstServerDropdown = serverVariableAllOptions.nth(0);
await expect(firstServerDropdown).toBeVisible();
await firstServerDropdown.locator('input').click();
const selectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(selectOptions).toHaveCount(10);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (1)'
);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC'))
).toBeVisible();
await page.locator('body').click({ position: { x: 0, y: 0 } });
const podVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('pod'))
.locator('..')
.locator('input');
await podVariable.click();
// length is 11 because of virtualized select options
const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..');
await expect(podSelectOptions).toHaveCount(11);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAC'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAD'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAE'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAF'))
).toBeVisible();
});
test('adding a value that is not part of dependents options should add the new values dependant options', async ({
gotoDashboardPage,
selectors,
page,
}) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({
orgId: '1',
'var-datacenter': 'A',
'var-server': 'AA',
'var-pod': 'AAA',
}),
});
// Click on datacenter variable dropdown
const datacenterVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A'))
.locator('input');
await expect(datacenterVariable).toBeVisible();
await datacenterVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
.click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText(
'Selected (2)'
);
// Close dropdown
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Verify datacenter shows "A,B"
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B')
)
).toBeVisible();
const serverVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA'))
.locator('input');
await expect(serverVariable).toBeVisible();
await serverVariable.click();
const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option);
await expect(serverSelectOptions).toHaveCount(11);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD'))
).toBeVisible();
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Click on pod variable dropdown
const podVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA'))
.locator('input');
await expect(podVariable).toBeVisible();
await podVariable.click();
// Verify pod dropdown has 10 options
const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option);
await expect(podSelectOptions).toHaveCount(10);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAC'))
).toBeVisible();
});
test('removing a value that is part of dependents options should remove the new values dependant options', async ({
gotoDashboardPage,
selectors,
page,
}) => {
const dashboardPage = await gotoDashboardPage({
// can't use queryParams here because it won't work with the multiple values for each variable
uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`,
});
// Click on datacenter variable dropdown
const datacenterVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B'))
.locator('input');
await expect(datacenterVariable).toBeVisible();
await datacenterVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A'))
.click();
// Close dropdown
await page.locator('body').click({ position: { x: 0, y: 0 } });
await expect(
dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')
)
).toBeVisible();
// Click on server variable dropdown
const serverVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB'))
.locator('input');
await expect(serverVariable).toBeVisible();
await serverVariable.click();
const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option);
await expect(serverSelectOptions).toHaveCount(10);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC'))
).toBeVisible();
// Close dropdown
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Click on pod variable dropdown
const podVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB'))
.locator('input');
await expect(podVariable).toBeVisible();
await podVariable.click();
const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option);
await expect(podSelectOptions).toHaveCount(10);
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC'))
).toBeVisible();
});
}
);
@@ -0,0 +1,69 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = 'ZqZnVvFZz';
test.use({
featureToggles: {
newDashboardSharingComponent: false, // Use legacy sharing component for this test
},
});
test.describe(
'Snapshots',
{
tag: ['@dashboards'],
},
() => {
test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
});
const panelsToCheck = [
'Raw Data Graph',
'Last non-null',
'min',
'Max',
'The data from graph above with seriesToColumns transform',
];
// Open the sharing modal
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click();
// Select the snapshot tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Snapshot')).click();
// Publish snapshot
await dashboardPage
.getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.PublishSnapshot)
.click();
// Copy link button should be visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlButton)
).toBeVisible();
// Get the snapshot URL from the input field
const urlInput = dashboardPage.getByGrafanaSelector(
selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlInput
);
const snapshotUrl = await urlInput.inputValue();
// Extract the snapshot key from the URL and navigate to the snapshot
const snapshotKey = getSnapshotKey(snapshotUrl);
await page.goto(`/dashboard/snapshot/${snapshotKey}`);
// Validate the dashboard controls are rendered
await expect(dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls)).toBeVisible();
// Validate the panels are rendered
for (const title of panelsToCheck) {
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title))).toBeVisible();
}
});
}
);
const getSnapshotKey = (url: string): string => {
return url.split('/').pop() || '';
};
@@ -0,0 +1,64 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = 'yBCC3aKGk';
test.describe(
'Templating dashboard links and variables',
{
tag: ['@dashboards'],
},
() => {
test('Tests dashboard links and variables in links', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
const verifyLinks = async (variableValue: string) => {
const dashboardLinks = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.link);
let linkCount = 0;
await expect
.poll(async () => {
linkCount = await dashboardLinks.count();
return linkCount;
})
.toBeGreaterThan(13);
for (let i = 0; i < linkCount; i++) {
const href = await dashboardLinks.nth(i).getAttribute('href');
expect(href).toContain(variableValue);
}
};
// Click on dashboard links dropdown
const dashboardLinksDropdown = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.dropDown);
await expect(dashboardLinksDropdown).toBeVisible();
await dashboardLinksDropdown.click();
// Verify links contain default variable value
await verifyLinks('var-custom=$__all');
// Close dropdown
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Change variable value from $__all to p2
const customVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all'))
.locator('input');
await expect(customVariable).toBeVisible();
await customVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2'))
.click();
// Close dropdown
await page.locator('body').click({ position: { x: 0, y: 0 } });
// Open dashboard links dropdown again
await expect(dashboardLinksDropdown).toBeVisible();
await dashboardLinksDropdown.click();
// Verify all links now contain the p2 value
await verifyLinks('p2');
});
}
);
@@ -0,0 +1,56 @@
import { Page } from 'playwright-core';
import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'AejrN1AMz';
test.describe(
'TextBox - load options scenarios',
{
tag: ['@dashboards'],
},
() => {
test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
await validateTextboxAndMarkup(page, dashboardPage, selectors, 'default value');
});
test('loading variable from url should be correct', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: PAGE_UNDER_TEST,
queryParams: new URLSearchParams({
'var-text': 'not default value',
}),
});
await validateTextboxAndMarkup(page, dashboardPage, selectors, 'not default value');
});
}
);
// Helper function to validate textbox and markup
async function validateTextboxAndMarkup(
page: Page,
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
value: string
) {
const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem);
await expect(submenuItem).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('text'))
).toBeVisible();
const textInput = submenuItem.locator('input');
await expect(textInput).toBeVisible();
await expect(textInput).toHaveValue(value);
const textPanel = page.locator(selectors.components.Panels.Visualization.Text.container(''));
await expect(textPanel).toBeVisible();
const headerElement = textPanel.locator('h1');
await expect(headerElement).toBeVisible();
await expect(headerElement).toHaveText(`variable: ${value}`);
}
@@ -0,0 +1,58 @@
export function makeNewDashboardRequestBody(dashboardName: string, folderUid?: string) {
return {
dashboard: {
annotations: {
list: [
{
builtIn: 1,
datasource: { type: 'grafana', uid: '-- Grafana --' },
enable: true,
hide: true,
iconColor: 'rgba(0, 211, 255, 1)',
name: 'Annotations & Alerts',
type: 'dashboard',
},
],
},
editable: true,
fiscalYearStartMonth: 0,
graphTooltip: 0,
links: [],
liveNow: false,
panels: [
{
datasource: { type: 'testdata', uid: '89_jzlT4k' },
gridPos: { h: 9, w: 12, x: 0, y: 0 },
id: 2,
options: {
code: {
language: 'plaintext',
showLineNumbers: false,
showMiniMap: false,
},
content: '***A nice little happy empty dashboard***',
mode: 'markdown',
},
pluginVersion: '9.4.0-pre',
title: 'Nothing to see here',
type: 'text',
},
],
refresh: '',
revision: 1,
schemaVersion: 38,
tags: [],
templating: { list: [] },
time: { from: 'now-6h', to: 'now' },
timepicker: {},
timezone: '',
title: dashboardName,
version: 0,
uid: '',
weekStart: '',
},
message: '',
overwrite: false,
folderUid,
} as const;
}
@@ -0,0 +1,107 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": ["sum"],
"show": false
},
"showHeader": true
},
"pluginVersion": "10.3.0-pre",
"targets": [
{
"channel": "plugin/testdata/random-20Hz-stream",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "measurements",
"refId": "A"
}
],
"title": "Live",
"type": "table"
}
],
"refresh": "",
"schemaVersion": 39,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "New dashboard",
"version": 0,
"uid": "live-e2e-test",
"weekStart": ""
}
@@ -0,0 +1,255 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"title": "",
"url": "/d/${__dashboard.uid}?var-instance=${__data.fields.test1}&${__url_time_range}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 4,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": ["sum"],
"show": false
},
"showHeader": true
},
"pluginVersion": "11.6.0-pre",
"targets": [
{
"alias": "test1",
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv"
}
],
"title": "Data links without slug",
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "11.6.0-pre",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"disableTextWrap": false,
"editorMode": "builder",
"expr": "counters_logins{geohash=\"$instance\"}",
"fullMetaSearch": false,
"includeNullMetadata": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
}
],
"title": "Panel Title",
"type": "timeseries"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": "9wvfgzurfzb",
"value": "9wvfgzurfzb"
},
"name": "instance",
"options": [
{
"selected": true,
"text": "9wvfgzurfzb",
"value": "9wvfgzurfzb"
},
{
"selected": false,
"text": "9yy21uzzxypg",
"value": "9yy21uzzxypg"
},
{
"selected": false,
"text": "dr199bpvpcru",
"value": "dr199bpvpcru"
},
{
"selected": false,
"text": "dre33fzyxcrz",
"value": "dre33fzyxcrz"
},
{
"selected": false,
"text": "gc6j7crvrcpf",
"value": "gc6j7crvrcpf"
},
{
"selected": false,
"text": "u6g9zuxvxypv",
"value": "u6g9zuxvxypv"
}
],
"query": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv",
"type": "custom"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "utc",
"title": "Data Link without slug test",
"uid": "data-link-no-slug",
"version": 3
}
@@ -0,0 +1,57 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"title": "Sandbox Panel test",
"type": "sandbox-test-panel"
}
],
"refresh": "",
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Sandbox Panel Test",
"uid": "c46b2460-16b7-42a5-82d1-b07fbf431950",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,208 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 6,
"options": {
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true,
"text": {}
},
"pluginVersion": "8.3.0-pre",
"title": "Gauge Example",
"type": "gauge"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.3.0-pre",
"title": "Stat",
"type": "stat"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 16
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"title": "Time series example",
"type": "timeseries"
}
],
"refresh": false,
"schemaVersion": 31,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "2021-09-01T04:00:00.000Z",
"to": "2021-09-15T04:00:00.000Z"
},
"timepicker": {},
"timezone": "",
"title": "E2E Test - Import Dashboard",
"uid": "kquZN5H7k",
"version": 4
}
@@ -0,0 +1,424 @@
{
"apiVersion": "dashboard.grafana.app/v2alpha1",
"kind": "Dashboard",
"metadata": {
"name": "admjzp8",
"namespace": "default",
"uid": "hrbekBWXeM7nC7GtouIbcngFyuqt8Xx7KlE4AnTwV7AX",
"resourceVersion": "1",
"generation": 1,
"creationTimestamp": "2025-05-16T09:41:56Z",
"labels": {
"grafana.app/deprecatedInternalID": "182"
},
"annotations": {
"grafana.app/createdBy": "user:cejvsh18uudxcf",
"grafana.app/updatedBy": "user:cejvsh18uudxcf",
"grafana.app/updatedTimestamp": "2025-05-16T09:41:56Z",
"grafana.app/folder": ""
}
},
"spec": {
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"builtIn": true,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts"
}
}
],
"cursorSync": "Off",
"description": "",
"editable": true,
"elements": {
"panel-1": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"hidden": false,
"query": {
"kind": "grafana-testdata-datasource",
"spec": {}
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 1,
"links": [],
"title": "New panel",
"vizConfig": {
"kind": "timeseries",
"spec": {
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.1.0-pre"
}
}
}
},
"panel-2": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"hidden": false,
"query": {
"kind": "grafana-testdata-datasource",
"spec": {}
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 2,
"links": [],
"title": "New panel",
"vizConfig": {
"kind": "timeseries",
"spec": {
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.1.0-pre"
}
}
}
},
"panel-3": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"hidden": false,
"query": {
"kind": "grafana-testdata-datasource",
"spec": {}
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 3,
"links": [],
"title": "New panel",
"vizConfig": {
"kind": "timeseries",
"spec": {
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.1.0-pre"
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-1"
},
"height": 8,
"width": 12,
"x": 0,
"y": 0
}
},
{
"kind": "GridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-2"
},
"height": 8,
"width": 12,
"x": 12,
"y": 0
}
},
{
"kind": "GridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-3"
},
"height": 8,
"width": 12,
"x": 0,
"y": 8
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [],
"timeSettings": {
"autoRefresh": "",
"autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"fiscalYearStartMonth": 0,
"from": "now-6h",
"hideTimepicker": false,
"timezone": "browser",
"to": "now"
},
"title": "New Test V2 Dashboard",
"variables": []
},
"status": {}
}
@@ -0,0 +1,323 @@
{
"results": {
"A": {
"frames": [
{
"schema": {
"name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))",
"refId": "A",
"meta": { "custom": { "resultType": "matrix" } },
"fields": [
{ "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } },
{
"name": "Value",
"type": "number",
"typeInfo": { "frame": "float64" },
"labels": {},
"config": {
"displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))"
}
}
]
},
"data": {
"values": [
[
1633619595000, 1633619610000, 1633619625000, 1633619640000, 1633619655000, 1633619670000, 1633619685000,
1633619700000, 1633619715000, 1633619730000, 1633619745000, 1633619760000, 1633619775000, 1633619790000,
1633619805000, 1633619820000, 1633619835000, 1633619850000, 1633619865000, 1633619880000, 1633619895000
],
[
0.07245212135073513, 0.07253198890830721, 0.07247862573797707, 0.07238248338231042, 0.07221687487740913,
0.07223291298743946, 0.07225427016727755, 0.024531677091864545, 0.02317081920915543,
0.07548902139580993, 0.0777721702857508, 0.07768649905047344, 0.07782257603228229, 0.07788810213200052,
0.07791835055437593, 0.07798387201529966, 0.07790826751849372, 0.07794858648610933, 0.07778729925797964,
0.07769657495236215, 0.077550401329267
]
]
}
},
{
"schema": {
"name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))",
"refId": "A",
"meta": { "custom": { "resultType": "vector" } },
"fields": [
{ "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } },
{
"name": "Value",
"type": "number",
"typeInfo": { "frame": "float64" },
"labels": {},
"config": {
"displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))"
}
}
]
},
"data": { "values": [[1633619900000], [0.0775504013292671]] }
},
{
"schema": {
"name": "exemplar",
"refId": "A",
"meta": { "custom": { "resultType": "exemplar" } },
"fields": [
{ "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } },
{ "name": "Value", "type": "number", "typeInfo": { "frame": "float64" } },
{ "name": "instance", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "__name__", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "job", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "status_code", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "method", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "traceID", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "route", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "ws", "type": "string", "typeInfo": { "frame": "string" } },
{ "name": "le", "type": "string", "typeInfo": { "frame": "string" } }
]
},
"data": {
"values": [
[
1633619598000, 1633619622000, 1633619625000, 1633619646000, 1633619658000, 1633619682000, 1633619695000,
1633619712000, 1633619712000, 1633619724000, 1633619717000, 1633619742000, 1633619757000, 1633619771000,
1633619784000, 1633619801000, 1633619806000, 1633619833000, 1633619833000, 1633619845000, 1633619862000,
1633619877000, 1633619889000
],
[
0.0146153, 0.0118506, 0.0473847, 0.026997, 0.0164318, 0.0113532, 0.0105197, 0.162789, 0.0556026,
0.148856, 0.0433809, 0.0117758, 0.0114496, 0.0114099, 0.0421927, 0.0134148, 0.0152827, 0.6975967,
0.0394788, 0.0137441, 0.0110939, 0.0104496, 0.0101284
],
[
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"db:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80",
"app:80"
],
[
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket",
"tns_request_duration_seconds_bucket"
],
[
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/db",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app",
"tns/app"
],
[
"302",
"200",
"200",
"200",
"200",
"200",
"200",
"500",
"200",
"302",
"208",
"200",
"200",
"200",
"200",
"200",
"302",
"200",
"200",
"200",
"200",
"200",
"200"
],
[
"POST",
"GET",
"GET",
"GET",
"GET",
"GET",
"GET",
"GET",
"GET",
"POST",
"POST",
"GET",
"GET",
"GET",
"GET",
"GET",
"POST",
"GET",
"GET",
"GET",
"GET",
"GET",
"GET"
],
[
"6a3cf561ef6c32a0",
"396bcdf29601a149",
"57c04ef608f11158",
"77c757dab83c665f",
"3d1069567e873f5e",
"b337949f6213efd",
"21b20cbe533cf099",
"2c10b3aa30fabd66",
"42ac6088a757636b",
"2f81158008cd4dcc",
"320b803ad7323b37",
"7f15fd82aeb8b361",
"11c79266da8a74cd",
"5a8571bdcc04c990",
"3de3f4f42ccb93ae",
"23343ac91cc0638",
"5cea3aad17ab11c8",
"5d334e2843d3405a",
"3cf6834596d4b6b6",
"1ab6cff012959723",
"2f78bc2c398b8b20",
"6d5862a70c3abd42",
"f5421be4054f501"
],
[
"post",
"root",
"root",
"root",
"root",
"root",
"root",
"root",
"root",
"post",
"post",
"root",
"root",
"root",
"root",
"root",
"post",
"metrics",
"root",
"root",
"root",
"root",
"root"
],
[
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false",
"false"
],
[
"0.025",
"0.025",
"0.05",
"0.05",
"0.025",
"0.025",
"0.025",
"0.25",
"0.1",
"0.25",
"0.05",
"0.025",
"0.025",
"0.025",
"0.05",
"0.025",
"0.025",
"1.0",
"0.05",
"0.025",
"0.025",
"0.025",
"0.025"
]
]
}
}
]
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
import { test, expect } from '@grafana/plugin-e2e';
const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables';
test.describe(
'Panels test: DashList panel',
{
tag: '@panels',
},
() => {
// this is to prevent the fix for https://github.com/grafana/grafana/issues/76800 from regressing
test('should pass current variable values correctly when `Include current template variable values` is set', async ({
gotoDashboardPage,
selectors,
page,
}) => {
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
// check the initial value of the urls contain the variable value correctly
const panel = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title('Include time range and variables enabled')
);
await expect(panel).toBeVisible();
const links = panel.locator('a');
const linkCount = await links.count();
for (let i = 0; i < linkCount; i++) {
const href = await links.nth(i).getAttribute('href');
expect(href).toContain('var-server=A');
}
// update variable to b
const serverVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('server'))
.locator('..')
.locator('input');
await serverVariable.click();
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B'))
.click();
// blur the dropdown
await page.locator('body').click({ position: { x: 0, y: 0 } });
// check the urls are updated with the new variable value
await expect(panel).toBeVisible();
const updatedLinks = panel.locator('a');
const updatedLinkCount = await updatedLinks.count();
for (let i = 0; i < updatedLinkCount; i++) {
const href = await updatedLinks.nth(i).getAttribute('href');
expect(href).toContain('var-server=B');
}
});
}
);
@@ -0,0 +1,154 @@
import { test, expect } from '@grafana/plugin-e2e';
import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json';
const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950';
test.beforeAll(async ({ request }) => {
// Import the dashboard
await request.post('/api/dashboards/import', {
data: {
dashboard: panelSandboxDashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
});
test.describe(
'Panels test: Panel sandbox',
{
tag: ['@panels'],
},
() => {
test.describe('Sandbox disabled', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
});
await page.reload();
});
test('Add iframes to body', async ({ page, gotoDashboardPage }) => {
await gotoDashboardPage({
uid: DASHBOARD_ID,
});
// this button adds iframes to the body
await page.locator('[data-testid="button-create-iframes"]').click();
const iframeIds = [
'createElementIframe',
'innerHTMLIframe',
'appendIframe',
'prependIframe',
'afterIframe',
'beforeIframe',
'outerHTMLIframe',
'parseFromStringIframe',
'insertBeforeIframe',
'replaceChildIframe',
];
for (const id of iframeIds) {
await expect(page.locator(`#${id}`)).toBeVisible();
}
});
test('Reaches out of panel div', async ({ page, gotoDashboardPage }) => {
await gotoDashboardPage({
uid: DASHBOARD_ID,
});
// this button reaches out of the panel div and modifies the element dataset
await page.locator('[data-testid="button-reach-out"]').click();
await expect(page.locator('[data-sandbox-test="true"]')).toBeVisible();
});
test('Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => {
await gotoDashboardPage({
uid: DASHBOARD_ID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
const input = page.locator('[data-testid="panel-editor-custom-editor-input"]');
await expect(input).toBeEnabled();
await expect(input).toHaveValue('');
await input.fill('x');
await expect(input).toHaveValue('x');
await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeVisible();
});
});
test.describe('Sandbox enabled', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
});
await page.reload();
});
test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => {
await gotoDashboardPage({
uid: DASHBOARD_ID,
});
// this button adds 3 iframes to the body
await page.locator('[data-testid="button-create-iframes"]').click();
const iframeIds = [
'createElementIframe',
'innerHTMLIframe',
'appendIframe',
'prependIframe',
'afterIframe',
'beforeIframe',
'outerHTMLIframe',
'parseFromStringIframe',
'insertBeforeIframe',
'replaceChildIframe',
];
for (const id of iframeIds) {
await expect(page.locator(`#${id}`)).toBeHidden();
}
});
test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => {
await gotoDashboardPage({
uid: DASHBOARD_ID,
});
// this button reaches out of the panel div and modifies the element dataset
await page.locator('[data-testid="button-reach-out"]').click();
await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden();
});
test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => {
await gotoDashboardPage({
uid: DASHBOARD_ID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
const input = page.locator('[data-testid="panel-editor-custom-editor-input"]');
await expect(input).toBeEnabled();
await input.fill('x');
await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden();
});
test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => {
await gotoDashboardPage({
uid: DASHBOARD_ID,
});
await page.locator('[data-testid="button-test-globals"]').click();
await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible();
await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible();
await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible();
});
});
}
);
@@ -0,0 +1,137 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_ID = 'P2jR04WVk';
const MAP_LAYERS_TYPE = 'Map layers Layer type';
const MAP_LAYERS_DATA = 'Map layers Data';
const MAP_LAYERS_GEOJSON = 'Map layers GeoJSON URL';
test.describe(
'Panels test: Geomap layer types',
{
tag: ['@panels'],
},
() => {
test('Tests changing the layer type', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_ID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toBeVisible();
const field = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE)
);
await expect(field).toBeVisible();
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('markers');
// Heatmap
const input = field.locator('input');
await input.fill('Heatmap');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('heatmap');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA))
).toBeVisible();
// GeoJSON
await input.fill('GeoJSON');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('geojson');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON))
).toBeVisible();
// Open Street Map
await input.fill('Open Street Map');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('osm-standard');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA))
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON))
).toBeHidden();
// CARTO basemap
await input.fill('CARTO basemap');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('carto');
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels')
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme'))
).toBeVisible();
// ArcGIS MapServer
await input.fill('ArcGIS MapServer');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('esri-xyz');
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance')
)
).toBeVisible();
// XYZ Tile layer
await input.fill('XYZ Tile layer');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('xyz');
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template')
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution')
)
).toBeVisible();
// Night / Day (Alpha)
await input.fill('Night / Day');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('dayNight');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show'))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Night region color')
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Display sun')
)
).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible();
// Route (Alpha)
await input.fill('Route');
await input.press('Enter');
await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('route');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA))
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Location Mode')
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Style'))
).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible();
});
}
);
@@ -0,0 +1,83 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_ID = 'P2jR04WVk';
test.describe(
'Panels test: Geomap layer controls options',
{
tag: ['@panels'],
},
() => {
test('Tests map controls options', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_ID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
// Wait until the query editor has been loaded by ensuring that the QueryEditor select contains the text 'flight_info_by_state.csv'
await expect(
page.locator(selectors.components.Select.singleValue('')).getByText('flight_info_by_state.csv')
).toBeVisible();
const mapControlsGroup = dashboardPage.getByGrafanaSelector(
selectors.components.OptionsGroup.group('Map controls')
);
await expect(mapControlsGroup).toBeVisible();
// Show zoom field
const showZoomField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showZoomField);
await expect(showZoomField).toBeVisible();
const zoomCheckbox = showZoomField.locator('input[type="checkbox"]');
await zoomCheckbox.check({ force: true });
await expect(zoomCheckbox).toBeChecked();
// Show attribution
const showAttributionField = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.showAttributionField
);
await expect(showAttributionField).toBeVisible();
const attributionCheckbox = showAttributionField.locator('input[type="checkbox"]');
await attributionCheckbox.check({ force: true });
await expect(attributionCheckbox).toBeChecked();
// Show scale
const showScaleField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showScaleField);
await expect(showScaleField).toBeVisible();
const scaleCheckbox = showScaleField.locator('input[type="checkbox"]');
await scaleCheckbox.check({ force: true });
await expect(scaleCheckbox).toBeChecked();
// Show measure tool
const showMeasureField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showMeasureField);
await expect(showMeasureField).toBeVisible();
const measureCheckbox = showMeasureField.locator('input[type="checkbox"]');
await measureCheckbox.check({ force: true });
await expect(measureCheckbox).toBeChecked();
// Show debug
const showDebugField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showDebugField);
await expect(showDebugField).toBeVisible();
const debugCheckbox = showDebugField.locator('input[type="checkbox"]');
await debugCheckbox.check({ force: true });
await expect(debugCheckbox).toBeChecked();
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content);
await expect(panelContent).toBeVisible();
// Verify zoom
await expect(panelContent.locator('.ol-zoom')).toBeVisible();
// Verify attribution
await expect(panelContent.locator('.ol-attribution')).toBeVisible();
// Verify scale
await expect(panelContent.locator('.ol-scale-line')).toBeVisible();
// Verify measure tool
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.measureButton)).toBeVisible();
// Verify debug tool
await expect(dashboardPage.getByGrafanaSelector(selectors.components.DebugOverlay.wrapper)).toBeVisible();
});
}
);
@@ -0,0 +1,194 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_ID = 'P2jR04WVk';
test.describe(
'Panels test: Geomap spatial operations',
{
tag: ['@panels'],
},
() => {
test('Tests location auto option', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_ID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations'))
.click();
const actionLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.actionLabel
);
const actionInput = actionLabel.locator('input');
await actionInput.fill('Prepare spatial field');
await actionInput.press('Enter');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel)
).toBeVisible();
const locationLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.locationLabel
);
const locationInput = locationLabel.locator('input');
await locationInput.fill('Auto');
await locationInput.press('Enter');
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true });
const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header);
await expect(tableHeader).toBeVisible();
await expect(tableHeader.getByText('Point')).toBeVisible();
});
test('Tests location coords option', async ({ gotoDashboardPage, dashboardPage, selectors }) => {
await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) });
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations'))
.click();
const actionLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.actionLabel
);
const actionInput = actionLabel.locator('input');
await actionInput.fill('Prepare spatial field');
await actionInput.press('Enter');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel)
).toBeVisible();
const locationLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.locationLabel
);
const locationInput = locationLabel.locator('input');
await locationInput.fill('Coords');
await locationInput.press('Enter');
const latitudeField = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel
);
await expect(latitudeField).toBeVisible();
const latitudeInput = latitudeField.locator('input');
await latitudeInput.fill('Lat');
await latitudeInput.press('Enter');
const longitudeField = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel
);
await expect(longitudeField).toBeVisible();
const longitudeInput = longitudeField.locator('input');
await longitudeInput.fill('Lng');
await longitudeInput.press('Enter');
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true });
const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header);
await expect(tableHeader).toBeVisible();
await expect(tableHeader.getByText('Point')).toBeVisible();
});
test('Tests geoshash field column appears in table view', async ({
gotoDashboardPage,
dashboardPage,
selectors,
}) => {
await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) });
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations'))
.click();
const actionLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.actionLabel
);
const actionInput = actionLabel.locator('input');
await actionInput.fill('Prepare spatial field');
await actionInput.press('Enter');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel)
).toBeVisible();
const locationLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.locationLabel
);
const locationInput = locationLabel.locator('input');
await locationInput.fill('Geohash');
await locationInput.press('Enter');
const geohashField = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.location.geohash.geohashFieldLabel
);
await expect(geohashField).toBeVisible();
const geohashFieldInput = geohashField.locator('input');
await geohashFieldInput.fill('State');
await geohashFieldInput.press('Enter');
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true });
const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header);
await expect(tableHeader).toBeVisible();
await expect(tableHeader.getByText('State 1')).toBeVisible();
});
test('Tests location lookup option', async ({ gotoDashboardPage, dashboardPage, selectors }) => {
await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) });
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations'))
.click();
const actionLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.actionLabel
);
const actionInput = actionLabel.locator('input');
await actionInput.fill('Prepare spatial field');
await actionInput.press('Enter');
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel)
).toBeVisible();
const locationLabel = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.locationLabel
);
const locationInput = locationLabel.locator('input');
await locationInput.fill('Lookup');
await locationInput.press('Enter');
const lookupField = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel
);
await expect(lookupField).toBeVisible();
const lookupFieldInput = lookupField.locator('input');
await lookupFieldInput.fill('State');
await lookupFieldInput.press('Enter');
const gazetteerField = dashboardPage.getByGrafanaSelector(
selectors.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel
);
await expect(gazetteerField).toBeVisible();
const gazetteerFieldInput = gazetteerField.locator('input');
await gazetteerFieldInput.fill('USA States');
await gazetteerFieldInput.press('Enter');
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true });
const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header);
await expect(tableHeader).toBeVisible();
await expect(tableHeader.getByText('Geometry')).toBeVisible();
});
}
);
@@ -0,0 +1,109 @@
import { test, expect } from '@grafana/plugin-e2e';
const PANEL_UNDER_TEST = 'Lines 500 data points';
test.describe(
'Panels test: Panel edit base',
{
tag: ['@panels'],
},
() => {
test('Tests various Panel edit scenarios', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: 'TkZXxlNG3' });
const panelTitle = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST));
await expect(panelTitle).toBeVisible();
// Check that the panel is visible
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0);
await expect(panelTitle.locator('[data-testid="uplot-main-div"]').first()).toBeVisible();
// Open panel menu and click edit
await panelTitle.hover();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click();
// New panel editor opens when navigating from Panel menu
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible();
// Queries tab is rendered and open by default
const dataPane = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content);
await expect(dataPane).toBeVisible();
// Check that Queries tab is visible and active
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries'))).toBeVisible();
await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Queries1'); // there's already a query so therefore Query + 1
// Check query content is visible and other tabs are not
await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden();
// Can change to Transform tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click();
await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Transformations0'); // no transforms so Transform + 0
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton)
).toBeVisible();
await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden();
// Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered)
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Alert')).click();
await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Alert0'); // no alert so Alert + 0
await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden();
// Go back to Queries tab
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries')).click();
// Check that Time series is chosen
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText(
'Time series'
);
// Check that table view works
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0);
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true });
const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header);
await expect(tableHeader).toBeVisible();
await expect(tableHeader.getByText('A-series')).toBeVisible();
// Change to Text panel
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Text')).click();
// Check current visualization shows Text
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText(
'Text'
);
// Data pane should not be rendered
await expect(dataPane).toBeHidden();
// Change to Table panel
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Table')).click();
// Check current visualization shows Table
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText(
'Table'
);
// Data pane should be rendered
await expect(dataPane).toBeVisible();
// Field & Overrides tabs (need to switch to React based vis, i.e. Table)
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true });
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header')
)
).toBeVisible();
await expect(
dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Column width')
)
).toBeVisible();
});
}
);
@@ -0,0 +1,118 @@
import { Locator, Page } from 'playwright-core';
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
test.describe(
'Panels test: Queries',
{
tag: ['@panels'],
},
() => {
test('Tests various Panel edit queries scenarios', async ({ selectors, gotoDashboardPage, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: '5SdHCadmz',
queryParams: new URLSearchParams({ editPanel: '3' }),
});
// New panel editor opens when navigating from Panel menu
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible();
// Queries tab is rendered and open by default
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content)).toBeVisible();
// We expect row with refId A to exist and be visible
const initialRows = dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows);
await expect(initialRows).toHaveCount(1);
// Add query button should be visible and clicking on it should create a new row
await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.addQuery).click();
// We expect row with refId A and B to exist and be visible
await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2);
// Remove refId A
await dashboardPage
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Remove query'))
.first()
.click();
// We expect row with refId B to exist and be visible
await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(1);
// Duplicate refId B
await dashboardPage
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Duplicate query'))
.first()
.click();
// We expect row with refId B and A to exist and be visible
await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2);
// Change to CSV Metric Values scenario for A
const scenarioSelect = dashboardPage
.getByGrafanaSelector(selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer)
.first();
await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').first().click();
await page.getByText('CSV Metric Values').first().click();
// Verify both queries are present in inspector
await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => {
const keyTexts = await keys.allTextContents();
const length = keyTexts.length;
const resultIds = new Set<string>([
keyTexts[length - 2], // last 2
keyTexts[length - 1], // last 2
]);
expect(resultIds.has('A:')).toBe(true);
expect(resultIds.has('B:')).toBe(true);
});
// Hide response for row with refId A
await dashboardPage
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response'))
.nth(1)
.click();
await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => {
const keyTexts = await keys.allTextContents();
const length = keyTexts.length;
expect(keyTexts[length - 1]).toBe('B:');
});
// Show response for row with refId A
await dashboardPage
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response'))
.nth(1)
.click();
await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => {
const keyTexts = await keys.allTextContents();
const length = keyTexts.length;
const resultIds = new Set<string>([
keyTexts[length - 2], // last 2
keyTexts[length - 1], // last 2
]);
expect(resultIds.has('A:')).toBe(true);
expect(resultIds.has('B:')).toBe(true);
});
});
}
);
const expectInspectorResultAndClose = async (
page: Page,
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
expectCallback: (keys: Locator) => Promise<void>
) => {
await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.queryInspectorButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.refreshButton).click();
const keys = page.locator(selectors.components.PanelInspector.Query.jsonObjectKeys(''));
await expect(keys.first()).toBeVisible();
await expectCallback(keys);
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click();
};
@@ -0,0 +1,44 @@
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Panels test: Transformations',
{
tag: ['@panels'],
},
() => {
test('Tests transformations editor', async ({ selectors, gotoDashboardPage }) => {
const dashboardPage = await gotoDashboardPage({
uid: 'TkZXxlNG3',
queryParams: new URLSearchParams({ editPanel: '47' }),
});
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.calculationsLabel)
).toBeVisible();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.modeLabel)).toBeVisible();
});
test('Tests case where transformations can be disabled and not clear out panel data', async ({
selectors,
gotoDashboardPage,
}) => {
const dashboardPage = await gotoDashboardPage({
uid: 'TkZXxlNG3',
queryParams: new URLSearchParams({ editPanel: '47' }),
});
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click();
await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.disableTransformationButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage)
).toBeHidden();
});
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'grafana-azure-monitor-datasource' });
await expect(await page.getByText('Type: Azure Monitor', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Authentication', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'stackdriver' });
await expect(await page.getByText('Type: Google Cloud Monitoring', { exact: true })).toBeVisible();
await expect(await page.getByText('Google JWT File', { exact: true })).toBeVisible();
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'cloudwatch' });
await expect(await page.getByText('Type: CloudWatch', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection Details', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'elasticsearch' });
await expect(await page.getByText('Type: Elasticsearch', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'graphite' });
await expect(await page.getByText('Type: Graphite', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'influxdb' });
await expect(await page.getByText('Type: InfluxDB', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'jaeger' });
await expect(await page.getByText('Type: Jaeger', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,8 @@
import { test, expect } from '@grafana/plugin-e2e';
test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'loki' });
await expect(page.getByText('Type: Loki', { exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
});
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'mssql' });
await expect(await page.getByText('Type: Microsoft SQL Server', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,72 @@
export const normalTableName = 'normalTable';
export const tableNameWithSpecialCharacter = 'table-name';
export const tablesResponse = {
results: {
tables: {
status: 200,
frames: [
{
schema: {
refId: 'tables',
meta: {
executedQueryString:
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'DataMaker' ORDER BY table_name",
},
fields: [{ name: 'TABLE_NAME', type: 'string', typeInfo: { frame: 'string', nullable: true } }],
},
data: { values: [[normalTableName, tableNameWithSpecialCharacter]] },
},
],
},
},
};
export const fieldsResponse = (refId: string) => ({
results: {
[refId]: {
status: 200,
frames: [
{
schema: {
refId,
meta: {
executedQueryString:
"SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'DataMaker' AND table_name = 'RandomIntsWithTimes' ORDER BY column_name",
},
fields: [
{ name: 'COLUMN_NAME', type: 'string', typeInfo: { frame: 'string', nullable: true } },
{ name: 'DATA_TYPE', type: 'string', typeInfo: { frame: 'string', nullable: true } },
],
},
data: {
values: [
['createdAt', 'id', 'time', 'updatedAt', 'bigint', 'name'],
['datetime', 'int', 'datetime', 'datetime', 'int', 'string'],
],
},
},
],
},
},
});
export const datasetResponse = {
results: {
datasets: {
status: 200,
frames: [
{
schema: {
refId: 'datasets',
meta: {
executedQueryString:
"SELECT DISTINCT TABLE_SCHEMA from information_schema.TABLES where TABLE_TYPE != 'SYSTEM VIEW' ORDER BY TABLE_SCHEMA",
},
fields: [{ name: 'TABLE_SCHEMA', type: 'string', typeInfo: { frame: 'string', nullable: true } }],
},
data: { values: [['DataMaker', 'mysql', 'performance_schema', 'sys']] },
},
],
},
},
};
@@ -0,0 +1,31 @@
import { expect, test } from '@grafana/plugin-e2e';
import { tableNameWithSpecialCharacter } from './mocks/mysql.mocks';
import { mockDataSourceRequest } from './utils';
test.beforeEach(mockDataSourceRequest);
test(
'code editor autocomplete should handle table name escaping/quoting',
{
tag: '@plugins',
},
async ({ explorePage, selectors, page }) => {
await page.getByLabel('Code').check();
const editor = explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox');
await editor.fill('S');
await page.getByLabel('SELECT <column> FROM <table>').locator('a').click();
await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible();
await page.keyboard.press('Enter');
await expect(editor).toHaveValue(`SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``);
for (let i = 0; i < tableNameWithSpecialCharacter.length + 2; i++) {
await page.keyboard.press('Backspace');
}
await page.keyboard.press('Control+I');
await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible();
}
);
+23
View File
@@ -0,0 +1,23 @@
import { PlaywrightTestArgs } from '@playwright/test';
import { PluginFixture } from '@grafana/plugin-e2e';
import { datasetResponse, fieldsResponse, tablesResponse } from './mocks/mysql.mocks';
export async function mockDataSourceRequest({ context, explorePage, selectors }: PlaywrightTestArgs & PluginFixture) {
await explorePage.datasource.set('gdev-mysql');
await context.route(selectors.apis.DataSource.queryPattern, async (route, request) => {
const refId = request.postDataJSON().queries[0].refId;
if (/fields-.*/g.test(refId)) {
return route.fulfill({ json: fieldsResponse(refId), status: 200 });
}
switch (refId) {
case 'tables':
return route.fulfill({ json: tablesResponse, status: 200 });
case 'datasets':
return route.fulfill({ json: datasetResponse, status: 200 });
default:
return route.continue();
}
});
}
@@ -0,0 +1,176 @@
import { selectors } from '@grafana/e2e-selectors';
import { test, expect } from '@grafana/plugin-e2e';
import { normalTableName } from './mocks/mysql.mocks';
import { mockDataSourceRequest } from './utils';
test.beforeEach(mockDataSourceRequest);
test.describe(
'mysql',
{
tag: '@plugins',
},
() => {
test('visual query builder should handle macros', async ({ explorePage, page }) => {
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click();
await page.getByText(normalTableName, { exact: true }).click();
// Open Data operations
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click();
const select = page.getByLabel('Select options menu');
await select.locator(page.getByText('$__timeGroupAlias')).click();
// Open column selector
await explorePage
.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column'))
.click();
await select.locator(page.getByText('createdAt')).click();
// Open Interval selector
await explorePage
.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval'))
.click();
await select.locator(page.getByText('$__interval')).click();
await page.getByRole('button', { name: 'Add column' }).click();
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click();
await select.locator(page.getByText('AVG')).click();
await explorePage
.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column'))
.nth(1)
.click();
await select.locator(page.getByText('bigint')).click();
// Validate the query
await expect(
explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox')
).toHaveValue(
`SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50`
);
});
test('visual query builder should handle time filter macro', async ({ explorePage, page }) => {
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click();
await page.getByText(normalTableName, { exact: true }).click();
// Open column selector
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectColumn).click();
const select = page.getByLabel('Select options menu');
await select.locator(page.getByText('createdAt')).click();
// Toggle where row
await page.getByLabel('Filter').last().click();
// Click add filter button
await page.getByRole('button', { name: 'Add filter' }).click();
await page.getByRole('button', { name: 'Add filter' }).click(); // For some reason we need to click twice
// Open field selector
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterField).click();
await select.locator(page.getByText('createdAt')).click();
// Open operator selector
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click();
await select.locator(page.getByText('Macros')).click();
// Open macros value selector
await explorePage.getByGrafanaSelector('Macros value selector').click();
await select.locator(page.getByText('timeFilter', { exact: true })).click();
// Validate that the timeFilter macro was added
await expect(
explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox')
).toHaveValue(
`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n $__timeFilter(createdAt)\nLIMIT\n 50`
);
// Validate that the timeFilter macro was removed when changed to equals operator
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click();
await select.locator(page.getByText('==')).click();
await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).click();
await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).blur();
await expect(
explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox')
).not.toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n createdAt = NULL\nLIMIT\n 50`);
});
test('visual query builder should not crash when filter is set to select_any_in', async ({ explorePage, page }) => {
const queryParams = new URLSearchParams();
queryParams.set('schemaVersion', '1');
queryParams.set('orgId', '1');
const panes = {
mmm: {
datasource: 'P4FDCC188E688367F',
queries: [
{
refId: 'A',
datasource: {
type: 'mysql',
uid: 'P4FDCC188E688367F',
},
format: 'table',
rawSql: "SELECT * FROM DataMaker.normalTable WHERE name IN ('a') LIMIT 50 ",
editorMode: 'builder',
sql: {
columns: [
{
type: 'function',
parameters: [
{
type: 'functionParameter',
name: '*',
},
],
},
],
groupBy: [
{
type: 'groupBy',
property: {
type: 'string',
},
},
],
limit: 50,
whereJsonTree: {
id: 'baa99aa9-0123-4456-b89a-b195d1dcfc6a',
type: 'group',
children1: [
{
type: 'rule',
id: 'bb9a8bba-89ab-4cde-b012-3195d1dd2c91',
properties: {
fieldSrc: 'field',
field: 'name',
operator: 'select_any_in',
value: ['a'],
valueSrc: ['value'],
valueType: ['text'],
},
},
],
},
whereString: "name IN ('a')",
},
dataset: 'DataMaker',
table: 'normalTable',
},
],
},
};
queryParams.set('panes', JSON.stringify(panes));
await explorePage.goto({ queryParams });
// Validate the query
await expect(
explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox')
).toHaveValue(`SELECT\n *\nFROM\n DataMaker.normalTable\nWHERE\n name IN ('a')\nLIMIT\n 50`);
});
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: decoupled frontend plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'opentsdb' });
await expect(await page.getByText('Type: OpenTSDB', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,3 @@
# @grafana/plugin-e2e API tests
The purpose of the E2E tests in this directory is not to test the plugins per se - it's to verify that the fixtures, models and expect matchers provided by the [`@grafana/plugin-e2e`](https://github.com/grafana/plugin-tools/tree/main/packages/plugin-e2e) package are compatible with the latest version of Grafana. If you find that any of these tests are failing, it's likely because you have made changes in the Grafana UI that breaks the end-to-end testing APIs in plugin-e2e. For information on how to address this, follow the instructions in the [contributing guidelines](https://github.com/grafana/plugin-tools/blob/main/packages/plugin-e2e/CONTRIBUTING.md#how-to-fix-broken-test-scenarios-after-changes-in-grafana) for the @grafana/plugin-e2e package in the plugin-tools repository.
@@ -0,0 +1,31 @@
import * as e2e from '@grafana/e2e-selectors';
import { expect, test } from '@grafana/plugin-e2e';
// let's disable the feature toggles for now, otherwise the getAlertRuleQueryRow fails and I don't see any other way to get the query row
test.use({ featureToggles: { alertingQueryAndExpressionsStepMode: false, alertingNotificationsStepMode: false } });
test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => {
test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({
page,
alertRuleEditPage,
}) => {
await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule');
//add working query
const queryA = alertRuleEditPage.getAlertRuleQueryRow('A');
await queryA.datasource.set('gdev-prometheus');
await queryA.locator.getByLabel('Code').click();
await page.waitForFunction(() => window.monaco);
await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click();
await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))');
//add broken query
const newQuery = await alertRuleEditPage.clickAddQueryRow();
await newQuery.datasource.set('gdev-prometheus');
await newQuery.locator.getByLabel('Code').click();
await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click();
await page.keyboard.insertText('topk(5,');
await expect(alertRuleEditPage.evaluate()).not.toBeOK();
});
});
@@ -0,0 +1,55 @@
import { expect, test } from '@grafana/plugin-e2e';
import { AlertVariant } from '@grafana/ui';
import {
successfulAnnotationQueryWithData,
failedAnnotationQueryWithMultipleErrors,
successfulAnnotationQueryWithoutData,
failedAnnotationQuery,
} from '../mocks/queries';
interface Scenario {
name: string;
mock: object;
text: string;
severity: AlertVariant;
status: number;
}
const scenarios: Scenario[] = [
{ name: 'error', severity: 'error', mock: failedAnnotationQuery, text: 'Google API Error 400', status: 400 },
{
name: 'multiple errors',
severity: 'error',
mock: failedAnnotationQueryWithMultipleErrors,
text: 'Google API Error 400Google API Error 401',
status: 400,
},
{
name: 'data',
severity: 'success',
mock: successfulAnnotationQueryWithData,
text: '2 events (from 2 fields)',
status: 200,
},
{
name: 'empty result',
severity: 'warning',
mock: successfulAnnotationQueryWithoutData,
text: 'No events found',
status: 200,
},
];
test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => {
for (const scenario of scenarios) {
test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => {
annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status);
await annotationEditPage.datasource.set('gdev-testdata');
await page.getByLabel('Scenario').last().fill('CSV Content');
await page.keyboard.press('Tab');
await annotationEditPage.runQuery();
await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text });
});
}
});
@@ -0,0 +1,23 @@
import { expect, test } from '@grafana/plugin-e2e';
const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' };
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => {
const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD);
await dashboardPage.addPanel();
await expect(page.url()).toContain('editPanel');
});
test('add panel in new dashboard', async ({ dashboardPage, page }) => {
const panelEditPage = await dashboardPage.addPanel();
await expect(panelEditPage.panel.locator).toBeVisible();
await expect(page.url()).toContain('editPanel');
});
}
);
@@ -0,0 +1,48 @@
import { expect, test } from '@grafana/plugin-e2e';
import { formatExpectError } from '../errors';
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => {
test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => {
const configPage = await createDataSourceConfigPage({ type: 'prometheus' });
await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090');
await expect(
configPage.saveAndTest(),
formatExpectError('Expected save data source config to fail when Prometheus server is not running')
).not.toBeOK();
});
test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => {
const configPage = await createDataSourceConfigPage({ type: 'prometheus' });
configPage.mockHealthCheckResponse({ status: 200 });
await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090');
await expect(
configPage.saveAndTest(),
formatExpectError('Expected data source config to be successfully saved')
).toBeOK();
});
});
test.describe('test data source with frontend only health check', () => {
test('valid credentials should display a success alert on the page', async ({
createDataSourceConfigPage,
page,
}) => {
const configPage = await createDataSourceConfigPage({ type: 'zipkin' });
configPage.mockHealthCheckResponse({ message: 'Data source is working', status: 'OK' }, 200);
await page.getByPlaceholder('http://localhost:9411').fill('http://localhost:9411');
await expect(configPage.saveAndTest()).toBeOK();
await expect(
configPage,
formatExpectError('Expected data source config to display success alert after save')
).toHaveAlert('success', { hasNotText: 'Datasource updated' });
});
});
}
);
@@ -0,0 +1,19 @@
import { expect, test } from '@grafana/plugin-e2e';
import { formatExpectError } from '../errors';
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test('query data response should be OK when query is valid', async ({ explorePage }) => {
await explorePage.datasource.set('gdev-testdata');
await expect(
explorePage.runQuery(),
formatExpectError('Expected Explore query to execute successfully')
).toBeOK();
});
}
);
@@ -0,0 +1,25 @@
import { expect, test } from '@grafana/plugin-e2e';
const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1';
const FALSY_CUSTOM_TOGGLE = 'custom_toggle2';
// override the feature toggles defined in playwright.config.ts only for tests in this file
test.use({
featureToggles: {
[TRUTHY_CUSTOM_TOGGLE]: true,
[FALSY_CUSTOM_TOGGLE]: false,
},
});
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => {
expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy();
expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy();
});
}
);
@@ -0,0 +1,95 @@
import * as e2e from '@grafana/e2e-selectors';
import { expect, test } from '@grafana/plugin-e2e';
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test.describe('Loki editor', () => {
test('Autocomplete features should work as expected.', async ({ page }) => {
// Go to loki datasource in explore
await page.goto(
'/explore?schemaVersion=1&panes=%7B%22iap%22:%7B%22datasource%22:%22gdev-loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22gdev-loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1'
);
const queryEditor = page.getByTestId(e2e.selectors.components.QueryField.container);
const queryEditorRows = page.getByTestId('query-editor-rows');
async function assertQueryEditorEmpty() {
const queryEditorEmptyText = /^Enter to Rename.+/;
await expect(queryEditor).toHaveText(queryEditorEmptyText);
}
async function clearInput() {
// Clear focused input
// Monaco appears to need some time to init keybindings after a change, adding this timeout to prevent flake
await page.waitForTimeout(100);
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('Backspace');
}
// assert that the query builder is shown by default
await expect(page.getByText('Label filters')).toHaveCount(1);
// switch to code editor
await page.getByLabel('Code').click();
await page.waitForFunction(() => window.monaco);
await expect(queryEditor).toHaveCount(1);
await assertQueryEditorEmpty();
// assert editor automatically adds close paren
await queryEditor.click();
await page.keyboard.type('time(');
await expect(queryEditor).toContainText('time()');
// removes closing brace when opening brace is removed
await clearInput();
await assertQueryEditorEmpty();
await page.keyboard.type('avg_over_time(');
await expect(queryEditor).toContainText('avg_over_time()');
await page.keyboard.press('Backspace');
await expect(queryEditor).not.toContainText('avg_over_time()');
await expect(queryEditor).toContainText('avg_over_time');
// keeps closing brace when opening brace is removed and inner values exist
await clearInput();
await assertQueryEditorEmpty();
await page.keyboard.type('time(test');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('Backspace');
await expect(queryEditor).toContainText('timetest');
// overrides an automatically inserted paren
await clearInput();
await assertQueryEditorEmpty();
await page.keyboard.type('time()');
await expect(queryEditor).toContainText('time()');
// does not override manually inserted braces
await clearInput();
await assertQueryEditorEmpty();
await page.keyboard.type('))');
await expect(queryEditor).toContainText('))');
// Should execute the query when enter with shift is pressed
await clearInput();
await assertQueryEditorEmpty();
await page.keyboard.press('Shift+Enter');
await expect(page.getByTestId('explore-no-data')).toHaveCount(1);
// Suggestions plugin
await clearInput();
await assertQueryEditorEmpty();
await page.keyboard.type('av');
await expect(queryEditorRows.getByLabel(/avg, docs:/)).toHaveCount(1);
await expect(queryEditorRows.getByLabel(/avg_over_time, docs:/)).toHaveCount(1);
});
});
}
);
@@ -0,0 +1,105 @@
import { expect, test } from '@grafana/plugin-e2e';
import { formatExpectError } from '../errors';
import { successfulDataQuery } from '../mocks/queries';
const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' };
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test.describe('panel edit page', () => {
test('table panel data assertions with provisioned dashboard', async ({ gotoPanelEditPage }) => {
const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' });
await expect(
panelEditPage.panel.locator,
formatExpectError('Could not locate panel in panel edit page')
).toBeVisible();
await expect(
panelEditPage.panel.fieldNames,
formatExpectError('Could not locate header elements in table panel')
).toContainText(['Field', 'Max', 'Mean', 'Last']);
});
test('table panel data assertions', async ({ panelEditPage }) => {
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
await panelEditPage.datasource.set('gdev-testdata');
await panelEditPage.setVisualization('Table');
await panelEditPage.refreshPanel();
await expect(
panelEditPage.panel.locator,
formatExpectError('Could not locate panel in panel edit page')
).toBeVisible();
await expect(
panelEditPage.panel.fieldNames,
formatExpectError('Could not locate header elements in table panel')
).toContainText(['col1', 'col2']);
await expect(
panelEditPage.panel.data,
formatExpectError('Could not locate headers in table panel')
).toContainText(['val1', 'val2', 'val3', 'val4']);
});
test('timeseries panel - table view assertions', async ({ panelEditPage }) => {
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
await panelEditPage.datasource.set('gdev-testdata');
await panelEditPage.setVisualization('Time series');
await panelEditPage.refreshPanel();
await panelEditPage.toggleTableView();
await expect(
panelEditPage.panel.locator,
formatExpectError('Could not locate panel in panel edit page')
).toBeVisible();
await expect(
panelEditPage.panel.fieldNames,
formatExpectError('Could not locate header elements in table panel')
).toContainText(['col1', 'col2']);
await expect(
panelEditPage.panel.data,
formatExpectError('Could not locate data elements in table panel')
).toContainText(['val1', 'val2', 'val3', 'val4']);
});
});
test.describe('dashboard page', () => {
test('getting panel by title', async ({ gotoDashboardPage }) => {
const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD);
await dashboardPage.goto();
const panel = await dashboardPage.getPanelByTitle('Colored background');
await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']);
});
test('getting panel by id', async ({ gotoDashboardPage }) => {
const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD);
await dashboardPage.goto();
const panel = await dashboardPage.getPanelByTitle('Colored background');
await expect(
panel.fieldNames,
formatExpectError('Could not locate header elements in table panel')
).toContainText(['Field', 'Max', 'Mean', 'Last']);
});
});
test.describe('explore page', () => {
test('table panel', async ({ explorePage }) => {
const url =
'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1';
await explorePage.goto({
queryParams: new URLSearchParams(url),
});
await expect(
explorePage.timeSeriesPanel.locator,
formatExpectError('Could not locate time series panel in explore page')
).toBeVisible();
await expect(
explorePage.tablePanel.locator,
formatExpectError('Could not locate table panel in explore page')
).toBeVisible();
await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']);
});
});
}
);
@@ -0,0 +1,214 @@
import { expect, test } from '@grafana/plugin-e2e';
import { formatExpectError } from '../errors';
import { successfulDataQuery } from '../mocks/queries';
import { scenarios } from '../mocks/resources';
const PANEL_TITLE = 'Table panel E2E test';
const TABLE_VIZ_NAME = 'Table';
const TIME_SERIES_VIZ_NAME = 'Time series';
const STANDARD_OTIONS_CATEGORY = 'Standard options';
const DISPLAY_NAME_LABEL = 'Display name';
const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' };
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test.describe('query editor query data', () => {
test('query data response should be OK when query is valid', async ({ panelEditPage }) => {
await panelEditPage.datasource.set('gdev-testdata');
await expect(
panelEditPage.refreshPanel(),
formatExpectError('Expected panel query to execute successfully')
).toBeOK();
});
test('query data response should not be OK and panel error should be displayed when query is invalid', async ({
panelEditPage,
}) => {
await panelEditPage.datasource.set('gdev-testdata');
const queryEditorRow = await panelEditPage.getQueryEditorRow('A');
await queryEditorRow.getByLabel('Labels').fill('invalid-label-format');
await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK();
await expect(
panelEditPage.panel.getErrorIcon(),
formatExpectError('Expected panel error to be displayed after query execution')
).toBeVisible();
});
});
test.describe('query editor with mocked responses', () => {
test('and resource `scenarios` is mocked', async ({ selectors, dashboardPage }) => {
await dashboardPage.mockResourceResponse('scenarios', scenarios);
const panelEditPage = await dashboardPage.addPanel();
await panelEditPage.datasource.set('gdev-testdata');
const queryEditorRow = await panelEditPage.getQueryEditorRow('A');
await queryEditorRow.getByLabel('Scenario').last().click();
await expect(
panelEditPage.getByGrafanaSelector(selectors.components.Select.option),
formatExpectError('Expected certain select options to be displayed after clicking on the select input')
).toHaveText(scenarios.map((s) => s.name));
});
test('mocked query data response', async ({ panelEditPage, selectors }) => {
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
await panelEditPage.datasource.set('gdev-testdata');
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
await panelEditPage.refreshPanel();
await expect(
panelEditPage.panel.getErrorIcon(),
formatExpectError('Did not expect panel error to be displayed after query execution')
).toBeHidden();
await expect(
panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body),
formatExpectError('Expected certain select options to be displayed after clicking on the select input')
).toHaveText('val1val2val3val4');
});
});
test.describe('edit panel plugin settings', () => {
test('change viz to table panel, set panel title and collapse section', async ({
panelEditPage,
selectors,
page,
}) => {
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
await expect(
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker),
formatExpectError('Expected panel visualization to be set to table')
).toHaveText(TABLE_VIZ_NAME);
await panelEditPage.setPanelTitle(PANEL_TITLE);
await expect(
panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_TITLE)),
formatExpectError('Expected panel title to be updated')
).toBeVisible();
await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY);
await expect(
page.getByText(DISPLAY_NAME_LABEL),
formatExpectError('Expected section to be collapsed')
).toBeVisible();
});
test('Select time zone in timezone picker', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const axisOptions = await panelEditPage.getCustomOptions('Axis');
const timeZonePicker = axisOptions.getSelect('Time zone');
await timeZonePicker.selectOption('Europe/Stockholm');
await expect(timeZonePicker).toHaveSelected('Europe/Stockholm');
});
test('select unit in unit picker', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const standardOptions = panelEditPage.getStandardOptions();
const unitPicker = standardOptions.getUnitPicker('Unit');
await unitPicker.selectOption('Misc > Pixels');
await expect(unitPicker).toHaveSelected('Pixels');
});
test('enter value in number input', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const axisOptions = panelEditPage.getCustomOptions('Axis');
const lineWith = axisOptions.getNumberInput('Soft min');
await lineWith.fill('10');
await expect(lineWith).toHaveValue('10');
});
test('enter value in slider', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const graphOptions = panelEditPage.getCustomOptions('Graph styles');
const lineWidth = graphOptions.getSliderInput('Line width');
await lineWidth.fill('10');
await expect(lineWidth).toHaveValue('10');
});
test('select value in single value select', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const standardOptions = panelEditPage.getStandardOptions();
const colorSchemeSelect = standardOptions.getSelect('Color scheme');
await colorSchemeSelect.selectOption('Classic palette');
await expect(colorSchemeSelect).toHaveSelected('Classic palette');
});
test('clear input', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const panelOptions = panelEditPage.getPanelOptions();
const title = panelOptions.getTextInput('Title');
await expect(title).toHaveValue('New panel');
await title.clear();
await expect(title).toHaveValue('');
});
test('enter value in input', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const panelOptions = panelEditPage.getPanelOptions();
const description = panelOptions.getTextInput('Description');
await expect(description).toHaveValue('');
await description.fill('This is a panel');
await expect(description).toHaveValue('This is a panel');
});
test('unchecking switch', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const axisOptions = panelEditPage.getCustomOptions('Axis');
const showBorder = axisOptions.getSwitch('Show border');
await expect(showBorder).toBeChecked({ checked: false });
await showBorder.check();
await expect(showBorder).toBeChecked();
await showBorder.uncheck();
await expect(showBorder).toBeChecked({ checked: false });
});
test('checking switch', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const axisOptions = panelEditPage.getCustomOptions('Axis');
const showBorder = axisOptions.getSwitch('Show border');
await expect(showBorder).toBeChecked({ checked: false });
await showBorder.check();
await expect(showBorder).toBeChecked();
});
test('re-selecting value in radio button group', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const axisOptions = panelEditPage.getCustomOptions('Axis');
const placement = axisOptions.getRadioGroup('Placement');
await placement.check('Right');
await expect(placement).toHaveChecked('Right');
await placement.check('Auto');
await expect(placement).toHaveChecked('Auto');
});
test('selecting value in radio button group', async ({ panelEditPage }) => {
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
const axisOptions = panelEditPage.getCustomOptions('Axis');
const placement = axisOptions.getRadioGroup('Placement');
await placement.check('Right');
await expect(placement).toHaveChecked('Right');
});
});
test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => {
const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' });
await panelEditPage.backToDashboard();
await expect(page.url()).not.toContain('editPanel');
});
}
);
@@ -0,0 +1,25 @@
import { expect, test } from '@grafana/plugin-e2e';
import { formatExpectError } from '../errors';
import { prometheusLabels } from '../mocks/resources';
test.describe(
'plugin-e2e-api-tests admin',
{
tag: ['@plugins'],
},
() => {
test('variable query with mocked response', async ({ variableEditPage, page }) => {
variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels);
variableEditPage.mockResourceResponse('suggestions*', prometheusLabels);
await variableEditPage.datasource.set('gdev-prometheus');
await variableEditPage.getByGrafanaSelector('Query type').fill('Label names');
await page.keyboard.press('Tab');
await variableEditPage.runQuery();
await expect(
variableEditPage,
formatExpectError('Expected variable edit page to display certain label names after query execution')
).toDisplayPreviews(prometheusLabels.data);
});
}
);
@@ -0,0 +1,24 @@
import { expect, test } from '@grafana/plugin-e2e';
test.describe(
'plugin-e2e-api-test viewer',
{
tag: ['@plugins'],
},
() => {
test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => {
await page.goto('/');
const homePageURL = new URL(page.url());
await page.goto('/datasources', { waitUntil: 'networkidle' });
const redirectedPageURL = new URL(page.url());
expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname);
});
test('current user should have viewer role', async ({ page, request }) => {
await page.goto('/');
const response = await request.get('/api/user/orgs');
await expect(response).toBeOK();
await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' }));
});
}
);
@@ -0,0 +1,4 @@
export const formatExpectError = (message: string) => {
return `Error while verifying @grafana/plugin-e2e scenarios: ${message}.
See https://github.com/grafana/grafana/blob/main/plugin-e2e/plugin-e2e-api-tests/README.md for more information.`;
};
@@ -0,0 +1,138 @@
export const successfulDataQuery = {
results: {
A: {
status: 200,
frames: [
{
schema: {
refId: 'A',
fields: [
{
name: 'col1',
type: 'string',
typeInfo: {
frame: 'string',
nullable: true,
},
},
{
name: 'col2',
type: 'string',
typeInfo: {
frame: 'string',
nullable: true,
},
},
],
},
data: {
values: [
['val1', 'val3'],
['val2', 'val4'],
],
},
},
],
},
},
};
export const failedAnnotationQuery: object = {
results: {
Anno: {
error: 'Google API Error 400',
errorSource: '',
status: 500,
},
},
};
export const failedAnnotationQueryWithMultipleErrors: object = {
results: {
Anno1: {
error: 'Google API Error 400',
errorSource: '',
status: 400,
},
Anno2: {
error: 'Google API Error 401',
errorSource: '',
status: 401,
},
},
};
export const successfulAnnotationQueryWithData: object = {
results: {
Anno: {
status: 200,
frames: [
{
schema: {
refId: 'Anno',
fields: [
{
name: 'time',
type: 'time',
typeInfo: {
frame: 'time.Time',
nullable: true,
},
},
{
name: 'col2',
type: 'string',
typeInfo: {
frame: 'string',
nullable: true,
},
},
],
},
data: {
values: [
[1702973084093, 1702973084099],
['val1', 'val2'],
],
},
},
],
},
},
};
export const successfulAnnotationQueryWithoutData: object = {
results: {
Anno: {
status: 200,
frames: [
{
schema: {
refId: 'Anno',
fields: [
{
name: 'time',
type: 'time',
typeInfo: {
frame: 'time.Time',
nullable: true,
},
},
{
name: 'col2',
type: 'string',
typeInfo: {
frame: 'string',
nullable: true,
},
},
],
},
data: {
values: [],
},
},
],
},
},
};
@@ -0,0 +1,19 @@
export const scenarios = [
{
description: '',
id: 'annotations',
name: 'Annotations',
stringInput: '',
},
{
description: '',
id: 'arrow',
name: 'Load Apache Arrow Data',
stringInput: '',
},
];
export const prometheusLabels = {
status: 'success',
data: ['__name__', 'action', 'active', 'address'],
};
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'grafana-postgresql-datasource' });
await expect(await page.getByText('Type: PostgreSQL', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
}
);
@@ -0,0 +1,14 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Smoke test: plugin loads',
{
tag: '@plugins',
},
async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'zipkin' });
await expect(await page.getByText('Type: Zipkin', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
}
);
+19
View File
@@ -0,0 +1,19 @@
import { test, expect } from '@grafana/plugin-e2e';
test(
'Scenario test: Can login successfully',
{
tag: ['@scenarios'],
},
async ({ selectors, page }) => {
await page.goto(selectors.pages.Login.url);
await page.getByTestId(selectors.pages.Login.username).fill('admin');
await page.getByTestId(selectors.pages.Login.password).fill('admin');
await page.getByTestId(selectors.pages.Login.submit).click();
await page.getByTestId(selectors.pages.Login.skip).click();
await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible();
}
);
@@ -0,0 +1,54 @@
import { v4 as uuidv4 } from 'uuid';
import { test, expect } from '@grafana/plugin-e2e';
test.describe(
'Smoke tests',
{
tag: ['@smoke'],
},
() => {
test('Login, create test data source, create dashboard and panel scenario', async ({
createDataSourceConfigPage,
gotoDashboardPage,
selectors,
page,
}) => {
const dataSourceConfigPage = await createDataSourceConfigPage({
name: `e2e-${uuidv4()}`,
type: 'grafana-testdata-datasource',
});
const { datasource } = dataSourceConfigPage;
await dataSourceConfigPage.saveAndTest({
path: `/api/datasources/uid/${datasource.uid}?accesscontrol=true`,
});
// Create new dashboard
const dashboardPage = await gotoDashboardPage({});
// Add new panel
await dashboardPage.addPanel();
// Select CSV Metric Values scenario
const scenarioSelect = dashboardPage.getByGrafanaSelector(
selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer
);
await expect(scenarioSelect).toBeVisible();
await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').click();
await page.getByText('CSV Metric Values').click();
// Verify the graph renders by checking legend
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series'))
).toBeVisible();
// Verify panel is added to dashboard
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series'))
).toBeVisible();
});
}
);
@@ -0,0 +1,49 @@
import { test, expect } from '@grafana/plugin-e2e';
import { GrafanaBootConfig } from '@grafana/runtime';
test.describe(
'Panels smokescreen',
{
tag: ['@smoke'],
},
() => {
test('Tests each panel type in the panel edit view to ensure no crash', async ({
gotoDashboardPage,
selectors,
page,
}) => {
// this test can absolutely take longer than the default 30s timeout
test.setTimeout(60000);
// Create new dashboard
const dashboardPage = await gotoDashboardPage({});
// Add new panel
await dashboardPage.addPanel();
// Get panel types from window object
const panelTypes = await page.evaluate(() => {
// @grafana/plugin-e2e doesn't export the full bootdata config
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const win = window as typeof window & { grafanaBootData: GrafanaBootConfig['bootData'] };
return win.grafanaBootData?.settings?.panels ?? {};
});
// Loop through every panel type and ensure no crash
for (const [_, panel] of Object.entries(panelTypes)) {
// Skip hidden and deprecated panels
if (!panel.hideFromList && panel.state !== 'deprecated') {
// Open visualization picker
const vizPicker = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker);
await vizPicker.click();
await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click();
// Verify panel type is selected
await expect(vizPicker).toHaveText(panel.name);
// Ensure no unexpected error occurred
await expect(page.getByText('An unexpected error happened')).toBeHidden();
}
}
});
}
);
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
. scripts/grafana-server/variables
LICENSE_PATH=""
if [ "$1" = "enterprise" ]; then
if [ "$2" != "dev" ] && [ "$2" != "debug" ]; then
LICENSE_PATH=$2/license.jwt
else
LICENSE_PATH=$3/license.jwt
fi
fi
if [ "$BASE_URL" != "" ]; then
echo -e "BASE_URL set, skipping starting server"
else
# Start it in the background
./scripts/grafana-server/start-server $LICENSE_PATH 2>&1 > scripts/grafana-server/server.log &
./scripts/grafana-server/wait-for-grafana
fi
PORT=3001 HOST=localhost yarn playwright test

Some files were not shown because too many files have changed in this diff Show More