[Console] Enable monaco by default (#184862)

## Summary

Closes https://github.com/elastic/kibana/issues/184025

This PR enables the migration from Ace to Monaco in Dev Tools Console by
default in the main branch. All serverless projects will still have the
migration disabled by default. After 8.15 is branched, the migration
will be disabled there as well. The intended release version for this
migration is 8.16.

### Functional tests 
This PR creates a copy of functional tests for Monaco Console and keeps
the tests for Ace in a separate folder. When the migration is released,
we can remove the code for Ace together with tests.
The Monaco tests are not the exact copy of the Ace tests, since some
functionality and autocomplete behaviour is slightly different in the
migrated Console. For example, the auto-closing of brackets works in
Monaco when typing something, but is not kicking in in the tests.

Flaky test runner 

### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2024-06-19 17:37:04 +02:00 committed by GitHub
parent 37ca5c6bd9
commit 5e346b2561
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1626 additions and 50 deletions

View file

@ -98,7 +98,8 @@ enabled:
- test/api_integration/config.js
- test/examples/config.js
- test/functional/apps/bundles/config.ts
- test/functional/apps/console/config.ts
- test/functional/apps/console/monaco/config.ts
- test/functional/apps/console/ace/config.ts
- test/functional/apps/context/config.ts
- test/functional/apps/dashboard_elements/controls/common/config.ts
- test/functional/apps/dashboard_elements/controls/options_list/config.ts

View file

@ -108,6 +108,8 @@ xpack.index_management.enableDataStreamsStorageColumn: false
xpack.index_management.enableMappingsSourceFieldSection: false
# Disable toggle for enabling data retention in DSL form from Index Management UI
xpack.index_management.enableTogglingDataRetention: false
# Disable the Monaco migration in Console
console.dev.enableMonaco: false
# Keep deeplinks visible so that they are shown in the sidenav
dev_tools.deeplinks.navLinkStatus: visible

View file

@ -15,7 +15,7 @@ export interface ParsedRequest {
startOffset: number;
endOffset?: number;
method: string;
url: string;
url?: string;
data?: Array<Record<string, unknown>>;
}
export interface ConsoleParserResult {

View file

@ -169,6 +169,7 @@ exports[`<CodeEditor /> is rendered 1`] = `
</p>
</React.Fragment>
}
data-test-subj="codeEditorAccessibilityOverlay"
delay="regular"
display="block"
position="top"

View file

@ -155,6 +155,8 @@ export interface CodeEditorProps {
* Enables the editor to get disabled when pressing ESC to resolve focus trapping for accessibility.
*/
accessibilityOverlayEnabled?: boolean;
dataTestSubj?: string;
}
export const CodeEditor: React.FC<CodeEditorProps> = ({
@ -186,6 +188,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
}),
fitToContent,
accessibilityOverlayEnabled = true,
dataTestSubj,
}) => {
const { colorMode, euiTheme } = useEuiTheme();
const useDarkTheme = useDarkThemeProp ?? colorMode === 'DARK';
@ -280,6 +283,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
return (
<EuiToolTip
data-test-subj="codeEditorAccessibilityOverlay"
display="block"
content={
<>
@ -485,7 +489,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
<div
css={styles.container}
onKeyDown={onKeyDown}
data-test-subj="kibanaCodeEditor"
data-test-subj={dataTestSubj ?? 'kibanaCodeEditor'}
className="kibanaCodeEditor"
>
{accessibilityOverlayEnabled && renderPrompt()}

View file

@ -115,6 +115,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
width: 100%;
`}
ref={divRef}
data-test-subj="consoleMonacoEditorContainer"
>
<EuiFlexGroup
className="conApp__editorActions"
@ -151,6 +152,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
</EuiFlexItem>
</EuiFlexGroup>
<CodeEditor
dataTestSubj={'consoleMonacoEditor'}
languageId={CONSOLE_LANG_ID}
value={value}
onChange={setValue}

View file

@ -93,6 +93,7 @@ export const MonacoEditorOutput: FunctionComponent = () => {
</label>
</EuiScreenReaderOnly>
<CodeEditor
dataTestSubj={'consoleMonacoOutput'}
languageId={mode}
value={value}
fullWidth={true}

View file

@ -22,8 +22,8 @@ import { AdjustedParsedRequest } from '../types';
* - the request body is stringified from an object using JSON.stringify
*/
export const stringifyRequest = (parsedRequest: ParsedRequest): EditorRequest => {
const url = removeTrailingWhitespaces(parsedRequest.url);
const method = parsedRequest.method.toUpperCase();
const url = parsedRequest.url ? removeTrailingWhitespaces(parsedRequest.url) : '';
const method = parsedRequest.method?.toUpperCase() ?? '';
const data = parsedRequest.data?.map((parsedData) => JSON.stringify(parsedData, null, 2));
return { url, method, data: data ?? [] };
};

View file

@ -25,7 +25,7 @@ export const parseLine = (line: string): ParsedLineTokens => {
// try to parse into method and url (split on whitespace)
const parts = line.split(whitespacesRegex);
// 1st part is the method
const method = parts[0];
const method = parts[0].toUpperCase();
// 2nd part is the url
const url = parts[1];
// try to parse into url path and url params (split on question mark)

View file

@ -31,7 +31,7 @@ const schemaLatest = schema.object(
defaultValue: 'stack',
}),
}),
dev: schema.object({ enableMonaco: schema.boolean({ defaultValue: false }) }),
dev: schema.object({ enableMonaco: schema.boolean({ defaultValue: true }) }),
},
{ defaultValue: undefined }
);

View file

@ -9,7 +9,7 @@
import _ from 'lodash';
import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');

View file

@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');

View file

@ -8,33 +8,8 @@
import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import { FtrProviderContext } from '../../ftr_provider_context';
const DEFAULT_REQUEST = `
# Welcome to the Dev Tools Console!
#
# You can use Console to explore the Elasticsearch API. See the \n Elasticsearch API reference to learn more:
# https://www.elastic.co/guide/en/elasticsearch/reference/current\n /rest-apis.html
#
# Here are a few examples to get you started.
# Create an index
PUT /my-index
# Add a document to my-index
POST /my-index/_doc
{
"id": "park_rocky-mountain",
"title": "Rocky Mountain",
"description": "Bisected north to south by the Continental \n Divide, this portion of the Rockies has ecosystems varying \n from over 150 riparian lakes to montane and subalpine forests \n to treeless alpine tundra."
}
# Perform a search in my-index
GET /my-index/_search?q="rocky mountain"
`.trim();
import { DEFAULT_INPUT_VALUE } from '@kbn/console-plugin/common/constants';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
@ -58,7 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
const actualRequest = await PageObjects.console.getRequest();
log.debug(actualRequest);
expect(actualRequest.trim()).to.eql(DEFAULT_REQUEST);
expect(actualRequest.replace(/\s/g, '')).to.eql(DEFAULT_INPUT_VALUE.replace(/\s/g, ''));
});
});

