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

E2E: Improve support to interact with panel edit options #1272

Merged
merged 50 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e0abc09
Added two suggestions on how to implement the selectors for panel edi…
mckn Oct 29, 2024
f82174a
Added some tests and implemented functions for input, radio button gr…
mckn Oct 29, 2024
03ab082
Added support for select.
mckn Oct 30, 2024
95e8075
added some more tests and apis.
mckn Oct 31, 2024
03dbc35
added support for unit picker.
mckn Oct 31, 2024
6a75306
verified time zone.
mckn Oct 31, 2024
4ea35b0
wip - proxy hack.
mckn Nov 4, 2024
ef91a82
wip
mckn Nov 15, 2024
b5a1776
Changed the APIs to be playwright-like.
mckn Dec 3, 2024
6f3f4ac
added unit picker as well.
mckn Dec 3, 2024
ef666ca
removed unused ctx prop.
mckn Dec 3, 2024
3441a14
removed unused import.
mckn Dec 3, 2024
c73e46f
use all supported versions in matrix
sunker Dec 4, 2024
dd03be6
Made it work in 11.3.x
mckn Dec 4, 2024
09e628a
fixed selectors for v11.2.*
mckn Dec 5, 2024
fffa740
made it work for 11.1.*
mckn Dec 5, 2024
82629ba
Fixed for 10.3.*
mckn Dec 5, 2024
858b5fe
Removed comment.
mckn Dec 5, 2024
52fdb3d
supporting 8.5.x
mckn Dec 9, 2024
b8d32a2
Merge branch 'main' into mckn/panel-edit-options-group
mckn Dec 10, 2024
9a76a3a
removed the need of having switch async.
mckn Dec 10, 2024
64160a0
removed private function.
mckn Dec 10, 2024
7bbe0bd
renamed switched to checked.
mckn Dec 10, 2024
c36f555
added ctx.
mckn Dec 10, 2024
44c31f3
minor refactoring to have access to ctx via component base.
mckn Dec 10, 2024
bd9eac6
removed unneccessary check.
mckn Dec 10, 2024
3570835
Merge branch 'main' into mckn/panel-edit-options-group
mckn Dec 11, 2024
1138214
using selector instead.
mckn Dec 11, 2024
94798d1
Merge branch 'main' into mckn/panel-edit-options-group
mckn Dec 11, 2024
8bb16e9
allow any rgb or hex color to be selected.
mckn Dec 11, 2024
a410338
Fixed so the switch works again.
mckn Dec 11, 2024
760ad9e
chaninging to checkbox for switch in 11.4.0
mckn Dec 11, 2024
6d42378
fixed slider in 9.1.8
mckn Dec 11, 2024
57e103c
Merge branch 'main' into mckn/panel-edit-options-group
mckn Dec 11, 2024
b4074de
Merge branch 'main' into mckn/panel-edit-options-group
mckn Jan 7, 2025
065b786
replacing with selectors.
mckn Jan 7, 2025
353554f
Fixed so we open Select menu by using selector.
mckn Jan 8, 2025
c012af2
wip
mckn Jan 8, 2025
8e40456
wip
mckn Jan 8, 2025
b12ea17
simplified the selector utility function.
mckn Jan 8, 2025
2772f84
moving selectors to versioned constants.
mckn Jan 8, 2025
e327491
Merge branch 'main' into mckn/panel-edit-options-group
mckn Jan 8, 2025
e34e12e
Merge branch 'main' into mckn/panel-edit-options-group
mckn Jan 9, 2025
532a475
added locator.
mckn Jan 9, 2025
924242f
Fixed type issues.
mckn Jan 9, 2025
8e5aa3a
updated range of switch.
mckn Jan 9, 2025
6fd8fb8
Merge branch 'main' into mckn/panel-edit-options-group
mckn Jan 10, 2025
7c31d75
Merge branch 'main' into mckn/panel-edit-options-group
mckn Jan 10, 2025
0aa48cb
updated lock file.
mckn Jan 10, 2025
059bc30
Merge branch 'main' into mckn/panel-edit-options-group
mckn Jan 13, 2025
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
1 change: 1 addition & 0 deletions packages/plugin-e2e/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_ANONYMOUS_ORG_NAME=Main Org.
- GF_AUTH_ANONYMOUS_ORG_ID=1
- GF_PANELS_ENABLE_ALPHA=true
- GOOGLE_JWT_FILE=${GOOGLE_JWT_FILE}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
Expand Down
37 changes: 37 additions & 0 deletions packages/plugin-e2e/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ import { GrafanaPage } from './models/pages/GrafanaPage';
import { VariableEditPage } from './models/pages/VariableEditPage';
import { variablePage } from './fixtures/variablePage';
import { gotoVariablePage } from './fixtures/commands/gotoVariablePage';
import { toHaveSelected } from './matchers/toHaveSelected';
import { Select } from './models/components/Select';
import { Switch } from './models/components/Switch';
import { toBeSwitched } from './matchers/toBeSwitched';
import { RadioGroup } from './models/components/RadioGroup';
import { toHaveChecked } from './matchers/toHaveChecked';
import { MultiSelect } from './models/components/MultiSelect';
import { toHaveColor } from './matchers/toHaveColor';
import { ColorPicker, SelectableColors } from './models/components/ColorPicker';

