Dashboard: POC to run existing e2e with dashboardScene feature toggle (#84598)

* Standarize e2e for addDashbaord e2e flow

* WIP: Duplicate e2e dashboard flows and smoke test for scene e2e tests

* Fix autoformatting mistake for bash file

* Enable dashboardScene using local storage and only for the scene folder

* Add missing folders

* Set the feature toggle in the before of all tests

* Revert "Standarize e2e for addDashbaord e2e flow"

This reverts commit 6b9ea9d5a4.

* Add missing e2e selectors to NavToolbarActions, and modify addDashboard scene flow

* e2e: panels_smokescreen.spec.ts migrated

* e2e smokeTestSceneario migrated

* Start migrating dashbaord-suite e2e

* WIP create variable types

* adjust tests for scenes

* restore dashboard json file

* update scenes version

* restore pkg/build/wire/internal/wire/testdata modifications

* finalising test adjusments

* restore pkg/build/wire/internal/wire/testdata files

* add latest scenes version and update tests

* add drone setup for dashboard scenes tests

* update to latest scenes version

* adjust drone errors

* adjust indentation in drone yml file

* drone adjustments

* add github workflow to run scenes e2e

* restore drone file

* adjust github workflow

* wip: github workflow adjustments

* test remove gpu

* bump

* undo formating changes

* wip: github workflow debugging

* adjusting flaky tests

* update to latest scenes

* clean up workflow file

* adjust flaky test

* clean up pr

* finalise worflow logic and add to codeowners

* clean up launching old arch dashboards e2e separately

---------

Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com>
Co-authored-by: Jeff Levin <jeff@levinology.com>
This commit is contained in:
Alexa V 2024-05-01 16:56:48 +02:00 committed by GitHub
parent 0fbfb67b15
commit 9ea1042329
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 4534 additions and 6 deletions

View File

@ -5,6 +5,9 @@
//
exports[`better eslint`] = {
value: `{
"e2e/scenes/utils/support/types.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"e2e/utils/support/types.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -6122,6 +6125,9 @@ exports[`no undocumented stories`] = {
exports[`no gf-form usage`] = {
value: `{
"e2e/scenes/utils/flows/addDataSource.ts:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"e2e/utils/flows/addDataSource.ts:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],

1
.github/CODEOWNERS vendored
View File

@ -695,6 +695,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/i18n-crowdin-upload.yml @grafana/grafana-frontend-platform
/.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform
/.github/workflows/pr-go-workspace-check.yml @grafana/grafana-app-platform-squad
/.github/workflows/run-scenes-e2e.yml @grafana/dashboards-squad
# Generated files not requiring owner approval
/packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot

41
.github/workflows/run-scenes-e2e.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Run dashboard scenes e2e
on:
schedule:
- cron: "0 8 * * 1-5" # every day at 08:00UTC on weekdays
env:
ARCH: linux-amd64
jobs:
dashboard-scenes-e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: yarn install --immutable
- name: Build grafana
run: make build
- name: Install Cypress dependencies
uses: cypress-io/github-action@v6
with:
runTests: false
- name: Run dashboard scenes e2e
run: yarn e2e:scenes
- name: "Send Slack notification"
if: ${{ failure() }}
uses: slackapi/slack-github-action@v1.26.0
with:
payload: >
{
"icon_emoji": ":this-is-fine-fire:",
"username": "Dashboard scenes e2e tests failed",
"text": "Link to run: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}",
"channel": "#grafana-dashboards-squad"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@ -26,3 +26,9 @@ Cypress.Commands.add('startBenchmarking', (testName) => {
Cypress.Commands.add('stopBenchmarking', (testName, appStats) => {
return cy.task('stopBenchmarking', { testName, appStats });
});
Cypress.Commands.add('setLocalStorage', (key, value) => {
cy.window().then((win) => {
win.localStorage.setItem(key, value);
});
});

View File

@ -43,3 +43,11 @@ Cypress.on('uncaught:exception', (err) => {
// // failing the test
// return false;
// });
//
beforeEach(() => {
if (Cypress.env('SCENES')) {
cy.logToConsole('enabling dashboardScene feature toggle in localstorage');
cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=true');
}
});

View File

@ -8,5 +8,6 @@ declare namespace Cypress {
startBenchmarking(testName: string): void;
stopBenchmarking(testName: string, appStats: Record<string, unknown>): void;
checkHealthRetryable(fn: Function, retryCount: number): Chainable;
setLocalStorage(key: string, value: string);
}
}

View File

@ -27,6 +27,7 @@ declare -A env=(
testFilesForSingleSuite="*.spec.ts"
rootForEnterpriseSuite="./e2e/extensions-suite"
rootForScenesSuite="./e2e/scenes"
declare -A cypressConfig=(
[screenshotsFolder]=./e2e/"${args[0]}"/screenshots
@ -53,6 +54,7 @@ case "$1" in
echo "Dev mode"
CMD="cypress open"
;;
"benchmark")
echo "Benchmark"
PARAMS="--headed --no-runner-ui"
@ -85,6 +87,13 @@ case "$1" in
;;
"")
;;
"scenes")
env[SCENES]=true
cypressConfig[specPattern]=$rootForScenesSuite/*/$testFilesForSingleSuite
cypressConfig[video]=false
# CMD="cypress open"
;;
*)
cypressConfig[specPattern]=./e2e/"${args[0]}"/$testFilesForSingleSuite
cypressConfig[video]=${args[1]}

View File

@ -0,0 +1,99 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally';
describe('Repeating a panel horizontally', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('should be able to repeat a panel horizontally', () => {
e2e.flows.openDashboard({ 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'];
panelTitles.forEach((title) => {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(left).to.be.greaterThan(prevLeft);
if (prevTop !== null) {
expect(top).to.be.equal(prevTop);
}
prevLeft = left;
prevTop = top;
});
});
});
it('responds to changes to the variables', () => {
e2e.flows.openDashboard({ 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'];
panelTitles.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('be.visible');
});
// Change to only show panels 1 + 3
e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal')
.parent()
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().contains('1').click();
e2e.components.Select.option().contains('3').click();
// blur the dropdown
cy.get('body').click();
const panelsShown = ['Panel Title 1', 'Panel Title 3'];
const panelsNotShown = ['Panel Title 2'];
panelsShown.forEach((title) => {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(left).to.be.greaterThan(prevLeft);
if (prevTop !== null) {
expect(top).to.be.equal(prevTop);
}
prevLeft = left;
prevTop = top;
});
});
panelsNotShown.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('not.exist');
});
});
it('loads a dashboard based on the query params correctly', () => {
// Have to manually add the queryParams to the url because they have the same name
e2e.flows.openDashboard({ 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
panelsShown.forEach((title) => {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(left).to.be.greaterThan(prevLeft);
if (prevTop !== null) {
expect(top).to.be.equal(prevTop);
}
prevLeft = left;
prevTop = top;
});
});
panelsNotShown.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('not.exist');
});
});
});

View File

@ -0,0 +1,98 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically';
describe('Repeating a panel vertically', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('should be able to repeat a panel vertically', () => {
e2e.flows.openDashboard({ 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'];
panelTitles.forEach((title) => {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(top).to.be.greaterThan(prevTop);
if (prevLeft !== null) {
expect(left).to.be.equal(prevLeft);
}
prevLeft = left;
prevTop = top;
});
});
});
it('responds to changes to the variables', () => {
e2e.flows.openDashboard({ 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'];
panelTitles.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('be.visible');
});
// Change to only show panels 1 + 3
e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical')
.parent()
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().contains('1').click();
e2e.components.Select.option().contains('3').click();
// blur the dropdown
cy.get('body').click();
const panelsShown = ['Panel Title 1', 'Panel Title 3'];
const panelsNotShown = ['Panel Title 2'];
panelsShown.forEach((title) => {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(top).to.be.greaterThan(prevTop);
if (prevLeft !== null) {
expect(left).to.be.equal(prevLeft);
}
prevLeft = left;
prevTop = top;
});
});
panelsNotShown.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('not.exist');
});
});
it('loads a dashboard based on the query params correctly', () => {
// Have to manually add the queryParams to the url because they have the same name
e2e.flows.openDashboard({ 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'];
panelsShown.forEach((title) => {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(top).to.be.greaterThan(prevTop);
if (prevLeft !== null) {
expect(left).to.be.equal(prevLeft);
}
prevLeft = left;
prevTop = top;
});
});
panelsNotShown.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('not.exist');
});
});
});

View File

@ -0,0 +1,85 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row';
describe.skip('Repeating empty rows', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('should be able to repeat empty rows vertically', () => {
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
let prevTop = Number.NEGATIVE_INFINITY;
const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3'];
rowTitles.forEach((title) => {
e2e.components.DashboardRow.title(title)
.should('be.visible')
.then(($el) => {
const { top } = $el[0].getBoundingClientRect();
expect(top).to.be.greaterThan(prevTop);
prevTop = top;
});
});
});
it('responds to changes to the variables', () => {
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
let prevTop = Number.NEGATIVE_INFINITY;
const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3'];
rowTitles.forEach((title) => {
e2e.components.DashboardRow.title(title).should('be.visible');
});
e2e.pages.Dashboard.SubMenu.submenuItemLabels('row')
.parent()
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().contains('1').click();
e2e.components.Select.option().contains('3').click();
// blur the dropdown
cy.get('body').click();
const rowsShown = ['Row title 1', 'Row title 3'];
const rowsNotShown = ['Row title 2'];
rowsShown.forEach((title) => {
e2e.components.DashboardRow.title(title)
.should('be.visible')
.then(($el) => {
const { top } = $el[0].getBoundingClientRect();
expect(top).to.be.greaterThan(prevTop);
prevTop = top;
});
});
rowsNotShown.forEach((title) => {
e2e.components.DashboardRow.title(title).should('not.exist');
});
});
it('loads a dashboard based on the query params correctly', () => {
// Have to manually add the queryParams to the url because they have the same name
e2e.flows.openDashboard({ 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'];
rowsShown.forEach((title) => {
e2e.components.DashboardRow.title(title)
.should('be.visible')
.then(($el) => {
const { top } = $el[0].getBoundingClientRect();
expect(top).to.be.greaterThan(prevTop);
prevTop = top;
});
});
rowsNotShown.forEach((title) => {
e2e.components.DashboardRow.title(title).should('not.exist');
});
});
});

View File

@ -0,0 +1,140 @@
import { e2e } from '../utils';
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 enable this test when nested folders goes live
describe.skip('Dashboard browse (nested)', () => {
const dashboardUIDsToCleanUp: string[] = [];
const folderUIDsToCleanUp: string[] = [];
// Add nested folder structure
before(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false);
// Add root folders
for (let i = 0; i < NUM_ROOT_FOLDERS; i++) {
cy.request({
method: 'POST',
url: '/api/folders',
body: {
title: `Root folder ${i.toString().padStart(2, '0')}`,
},
headers: {
'Content-Type': 'application/json',
},
}).then((response) => {
folderUIDsToCleanUp.push(response.body.uid);
});
}
// Add root dashboards
for (let i = 0; i < NUM_ROOT_DASHBOARDS; i++) {
cy.request({
method: 'POST',
url: '/api/dashboards/db',
body: makeNewDashboardRequestBody(`Root dashboard ${i.toString().padStart(2, '0')}`),
headers: {
'Content-Type': 'application/json',
},
}).then((response) => {
dashboardUIDsToCleanUp.push(response.body.uid);
});
}
// Add folder with children
cy.request({
method: 'POST',
url: '/api/folders',
body: {
title: 'A root folder with children',
},
headers: {
'Content-Type': 'application/json',
},
}).then((response) => {
const folderUid = response.body.uid;
folderUIDsToCleanUp.push(folderUid);
// Add nested folders
for (let i = 0; i < NUM_NESTED_FOLDERS; i++) {
cy.request({
method: 'POST',
url: '/api/folders',
body: {
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++) {
cy.request({
method: 'POST',
url: '/api/dashboards/db',
body: makeNewDashboardRequestBody(`Nested dashboard ${i.toString().padStart(2, '0')}`, folderUid),
headers: {
'Content-Type': 'application/json',
},
});
}
});
});
// Remove nested folder structure
after(() => {
// Clean up root dashboards
for (const dashboardUID of dashboardUIDsToCleanUp) {
e2e.flows.deleteDashboard({
uid: dashboardUID,
quick: true,
title: '',
});
}
// Clean up root folders (cascading delete will remove any nested folders and dashboards)
for (const folderUID of folderUIDsToCleanUp) {
cy.request({
method: 'DELETE',
url: `/api/folders/${folderUID}`,
qs: {
forceDeleteRules: false,
},
});
}
});
it('pagination works correctly for folders and root', () => {
e2e.pages.Dashboards.visit();
cy.contains('A root folder with children').should('be.visible');
// Expand A root folder with children
cy.get('[aria-label="Expand folder A root folder with children"]').click();
cy.contains('Nested folder 00').should('be.visible');
// Scroll the page and check visibility of next set of items
e2e.pages.BrowseDashboards.table.body().find('> div').scrollTo(0, 1700);
cy.contains('Nested folder 59').should('be.visible');
cy.contains('Nested dashboard 00').should('be.visible');
// Scroll the page and check visibility of next set of items
e2e.pages.BrowseDashboards.table.body().find('> div').scrollTo(0, 3800);
cy.contains('Nested dashboard 59').should('be.visible');
cy.contains('Root folder 00').should('be.visible');
// Scroll the page and check visibility of next set of items
e2e.pages.BrowseDashboards.table.body().find('> div').scrollTo(0, 5900);
cy.contains('Root folder 59').should('be.visible');
cy.contains('Root dashboard 00').should('be.visible');
// Scroll the page and check visibility of next set of items
e2e.pages.BrowseDashboards.table.body().find('> div').scrollTo(0, 8000);
cy.contains('Root dashboard 59').should('be.visible');
});
});

View File

@ -0,0 +1,52 @@
import testDashboard from '../dashboards/TestDashboard.json';
import { e2e } from '../utils';
describe('Dashboard browse', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('Manage Dashboards tests', () => {
e2e.flows.importDashboard(testDashboard, 1000, true);
e2e.pages.Dashboards.visit();
// Folders and dashboards should be visible
e2e.pages.BrowseDashboards.table.row('gdev dashboards').should('be.visible');
e2e.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard').should('be.visible');
// gdev dashboards folder is collapsed - its content should not be visible
e2e.pages.BrowseDashboards.table.row('Bar Gauge Demo').should('not.exist');
// should click a folder and see it's children
e2e.pages.BrowseDashboards.table.row('gdev dashboards').find('[aria-label^="Expand folder"]').click();
e2e.pages.BrowseDashboards.table.row('Bar Gauge Demo').should('be.visible');
// Open the new folder drawer
cy.contains('button', 'New').click();
cy.contains('button', 'New folder').click();
// And create a new folder
e2e.pages.BrowseDashboards.NewFolderForm.nameInput().type('My new folder');
e2e.pages.BrowseDashboards.NewFolderForm.form().contains('button', 'Create').click();
e2e.components.Alert.alertV2('success').find('button[aria-label="Close alert"]').click();
cy.contains('h1', 'My new folder').should('be.visible');
// Delete the folder and expect to go back to the root
cy.contains('button', 'Folder actions').click();
cy.contains('button', 'Delete').click();
e2e.flows.confirmDelete();
cy.contains('h1', 'Dashboards').should('be.visible');
// Can collapse the gdev folder and delete the dashboard we imported
e2e.pages.BrowseDashboards.table.row('gdev dashboards').find('[aria-label^="Collapse folder"]').click();
e2e.pages.BrowseDashboards.table
.row('E2E Test - Import Dashboard')
.find('[type="checkbox"]')
.click({ force: true });
cy.contains('button', 'Delete').click();
e2e.flows.confirmDelete();
e2e.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard').should('not.exist');
});
});

View File

@ -0,0 +1,16 @@
import testDashboard from '../dashboards/DashboardLiveTest.json';
import { e2e } from '../utils';
describe('Dashboard Live streaming support', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
e2e.flows.importDashboard(testDashboard, 1000);
});
it('Should receive streaming data', () => {
e2e.flows.openDashboard({ uid: 'live-e2e-test' });
cy.wait(1000);
e2e.components.Panels.Panel.title('Live').should('exist');
e2e.components.Panels.Visualization.Table.body().find('[role="row"]').should('have.length.at.least', 5);
});
});

View File

@ -0,0 +1,153 @@
import { e2e } from '../utils';
describe('Public dashboards', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('Create a public dashboard', () => {
// Opening a dashboard without template variables
cy.intercept({
pathname: '/api/ds/query',
}).as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
cy.wait('@query');
// Open sharing modal
e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
// Create button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled');
// Create flow shouldn't show these elements
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('not.exist');
// Acknowledge checkboxes
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.enabled').click({ force: true });
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.enabled').click({ force: true });
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.enabled').click({ force: true });
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.enabled');
// Create public dashboard
cy.intercept('POST', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('save');
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().click();
cy.wait('@save');
// These elements shouldn't be rendered after creating public dashboard
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('not.exist');
// These elements should be rendered
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().click();
// There elements should be rendered once the Settings dropdown is opened
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
});
it('Open a public dashboard', () => {
// Opening a dashboard without template variables
cy.intercept({
method: 'POST',
pathname: '/api/ds/query',
}).as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
cy.wait('@query');
// Tag indicating a dashboard is public
e2e.pages.Dashboard.DashNav.publicDashboardTag().should('exist');
// Open sharing modal
e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
cy.wait('@query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().click();
// There elements should be rendered once the Settings dropdown is opened
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
// Make a request to public dashboards api endpoint without authentication
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
.invoke('val')
.then((url) => {
cy.clearCookies()
.request(getPublicDashboardAPIUrl(String(url)))
.then((resp) => {
expect(resp.status).to.eq(200);
});
});
});
it('Disable a public dashboard', () => {
// Opening a dashboard without template variables
cy.intercept({
method: 'POST',
pathname: '/api/ds/query',
}).as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
cy.wait('@query');
// Open sharing modal
e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
cy.wait('@query-public-dashboard');
// save url before disabling public dashboard
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
.invoke('val')
.then((text) => cy.wrap(text).as('url'));
// Save public dashboard
cy.intercept('PATCH', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
// Switch off enabling toggle
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('be.enabled').click({ force: true });
cy.wait('@update');
// Url should be hidden
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('be.disabled');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('be.disabled');
// Make a request to public dashboards api endpoint without authentication
cy.get('@url').then((url) => {
cy.clearCookies()
.request({ url: getPublicDashboardAPIUrl(String(url)), failOnStatusCode: false })
.then((resp) => {
expect(resp.status).to.eq(403);
});
});
});
});
const getPublicDashboardAPIUrl = (url: string): string => {
let accessToken = url.split('/').pop();
return `/api/public/dashboards/${accessToken}`;
};

View File

@ -0,0 +1,29 @@
import { e2e } from '../utils';
describe('Create a public dashboard with template variables shows a template variable warning', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('Create a public dashboard with template variables shows a template variable warning', () => {
// Opening a dashboard with template variables
e2e.flows.openDashboard({ uid: 'HYaGDGIMk' });
// Open sharing modal
e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
// Warning Alert dashboard cannot be made public because it has template variables
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');
// Configuration elements for public dashboards should not exist
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('not.exist');
});
});

View File

@ -0,0 +1,60 @@
import { e2e } from '../utils';
describe('Dashboard templating', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('Verify variable interpolation works', () => {
// Open dashboard global variables and interpolation
e2e.flows.openDashboard({ uid: 'HYaGDGIMk' });
const items: string[] = [];
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=A%27A%22A&var-Server=BB%5CB&var-Server=CCC`,
`1 < 2`,
`Example: from=now-6h&to=now`,
];
cy.get('.markdown-html li')
.should('have.length', 26)
.each((element) => {
items.push(element.text());
})
.then(() => {
expectedItems.forEach((expected, index) => {
expect(items[index]).to.equal(expected);
});
});
// Check link interpolation is working correctly
cy.contains('a', 'Example: from=now-6h&to=now').should(
'have.attr',
'href',
'https://example.com/?from=now-6h&to=now'
);
});
});

View File

@ -0,0 +1,263 @@
import {
addDays,
addHours,
differenceInCalendarDays,
differenceInMinutes,
format,
isBefore,
parseISO,
toDate,
} from 'date-fns';
import { e2e } from '../utils';
describe('Dashboard time zone support', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it.skip('Tests dashboard time zone scenarios', () => {
e2e.flows.openDashboard({ uid: '5SdHCasdf' });
const fromTimeZone = 'UTC';
const toTimeZone = 'America/Chicago';
const offset = offsetBetweenTimeZones(toTimeZone, fromTimeZone);
const panelsToCheck = [
'Random walk series',
'Millisecond res x-axis and tooltip',
'2 yaxis and axis labels',
'Stacking value ontop of nulls',
'Null between points',
'Legend Table No Scroll Visible',
];
const timesInUtc: Record<string, string> = {};
for (const title of panelsToCheck) {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.within(() => {
e2e.components.Panels.Visualization.Graph.xAxis.labels().should('be.visible');
e2e.components.Panels.Visualization.Graph.xAxis
.labels()
.last()
.should((element) => {
timesInUtc[title] = element.text();
});
});
}
e2e.components.PageToolbar.item('Dashboard settings').click();
e2e.components.TimeZonePicker.containerV2()
.should('be.visible')
.within(() => {
e2e.components.Select.singleValue().should('have.text', 'Coordinated Universal Time');
e2e.components.Select.input().should('be.visible').click();
});
e2e.components.Select.option().should('be.visible').contains(toTimeZone).click();
// click to go back to the dashboard.
e2e.pages.Dashboard.Settings.Actions.close().click();
e2e.components.RefreshPicker.runButtonV2().should('be.visible').click();
for (const title of panelsToCheck) {
e2e.components.Panels.Panel.title(title)
.should('be.visible')
.within(() => {
e2e.components.Panels.Visualization.Graph.xAxis.labels().should('be.visible');
e2e.components.Panels.Visualization.Graph.xAxis
.labels()
.last()
.should((element) => {
const inUtc = timesInUtc[title];
const inTz = element.text();
const isCorrect = isTimeCorrect(inUtc, inTz, offset);
expect(isCorrect).to.be.equal(true);
});
});
}
});
// TODO: remove skip once https://github.com/grafana/grafana/issues/86420 is done
it.skip('Tests relative timezone support and overrides', () => {
// Open dashboard
e2e.flows.openDashboard({
uid: 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8',
});
cy.intercept('/api/ds/query*').as('dataQuery');
// Switch to Browser timezone
e2e.flows.setTimeRange({
from: 'now-6h',
to: 'now',
zone: 'Browser',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
// Today so far, still in Browser timezone
e2e.flows.setTimeRange({
from: 'now/d',
to: 'now',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
e2e.components.Panels.Panel.title('Panel in timezone')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
// Test UTC timezone
e2e.flows.setTimeRange({
from: 'now-6h',
to: 'now',
zone: 'Coordinated Universal Time',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
// Today so far, still in UTC timezone
e2e.flows.setTimeRange({
from: 'now/d',
to: 'now',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
e2e.components.Panels.Panel.title('Panel in timezone')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
// Test Tokyo timezone
e2e.flows.setTimeRange({
from: 'now-6h',
to: 'now',
zone: 'Asia/Tokyo',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
// Today so far, still in Tokyo timezone
e2e.flows.setTimeRange({
from: 'now/d',
to: 'now',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
e2e.components.Panels.Panel.title('Panel in timezone')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
// Test LA timezone
e2e.flows.setTimeRange({
from: 'now-6h',
to: 'now',
zone: 'America/Los_Angeles',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
// Today so far, still in LA timezone
e2e.flows.setTimeRange({
from: 'now/d',
to: 'now',
});
// Need to wait for 2 calls as there's 2 panels
cy.wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
e2e.components.Panels.Panel.title('Panel in timezone')
.should('be.visible')
.within(() => {
cy.contains('[role="row"]', '00:00:00').should('be.visible');
});
});
});
const isTimeCorrect = (inUtc: string, inTz: string, offset: number): boolean => {
if (inUtc === inTz) {
// we need to catch issues when timezone isn't changed for some reason like https://github.com/grafana/grafana/issues/35504
return false;
}
const reference = format(new Date(), 'yyyy-LL-dd');
const utcDate = toDate(parseISO(`${reference} ${inUtc}`));
const utcDateWithOffset = addHours(toDate(parseISO(`${reference} ${inUtc}`)), offset);
const dayDifference = differenceInCalendarDays(utcDate, utcDateWithOffset); // if the utcDate +/- offset is the day before/after then we need to adjust reference
const dayOffset = isBefore(utcDateWithOffset, utcDate) ? dayDifference * -1 : dayDifference;
const tzDate = addDays(toDate(parseISO(`${reference} ${inTz}`)), dayOffset); // adjust tzDate with any dayOffset
const diff = Math.abs(differenceInMinutes(utcDate, tzDate)); // use Math.abs if tzDate is in future
return diff <= Math.abs(offset * 60);
};
const offsetBetweenTimeZones = (timeZone1: string, timeZone2: string, when: Date = new Date()): number => {
const t1 = convertDateToAnotherTimeZone(when, timeZone1);
const t2 = convertDateToAnotherTimeZone(when, timeZone2);
return (t1.getTime() - t2.getTime()) / (1000 * 60 * 60);
};
const convertDateToAnotherTimeZone = (date: Date, timeZone: string): Date => {
const dateString = date.toLocaleString('en-US', {
timeZone: timeZone,
});
return new Date(dateString);
};

View File

@ -0,0 +1,46 @@
import { e2e } from '../utils';
describe('Dashboard timepicker', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('Shows the correct calendar days with custom timezone set via preferences', () => {
e2e.flows.setUserPreferences({
timezone: 'Asia/Tokyo',
});
// Open dashboard with time range from 8th to end of 10th.
// Will be Tokyo time because of above preference
e2e.flows.openDashboard({
uid: '5SdHCasdf',
timeRange: {
zone: 'Default',
from: '2022-06-08 00:00:00',
to: '2022-06-10 23:59:59',
},
});
// Assert that the calendar shows 08 and 09 and 10 as selected days
e2e.components.TimePicker.openButton().click();
e2e.components.TimePicker.calendar.openButton().first().click();
cy.get('.react-calendar__tile--active, .react-calendar__tile--hasActive').should('have.length', 3);
});
it('Shows the correct calendar days with custom timezone set via time picker', () => {
// Open dashboard with time range from 2022-06-08 00:00:00 to 2022-06-10 23:59:59 in Tokyo time
e2e.flows.openDashboard({
uid: '5SdHCasdf',
timeRange: {
zone: 'Asia/Tokyo',
from: '2022-06-08 00:00:00',
to: '2022-06-10 23:59:59',
},
});
// Assert that the calendar shows 08 and 09 and 10 as selected days
e2e.components.TimePicker.openButton().click();
e2e.components.TimePicker.calendar.openButton().first().click();
cy.get('.react-calendar__tile--active, .react-calendar__tile--hasActive').should('have.length', 3);
});
});

View File

@ -0,0 +1,24 @@
import { selectors } from '@grafana/e2e-selectors';
import { e2e } from '../utils';
import { fromBaseUrl } from '../utils/support/url';
describe('Embedded dashboard', function () {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('open test page', function () {
cy.visit(fromBaseUrl('/dashboards/embedding-test'));
// Verify pie charts are rendered
cy.get(
`[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]`
).should('have.length', 5);
// Verify no url sync
e2e.components.TimePicker.openButton().click();
cy.get('label:contains("Last 1 hour")').click();
cy.url().should('eq', fromBaseUrl('/dashboards/embedding-test'));
});
});

View File

@ -0,0 +1,35 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
describe('Dashboards', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('should restore scroll position', () => {
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
e2e.components.Panels.Panel.title('Panel #1').should('be.visible');
// scroll to the bottom
e2e.pages.Dashboard.DashNav.scrollContainer()
.children()
.first()
.scrollTo('bottom', {
timeout: 5 * 1000,
});
// The last panel should be visible...
e2e.components.Panels.Panel.title('Panel #50').should('be.visible');
// Then we open and close the panel editor
e2e.components.Panels.Panel.menu('Panel #50').click({ force: true }); // it only shows on hover
e2e.components.Panels.Panel.menuItems('Edit').click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
// And the last panel should still be visible!
// TODO: investigate scroll to on navigating back
// e2e.components.Panels.Panel.title('Panel #50').should('be.visible');
e2e.components.Panels.Panel.title('Panel #1').should('be.visible');
});
});

View File

@ -0,0 +1,12 @@
import testDashboard from '../dashboards/TestDashboard.json';
import { e2e } from '../utils';
describe('Import Dashboards Test', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('Ensure you can import a number of json test dashboards from a specific test directory', () => {
e2e.flows.importDashboard(testDashboard, 1000);
});
});

View File

@ -0,0 +1,203 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
describe('Variables - Load options from Url', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('default options should be correct', () => {
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
cy.intercept({
method: 'POST',
pathname: '/api/ds/query*',
}).as('query');
cy.wait('@query');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.children()
.children()
.first()
.click();
e2e.components.Select.option().parent().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'B')
.next()
.should('have.text', 'C')
.next()
.should('have.text', 'D');
cy.get('body').click(0, 0);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')
.should('be.visible')
.children()
.children()
.first()
.click();
e2e.components.Select.option().parent().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'AB')
.next()
.should('have.text', 'AC')
.next()
.should('have.text', 'AD');
cy.get('body').click(0, 0);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')
.should('be.visible')
.children()
.children()
.first()
.click();
e2e.components.Select.option().parent().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'AAA')
.parent()
.next()
.should('have.text', 'AAB')
.next()
.should('have.text', 'AAC')
.next()
.should('have.text', 'AAD');
});
it('options set in url should load correct options', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=B&var-server=BB&var-pod=BBB` });
cy.intercept({
method: 'POST',
pathname: '/api/ds/query',
}).as('query');
cy.wait('@query');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')
.should('be.visible')
.children()
.children()
.first()
.click();
e2e.components.Select.option().parent().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'A')
.next()
.should('have.text', 'C')
.next()
.should('have.text', 'D');
cy.get('body').click(0, 0);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')
.should('be.visible')
.children()
.children()
.first()
.click();
e2e.components.Select.option().parent().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'BA')
.next()
.should('have.text', 'BC')
.next()
.should('have.text', 'BD');
cy.get('body').click(0, 0);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')
.should('be.visible')
.children()
.children()
.first()
.click();
e2e.components.Select.option().parent().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'BBA')
.next()
.should('have.text', 'BBC')
.next()
.should('have.text', 'BBD');
});
it('options set in url that do not exist should load correct options', () => {
// @ts-ignore some typing issue
cy.on('uncaught:exception', (err) => {
if (err.stack?.indexOf("Couldn't find any field of type string in the results.") !== -1) {
// return false to prevent the error from
// failing this test
return false;
}
return true;
});
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=X` });
cy.intercept({
method: 'POST',
pathname: '/api/ds/query',
}).as('query');
cy.wait('@query');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X')
.should('be.visible')
.children()
.children()
.first()
.click();
e2e.components.Select.option().parent().should('have.length', 9);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'A')
.next()
.should('have.text', 'B')
.next()
.should('have.text', 'C');
cy.get('body').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')
.should('be.visible')
.should('have.length', 2);
});
});

View File

@ -0,0 +1,38 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
describe('Variables - Constant', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('can add a new constant variable', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
// Create a new "Constant" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
cy.get('input').type('Constant{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type('pesto').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
// e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto');
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.components.RefreshPicker.runButtonV2().click();
// Assert it was rendered
cy.get('.markdown-html').should('include.text', 'VariableUnderTest: pesto');
// Assert the variable is not visible in the dashboard nav
e2e.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test').should('not.exist');
});
});

View File

@ -0,0 +1,68 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
function fillInCustomVariable(name: string, label: string, value: string) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
cy.get('input').type('Custom{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput().type(value).blur();
}
function assertPreviewValues(expectedValues: string[]) {
for (const expected of expectedValues) {
const index = expectedValues.indexOf(expected);
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(index).should('have.text', expected);
}
}
describe('Variables - Custom', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('can add a custom template variable', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
// Create a new "Custom" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
fillInCustomVariable('VariableUnderTest', 'Variable under test', 'one,two,three');
assertPreviewValues(['one', 'two', 'three']);
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one').click();
e2e.components.Select.option().contains('two').click();
// Assert it was rendered
cy.get('.markdown-html').should('include.text', 'VariableUnderTest: two');
});
it('can add a custom template variable with labels', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
// Create a new "Custom" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
cy.get('input').type('Custom{enter}');
});
// Set its name, label, and content
fillInCustomVariable('VariableUnderTest', 'Variable under test', 'One : 1,Two : 2, Three : 3');
assertPreviewValues(['One', 'Two', 'Three']);
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1').click();
e2e.components.Select.option().contains('Two').click();
// Assert it was rendered
cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 2');
});
});

View File

@ -0,0 +1,49 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
describe('Variables - Datasource', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('can add a new datasource variable', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
// Create a new "Datasource" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
cy.get('input').type('Data source{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
// If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus"
// Or, just update is to match some gdev datasources to test with :)
e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().within(() => {
cy.get('input').type('Prometheus{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should(
'contain.text',
'gdev-prometheus'
);
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should(
'contain.text',
'gdev-slow-prometheus'
);
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.components.RefreshPicker.runButtonV2().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus').click();
e2e.components.Select.option().contains('gdev-slow-prometheus').click();
// Assert it was rendered
cy.get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus-uid');
cy.get('.markdown-html').should('include.text', 'VariableUnderTestText: gdev-slow-prometheus');
});
});

View File

@ -0,0 +1,50 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
function assertPreviewValues(expectedValues: string[]) {
for (const expected of expectedValues) {
const index = expectedValues.indexOf(expected);
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(index).should('have.text', expected);
}
}
describe('Variables - Interval', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
// TODO: remove skip once https://github.com/grafana/grafana/issues/84727 is done
it.skip('can add a new interval variable', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
// Create a new "Interval" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
cy.get('input').type('Interval{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput()
.clear()
.type('10s,10m,60m,90m,1h30m')
.blur();
assertPreviewValues(['10s', '10m', '60m', '90m', '1h30m']);
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.components.RefreshPicker.runButtonV2().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('10s').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1h30m').click();
// Assert it was rendered
cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 1h30m');
});
});

View File

@ -0,0 +1,181 @@
import { selectors } from '@grafana/e2e-selectors';
import { e2e } from '../utils';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
const DASHBOARD_NAME = 'Templating - Nested Template Variables';
describe('Variables - Query - Add variable', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('query variable should be default and default fields should be correct', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`)
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2()
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('Variable name');
expect(input.val()).equals('query0');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', 'Query');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2()
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('Label name');
expect(input.val()).equals('');
});
cy.get('[placeholder="Descriptive text"]')
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('Descriptive text');
expect(input.val()).equals('');
});
cy.get('label').contains('Show on dashboard').should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.get('input[placeholder="gdev-testdata"]')
.scrollIntoView()
.should('be.visible');
cy.get('label').contains('Refresh').scrollIntoView().should('be.visible');
cy.get('label').contains('On dashboard load').scrollIntoView().should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2()
.should('be.visible')
.within((input) => {
const placeholder = '/.*-(?<text>.*)-(?<value>.*)-.*/';
expect(input.attr('placeholder')).equals(placeholder);
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', 'Disabled');
});
cy.contains('label', 'Multi-value').within(() => {
cy.get('input[type="checkbox"]').should('not.be.checked');
});
cy.contains('label', 'Include All option').within(() => {
cy.get('input[type="checkbox"]').should('not.be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.have.text');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist');
});
it('adding a single value query variable', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`)
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2()
.should('be.visible')
.clear()
.type('a label');
cy.get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description');
e2e.components.DataSourcePicker.container().should('be.visible').type('gdev-testdata{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
.should('be.visible')
.type('*')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2()
.should('be.visible')
.type('/.*C.*/')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('have.length', 4)
.eq(3)
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().should('have.length', 1);
e2e.components.Select.option().contains('C');
});
it('adding a multi value query variable', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`)
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2()
.should('be.visible')
.clear()
.type('a label');
cy.get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description');
e2e.components.DataSourcePicker.container().type('gdev-testdata{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
.should('be.visible')
.type('*')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2()
.should('be.visible')
.type('/.*C.*/')
.blur();
cy.contains('label', 'Multi-value').within(() => {
cy.get('input[type="checkbox"]').click({ force: true }).should('be.checked');
});
cy.contains('label', 'Include All option').within(() => {
cy.get('input[type="checkbox"]').click({ force: true }).should('be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().within((input) => {
expect(input.attr('placeholder')).equals('blank = auto');
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('have.length', 4)
.eq(3)
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().should('have.length', 1);
e2e.components.Select.option().contains('All');
});
});

View File

@ -0,0 +1,34 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
const DASHBOARD_NAME = 'Test variable output';
describe('Variables - Text box', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('can add a new text box variable', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` });
cy.contains(DASHBOARD_NAME).should('be.visible');
// Create a new "text box" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
cy.get('input').type('Textbox{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().type('cat-dog').blur();
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
e2e.pages.Dashboard.SubMenu.submenuItem().within(() => {
cy.get('input').clear().type('dog-cat').blur();
});
// Assert it was rendered
cy.get('.markdown-html').should('include.text', 'VariableUnderTest: dog-cat');
});
});

View File

@ -0,0 +1,190 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
describe('Variables - Set options from ui', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('clicking a value that is not part of dependents options should change these to All', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` });
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click().click();
e2e.components.Select.option().contains('B').click();
cy.get('body').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')
.should('have.length', 2)
.eq(0)
.should('be.visible')
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().parent().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'BA')
.parent()
.next()
.should('have.text', 'BB')
.next()
.should('have.text', 'BC');
cy.get('body').click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels('pod')
.next()
.within(() => {
cy.get('input').click();
});
// length is 11 because of virtualized select options
e2e.components.Select.option().parent().should('have.length', 11);
e2e.components.Select.option()
.first()
.should('have.text', 'BAA')
.parent()
.next()
.should('have.text', 'BAB')
.next()
.should('have.text', 'BAC')
.next()
.should('have.text', 'BAD')
.next()
.should('have.text', 'BAE')
.next()
.should('have.text', 'BAF');
});
it('adding a value that is not part of dependents options should add the new values dependant options', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` });
cy.intercept({
pathname: '/api/ds/query',
}).as('query');
cy.wait('@query');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().contains('B').click();
cy.get('body').click();
cy.wait('@query');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B').scrollIntoView().should('be.visible');
e2e.components.LoadingIndicator.icon().should('have.length', 0);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')
.should('be.visible')
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().should('have.length', 11);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'AB')
.next()
.should('have.text', 'AC')
.next()
.should('have.text', 'AD');
cy.get('body').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA')
.should('be.visible')
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'AAB')
.next()
.should('have.text', 'AAC');
});
it('removing a value that is part of dependents options should remove the new values dependant options', () => {
e2e.flows.openDashboard({
uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`,
});
cy.intercept({ pathname: '/api/ds/query' }).as('query');
cy.wait('@query');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B')
.should('be.visible')
.children()
.first()
.click();
cy.get('body').click();
cy.wait(300);
cy.wait('@query');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')
.scrollIntoView()
.should('be.visible')
.within(() => {
cy.get('input').click();
});
cy.get('body').click();
e2e.components.LoadingIndicator.icon().should('have.length', 0);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')
.should('be.visible')
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'BA')
.next()
.should('have.text', 'BC');
cy.get('body').click(0, 0);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')
.should('be.visible')
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().should('have.length', 8);
e2e.components.Select.option()
.first()
.should('have.text', 'All')
.parent()
.next()
.should('have.text', 'BBA')
.next()
.should('have.text', 'BBC');
});
});