View file

@ -7,7 +7,7 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');

View file

@ -7,7 +7,7 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');

View file

@ -7,7 +7,7 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');

View file

@ -7,7 +7,7 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getService, getPageObjects }: FtrProviderContext) => {
const retry = getService('retry');

View file

@ -7,7 +7,7 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'console', 'header', 'home']);

View file

@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { rgbToHex } from '@elastic/eui';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getService, getPageObjects }: FtrProviderContext) => {
const retry = getService('retry');

View file

@ -0,0 +1,27 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FtrConfigProviderContext } from '@kbn/test';
import { configureHTTP2 } from '../../../../common/configure_http2';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js'));
return configureHTTP2({
...functionalConfig.getAll(),
testFiles: [require.resolve('.')],
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
// disabling the monaco editor to run tests for ace
`--console.dev.enableMonaco=false`,
],
},
});
}

View file

@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
export default function ({ getService, loadTestFile }) {
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const browser = getService('browser');
const config = getService('config');

View file

@ -0,0 +1,374 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'console', 'header']);
const find = getService('find');
describe('console autocomplete feature', function describeIndexTests() {
this.tags('includeFirefox');
before(async () => {
log.debug('navigateTo console');
await PageObjects.common.navigateToApp('console');
// Ensure that the text area can be interacted with
await PageObjects.console.closeHelpIfExists();
await PageObjects.console.monaco.clearEditorText();
log.debug('setAutocompleteTrace true');
await PageObjects.console.setAutocompleteTrace(true);
});
after(async () => {
log.debug('setAutocompleteTrace false');
await PageObjects.console.setAutocompleteTrace(false);
});
it('should provide basic auto-complete functionality', async () => {
await PageObjects.console.monaco.enterText(`GET _search\n`);
await PageObjects.console.monaco.pressEnter();
await PageObjects.console.monaco.enterText(`{\n\t"query": {`);
await PageObjects.console.monaco.pressEnter();
await PageObjects.console.sleepForDebouncePeriod();
await PageObjects.console.monaco.promptAutocomplete();
expect(PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
});
describe('Autocomplete behavior', () => {
beforeEach(async () => {
await PageObjects.console.monaco.clearEditorText();
});
it('HTTP methods', async () => {
const suggestions = {
G: ['GET'],
P: ['PATCH', 'POST', 'PUT'],
D: ['DELETE'],
H: ['HEAD'],
};
for (const [char, methods] of Object.entries(suggestions)) {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type "%s"', char);
await PageObjects.console.monaco.enterText(char);
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
for (const [i, method] of methods.entries()) {
expect(await PageObjects.console.monaco.getAutocompleteSuggestion(i)).to.be.eql(method);
}
await PageObjects.console.monaco.pressEscape();
await PageObjects.console.monaco.clearEditorText();
}
});
it('ES API endpoints', async () => {
const suggestions = {
'GET _': ['_alias', '_all'],
'PUT _': ['_all'],
'POST _': ['_aliases', '_all'],
'DELETE _': ['_all'],
'HEAD _': ['_alias', '_all'],
};
for (const [text, endpoints] of Object.entries(suggestions)) {
for (const char of text) {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type "%s"', char);
await PageObjects.console.monaco.enterText(char);
}
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
for (const [i, endpoint] of endpoints.entries()) {
expect(await PageObjects.console.monaco.getAutocompleteSuggestion(i)).to.be.eql(
endpoint
);
}
await PageObjects.console.monaco.pressEscape();
await PageObjects.console.monaco.pressEnter();
}
});
it('JSON autocompletion with placeholder fields', async () => {
await PageObjects.console.monaco.enterText('GET _search\n');
await PageObjects.console.monaco.enterText('{');
await PageObjects.console.monaco.pressEnter();
for (const char of '"ag') {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type "%s"', char);
await PageObjects.console.monaco.enterText(char);
}
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
await PageObjects.console.monaco.pressEnter();
await PageObjects.console.sleepForDebouncePeriod();
expect((await PageObjects.console.monaco.getEditorText()).replace(/\s/g, '')).to.be.eql(
`
GET _search
{
"aggs": {
"NAME": {
"AGG_TYPE": {}
}
}
}
`.replace(/\s/g, '')
);
});
it('Dynamic autocomplete', async () => {
await PageObjects.console.monaco.enterText('POST test/_doc\n{}');
await PageObjects.console.clickPlay();
await PageObjects.header.waitUntilLoadingHasFinished();
expect(await PageObjects.console.getResponseStatus()).to.be('201');
await PageObjects.console.monaco.pressEnter();
for (const char of 'POST t') {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type "%s"', char);
await PageObjects.console.monaco.enterText(char);
}
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
expect(await PageObjects.console.monaco.getAutocompleteSuggestion(0)).to.be.eql('test');
});
});
describe('anti-regression watchdogs', () => {
beforeEach(async () => {
await PageObjects.console.monaco.clearEditorText();
});
// flaky
it.skip('should suppress auto-complete on arrow keys', async () => {
await PageObjects.console.monaco.enterText(`\nGET _search\nGET _search`);
await PageObjects.console.monaco.pressEnter();
const keyPresses = [
'pressUp',
'pressUp',
'pressDown',
'pressDown',
'pressRight',
'pressRight',
'pressLeft',
'pressLeft',
] as const;
for (const keyPress of keyPresses) {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key', keyPress);
await PageObjects.console.monaco[keyPress]();
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(false);
}
});
it('should activate auto-complete for methods case-insensitively', async () => {
const methods = _.sampleSize(
_.compact(
`
GET GEt GeT Get gET gEt geT get
PUT PUt PuT Put pUT pUt puT put
POST POSt POsT POst PoST PoSt PosT Post pOST pOSt pOsT pOst poST poSt posT post
DELETE DELETe DELEtE DELEte DELeTE DELeTe DELetE DELete DElETE DElETe DElEtE DElEte DEleTE DEleTe DEletE DElete
DeLETE DeLETe DeLEtE DeLEte DeLeTE DeLeTe DeLetE DeLete DelETE DelETe DelEtE DelEte DeleTE DeleTe DeletE Delete
dELETE dELETe dELEtE dELEte dELeTE dELeTe dELetE dELete dElETE dElETe dElEtE dElEte dEleTE dEleTe dEletE dElete
deLETE deLETe deLEtE deLEte deLeTE deLeTe deLetE deLete delETE delETe delEtE delEte deleTE deleTe deletE delete
HEAD HEAd HEaD HEad HeAD HeAd HeaD Head hEAD hEAd hEaD hEad heAD heAd heaD head
`.split(/\s+/m)
),
20 // 20 of 112 (approx. one-fifth) should be enough for testing
);
for (const method of methods) {
await PageObjects.console.monaco.clearEditorText();
for (const char of method.slice(0, -1)) {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type "%s"', char);
await PageObjects.console.monaco.enterText(char); // e.g. 'P' -> 'Po' -> 'Pos'
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
}
for (const char of [method.at(-1), ' ', '_']) {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type "%s"', char);
await PageObjects.console.monaco.enterText(char!); // e.g. 'Post ' -> 'Post _'
}
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
}
});
it('should activate auto-complete for a single character immediately following a slash in URL', async () => {
await PageObjects.console.monaco.enterText('GET .kibana');
for (const char of ['/', '_']) {
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type "%s"', char);
await PageObjects.console.monaco.enterText(char); // i.e. 'GET .kibana/' -> 'GET .kibana/_'
}
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
});
it('should activate auto-complete for multiple indices after comma in URL', async () => {
await PageObjects.console.monaco.enterText('GET _cat/indices/.kibana');
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type ","');
await PageObjects.console.monaco.enterText(','); // i.e. 'GET /_cat/indices/.kibana,'
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type Ctrl+SPACE');
await PageObjects.console.monaco.pressCtrlSpace();
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(true);
});
// not fixed for monaco yet https://github.com/elastic/kibana/issues/184442
it.skip('should not activate auto-complete after comma following endpoint in URL', async () => {
await PageObjects.console.monaco.enterText('GET _search');
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type ","');
await PageObjects.console.monaco.enterText(','); // i.e. 'GET _search,'
await PageObjects.console.sleepForDebouncePeriod();
log.debug('Key type Ctrl+SPACE');
await PageObjects.console.monaco.pressCtrlSpace();
expect(await PageObjects.console.monaco.isAutocompleteVisible()).to.be.eql(false);
});
});
// not implemented for monaco yet https://github.com/elastic/kibana/issues/184856
describe.skip('with a missing comma in query', () => {
const LINE_NUMBER = 4;
beforeEach(async () => {
await PageObjects.console.clearTextArea();
await PageObjects.console.enterRequest();
await PageObjects.console.pressEnter();
});
it('should add a comma after previous non empty line', async () => {
await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"match": {}`);
await PageObjects.console.pressEnter();
await PageObjects.console.pressEnter();
await PageObjects.console.pressEnter();
await PageObjects.console.sleepForDebouncePeriod();
await PageObjects.console.promptAutocomplete();
await PageObjects.console.pressEnter();
await retry.try(async () => {
let conApp = await find.byCssSelector('.conApp');
const firstInnerHtml = await conApp.getAttribute('innerHTML');
await PageObjects.common.sleep(500);
conApp = await find.byCssSelector('.conApp');
const secondInnerHtml = await conApp.getAttribute('innerHTML');
return firstInnerHtml === secondInnerHtml;
});
const textAreaString = await PageObjects.console.getAllVisibleText();
log.debug('Text Area String Value==================\n');
log.debug(textAreaString);
expect(textAreaString).to.contain(',');
const text = await PageObjects.console.getVisibleTextAt(LINE_NUMBER);
const lastChar = text.charAt(text.length - 1);
expect(lastChar).to.be.eql(',');
});
it('should add a comma after the triple quoted strings', async () => {
await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"term": """some data"""`);
await PageObjects.console.pressEnter();
await PageObjects.console.sleepForDebouncePeriod();
await PageObjects.console.promptAutocomplete();
await PageObjects.console.pressEnter();
await retry.waitForWithTimeout('text area to contain comma', 25000, async () => {
const textAreaString = await PageObjects.console.getAllVisibleText();
return textAreaString.includes(',');
});
const text = await PageObjects.console.getVisibleTextAt(LINE_NUMBER);
const lastChar = text.charAt(text.length - 1);
expect(lastChar).to.be.eql(',');
});
});
describe('with conditional templates', async () => {
const CONDITIONAL_TEMPLATES = [
{
type: 'fs',
template: `"location": "path"`,
},
{
type: 'url',
template: `"url": ""`,
},
{ type: 's3', template: `"bucket": ""` },
{
type: 'azure',
template: `"path": ""`,
},
];
beforeEach(async () => {
await PageObjects.console.monaco.clearEditorText();
await PageObjects.console.monaco.enterText('POST _snapshot/test_repo\n');
});
await asyncForEach(CONDITIONAL_TEMPLATES, async ({ type, template }) => {
it('should insert different templates depending on the value of type', async () => {
await PageObjects.console.monaco.enterText(`{\n\t"type": "${type}",\n`);
await PageObjects.console.sleepForDebouncePeriod();
// Prompt autocomplete for 'settings'
await PageObjects.console.monaco.promptAutocomplete('s');
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.monaco.isAutocompleteVisible()
);
await PageObjects.console.monaco.pressEnter();
await retry.try(async () => {
const request = await PageObjects.console.monaco.getEditorText();
log.debug(request);
expect(request).to.contain(`${template}`);
});
});
});
});
});
}

