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 data source config page #546

Merged
merged 19 commits into from
Nov 20, 2023
Merged
4 changes: 4 additions & 0 deletions .github/user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"cookies": [],
"origins": []
}
11 changes: 8 additions & 3 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ jobs:
strategy:
fail-fast: false
matrix:
GRAFANA_VERSION: ['latest', '9.5.5']
GRAFANA_VERSION: ['latest', '10.0.5', '9.5.5', '9.2.5']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Copy auth file
run: mkdir -p packages/plugin-e2e/playwright/.auth/ && cp .github/user.json packages/plugin-e2e/playwright/.auth/user.json
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wish playwright could create this file if it doesn't already exist, but doesn't seem like it's possible.


- name: Setup Node.js environment
uses: actions/setup-node@v3

Expand All @@ -28,14 +31,16 @@ jobs:
run: npx playwright install --with-deps chromium

- name: Start Grafana
run: docker run --rm -d -p 3000:3000 --name=grafana grafana/grafana:${{ matrix.GRAFANA_VERSION }}; sleep 30
run: docker run --rm -d -p 3000:3000 --name=grafana --env GF_INSTALL_PLUGINS=grafana-googlesheets-datasource grafana/grafana:${{ matrix.GRAFANA_VERSION }}; sleep 30

- name: Run Playwright tests
run: npm run playwright:test --w @grafana/plugin-e2e
env:
GOOGLE_JWT_FILE: ${{ secrets.GOOGLE_JWT_FILE }}

- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-${{ matrix.GRAFANA_VERSION }}
path: playwright-report/
path: packages/plugin-e2e/playwright-report/
retention-days: 30
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
.env

# Generated files
.docusaurus
Expand Down
4,189 changes: 1,362 additions & 2,827 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/plugin-e2e/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GOOGLE_JWT_FILE=
Copy link
Contributor Author

Choose a reason for hiding this comment

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

secret added to the repo. can also be found in plugin-provisioning repo if you want to try this locally (or provide your own ofc).

4 changes: 2 additions & 2 deletions packages/plugin-e2e/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ services:
grafana:
image: grafana/${GRAFANA_IMAGE:-grafana-enterprise}:${GRAFANA_VERSION:-main}
environment:
- GF_INSTALL_PLUGINS=grafana-clock-panel
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-googlesheets-datasource
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_ANONYMOUS_ORG_NAME=Main Org.
- GF_AUTH_ANONYMOUS_ORG_ID=1
ports:
- 3001:3000/tcp
- 3000:3000/tcp
volumes:
- ./provisioning:/etc/grafana/provisioning
7 changes: 6 additions & 1 deletion packages/plugin-e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
},
"devDependencies": {
"@playwright/test": "^1.39.0",
"semver": "^7.5.4"
"@types/uuid": "^9.0.7",
"dotenv": "^16.3.1"
},
"dependencies": {
"semver": "^7.5.4",
"uuid": "^9.0.1"
}
}
11 changes: 11 additions & 0 deletions packages/plugin-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import { defineConfig, devices } from '@playwright/test';
import { PluginOptions } from './src';
import dotenv from 'dotenv';

dotenv.config();