View File

@ -0,0 +1,64 @@
import { e2e } from '../utils';
describe('Templating', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('Tests dashboard links and variables in links', () => {
cy.intercept({
method: 'GET',
url: '/api/search?tag=templating&limit=100',
}).as('tagsTemplatingSearch');
cy.intercept({
method: 'GET',
url: '/api/search?tag=demo&limit=100',
}).as('tagsDemoSearch');
e2e.flows.openDashboard({ uid: 'yBCC3aKGk' });
// waiting for network requests first
cy.wait(['@tagsTemplatingSearch', '@tagsDemoSearch']);
const verifyLinks = (variableValue: string) => {
e2e.components.DashboardLinks.link()
.should('be.visible')
.should((links) => {
expect(links).to.have.length.greaterThan(13);
for (let index = 0; index < links.length; index++) {
expect(Cypress.$(links[index]).attr('href')).contains(variableValue);
}
});
};
e2e.components.DashboardLinks.dropDown().should('be.visible').click().wait('@tagsTemplatingSearch');
// verify all links, should have All value
verifyLinks('var-custom=p1&var-custom=p2&var-custom=p3');
cy.get('body').click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels('custom')
.next()
.should('have.text', 'All')
.parent()
.within(() => {
cy.get('input').click();
});
e2e.components.Select.option().contains('p2').click();
cy.get('body').click();
e2e.components.NavToolbar.container().click();
e2e.components.DashboardLinks.dropDown()
.scrollIntoView()
.should('be.visible')
.click()
.wait('@tagsTemplatingSearch');
// verify all links, should have p2 value
verifyLinks('p2');
});
});