View file

@ -0,0 +1,149 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const PageObjects = getPageObjects(['common', 'console', 'header']);
// flaky
describe.skip('console app', function testComments() {
this.tags('includeFirefox');
before(async () => {
log.debug('navigateTo console');
await PageObjects.common.navigateToApp('console');
await PageObjects.console.closeHelpIfExists();
});
describe('with comments', async () => {
const enterRequest = async (url: string, body: string) => {
await PageObjects.console.monaco.clearEditorText();
await PageObjects.console.monaco.enterText(`${url}\n${body}`);
};
async function runTests(
tests: Array<{ description: string; url?: string; body: string }>,
fn: () => Promise<void>
) {
await asyncForEach(tests, async ({ description, url, body }) => {
it(description, async () => {
await enterRequest(url ?? '\nGET search', body);
await fn();
});
});
}
describe('with single line comments', async () => {
await runTests(
[
{
url: '\n// GET _search',
body: '',
description: 'should allow in request url, using //',
},
{
body: '{\n\t\t"query": {\n\t\t\t// "match_all": {}\n}\n}',
description: 'should allow in request body, using //',
},
{
url: '\n # GET _search',
body: '',
description: 'should allow in request url, using #',
},
{
body: '{\n\t\t"query": {\n\t\t\t# "match_all": {}\n}\n}',
description: 'should allow in request body, using #',
},
{
description: 'should accept as field names, using //',
body: '{\n "//": {} }',
},
{
description: 'should accept as field values, using //',
body: '{\n "f": "//" }',
},
{
description: 'should accept as field names, using #',
body: '{\n "#": {} }',
},
{
description: 'should accept as field values, using #',
body: '{\n "f": "#" }',
},
],
async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
}
);
});
describe('with multiline comments', async () => {
await runTests(
[
{
url: '\n /* \nGET _search \n*/',
body: '',
description: 'should allow in request url, using /* */',
},
{
body: '{\n\t\t"query": {\n\t\t\t/* "match_all": {} */ \n}\n}',
description: 'should allow in request body, using /* */',
},
{
description: 'should accept as field names, using /*',
body: '{\n "/*": {} \n\t\t /* "f": 1 */ \n}',
},
{
description: 'should accept as field values, using */',
body: '{\n /* "f": 1 */ \n"f": "*/" \n}',
},
],
async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
}
);
});
describe('with invalid syntax in request body', async () => {
await runTests(
[
{
description: 'should highlight invalid syntax',
body: '{\n "query": \'\'', // E.g. using single quotes
},
],
async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(true);
}
);
});
describe('with invalid request', async () => {
await runTests(
[
{
description: 'with invalid character should display error marker',
body: '{\n $ "query": {}',
},
{
description: 'with missing field name',
body: '{\n "query": {},\n {}',
},
],
async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(true);
}
);
});
});
});
}

