Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin E2E: Make it possible to test query editor in panel edit page #551

Merged
merged 25 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
fail-fast: false
matrix:
GRAFANA_VERSION: ['latest', '10.0.5', '9.5.5', '9.2.5']
GOOGLE_SHEETS_VERSION: ['1.2.4', '1.2.0']
name: E2E Tests - Grafana@${{ matrix.GRAFANA_VERSION }} GoogleSheets@${{ matrix.GOOGLE_SHEETS_VERSION }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -31,7 +33,7 @@ jobs:
run: npx playwright install --with-deps chromium

- name: Start Grafana
run: docker run --rm -d -p 3000:3000 --name=grafana --env GF_INSTALL_PLUGINS=grafana-googlesheets-datasource grafana/grafana:${{ matrix.GRAFANA_VERSION }}; sleep 30
run: docker run --rm -d -p 3000:3000 --name=grafana --volume ./packages/plugin-e2e/provisioning:/etc/grafana/provisioning --env GOOGLE_JWT_FILE=${{secrets.GOOGLE_JWT_FILE}} --env "GF_INSTALL_PLUGINS=grafana-googlesheets-datasource ${{matrix.GOOGLE_SHEETS_VERSION}}" grafana/grafana:${{ matrix.GRAFANA_VERSION }}; sleep 30

- name: Run Playwright tests
run: npm run playwright:test --w @grafana/plugin-e2e
Expand All @@ -41,6 +43,6 @@ jobs:
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-${{ matrix.GRAFANA_VERSION }}
name: playwright-report-Grafana${{ matrix.GRAFANA_VERSION }}-GoogleSheets${{ matrix.GOOGLE_SHEETS_VERSION }}
path: packages/plugin-e2e/playwright-report/
retention-days: 30
3 changes: 2 additions & 1 deletion packages/plugin-e2e/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ services:
grafana:
image: grafana/${GRAFANA_IMAGE:-grafana-enterprise}:${GRAFANA_VERSION:-main}
environment:
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-googlesheets-datasource
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-googlesheets-datasource 1.2.4
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_ANONYMOUS_ORG_NAME=Main Org.
- GF_AUTH_ANONYMOUS_ORG_ID=1
- GOOGLE_JWT_FILE=${GOOGLE_JWT_FILE}
ports:
- 3000:3000/tcp
volumes:
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default defineConfig<PluginOptions>({
name: 'authenticate',
testMatch: [/.*auth\.setup\.ts/],
},
// 2. Run all tests in parallel with Chrome.
// 2. Run all tests in parallel using Chrome.
{
name: 'chromium',
use: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# config file version
apiVersion: 1

deleteDatasources:
- name: Google Sheets Service Account
orgId: 1

datasources:
- editable: true
enabled: true
jsonData:
authType: jwt
name: Google Sheets Service Account
secureJsonData:
jwt: ${GOOGLE_JWT_FILE}
type: grafana-googlesheets-datasource
version: 1
57 changes: 54 additions & 3 deletions packages/plugin-e2e/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { test as base, expect as baseExpect } from '@playwright/test';
import { test as base, expect as baseExpect, selectors } from '@playwright/test';
import { E2ESelectors } from './e2e-selectors/types';
import fixtures from './fixtures';
import { DataSourceConfigPage } from './models';
import matchers from './matchers';
import { CreateDataSourcePageArgs } from './types';
import { CreateDataSourceArgs, CreateDataSourcePageArgs, DataSource, ReadProvisionArgs } from './types';
import { PanelEditPage, GrafanaPage, DataSourceConfigPage, DashboardPage } from './models';
import { grafanaE2ESelectorEngine } from './selectorEngine';

export type PluginOptions = {
selectorRegistration: void;
Expand All @@ -23,6 +24,30 @@ export type PluginFixture = {
*/
selectors: E2ESelectors;

/**
* Isolated {@link DashboardPage} instance for each test.
*
* Navigates to a new dashboard page and adds a new panel.
*
* Use {@link PanelEditPage.setVisualization} to change the visualization
* Use {@link PanelEditPage.datasource.set} to change the datasource
* Use {@link PanelEditPage.getQueryEditorEditorRow} to retrieve the query
* editor row locator for a given query refId
*/
newDashboardPage: DashboardPage;

/**
* Isolated {@link PanelEditPage} instance for each test.
*
* Navigates to a new dashboard page, adds a new panel and moves to the panel edit page.
*
* Use {@link PanelEditPage.setVisualization} to change the visualization
* Use {@link PanelEditPage.datasource.set} to change the datasource
* Use {@link ExplorePage.getQueryEditorEditorRow} to retrieve the query
* editor row locator for a given query refId
*/
panelEditPage: PanelEditPage;

/**
* Fixture command that will create an isolated DataSourceConfigPage instance for a given data source type.
*
Expand All @@ -31,6 +56,15 @@ export type PluginFixture = {
*/
createDataSourceConfigPage: (args: CreateDataSourcePageArgs) => Promise<DataSourceConfigPage>;

/**
* Fixture command that creates a data source via the Grafana API.
*
* If you have tests that depend on the the existance of a data source,
* you may use this command in a setup project. Read more about setup projects
* here: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
*/
Copy link
Collaborator

@leventebalogh leventebalogh Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea: I think this comment is ok as it is and already really useful, however maybe in the future we could add some Grafana-related examples which we could link to from here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 100%! Actually this comment was made before I knew that it's possible to use env variables in provisioning files. That kind of makes this comment obsolete. I'll change it. But we should definitely have examples of using provisioning with (and without) secrets in plugin-examples eventually.

createDataSource: (args: CreateDataSourceArgs) => Promise<DataSource>;

/**
* Fixture command that login to Grafana using the Grafana API.
* If the same credentials should be used in every test,
Expand All @@ -56,11 +90,28 @@ export type PluginFixture = {
* test.use({ storageState: { cookies: [], origins: [] } });
*/
login: () => Promise<void>;

/**
* Fixture command that reads a the yaml file for a provisioned dashboard
* or data source and returns it as json.
*/
readProvision<T = any>(args: ReadProvisionArgs): Promise<T>;
};

// extend Playwright with Grafana plugin specific fixtures
export const test = base.extend<PluginFixture & PluginOptions>(fixtures);

export const expect = baseExpect.extend(matchers);

/** Register a custom selector engine that resolves locators for Grafana E2E selectors
*
* The same functionality is available in the {@link GrafanaPage.getByTestIdOrAriaLabel} method. However,
* by registering the selector engine, one can resolve locators by Grafana E2E selectors also within a locator.
*
* Example:
* const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); // returns a locator
* queryEditorRow.locator(`selector=${selectors.components.TimePicker.openButton}`).click();
* */
selectors.register('selector', grafanaE2ESelectorEngine);

export { selectors } from '@playwright/test';
2 changes: 1 addition & 1 deletion packages/plugin-e2e/src/e2e-selectors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type E2ESelectors = {

export type APIs = {
DataSource: {
getResource: string;
resource: string;
healthCheck: string;
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const versionedComponents = {
},
TimePicker: {
openButton: {
'8.1.0': 'data-testid TimePicker open button',
'8.1.0': 'data-testid TimePicker Open Button',
[MIN_GRAFANA_VERSION]: 'TimePicker open button',
},
fromField: 'Time Range from field',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const createDataSourceConfigPage: CreateDataSourceConfigPageFixture = async (
await datasourceConfigPage.goto();
return datasourceConfigPage;
});
deleteDataSource && datasourceConfigPage?.deleteDataSource();
deleteDataSource && (await datasourceConfigPage?.deleteDataSource());
};

export default createDataSourceConfigPage;
22 changes: 22 additions & 0 deletions packages/plugin-e2e/src/fixtures/commands/readProvision.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TestFixture } from '@playwright/test';
import { promises } from 'fs';
import { resolve as resolvePath } from 'path';
import { parse as parseYml } from 'yaml';
import { PluginFixture, PluginOptions } from '../../api';
import { ReadProvisionArgs } from '../../types';
import { PlaywrightCombinedArgs } from '../types';

type ReadProvisionFixture = TestFixture<
<T = any>(args: ReadProvisionArgs) => Promise<T>,
PluginFixture & PluginOptions & PlaywrightCombinedArgs
>;

const readProvision: ReadProvisionFixture = async ({}, use) => {
await use(async ({ filePath }) => {
const resolvedPath = resolvePath(process.cwd(), 'provisioning', filePath);
const contents = await promises.readFile(resolvedPath, 'utf8');
return parseYml(contents);
});
};

export default readProvision;
8 changes: 8 additions & 0 deletions packages/plugin-e2e/src/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import grafanaVersion from './grafanaVersion';
import selectors from './selectors';
import login from './commands/login';
import createDataSourceConfigPage from './commands/createDataSourceConfigPage';
import panelEditPage from './panelEditPage';
import createDataSource from './commands/createDataSource';
import readProvision from './commands/readProvision';
import newDashboardPage from './newDashboardPage';

export default {
selectors,
grafanaVersion,
login,
createDataSourceConfigPage,
newDashboardPage,
panelEditPage,
createDataSource,
readProvision,
};
14 changes: 14 additions & 0 deletions packages/plugin-e2e/src/fixtures/newDashboardPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TestFixture, expect } from '@playwright/test';
import { PluginFixture, PluginOptions } from '../api';
import { DashboardPage } from '../models';
import { PlaywrightCombinedArgs } from './types';

type NewDashboardPageFixture = TestFixture<DashboardPage, PluginFixture & PluginOptions & PlaywrightCombinedArgs>;

const newDashboardPage: NewDashboardPageFixture = async ({ page, request, selectors, grafanaVersion }, use) => {
const newDashboardPage = new DashboardPage({ page, selectors, grafanaVersion, request }, expect);
await newDashboardPage.goto();
await use(newDashboardPage);
};

export default newDashboardPage;
13 changes: 13 additions & 0 deletions packages/plugin-e2e/src/fixtures/panelEditPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TestFixture } from '@playwright/test';
import { PluginFixture, PluginOptions } from '../api';
import { PanelEditPage } from '../models';
import { PlaywrightCombinedArgs } from './types';

type PanelEditPageFixture = TestFixture<PanelEditPage, PluginFixture & PluginOptions & PlaywrightCombinedArgs>;

const panelEditPage: PanelEditPageFixture = async ({ newDashboardPage }, use) => {
const panelEditPage = await newDashboardPage.addPanel();
await use(panelEditPage);
};

export default panelEditPage;
2 changes: 2 additions & 0 deletions packages/plugin-e2e/src/matchers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import toBeOK from './toBeOK';
import toHavePanelError from './toHavePanelError';

export default {
toBeOK,
toHavePanelError,
};
26 changes: 26 additions & 0 deletions packages/plugin-e2e/src/matchers/toHavePanelError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect } from '@playwright/test';
import { PanelError } from '../types';
import { getMessage } from './utils';

const toHavePanelError = async (panelError: PanelError, options?: { timeout?: number }) => {
let pass = true;
let actual;
let message: any = 'A panel error to be displayed';

try {
const numberOfErrors = await panelError.getPanelError().count();
await expect(numberOfErrors).toBe(1);
} catch (_) {
message = getMessage(message, 'No panel error was found on the page');
actual = await panelError.getPanelError().count();
pass = false;
}

return {
message: () => message,
pass,
actual,
};
};

export default toHavePanelError;
65 changes: 65 additions & 0 deletions packages/plugin-e2e/src/models/DashboardPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const gte = require('semver/functions/gte');

import { GotoDashboardArgs, PluginTestCtx } from '../types';

import { Expect } from '@playwright/test';
import { DataSourcePicker } from './DataSourcePicker';
import { GrafanaPage } from './GrafanaPage';
import { PanelEditPage } from './PanelEditPage';
import { TimeRange } from './TimeRange';

export class DashboardPage extends GrafanaPage {
dataSourcePicker: any;
timeRange: TimeRange;

constructor(ctx: PluginTestCtx, expect: Expect<any>, protected readonly dashboardUid?: string) {
super(ctx, expect);
this.dataSourcePicker = new DataSourcePicker(ctx, expect);
this.timeRange = new TimeRange(ctx, this.expect);
}

async goto(opts?: GotoDashboardArgs) {
const uid = opts?.uid || this.dashboardUid;
let url = uid ? this.ctx.selectors.pages.Dashboard.url(uid) : this.ctx.selectors.pages.AddDashboard.url;
if (opts?.queryParams) {
url += `?${opts.queryParams.toString()}`;
}
await this.ctx.page.goto(url, {
waitUntil: 'networkidle',
});
if (opts?.timeRange) {
await this.timeRange.set(opts.timeRange);
}
}

async gotoPanelEditPage(panelId: string) {
const url = this.ctx.selectors.pages.Dashboard.url(this.dashboardUid ?? '');
await this.ctx.page.goto(`${url}?editPanel=${panelId}`, {
waitUntil: 'networkidle',
});
return new PanelEditPage(this.ctx, this.expect);
}

async addPanel(): Promise<PanelEditPage> {
if (gte(this.ctx.grafanaVersion, '10.0.0')) {
//TODO: add new selector and use it in grafana/ui
const title = gte(this.ctx.grafanaVersion, '10.1.0') ? 'Add button' : 'Add panel button';
await this.getByTestIdOrAriaLabel(this.ctx.selectors.components.PageToolbar.itemButton(title)).click();
await this.getByTestIdOrAriaLabel(
this.ctx.selectors.pages.AddDashboard.itemButton('Add new visualization menu item')
).click();
} else {
await this.getByTestIdOrAriaLabel(this.ctx.selectors.pages.AddDashboard.addNewPanel).click();
}

return new PanelEditPage(this.ctx, this.expect);
}

async deleteDashboard() {
await this.ctx.request.delete(`/api/datasources/uid/${this.dashboardUid}`);
}

async refreshDashboard() {
await this.ctx.page.getByTestId(this.ctx.selectors.components.RefreshPicker.runButtonV2).click();
}
}
21 changes: 21 additions & 0 deletions packages/plugin-e2e/src/models/DataSourcePicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Expect } from '@playwright/test';
import { PluginTestCtx } from '../types';
import { GrafanaPage } from './GrafanaPage';

export class DataSourcePicker extends GrafanaPage {
constructor(ctx: PluginTestCtx, expect: Expect<any>) {
super(ctx, expect);
}

async set(name: string) {
await this.getByTestIdOrAriaLabel(this.ctx.selectors.components.DataSourcePicker.container)
.locator('input')
.fill(name);

// this is a hack to get the selection to work in 10.ish versions of Grafana.
// TODO: investigate if the select component can somehow be refactored so that its easier to test with playwright
await this.ctx.page.keyboard.press('ArrowDown');
await this.ctx.page.keyboard.press('ArrowUp');
await this.ctx.page.keyboard.press('Enter');
}
}
Loading
Loading