View File

@ -0,0 +1,237 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'AejrN1AMz';
describe('TextBox - load options scenarios', function () {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
// TODO: remove skip after https://github.com/grafana/grafana/issues/86435
it.skip('default options should be correct', function () {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}/templating-textbox-e2e-scenarios?orgId=1` });
validateTextboxAndMarkup('default value');
});
it('loading variable from url should be correct', function () {
e2e.flows.openDashboard({
uid: `${PAGE_UNDER_TEST}/templating-textbox-e2e-scenarios?orgId=1&var-text=not default value`,
});
validateTextboxAndMarkup('not default value');
});
});
describe.skip('TextBox - change query scenarios', function () {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('when changing the query value and not saving current as default should revert query value', function () {
copyExistingDashboard();
changeQueryInput();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
validateTextboxAndMarkup('changed value');
saveDashboard(false);
cy.get<string>('@dashuid').then((dashuid) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
cy.wait('@load-dash');
validateTextboxAndMarkup('default value');
validateVariable('changed value');
});
});
it('when changing the query value and saving current as default should change query value', function () {
copyExistingDashboard();
changeQueryInput();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
validateTextboxAndMarkup('changed value');
saveDashboard(true);
cy.get<string>('@dashuid').then((dashuid) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
cy.wait('@load-dash');
validateTextboxAndMarkup('changed value');
validateVariable('changed value');
});
});
});
describe.skip('TextBox - change picker value scenarios', function () {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('when changing the input value and not saving current as default should revert query value', function () {
copyExistingDashboard();
changeTextBoxInput();
validateTextboxAndMarkup('changed value');
saveDashboard(false);
cy.get<string>('@dashuid').then((dashuid) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
cy.wait('@load-dash');
validateTextboxAndMarkup('default value');
validateVariable('default value');
});
});
it('when changing the input value and saving current as default should change query value', function () {
copyExistingDashboard();
changeTextBoxInput();
validateTextboxAndMarkup('changed value');
saveDashboard(true);
cy.get<string>('@dashuid').then((dashuid) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
cy.wait('@load-dash');
validateTextboxAndMarkup('changed value');
validateVariable('changed value');
});
});
});
function copyExistingDashboard() {
cy.intercept({
method: 'GET',
url: '/api/search?query=&type=dash-folder&permission=Edit',
}).as('dash-settings');
cy.intercept({
method: 'POST',
url: '/api/dashboards/db/',
}).as('save-dash');
cy.intercept({
method: 'GET',
url: /\/api\/dashboards\/uid\/(?!AejrN1AMz)\w+/,
}).as('load-dash');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}/templating-textbox-e2e-scenarios?orgId=1&editview=settings` });
cy.wait('@dash-settings');
e2e.pages.Dashboard.Settings.General.saveAsDashBoard().should('be.visible').click();
e2e.pages.SaveDashboardAsModal.newName().should('be.visible').type(`${Date.now()}`);
e2e.pages.SaveDashboardAsModal.save().should('be.visible').click();
cy.wait('@save-dash');
cy.wait('@load-dash');
e2e.pages.Dashboard.SubMenu.submenuItem().should('be.visible');
cy.location().then((loc) => {
const dashuid = /\/d\/(\w+)\//.exec(loc.href)![1];
cy.wrap(dashuid).as('dashuid');
});
cy.wait(500);
}
function saveDashboard(saveVariables: boolean) {
e2e.components.PageToolbar.item('Save dashboard').should('be.visible').click();
if (saveVariables) {
e2e.pages.SaveDashboardModal.saveVariables().should('exist').click({ force: true });
}
e2e.pages.SaveDashboardModal.save().should('be.visible').click();
cy.wait('@save-dash');
}
function validateTextboxAndMarkup(value: string) {
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('be.visible')
.within(() => {
e2e.pages.Dashboard.SubMenu.submenuItemLabels('text').should('be.visible');
cy.get('input').should('be.visible').should('have.value', value);
});
e2e.components.Panels.Visualization.Text.container()
.should('be.visible')
.within(() => {
cy.get('h1').should('be.visible').should('have.text', `variable: ${value}`);
});
}
function validateVariable(value: string) {
e2e.components.PageToolbar.item('Dashboard settings').should('be.visible').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text').should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2()
.should('be.visible')
.should('have.value', value);
}
function changeTextBoxInput() {
e2e.pages.Dashboard.SubMenu.submenuItemLabels('text').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('be.visible')
.within(() => {
cy.get('input')
.should('be.visible')
.should('have.value', 'default value')
.clear()
.type('changed value')
.type('{enter}');
});
cy.location().should((loc) => {
expect(loc.search).to.contain('var-text=changed%20value');
});
}
function changeQueryInput() {
e2e.components.PageToolbar.item('Dashboard settings').should('be.visible').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text').should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2()
.should('be.visible')
.clear()
.type('changed value')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
.should('have.length', 1)
.should('have.text', 'changed 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,
weekStart: '',
uid: '',
},
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,209 @@
{
"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,
"id": 321,
"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 - Dashboard Search",
"uid": "kquZN5H7k",
"version": 4
}

View File

@ -0,0 +1,58 @@
{
"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,
"id": 118,
"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,209 @@
{
"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,
"id": 321,
"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,40 @@
import { e2e } from '../utils';
export const smokeTestScenario = () =>
describe('Smoke tests', () => {
before(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false);
cy.logToConsole('enabling dashboardScene feature toggle in localstorage');
cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=true');
cy.reload();
e2e.flows.addDataSource();
e2e.flows.addDashboard();
e2e.flows.addPanel({
dataSourceName: 'gdev-testdata',
visitDashboardAtStart: false,
timeout: 10000,
});
});
after(() => {
e2e.flows.revertAllChanges();
});
it('Login scenario, create test data source, dashboard, panel, and export scenario', () => {
e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer()
.should('be.visible')
.within(() => {
cy.get('input[id*="test-data-scenario-select-"]').should('be.visible').click();
});
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
// Make sure the graph renders via checking legend
e2e.components.VizLegend.seriesName('A-series').should('be.visible');
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
// Make sure panel is & visualization is added to dashboard
e2e.components.VizLegend.seriesName('A-series').should('be.visible');
});
});

View File

@ -0,0 +1,3 @@
import { smokeTestScenario } from '../shared/smokeTestScenario';
smokeTestScenario();

View File

@ -0,0 +1,38 @@
import { GrafanaBootConfig } from '@grafana/runtime';
import { e2e } from '../utils';
describe('Panels smokescreen', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false);
});
after(() => {
e2e.flows.revertAllChanges();
});
it('Tests each panel type in the panel edit view to ensure no crash', () => {
e2e.flows.addDashboard();
e2e.flows.addPanel({
dataSourceName: 'gdev-testdata',
timeout: 10000,
visitDashboardAtStart: false,
});
cy.window().then((win: Cypress.AUTWindow & { grafanaBootData: GrafanaBootConfig['bootData'] }) => {
// Loop through every panel type and ensure no crash
Object.entries(win.grafanaBootData.settings.panels).forEach(([_, panel]) => {
// TODO: Remove Flame Graph check as part of addressing #66803
if (!panel.hideFromList && panel.state !== 'deprecated') {
e2e.components.PanelEditor.toggleVizPicker().click();
e2e.components.PluginVisualization.item(panel.name).scrollIntoView().should('be.visible').click();
e2e.components.PanelEditor.toggleVizPicker().should((e) => expect(e).to.contain(panel.name));
// TODO: Come up with better check / better failure messaging to clearly indicate which panel failed
cy.contains('An unexpected error happened').should('not.exist');
}
});
});
});
});