View file

@ -0,0 +1,150 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import { DEFAULT_INPUT_VALUE } from '@kbn/console-plugin/common/constants';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const log = getService('log');
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'console', 'header']);
const security = getService('security');
describe('console app', function describeIndexTests() {
this.tags('includeFirefox');
before(async () => {
log.debug('navigateTo console');
await PageObjects.common.navigateToApp('console');
});
beforeEach(async () => {
await PageObjects.console.closeHelpIfExists();
});
it('should show the default request', async () => {
await retry.try(async () => {
const actualRequest = await PageObjects.console.monaco.getEditorText();
log.debug(actualRequest);
expect(actualRequest.replace(/\s/g, '')).to.eql(DEFAULT_INPUT_VALUE.replace(/\s/g, ''));
});
});
// issue with the url params with whitespaces https://github.com/elastic/kibana/issues/184927
it.skip('default request response should include `"timed_out" : false`', async () => {
const expectedResponseContains = `"timed_out": false`;
await PageObjects.console.monaco.selectAllRequests();
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.monaco.getOutputText();
log.debug(actualResponse);
expect(actualResponse).to.contain(expectedResponseContains);
});
});
// the resizer doesn't work the same as in ace https://github.com/elastic/kibana/issues/184352
it.skip('should resize the editor', async () => {
const editor = await PageObjects.console.monaco.getEditor();
await browser.setWindowSize(1300, 1100);
const initialSize = await editor.getSize();
await browser.setWindowSize(1000, 1100);
const afterSize = await editor.getSize();
expect(initialSize.width).to.be.greaterThan(afterSize.width);
});
it('should return statusCode 400 to unsupported HTTP verbs', async () => {
const expectedResponseContains = '"statusCode": 400';
await PageObjects.console.monaco.clearEditorText();
await PageObjects.console.monaco.enterText('OPTIONS /');
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.monaco.getOutputText();
log.debug(actualResponse);
expect(actualResponse).to.contain(expectedResponseContains);
expect(await PageObjects.console.hasSuccessBadge()).to.be(false);
});
});
describe('with kbn: prefix in request', () => {
before(async () => {
await PageObjects.console.monaco.clearEditorText();
});
it('it should send successful request to Kibana API', async () => {
const expectedResponseContains = 'default space';
await PageObjects.console.monaco.enterText('GET kbn:/api/spaces/space');
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.monaco.getOutputText();
log.debug(actualResponse);
expect(actualResponse).to.contain(expectedResponseContains);
});
});
});
describe('with query params', () => {
it('should issue a successful request', async () => {
await PageObjects.console.monaco.clearEditorText();
await PageObjects.console.monaco.enterText(
'GET _cat/aliases?format=json&v=true&pretty=true'
);
await PageObjects.console.clickPlay();
await PageObjects.header.waitUntilLoadingHasFinished();
// set the width of the browser, so that the response status is visible
await browser.setWindowSize(1300, 1100);
await retry.try(async () => {
const status = await PageObjects.console.getResponseStatus();
expect(status).to.eql(200);
});
});
});
describe('multiple requests output', function () {
const sendMultipleRequests = async (requests: string[]) => {
await asyncForEach(requests, async (request) => {
await PageObjects.console.monaco.enterText(request);
});
await PageObjects.console.monaco.selectAllRequests();
await PageObjects.console.clickPlay();
};
before(async () => {
await security.testUser.setRoles(['kibana_admin', 'test_index']);
});
after(async () => {
await security.testUser.restoreDefaults();
});
beforeEach(async () => {
await PageObjects.console.closeHelpIfExists();
await PageObjects.console.monaco.clearEditorText();
});
it('should contain comments starting with # symbol', async () => {
await sendMultipleRequests(['\n PUT test-index', '\n DELETE test-index']);
await retry.try(async () => {
const response = await PageObjects.console.monaco.getOutputText();
log.debug(response);
expect(response).to.contain('# PUT test-index 200');
expect(response).to.contain('# DELETE test-index 200');
});
});
// not implemented for monaco yet https://github.com/elastic/kibana/issues/184010
it.skip('should display status badges', async () => {
await sendMultipleRequests(['\n GET _search/test', '\n GET _search']);
await PageObjects.header.waitUntilLoadingHasFinished();
expect(await PageObjects.console.hasWarningBadge()).to.be(true);
expect(await PageObjects.console.hasSuccessBadge()).to.be(true);
});
});
});
}

View file

