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
parent ea0ddb3fc9
commit b6580ccb10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
250 changed files with 22677 additions and 1038 deletions

View File

@ -35,7 +35,7 @@ module.exports = [
'data/',
'deployment_tools_config.json',
'devenv',
'e2e/test-plugins',
'e2e-playwright/test-plugins',
'e2e/tmp',
'packages/grafana-ui/src/components/Icon/iconBundle.ts',
'pkg',

View File

@ -289,7 +289,7 @@ steps:
- commands:
- npx wait-on@7.0.1 http://$HOST:$PORT
- yarn playwright install --with-deps chromium
- yarn e2e:playwright
- yarn e2e:playwright --grep @plugins
depends_on:
- grafana-server
- build-test-plugins
@ -761,7 +761,7 @@ steps:
- commands:
- npx wait-on@7.0.1 http://$HOST:$PORT
- yarn playwright install --with-deps chromium
- yarn e2e:playwright
- yarn e2e:playwright --grep @plugins
depends_on:
- grafana-server
- build-test-plugins
@ -2986,6 +2986,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 1198b1489e48a9ced211633a0325d112814553246847fc7320fb5ac2bcb32b7d
hmac: d20f1d6e2e8347701f82114ad352f53db57dc95b5b3831941fa93d063a92b9d8
...

9
.github/CODEOWNERS vendored
View File

@ -400,8 +400,10 @@
/public/app/core/internationalization/ @grafana/grafana-frontend-platform
/e2e/ @grafana/grafana-frontend-platform
/e2e/cloud-plugins-suite/ @grafana/partner-datasources
/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend
/e2e-playwright/ @grafana/grafana-frontend-platform
/e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources
/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
/e2e-playwright/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend
# Packages
/packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
@ -477,6 +479,7 @@
/cypress.config.js @grafana/grafana-frontend-platform
/.levignore.js @grafana/plugins-platform-frontend
playwright.config.ts @grafana/plugins-platform-frontend
playwright.storybook.config.ts @grafana/grafana-frontend-platform
# public folder
/public/app/api/ @grafana/grafana-frontend-platform
@ -770,6 +773,7 @@ embed.go @grafana/grafana-as-code
/.github/pr-checks.json @tolzhabayev
/.github/pr-commands.json @tolzhabayev
/.github/renovate.json5 @grafana/frontend-ops
/.github/actions/check-jobs/action.yml @grafana/grafana-frontend-platform
/.github/actions/setup-enterprise/action.yml @grafana/grafana-backend-group
/.github/actions/setup-grafana-bench/ @Proximyst
/.github/actions/build-package @grafana/grafana-developer-enablement-squad
@ -817,6 +821,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend
/.github/workflows/stale.yml @grafana/grafana-developer-enablement-squad
/.github/workflows/storybook-verification.yml @grafana/grafana-frontend-platform
/.github/workflows/storybook-verification-playwright.yml @grafana/grafana-frontend-platform
/.github/workflows/update-make-docs.yml @grafana/docs-tooling
/.github/workflows/scripts/kinds/verify-kinds.go @grafana/platform-monitoring
/.github/workflows/scripts/create-security-branch/create-security-branch.sh @grafana/grafana-developer-enablement-squad

48
.github/actions/check-jobs/action.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: Check jobs results
description: Checks if any jobs have failed and exits with error if failures are found. Use to check the results of matrix test runs.
inputs:
needs:
description: JSON string containing the needs context from the workflow
required: true
failure-message:
description: Custom message to display when failures are found
required: false
default: "One or more jobs have failed"
success-message:
description: Custom message to display when all jobs pass
required: false
default: "All jobs passed successfully"
outputs:
any-failed:
description: Whether any jobs failed
value: ${{ steps.check-jobs.outputs.any-failed }}
runs:
using: "composite"
steps:
- name: Check test suites
id: check-jobs
shell: bash
env:
NEEDS: ${{ inputs.needs }}
FAILURE_MSG: ${{ inputs.failure-message }}
SUCCESS_MSG: ${{ inputs.success-message }}
run: |
set -euo pipefail
# Print the needs context, debugging
echo "$NEEDS" | jq
# Extract failures
FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')"
# Check if there are any failures
if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then
echo "❌ $FAILURE_MSG"
echo "Failed suites:"
echo "$FAILURES" | jq -r 'to_entries[] | "- \(.key): \(.value)"'
echo "any-failed=true" >> "$GITHUB_OUTPUT"
exit 1
fi
echo "✅ $SUCCESS_MSG"

View File

@ -7,12 +7,17 @@ on:
- main
- release-*.*.*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
# TODO: re-enable this before merging
# concurrency:
# group: ${{ github.workflow }}-${{ github.ref }}
# cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
permissions: {}
env:
ACTIONS_STEP_DEBUG: true
RUNNER_DEBUG: 1
jobs:
detect-changes:
name: Detect whether code changed
@ -44,22 +49,56 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
path: ./grafana
persist-credentials: false
- uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
# TODO: add a cleanup workflow to remove the cache when the PR is closed
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
# TODO: maybe we could just use the cache to store the build, instead of uploading as an artifact?
- uses: actions/cache@v4
id: cache
with:
key: "build-grafana-${{ runner.os }}-${{ hashFiles('yarn.lock', 'public/*', 'packages/*', 'pkg/**/*.go', '**/go.mod', '**/go.sum', '!**_test.go', '!**.test.ts', '!**.test.tsx') }}"
path: |
build-dir
# If no cache hit, build Grafana
- name: Build Grafana
if: steps.cache.outputs.cache-hit != 'true'
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt
- run: mv "$(cat out.txt)" grafana.tar.gz
- run: echo "artifact=grafana-e2e-${{github.run_number}}" >> "$GITHUB_OUTPUT"
args: go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}" > out.txt
- name: Cat built artifact
if: steps.cache.outputs.cache-hit != 'true'
run: cat out.txt
- name: Move built artifact
if: steps.cache.outputs.cache-hit != 'true'
run: |
mkdir -p build-dir
mv "$(cat out.txt)" build-dir/grafana.tar.gz
# If cache hit, validate the artifact is present
- name: Validate artifact
if: steps.cache.outputs.cache-hit == 'true'
run: |
if [ ! -f build-dir/grafana.tar.gz ]; then
echo "Error: build-dir/grafana.tar.gz not found in cache"
exit 1
fi
- name: Set artifact name
run: echo "artifact=grafana-server-${{github.run_number}}" >> "$GITHUB_OUTPUT"
id: artifact
- uses: actions/upload-artifact@v4
- name: Upload artifact
uses: actions/upload-artifact@v4
id: upload
with:
retention-days: 1
name: ${{ steps.artifact.outputs.artifact }}
path: grafana.tar.gz
path: build-dir/grafana.tar.gz
# TODO: we won't need this when we only have playwright
build-e2e-runner:
needs: detect-changes
if: needs.detect-changes.outputs.changed == 'true'
@ -159,6 +198,119 @@ jobs:
path: videos
retention-days: 1
run-playwright-tests:
needs:
- build-grafana
name: Playwright E2E tests (${{ matrix.shard }}/${{ matrix.shardTotal }})
runs-on: ubuntu-latest-8-cores
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build-grafana.outputs.artifact }}
- name: Run E2E tests
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go run ./pkg/build/e2e-playwright --package=grafana.tar.gz --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} --blob-dir=./blob-report
- uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: playwright-blob-${{ github.run_number }}-${{ matrix.shard }}
path: ./blob-report
retention-days: 1
required-playwright-tests:
needs:
- run-playwright-tests
- build-grafana
if: ${{ !cancelled() }}
name: All Playwright tests complete
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: blobs
pattern: playwright-blob-*
merge-multiple: true
- name: Check blob reports
run: |
if [ ! "$(ls -A ./blobs)" ]; then
echo "Error: No blob reports found in ./blobs directory"
echo "Did the Playwright tests run at all?"
exit 1
fi
echo "Found blob reports in ./blobs:"
ls -lah ./blobs
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./blobs
- name: Merge into JSON Report
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: /tmp/playwright-results.json
run: npx playwright merge-reports --reporter=json ./blobs
- name: Bench report
run: |
docker run --rm \
--volume="/tmp/playwright-results.json:/home/bench/tests/playwright-results.json" \
us-docker.pkg.dev/grafanalabs-global/docker-grafana-bench-prod/grafana-bench:v0.5.1 report \
--grafana-url "http://localhost:3000" \
--grafana-version "CI- ${{ github.sha }}" \
--test-suite-name "FrontendCore" \
--report-input playwright \
--report-output log \
--log-level DEBUG \
/home/bench/tests/playwright-results.json
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-html-${{ github.run_number }}
path: playwright-report
retention-days: 7
- name: Check test suites
id: check-jobs
uses: ./.github/actions/check-jobs
continue-on-error: true # Failure will be reported on Show test results step
with:
needs: ${{ toJson(needs) }}
failure-message: "One or more E2E test suites have failed"
success-message: "All E2E test suites completed successfully"
- name: Show test results
env:
FAILED: ${{ steps.check-jobs.outputs.any-failed }}
# sed removes the leading `../../src/` from the paths
run: |
npx playwright merge-reports --reporter list ./all-blob-reports | sed 's|\(\.\./\)\{1,\}src/|/|g'
if [ "$FAILED" = "true" ]; then
exit 1
fi
run-a11y-test:
needs:
- build-grafana
@ -193,6 +345,7 @@ jobs:
required-e2e-tests:
needs:
- run-e2e-tests
- build-grafana
# a11y test is not listed on purpose: it is not an important E2E test.
# It is also totally fine to fail right now.
# always() is the best function here.
@ -203,13 +356,13 @@ jobs:
name: All E2E tests complete
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Check test suites
env:
NEEDS: ${{ toJson(needs) }}
run: |
FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')"
echo "$FAILURES"
if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then
exit 1
fi
echo "All OK!"
uses: ./.github/actions/check-jobs
with:
needs: ${{ toJson(needs) }}
failure-message: "One or more E2E test suites have failed"
success-message: "All E2E test suites completed successfully"