View File

@ -0,0 +1,302 @@
import { v4 as uuidv4 } from 'uuid';
import { e2e } from '../index';
import { getDashboardUid } from '../support/url';
import { selectOption } from './selectOption';
import { setDashboardTimeRange, TimeRangeConfig } from './setDashboardTimeRange';
export interface AddAnnotationConfig {
dataSource: string;
dataSourceForm?: () => void;
name: string;
}
export interface AddDashboardConfig {
annotations: AddAnnotationConfig[];
timeRange: TimeRangeConfig;
title: string;
variables: PartialAddVariableConfig[];
}
interface AddVariableDefault {
hide: string;
type: string;
}
interface AddVariableOptional {
constantValue?: string;
dataSource?: string;
label?: string;
query?: string;
regex?: string;
variableQueryForm?: (config: AddVariableConfig) => void;
}
interface AddVariableRequired {
name: string;
}
export type PartialAddVariableConfig = Partial<AddVariableDefault> & AddVariableOptional & AddVariableRequired;
export type AddVariableConfig = AddVariableDefault & AddVariableOptional & AddVariableRequired;
/**
* This flow is used to add a dashboard with whatever configuration specified.
* @param config Configuration object. Currently supports configuring dashboard time range, annotations, and variables (support dependant on type).
* @see{@link AddDashboardConfig}
*
* @example
* ```
* // Configuring a simple dashboard
* addDashboard({
* timeRange: {
* from: '2022-10-03 00:00:00',
* to: '2022-10-03 23:59:59',
* zone: 'Coordinated Universal Time',
* },
* title: 'Test Dashboard',
* })
* ```
*
* @example
* ```
* // Configuring a dashboard with annotations
* addDashboard({
* title: 'Test Dashboard',
* annotations: [
* {
* // This should match the datasource name
* dataSource: 'azure-monitor',
* name: 'Test Annotation',
* dataSourceForm: () => {
* // Insert steps to create annotation using datasource form
* }
* }
* ]
* })
* ```
*
* @see{@link AddAnnotationConfig}
*
* @example
* ```
* // Configuring a dashboard with variables
* addDashboard({
* title: 'Test Dashboard',
* variables: [
* {
* name: 'test-query-variable',
* label: 'Testing Query',
* hide: '',
* type: e2e.flows.VARIABLE_TYPE_QUERY,
* dataSource: 'azure-monitor',
* variableQueryForm: () => {
* // Insert steps to create variable using datasource form
* },
* },
* {
* name: 'test-constant-variable',
* label: 'Testing Constant',
* type: e2e.flows.VARIABLE_TYPE_CONSTANT,
* constantValue: 'constant',
* }
* ]
* })
* ```
*
* @see{@link AddVariableConfig}
*
* @see{@link https://github.com/grafana/grafana/blob/main/e2e/cloud-plugins-suite/azure-monitor.spec.ts Azure Monitor Tests for full examples}
*/
export const addDashboard = (config?: Partial<AddDashboardConfig>) => {
const fullConfig: AddDashboardConfig = {
annotations: [],
title: `e2e-${uuidv4()}`,
variables: [],
...config,
timeRange: {
from: '2020-01-01 00:00:00',
to: '2020-01-01 06:00:00',
zone: 'Coordinated Universal Time',
...config?.timeRange,
},
};
const { annotations, timeRange, title, variables } = fullConfig;
cy.logToConsole('Adding dashboard with title:', title);
e2e.pages.AddDashboard.visit();
if (annotations.length > 0 || variables.length > 0) {
e2e.components.PageToolbar.item('Dashboard settings').click();
addAnnotations(annotations);
fullConfig.variables = addVariables(variables);
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
}
setDashboardTimeRange(timeRange);
e2e.components.NavToolbar.editDashboard.saveButton().click();
e2e.components.Drawer.DashboardSaveDrawer.saveAsTitleInput().clear().type(title, { force: true });
e2e.components.Drawer.DashboardSaveDrawer.saveButton().click();
e2e.flows.assertSuccessNotification();
e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible');
cy.logToConsole('Added dashboard with title:', title);
return cy
.url()
.should('contain', '/d/')
.then((url: string) => {
const uid = getDashboardUid(url);
e2e.getScenarioContext().then(({ addedDashboards }) => {
e2e.setScenarioContext({
addedDashboards: [...addedDashboards, { title, uid }],
});
});
// @todo remove `wrap` when possible
return cy.wrap(
{
config: fullConfig,
uid,
},
{ log: false }
);
});
};
const addAnnotation = (config: AddAnnotationConfig, isFirst: boolean) => {
if (isFirst) {
if (e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2) {
e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2().click();
} else {
e2e.pages.Dashboard.Settings.Annotations.List.addAnnotationCTA().click();
}
} else {
cy.contains('New query').click();
}
const { dataSource, dataSourceForm, name } = config;
selectOption({
container: e2e.components.DataSourcePicker.container(),
optionText: dataSource,
});
e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type(name);
if (dataSourceForm) {
dataSourceForm();
}
};
const addAnnotations = (configs: AddAnnotationConfig[]) => {
if (configs.length > 0) {
e2e.pages.Dashboard.Settings.General.sectionItems('Annotations').click();
}
return configs.forEach((config, i) => addAnnotation(config, i === 0));
};
export const VARIABLE_HIDE_LABEL = 'Label';
export const VARIABLE_HIDE_NOTHING = '';
export const VARIABLE_HIDE_VARIABLE = 'Variable';
export const VARIABLE_TYPE_AD_HOC_FILTERS = 'Ad hoc filters';
export const VARIABLE_TYPE_CONSTANT = 'Constant';
export const VARIABLE_TYPE_DATASOURCE = 'Datasource';
export const VARIABLE_TYPE_QUERY = 'Query';
const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVariableConfig => {
const fullConfig = {
hide: VARIABLE_HIDE_NOTHING,
type: VARIABLE_TYPE_QUERY,
...config,
};
if (isFirst) {
if (e2e.pages.Dashboard.Settings.Variables.List.addVariableCTAV2) {
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTAV2().click();
} else {
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
}
} else {
e2e.pages.Dashboard.Settings.Variables.List.newButton().click();
}
const { constantValue, dataSource, label, name, query, regex, type, variableQueryForm } = fullConfig;
// This field is key to many reactive changes
if (type !== VARIABLE_TYPE_QUERY) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2()
.should('be.visible')
.within(() => {
e2e.components.Select.singleValue().should('have.text', 'Query').parent().click();
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().find('input').type(`${type}{enter}`);
}
if (label) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label);
}
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name);
if (
dataSource &&
(type === VARIABLE_TYPE_AD_HOC_FILTERS || type === VARIABLE_TYPE_DATASOURCE || type === VARIABLE_TYPE_QUERY)
) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.should('be.visible')
.within(() => {
e2e.components.DataSourcePicker.inputV2().type(`${dataSource}{enter}`);
});
}
if (constantValue && type === VARIABLE_TYPE_CONSTANT) {
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type(constantValue);
}
if (type === VARIABLE_TYPE_QUERY) {
if (query) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().type(query);
}
if (regex) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2().type(regex);
}
if (variableQueryForm) {
variableQueryForm(fullConfig);
}
}
// Avoid flakiness
cy.focused().blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
.should('exist')
.within((previewOfValues) => {
if (type === VARIABLE_TYPE_CONSTANT) {
expect(previewOfValues.text()).equals(constantValue);
}
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click();
return fullConfig;
};
const addVariables = (configs: PartialAddVariableConfig[]): AddVariableConfig[] => {
if (configs.length > 0) {
e2e.components.Tab.title('Variables').click();
}
return configs.map((config, i) => addVariable(config, i === 0));
};

View File

@ -0,0 +1,109 @@
import { v4 as uuidv4 } from 'uuid';
import { e2e } from '../index';
export interface AddDataSourceConfig {
basicAuth: boolean;
basicAuthPassword: string;
basicAuthUser: string;
expectedAlertMessage: string | RegExp;
form: () => void;
name: string;
skipTlsVerify: boolean;
type: string;
timeout?: number;
awaitHealth?: boolean;
}
// @todo this actually returns type `Cypress.Chainable<AddDaaSourceConfig>`
export const addDataSource = (config?: Partial<AddDataSourceConfig>) => {
const fullConfig: AddDataSourceConfig = {
basicAuth: false,
basicAuthPassword: '',
basicAuthUser: '',
expectedAlertMessage: 'Data source is working',
form: () => {},
name: `e2e-${uuidv4()}`,
skipTlsVerify: false,
type: 'TestData',
...config,
};
const {
basicAuth,
basicAuthPassword,
basicAuthUser,
expectedAlertMessage,
form,
name,
skipTlsVerify,
type,
timeout,
awaitHealth,
} = fullConfig;
if (awaitHealth) {
cy.intercept(/health/).as('health');
}
cy.logToConsole('Adding data source with name:', name);
e2e.pages.AddDataSource.visit();
e2e.pages.AddDataSource.dataSourcePluginsV2(type)
.scrollIntoView()
.should('be.visible') // prevents flakiness
.click();
e2e.pages.DataSource.name().clear();
e2e.pages.DataSource.name().type(name);
if (basicAuth) {
cy.contains('label', 'Basic auth').scrollIntoView().click();
cy.contains('.gf-form-group', 'Basic Auth Details')
.should('be.visible')
.scrollIntoView()
.within(() => {
if (basicAuthUser) {
cy.get('[placeholder=user]').type(basicAuthUser);
}
if (basicAuthPassword) {
cy.get('[placeholder=Password]').type(basicAuthPassword);
}
});
}
if (skipTlsVerify) {
cy.contains('label', 'Skip TLS Verify').scrollIntoView().click();
}
form();
e2e.pages.DataSource.saveAndTest().click();
if (awaitHealth) {
cy.wait('@health', { timeout: timeout ?? Cypress.config().defaultCommandTimeout });
}
// use the timeout passed in if it exists, otherwise, continue to use the default
e2e.pages.DataSource.alert()
.should('exist')
.contains(expectedAlertMessage, {
timeout: timeout ?? Cypress.config().defaultCommandTimeout,
});
cy.logToConsole('Added data source with name:', name);
return cy.url().then(() => {
e2e.getScenarioContext().then(({ addedDataSources }) => {
e2e.setScenarioContext({
addedDataSources: [...addedDataSources, { name, id: '' }],
});
});
// @todo remove `wrap` when possible
return cy.wrap(
{
config: fullConfig,
},
{ log: false }
);
});
};

View File

@ -0,0 +1,15 @@
import { v4 as uuidv4 } from 'uuid';
import { getScenarioContext } from '../support/scenarioContext';
import { configurePanel, PartialAddPanelConfig } from './configurePanel';
export const addPanel = (config?: Partial<PartialAddPanelConfig>) =>
getScenarioContext().then(({ lastAddedDataSource }) =>
configurePanel({
dataSourceName: lastAddedDataSource,
panelTitle: `e2e-${uuidv4()}`,
...config,
isEdit: false,
})
);

View File

@ -0,0 +1,9 @@
import { e2e } from '../index';
export const assertSuccessNotification = () => {
if (e2e.components.Alert.alertV2) {
e2e.components.Alert.alertV2('success').should('exist');
} else {
e2e.components.Alert.alert('success').should('exist');
}
};

View File

@ -0,0 +1,177 @@
import { e2e } from '..';
import { getScenarioContext } from '../support/scenarioContext';
import { setDashboardTimeRange } from './setDashboardTimeRange';
import { TimeRangeConfig } from './setTimeRange';
interface AddPanelOverrides {
dataSourceName: string;
queriesForm: (config: AddPanelConfig) => void;
panelTitle: string;
}
interface EditPanelOverrides {
queriesForm?: (config: EditPanelConfig) => void;
panelTitle: string;
}
interface ConfigurePanelDefault {
chartData: {
method: string;
route: string | RegExp;
};
dashboardUid: string;
saveDashboard: boolean;
visitDashboardAtStart: boolean; // @todo remove when possible
}
interface ConfigurePanelOptional {
dataSourceName?: string;
queriesForm?: (config: ConfigurePanelConfig) => void;
panelTitle?: string;
timeRange?: TimeRangeConfig;
visualizationName?: string;
timeout?: number;
}
interface ConfigurePanelRequired {
isEdit: boolean;
}
export type PartialConfigurePanelConfig = Partial<ConfigurePanelDefault> &
ConfigurePanelOptional &
ConfigurePanelRequired;
export type ConfigurePanelConfig = ConfigurePanelDefault & ConfigurePanelOptional & ConfigurePanelRequired;
export type PartialAddPanelConfig = PartialConfigurePanelConfig & AddPanelOverrides;
export type AddPanelConfig = ConfigurePanelConfig & AddPanelOverrides;
export type PartialEditPanelConfig = PartialConfigurePanelConfig & EditPanelOverrides;
export type EditPanelConfig = ConfigurePanelConfig & EditPanelOverrides;
export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelConfig | PartialConfigurePanelConfig) =>
getScenarioContext().then(({ lastAddedDashboardUid }) => {
const fullConfig: AddPanelConfig | EditPanelConfig | ConfigurePanelConfig = {
chartData: {
method: 'POST',
route: '/api/ds/query',
},
dashboardUid: lastAddedDashboardUid,
saveDashboard: true,
visitDashboardAtStart: true,
...config,
};
const {
chartData,
dashboardUid,
dataSourceName,
isEdit,
panelTitle,
queriesForm,
timeRange,
visitDashboardAtStart,
visualizationName,
timeout,
} = fullConfig;
if (visitDashboardAtStart) {
e2e.flows.openDashboard({ uid: dashboardUid });
}
if (isEdit) {
e2e.components.Panels.Panel.title(panelTitle).click();
e2e.components.Panels.Panel.headerItems('Edit').click();
} else {
try {
//Enter edit mode
e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click();
e2e.components.PageToolbar.itemButton('Add button').should('be.visible').click();
e2e.components.NavToolbar.editDashboard.addVisualizationButton().should('be.visible').click();
} catch (e) {
// Depending on the screen size, the "Add" button might be hidden
e2e.components.PageToolbar.item('Show more items').click();
e2e.components.PageToolbar.item('Add button').last().click();
}
// e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible');
// e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click();
}
if (timeRange) {
setDashboardTimeRange(timeRange);
}
// @todo alias '/**/*.js*' as '@pluginModule' when possible: https://github.com/cypress-io/cypress/issues/1296
cy.intercept(chartData.method, chartData.route).as('chartData');
if (dataSourceName) {
e2e.components.DataSourcePicker.container().click().type(`${dataSourceName}{downArrow}{enter}`);
}
// @todo instead wait for '@pluginModule' if not already loaded
cy.wait(2000);
// `panelTitle` is needed to edit the panel, and unlikely to have its value changed at that point
const changeTitle = panelTitle && !isEdit;
if (changeTitle || visualizationName) {
if (changeTitle && panelTitle) {
e2e.components.PanelEditor.OptionsPane.fieldLabel('Panel options Title').type(`{selectall}${panelTitle}`);
}
if (visualizationName) {
e2e.components.PluginVisualization.item(visualizationName).scrollIntoView().click();
// @todo wait for '@pluginModule' if not a core visualization and not already loaded
cy.wait(2000);
}
} else {
// Consistently closed
closeOptions();
}
if (queriesForm) {
queriesForm(fullConfig);
// Wait for a possible complex visualization to render (or something related, as this isn't necessary on the dashboard page)
// Can't assert that its HTML changed because a new query could produce the same results
cy.wait(1000);
}
// @todo enable when plugins have this implemented
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
//cy.wait('@chartData');
//e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content').contains('No data');
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
//cy.wait('@chartData');
// Avoid annotations flakiness
e2e.components.RefreshPicker.runButtonV2().first().click({ force: true });
// Wait for RxJS
cy.wait(timeout ?? Cypress.config().defaultCommandTimeout);
// @todo remove `wrap` when possible
return cy.wrap({ config: fullConfig }, { log: false });
});
const closeOptions = () => e2e.components.PanelEditor.toggleVizOptions().click();
export const VISUALIZATION_ALERT_LIST = 'Alert list';
export const VISUALIZATION_BAR_GAUGE = 'Bar gauge';
export const VISUALIZATION_CLOCK = 'Clock';
export const VISUALIZATION_DASHBOARD_LIST = 'Dashboard list';
export const VISUALIZATION_GAUGE = 'Gauge';
export const VISUALIZATION_GRAPH = 'Graph';
export const VISUALIZATION_HEAT_MAP = 'Heatmap';
export const VISUALIZATION_LOGS = 'Logs';
export const VISUALIZATION_NEWS = 'News';
export const VISUALIZATION_PIE_CHART = 'Pie Chart';
export const VISUALIZATION_PLUGIN_LIST = 'Plugin list';
export const VISUALIZATION_POLYSTAT = 'Polystat';
export const VISUALIZATION_STAT = 'Stat';
export const VISUALIZATION_TABLE = 'Table';
export const VISUALIZATION_TEXT = 'Text';
export const VISUALIZATION_WORLD_MAP = 'Worldmap Panel';