@ -0,0 +1,54 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const log = getService('log');
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'console']);
const remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver');
describe('Console App CCS', function describeIndexTests() {
this.tags('includeFirefox');
before(async () => {
await remoteEsArchiver.loadIfNeeded(
'test/functional/fixtures/es_archiver/logstash_functional'
);
// resize the editor to allow the whole of the response to be displayed
await browser.setWindowSize(1200, 1800);
log.debug('navigateTo console');
await PageObjects.common.navigateToApp('console');
await retry.try(async () => {
await PageObjects.console.collapseHelp();
});
});
after(async () => {
await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
});
describe('Perform CCS Search in Console', () => {
before(async () => {
await PageObjects.console.monaco.clearEditorText();
});
it('it should be able to access remote data', async () => {
await PageObjects.console.monaco.enterText(
'\nGET ftr-remote:logstash-*/_search\n {\n "query": {\n "bool": {\n "must": [\n {"match": {"extension" : "jpg"} \n}\n]\n}\n}\n}'
);
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.monaco.getOutputText();
expect(actualResponse).to.contain('"_index": "ftr-remote:logstash-2015.09.20"');
});
});
});
});
}

View file

@ -0,0 +1,104 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'console']);
const browser = getService('browser');
const toasts = getService('toasts');
describe('console context menu', function testContextMenu() {
before(async () => {
await PageObjects.common.navigateToApp('console');
// Ensure that the text area can be interacted with
await PageObjects.console.closeHelpIfExists();
await PageObjects.console.monaco.clearEditorText();
await PageObjects.console.monaco.enterText('GET _search');
});
it('should open context menu', async () => {
expect(await PageObjects.console.isContextMenuOpen()).to.be(false);
await PageObjects.console.clickContextMenu();
expect(PageObjects.console.isContextMenuOpen()).to.be.eql(true);
});
it('should have options to copy as curl, open documentation, and auto indent', async () => {
await PageObjects.console.clickContextMenu();
expect(PageObjects.console.isContextMenuOpen()).to.be.eql(true);
expect(PageObjects.console.isCopyAsCurlButtonVisible()).to.be.eql(true);
expect(PageObjects.console.isOpenDocumentationButtonVisible()).to.be.eql(true);
expect(PageObjects.console.isAutoIndentButtonVisible()).to.be.eql(true);
});
it('should copy as curl and show toast when copy as curl button is clicked', async () => {
await PageObjects.console.clickContextMenu();
await PageObjects.console.clickCopyAsCurlButton();
const resultToast = await toasts.getElementByIndex(1);
const toastText = await resultToast.getVisibleText();
if (toastText.includes('Write permission denied')) {
log.debug('Write permission denied, skipping test');
return;
}
expect(toastText).to.be('Request copied as cURL');
const canReadClipboard = await browser.checkBrowserPermission('clipboard-read');
if (canReadClipboard) {
const clipboardText = await browser.getClipboardValue();
expect(clipboardText).to.contain('curl -XGET');
}
});
it('should open documentation when open documentation button is clicked', async () => {
await PageObjects.console.clickContextMenu();
await PageObjects.console.clickOpenDocumentationButton();
await retry.tryForTime(10000, async () => {
await browser.switchTab(1);
});
// Retry until the documentation is loaded
await retry.try(async () => {
const url = await browser.getCurrentUrl();
expect(url).to.contain('search-search.html');
});
// Close the documentation tab
await browser.closeCurrentWindow();
await browser.switchTab(0);
});
// not implemented yet for monaco https://github.com/elastic/kibana/issues/185891
it.skip('should toggle auto indent when auto indent button is clicked', async () => {
await PageObjects.console.clearTextArea();
await PageObjects.console.enterRequest('GET _search\n{"query": {"match_all": {}}}');
await PageObjects.console.clickContextMenu();
await PageObjects.console.clickAutoIndentButton();
// Retry until the request is auto indented
await retry.try(async () => {
const request = await PageObjects.console.getRequest();
expect(request).to.be.eql('GET _search\n{\n "query": {\n "match_all": {}\n }\n}');
});
await PageObjects.console.clickContextMenu();
// Click the auto-indent button again to condense request
await PageObjects.console.clickAutoIndentButton();
// Retry until the request is condensed
await retry.try(async () => {
const request = await PageObjects.console.getRequest();
expect(request).to.be.eql('GET _search\n{"query":{"match_all":{}}}');
});
});
});
}

View file

@ -0,0 +1,129 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'console', 'header']);
describe('misc console behavior', function testMiscConsoleBehavior() {
this.tags('includeFirefox');
before(async () => {
await browser.setWindowSize(1200, 800);
await PageObjects.common.navigateToApp('console');
// Ensure that the text area can be interacted with
await PageObjects.console.closeHelpIfExists();
});
beforeEach(async () => {
await PageObjects.console.monaco.clearEditorText();
});
describe('keyboard shortcuts', () => {
let tabCount = 1;
after(async () => {
if (tabCount > 1) {
await browser.closeCurrentWindow();
await browser.switchTab(0);
}
});
it('should execute the request when Ctrl+Enter is pressed', async () => {
await PageObjects.console.monaco.enterText('GET _search');
await PageObjects.console.monaco.pressCtrlEnter();
await retry.try(async () => {
const response = await PageObjects.console.monaco.getOutputText();
expect(response).to.contain('"timed_out": false');
});
});
it('should auto indent current request when Ctrl+I is pressed', async () => {
await PageObjects.console.monaco.enterText('GET _search\n{"query": {"match_all": {}}}');
await PageObjects.console.monaco.selectCurrentRequest();
await PageObjects.console.monaco.pressCtrlI();
await retry.waitFor('request to be auto indented', async () => {
const request = await PageObjects.console.monaco.getEditorText();
return request === 'GET _search\n{\n "query": {\n "match_all": {}\n }\n}';
});
});
it('should jump to the previous request when Ctrl+Up is pressed', async () => {
await PageObjects.console.monaco.enterText('\nGET _search/foo');
await PageObjects.console.monaco.enterText('\nGET _search/bar');
await PageObjects.console.monaco.pressCtrlUp();
await retry.waitFor('request to be selected', async () => {
const request = await PageObjects.console.monaco.getEditorTextAtLine(1);
return request === 'GET _search/foo';
});
});
it('should jump to the next request when Ctrl+Down is pressed', async () => {
await PageObjects.console.monaco.enterText('\nGET _search/foo');
await PageObjects.console.monaco.enterText('\nGET _search/bar');
await PageObjects.console.monaco.pressCtrlUp();
await PageObjects.console.monaco.pressCtrlDown();
await retry.waitFor('request to be selected', async () => {
const request = await PageObjects.console.monaco.getEditorTextAtLine(2);
return request === 'GET _search/bar';
});
});
// flaky
it.skip('should go to line number when Ctrl+L is pressed', async () => {
await PageObjects.console.monaco.enterText(
'\nGET _search/foo\n{\n "query": {\n "match_all": {} \n} \n}'
);
await PageObjects.console.monaco.pressCtrlL();
// Sleep to allow the line number input to be focused
await PageObjects.common.sleep(1000);
const alert = await browser.getAlert();
await alert?.sendKeys('4');
await alert?.accept();
await PageObjects.common.sleep(1000);
expect(await PageObjects.console.monaco.getCurrentLineNumber()).to.be(4);
});
// flaky
it.skip('should open documentation when Ctrl+/ is pressed', async () => {
await PageObjects.console.monaco.enterText('GET _search');
await PageObjects.console.monaco.pressEscape();
await PageObjects.console.monaco.pressCtrlSlash();
await retry.tryForTime(10000, async () => {
await browser.switchTab(1);
tabCount++;
});
// Retry until the documentation is loaded
await retry.try(async () => {
const url = await browser.getCurrentUrl();
expect(url).to.contain('search-search.html');
});
});
});
describe('customizable font size', () => {
// flaky
it.skip('should allow the font size to be customized', async () => {
await PageObjects.console.setFontSizeSetting(20);
await retry.try(async () => {
// the settings are not applied synchronously, so we retry for a time
expect(await PageObjects.console.monaco.getFontSize()).to.be('20px');
});
await PageObjects.console.setFontSizeSetting(24);
await retry.try(async () => {
// the settings are not applied synchronously, so we retry for a time
expect(await PageObjects.console.monaco.getFontSize()).to.be('24px');
});
});
});
});
}