View File

@ -208,7 +208,7 @@ jobs:
if: ${{ inputs.bump == true || inputs.bump == 'true' }}
run: |
git add package.json lerna.json yarn.lock packages public
test -e e2e/test-plugins && git add e2e/test-plugins
test -e e2e-playwright/test-plugins && git add e2e-playwright/test-plugins
git commit -m "Update version to $VERSION"
- name: Git push

View File

@ -64,7 +64,7 @@ jobs:
tools/
public/
conf/
e2e/test-plugins/
e2e-playwright/test-plugins/
devenv/
key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }}
# only rebuild grafana if search files have changed ( or dependencies )
@ -116,7 +116,7 @@ jobs:
tools/
public/
conf/
e2e/test-plugins/
e2e-playwright/test-plugins/
devenv/
key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }}
- name: Set the step name

View File

@ -0,0 +1,45 @@
name: Verify Storybook (Playwright)
on:
pull_request:
paths:
- 'packages/grafana-ui/**'
- '!docs/**'
- '!*.md'
push:
branches:
- main
paths:
- 'packages/grafana-ui/**'
- '!docs/**'
- '!*.md'
permissions: {}
jobs:
verify-storybook:
name: Verify Storybook (Playwright)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Storybook and E2E tests
run: yarn e2e:playwright:storybook