export default defineConfig<PluginOptions>({
testDir: './tests',
Expand Down Expand Up @@ -30,11 +33,19 @@ export default defineConfig<PluginOptions>({

/* List of projects to run. See https://playwright.dev/docs/test-configuration#projects */
projects: [
// 1. Login to Grafana and store the cookie on disk for use in other tests.
{
name: 'authenticate',
testMatch: [/.*auth\.setup\.ts/],
},
// 2. Run all tests in parallel with Chrome.
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['authenticate'],
},
],
});
43 changes: 41 additions & 2 deletions packages/plugin-e2e/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { test as base } from '@playwright/test';
import { test as base, expect as baseExpect } 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';

export type PluginOptions = {
selectorRegistration: void;
Expand All @@ -19,9 +22,45 @@ export type PluginFixture = {
* The E2E selectors to use for the current version of Grafana
*/
selectors: E2ESelectors;

/**
* Fixture command that will create an isolated DataSourceConfigPage instance for a given data source type.
*
* The data source config page cannot be navigated to without a data source uid, so this fixture will create a new
* data source using the Grafana API, create a new DataSourceConfigPage instance and navigate to the page.
*/
createDataSourceConfigPage: (args: CreateDataSourcePageArgs) => Promise<DataSourceConfigPage>;

/**
* Fixture command that login to Grafana using the Grafana API.
* If the same credentials should be used in every test,
* invoke this fixture in a setup project.
* See https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
*
* If no credentials are provided, the default admin/admin credentials will be used.
*
* The default credentials can be overridden in the playwright.config.ts file:
* eg.
* export default defineConfig({
use: {
httpCredentials: {
username: 'user',
password: 'pass',
},
},
});
*
* To override credentials in a single test:
* test.use({ httpCredentials: { username: 'admin', password: 'admin' } });
* To avoid authentication in a single test:
* test.use({ storageState: { cookies: [], origins: [] } });
*/
login: () => Promise<void>;
};

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

export { selectors, expect } from '@playwright/test';
export const expect = baseExpect.extend(matchers);

export { selectors } from '@playwright/test';
1 change: 1 addition & 0 deletions packages/plugin-e2e/src/e2e-selectors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type E2ESelectors = {
export type APIs = {
DataSource: {
getResource: string;
healthCheck: string;
};
};

Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-e2e/src/e2e-selectors/versioned/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ export const versionedAPIs = {
query: {
[MIN_GRAFANA_VERSION]: '*/**/api/ds/query*',
},
healthCheck: {
[MIN_GRAFANA_VERSION]: 'api/datasources/uid/*/health',
},
},
};
5 changes: 4 additions & 1 deletion packages/plugin-e2e/src/e2e-selectors/versioned/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export const versionedPages = {
dataSources: (dataSourceName: string) => `Data source list item ${dataSourceName}`,
},
EditDataSource: {
url: (dataSourceUid: string) => `/datasources/edit/${dataSourceUid}`,
url: {
'10.2.0': (dataSourceUid: string) => `/connections/datasources/edit/${dataSourceUid}`,
[MIN_GRAFANA_VERSION]: (dataSourceUid: string) => `/datasources/edit/${dataSourceUid}`,
},
settings: 'Datasource settings page basic settings',
},
AddDataSource: {
Expand Down
50 changes: 50 additions & 0 deletions packages/plugin-e2e/src/fixtures/commands/createDataSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { v4 as uuidv4 } from 'uuid';
import { APIRequestContext, expect, TestFixture } from '@playwright/test';
import { PluginFixture, PluginOptions } from '../../api';
import { CreateDataSourceArgs, DataSource } from '../../types';
import { PlaywrightCombinedArgs } from '../types';

type CreateDataSourceViaAPIFixture = TestFixture<
(args: CreateDataSourceArgs) => Promise<DataSource>,
PluginFixture & PluginOptions & PlaywrightCombinedArgs
>;

export const createDataSourceViaAPI = async (
request: APIRequestContext,
datasource: DataSource
): Promise<DataSource> => {
const { type, name } = datasource;
const dsName = name ?? `${type}-${uuidv4()}`;

const existingDataSource = await request.get(`/api/datasources/name/${dsName}`);
if (await existingDataSource.ok()) {
const json = await existingDataSource.json();
await request.delete(`/api/datasources/uid/${json.uid}`);
}

const createDsReq = await request.post('/api/datasources', {
data: {
...datasource,
name: dsName,
access: 'proxy',
isDefault: false,
},
});
const text = await createDsReq.text();
const status = await createDsReq.status();
if (status === 200) {
console.log('Data source created: ', name);
return createDsReq.json().then((r) => r.datasource);
}

expect.soft(createDsReq.ok(), `Failed to create data source: ${text}`).toBeTruthy();
return existingDataSource.json();
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code here is pretty rough and probably doesn't cover all scenarios...will probably come back to this in upcoming PRs.


const createDataSource: CreateDataSourceViaAPIFixture = async ({ request }, use) => {
await use(async (args) => {
return createDataSourceViaAPI(request, args.datasource);
});
};

export default createDataSource;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, TestFixture } from '@playwright/test';
import { PluginFixture, PluginOptions } from '../../api';
import { CreateDataSourcePageArgs } from '../../types';
import { PlaywrightCombinedArgs } from '../types';
import { DataSourceConfigPage } from '../../models';
import { createDataSourceViaAPI } from './createDataSource';

type CreateDataSourceConfigPageFixture = TestFixture<
(args: CreateDataSourcePageArgs) => Promise<DataSourceConfigPage>,
PluginFixture & PluginOptions & PlaywrightCombinedArgs
>;

const createDataSourceConfigPage: CreateDataSourceConfigPageFixture = async (
{ request, page, selectors, grafanaVersion },
use
) => {
let datasourceConfigPage: DataSourceConfigPage | undefined;
let deleteDataSource = true;
await use(async (args) => {
deleteDataSource = args.deleteDataSourceAfterTest ?? true;
const datasource = await createDataSourceViaAPI(request, args);
datasourceConfigPage = new DataSourceConfigPage(
{ page, selectors, grafanaVersion, request },
expect,
datasource.uid
);
await datasourceConfigPage.goto();
return datasourceConfigPage;
});
deleteDataSource && datasourceConfigPage?.deleteDataSource();
};

export default createDataSourceConfigPage;
20 changes: 20 additions & 0 deletions packages/plugin-e2e/src/fixtures/commands/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from 'path';
import { expect, TestFixture } from '@playwright/test';
import { PluginFixture, PluginOptions } from '../../api';
import { PlaywrightCombinedArgs } from '../types';

const authFile = 'playwright/.auth/user.json';

type LoginFixture = TestFixture<() => Promise<void>, PluginFixture & PluginOptions & PlaywrightCombinedArgs>;

const login: LoginFixture = async ({ request, httpCredentials }, use) => {
await use(async () => {
const data = httpCredentials ? { ...httpCredentials, user: 'admin' } : { user: 'admin', password: 'admin' };
const loginReq = await request.post('/login', { data });
const text = await loginReq.text();
expect.soft(loginReq.ok(), `Could not log in to Grafana: ${text}`).toBeTruthy();
await request.storageState({ path: path.join(process.cwd(), authFile) });
});
};

export default login;
4 changes: 4 additions & 0 deletions packages/plugin-e2e/src/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import grafanaVersion from './grafanaVersion';
import selectors from './selectors';
import login from './commands/login';
import createDataSourceConfigPage from './commands/createDataSourceConfigPage';

export default {
selectors,
grafanaVersion,
login,
createDataSourceConfigPage,
};
5 changes: 5 additions & 0 deletions packages/plugin-e2e/src/matchers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import toBeOK from './toBeOK';

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

const toBeOK = async (request: Promise<Response>) => {
let pass = false;
let actual;
let message: any = 'Response status code is within 200..299 range.';

try {
const response = await request;
return {
message: () => getMessage(message, response.status().toString()),
pass: response.ok(),
actual: response.status(),
};
} catch (err: unknown) {
return {
message: () => getMessage(message, err instanceof Error ? err.toString() : 'Unknown error'),
pass,
actual,
};
}
};

export default toBeOK;
1 change: 1 addition & 0 deletions packages/plugin-e2e/src/matchers/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getMessage = (expected: string, received: string) => `Expected: ${expected}\nReceived: ${received}`;
35 changes: 35 additions & 0 deletions packages/plugin-e2e/src/models/DataSourceConfigPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Expect } from '@playwright/test';
import { PluginTestCtx } from '../types';
import { GrafanaPage } from './GrafanaPage';

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

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

async goto() {
await this.ctx.page.goto(this.ctx.selectors.pages.EditDataSource.url(this.uid), {
waitUntil: 'load',
});
}

/**
* Mocks the response of the datasource health check call
* @param json the json response to return
* @param status the HTTP status code to return. Defaults to 200
*/
async mockHealthCheckResponse<T = any>(json: T, status = 200) {
await this.ctx.page.route(`${this.ctx.selectors.apis.DataSource.healthCheck}`, async (route) => {
await route.fulfill({ json, status });
});
}

async saveAndTest() {
await this.getByTestIdOrAriaLabel(this.ctx.selectors.pages.DataSource.saveAndTest).click();
return this.ctx.page.waitForResponse((resp) => resp.url().includes('/health'));
}
}
Loading
Loading