// models
export { DataSourcePicker } from './models/components/DataSourcePicker';
Expand Down Expand Up @@ -92,6 +101,10 @@ export const expect = baseExpect.extend({
toHaveAlert,
toDisplayPreviews,
toBeOK,
toHaveSelected,
toBeSwitched,
toHaveChecked,
toHaveColor,
});

export { selectors } from '@playwright/test';
Expand Down Expand Up @@ -131,6 +144,30 @@ declare global {
* Asserts that a GrafanaPage contains an alert with the specified severity. Use the options to specify the timeout and to filter the alerts.
*/
toHaveAlert(this: Matchers<unknown, GrafanaPage>, severity: AlertVariant, options?: AlertPageOptions): Promise<R>;

/**
* Asserts that a Selector has the specified value selected
*/
toHaveSelected(
select: Select | MultiSelect,
value: string | RegExp | string[] | RegExp[],
options?: ContainTextOptions
): Promise<R>;

/**
* Asserts that a Switch is on or off (on by default)
*/
toBeSwitched(target: Switch, options?: { on?: boolean; timeout?: number }): Promise<R>;

/**
* Asserts that a Radio has expected value selected
*/
toHaveChecked(radioGroup: RadioGroup, expected: string, options?: { timeout?: number }): Promise<R>;

/**
* Asserts that a color picker has expected color selected
*/
toHaveColor(colorPicker: ColorPicker, color: SelectableColors, options?: { timeout?: number }): Promise<R>;
}
}
}
25 changes: 25 additions & 0 deletions packages/plugin-e2e/src/matchers/toBeSwitched.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, MatcherReturnType } from '@playwright/test';
import { getMessage } from './utils';

import { Switch } from '../models/components/Switch';

export async function toBeSwitched(
mckn marked this conversation as resolved.
Show resolved Hide resolved
target: Switch,
options?: { on?: boolean; timeout?: number }
): Promise<MatcherReturnType> {
const expected = options?.on ?? true;
try {
await expect(target.locator()).toBeChecked({ ...options, checked: expected });

return {
pass: true,
expected,
message: () => `Value successfully selected`,
};
} catch (err: unknown) {
return {
message: () => getMessage(expected.toString(), err instanceof Error ? err.toString() : 'Unknown error'),
pass: false,
};
}
}
25 changes: 25 additions & 0 deletions packages/plugin-e2e/src/matchers/toHaveChecked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, MatcherReturnType } from '@playwright/test';
import { getMessage } from './utils';

import { RadioGroup } from '../models/components/RadioGroup';

export async function toHaveChecked(
radioGroup: RadioGroup,
expected: string,
options?: { timeout?: number }
): Promise<MatcherReturnType> {
try {
await expect(radioGroup.locator().getByLabel(expected)).toBeChecked(options);

return {
pass: true,
expected,
message: () => `Value successfully selected`,
};
} catch (err: unknown) {
return {
message: () => getMessage(expected.toString(), err instanceof Error ? err.toString() : 'Unknown error'),
pass: false,
};
}
}
25 changes: 25 additions & 0 deletions packages/plugin-e2e/src/matchers/toHaveColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, MatcherReturnType } from '@playwright/test';
import { getMessage } from './utils';

