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

Open
wants to merge 34 commits into
base: main
Choose a base branch
from

Conversation

mckn
Copy link
Collaborator

@mckn mckn commented Oct 29, 2024

What this PR does / why we need it:
This PR introduce an abstraction to interact with the panel edit options. This is useful when developing panel plugins and you want to verify that different combinations of settings works as expected. The goal is to make it easier and more robust to write tests that interacts with the panel editor. This will also give us a layer where we can cater for differences in the panel editor between Grafana versions.

One example of this is the recent change of the switch component that changed role from checkbox to switch. Developers writing tests interacting with the switch directly would need to cater for this in the test which will make it harder and more complex to test your plugin against multiple versions of Grafana.

Note: A more robust way to test this is to provision one panel per setting. So you never interact with the panel editor while running tests. You only test that the setting has the expected behaviour on your panel. This might not be possible in all scenarios e.g. when adding a custom panel editor or having cascading settings.

This is how the configuration of the panel edit options map into the test APIs.

  • addNumberInput -> getNumberInput
  • addSliderInput -> getSliderInput
  • addTextInput -> getTextInput
  • addSelect -> getSelect()
  • addMultiSelect -> getMultiSelect()
  • addRadio -> getRadio()
  • addBooleanSwitch -> getSwitch
  • addColorPicker -> getColorPicker
  • addTimeZonePicker -> getSelect
  • addUnitPicker -> getUnitPicker
  • addFieldNamePicker -> getSelect
  • addDashboardPicker -> getSelect

Which issue(s) this PR fixes:

Fixes #

Special notes for your reviewer:
This is a suggestion and I would like to get more eyes on this to get better naming, structure and ways to interact with the panel editor in your test.

On thing that isn't that great is that while writing the tests you use the "labels" visible in the UI to select options to interact with. This is great for the options that the plugin adds because the plugin controls the labels that they add. But for the standard options or options that is added by Grafana we can still run into the problem that a plugin developer writes a test and interacts with the Standard options label. If we, for some reason, change that in a Grafana version the tests will stop working and the tests will not be able to run across multiple versions of Grafana.

Copy link

github-actions bot commented Oct 29, 2024

Hello! 👋 This repository uses Auto for releasing packages using PR labels.

✨ This PR can be merged but will not trigger a new release. To trigger a new release add the release label before merging.
NOTE: When merging a PR with the release label please avoid merging another PR. For further information see here.

Copy link
Contributor

@sunker sunker left a comment

Choose a reason for hiding this comment

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

Nice! I think I prefer option 1 as option 2 would make us slightly more vulnerable to changes in the core Playwright api.

Question: Would it be better to replace input and switch with getByRole(role: 'input' | 'switch' | ..., { name: string }: Options)? This would make our abstraction look closer to the playwright api?

const group = panelEditPage.getOptionsGroup('Clock panel')
await group.getByRole('switch', { name: 'enableClock' }).uncheck()

@mckn
Copy link
Collaborator Author

mckn commented Oct 29, 2024

Nice! I think I prefer option 1 as option 2 would make us slightly more vulnerable to changes in the core Playwright api.

Question: Would it be better to replace input and switch with getByRole(role: 'input' | 'switch' | ..., { name: string }: Options)? This would make our abstraction look closer to the playwright api?

const group = panelEditPage.getOptionsGroup('Clock panel')
await group.getByRole('switch', { name: 'enableClock' }).uncheck()

Yeah, but on the other hand 'input' is not a role. In the scenario where with the switch-bug you would still need to know if you are trying to fetch a combobox or a switch role.

@mckn
Copy link
Collaborator Author

mckn commented Oct 29, 2024

@sunker I added some more code and tests. Lets have a sync about this before I continue further but it feels kind of OK to work with when writing tests.

@sunker
Copy link
Contributor

sunker commented Oct 30, 2024

Nice! Great tests!

I had a quick look at the helper methods in the panel options API and this is what you can do:

addNestedOptions (can skip this for now)
addNumberInput (textbox)
addSliderInput (not sure, textbox I guess?)
addTextInput (textbox or textarea)
addSelect (combobox)
addMultiSelect (combobox)
addRadio (radiogroup)
addBooleanSwitch (switch in recent versions) 
addColorPicker (don't know - can probably skip for now)
addTimeZonePicker (combobox)
addUnitPicker (comobobox)
addFieldNamePicker (combobox)
addDashboardPicker (combobox) 

So basically we need to support textbox, textarea, combobox, radiogroup and switch. So I guess we'll have to choose between:

group.getByRole(role: textbox | textarea | combobox | radiogroup | switch, options: { ... })
// or implementing each specifically like in this PR. e.g
group.getRadioButtonGroup(label: string)

I don't have strong opinions here. The tricky bit will be to get them to work in >= 8.5.0.

@mckn
Copy link
Collaborator Author

mckn commented Oct 30, 2024

Nice! Great tests!

I had a quick look at the helper methods in the panel options API and this is what you can do:

addNestedOptions (can skip this for now)
addNumberInput (textbox)
addSliderInput (not sure, textbox I guess?)
addTextInput (textbox or textarea)
addSelect (combobox)
addMultiSelect (combobox)
addRadio (radiogroup)
addBooleanSwitch (switch in recent versions) 
addColorPicker (don't know - can probably skip for now)
addTimeZonePicker (combobox)
addUnitPicker (comobobox)
addFieldNamePicker (combobox)
addDashboardPicker (combobox) 

So basically we need to support textbox, textarea, combobox, radiogroup and switch. So I guess we'll have to choose between:

group.getByRole(role: textbox | textarea | combobox | radiogroup | switch, options: { ... })
// or implementing each specifically like in this PR. e.g
group.getRadioButtonGroup(label: string)

I don't have strong opinions here. The tricky bit will be to get them to work in >= 8.5.0.

I'm happy with both as well. The only thing I feel is a bit negative with the getByRole is that it diverge from the standard getByRole since we are introducing "new" roles.

But I'm happy to proceed with that approach as well. Maybe ask for some more input here?

@sunker
Copy link
Contributor

sunker commented Oct 30, 2024

The only thing I feel is a bit negative with the getByRole is that it diverge from the standard getByRole since we are introducing "new" roles.

You mean because they only make up a subset of all roles that exists?

Maybe ask for some more input here?

Sounds good! Maybe Jack? He's affected by the recent change in his traffic light panel. :)

@mckn
Copy link
Collaborator Author

mckn commented Oct 30, 2024

You mean because they only make up a subset of all roles that exists?

Yes, and we would need to have custom roles that match to our components. E.g. in the scenario with the recent change the switch got changed from a 'checkbox' to a 'switch' and in those cases we would not want the developer to need to update their tests. In case they have getByRole('checkbox', ...);.

I understood your suggestion as we don't use the default roles but use our own that maps to our components rather than the html roles. Otherwise we don't resolve issues as the one with the switch.

@mckn mckn self-assigned this Oct 30, 2024
@mckn mckn added plugin-e2e related to the plugin-e2e package minor Increment the minor version when merged labels Oct 30, 2024
@mckn mckn requested a review from jackw October 31, 2024 15:47
@mckn mckn requested a review from sunker December 3, 2024 20:12
@mckn mckn force-pushed the mckn/panel-edit-options-group branch from d073a1d to ef666ca Compare December 3, 2024 20:18
actual = await select
.locator('div[class*="-grafana-select-value-container"]')
.locator('div[class*="-singleValue"]')
.innerText(options);
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there an e2e selector for this? If not, we should add one to Grafana.

One problem with this and the other new matchers is that that they don't have access to the test context. There's no way for them to check grafanaVersion and use the resolved selectors.

Copy link
Contributor

Choose a reason for hiding this comment

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

But IDK - the Selector class has access to the context, so maybe it can be encapsulated there instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hmm, that is a valid concern. But I think we can fix that in the index.ts file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The ctx is available with that the selectors as well. So I will see if I can change these to selectors from the grafana-selectors package istead.

Copy link
Contributor

Choose a reason for hiding this comment

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

How? This is fine in older versions of Grafana, but we need a stable e2e-selector in Grafana to be future proof. Looks like there's a data-testid set here. Can we use that instead of the classnames?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hmm, I don't think we pass the data-testid when rendering the select component. Or I can't find it in the dom.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, can we add a new one then?

packages/plugin-e2e/src/models/components/ColorPicker.ts Outdated Show resolved Hide resolved
packages/plugin-e2e/src/models/components/ComponentBase.ts Outdated Show resolved Hide resolved

export async function openSelect(element: Locator, options?: SelectOptionsType): Promise<Locator> {
await element.getByRole('combobox').click(options);
return element.page().getByLabel('Select options menu', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use selectors.components.Select.menu instead

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, nice. Let's use than in +11.4

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use the selector here too

packages/plugin-e2e/src/models/components/Select.ts Outdated Show resolved Hide resolved
packages/plugin-e2e/src/models/pages/PanelEditPage.ts Outdated Show resolved Hide resolved
packages/plugin-e2e/src/matchers/toBeSwitched.ts Outdated Show resolved Hide resolved
@mckn mckn marked this pull request as ready for review December 10, 2024 09:53
@mckn mckn requested a review from a team as a code owner December 10, 2024 09:53
Copy link
Contributor

@sunker sunker left a comment

Choose a reason for hiding this comment

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

This is starting to look really good!

actual = await select
.locator('div[class*="-grafana-select-value-container"]')
.locator('div[class*="-singleValue"]')
.innerText(options);
Copy link
Contributor

Choose a reason for hiding this comment

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

How? This is fine in older versions of Grafana, but we need a stable e2e-selector in Grafana to be future proof. Looks like there's a data-testid set here. Can we use that instead of the classnames?

packages/plugin-e2e/src/models/components/ColorPicker.ts Outdated Show resolved Hide resolved
packages/plugin-e2e/src/models/components/ColorPicker.ts Outdated Show resolved Hide resolved
}

await this.ctx.page
.locator('#grafana-portal-container')
Copy link
Contributor

Choose a reason for hiding this comment

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

Here as well. This is fine in older versions of Grafana, but now can we add a selector to this element?


export async function openSelect(element: Locator, options?: SelectOptionsType): Promise<Locator> {
await element.getByRole('combobox').click(options);
return element.page().getByLabel('Select options menu', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, nice. Let's use than in +11.4

@mckn mckn requested a review from sunker December 11, 2024 16:59
Copy link
Contributor

@sunker sunker left a comment

Choose a reason for hiding this comment

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

Great stuff! From my standpoint we're ready to ship this, but I think we should try to add proper selector for a few of the elements in Grafana first.

@@ -25,7 +25,7 @@ jobs:
with:
version-resolver-type: plugin-grafana-dependency
grafana-dependency: '>=8.5.0'
# limit: 0 # Uncomment to test all versions since 8.5.0. Useful when testing compatibility for new APIs.
limit: 0
Copy link
Contributor

@sunker sunker Dec 12, 2024

Choose a reason for hiding this comment

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

now that all tests pass, we can revert the change in this file :)

actual = await select
.locator('div[class*="-grafana-select-value-container"]')
.locator('div[class*="-singleValue"]')
.innerText(options);
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, can we add a new one then?

if (gte(this.ctx.grafanaVersion, '8.7.0')) {
return this.ctx.page.locator('#grafana-portal-container');
}
return this.ctx.page.locator('body > div').last();
Copy link
Contributor

Choose a reason for hiding this comment

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

if you could add a new testid in Grafana and use it here that would be great!


export async function openSelect(element: Locator, options?: SelectOptionsType): Promise<Locator> {
await element.getByRole('combobox').click(options);
return element.page().getByLabel('Select options menu', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use the selector here too

}

const id = await this.element.getAttribute('id', options);
return this.group.locator(`label[for='${id}']`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Maybe reverse the order here so that backwards compatibility becomes the "exception" rather than the default case. That way it will be easier to remove legacy code in the future.


private async getOption(selector: string, options?: SelectOptionsType): Promise<Locator> {
const steps = selector.split('>').map((step) => step.trim());
const container = this.ctx.page.locator('div[class="rc-cascader-menus"]');
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we introduce a new data test id here too?


function getPanelContent(grafanaVersion: string, page: Page): Locator {
if (gte(grafanaVersion, '9.5.0')) {
return page.locator('div[class*="-panel-content"]');
Copy link
Contributor

Choose a reason for hiding this comment

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

Can use selectors.components.Panels.Panel.content?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
minor Increment the minor version when merged plugin-e2e related to the plugin-e2e package
Projects
Status: 🧑‍💻 In development
Development

Successfully merging this pull request may close these issues.

2 participants