View file

@ -0,0 +1,41 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const PageObjects = getPageObjects(['common', 'console']);
describe('console settings', function testSettings() {
this.tags('includeFirefox');
before(async () => {
log.debug('navigateTo console');
await PageObjects.common.navigateToApp('console');
// Ensure that the text area can be interacted with
await PageObjects.console.closeHelpIfExists();
await PageObjects.console.monaco.clearEditorText();
});
it('displays the a11y overlay', async () => {
await PageObjects.console.monaco.pressEscape();
const isOverlayVisible = await PageObjects.console.monaco.isA11yOverlayVisible();
expect(isOverlayVisible).to.be(true);
});
it('disables the a11y overlay via settings', async () => {
await PageObjects.console.openSettings();
await PageObjects.console.toggleA11yOverlaySetting();
await PageObjects.console.monaco.pressEscape();
const isOverlayVisible = await PageObjects.console.monaco.isA11yOverlayVisible();
expect(isOverlayVisible).to.be(false);
});
});
}

View file

@ -0,0 +1,101 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const toasts = getService('toasts');
const PageObjects = getPageObjects(['common', 'console', 'header']);
describe('text input', function testTextInput() {
before(async () => {
await PageObjects.common.navigateToApp('console');
await PageObjects.console.closeHelpIfExists();
});
beforeEach(async () => {
await PageObjects.console.monaco.clearEditorText();
});
describe('with a data URI in the load_from query', () => {
it('loads the data from the URI', async () => {
await PageObjects.common.navigateToApp('console', {
hash: '#/console?load_from=data:text/plain,BYUwNmD2Q',
});
await retry.try(async () => {
const actualRequest = await PageObjects.console.monaco.getEditorText();
expect(actualRequest.trim()).to.eql('hello');
});
});
describe('with invalid data', () => {
it('shows a toast error', async () => {
await PageObjects.common.navigateToApp('console', {
hash: '#/console?load_from=data:text/plain,BYUwNmD2',
});
await retry.try(async () => {
expect(await toasts.getCount()).to.equal(1);
});
});
});
});
// not yet implemented for monaco https://github.com/elastic/kibana/issues/186001
describe.skip('copy/pasting cURL commands into the console', () => {
it('should convert cURL commands into the console request format', async () => {
await PageObjects.console.monaco.enterText(
`\n curl -XGET "http://localhost:9200/_search?pretty" -d'\n{"query": {"match_all": {}}}'`
);
await PageObjects.console.monaco.copyRequestsToClipboard();
await PageObjects.console.monaco.clearEditorText();
await PageObjects.console.monaco.pasteClipboardValue();
await retry.try(async () => {
const actualRequest = await PageObjects.console.monaco.getEditorText();
expect(actualRequest.trim()).to.eql('GET /_search?pretty\n {"query": {"match_all": {}}}');
});
});
});
describe('console history', () => {
const sendRequest = async (request: string) => {
await PageObjects.console.monaco.enterText(request);
await PageObjects.console.clickPlay();
await PageObjects.header.waitUntilLoadingHasFinished();
};
it('should show the history', async () => {
await sendRequest('GET /_search?pretty');
await PageObjects.console.clickHistory();
await retry.try(async () => {
const history = await PageObjects.console.getHistoryEntries();
expect(history).to.eql(['/_search?pretty (a few seconds ago)']);
});
// Clear the history
await PageObjects.console.clickClearHistory();
await PageObjects.console.closeHistory();
});
it('should load a request from history', async () => {
await sendRequest('GET _search\n{"query": {"match_all": {}}}');
await PageObjects.console.monaco.clearEditorText();
await PageObjects.console.clickHistory();
await PageObjects.console.loadRequestFromHistory(0);
await retry.try(async () => {
const actualRequest = await PageObjects.console.monaco.getEditorText();
expect(actualRequest.trim()).to.eql(
'GET _search\n{\n "query": {\n "match_all": {}\n }\n}'
);
});
});
});
});
}

View file

@ -0,0 +1,71 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getService, getPageObjects }: FtrProviderContext) => {
const retry = getService('retry');
const log = getService('log');
const PageObjects = getPageObjects(['common', 'console', 'header']);
describe('Console variables', function testConsoleVariables() {
// FLAKY on firefox: https://github.com/elastic/kibana/issues/157776
// this.tags('includeFirefox');
before(async () => {
log.debug('navigateTo console');
await PageObjects.common.navigateToApp('console');
await PageObjects.console.closeHelpIfExists();
await PageObjects.console.monaco.clearEditorText();
});
it('should allow creating a new variable', async () => {
await PageObjects.console.addNewVariable({ name: 'index1', value: 'test' });
const variables = await PageObjects.console.getVariables();
log.debug(variables);
expect(variables).to.contain('index1');
});
it('should allow removing a variable', async () => {
await PageObjects.console.addNewVariable({ name: 'index2', value: 'test' });
await PageObjects.console.removeVariables();
const variables = await PageObjects.console.getVariables();
expect(variables).to.eql([]);
});
describe('with variables in url', () => {
it('should send a successful request', async () => {
await PageObjects.console.addNewVariable({ name: 'index3', value: '_search' });
await PageObjects.console.monaco.enterText('\n GET ${index3}');
await PageObjects.console.clickPlay();
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async () => {
const status = await PageObjects.console.getResponseStatus();
expect(status).to.eql(200);
});
});
});
describe('with variables in request body', () => {
// bug in monaco https://github.com/elastic/kibana/issues/185999
it.skip('should send a successful request', async () => {
await PageObjects.console.addNewVariable({ name: 'query1', value: '{"match_all": {}}' });
await PageObjects.console.monaco.enterText('\n GET _search\n');
await PageObjects.console.monaco.enterText(`{\n\t"query": "\${query1}"`);
await PageObjects.console.clickPlay();
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async () => {
const status = await PageObjects.console.getResponseStatus();
expect(status).to.eql(200);
});
});
});
});
};