import { ColorPicker, SelectableColors } from '../models/components/ColorPicker';

export async function toHaveColor(
colorPicker: ColorPicker,
color: SelectableColors,
options?: { timeout?: number }
): Promise<MatcherReturnType> {
try {
await expect(colorPicker.locator().getByRole('textbox')).toHaveValue(color, options);

return {
pass: true,
expected: color,
message: () => `Value successfully selected`,
};
} catch (err: unknown) {
return {
message: () => getMessage(color, err instanceof Error ? err.toString() : 'Unknown error'),
pass: false,
};
}
}
126 changes: 126 additions & 0 deletions packages/plugin-e2e/src/matchers/toHaveSelected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { expect, MatcherReturnType } from '@playwright/test';
import { getMessage } from './utils';
import { ContainTextOptions } from '../types';

import { Select } from '../models/components/Select';
import { MultiSelect } from '../models/components/MultiSelect';
import { UnitPicker } from '../models/components/UnitPicker';

export async function toHaveSelected(
target: Select | MultiSelect | UnitPicker,
value: string | RegExp | string[] | RegExp[],
options?: ContainTextOptions
): Promise<MatcherReturnType> {
if (target instanceof MultiSelect) {
if (Array.isArray(value)) {
return expectMultiSelectToBe(target, value);
}
return expectMultiSelectToBe(target, [value]);
}

if (target instanceof Select) {
if (Array.isArray(value)) {
throw new Error(
`Select only support a single value to be selected. You are asserting that multiple values have been selected: "${value}"`
);
}

return expectSelectToBe(target, value, options);
}

if (target instanceof UnitPicker) {
if (Array.isArray(value)) {
throw new Error(
`UnitPicker only support a single value to be selected. You are asserting that multiple values have been selected: "${value}"`
);
}
return expectUnitPickerToBe(target, value, options);
}

throw Error('Unsupported parameters passed to "toBeSelected"');
}

async function expectSelectToBe(
select: Select,
value: string | RegExp,
options?: ContainTextOptions
): Promise<MatcherReturnType> {
let actual = '';

try {
actual = await select
.locator('div[class*="-grafana-select-value-container"]')
.locator('div[class*="-singleValue"]')
.innerText(options);
mckn marked this conversation as resolved.
Show resolved Hide resolved

expect(actual).toMatch(value);

return {
pass: true,
actual: actual,
expected: value,
message: () => `Value successfully selected`,
};
} catch (err: unknown) {
return {
message: () => getMessage(value.toString(), err instanceof Error ? err.toString() : 'Unknown error'),
pass: false,
actual,
};
}
}

async function expectMultiSelectToBe(select: MultiSelect, values: Array<string | RegExp>): Promise<MatcherReturnType> {
let actual = '';

try {
const actual = await select
.locator('div[class*="-grafana-select-multi-value-container"]')
.locator('div[class*="-grafana-select-multi-value-container"] > div')
.allInnerTexts();

expect(actual).toMatchObject(values);

return {
pass: true,
actual: actual,
expected: values,
message: () => `Values successfully selected`,
};
} catch (err: unknown) {
return {
message: () => getMessage(values.join(', '), err instanceof Error ? err.toString() : 'Unknown error'),
pass: false,
actual,
expected: values,
};
}
}

async function expectUnitPickerToBe(
unitPicker: UnitPicker,
value: string | RegExp,
options?: ContainTextOptions
): Promise<MatcherReturnType> {
let actual = '';

try {
const input = unitPicker.locator().getByRole('textbox');

actual = await input.inputValue(options);
await expect(input).toHaveValue(value);

return {
pass: true,
actual: actual,
expected: value,
message: () => `Value successfully selected`,
};
} catch (err: unknown) {
return {
message: () => getMessage(value.toString(), err instanceof Error ? err.toString() : 'Unknown error'),
pass: false,
actual,
};
}
}
20 changes: 20 additions & 0 deletions packages/plugin-e2e/src/models/components/ColorPicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Locator } from '@playwright/test';
import { PluginTestCtx } from '../../types';
import { ComponentBase } from './ComponentBase';
import { SelectOptionsType } from './types';