View File

@ -0,0 +1,6 @@
import { e2e } from '..';
export function confirmDelete() {
cy.get(`input[placeholder='Type "Delete" to confirm']`).type('Delete');
e2e.pages.ConfirmModal.delete().click();
}

View File

@ -0,0 +1,49 @@
import { e2e } from '../index';
import { fromBaseUrl } from '../support/url';
export interface DeleteDashboardConfig {
quick?: boolean;
title: string;
uid: string;
}
export const deleteDashboard = ({ quick = false, title, uid }: DeleteDashboardConfig) => {
cy.logToConsole('Deleting dashboard with uid:', uid);
if (quick) {
quickDelete(uid);
} else {
uiDelete(uid, title);
}
cy.logToConsole('Deleted dashboard with uid:', uid);
e2e.getScenarioContext().then(({ addedDashboards }) => {
e2e.setScenarioContext({
addedDashboards: addedDashboards.filter((dashboard: DeleteDashboardConfig) => {
return dashboard.title !== title && dashboard.uid !== uid;
}),
});
});
};
const quickDelete = (uid: string) => {
cy.request('DELETE', fromBaseUrl(`/api/dashboards/uid/${uid}`));
};
const uiDelete = (uid: string, title: string) => {
e2e.pages.Dashboard.visit(uid);
e2e.components.PageToolbar.item('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.deleteDashBoard().click();
e2e.pages.ConfirmModal.delete().click();
e2e.flows.assertSuccessNotification();
e2e.pages.Dashboards.visit();
// @todo replace `e2e.pages.Dashboards.dashboards` with this when argument is empty
if (e2e.components.Search.dashboardItems) {
e2e.components.Search.dashboardItems().each((item) => cy.wrap(item).should('not.contain', title));
} else {
cy.get('[aria-label^="Dashboard search item "]').each((item) => cy.wrap(item).should('not.contain', title));
}
};

View File

@ -0,0 +1,44 @@
import { e2e } from '../index';
import { fromBaseUrl } from '../support/url';
export interface DeleteDataSourceConfig {
id: string;
name: string;
quick?: boolean;
}
export const deleteDataSource = ({ id, name, quick = false }: DeleteDataSourceConfig) => {
cy.logToConsole('Deleting data source with name:', name);
if (quick) {
quickDelete(name);
} else {
uiDelete(name);
}
cy.logToConsole('Deleted data source with name:', name);
e2e.getScenarioContext().then(({ addedDataSources }) => {
e2e.setScenarioContext({
addedDataSources: addedDataSources.filter((dataSource: DeleteDataSourceConfig) => {
return dataSource.id !== id && dataSource.name !== name;
}),
});
});
};
const quickDelete = (name: string) => {
cy.request('DELETE', fromBaseUrl(`/api/datasources/name/${name}`));
};
const uiDelete = (name: string) => {
e2e.pages.DataSources.visit();
e2e.pages.DataSources.dataSources(name).click();
e2e.pages.DataSource.delete().click();
e2e.pages.ConfirmModal.delete().click();
e2e.pages.DataSources.visit();
// @todo replace `e2e.pages.DataSources.dataSources` with this when argument is empty
cy.get('[aria-label^="Data source list item "]').each((item) => cy.wrap(item).should('not.contain', name));
};

View File

@ -0,0 +1,7 @@
import { configurePanel, PartialEditPanelConfig } from './configurePanel';
export const editPanel = (config: Partial<PartialEditPanelConfig>) =>
configurePanel({
...config,
isEdit: true,
});

View File

@ -0,0 +1,69 @@
import { e2e } from '../index';
import { fromBaseUrl, getDashboardUid } from '../support/url';
import { DeleteDashboardConfig } from '.';
type Panel = {
title: string;
[key: string]: unknown;
};
export type Dashboard = { title: string; panels: Panel[]; uid: string; [key: string]: unknown };
/**
* Smoke test a particular dashboard by quickly importing a json file and validate that all the panels finish loading
* @param dashboardToImport a sample dashboard
* @param queryTimeout a number of ms to wait for the imported dashboard to finish loading
* @param skipPanelValidation skip panel validation
*/
export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: number, skipPanelValidation?: boolean) => {
cy.visit(fromBaseUrl('/dashboard/import'));
// Note: normally we'd use 'click' and then 'type' here, but the json object is so big that using 'val' is much faster
e2e.components.DashboardImportPage.textarea().should('be.visible');
e2e.components.DashboardImportPage.textarea().click();
e2e.components.DashboardImportPage.textarea().invoke('val', JSON.stringify(dashboardToImport));
e2e.components.DashboardImportPage.submit().should('be.visible').click();
e2e.components.ImportDashboardForm.name().should('be.visible').click().clear().type(dashboardToImport.title);
e2e.components.ImportDashboardForm.submit().should('be.visible').click();
// wait for dashboard to load
cy.wait(queryTimeout || 6000);
// save the newly imported dashboard to context so it'll get properly deleted later
cy.url()
.should('contain', '/d/')
.then((url: string) => {
const uid = getDashboardUid(url);
e2e.getScenarioContext().then(({ addedDashboards }: { addedDashboards: DeleteDashboardConfig[] }) => {
e2e.setScenarioContext({
addedDashboards: [...addedDashboards, { title: dashboardToImport.title, uid }],
});
});
expect(dashboardToImport.uid).to.equal(uid);
});
if (!skipPanelValidation) {
dashboardToImport.panels.forEach((panel) => {
// Look at the json data
e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover
e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click();
e2e.components.Tab.title('JSON').should('be.visible').click();
e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true });
e2e.components.Select.option().should('be.visible').contains('Panel data').click();
// ensures that panel has loaded without knowingly hitting an error
// note: this does not prove that data came back as we expected it,
// it could get `state: Done` for no data for example
// but it ensures we didn't hit a 401 or 500 or something like that
e2e.components.CodeEditor.container()
.should('be.visible')
.contains(/"state": "(Done|Streaming)"/);
// need to close panel
e2e.components.Drawer.General.close().click();
});
}
};