View file

@ -0,0 +1,51 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'console', 'header', 'home']);
const retry = getService('retry');
const security = getService('security');
describe('console vector tiles response validation', function describeIndexTests() {
before(async () => {
await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']);
await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', {
useActualUrl: true,
});
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.home.addSampleDataSet('logs');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.common.navigateToApp('console');
await PageObjects.console.closeHelpIfExists();
await PageObjects.console.monaco.clearEditorText();
});
it('should validate response', async () => {
await PageObjects.console.monaco.enterText(
`GET kibana_sample_data_logs/_mvt/geo.coordinates/0/0/0`
);
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.monaco.getOutputText();
expect(actualResponse).to.contain('"meta": [');
});
});
after(async () => {
await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', {
useActualUrl: true,
});
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.home.removeSampleDataSet('logs');
await security.testUser.restoreDefaults();
});
});
}

View file

@ -0,0 +1,118 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getService, getPageObjects }: FtrProviderContext) => {
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'console', 'header']);
describe('XJSON', function testXjson() {
this.tags('includeFirefox');
before(async () => {
await PageObjects.common.navigateToApp('console');
await PageObjects.console.closeHelpIfExists();
});
beforeEach(async () => {
await PageObjects.console.monaco.clearEditorText();
});
const executeRequest = async (request = '\n GET _search') => {
await PageObjects.console.monaco.enterText(request);
await PageObjects.console.clickPlay();
await PageObjects.header.waitUntilLoadingHasFinished();
};
describe('inline http request', () => {
it('should not have validation errors', async () => {
await PageObjects.console.monaco.enterText('\n GET foo/bar');
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
});
it('should have validation error for invalid method', async () => {
await PageObjects.console.monaco.enterText('\n FOO foo/bar');
// Retry because the error marker is not always immediately visible.
await retry.try(async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(true);
});
});
it('should have validation error for invalid path', async () => {
await PageObjects.console.monaco.enterText('\n GET');
// Retry because the error marker is not always immediately visible.
await retry.try(async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(true);
});
});
it('should have validation error for invalid body', async () => {
await PageObjects.console.monaco.enterText('\n POST foo/bar\n {"foo": "bar"');
// Retry because the error marker is not always immediately visible.
await retry.try(async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(true);
});
});
it('should not trigger error for multiple bodies for _msearch requests', async () => {
await PageObjects.console.monaco.enterText(
'\nGET foo/_msearch \n{}\n{"query": {"match_all": {}}}\n{"index": "bar"}\n{"query": {"match_all": {}}}'
);
// Retry until typing is finished.
await retry.try(async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
});
});
it('should not trigger validation errors for multiple JSON blocks', async () => {
await PageObjects.console.monaco.enterText('\nPOST test/doc/1 \n{\n "foo": "bar"\n}');
await PageObjects.console.monaco.enterText('\nPOST test/doc/2 \n{\n "foo": "baz"\n}');
await PageObjects.console.monaco.enterText('\nPOST test/doc/3 \n{\n "foo": "qux"\n}');
// Retry until typing is finished.
await retry.try(async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
});
});
it('should allow escaping quotation mark by wrapping it in triple quotes', async () => {
await PageObjects.console.monaco.enterText(
'\nPOST test/_doc/1 \n{\n "foo": """look "escaped" quotes"""\n}'
);
// Retry until typing is finished and validation errors are gone.
await retry.try(async () => {
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
});
});
it('should allow inline comments in request url row', async () => {
await executeRequest('\n GET _search // inline comment');
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
expect(await PageObjects.console.getResponseStatus()).to.eql(200);
});
it('should allow inline comments in request body', async () => {
await executeRequest(
'\n GET _search \n{\n "query": {\n "match_all": {} // inline comment\n}\n}'
);
expect(await PageObjects.console.monaco.hasInvalidSyntax()).to.be(false);
expect(await PageObjects.console.getResponseStatus()).to.eql(200);
});
it('should print warning for deprecated request', async () => {
await executeRequest('\nGET .kibana');
expect(await PageObjects.console.monaco.responseHasDeprecationWarning()).to.be(true);
});
it('should not print warning for non-deprecated request', async () => {
await executeRequest('\n GET _search');
expect(await PageObjects.console.monaco.responseHasDeprecationWarning()).to.be(false);
});
});
});
};

View file

@ -7,10 +7,10 @@
*/
import { FtrConfigProviderContext } from '@kbn/test';
import { configureHTTP2 } from '../../../common/configure_http2';
import { configureHTTP2 } from '../../../../common/configure_http2';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('../../config.base.js'));
const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js'));
return configureHTTP2({
...functionalConfig.getAll(),

View file

@ -0,0 +1,34 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const browser = getService('browser');
const config = getService('config');
describe('console app', function () {
before(async function () {
await browser.setWindowSize(1300, 1100);
});
if (config.get('esTestCluster.ccs')) {
loadTestFile(require.resolve('./_console_ccs'));
} else {
loadTestFile(require.resolve('./_console'));
loadTestFile(require.resolve('./_autocomplete'));
loadTestFile(require.resolve('./_vector_tile'));
loadTestFile(require.resolve('./_comments'));
loadTestFile(require.resolve('./_variables'));
loadTestFile(require.resolve('./_xjson'));
loadTestFile(require.resolve('./_misc_console_behavior'));
loadTestFile(require.resolve('./_context_menu'));
loadTestFile(require.resolve('./_text_input'));
loadTestFile(require.resolve('./_settings'));
}
});
}

View file

@ -17,5 +17,13 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
junit: {
reportName: 'Dashboard Elements - Controls tests',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
// disabling the monaco editor to run tests for ace
`--console.dev.enableMonaco=false`,
],
},
};
}

