diff --git a/CHANGELOG.md b/CHANGELOG.md index 46a73e2..523aa94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 3.9.0 (IN PROGRESS) + +### Features / Enhancements + +- Updatd e2e tests (#250) + ## 3.8.0 (2024-10-25) ### Features / Enhancements diff --git a/package-lock.json b/package-lock.json index bef8262..3cb4f1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "business-panel", - "version": "3.8.0", + "version": "3.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "business-panel", - "version": "3.8.0", + "version": "3.9.0", "license": "Apache-2.0", "dependencies": { "@emotion/css": "^11.13.0", @@ -27,7 +27,7 @@ "devDependencies": { "@babel/core": "^7.25.9", "@grafana/eslint-config": "^8.0.0", - "@grafana/plugin-e2e": "^1.10.0", + "@grafana/plugin-e2e": "^1.11.0", "@grafana/tsconfig": "^2.0.0", "@playwright/test": "^1.48.1", "@swc/core": "^1.7.39", @@ -1048,11 +1048,12 @@ } }, "node_modules/@grafana/plugin-e2e": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@grafana/plugin-e2e/-/plugin-e2e-1.10.0.tgz", - "integrity": "sha512-3I49jLwN5rKYhmxLMC5DIHiUISw0ENaUTEZBJ/dgY2i4gYS3jbPhkFte4+rgYOufO07BfrRAipCQnf1ndtukVA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@grafana/plugin-e2e/-/plugin-e2e-1.11.0.tgz", + "integrity": "sha512-pjmDIcVshHD8P/9brho10zb4XleO+6/DL2YWLA1QqC7ppelyHXCx2CdShVQvzjicRgiIgSeQ9UDvVOQzZfCNBQ==", "dev": true, "dependencies": { + "@grafana/e2e-selectors": "^11.4.0-204289", "semver": "^7.5.4", "uuid": "^10.0.0", "yaml": "^2.3.4" @@ -1064,6 +1065,18 @@ "@playwright/test": "^1.41.2" } }, + "node_modules/@grafana/plugin-e2e/node_modules/@grafana/e2e-selectors": { + "version": "11.4.0-205124", + "resolved": "https://registry.npmjs.org/@grafana/e2e-selectors/-/e2e-selectors-11.4.0-205124.tgz", + "integrity": "sha512-PmvEsMR8QGhwrnbNnUca37B0KNDTvsZvqrAuziHoZNXW/m6H3XYzP5Sgi7i6Bk78DGQz1mVEtmlOZeDrn3mIKQ==", + "dev": true, + "dependencies": { + "@grafana/tsconfig": "^2.0.0", + "semver": "7.6.3", + "tslib": "2.7.0", + "typescript": "5.5.4" + } + }, "node_modules/@grafana/plugin-e2e/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -1076,6 +1089,25 @@ "node": ">=10" } }, + "node_modules/@grafana/plugin-e2e/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, + "node_modules/@grafana/plugin-e2e/node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@grafana/runtime": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/@grafana/runtime/-/runtime-11.3.0.tgz", diff --git a/package.json b/package.json index f5d1c28..222f366 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@babel/core": "^7.25.9", "@grafana/eslint-config": "^8.0.0", - "@grafana/plugin-e2e": "^1.10.0", + "@grafana/plugin-e2e": "^1.11.0", "@grafana/tsconfig": "^2.0.0", "@playwright/test": "^1.48.1", "@swc/core": "^1.7.39", @@ -81,5 +81,5 @@ "test:e2e:docker": "docker compose --profile e2e up --exit-code-from test", "upgrade": "npm upgrade --save" }, - "version": "3.8.0" + "version": "3.9.0" } diff --git a/test/panel.spec.ts b/test/panel.spec.ts index 4852f94..ddc45fd 100644 --- a/test/panel.spec.ts +++ b/test/panel.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '@grafana/plugin-e2e'; import { TEST_IDS } from '../src/constants'; import { View } from '../src/types'; +import { PanelHelper, UrlHelper } from './utils'; test.describe('Volkovlabs Calendar Panel', () => { test('Check grafana version', async ({ grafanaVersion }) => { @@ -8,66 +9,266 @@ test.describe('Volkovlabs Calendar Panel', () => { expect(grafanaVersion).toEqual(grafanaVersion); }); - test('Should display a Calendar', async ({ gotoDashboardPage, page, gotoPanelEditPage }) => { + test('Should display a Calendar panel', async ({ readProvisionedDashboard, gotoDashboardPage }) => { /** * Go To Panels dashboard panels.json * return dashboardPage */ - await gotoDashboardPage({ uid: 'hHK1qmpnk' }); + const dashboard = await readProvisionedDashboard({ fileName: 'panels.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); - await expect(page.getByRole('heading', { name: 'Calendar' }).first()).toBeVisible(); + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Calendar'); + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const toolbar = panel.getToolbar(); + + await toolbar.todayButtonCheckPresence(); + await toolbar.nextButtonCheckPresence(); + await toolbar.backButtonCheckPresence(); + await toolbar.viewButtonCheckPresence(View.DAY); + await toolbar.viewButtonCheckPresence(View.AGENDA); + await toolbar.viewButtonCheckPresence(View.MONTH); + await toolbar.viewButtonCheckPresence(View.WEEK); + await toolbar.viewButtonCheckPresence(View.WORK_WEEK); + await toolbar.viewButtonCheckPresence(View.YEAR); + + await panel.checkDayHeaderPresence('Sun'); + await panel.checkDayHeaderPresence('Mon'); + await panel.checkDayHeaderPresence('Tue'); + await panel.checkDayHeaderPresence('Thu'); + await panel.checkDayHeaderPresence('Fri'); + await panel.checkDayHeaderPresence('Sat'); + }); + test('Should display a Calendar panel after refresh without errors', async ({ + readProvisionedDashboard, + gotoDashboardPage, + }) => { /** - * Go to panel Edit page + * Go To Panels dashboard panels.json + * return dashboardPage */ - await gotoPanelEditPage({ dashboard: { uid: 'hHK1qmpnk' }, id: '16' }); + const dashboard = await readProvisionedDashboard({ fileName: 'panels.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); /** - * Wait canvas is visible and animation is finished + * Check Presence */ - await page.waitForTimeout(3000); + const panel = new PanelHelper(dashboardPage, 'Calendar'); + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + await dashboardPage.refreshDashboard(); + await panel.checkIfNoErrors(); + await panel.checkPresence(); + }); + + test('Should add empty default calendar', async ({ readProvisionedDashboard, gotoDashboardPage }) => { /** - * Calendar should be visible + * Go To Panels dashboard weekly.json + * return dashboardPage */ - await expect(page.getByTestId(TEST_IDS.bigCalendar.root)).toBeVisible(); + const dashboard = await readProvisionedDashboard({ fileName: 'weekly.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); /** - * Days of the week should be + * Add new visualization */ - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('columnheader', { name: 'Sun' })).toBeVisible(); - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('columnheader', { name: 'Mon' })).toBeVisible(); - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('columnheader', { name: 'Tue' })).toBeVisible(); - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('columnheader', { name: 'Wed' })).toBeVisible(); - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('columnheader', { name: 'Thu' })).toBeVisible(); - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('columnheader', { name: 'Fri' })).toBeVisible(); - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('columnheader', { name: 'Sat' })).toBeVisible(); + const editPage = await dashboardPage.addPanel(); + await editPage.setVisualization('Business Calendar'); + await editPage.setPanelTitle('Business Calendar Test'); + await editPage.backToDashboard(); /** - * Check calendar navigation controls + * Should add empty visualization without errors */ - await expect(page.getByTestId(TEST_IDS.bigCalendar.root).getByRole('button', { name: 'Today' })).toBeVisible(); + const panel = new PanelHelper(dashboardPage, 'Business Calendar Test'); + await panel.checkIfNoErrors(); + await panel.checkPresence(); /** - * Check if calendar supports all views + * Should display day headers */ - await expect( - page.getByTestId(TEST_IDS.bigCalendar.root).getByTestId(TEST_IDS.bigCalendarToolbar.buttonView(View.DAY)) - ).toBeVisible(); - await expect( - page.getByTestId(TEST_IDS.bigCalendar.root).getByTestId(TEST_IDS.bigCalendarToolbar.buttonView(View.WEEK)) - ).toBeVisible(); - await expect( - page.getByTestId(TEST_IDS.bigCalendar.root).getByTestId(TEST_IDS.bigCalendarToolbar.buttonView(View.WORK_WEEK)) - ).toBeVisible(); - await expect( - page.getByTestId(TEST_IDS.bigCalendar.root).getByTestId(TEST_IDS.bigCalendarToolbar.buttonView(View.MONTH)) - ).toBeVisible(); - await expect( - page.getByTestId(TEST_IDS.bigCalendar.root).getByTestId(TEST_IDS.bigCalendarToolbar.buttonView(View.YEAR)) - ).toBeVisible(); - await expect( - page.getByTestId(TEST_IDS.bigCalendar.root).getByTestId(TEST_IDS.bigCalendarToolbar.buttonView(View.AGENDA)) - ).toBeVisible(); + await panel.checkDayHeaderPresence('Sun'); + await panel.checkDayHeaderPresence('Mon'); + await panel.checkDayHeaderPresence('Tue'); + await panel.checkDayHeaderPresence('Thu'); + await panel.checkDayHeaderPresence('Fri'); + await panel.checkDayHeaderPresence('Sat'); + }); + + test.describe('Day view', () => { + test('Should change view on Day', async ({ readProvisionedDashboard, gotoDashboardPage }) => { + /** + * Go To Panels dashboard panels.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'panels.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Calendar'); + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const toolbar = panel.getToolbar(); + + await toolbar.viewButtonCheckPresence(View.DAY); + await toolbar.viewButtonCheckPresence(View.MONTH); + await panel.checkDayHeaderPresence('Sun'); + await panel.checkDayHeaderPresence('Mon'); + + /** + * Change view + */ + await toolbar.changeView(View.DAY); + + /** + * Day headers should not be present on day view + */ + await panel.checkDayHeaderNotPresence('Sun'); + await panel.checkDayHeaderNotPresence('Mon'); + + await toolbar.isViewButtonDisabled(View.DAY); + await panel.checkIfNoErrors(); + + /** + * Change view + */ + await toolbar.changeView(View.MONTH); + }); + }); + + test.describe('Year view', () => { + test('Should change view on year', async ({ readProvisionedDashboard, gotoDashboardPage }) => { + /** + * Go To Panels dashboard panels.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'panels.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Calendar'); + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const toolbar = panel.getToolbar(); + + await toolbar.viewButtonCheckPresence(View.YEAR); + await toolbar.viewButtonCheckPresence(View.MONTH); + await panel.checkDayHeaderPresence('Sun'); + await panel.checkDayHeaderPresence('Mon'); + + /** + * Change view + */ + await toolbar.changeView(View.YEAR); + + /** + * Day headers should not be present on day view + */ + await panel.checkDayHeaderNotPresence('Sun'); + await panel.checkDayHeaderNotPresence('Mon'); + + await toolbar.isViewButtonDisabled(View.YEAR); + await panel.checkIfNoErrors(); + + const yearView = panel.getYearView(); + await yearView.checkPresence(); + + /** + * Check first and last month + */ + await yearView.checkMonthPresence(0); + await yearView.checkMonthPresence(11); + + /** + * Change view + */ + await toolbar.changeView(View.MONTH); + }); + + test('Should navigate between years', async ({ readProvisionedDashboard, gotoDashboardPage, page }) => { + /** + * Go To Panels dashboard panels.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'panels.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Calendar'); + await panel.checkPresence(); + + const toolbar = panel.getToolbar(); + + /** + * Change view + */ + await toolbar.changeView(View.YEAR); + await toolbar.isViewButtonDisabled(View.YEAR); + + const yearView = panel.getYearView(); + await yearView.checkPresence(); + + const urlParams = new UrlHelper(await page.url()); + + await toolbar.goBack(); + + await urlParams.isParamDifferent('to', await page.url()); + /** + * Change view + */ + await toolbar.changeView(View.MONTH); + }); + }); + + test.describe('Languages', () => { + test('Should display different languages for panels on dashboard', async ({ + readProvisionedDashboard, + gotoDashboardPage, + }) => { + /** + * Go To Panels dashboard different.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'different.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panelSpanish = new PanelHelper(dashboardPage, 'Spanish'); + const panelEnglish = new PanelHelper(dashboardPage, 'English'); + const panelGerman = new PanelHelper(dashboardPage, 'German'); + await panelSpanish.checkPresence(); + await panelEnglish.checkPresence(); + await panelGerman.checkPresence(); + + await panelSpanish.checkDayHeaderPresence('dom.'); + await panelEnglish.checkDayHeaderPresence('Sun'); + await panelGerman.checkDayHeaderPresence('So.'); + + await dashboardPage.refreshDashboard(); + + /** + * Check days after refresh + */ + await panelSpanish.checkDayHeaderPresence('dom.'); + await panelEnglish.checkDayHeaderPresence('Sun'); + await panelGerman.checkDayHeaderPresence('So.'); + }); }); }); diff --git a/test/utils/calendar.ts b/test/utils/calendar.ts new file mode 100644 index 0000000..301709a --- /dev/null +++ b/test/utils/calendar.ts @@ -0,0 +1,132 @@ +import { Locator } from '@playwright/test'; +import { DashboardPage, expect, Panel } from '@grafana/plugin-e2e'; +import { TEST_IDS } from '../../src/constants'; +import { getLocatorSelectors, LocatorSelectors } from './selectors'; + +const getToolbarSelectors = getLocatorSelectors(TEST_IDS.bigCalendarToolbar); +const getYearViewSelectors = getLocatorSelectors(TEST_IDS.yearView); + +/** + * Year View Helper + */ +class YearViewHelper { + public selectors: LocatorSelectors; + + constructor(public readonly locator: Locator) { + this.selectors = this.getSelectors(locator); + } + + private getMsg(msg: string): string { + return `YearView: ${msg}`; + } + + private getSelectors(locator: Locator) { + return getYearViewSelectors(locator); + } + + public async checkPresence() { + return expect(this.selectors.root(), this.getMsg('Year view Presence')).toBeVisible(); + } + + public async checkMonthPresence(month: number) { + return expect(this.selectors.month(month), this.getMsg(`Month - ${month} Presence`)).toBeVisible(); + } +} + +/** + * Toolbar Helper + */ +class ToolbarHelper { + public selectors: LocatorSelectors; + + constructor(public readonly locator: Locator) { + this.selectors = this.getSelectors(locator); + } + + private getMsg(msg: string): string { + return `Toolbar: ${msg}`; + } + + private getSelectors(locator: Locator) { + return getToolbarSelectors(locator); + } + + public async todayButtonCheckPresence() { + return expect(this.selectors.buttonToday(), this.getMsg('Button "Today" Presence')).toBeVisible(); + } + + public async nextButtonCheckPresence() { + return expect(this.selectors.buttonNext(), this.getMsg('Button "Next" Presence')).toBeVisible(); + } + + public async backButtonCheckPresence() { + return expect(this.selectors.buttonBack(), this.getMsg('Button "Back" Presence')).toBeVisible(); + } + + public async viewButtonCheckPresence(view: string) { + return expect(this.selectors.buttonView(view), this.getMsg(`Button ${view} Presence`)).toBeVisible(); + } + + public async changeView(view: string) { + return this.selectors.buttonView(view).click(); + } + + public async isViewButtonDisabled(view: string) { + return expect(this.selectors.buttonView(view), this.getMsg(`Button ${view} Disabled`)).toBeDisabled(); + } + + public async goBack() { + return this.selectors.buttonBack().click(); + } +} + +/** + * Panel Helper + */ +export class PanelHelper { + private readonly locator: Locator; + private readonly panel: Panel; + private readonly title: string; + private readonly selectors: LocatorSelectors; + + constructor(dashboardPage: DashboardPage, panelTitle: string) { + this.panel = dashboardPage.getPanelByTitle(panelTitle); + this.title = panelTitle; + this.locator = this.panel.locator; + this.selectors = getLocatorSelectors(TEST_IDS.bigCalendar)(this.locator); + } + + private getMsg(msg: string): string { + return `Panel: ${msg}`; + } + + public getToolbar() { + return new ToolbarHelper(this.locator); + } + + public getYearView() { + return new YearViewHelper(this.locator); + } + + public async checkIfNoErrors() { + return expect(this.panel.getErrorIcon(), this.getMsg('Check If No Errors')).not.toBeVisible(); + } + + public async checkPresence() { + return expect(this.selectors.root(), this.getMsg(`Check ${this.title} Presence`)).toBeVisible(); + } + + public async checkDayHeaderPresence(dayName: string) { + return expect( + this.selectors.root().getByRole('columnheader', { name: dayName }), + this.getMsg(`Check ${dayName} Presence`) + ).toBeVisible(); + } + + public async checkDayHeaderNotPresence(dayName: string) { + return expect( + this.selectors.root().getByRole('columnheader', { name: dayName }), + this.getMsg(`Check ${dayName} Not Presence`) + ).not.toBeVisible(); + } +} diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 0000000..e0e165c --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1,3 @@ +export * from './calendar'; +export * from './selectors'; +export * from './url'; diff --git a/test/utils/selectors.ts b/test/utils/selectors.ts new file mode 100644 index 0000000..0da35dc --- /dev/null +++ b/test/utils/selectors.ts @@ -0,0 +1,51 @@ +import { Locator } from '@playwright/test'; + +/** + * Selector + */ +type LocatorSelector = (...args: TArgs) => ReturnType<() => Locator>; + +/** + * Check If Selector Object + */ +type IsSelectorObject = TCandidate extends { + selector: (...args: unknown[]) => void; + apply: (...args: unknown[]) => void; +} + ? TCandidate & { selector: TCandidate['selector']; apply: TCandidate['apply'] } + : never; + +/** + * Selectors + */ +export type LocatorSelectors = { + [K in keyof T]: T[K] extends (...args: infer Args) => void + ? LocatorSelector + : T[K] extends IsSelectorObject + ? LocatorSelector> + : LocatorSelector<[]>; +}; + +export const getLocatorSelectors = + >( + selectors: TSelectors + ): ((locator: Locator) => LocatorSelectors) => + (locator) => { + return Object.entries(selectors).reduce((acc, [key, selector]) => { + const getElement = (...args: unknown[]): Locator => { + const getValue = typeof selector === 'object' && 'selector' in selector! ? selector.selector : selector; + const value = typeof getValue === 'function' ? getValue(...args) : getValue; + + if (value.startsWith('data-testid')) { + return locator.getByTestId(value); + } + + return locator.getByLabel(value); + }; + + return { + ...acc, + [key]: getElement, + }; + }, {} as LocatorSelectors); + }; diff --git a/test/utils/url.ts b/test/utils/url.ts new file mode 100644 index 0000000..9c182b2 --- /dev/null +++ b/test/utils/url.ts @@ -0,0 +1,26 @@ +import { expect } from '@grafana/plugin-e2e'; + +/** + * Url params Helper + */ +export class UrlHelper { + public params: URLSearchParams; + + constructor(url: string) { + this.params = new URLSearchParams(url); + } + + public getParam(name: string) { + return this.params.get(name); + } + + public async isParamDifferent(name: string, url: string) { + const param = this.params.get(name); + const updatedParams = new URLSearchParams(url); + return expect(param).not.toEqual(updatedParams.get(name)); + } + + public updateParams(url: string) { + this.params = new URLSearchParams(url); + } +}