View File

@ -0,0 +1,17 @@
import { importDashboard, Dashboard } from './importDashboard';
/**
* Smoke test several dashboard json files from a test directory
* and validate that all the panels in each import finish loading their queries
* @param dirPath the relative path to a directory which contains json files representing dashboards,
* for example if your dashboards live in `cypress/testDashboards` you can pass `/testDashboards`
* @param queryTimeout a number of ms to wait for the imported dashboard to finish loading
* @param skipPanelValidation skips panel validation
*/
export const importDashboards = async (dirPath: string, queryTimeout?: number, skipPanelValidation?: boolean) => {
cy.getJSONFilesFromDir(dirPath).then((jsonFiles: Dashboard[]) => {
jsonFiles.forEach((file) => {
importDashboard(file, queryTimeout || 6000, skipPanelValidation);
});
});
};

View File

@ -0,0 +1,37 @@
export * from './addDashboard';
export * from './addDataSource';
export * from './addPanel';
export * from './assertSuccessNotification';
export * from './deleteDashboard';
export * from './deleteDataSource';
export * from './editPanel';
export * from './login';
export * from './openDashboard';
export * from './openPanelMenuItem';
export * from './revertAllChanges';
export * from './saveDashboard';
export * from './selectOption';
export * from './setTimeRange';
export * from './importDashboard';
export * from './importDashboards';
export * from './userPreferences';
export * from './confirmModal';
export {
VISUALIZATION_ALERT_LIST,
VISUALIZATION_BAR_GAUGE,
VISUALIZATION_CLOCK,
VISUALIZATION_DASHBOARD_LIST,
VISUALIZATION_GAUGE,
VISUALIZATION_GRAPH,
VISUALIZATION_HEAT_MAP,
VISUALIZATION_LOGS,
VISUALIZATION_NEWS,
VISUALIZATION_PIE_CHART,
VISUALIZATION_PLUGIN_LIST,
VISUALIZATION_POLYSTAT,
VISUALIZATION_STAT,
VISUALIZATION_TABLE,
VISUALIZATION_TEXT,
VISUALIZATION_WORLD_MAP,
} from './configurePanel';

View File

@ -0,0 +1,42 @@
import { e2e } from '../index';
import { fromBaseUrl } from '../support/url';
const DEFAULT_USERNAME = 'admin';
const DEFAULT_PASSWORD = 'admin';
const loginApi = (username: string, password: string) => {
cy.request({
method: 'POST',
url: fromBaseUrl('/login'),
body: {
user: username,
password,
},
});
};
const loginUi = (username: string, password: string) => {
cy.logToConsole('Logging in with username:', username);
e2e.pages.Login.visit();
e2e.pages.Login.username()
.should('be.visible') // prevents flakiness
.type(username);
e2e.pages.Login.password().type(password);
e2e.pages.Login.submit().click();
// Local tests will have insecure credentials
if (password === DEFAULT_PASSWORD) {
e2e.pages.Login.skip().should('be.visible').click();
}
cy.get('.login-page').should('not.exist');
};
export const login = (username = DEFAULT_USERNAME, password = DEFAULT_PASSWORD, loginViaApi = true) => {
if (loginViaApi) {
loginApi(username, password);
} else {
loginUi(username, password);
}
cy.logToConsole('Logged in with username:', username);
};

View File

@ -0,0 +1,35 @@
import { e2e } from '../index';
import { getScenarioContext } from '../support/scenarioContext';
import { setDashboardTimeRange, TimeRangeConfig } from './setDashboardTimeRange';
interface OpenDashboardDefault {
uid: string;
}
interface OpenDashboardOptional {
timeRange?: TimeRangeConfig;
queryParams?: object;
}
export type PartialOpenDashboardConfig = Partial<OpenDashboardDefault> & OpenDashboardOptional;
export type OpenDashboardConfig = OpenDashboardDefault & OpenDashboardOptional;
export const openDashboard = (config?: PartialOpenDashboardConfig) =>
getScenarioContext().then(({ lastAddedDashboardUid }) => {
const fullConfig: OpenDashboardConfig = {
uid: lastAddedDashboardUid,
...config,
};
const { timeRange, uid, queryParams } = fullConfig;
e2e.pages.Dashboard.visit(uid, queryParams);
if (timeRange) {
setDashboardTimeRange(timeRange);
}
// @todo remove `wrap` when possible
return cy.wrap({ config: fullConfig }, { log: false });
});

View File