export type SelectableColors = 'red' | 'blue' | 'orange' | 'green' | 'yellow';
mckn marked this conversation as resolved.
Show resolved Hide resolved

export class ColorPicker extends ComponentBase {
constructor(private ctx: PluginTestCtx, element: Locator) {
super(element);
}

async selectOption(color: SelectableColors, options?: SelectOptionsType): Promise<void> {
await this.element.getByRole('button').click(options);
await this.ctx.page
.locator('#grafana-portal-container')
mckn marked this conversation as resolved.
Show resolved Hide resolved
.getByRole('button', { name: `${color} color`, exact: true })
.click(options);
}
}
14 changes: 14 additions & 0 deletions packages/plugin-e2e/src/models/components/ComponentBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Locator } from '@playwright/test';

type LocatorParams = Parameters<Locator['locator']>;

export abstract class ComponentBase {
constructor(protected readonly element: Locator) {}
mckn marked this conversation as resolved.
Show resolved Hide resolved

locator(selectorOrLocator?: LocatorParams[0], options?: LocatorParams[1]): Locator {
mckn marked this conversation as resolved.
Show resolved Hide resolved
if (!selectorOrLocator) {
return this.element;
}
return this.element.locator(selectorOrLocator, options);
}
}
20 changes: 20 additions & 0 deletions packages/plugin-e2e/src/models/components/MultiSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Locator } from '@playwright/test';
import { openSelect, selectByValueOrLabel } from './Select';
import { ComponentBase } from './ComponentBase';
import { SelectOptionsType } from './types';

export class MultiSelect extends ComponentBase {
constructor(element: Locator) {
super(element);
}

async selectOptions(values: string[], options?: SelectOptionsType): Promise<string[]> {
const menu = await openSelect(this.element, options);

return Promise.all(
values.map((value) => {
return selectByValueOrLabel(value, menu, options);
})
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Locator } from '@playwright/test';
import { PluginTestCtx } from '../../types';
import { ColorPicker } from './ColorPicker';
import { UnitPicker } from './UnitPicker';
import { Select } from './Select';
import { MultiSelect } from './MultiSelect';
import { Switch } from './Switch';
import { gte } from 'semver';
import { RadioGroup } from './RadioGroup';

export class PanelEditOptionsGroup {
constructor(private ctx: PluginTestCtx, public readonly element: Locator, private groupLabel: string) {}

getRadio(label: string): RadioGroup {
mckn marked this conversation as resolved.
Show resolved Hide resolved
return new RadioGroup(this.getByLabel(label).getByRole('radiogroup'));
}

async getSwitch(label: string): Promise<Switch> {
if (gte(this.ctx.grafanaVersion, '11.4.0')) {
const id = await this.getByLabel(label).getByRole('switch').getAttribute('id');
mckn marked this conversation as resolved.
Show resolved Hide resolved
return new Switch(this.getByLabel(label).locator(`label[for='${id}']`));
}

const id = await this.getByLabel(label).getByRole('checkbox').getAttribute('id');
return new Switch(this.getByLabel(label).locator(`label[for='${id}']`));
}

getTextInput(label: string): Locator {
return this.getByLabel(label).getByRole('textbox');
}

getNumberInput(label: string): Locator {
return this.getByLabel(label).getByRole('spinbutton');
}

getSliderInput(label: string): Locator {
return this.getNumberInput(label);
}
mckn marked this conversation as resolved.
Show resolved Hide resolved

getSelect(label: string): Select {
return new Select(this.getByLabel(label));
}

getMultiSelect(label: string): MultiSelect {
return new MultiSelect(this.getByLabel(label));
}

getColorPicker(label: string): ColorPicker {
return new ColorPicker(this.ctx, this.getByLabel(label));
}

getUnitPicker(label: string): UnitPicker {
return new UnitPicker(this.ctx, this.getByLabel(label));
}

private getByLabel(optionLabel: string): Locator {
return this.element.getByLabel(`${this.groupLabel} ${optionLabel} field property editor`);
}
}
Loading
Loading