Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
13 changes: 13 additions & 0 deletions packages/plugin-e2e/src/fixtures/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TestFixture } from '@playwright/test';
import { PlaywrightArgs } from '../types';
import { Components } from '../models/Components';

type ComponentsFixture = TestFixture<Components, PlaywrightArgs>;

export const components: ComponentsFixture = async (
{ request, page, selectors, grafanaVersion },
use,
testInfo
) => {
await use(new Components({ page, selectors, grafanaVersion, request, testInfo }));
};
4 changes: 4 additions & 0 deletions packages/plugin-e2e/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
A11yViolationsOptions,
} from './types';
import { annotationEditPage } from './fixtures/annotationEditPage';
import { components } from './fixtures/components';
import { grafanaAPIClient } from './fixtures/grafanaAPIClient';
import { createDataSource } from './fixtures/commands/createDataSource';
import { createDataSourceConfigPage } from './fixtures/commands/createDataSourceConfigPage';
Expand Down Expand Up @@ -62,7 +63,9 @@ import { toHavePanelErrors } from './matchers/toHavePanelErrors';
import { DashboardPage } from './models/pages/DashboardPage';

// models
export { Components } from './models/Components';
export { DataSourcePicker } from './models/components/DataSourcePicker';
export { ScopedComponent } from './models/components/ScopedComponent';
export { Panel } from './models/components/Panel';
export { TimeRange } from './models/components/TimeRange';
export { AnnotationEditPage } from './models/pages/AnnotationEditPage';
Expand Down Expand Up @@ -120,6 +123,7 @@ export const test = testWithInternal.extend<PluginFixture, PluginOptions>({
gotoVariablePage,
gotoAnnotationEditPage,
gotoAlertRuleEditPage,
components,
gotoDataSourceConfigPage,
Comment thread
mckn marked this conversation as resolved.
gotoAppConfigPage,
gotoAppPage,
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-e2e/src/models/Components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PluginTestCtx } from '../types';
import { DataSourcePicker } from './components/DataSourcePicker';
import { TimeRange } from './components/TimeRange';

/**
* Factory for components that are not attached to a specific page.
*
* Use this when you need to interact with a Grafana UI component on a page
* that is not covered by one of the page fixtures (e.g. {@link PanelEditPage}
* or {@link ExplorePage}).
*
* To scope a component to a sub-tree of the DOM, use `within(root)`:
*
* @example
* ```typescript
* await components.dataSourcePicker.set('prom');
* await components.dataSourcePicker.within(panel).set('prom');
* ```
*/
export class Components {
readonly dataSourcePicker: DataSourcePicker;
readonly timeRangePicker: TimeRange;

constructor(ctx: PluginTestCtx) {
this.dataSourcePicker = new DataSourcePicker(ctx);
Comment thread
mckn marked this conversation as resolved.
this.timeRangePicker = new TimeRange(ctx);
}
}
14 changes: 3 additions & 11 deletions packages/plugin-e2e/src/models/components/DataSourcePicker.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import * as semver from 'semver';
import { Locator, expect } from '@playwright/test';
import { PluginTestCtx } from '../../types';
import { GrafanaPage } from '../pages/GrafanaPage';

export class DataSourcePicker extends GrafanaPage {
constructor(
ctx: PluginTestCtx,
private root?: Locator
) {
super(ctx);
}
import { expect } from '@playwright/test';
import { ScopedComponent } from './ScopedComponent';

export class DataSourcePicker extends ScopedComponent {
/**
* Sets the data source picker to the provided name
*/
Expand Down
25 changes: 25 additions & 0 deletions packages/plugin-e2e/src/models/components/ScopedComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Locator } from '@playwright/test';
import { PluginTestCtx } from '../../types';
import { GrafanaPage } from '../pages/GrafanaPage';

/**
* Base class for components that live at the page level but can optionally
* be scoped to a sub-tree of the DOM via `within()`.
*/
export abstract class ScopedComponent extends GrafanaPage {
constructor(
ctx: PluginTestCtx,
protected readonly root?: Locator
) {
super(ctx);
}

/**
* Returns a new instance of this component scoped to the given root locator.
* Mirrors Playwright's `locator.locator(...)` chaining pattern.
*/
within(root: Locator): this {
const Ctor = this.constructor as new (ctx: PluginTestCtx, root?: Locator) => this;
return new Ctor(this.ctx, root);
}
}
12 changes: 4 additions & 8 deletions packages/plugin-e2e/src/models/components/TimeRange.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import * as semver from 'semver';
import { PluginTestCtx, TimeRangeArgs } from '../../types';
import { GrafanaPage } from '../pages/GrafanaPage';

export class TimeRange extends GrafanaPage {
constructor(ctx: PluginTestCtx) {
super(ctx);
}
import { TimeRangeArgs } from '../../types';
import { ScopedComponent } from './ScopedComponent';

export class TimeRange extends ScopedComponent {
/**
* Opens the time picker and sets the time range to the provided values
*/
async set({ from, to, zone }: TimeRangeArgs) {
const { TimeZonePicker, TimePicker, Select } = this.ctx.selectors.components;
try {
await this.getByGrafanaSelector(TimePicker.openButton).click();
await this.getByGrafanaSelector(TimePicker.openButton, { root: this.root }).click();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Curious why we need this change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It is because you can do the within which change the scope of the time picker. It should try to find the time picker within that new root element instead of the page.

Does this make sense?

} catch (e) {
// seems like in older versions of Grafana the time picker markup is rendered twice
await this.ctx.page.locator('[aria-controls="TimePickerContent"]').last().click();
Expand Down
17 changes: 17 additions & 0 deletions packages/plugin-e2e/src/models/pages/DashboardPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ export class DashboardPage extends GrafanaPage {
return super.navigate(url, options);
}

/**
* Returns a locator for the dashboard toolbar area that contains the time range controls.
*
* - Grafana ≥ 11.1.0: resolves to `Dashboard.Controls` (scenes-based dashboard controls bar)
* - Grafana 9.4.0–11.0.x: resolves to `NavToolbar.container`
* - Grafana < 9.4.0: falls back to `.page-toolbar`
*/
get toolbar() {
const { components, pages } = this.ctx.selectors;
if (semver.gte(this.ctx.grafanaVersion, '11.1.0')) {
return this.getByGrafanaSelector(pages.Dashboard.Controls);
}
return semver.gte(this.ctx.grafanaVersion, '9.4.0')
? this.getByGrafanaSelector(components.NavToolbar.container)
: this.ctx.page.locator('.page-toolbar');
}

/**
* Scrolls the page viewport-by-viewport to trigger below-fold panel queries.
*
Expand Down
17 changes: 17 additions & 0 deletions packages/plugin-e2e/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AlertRuleEditPage } from './models/pages/AlertRuleEditPage';
import { AnnotationEditPage } from './models/pages/AnnotationEditPage';
import { AppConfigPage } from './models/pages/AppConfigPage';
import { AppPage } from './models/pages/AppPage';
import { Components } from './models/Components';
import { DashboardPage } from './models/pages/DashboardPage';
import { DataSourceConfigPage } from './models/pages/DataSourceConfigPage';
import { ExplorePage } from './models/pages/ExplorePage';
Expand Down Expand Up @@ -433,6 +434,22 @@ export type PluginFixture = {
* You can use this in conjunction with the .toHaveNoA11yViolations matcher to assert that there are no accessibility violations on the page.
*/
scanForA11yViolations: (context?: AxeScanContext) => Promise<AxeResults>;

/**
* Factory for components that are not attached to a specific page.
*
* Use this fixture to interact with a Grafana UI component on a page that is
* not covered by other page fixtures (e.g. {@link PanelEditPage} or {@link ExplorePage}).
*
Comment thread
mckn marked this conversation as resolved.
* @example
* ```typescript
* test('my test', async ({ components }) => {
* await components.dataSourcePicker.set('gdev-prometheus');
* await components.dataSourcePicker.within(someLocator).set('gdev-tempo');
* });
* ```
*/
components: Components;
Comment thread
mckn marked this conversation as resolved.
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect, test } from '../../../src';

test('components.dataSourcePicker should set the data source', async ({
panelEditPage,
components,
readProvisionedDataSource,
}) => {
const ds = await readProvisionedDataSource({ fileName: 'testdatasource.yaml' });
await components.dataSourcePicker.set(ds.name);
await expect(panelEditPage.getQueryEditorRow('A').getByRole('textbox', { name: 'Query Text' })).toBeVisible();
});

test('components.dataSourcePicker.within should set the data source when scoped to a root locator', async ({
panelEditPage,
components,
readProvisionedDataSource,
selectors,
}) => {
const ds = await readProvisionedDataSource({ fileName: 'testdatasource.yaml' });
const root = panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content);
await components.dataSourcePicker.within(root).set(ds.name);
await expect(panelEditPage.getQueryEditorRow('A').getByRole('textbox', { name: 'Query Text' })).toBeVisible();
});

test('components.timeRangePicker should set the time range', async ({
panelEditPage,
components,
selectors,
}) => {
await components.timeRangePicker.set({ from: '2020-01-01 00:00:00', to: '2020-01-02 00:00:00' });
// older Grafana versions render the time picker twice, so we use .first() to avoid strict mode violations
const openButton = panelEditPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).first();
await expect(openButton).toContainText('2020-01-01 00:00:00');
});

test('components.timeRangePicker.within should set the time range when scoped to a root locator', async ({
gotoDashboardPage,
readProvisionedDashboard,
components,
selectors,
}) => {
const dashboard = await readProvisionedDashboard({ fileName: 'testdatasource.json' });
const dashboardPage = await gotoDashboardPage(dashboard);
await components.timeRangePicker.within(dashboardPage.toolbar).set({ from: '2020-01-01 00:00:00', to: '2020-01-02 00:00:00' });
const openButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).first();
await expect(openButton).toContainText('2020-01-01 00:00:00');
});
Loading