@ -0,0 +1,57 @@
import { e2e } from '../index';
export enum PanelMenuItems {
Edit = 'Edit',
Inspect = 'Inspect',
More = 'More...',
Extensions = 'Extensions',
}
export const openPanelMenuItem = (menu: PanelMenuItems, panelTitle = 'Panel Title') => {
// we changed the way we open the panel menu in react panels with the new panel header
detectPanelType(panelTitle, (isAngularPanel) => {
if (isAngularPanel) {
e2e.components.Panels.Panel.title(panelTitle).should('be.visible').click();
e2e.components.Panels.Panel.headerItems(menu).should('be.visible').click();
} else {
e2e.components.Panels.Panel.menu(panelTitle).click({ force: true }); // force click because menu is hidden and show on hover
e2e.components.Panels.Panel.menuItems(menu).should('be.visible').click();
}
});
};
export const openPanelMenuExtension = (extensionTitle: string, panelTitle = 'Panel Title') => {
const menuItem = PanelMenuItems.Extensions;
// we changed the way we open the panel menu in react panels with the new panel header
detectPanelType(panelTitle, (isAngularPanel) => {
if (isAngularPanel) {
e2e.components.Panels.Panel.title(panelTitle).should('be.visible').click();
e2e.components.Panels.Panel.headerItems(menuItem)
.should('be.visible')
.parent()
.parent()
.invoke('addClass', 'open');
e2e.components.Panels.Panel.headerItems(extensionTitle).should('be.visible').click();
} else {
e2e.components.Panels.Panel.menu(panelTitle).click({ force: true }); // force click because menu is hidden and show on hover
e2e.components.Panels.Panel.menuItems(menuItem).trigger('mouseover', { force: true });
e2e.components.Panels.Panel.menuItems(extensionTitle).click({ force: true });
}
});
};
function detectPanelType(panelTitle: string, detected: (isAngularPanel: boolean) => void) {
e2e.components.Panels.Panel.title(panelTitle).then((el) => {
const isAngularPanel = el.find('plugin-component.ng-scope').length > 0;
if (isAngularPanel) {
Cypress.log({
name: 'detectPanelType',
displayName: 'detector',
message: 'Angular panel detected, will use legacy selectors.',
});
}
detected(isAngularPanel);
});
}

View File

@ -0,0 +1,12 @@
import { e2e } from '../index';
export const revertAllChanges = () => {
e2e.getScenarioContext().then(({ addedDashboards, addedDataSources, hasChangedUserPreferences }) => {
addedDashboards.forEach((dashboard) => e2e.flows.deleteDashboard({ ...dashboard, quick: true }));
addedDataSources.forEach((dataSource) => e2e.flows.deleteDataSource({ ...dataSource, quick: true }));
if (hasChangedUserPreferences) {
e2e.flows.setDefaultUserPreferences();
}
});
};

View File

@ -0,0 +1,9 @@
import { e2e } from '../index';
export const saveDashboard = () => {
e2e.components.PageToolbar.item('Save dashboard').click();
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
};

View File

@ -0,0 +1,40 @@
import { e2e } from '../index';
export interface SelectOptionConfig {
clickToOpen?: boolean;
container: Cypress.Chainable<JQuery<HTMLElement>>;
forceClickOption?: boolean;
optionText: string | RegExp;
}
export const selectOption = (config: SelectOptionConfig) => {
const fullConfig: SelectOptionConfig = {
clickToOpen: true,
forceClickOption: false,
...config,
};
const { clickToOpen, container, forceClickOption, optionText } = fullConfig;
container.within(() => {
if (clickToOpen) {
cy.get('[class$="-input-suffix"]', { timeout: 1000 }).then((element) => {
expect(Cypress.dom.isAttached(element)).to.eq(true);
cy.get('[class$="-input-suffix"]', { timeout: 1000 }).click({ force: true });
});
}
});
return e2e.components.Select.option()
.filter((_, { textContent }) => {
if (textContent === null) {
return false;
} else if (typeof optionText === 'string') {
return textContent.includes(optionText);
} else {
return optionText.test(textContent);
}
})
.scrollIntoView()
.click({ force: forceClickOption });
};

View File

@ -0,0 +1,5 @@
import { setTimeRange, TimeRangeConfig } from './setTimeRange';
export type { TimeRangeConfig };
export const setDashboardTimeRange = (config: TimeRangeConfig) => setTimeRange(config);

View File

@ -0,0 +1,40 @@
import { e2e } from '../index';
import { selectOption } from './selectOption';
export interface TimeRangeConfig {
from: string;
to: string;
zone?: string;
}
export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
e2e.components.TimePicker.openButton().click();
if (zone) {
cy.contains('button', 'Change time settings').click();
cy.log('setting time zone to ' + zone);
if (e2e.components.TimeZonePicker.containerV2) {
selectOption({
clickToOpen: true,
container: e2e.components.TimeZonePicker.containerV2(),
optionText: zone,
});
} else {
selectOption({
clickToOpen: true,
container: e2e.components.TimeZonePicker.container(),
optionText: zone,
});
}
}
// For smaller screens
e2e.components.TimePicker.absoluteTimeRangeTitle().click();
e2e.components.TimePicker.fromField().clear().type(from);
e2e.components.TimePicker.toField().clear().type(to);
e2e.components.TimePicker.applyTimeRange().click();
};

View File

@ -0,0 +1,25 @@
import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen';
import { e2e } from '..';
import { fromBaseUrl } from '../support/url';
const defaultUserPreferences = {
timezone: '', // "Default" option
} as const; // TODO: when we update typescript >4.9 change to `as const satisfies UserPreferencesDTO`
// Only accept preferences we have defaults for as arguments. To allow a new preference to be set, add a default for it
type UserPreferences = Pick<UserPreferencesDTO, keyof typeof defaultUserPreferences>;
export function setUserPreferences(prefs: UserPreferences) {
e2e.setScenarioContext({ hasChangedUserPreferences: prefs !== defaultUserPreferences });
return cy.request({
method: 'PUT',
url: fromBaseUrl('/api/user/preferences'),
body: prefs,
});
}
export function setDefaultUserPreferences() {
return setUserPreferences(defaultUserPreferences);
}

18
e2e/scenes/utils/index.ts Normal file
View File

@ -0,0 +1,18 @@
import { E2ESelectors, Selectors, selectors } from '@grafana/e2e-selectors';
import * as flows from './flows';
import { e2eFactory } from './support';
import { benchmark } from './support/benchmark';
import { getScenarioContext, setScenarioContext } from './support/scenarioContext';
import * as typings from './typings';
export const e2e = {
benchmark,
pages: e2eFactory({ selectors: selectors.pages }),
typings,
components: e2eFactory({ selectors: selectors.components }),
flows,
getScenarioContext,
setScenarioContext,
getSelectors: <T extends Selectors>(selectors: E2ESelectors<T>) => e2eFactory({ selectors }),
};

View File

@ -0,0 +1,70 @@
import { e2e } from '../';
export interface BenchmarkArguments {
name: string;
dashboard: {
folder: string;
delayAfterOpening: number;
skipPanelValidation: boolean;
};
repeat: number;
duration: number;
appStats?: {
startCollecting?: (window: Window) => void;
collect: (window: Window) => Record<string, unknown>;
};
skipScenario?: boolean;
}
export const benchmark = ({
name,
skipScenario = false,
repeat,
duration,
appStats,
dashboard,
}: BenchmarkArguments) => {
if (skipScenario) {
describe(name, () => {
it.skip(name, () => {});
});
} else {
describe(name, () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
e2e.flows.importDashboards(dashboard.folder, 1000, dashboard.skipPanelValidation);
});
afterEach(() => e2e.flows.revertAllChanges());
Array(repeat)
.fill(0)
.map((_, i) => {
const testName = `${name}-${i}`;
return it(testName, () => {
e2e.flows.openDashboard();
cy.wait(dashboard.delayAfterOpening);
if (appStats) {
const startCollecting = appStats.startCollecting;
if (startCollecting) {
cy.window().then((win) => startCollecting(win));
}
cy.startBenchmarking(testName);
cy.wait(duration);
cy.window().then((win) => {
cy.stopBenchmarking(testName, appStats.collect(win));
});
} else {
cy.startBenchmarking(testName);
cy.wait(duration);
cy.stopBenchmarking(testName, {});
}
});
});
});
}
};

View File

@ -0,0 +1,4 @@
export * from './localStorage';
export * from './scenarioContext';
export * from './selector';
export * from './types';

View File

@ -0,0 +1,17 @@
// @todo this actually returns type `Cypress.Chainable`
const get = (key: string) =>
cy.wrap({ getLocalStorage: () => localStorage.getItem(key) }, { log: false }).invoke('getLocalStorage');
export const getLocalStorage = (key: string) =>
get(key).then((value) => {
if (value === null) {
return value;
} else {
return JSON.parse(value);
}
});
export const requireLocalStorage = (key: string) =>
get(key) // `getLocalStorage()` would turn 'null' into `null`
.should('not.equal', null)
.then((value) => JSON.parse(value));

View File

@ -0,0 +1,7 @@
import { e2e } from '../index';
export function waitForMonacoToLoad() {
e2e.components.QueryField.container().children('[data-testid="Spinner"]').should('not.exist');
cy.window().its('monaco').should('exist');
cy.get('.monaco-editor textarea:first').should('exist');
}

View File

@ -0,0 +1,57 @@
import { DeleteDashboardConfig } from '../flows/deleteDashboard';
import { DeleteDataSourceConfig } from '../flows/deleteDataSource';
export interface ScenarioContext {
addedDashboards: DeleteDashboardConfig[];
addedDataSources: DeleteDataSourceConfig[];
lastAddedDashboard: string; // @todo rename to `lastAddedDashboardTitle`
lastAddedDashboardUid: string;
lastAddedDataSource: string; // @todo rename to `lastAddedDataSourceName`
lastAddedDataSourceId: string;
hasChangedUserPreferences: boolean;
}
const scenarioContext: ScenarioContext = {
addedDashboards: [],
addedDataSources: [],
hasChangedUserPreferences: false,
get lastAddedDashboard() {
return lastProperty(this.addedDashboards, 'title');
},
get lastAddedDashboardUid() {
return lastProperty(this.addedDashboards, 'uid');
},
get lastAddedDataSource() {
return lastProperty(this.addedDataSources, 'name');
},
get lastAddedDataSourceId() {
return lastProperty(this.addedDataSources, 'id');
},
};
const lastProperty = <T extends DeleteDashboardConfig | DeleteDataSourceConfig, K extends keyof T>(
items: T[],
key: K
) => items[items.length - 1]?.[key] ?? '';
export const getScenarioContext = (): Cypress.Chainable<ScenarioContext> =>
cy
.wrap(
{
getScenarioContext: (): ScenarioContext => ({ ...scenarioContext }),
},
{ log: false }
)
.invoke({ log: false }, 'getScenarioContext');
export const setScenarioContext = (newContext: Partial<ScenarioContext>): Cypress.Chainable<ScenarioContext> =>
cy
.wrap(
{
setScenarioContext: () => {
Object.assign(scenarioContext, newContext);
},
},
{ log: false }
)
.invoke({ log: false }, 'setScenarioContext');

View File

@ -0,0 +1,11 @@
export interface SelectorApi {
fromAriaLabel: (selector: string) => string;
fromDataTestId: (selector: string) => string;
fromSelector: (selector: string) => string;
}
export const Selector: SelectorApi = {
fromAriaLabel: (selector: string) => `[aria-label="${selector}"]`,
fromDataTestId: (selector: string) => `[data-testid="${selector}"]`,
fromSelector: (selector: string) => selector,
};

View File