2
.gitignore vendored
View File

@ -237,4 +237,4 @@ public/app/plugins/**/dist/
# Mock service worker used for fake API responses in frontend development
public/mockServiceWorker.js
/e2e/test-plugins/*/dist
/e2e-playwright/test-plugins/*/dist

View File

@ -6,9 +6,9 @@ When end-to-end testing Grafana plugins, a best practice is to use the [`@grafan
## Add end-to-end tests for a core plugin
You can add Playwright end-to-end tests for plugins to the [`e2e/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e) directory.
You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory.
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e). This is the directory where your plugin tests will be kept.
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept.
1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration.
In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests.

View File

@ -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();
}
}
);

View File

@ -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
);
});
}
);

View File

@ -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);
});
}
);

View File

@ -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();
});
}
);

View File

@ -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();
});
}
);

View File

@ -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]}"`);
});
}
);

View File

@ -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}`);
});
}
);

View File

@ -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]}`);
});
}
);

View File

@ -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`);
});
}
);

View File

@ -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\)/);
});
}
);

View File

@ -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}`);
});
}
);

View File

@ -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');
});
}
);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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');
});
}
);

View File

@ -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;
};

View File

@ -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 });
}

View File

@ -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();
});
}
);

View File

@ -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();
});
}
);

