[9.0] [Scout] add painless lab (#219124)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Scout] add painless
lab](https://github.com/elastic/kibana/pull/216446)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Charis
Kalpakis","email":"39087493+fake-haris@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-04-24T15:19:23Z","message":"[Scout]
add painless
lab","sha":"2713a79ba90500abf47d0cc73d7749410cc55b23","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:version","test:scout","v9.1.0","v8.19.0","v9.0.1"],"title":"[Scout]
add painless
lab","number":216446,"url":"https://github.com/elastic/kibana/pull/216446","mergeCommit":{"message":"[Scout]
add painless
lab","sha":"2713a79ba90500abf47d0cc73d7749410cc55b23"}},"sourceBranch":"main","suggestedTargetBranches":["8.19","9.0"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/216446","number":216446,"mergeCommit":{"message":"[Scout]
add painless
lab","sha":"2713a79ba90500abf47d0cc73d7749410cc55b23"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: Charis Kalpakis <39087493+fake-haris@users.noreply.github.com>
Co-authored-by: fake-haris <charis.kalpakis@elastic.co>
This commit is contained in:
Kibana Machine 2025-04-25 15:17:52 +02:00 committed by GitHub
parent eb6f9d7ff2
commit bb1dbf6f98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 219 additions and 8 deletions

View file

@ -5,5 +5,6 @@ ui_tests:
- discover_enhanced
- maps
- observability_onboarding
- painless_lab
- security_solution
disabled:

1
.github/CODEOWNERS vendored
View file

@ -1601,6 +1601,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
/x-pack/platform/plugins/shared/maps/ui_tests @elastic/appex-qa # temporarily
/x-pack/platform/plugins/private/discover_enhanced/ui_tests/ @elastic/appex-qa # temporarily
/x-pack/platform/plugins/private/discover_enhanced/ui_tests/tests/discover_cdp_perf.spec.ts @elastic/kibana-data-discovery # test tracks bundle size limits
/x-pack/platform/plugins/private/painless_lab/ui_tests # temporarily
/x-pack/test/functional/fixtures/package_registry_config.yml @elastic/appex-qa # No usages found
/x-pack/test/functional/fixtures/kbn_archiver/packaging.json @elastic/appex-qa # No usages found
/x-pack/test/functional/es_archives/filebeat @elastic/appex-qa

View file

@ -50,7 +50,7 @@ export const OutputPane: FunctionComponent<Props> = ({ isLoading, response }) =>
<div className="painlessLabRightPane">
<EuiTabbedContent
className="painlessLabRightPane__tabs"
data-test-subj="painlessTabs"
data-test-subj={isLoading ? `painlessTabs-loading` : `painlessTabs-loaded`}
size="s"
tabs={[
{

View file

@ -7,6 +7,7 @@
"common/**/*",
"public/**/*",
"server/**/*",
"ui_tests/**/*",
],
"kbn_references": [
"@kbn/core",
@ -21,6 +22,7 @@
"@kbn/config-schema",
"@kbn/code-editor",
"@kbn/react-kibana-context-render",
"@kbn/scout",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,16 @@
## How to run tests
First start the servers with
```bash
// ESS
node scripts/scout.js start-server --stateful
```
Then you can run the tests multiple times in another terminal with:
```bash
// ESS
npx playwright test --config x-pack/platform/plugins/private/painless_lab/ui_tests/playwright.config.ts --project local --grep @ess
```
Test results are available in `x-pack/platform/plugins/private/painless_lab/ui_tests/output`

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { test as base } from '@kbn/scout';
import type { ScoutPage, ScoutTestFixtures, ScoutWorkerFixtures } from '@kbn/scout';
import { extendPageObjects, PainlessLabPageObjects } from './page_objects';
export interface PainlessLabTestFixtures extends ScoutTestFixtures {
pageObjects: PainlessLabPageObjects;
}
export const test = base.extend<PainlessLabTestFixtures, ScoutWorkerFixtures>({
pageObjects: async (
{
pageObjects,
page,
}: {
pageObjects: PainlessLabPageObjects;
page: ScoutPage;
},
use: (pageObjects: PainlessLabPageObjects) => Promise<void>
) => {
const extendedPageObjects = extendPageObjects(pageObjects, page);
await use(extendedPageObjects);
},
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PageObjects, ScoutPage, createLazyPageObject } from '@kbn/scout';
import { PainlessLab } from './painless_lab_page';
export interface PainlessLabPageObjects extends PageObjects {
painlessLab: PainlessLab;
}
export function extendPageObjects(
pageObjects: PageObjects,
page: ScoutPage
): PainlessLabPageObjects {
return {
...pageObjects,
painlessLab: createLazyPageObject(PainlessLab, page),
};
}

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Locator } from '@kbn/scout';
import { ScoutPage } from '@kbn/scout/src/playwright';
export class PainlessLab {
public editorOutputPane: Locator;
public requestFlyoutHeader: Locator;
public viewRequestButton: Locator;
public flyoutResponseTab: Locator;
constructor(private readonly page: ScoutPage) {
this.editorOutputPane = this.page.testSubj.locator('painlessTabs-loaded');
this.requestFlyoutHeader = this.page.testSubj.locator('painlessLabRequestFlyoutHeader');
this.viewRequestButton = this.page.testSubj.locator('btnViewRequest');
this.flyoutResponseTab = this.page.locator('#response');
}
async goto() {
return this.page.gotoApp('dev_tools', { hash: 'painless_lab' });
}
async waitForEditorToLoad() {
// wait for page to be rendered
await this.page.testSubj.locator('kibanaCodeEditor').waitFor({ state: 'visible' });
await this.editorOutputPane.waitFor({ state: 'visible' });
}
async setCodeEditorValue(value: string, nthIndex?: number): Promise<void> {
await this.page.evaluate(
({ editorIndex, codeEditorValue }: { editorIndex?: number; codeEditorValue: string }) => {
const editor = (window.MonacoEnvironment as any)!.monaco!.editor;
const textModels = editor.getModels();
if (editorIndex !== undefined) {
textModels[editorIndex].setValue(codeEditorValue);
} else {
textModels.forEach((model: { setValue: (arg0: string) => any }) =>
model.setValue(codeEditorValue)
);
}
},
{ editorIndex: nthIndex, codeEditorValue: value }
);
}
async getFlyoutRequestBody() {
return this.page.testSubj.locator('painlessLabFlyoutRequest').innerText();
}
async getFlyoutResponseBody() {
const flyoutResponse = this.page.testSubj.locator('painlessLabFlyoutResponse');
await flyoutResponse.waitFor({ state: 'visible' });
return flyoutResponse.innerText();
}
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createPlaywrightConfig } from '@kbn/scout';
// eslint-disable-next-line import/no-default-export
export default createPlaywrightConfig({
testDir: './tests',
});

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { expect, tags } from '@kbn/scout';
import { test } from '../fixtures';
const space = ' ';
const TEST_SCRIPT_RESULT = '45';
const UPDATED_TEST_SCRIPT_RESPONSE = '"45"';
const TEST_SCRIPT = `
int total = 0;
for (int i = 0; i < 10; ++i) {
total += i;
}
return total;
`.trim();
const TEST_SCRIPT_REQUEST = `POST _scripts/painless/_execute
{
"script": {
"source": """int total = 0;
${space}
for (int i = 0; i < 10; ++i) {
total += i;
}
${space}
return total;""",
"params": {
"string_parameter": "string value",
"number_parameter": 1.5,
"boolean_parameter": true
}
}
}`;
test.describe('Painless Lab', { tag: tags.ESS_ONLY }, () => {
test.beforeEach(async ({ browserAuth, pageObjects }) => {
await browserAuth.loginAsAdmin();
await pageObjects.painlessLab.goto();
await pageObjects.painlessLab.waitForEditorToLoad();
});
test('validate painless lab editor and request', async ({ pageObjects }) => {
await pageObjects.painlessLab.setCodeEditorValue(TEST_SCRIPT);
await pageObjects.painlessLab.editorOutputPane.waitFor({ state: 'visible' });
await expect(pageObjects.painlessLab.editorOutputPane).toContainText(TEST_SCRIPT_RESULT);
await pageObjects.painlessLab.viewRequestButton.click();
await expect(pageObjects.painlessLab.requestFlyoutHeader).toBeVisible();
expect(await pageObjects.painlessLab.getFlyoutRequestBody()).toBe(TEST_SCRIPT_REQUEST);
await pageObjects.painlessLab.flyoutResponseTab.click();
expect(await pageObjects.painlessLab.getFlyoutResponseBody()).toBe(
UPDATED_TEST_SCRIPT_RESPONSE
);
});
});

View file

@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('click on the output button', async () => {
const painlessTabsOutput = await find.byCssSelector(
'[data-test-subj="painlessTabs"] #output'
'[data-test-subj="painlessTabs-loaded"] #output'
);
await painlessTabsOutput.click();
await a11y.testAppSnapshot();
@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('click on the parameters button', async () => {
const painlessTabsParameters = await find.byCssSelector(
'[data-test-subj="painlessTabs"] #parameters'
'[data-test-subj="painlessTabs-loaded"] #parameters'
);
await painlessTabsParameters.click();
await a11y.testAppSnapshot();
@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('click on the context button', async () => {
const painlessTabsContext = await find.byCssSelector(
'[data-test-subj="painlessTabs"] #context'
'[data-test-subj="painlessTabs-loaded"] #context'
);
await painlessTabsContext.click();
await a11y.testAppSnapshot();

View file

@ -35,7 +35,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show the editor and preview panels', async () => {
const editor = await testSubjects.find('kibanaCodeEditor');
const preview = await testSubjects.find('painlessTabs');
const preview = await testSubjects.find('painlessTabs-loaded');
expect(await editor.isDisplayed()).to.be(true);
expect(await preview.isDisplayed()).to.be(true);
@ -45,7 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await monacoEditor.setCodeEditorValue(TEST_SCRIPT);
await retry.try(async () => {
const result = await testSubjects.find('painlessTabs');
const result = await testSubjects.find('painlessTabs-loaded');
expect(await result.getVisibleText()).to.contain(TEST_SCRIPT_RESULT.toString());
});
});

View file

@ -36,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show the editor and preview panels', async () => {
const editor = await testSubjects.find('kibanaCodeEditor');
const preview = await testSubjects.find('painlessTabs');
const preview = await testSubjects.find('painlessTabs-loaded');
expect(await editor.isDisplayed()).to.be(true);
expect(await preview.isDisplayed()).to.be(true);
@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await monacoEditor.setCodeEditorValue(TEST_SCRIPT);
await retry.try(async () => {
const result = await testSubjects.find('painlessTabs');
const result = await testSubjects.find('painlessTabs-loaded');
expect(await result.getVisibleText()).to.contain(TEST_SCRIPT_RESULT.toString());
});
});