View file

@ -17,5 +17,13 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
junit: {
reportName: 'Dashboard Elements - Controls Options List tests',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
// disabling the monaco editor to run tests for ace
`--console.dev.enableMonaco=false`,
],
},
};
}

View file

@ -20,7 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
testFiles: [
require.resolve('./apps/dashboard/group3'),
require.resolve('./apps/discover/ccs_compatibility'),
require.resolve('./apps/console/_console_ccs'),
require.resolve('./apps/console/monaco/_console_ccs'),
require.resolve('./apps/management/ccs_compatibility'),
require.resolve('./apps/getting_started'),
],

View file

@ -16,7 +16,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
return {
...baseConfig.getAll(),
testFiles: [require.resolve('../apps/console')],
testFiles: [require.resolve('../apps/console/monaco')],
junit: {
reportName: 'Firefox UI Functional Tests - Console',

View file

@ -18,6 +18,166 @@ export class ConsolePageObject extends FtrService {
private readonly common = this.ctx.getPageObject('common');
private readonly browser = this.ctx.getService('browser');
public monaco = {
getTextArea: async () => {
const codeEditor = await this.testSubjects.find('consoleMonacoEditor');
return await codeEditor.findByTagName('textarea');
},
getEditorText: async () => {
const codeEditor = await this.testSubjects.find('consoleMonacoEditor');
const editorViewDiv = await codeEditor.findByClassName('view-lines');
return await editorViewDiv.getVisibleText();
},
getEditorTextAtLine: async (line: number) => {
const codeEditor = await this.testSubjects.find('consoleMonacoEditor');
const editorViewDiv = await codeEditor.findAllByClassName('view-line');
return await editorViewDiv[line].getVisibleText();
},
getCurrentLineNumber: async () => {
const textArea = await this.monaco.getTextArea();
const styleAttribute = (await textArea.getAttribute('style')) ?? '';
const height = parseFloat(styleAttribute.replace(/.*height: ([+-]?\d+(\.\d+)?).*/, '$1'));
const top = parseFloat(styleAttribute.replace(/.*top: ([+-]?\d+(\.\d+)?).*/, '$1'));
// calculate the line number by dividing the top position by the line height
// and adding 1 because line numbers start at 1
return Math.ceil(top / height) + 1;
},
clearEditorText: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.clickMouseButton();
await textArea.clearValueWithKeyboard();
},
getOutputText: async () => {
const outputPanel = await this.testSubjects.find('consoleMonacoOutput');
const outputViewDiv = await outputPanel.findByClassName('monaco-scrollable-element');
return await outputViewDiv.getVisibleText();
},
pressEnter: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys(Key.ENTER);
},
enterText: async (text: string) => {
const textArea = await this.monaco.getTextArea();
await textArea.type(text);
},
promptAutocomplete: async (letter = 'b') => {
const textArea = await this.monaco.getTextArea();
await textArea.type(letter);
await this.retry.waitFor('autocomplete to be visible', () =>
this.monaco.isAutocompleteVisible()
);
},
isAutocompleteVisible: async () => {
const element = await this.find.byClassName('suggest-widget').catch(() => null);
if (!element) return false;
const attribute = await element.getAttribute('style');
return !attribute?.includes('display: none;');
},
getAutocompleteSuggestion: async (index: number) => {
const suggestionsWidget = await this.find.byClassName('suggest-widget');
const suggestions = await suggestionsWidget.findAllByClassName('monaco-list-row');
const label = await suggestions[index].findByClassName('label-name');
return label.getVisibleText();
},
pressUp: async (shift: boolean = false) => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys(shift ? [Key.SHIFT, Key.UP] : Key.UP);
},
pressDown: async (shift: boolean = false) => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys(shift ? [Key.SHIFT, Key.DOWN] : Key.DOWN);
},
pressRight: async (shift: boolean = false) => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys(shift ? [Key.SHIFT, Key.RIGHT] : Key.RIGHT);
},
pressLeft: async (shift: boolean = false) => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys(shift ? [Key.SHIFT, Key.LEFT] : Key.LEFT);
},
pressCtrlSpace: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([
Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'],
Key.SPACE,
]);
},
pressCtrlEnter: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([
Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'],
Key.ENTER,
]);
},
pressCtrlI: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'], 'i']);
},
pressCtrlUp: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([
Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'],
Key.UP,
]);
},
pressCtrlDown: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([
Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'],
Key.DOWN,
]);
},
pressCtrlL: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'], 'l']);
},
pressCtrlSlash: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'], '/']);
},
pressEscape: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys(Key.ESCAPE);
},
selectAllRequests: async () => {
const textArea = await this.monaco.getTextArea();
const selectionKey = Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'];
await textArea.pressKeys([selectionKey, 'a']);
},
getEditor: async () => {
return await this.testSubjects.find('consoleMonacoEditor');
},
hasInvalidSyntax: async () => {
return await this.find.existsByCssSelector('.squiggly-error');
},
responseHasDeprecationWarning: async () => {
const response = await this.monaco.getOutputText();
return response.trim().startsWith('#!');
},
selectCurrentRequest: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.clickMouseButton();
},
getFontSize: async () => {
const codeEditor = await this.testSubjects.find('consoleMonacoEditor');
const editorViewDiv = await codeEditor.findByClassName('view-line');
return await editorViewDiv.getComputedStyle('font-size');
},
pasteClipboardValue: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'], 'v']);
},
copyRequestsToClipboard: async () => {
const textArea = await this.monaco.getTextArea();
await textArea.pressKeys([Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'], 'a']);
await textArea.pressKeys([Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'], 'c']);
},
isA11yOverlayVisible: async () => {
return await this.testSubjects.exists('codeEditorAccessibilityOverlay');
},
};
public async getVisibleTextFromAceEditor(editor: WebElementWrapper) {
const lines = await editor.findAllByClassName('ace_line_group');
const linesText = await Promise.all(lines.map(async (line) => await line.getVisibleText()));

View file

@ -72,5 +72,6 @@
"@kbn/ftr-common-functional-ui-services",
"@kbn/monaco",
"@kbn/search-types",
"@kbn/console-plugin",
]
}

View file

@ -13,5 +13,13 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
return {
...functionalConfig.getAll(),
testFiles: [require.resolve('.')],
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
// disabling the monaco editor to run tests for ace
`--console.dev.enableMonaco=false`,
],
},
};
}