View File

@ -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.*/);
});
}
);

View File

@ -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();
});
}
);

View File

@ -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));
});
}
);

View File

@ -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);
});
}
);

View File

@ -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}`;
};

View File

@ -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();
});
}
);

View File

@ -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);
});
}
);

View File

@ -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();
});
}
);

View File

@ -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}`;
};

View File

@ -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)}`
);
});
}
);

View File

@ -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);
});
}
);

View File

@ -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$/);
});
}
);

View File

@ -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();
});
}
);

View File

@ -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 = '';
}
});
}
);

View File

@ -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();
});
}
);

View File

@ -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();
});
}
);

View File

@ -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');
});
}
);

View File

@ -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');
});
}
);

View File

@ -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');
});
}
);

View File

@ -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();
});
}
);

View File

@ -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');
});
}
);

View File

@ -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();
}
});
}
);

View File

@ -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();
}
});
}
);

View File

@ -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();
}
});
}
);

View File

@ -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();
});
}
);

View File

@ -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() || '';
};

View File

@ -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');
});
}
);

View File

@ -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}`);
}

View File

@ -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;
}

View File

@ -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": ""
}

View File

@ -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
}

View File

@ -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": ""
}

View File

@ -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
}

View File

@ -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": {}
}

View File

@ -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

View File

@ -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');
}
});
}
);

View File

@ -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();
});
});
}
);

View File

@ -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();
});
}
);

View File

@ -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();
});
}
);

View File

@ -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();
});
}
);

View File

@ -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();
});
}
);

View File

@ -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();
};

View File

@ -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();
});
}
);

View File

@ -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();
}
);

View File

@ -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();
}
);

View File

@ -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();
}
);

View File

@ -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();
}
);

View File

@ -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();
}
);

View File

@ -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();
}
);

View File

@ -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();
}
);

View File

@ -3,6 +3,6 @@ import { test, expect } from '@grafana/plugin-e2e';
test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'loki' });
await expect(await page.getByText('Type: Loki', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
await expect(page.getByText('Type: Loki', { exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
});

View File

@ -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();
}
);

View File

@ -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();
}
);

View File

@ -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`);
});
}
);

View File

@ -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();
}
);

View File

@ -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();
});
});

View File

@ -41,13 +41,15 @@ const scenarios: Scenario[] = [
},
];
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 });
});
}
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 });
});
}
});

View File

@ -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');
});
}
);

View File

@ -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' });
});
});
}
);

View File

@ -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();
});
}
);

View File

@ -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();
});
}
);

View File

@ -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);
});
});
}
);

View File

@ -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']);
});
});
}
);

View File

@ -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');
});
}
);

View File

@ -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);
});
}
);

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