mirror of https://github.com/grafana/grafana
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>
pull/86012/head
parent
0fbfb67b15
commit
9ea1042329
@ -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 }} |
@ -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'); |
||||
}); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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); |
||||
}); |
||||
}); |
@ -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}`; |
||||
}; |
@ -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'); |
||||
}); |
||||
}); |
@ -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'A"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' |
||||
); |
||||
}); |
||||
}); |
@ -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); |
||||
}; |
@ -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); |
||||
}); |
||||
}); |
@ -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')); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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); |
||||
}); |
||||
}); |
@ -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); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
@ -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'); |
||||
} |
@ -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; |
||||
} |
@ -0,0 +1,107 @@ |
||||
{ |
||||
"annotations": { |
||||
"list": [ |
||||
{ |
||||
"builtIn": 1, |
||||
"datasource": { |
||||
"type": "grafana", |
||||
"uid": "-- Grafana --" |
||||
}, |
||||
"enable": true, |
||||
"hide": true, |
||||
"iconColor": "rgba(0, 211, 255, 1)", |
||||
"name": "Annotations & Alerts", |
||||
"type": "dashboard" |
||||
} |
||||
] |
||||
}, |
||||
"editable": true, |
||||
"fiscalYearStartMonth": 0, |
||||
"graphTooltip": 0, |
||||
"links": [], |
||||
"liveNow": false, |
||||
"panels": [ |
||||
{ |
||||
"datasource": { |
||||
"type": "datasource", |
||||
"uid": "grafana" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "thresholds" |
||||
}, |
||||
"custom": { |
||||
"align": "auto", |
||||
"cellOptions": { |
||||
"type": "auto" |
||||
}, |
||||
"inspect": false |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 12, |
||||
"x": 0, |
||||
"y": 0 |
||||
}, |
||||
"id": 1, |
||||
"options": { |
||||
"cellHeight": "sm", |
||||
"footer": { |
||||
"countRows": false, |
||||
"fields": "", |
||||
"reducer": ["sum"], |
||||
"show": false |
||||
}, |
||||
"showHeader": true |
||||
}, |
||||
"pluginVersion": "10.3.0-pre", |
||||
"targets": [ |
||||
{ |
||||
"channel": "plugin/testdata/random-20Hz-stream", |
||||
"datasource": { |
||||
"type": "datasource", |
||||
"uid": "grafana" |
||||
}, |
||||
"queryType": "measurements", |
||||
"refId": "A" |
||||
} |
||||
], |
||||
"title": "Live", |
||||
"type": "table" |
||||
} |
||||
], |
||||
"refresh": "", |
||||
"schemaVersion": 39, |
||||
"tags": [], |
||||
"templating": { |
||||
"list": [] |
||||
}, |
||||
"time": { |
||||
"from": "now-6h", |
||||
"to": "now" |
||||
}, |
||||
"timepicker": {}, |
||||
"timezone": "", |
||||
"title": "New dashboard", |
||||
"version": 0, |
||||
"uid": "live-e2e-test", |
||||
"weekStart": "" |
||||
} |
@ -0,0 +1,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 |
||||
} |
@ -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": "" |
||||
} |
@ -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 |
||||
} |
@ -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'); |
||||
}); |
||||
}); |
@ -0,0 +1,3 @@ |
||||
import { smokeTestScenario } from '../shared/smokeTestScenario'; |
||||
|
||||
smokeTestScenario(); |
@ -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'); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -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)); |
||||
}; |
@ -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 } |
||||
); |
||||
}); |
||||
}; |
@ -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, |
||||
}) |
||||
); |
@ -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'); |
||||
} |
||||
}; |
@ -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'; |
@ -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(); |
||||
} |
@ -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)); |
||||
} |
||||
}; |
@ -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)); |
||||
}; |
@ -0,0 +1,7 @@ |
||||
import { configurePanel, PartialEditPanelConfig } from './configurePanel'; |
||||
|
||||
export const editPanel = (config: Partial<PartialEditPanelConfig>) => |
||||
configurePanel({ |
||||
...config, |
||||
isEdit: true, |
||||
}); |
@ -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(); |
||||
}); |
||||
} |
||||
}; |
@ -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); |
||||
}); |
||||
}); |
||||
}; |
@ -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'; |
@ -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); |
||||
}; |
@ -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 }); |
||||
}); |
@ -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); |
||||
}); |
||||
} |
@ -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(); |
||||
} |
||||
}); |
||||
}; |
@ -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(); |
||||
}; |
@ -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 }); |
||||
}; |
@ -0,0 +1,5 @@ |
||||
import { setTimeRange, TimeRangeConfig } from './setTimeRange'; |
||||
|
||||
export type { TimeRangeConfig }; |
||||
|
||||
export const setDashboardTimeRange = (config: TimeRangeConfig) => setTimeRange(config); |
@ -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(); |
||||
}; |
@ -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); |
||||
} |
@ -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 }), |
||||
}; |
@ -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, {}); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
}; |
@ -0,0 +1,4 @@ |
||||
export * from './localStorage'; |
||||
export * from './scenarioContext'; |
||||
export * from './selector'; |
||||
export * from './types'; |
@ -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)); |
@ -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'); |
||||
} |
@ -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'); |
@ -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, |
||||
}; |
@ -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 }; |
||||
}; |
@ -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]; |
||||
} |
||||
}; |
@ -0,0 +1 @@ |
||||
export { undo } from './undo'; |
@ -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'; |
||||
} |
||||
}; |
Loading…
Reference in new issue