@ -0,0 +1,136 @@
import { CssSelector, FunctionSelector, Selectors, StringSelector, UrlSelector } from '@grafana/e2e-selectors';
import { Selector } from './selector';
import { fromBaseUrl } from './url';
export type VisitFunction = (args?: string, queryParams?: object) => Cypress.Chainable<Window>;
export type E2EVisit = { visit: VisitFunction };
export type E2EFunction = ((text?: string, options?: CypressOptions) => Cypress.Chainable<JQuery<HTMLElement>>) &
E2EFunctionWithOnlyOptions;
export type E2EFunctionWithOnlyOptions = (options?: CypressOptions) => Cypress.Chainable<JQuery<HTMLElement>>;
export type TypeSelectors<S> = S extends StringSelector
? E2EFunctionWithOnlyOptions
: S extends FunctionSelector
? E2EFunction
: S extends CssSelector
? E2EFunction
: S extends UrlSelector
? E2EVisit & Omit<E2EFunctions<S>, 'url'>
: S extends Record<string, string | FunctionSelector | CssSelector | UrlSelector | Selectors>
? E2EFunctions<S>
: S;
export type E2EFunctions<S extends Selectors> = {
[P in keyof S]: TypeSelectors<S[P]>;
};
export type E2EObjects<S extends Selectors> = E2EFunctions<S>;
export type E2EFactoryArgs<S extends Selectors> = { selectors: S };
export type CypressOptions = Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>;
const processSelectors = <S extends Selectors>(e2eObjects: E2EFunctions<S>, selectors: S): E2EFunctions<S> => {
const logOutput = (data: unknown) => cy.logToConsole('Retrieving Selector:', data);
const keys = Object.keys(selectors);
for (let index = 0; index < keys.length; index++) {
const key = keys[index];
const value = selectors[key];
if (key === 'url') {
// @ts-ignore
e2eObjects['visit'] = (args?: string, queryParams?: object) => {
let parsedUrl = '';
if (typeof value === 'string') {
parsedUrl = fromBaseUrl(value);
}
if (typeof value === 'function' && args) {
parsedUrl = fromBaseUrl(value(args));
}
cy.logToConsole('Visiting', parsedUrl);
if (queryParams) {
return cy.visit({ url: parsedUrl, qs: queryParams });
} else {
return cy.visit(parsedUrl);
}
};
continue;
}
if (typeof value === 'string') {
// @ts-ignore
e2eObjects[key] = (options?: CypressOptions) => {
logOutput(value);
const selector = value.startsWith('data-testid')
? Selector.fromDataTestId(value)
: Selector.fromAriaLabel(value);
return cy.get(selector, options);
};
continue;
}
if (typeof value === 'function') {
// @ts-ignore
e2eObjects[key] = function (textOrOptions?: string | CypressOptions, options?: CypressOptions) {
// the input can only be ()
if (arguments.length === 0) {
const selector = value('');
logOutput(selector);
return cy.get(selector);
}
// the input can be (text) or (options)
if (arguments.length === 1) {
if (typeof textOrOptions === 'string') {
const selectorText = value(textOrOptions);
const selector = selectorText.startsWith('data-testid')
? Selector.fromDataTestId(selectorText)
: Selector.fromAriaLabel(selectorText);
logOutput(selector);
return cy.get(selector);
}
const selector = value('');
logOutput(selector);
return cy.get(selector, textOrOptions);
}
// the input can only be (text, options)
if (arguments.length === 2 && typeof textOrOptions === 'string') {
const text = textOrOptions;
const selectorText = value(text);
const selector = text.startsWith('data-testid')
? Selector.fromDataTestId(selectorText)
: Selector.fromAriaLabel(selectorText);
logOutput(selector);
return cy.get(selector, options);
}
};
continue;
}
if (typeof value === 'object') {
// @ts-ignore
e2eObjects[key] = processSelectors({}, value);
}
}
return e2eObjects;
};
export const e2eFactory = <S extends Selectors>({ selectors }: E2EFactoryArgs<S>): E2EObjects<S> => {
const e2eObjects: E2EFunctions<S> = {} as E2EFunctions<S>;
processSelectors(e2eObjects, selectors);
return { ...e2eObjects };
};

View File

@ -0,0 +1,14 @@
import { e2e } from '../index';
const getBaseUrl = () => Cypress.env('BASE_URL') || Cypress.config().baseUrl || 'http://localhost:3000';
export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href;
export const getDashboardUid = (url: string): string => {
const matches = new URL(url).pathname.match(/\/d\/([^/]+)/);
if (!matches) {
throw new Error(`Couldn't parse uid from ${url}`);
} else {
return matches[1];
}
};

View File

@ -0,0 +1 @@
export { undo } from './undo';

View File

@ -0,0 +1,19 @@
// https://nodejs.org/api/os.html#os_os_platform
enum Platform {
osx = 'darwin',
windows = 'win32',
linux = 'linux',
aix = 'aix',
freebsd = 'freebsd',
openbsd = 'openbsd',
sunos = 'sunos',
}
export const undo = () => {
switch (Cypress.platform) {
case Platform.osx:
return '{cmd}z';
default:
return '{ctrl}z';
}
};

View File

@ -10,6 +10,7 @@
"build:nominify": "yarn run build -- --env noMinify=1",
"dev": "NODE_ENV=dev nx exec -- webpack --config scripts/webpack/webpack.dev.js",
"e2e": "./e2e/start-and-run-suite",
"e2e:scenes": "./e2e/start-and-run-suite scenes",
"e2e:debug": "./e2e/start-and-run-suite debug",
"e2e:dev": "./e2e/start-and-run-suite dev",
"e2e:benchmark:live": "./e2e/start-and-run-suite benchmark live",

View File

@ -215,6 +215,11 @@ export const Components = {
rcContentWrapper: () => '.rc-drawer-content-wrapper',
subtitle: 'data-testid drawer subtitle',
},
DashboardSaveDrawer: {
saveButton: 'data-testid Save dashboard drawer button',
saveAsButton: 'data-testid Save as dashboard drawer button',
saveAsTitleInput: 'Save dashboard title field',
},
},
PanelEditor: {
General: {
@ -373,7 +378,25 @@ export const Components = {
},
NavToolbar: {
container: 'data-testid Nav toolbar',
shareDashboard: 'data-testid Share dashboard',
markAsFavorite: 'data-testid Mark as favorite',
editDashboard: {
editButton: 'data-testid Edit dashboard button',
saveButton: 'data-testid Save dashboard button',
exitButton: 'data-testid Exit edit mode button',
settingsButton: 'data-testid Dashboard settings',
addRowButton: 'data-testid Add row button',
addLibraryPanelButton: 'data-testid Add a panel from the panel library button',
addVisualizationButton: 'data-testid Add new visualization menu item',
pastePanelButton: 'data-testid Paste panel button',
discardChangesButton: 'data-testid Discard changes button',
discardLibraryPanelButton: 'data-testid Discard library panel button',
unlinkLibraryPanelButton: 'data-testid Unlink library panel button',
saveLibraryPanelButton: 'data-testid Save library panel button',
backToDashboardButton: 'data-testid Back to dashboard button',
},
},
PageToolbar: {
container: () => '.page-toolbar',
item: (tooltip: string) => `${tooltip}`,

View File

@ -57,6 +57,7 @@ export const Pages = {
navV2: 'data-testid Dashboard navigation',
publicDashboardTag: 'data-testid public dashboard tag',
shareButton: 'data-testid share-button',
scrollContainer: 'data-testid Dashboard canvas scroll container',
playlistControls: {
prev: 'data-testid playlist previous dashboard button',
stop: 'data-testid playlist stop dashboard button',
@ -258,6 +259,9 @@ export const Pages = {
ReshareLink: 'data-testid public dashboard reshare link button',
},
},
PublicDashboardScene: {
Tab: 'Tab Public Dashboard',
},
},
PublicDashboard: {
page: 'public-dashboard-page',

View File

@ -2,6 +2,7 @@ import debounce from 'debounce-promise';
import React, { ChangeEvent } from 'react';
import { UseFormSetValue, useForm } from 'react-hook-form';
import { selectors } from '@grafana/e2e-selectors';
import { Dashboard } from '@grafana/schema';
import { Button, Input, Switch, Field, Label, TextArea, Stack, Alert, Box } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
@ -97,6 +98,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
<Input
{...register('title', { required: 'Required', validate: validateDashboardName })}
aria-label="Save dashboard title field"
data-testid={selectors.components.Drawer.DashboardSaveDrawer.saveAsTitleInput}
onChange={debounce(async (e: ChangeEvent<HTMLInputElement>) => {
setValue('title', e.target.value, { shouldValidate: true });
}, 400)}

View File

@ -122,7 +122,7 @@ describe('SaveDashboardDrawer', () => {
mockSaveDashboard();
await userEvent.click(await screen.findByTestId(selectors.pages.SaveDashboardModal.save));
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
const dataSent = saveDashboardMutationMock.mock.calls[0][0];
expect(dataSent.dashboard.title).toEqual('New title');
@ -140,13 +140,13 @@ describe('SaveDashboardDrawer', () => {
mockSaveDashboard({ saveError: 'version-mismatch' });
await userEvent.click(await screen.findByTestId(selectors.pages.SaveDashboardModal.save));
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
expect(await screen.findByText('Someone else has updated this dashboard')).toBeInTheDocument();
expect(await screen.findByText('Save and overwrite')).toBeInTheDocument();
// Now save and overwrite
await userEvent.click(await screen.findByTestId(selectors.pages.SaveDashboardModal.save));
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
const dataSent = saveDashboardMutationMock.mock.calls[1][0];
expect(dataSent.overwrite).toEqual(true);
@ -162,7 +162,7 @@ describe('SaveDashboardDrawer', () => {
mockSaveDashboard();
await userEvent.click(await screen.findByTestId(selectors.pages.SaveDashboardModal.save));
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
const dataSent = saveDashboardMutationMock.mock.calls[0][0];
expect(dataSent.dashboard.uid).toEqual('');

View File

@ -65,9 +65,9 @@ export function SaveButton({ overwrite, isLoading, isValid, onSave }: SaveButton
<Button
disabled={!isValid || isLoading}
icon={isLoading ? 'spinner' : undefined}
data-testid={selectors.pages.SaveDashboardModal.save}
onClick={() => onSave(overwrite)}
variant={overwrite ? 'destructive' : 'primary'}
data-testid={selectors.components.Drawer.DashboardSaveDrawer.saveButton}
>
{isLoading ? 'Saving...' : overwrite ? 'Save and overwrite' : 'Save'}
</Button>

View File

@ -3,6 +3,7 @@ import React from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { SceneComponentProps } from '@grafana/scenes';
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
@ -62,7 +63,11 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
<controls.Component model={controls} />
</div>
)}
<CustomScrollbar autoHeightMin={'100%'} className={styles.scrollbarContainer}>
<CustomScrollbar
autoHeightMin={'100%'}
className={styles.scrollbarContainer}
testId={selectors.pages.Dashboard.DashNav.scrollContainer}
>
<div className={cx(styles.canvasContent, isHomePage && styles.homePagePadding)}>
<>{isEmpty && emptyState}</>
{withPanels}

View File

@ -89,6 +89,7 @@ export function ToolbarActions({ dashboard }: Props) {
<Icon name={meta.isStarred ? 'favorite' : 'star'} size="lg" type={meta.isStarred ? 'mono' : 'default'} />
}
key="star-dashboard-button"
data-testid={selectors.components.NavToolbar.markAsFavorite}
onClick={() => {
DashboardInteractions.toolbarFavoritesClick();
dashboard.onStarDashboard();
@ -274,6 +275,7 @@ export function ToolbarActions({ dashboard }: Props) {
variant="secondary"
size="sm"
icon="arrow-left"
data-testid={selectors.components.NavToolbar.editDashboard.backToDashboardButton}
>
Back to dashboard
</Button>
@ -294,6 +296,7 @@ export function ToolbarActions({ dashboard }: Props) {
variant="secondary"
size="sm"
icon="arrow-left"
data-testid={selectors.components.NavToolbar.editDashboard.backToDashboardButton}
>
Back to dashboard
</Button>
@ -314,6 +317,7 @@ export function ToolbarActions({ dashboard }: Props) {
DashboardInteractions.toolbarShareClick();
dashboard.showModal(new ShareModal({ dashboardRef: dashboard.getRef() }));
}}
data-testid={selectors.components.NavToolbar.shareDashboard}
>
Share
</Button>
@ -333,6 +337,7 @@ export function ToolbarActions({ dashboard }: Props) {
className={styles.buttonWithExtraMargin}
variant="primary"
size="sm"
data-testid={selectors.components.NavToolbar.editDashboard.editButton}
>
Edit
</Button>
@ -352,6 +357,7 @@ export function ToolbarActions({ dashboard }: Props) {
size="sm"
key="settings"
variant="secondary"
data-testid={selectors.components.NavToolbar.editDashboard.settingsButton}
>
Settings
</Button>
@ -369,6 +375,7 @@ export function ToolbarActions({ dashboard }: Props) {
key="discard"
fill="text"
variant="primary"
data-testid={selectors.components.NavToolbar.editDashboard.exitButton}
>
Exit edit
</Button>
@ -386,6 +393,7 @@ export function ToolbarActions({ dashboard }: Props) {
key="discard"
fill="outline"
variant="destructive"
data-testid={selectors.components.NavToolbar.editDashboard.discardChangesButton}
>
Discard panel changes
</Button>
@ -403,6 +411,7 @@ export function ToolbarActions({ dashboard }: Props) {
key="discardLibraryPanel"
fill="outline"
variant="destructive"
data-testid={selectors.components.NavToolbar.editDashboard.discardChangesButton}
>
Discard library panel changes
</Button>
@ -420,6 +429,7 @@ export function ToolbarActions({ dashboard }: Props) {
key="unlinkLibraryPanel"
fill="outline"
variant="secondary"
data-testid={selectors.components.NavToolbar.editDashboard.unlinkLibraryPanelButton}
>
Unlink library panel
</Button>
@ -437,6 +447,7 @@ export function ToolbarActions({ dashboard }: Props) {
key="saveLibraryPanel"
fill="outline"
variant="primary"
data-testid={selectors.components.NavToolbar.editDashboard.saveLibraryPanelButton}
>
Save library panel
</Button>
@ -460,6 +471,7 @@ export function ToolbarActions({ dashboard }: Props) {
key="save"
size="sm"
variant={'primary'}
data-testid={selectors.components.NavToolbar.editDashboard.saveButton}
>
Save dashboard
</Button>
@ -516,6 +528,7 @@ export function ToolbarActions({ dashboard }: Props) {
}}
tooltip="Save changes"
size="sm"
data-testid={selectors.components.NavToolbar.editDashboard.saveButton}
variant={isDirty ? 'primary' : 'secondary'}
>
Save dashboard