diff --git a/deploy/kubernetes/helm/che/templates/configmap.yaml b/deploy/kubernetes/helm/che/templates/configmap.yaml index bcb44580bc4..a73f8532209 100644 --- a/deploy/kubernetes/helm/che/templates/configmap.yaml +++ b/deploy/kubernetes/helm/che/templates/configmap.yaml @@ -112,3 +112,6 @@ data: JAEGER_SAMPLER_PARAM: "1" JAEGER_REPORTER_MAX_QUEUE_SIZE: "10000" CHE_METRICS_ENABLED: {{ .Values.global.metricsEnabled | quote }} + CHE_WORKSPACE_JAVA__OPTIONS: "-Xmx2000m" + CHE_WORKSPACE_MAVEN__OPTIONS: "-Xmx20000m" + CHE_INFRA_KUBERNETES_WORKSPACE__START__TIMEOUT__MIN: "15" diff --git a/e2e/driver/CheReporter.ts b/e2e/driver/CheReporter.ts index 9b69e1f38a7..8ad7cb2b5cd 100644 --- a/e2e/driver/CheReporter.ts +++ b/e2e/driver/CheReporter.ts @@ -10,11 +10,14 @@ import * as mocha from 'mocha'; import { IDriver } from './IDriver'; import { e2eContainer } from '../inversify.config'; -import { TYPES } from '../inversify.types'; +import { TYPES, CLASSES } from '../inversify.types'; import * as fs from 'fs'; import { TestConstants } from '../TestConstants'; +import { logging } from 'selenium-webdriver'; +import { DriverHelper } from '../utils/DriverHelper'; const driver: IDriver = e2eContainer.get(TYPES.Driver); +const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); class CheReporter extends mocha.reporters.Spec { @@ -65,6 +68,8 @@ class CheReporter extends mocha.reporters.Spec { const testReportDirPath: string = `${reportDirPath}/${testFullTitle}`; const screenshotFileName: string = `${testReportDirPath}/screenshot-${testTitle}.png`; const pageSourceFileName: string = `${testReportDirPath}/pagesource-${testTitle}.html`; + const browserLogsFileName: string = `${testReportDirPath}/browserlogs-${testTitle}.txt`; + // create reporter dir if not exist const reportDirExists: boolean = fs.existsSync(reportDirPath); @@ -91,8 +96,19 @@ class CheReporter extends mocha.reporters.Spec { const pageSourceStream = fs.createWriteStream(pageSourceFileName); pageSourceStream.write(new Buffer(pageSource)); pageSourceStream.end(); - }); + // take browser console logs and write to file + const browserLogsEntries: logging.Entry[] = await driverHelper.getDriver().manage().logs().get('browser'); + let browserLogs: string = ''; + + browserLogsEntries.forEach(log => { + browserLogs += `\"${log.level}\" \"${log.type}\" \"${log.message}\"\n`; + }); + + const browserLogsStream = fs.createWriteStream(browserLogsFileName); + browserLogsStream.write(new Buffer(browserLogs)); + browserLogsStream.end(); + }); } } diff --git a/e2e/files/happy-path/containers-happy-path.yaml b/e2e/files/happy-path/containers-happy-path.yaml new file mode 100644 index 00000000000..e7aef00dcb8 --- /dev/null +++ b/e2e/files/happy-path/containers-happy-path.yaml @@ -0,0 +1,127 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: petclinic +spec: + ports: + - port: 3306 + targetPort: 3306 + selector: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: petclinic +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: db + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: petclinic +spec: + selector: + matchLabels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: petclinic + replicas: 1 + template: + metadata: + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: petclinic + spec: + containers: + - name: mysql + image: centos/mysql-57-centos7 + resources: + requests: + memory: 512Mi + env: + - name: MYSQL_USER + value: petclinic + - name: MYSQL_PASSWORD + value: petclinic + - name: MYSQL_ROOT_PASSWORD + value: petclinic + - name: MYSQL_DATABASE + value: petclinic + ports: + - containerPort: 3306 +--- +apiVersion: v1 +kind: Service +metadata: + name: spring-boot-app + labels: + app.kubernetes.io/name: petclinic + app.kubernetes.io/component: webapp + app.kubernetes.io/part-of: petclinic +spec: + ports: + - port: 8080 + targetPort: 8080 + selector: + app.kubernetes.io/name: petclinic + app.kubernetes.io/component: webapp + app.kubernetes.io/part-of: petclinic +--- +apiVersion: apps/v1 # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1 +kind: Deployment +metadata: + name: spring-boot-app + labels: + app.kubernetes.io/name: petclinic + app.kubernetes.io/component: webapp + app.kubernetes.io/part-of: petclinic +spec: + selector: + matchLabels: + app.kubernetes.io/name: petclinic + app.kubernetes.io/component: webapp + app.kubernetes.io/part-of: petclinic + replicas: 1 + template: + metadata: + labels: + app.kubernetes.io/name: petclinic + app.kubernetes.io/component: webapp + app.kubernetes.io/part-of: petclinic + spec: + containers: + - name: spring-boot + image: mariolet/petclinic:d2831f9b + resources: + requests: + memory: 1000Mi + ports: + - containerPort: 8080 +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: spring-boot-app + labels: + app: spring-petclinic + tier: frontend + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "3600" + nginx.ingress.kubernetes.io/ssl-redirect: "false" +spec: + rules: + - host: 192.168.99.100.nip.io + http: + paths: + - path: / + backend: + serviceName: spring-boot-app + servicePort: 8080 diff --git a/e2e/files/happy-path/happy-path-workspace.yaml b/e2e/files/happy-path/happy-path-workspace.yaml new file mode 100644 index 00000000000..fb7280a04a7 --- /dev/null +++ b/e2e/files/happy-path/happy-path-workspace.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: 1.0.0 +metadata: + name: petclinic-dev-environment +projects: + - name: petclinic + source: + type: git + location: 'https://github.com/spring-projects/spring-petclinic.git' +components: + - type: kubernetes + alias: petclinic-web + reference: https://raw.githubusercontent.com/eclipse/che/selen-happy-4/e2e/files/happy-path/containers-happy-path.yaml + selector: + app.kubernetes.io/component: webapp + entrypoints: + - containerName: spring-boot + command: ["tail"] + args: ["-f", "/dev/null"] + - type: kubernetes + alias: petclinic-db + reference: https://raw.githubusercontent.com/eclipse/che/selen-happy-4/e2e/files/happy-path/containers-happy-path.yaml + selector: + app.kubernetes.io/component: database + - type: dockerimage + alias: maven-container + image: bujhtr5555/maven-with-artifacts:latest + command: ['sleep'] + args: ['infinity'] + env: + - name: MAVEN_CONFIG + value: /home/user/.m2 + memoryLimit: 4Gi + mountSources: true + - type: cheEditor + id: eclipse/che-theia/next + - type: chePlugin + id: redhat/java/latest + - type: chePlugin + id: redhat/vscode-yaml/latest +commands: + - name: build + actions: + - type: exec + component: maven-container + command: mvn clean package >> build.txt + workdir: /projects/petclinic + - name: build-file-output + actions: + - type: exec + component: maven-container + command: 'mkdir -p /projects/petclinic/?/.m2 && cp -r /.m2/* /projects/petclinic/?/.m2 && cd /projects/petclinic && mvn package >> build-output.txt' + workdir: /projects/petclinic + - name: run + actions: + - type: exec + component: maven-container + command: java -jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar --spring.profiles.active=mysql + workdir: /projects/petclinic/target + - name: run-with-changes + actions: + - type: exec + component: maven-container + command: java -jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar --spring.profiles.active=mysql + workdir: /projects/petclinic/target + - name: run-debug + actions: + - type: exec + component: maven-container + command: java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1044 spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar --spring.profiles.active=mysql + workdir: /projects/petclinic/target diff --git a/e2e/inversify.config.ts b/e2e/inversify.config.ts index 4fb725a8732..23cffc46e6e 100644 --- a/e2e/inversify.config.ts +++ b/e2e/inversify.config.ts @@ -28,6 +28,9 @@ import { QuickOpenContainer } from './pageobjects/ide/QuickOpenContainer'; import { PreviewWidget } from './pageobjects/ide/PreviewWidget'; import { GitHubPlugin } from './pageobjects/ide/GitHubPlugin'; import { RightToolbar } from './pageobjects/ide/RightToolbar'; +import { Terminal } from './pageobjects/ide/Terminal'; +import { DebugView } from './pageobjects/ide/DebugView'; +import { WarningDialog } from './pageobjects/ide/WarningDialog'; const e2eContainer = new Container(); @@ -49,5 +52,8 @@ e2eContainer.bind(CLASSES.QuickOpenContainer).to(QuickOpenCo e2eContainer.bind(CLASSES.PreviewWidget).to(PreviewWidget).inSingletonScope(); e2eContainer.bind(CLASSES.GitHubPlugin).to(GitHubPlugin).inSingletonScope(); e2eContainer.bind(CLASSES.RightToolbar).to(RightToolbar).inSingletonScope(); +e2eContainer.bind(CLASSES.Terminal).to(Terminal).inSingletonScope(); +e2eContainer.bind(CLASSES.DebugView).to(DebugView).inSingletonScope(); +e2eContainer.bind(CLASSES.WarningDialog).to(WarningDialog).inSingletonScope(); export { e2eContainer }; diff --git a/e2e/inversify.types.ts b/e2e/inversify.types.ts index 06f1b669e13..20e73e51039 100644 --- a/e2e/inversify.types.ts +++ b/e2e/inversify.types.ts @@ -28,7 +28,10 @@ const CLASSES = { QuickOpenContainer: 'QuickOpenContainer', PreviewWidget: 'PreviewWidget', GitHubPlugin: 'GitHubPlugin', - RightToolbar: 'RightToolbar' + RightToolbar: 'RightToolbar', + Terminal: 'Terminal', + DebugView: 'DebugView', + WarningDialog: 'WarningDialog' }; export { TYPES, CLASSES }; diff --git a/e2e/pageobjects/dashboard/NewWorkspace.ts b/e2e/pageobjects/dashboard/NewWorkspace.ts index 1464aedc0b6..8ed87c3b322 100644 --- a/e2e/pageobjects/dashboard/NewWorkspace.ts +++ b/e2e/pageobjects/dashboard/NewWorkspace.ts @@ -49,8 +49,8 @@ export class NewWorkspace { } async waitPageAbsence(timeout: number = TestConstants.TS_SELENIUM_LOAD_PAGE_TIMEOUT) { - await this.driverHelper.waitDisappearanceTestWithTimeout(By.css(NewWorkspace.NAME_FIELD_CSS), timeout); - await this.driverHelper.waitDisappearanceTestWithTimeout(By.css(NewWorkspace.TITLE_CSS), timeout); + await this.driverHelper.waitDisappearanceWithTimeout(By.css(NewWorkspace.NAME_FIELD_CSS), timeout); + await this.driverHelper.waitDisappearanceWithTimeout(By.css(NewWorkspace.TITLE_CSS), timeout); } async createWorkspaceAndProceedEditing(workspaceName: string, dataStackId: string, sampleName: string) { diff --git a/e2e/pageobjects/ide/DebugView.ts b/e2e/pageobjects/ide/DebugView.ts new file mode 100644 index 00000000000..152c13d503d --- /dev/null +++ b/e2e/pageobjects/ide/DebugView.ts @@ -0,0 +1,39 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +import { inject, injectable } from 'inversify'; +import { CLASSES } from '../../inversify.types'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { By, Key } from 'selenium-webdriver'; +import { Ide } from './Ide'; + + +@injectable() +export class DebugView { + constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper, + @inject(CLASSES.Ide) private readonly ide: Ide) { } + + async clickOnDebugConfigurationDropDown() { + await this.driverHelper.waitAndClick(By.css('select.debug-configuration')); + } + + async clickOnDebugConfigurationItem(itemText: string) { + const configurationItemLocator: By = By.xpath(`//select[@class='debug-configuration']//option[text()=\'${itemText}\']`); + + await this.driverHelper.waitAndClick(configurationItemLocator); + await this.ide.performKeyCombination(Key.ESCAPE); + } + + async clickOnRunDebugButton() { + const runDebugButtonLocator: By = By.xpath('//span[@title=\'Start Debugging\']'); + + await this.driverHelper.waitAndClick(runDebugButtonLocator); + } + +} diff --git a/e2e/pageobjects/ide/Editor.ts b/e2e/pageobjects/ide/Editor.ts index c5a2644dfc2..575326b5d56 100644 --- a/e2e/pageobjects/ide/Editor.ts +++ b/e2e/pageobjects/ide/Editor.ts @@ -12,27 +12,29 @@ import { injectable, inject } from 'inversify'; import { DriverHelper } from '../../utils/DriverHelper'; import { CLASSES } from '../../inversify.types'; import { TestConstants } from '../../TestConstants'; -import { By, Key, error } from 'selenium-webdriver'; +import { By, Key, error, WebElement } from 'selenium-webdriver'; +import { Ide } from './Ide'; @injectable() export class Editor { private static readonly SUGGESTION_WIDGET_BODY_CSS: string = 'div.visible[widgetId=\'editor.widget.suggestWidget\']'; - constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper) { } + constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper, + @inject(CLASSES.Ide) private readonly ide: Ide) { } public async waitSuggestionContainer(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { await this.driverHelper.waitVisibility(By.css(Editor.SUGGESTION_WIDGET_BODY_CSS), timeout); } public async waitSuggestionContainerClosed() { - await this.driverHelper.waitDisappearanceTestWithTimeout(By.css(Editor.SUGGESTION_WIDGET_BODY_CSS)); + await this.driverHelper.waitDisappearanceWithTimeout(By.css(Editor.SUGGESTION_WIDGET_BODY_CSS)); } public async waitSuggestion(editorTabTitle: string, suggestionText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { - const suggestionLocator: By = By.xpath(this.getSuggestionLineXpathLocator(suggestionText)); + const suggestionLocator: By = this.getSuggestionLineXpathLocator(suggestionText); await this.driverHelper.getDriver().wait(async () => { await this.waitSuggestionContainer(); @@ -40,8 +42,7 @@ export class Editor { await this.driverHelper.waitVisibility(suggestionLocator, 5000); return true; } catch (err) { - const isTimeoutError: boolean = err instanceof error.TimeoutError; - if (!isTimeoutError) { + if (!(err instanceof error.TimeoutError)) { throw err; } @@ -61,7 +62,7 @@ export class Editor { } public async clickOnSuggestion(suggestionText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { - await this.driverHelper.waitAndClick(By.xpath(this.getSuggestionLineXpathLocator(suggestionText)), timeout); + await this.driverHelper.waitAndClick(this.getSuggestionLineXpathLocator(suggestionText), timeout); } public async waitTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { @@ -75,6 +76,7 @@ export class Editor { } public async clickOnTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.ide.closeAllNotifications(); await this.driverHelper.waitAndClick(By.xpath(this.getTabXpathLocator(tabTitle)), timeout); } @@ -82,9 +84,13 @@ export class Editor { const focusedTabLocator: By = By.xpath(`//li[contains(@class, 'p-TabBar-tab') and contains(@class, 'theia-mod-active')]//div[text()='${tabTitle}']`); await this.driverHelper.waitVisibility(focusedTabLocator, timeout); + } - // wait for increasing stability - await this.driverHelper.wait(2000); + public async selectTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.ide.closeAllNotifications(); + await this.waitTab(tabTitle, timeout); + await this.clickOnTab(tabTitle, timeout); + await this.waitTabFocused(tabTitle, timeout); } async closeTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { @@ -93,6 +99,19 @@ export class Editor { await this.driverHelper.waitAndClick(tabCloseButtonLocator, timeout); } + async waitTabWithUnsavedStatus(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const unsavedTabLocator: By = this.getTabWithUnsavedStatus(tabTitle); + + await this.driverHelper.waitVisibility(unsavedTabLocator, timeout); + } + + async waitTabWithSavedStatus(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const unsavedTabLocator: By = this.getTabWithUnsavedStatus(tabTitle); + + await this.driverHelper.waitDisappearanceWithTimeout(unsavedTabLocator, timeout); + await this.waitTab(tabTitle, timeout); + } + async waitEditorOpened(editorTabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { const firstEditorLineLocator: By = By.xpath(this.getEditorLineXpathLocator(1)); @@ -105,33 +124,34 @@ export class Editor { await this.waitEditorOpened(tabTitle, timeout); } - async getLineText(lineNumber: number): Promise { + async getLineText(tabTitle: string, lineNumber: number): Promise { const lineIndex: number = lineNumber - 1; - const editorText: string = await this.getEditorVisibleText(); + const editorText: string = await this.getEditorVisibleText(tabTitle); const editorLines: string[] = editorText.split('\n'); const editorLine = editorLines[lineIndex] + '\n'; return editorLine; } - async getEditorVisibleText(): Promise { - const editorBodyLocator: By = By.xpath('//div[@class=\'view-lines\']'); + async getEditorVisibleText(tabTitle: string): Promise { + const editorBodyLocator: By = By.xpath(`//div[contains(@data-uri, \'${tabTitle}')]//div[@class=\'view-lines\']`); + // const editorBodyLocator: By = By.xpath('//div[@class=\'view-lines\']'); const editorText: string = await this.driverHelper.waitAndGetText(editorBodyLocator); return editorText; } - async waitText(expectedText: string, + async waitText(tabTitle: string, expectedText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT, polling: number = TestConstants.TS_SELENIUM_DEFAULT_POLLING) { await this.driverHelper.getDriver().wait(async () => { - const editorText: string = await this.getEditorVisibleText(); + const editorText: string = await this.getEditorVisibleText(tabTitle); const isEditorContainText: boolean = editorText.includes(expectedText); if (isEditorContainText) { return true; } - this.driverHelper.wait(polling); + await this.driverHelper.wait(polling); }, timeout); } @@ -140,9 +160,10 @@ export class Editor { timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT, polling: number = TestConstants.TS_SELENIUM_DEFAULT_POLLING) { + await this.selectTab(editorTabTitle); await this.driverHelper.getDriver().wait(async () => { await this.performKeyCombination(editorTabTitle, Key.chord(Key.CONTROL, Key.END)); - const editorText: string = await this.getEditorVisibleText(); + const editorText: string = await this.getEditorVisibleText(editorTabTitle); const isEditorContainText: boolean = editorText.includes(expectedText); @@ -158,9 +179,6 @@ export class Editor { // set cursor to the 1:1 position await this.performKeyCombination(editorTabTitle, Key.chord(Key.CONTROL, Key.HOME)); - // for ensuring that cursor has been set to the 1:1 position - await this.driverHelper.wait(1000); - // move cursor to line for (let i = 1; i < line; i++) { await this.performKeyCombination(editorTabTitle, Key.ARROW_DOWN); @@ -179,10 +197,127 @@ export class Editor { } async type(editorTabTitle: string, text: string, line: number) { + await this.selectTab(editorTabTitle); await this.moveCursorToLineAndChar(editorTabTitle, line, 1); await this.performKeyCombination(editorTabTitle, text); } + async waitErrorInLine(lineNumber: number, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const errorInLineLocator: By = await this.getErrorInLineLocator(lineNumber); + + await this.driverHelper.waitVisibility(errorInLineLocator, timeout); + } + + async waitErrorInLineDisappearance(lineNumber: number, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const errorInLineLocator: By = await this.getErrorInLineLocator(lineNumber); + + await this.driverHelper.waitDisappearanceWithTimeout(errorInLineLocator, timeout); + } + + async waitStoppedDebugBreakpoint(tabTitle: string, lineNumber: number, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const stoppedDebugBreakpointLocator: By = By.xpath(await this.getStoppedDebugBreakpointXpathLocator(tabTitle, lineNumber)); + + await this.driverHelper.waitVisibility(stoppedDebugBreakpointLocator, timeout); + } + + async waitBreakpoint(tabTitle: string, lineNumber: number, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const debugBreakpointLocator: By = await this.getDebugBreakpointLocator(tabTitle, lineNumber); + + await this.driverHelper.waitVisibility(debugBreakpointLocator, timeout); + } + + async waitBreakpointAbsence(tabTitle: string, lineNumber: number, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const debugBreakpointLocator: By = await this.getDebugBreakpointLocator(tabTitle, lineNumber); + + await this.driverHelper.waitDisappearanceWithTimeout(debugBreakpointLocator, timeout); + } + + async waitBreakpointHint(tabTitle: string, lineNumber: number, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const debugBreakpointHintLocator: By = await this.getDebugBreakpointHintLocator(tabTitle, lineNumber); + + await this.driverHelper.waitVisibility(debugBreakpointHintLocator, timeout); + } + + async waitBreakpointHintDisappearance(tabTitle: string, lineNumber: number, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const debugBreakpointHintLocator: By = await this.getDebugBreakpointHintLocator(tabTitle, lineNumber); + + await this.driverHelper.waitDisappearanceWithTimeout(debugBreakpointHintLocator, timeout); + } + + async activateBreakpoint(tabTitle: string, + lineNumber: number, + timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + + const attempts: number = TestConstants.TS_SELENIUM_DEFAULT_ATTEMPTS; + const polling: number = TestConstants.TS_SELENIUM_DEFAULT_POLLING; + + for (let i = 0; i < attempts; i++) { + const elementLocator: By = await this.getLineNumberBlockLocator(tabTitle, lineNumber); + const element: WebElement = await this.driverHelper.waitVisibility(elementLocator, timeout); + + try { + await this.driverHelper.getAction().mouseMove(element, { x: 5, y: 5 }).perform(); + await this.waitBreakpointHint(tabTitle, lineNumber); + await this.driverHelper.getAction().click().perform(); + await this.waitBreakpoint(tabTitle, lineNumber); + return; + } catch (err) { + if (i === attempts - i) { + throw err(`Exceeded maximum breakpoint activation attempts`); + } + // ignore errors and wait + await this.driverHelper.wait(polling); + } + } + } + + private async getLineYCoordinates(lineNumber: number): Promise { + const lineNumberLocator: By = By.xpath(`//div[contains(@class, 'line-numbers') and text()='${lineNumber}']` + + `//parent::div[contains(@style, 'position')]`); + + let elementStyleValue: string = await this.driverHelper.waitAndGetElementAttribute(lineNumberLocator, 'style'); + + elementStyleValue = elementStyleValue.replace('position: absolute; top: ', ''); + elementStyleValue = elementStyleValue.replace('px; width: 100%; height: 19px;', ''); + + const lineYCoordinate: number = Number.parseInt(elementStyleValue, 10); + + if (Number.isNaN(lineYCoordinate)) { + throw new error.UnsupportedOperationError(`Failed to parse the ${elementStyleValue} row to number format`); + } + + return lineYCoordinate; + } + + private getTabWithUnsavedStatus(tabTitle: string): By { + return By.xpath(`//div[text()='${tabTitle}']/parent::li[contains(@class, 'theia-mod-dirty')]`); + } + + private async getStoppedDebugBreakpointXpathLocator(tabTitle: string, lineNumber: number): Promise { + const lineYPixelCoordinates: number = await this.getLineYCoordinates(lineNumber); + const stoppedDebugBreakpointXpathLocator: string = `//div[contains(@id, '${tabTitle}')]//div[@class='margin']` + + `//div[contains(@style, '${lineYPixelCoordinates}px')]` + + '//div[contains(@class, \'theia-debug-top-stack-frame\')]'; + + return stoppedDebugBreakpointXpathLocator; + } + + private async getDebugBreakpointLocator(tabTitle: string, lineNumber: number): Promise { + const lineYPixelCoordinates: number = await this.getLineYCoordinates(lineNumber); + + return By.xpath(`//div[contains(@id, '${tabTitle}')]//div[@class='margin']` + + `//div[contains(@style, '${lineYPixelCoordinates}px')]` + + '//div[contains(@class, \'theia-debug-breakpoint\')]'); + } + + private async getDebugBreakpointHintLocator(tabTitle: string, lineNumber: number): Promise { + const lineYPixelCoordinates: number = await this.getLineYCoordinates(lineNumber); + + return By.xpath(`//div[contains(@id, '${tabTitle}')]//div[@class='margin']` + + `//div[contains(@style, '${lineYPixelCoordinates}px')]` + + '//div[contains(@class, \'theia-debug-breakpoint-hint\')]'); + } + private getEditorBodyLocator(editorTabTitle: string): By { const editorXpathLocator: string = `//div[@id='theia-main-content-panel']//div[contains(@class, 'monaco-editor')` + ` and contains(@data-uri, '${editorTabTitle}')]//*[contains(@class, 'lines-content')]`; @@ -201,11 +336,24 @@ export class Editor { return `(//div[contains(@class,'lines-content')]//div[@class='view-lines']/div[@class='view-line'])[${lineNumber}]`; } - private getSuggestionLineXpathLocator(suggestionText: string): string { - return `//div[@widgetid='editor.widget.suggestWidget']//div[@class='monaco-list-row']//span[text()='${suggestionText}']`; + private async getLineNumberBlockLocator(tabTitle: string, lineNumber: number): Promise { + const lineYPixelCoordinates: number = await this.getLineYCoordinates(lineNumber); + + return By.xpath(`//div[contains(@id, '${tabTitle}')]//div[@class='margin']` + + `//div[contains(@style, '${lineYPixelCoordinates}px')]`); + } + + private getSuggestionLineXpathLocator(suggestionText: string): By { + return By.xpath(`//div[@widgetid='editor.widget.suggestWidget']//div[@aria-label='${suggestionText}, suggestion, has details']`); } private getTabXpathLocator(tabTitle: string): string { return `//li[contains(@class, 'p-TabBar-tab')]//div[text()='${tabTitle}']`; } + + private async getErrorInLineLocator(lineNumber: number): Promise { + const lineYCoordinates: number = await this.getLineYCoordinates(lineNumber); + + return By.xpath(`//div[contains(@style, 'top:${lineYCoordinates}px')]//div[contains(@class, 'squiggly-error')]`); + } } diff --git a/e2e/pageobjects/ide/GitHubPlugin.ts b/e2e/pageobjects/ide/GitHubPlugin.ts index 10cda54bf84..c53aa2df56e 100644 --- a/e2e/pageobjects/ide/GitHubPlugin.ts +++ b/e2e/pageobjects/ide/GitHubPlugin.ts @@ -3,7 +3,7 @@ import { CLASSES } from '../../inversify.types'; import { DriverHelper } from '../../utils/DriverHelper'; import { TestConstants } from '../../TestConstants'; import { By, WebElement } from 'selenium-webdriver'; -import { Ide } from './Ide'; +import { Ide, RightToolbarButton } from './Ide'; /********************************************************************* * Copyright (c) 2019 Red Hat, Inc. @@ -23,11 +23,11 @@ export class GitHubPlugin { async openGitHubPluginContainer(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { const selectedGitButtonLocator: By = By.xpath(Ide.SELECTED_GIT_BUTTON_XPATH); - await this.ide.waitGitButton(); + await this.ide.waitRightToolbarButton(RightToolbarButton.Git, timeout); const isButtonEnabled: boolean = await this.driverHelper.waitVisibilityBoolean(selectedGitButtonLocator); if (!isButtonEnabled) { - await this.ide.clickOnGitButton(); + await this.ide.waitAndClickRightToolbarButton(RightToolbarButton.Git); } await this.waitGitHubContainer(timeout); diff --git a/e2e/pageobjects/ide/Ide.ts b/e2e/pageobjects/ide/Ide.ts index 36c302e2fad..a3d5706c710 100644 --- a/e2e/pageobjects/ide/Ide.ts +++ b/e2e/pageobjects/ide/Ide.ts @@ -11,16 +11,20 @@ import { DriverHelper } from '../../utils/DriverHelper'; import { injectable, inject } from 'inversify'; import { CLASSES } from '../../inversify.types'; import { TestConstants } from '../../TestConstants'; -import { By, WebElement } from 'selenium-webdriver'; +import { By, WebElement, error } from 'selenium-webdriver'; import { TestWorkspaceUtil, WorkspaceStatus } from '../../utils/workspace/TestWorkspaceUtil'; +export enum RightToolbarButton { + Explorer = 'Explorer', + Git = 'Git', + Debug = 'Debug' +} @injectable() export class Ide { public static readonly EXPLORER_BUTTON_XPATH: string = '(//ul[@class=\'p-TabBar-content\']//li[@title=\'Explorer\'])[1]'; public static readonly SELECTED_EXPLORER_BUTTON_XPATH: string = '(//ul[@class=\'p-TabBar-content\']//li[@title=\'Explorer\' and contains(@class, \'p-mod-current\')])[1]'; public static readonly ACTIVATED_IDE_IFRAME_CSS: string = '#ide-iframe-window[aria-hidden=\'false\']'; - public static readonly GIT_BUTTON_XPATH: string = '(//ul[@class=\'p-TabBar-content\']//li[@title=\'Git\'])[1]'; public static readonly SELECTED_GIT_BUTTON_XPATH: string = '(//ul[@class=\'p-TabBar-content\']//li[@title=\'Git\' and contains(@class, \'p-mod-current\')])[1]'; private static readonly TOP_MENU_PANEL_CSS: string = '#theia-app-shell #theia-top-panel .p-MenuBar-content'; private static readonly LEFT_CONTENT_PANEL_CSS: string = '#theia-left-content-panel'; @@ -31,26 +35,30 @@ export class Ide { @inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper, @inject(CLASSES.TestWorkspaceUtil) private readonly testWorkspaceUtil: TestWorkspaceUtil) { } - async waitGitButton() { - const gitButtonLocator: By = By.xpath(Ide.GIT_BUTTON_XPATH); - - await this.driverHelper.waitVisibility(gitButtonLocator); + async waitAndSwitchToIdeFrame(timeout: number = TestConstants.TS_SELENIUM_LOAD_PAGE_TIMEOUT) { + await this.driverHelper.waitAndSwitchToFrame(By.css(Ide.IDE_IFRAME_CSS), timeout); } - async clickOnGitButton() { - const gitButtonLocator: By = By.xpath(Ide.GIT_BUTTON_XPATH); + async waitNotification(notificationText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const notificationLocator: By = By.xpath(this.getNotificationXpathLocator(notificationText)); - await this.driverHelper.waitAndClick(gitButtonLocator); + await this.driverHelper.waitVisibility(notificationLocator, timeout); } - async waitAndSwitchToIdeFrame(timeout: number = TestConstants.TS_SELENIUM_LOAD_PAGE_TIMEOUT) { - await this.driverHelper.waitAndSwitchToFrame(By.css(Ide.IDE_IFRAME_CSS), timeout); + async waitNotificationAndConfirm(notificationText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.waitNotification(notificationText, timeout); + await this.clickOnNotificationButton(notificationText, 'yes'); } - async waitNotification(notificationText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + async waitNotificationAndOpenLink(notificationText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.waitNotification(notificationText, timeout); + await this.clickOnNotificationButton(notificationText, 'Open Link'); + } + + async isNotificationPresent(notificationText: string): Promise { const notificationLocator: By = By.xpath(this.getNotificationXpathLocator(notificationText)); - await this.driverHelper.waitVisibility(notificationLocator, timeout); + return await this.driverHelper.waitVisibilityBoolean(notificationLocator); } async waitNotificationDisappearance(notificationText: string, @@ -85,12 +93,16 @@ export class Ide { } } - async waitExplorerButton(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { - await this.driverHelper.waitVisibility(By.xpath(Ide.EXPLORER_BUTTON_XPATH), timeout); + async waitRightToolbarButton(buttonTitle: RightToolbarButton, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const buttonLocator: By = this.getRightToolbarButtonLocator(buttonTitle); + + await this.driverHelper.waitVisibility(buttonLocator, timeout); } - async clickOnExplorerButton(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { - await this.driverHelper.waitAndClick(By.xpath(Ide.EXPLORER_BUTTON_XPATH), timeout); + async waitAndClickRightToolbarButton(buttonTitle: RightToolbarButton, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const buttonLocator: By = this.getRightToolbarButtonLocator(buttonTitle); + + await this.driverHelper.waitAndClick(buttonLocator, timeout); } async waitTopMenuPanel(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { @@ -120,25 +132,43 @@ export class Ide { }, timeout); } - async waitStatusBarTextAbcence(expectedText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + async waitStatusBarTextAbsence(expectedText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { const statusBarLocator: By = By.css('div[id=\'theia-statusBar\']'); - await this.driverHelper.getDriver().wait(async () => { - const elementText: string = await this.driverHelper.waitAndGetText(statusBarLocator, timeout); + // for ensuring that check is not invoked in the gap of status displaying + for (let i: number = 0; i < 3; i++) { + await this.driverHelper.getDriver().wait(async () => { + const elementText: string = await this.driverHelper.waitAndGetText(statusBarLocator, timeout); - const isTextAbsent: boolean = elementText.search(expectedText) === -1; + const isTextAbsent: boolean = elementText.search(expectedText) === -1; - if (isTextAbsent) { - return true; - } + if (isTextAbsent) { + return true; + } - }, timeout); + }, timeout); + } } async waitIdeFrameAndSwitchOnIt(timeout: number = TestConstants.TS_SELENIUM_LOAD_PAGE_TIMEOUT) { await this.driverHelper.waitAndSwitchToFrame(By.css(Ide.IDE_IFRAME_CSS), timeout); } + async checkLsInitializationStart(expectedTextInStatusBar: string) { + try { + await this.waitStatusBarContains(expectedTextInStatusBar, 20000); + } catch (err) { + if (!(err instanceof error.TimeoutError)) { + throw err; + } + + await this.driverHelper.getDriver().navigate().refresh(); + await this.waitAndSwitchToIdeFrame(); + await this.waitStatusBarContains(expectedTextInStatusBar); + } + + } + async closeAllNotifications() { const notificationLocator: By = By.css('.theia-Notification'); @@ -162,8 +192,29 @@ export class Ide { } } + async performKeyCombination(keyCombination: string) { + const bodyLocator: By = By.tagName('body'); + + await this.driverHelper.type(bodyLocator, keyCombination); + } + + async waitRightToolbarButtonSelection(buttonTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const selectedRightToolbarButtonLocator: By = this.getSelectedRightToolbarButtonLocator(buttonTitle); + + await this.driverHelper.waitVisibility(selectedRightToolbarButtonLocator, timeout); + } + + private getSelectedRightToolbarButtonLocator(buttonTitle: string): By { + return By.xpath(`//div[@id='theia-left-content-panel']//ul[@class='p-TabBar-content']` + + `//li[@title='${buttonTitle}' and contains(@id, 'shell-tab')] and contains(@class, 'p-mod-current')`); + } + + private getRightToolbarButtonLocator(buttonTitle: String): By { + return By.xpath(`//div[@id='theia-left-content-panel']//ul[@class='p-TabBar-content']` + + `//li[@title='${buttonTitle}' and contains(@id, 'shell-tab')]`); + } + private getNotificationXpathLocator(notificationText: string): string { return `//div[@class='theia-Notification' and contains(@id,'${notificationText}')]`; } - } diff --git a/e2e/pageobjects/ide/PreviewWidget.ts b/e2e/pageobjects/ide/PreviewWidget.ts index ac440f16cd8..51f29fea537 100644 --- a/e2e/pageobjects/ide/PreviewWidget.ts +++ b/e2e/pageobjects/ide/PreviewWidget.ts @@ -1,10 +1,3 @@ -import { injectable, inject } from 'inversify'; -import { CLASSES } from '../../inversify.types'; -import { DriverHelper } from '../../utils/DriverHelper'; -import { By } from 'selenium-webdriver'; -import { TestConstants } from '../../TestConstants'; -import { Ide } from './Ide'; - /********************************************************************* * Copyright (c) 2019 Red Hat, Inc. * @@ -14,6 +7,12 @@ import { Ide } from './Ide'; * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ +import { injectable, inject } from 'inversify'; +import { CLASSES } from '../../inversify.types'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { By } from 'selenium-webdriver'; +import { TestConstants } from '../../TestConstants'; +import { Ide } from './Ide'; @injectable() export class PreviewWidget { @@ -50,17 +49,29 @@ export class PreviewWidget { return true; } - await this.driverHelper.getDriver().switchTo().defaultContent(); - await this.ide.waitAndSwitchToIdeFrame(); + await this.switchBackToIdeFrame(); await this.refreshPage(); await this.waitAndSwitchToWidgetFrame(); await this.driverHelper.wait(polling); }, timeout); } + async waitVisibility(element: By, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.driverHelper.waitVisibility(element, timeout); + } + + async waitAndClick(element: By, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.driverHelper.waitAndClick(element, timeout); + } + async refreshPage() { const refreshButtonLocator: By = By.css('.theia-mini-browser .theia-mini-browser-refresh'); await this.driverHelper.waitAndClick(refreshButtonLocator); } + async switchBackToIdeFrame() { + await this.driverHelper.getDriver().switchTo().defaultContent(); + await this.ide.waitAndSwitchToIdeFrame(); + } + } diff --git a/e2e/pageobjects/ide/ProjectTree.ts b/e2e/pageobjects/ide/ProjectTree.ts index df152909214..d6d45d798f2 100644 --- a/e2e/pageobjects/ide/ProjectTree.ts +++ b/e2e/pageobjects/ide/ProjectTree.ts @@ -11,7 +11,7 @@ import 'reflect-metadata'; import { injectable, inject } from 'inversify'; import { DriverHelper } from '../../utils/DriverHelper'; import { CLASSES } from '../../inversify.types'; -import { Ide } from './Ide'; +import { Ide, RightToolbarButton } from './Ide'; import { TestConstants } from '../../TestConstants'; import { By } from 'selenium-webdriver'; import { Editor } from './Editor'; @@ -28,12 +28,12 @@ export class ProjectTree { async openProjectTreeContainer(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { const selectedExplorerButtonLocator: By = By.xpath(Ide.SELECTED_EXPLORER_BUTTON_XPATH); - await this.ide.waitExplorerButton(timeout); + await this.ide.waitRightToolbarButton(RightToolbarButton.Explorer, timeout); const isButtonEnabled: boolean = await this.driverHelper.waitVisibilityBoolean(selectedExplorerButtonLocator); if (!isButtonEnabled) { - await this.ide.clickOnExplorerButton(); + await this.ide.waitAndClickRightToolbarButton(RightToolbarButton.Explorer, timeout); } await this.waitProjectTreeContainer(); diff --git a/e2e/pageobjects/ide/QuickOpenContainer.ts b/e2e/pageobjects/ide/QuickOpenContainer.ts index 65ea7cda585..f7b5b02509f 100644 --- a/e2e/pageobjects/ide/QuickOpenContainer.ts +++ b/e2e/pageobjects/ide/QuickOpenContainer.ts @@ -16,7 +16,6 @@ import { By } from 'selenium-webdriver'; @injectable() export class QuickOpenContainer { - constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper) { } public async waitContainer(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { @@ -37,4 +36,8 @@ export class QuickOpenContainer { await this.waitContainerDisappearance(); } + public async type(text: string) { + await this.driverHelper.enterValue(By.css('.quick-open-input input'), text); + } + } diff --git a/e2e/pageobjects/ide/Terminal.ts b/e2e/pageobjects/ide/Terminal.ts new file mode 100644 index 00000000000..dcdf996de6a --- /dev/null +++ b/e2e/pageobjects/ide/Terminal.ts @@ -0,0 +1,109 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +import { injectable, inject } from 'inversify'; +import { CLASSES } from '../../inversify.types'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { By, Key, WebElement, error } from 'selenium-webdriver'; +import { TestConstants } from '../../TestConstants'; + +@injectable() +export class Terminal { + constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper) { } + + async waitTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const terminalTabLocator: By = By.css(this.getTerminalTabCssLocator(tabTitle)); + + await this.driverHelper.waitVisibility(terminalTabLocator, timeout); + } + + async waitTabAbsence(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const terminalTabLocator: By = By.css(this.getTerminalTabCssLocator(tabTitle)); + + await this.driverHelper.waitDisappearanceWithTimeout(terminalTabLocator, timeout); + } + + async clickOnTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const terminalTabLocator: By = By.css(this.getTerminalTabCssLocator(tabTitle)); + + await this, this.driverHelper.waitAndClick(terminalTabLocator, timeout); + } + + async waitTabFocused(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const focusedTerminalTabLocator: By = this.getFocusedTerminalTabLocator(tabTitle); + + await this.driverHelper.waitVisibility(focusedTerminalTabLocator, timeout); + } + + async selectTerminalTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.clickOnTab(tabTitle, timeout); + await this.waitTabFocused(tabTitle, timeout); + } + + async clickOnTabCloseIcon(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const terminalTabCloseIconLocator: By = + By.css(`${this.getTerminalTabCssLocator(tabTitle)} div.p-TabBar-tabCloseIcon`); + + await this.driverHelper.waitAndClick(terminalTabCloseIconLocator, timeout); + } + + async closeTerminalTab(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.clickOnTabCloseIcon(tabTitle, timeout); + await this.waitTabAbsence(tabTitle, timeout); + } + + async type(terminalTabTitle: string, text: string) { + const terminalIndex: number = await this.getTerminalIndex(terminalTabTitle); + const terminalInteractionContainer: By = this.getTerminalEditorInteractionEditorLocator(terminalIndex); + + await this.driverHelper.typeToInvisible(terminalInteractionContainer, text); + } + + async rejectTerminalProcess(tabTitle: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.selectTerminalTab(tabTitle, timeout); + await this.type(tabTitle, Key.chord(Key.CONTROL, 'c')); + } + + private getTerminalTabCssLocator(tabTitle: string): string { + return `li[title='${tabTitle}']`; + } + + private getFocusedTerminalTabLocator(tabTitle: string): By { + return By.css(`li[title='${tabTitle}'].p-mod-current.theia-mod-active`); + } + + private async getTerminalIndex(terminalTitle: string): Promise { + const terminalTabTitleXpathLocator: string = `//div[@id='theia-bottom-content-panel']` + + `//li[contains(@id, 'shell-tab-terminal') or contains(@id, 'shell-tab-plugin')]` + + `//div[@class='p-TabBar-tabLabel']`; + + const terminalTabs: WebElement[] = await this.driverHelper.waitAllPresence(By.xpath(terminalTabTitleXpathLocator)); + let terminalTitles: string[] = []; + + + for (let i: number = 1; i <= terminalTabs.length; i++) { + const terminalTabLocator: By = By.xpath(`(${terminalTabTitleXpathLocator})[${i}]`); + const currentTerminalTitle: string = await this.driverHelper.waitAndGetText(terminalTabLocator); + + if (currentTerminalTitle.search(terminalTitle) > -1) { + return i; + } + + terminalTitles.push(currentTerminalTitle); + } + + throw new error.WebDriverError(`The terminal with title '${terminalTitle}' has not been found.\n` + + `List of the tabs:\n${terminalTitles}`); + } + + private getTerminalEditorInteractionEditorLocator(terminalIndex: number): By { + return By.xpath(`(//textarea[@aria-label='Terminal input'])[${terminalIndex}]`); + } + +} diff --git a/e2e/pageobjects/ide/TopMenu.ts b/e2e/pageobjects/ide/TopMenu.ts index 48b00ec642e..5cdb25a46e6 100644 --- a/e2e/pageobjects/ide/TopMenu.ts +++ b/e2e/pageobjects/ide/TopMenu.ts @@ -3,6 +3,7 @@ import { CLASSES } from '../../inversify.types'; import { DriverHelper } from '../../utils/DriverHelper'; import { TestConstants } from '../../TestConstants'; import { By } from 'selenium-webdriver'; +import { Ide } from './Ide'; /********************************************************************* * Copyright (c) 2019 Red Hat, Inc. @@ -18,7 +19,8 @@ import { By } from 'selenium-webdriver'; export class TopMenu { private static readonly TOP_MENU_BUTTONS: string[] = ['File', 'Edit', 'Selection', 'View', 'Go', 'Debug', 'Terminal', 'Help']; - constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper) { } + constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper, + @inject(CLASSES.Ide) private readonly ide: Ide) { } public async waitTopMenu(timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { for (const buttonText of TopMenu.TOP_MENU_BUTTONS) { @@ -27,8 +29,15 @@ export class TopMenu { } } + public async selectOption(topMenuButtonText: string, submenuItemtext: string) { + await this.clickOnTopMenuButton(topMenuButtonText); + await this.clickOnSubmenuItem(submenuItemtext); + } + public async clickOnTopMenuButton(buttonText: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { const buttonLocator: By = this.getTopMenuButtonLocator(buttonText); + + await this.ide.closeAllNotifications(); await this.driverHelper.waitAndClick(buttonLocator, timeout); } diff --git a/e2e/pageobjects/ide/WarningDialog.ts b/e2e/pageobjects/ide/WarningDialog.ts new file mode 100644 index 00000000000..c76b4976a26 --- /dev/null +++ b/e2e/pageobjects/ide/WarningDialog.ts @@ -0,0 +1,45 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +import { injectable, inject } from 'inversify'; +import { CLASSES } from '../../inversify.types'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { By } from 'selenium-webdriver'; + +@injectable() +export class WarningDialog { + private static readonly DIALOG_BODY_XPATH_LOCATOR: string = '//div[@id=\'theia-dialog-shell\']//div[@class=\'dialogBlock\']'; + private static readonly CLOSE_BUTTON_XPATH_LOCATOR: string = `${WarningDialog.DIALOG_BODY_XPATH_LOCATOR}//button[text()='close']`; + + constructor(@inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper) { } + + async dialogDisplayes(): Promise { + + return await this.driverHelper.isVisible(By.xpath(WarningDialog.DIALOG_BODY_XPATH_LOCATOR)); + } + + async waitAndCloseIfAppear() { + const dialogDisplayes: boolean = await this.driverHelper.waitVisibilityBoolean(By.xpath(WarningDialog.DIALOG_BODY_XPATH_LOCATOR)); + + if (dialogDisplayes) { + await this.closeDialog(); + await this.waitDialogDissappearance(); + } + + } + + async closeDialog() { + await this.driverHelper.waitAndClick(By.xpath(WarningDialog.CLOSE_BUTTON_XPATH_LOCATOR)); + } + + async waitDialogDissappearance() { + await this.driverHelper.waitDisappearanceWithTimeout(By.xpath(WarningDialog.CLOSE_BUTTON_XPATH_LOCATOR)); + } + +} diff --git a/e2e/tests/e2e/WorkspaceCreationAndLsInitialization.spec.ts b/e2e/tests/e2e/WorkspaceCreationAndLsInitialization.spec.ts index 3214be295b9..26349c9708e 100644 --- a/e2e/tests/e2e/WorkspaceCreationAndLsInitialization.spec.ts +++ b/e2e/tests/e2e/WorkspaceCreationAndLsInitialization.spec.ts @@ -68,7 +68,7 @@ suite('E2E', async () => { test('Check "Java Language Server" initialization by statusbar', async () => { await ide.waitStatusBarContains('Starting Java Language Server'); - await ide.waitStatusBarTextAbcence('Starting Java Language Server'); + await ide.waitStatusBarTextAbsence('Starting Java Language Server'); }); test('Check "Java Language Server" initialization by suggestion invoking', async () => { diff --git a/e2e/tests/e2e_happy_path/HappyPath.spec.ts b/e2e/tests/e2e_happy_path/HappyPath.spec.ts index cee85d2ae0b..a9e2cf313db 100644 --- a/e2e/tests/e2e_happy_path/HappyPath.spec.ts +++ b/e2e/tests/e2e_happy_path/HappyPath.spec.ts @@ -11,16 +11,18 @@ import { e2eContainer } from '../../inversify.config'; import { DriverHelper } from '../../utils/DriverHelper'; import { CLASSES } from '../../inversify.types'; -import { Ide } from '../../pageobjects/ide/Ide'; +import { Ide, RightToolbarButton } from '../../pageobjects/ide/Ide'; import { ProjectTree } from '../../pageobjects/ide/ProjectTree'; import { TopMenu } from '../../pageobjects/ide/TopMenu'; import { QuickOpenContainer } from '../../pageobjects/ide/QuickOpenContainer'; import { Editor } from '../../pageobjects/ide/Editor'; import { PreviewWidget } from '../../pageobjects/ide/PreviewWidget'; -import { GitHubPlugin } from '../../pageobjects/ide/GitHubPlugin'; import { TestConstants } from '../../TestConstants'; import { RightToolbar } from '../../pageobjects/ide/RightToolbar'; -import { By } from 'selenium-webdriver'; +import { By, Key, error } from 'selenium-webdriver'; +import { Terminal } from '../../pageobjects/ide/Terminal'; +import { DebugView } from '../../pageobjects/ide/DebugView'; +import { WarningDialog } from '../../pageobjects/ide/WarningDialog'; const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); const ide: Ide = e2eContainer.get(CLASSES.Ide); @@ -29,67 +31,124 @@ const topMenu: TopMenu = e2eContainer.get(CLASSES.TopMenu); const quickOpenContainer: QuickOpenContainer = e2eContainer.get(CLASSES.QuickOpenContainer); const editor: Editor = e2eContainer.get(CLASSES.Editor); const previewWidget: PreviewWidget = e2eContainer.get(CLASSES.PreviewWidget); -const githubPlugin: GitHubPlugin = e2eContainer.get(CLASSES.GitHubPlugin); const rightToolbar: RightToolbar = e2eContainer.get(CLASSES.RightToolbar); +const terminal: Terminal = e2eContainer.get(CLASSES.Terminal); +const debugView: DebugView = e2eContainer.get(CLASSES.DebugView); +const warningDialog: WarningDialog = e2eContainer.get(CLASSES.WarningDialog); const projectName: string = 'petclinic'; const namespace: string = TestConstants.TS_SELENIUM_USERNAME; const workspaceName: string = TestConstants.TS_SELENIUM_HAPPY_PATH_WORKSPACE_NAME; const workspaceUrl: string = `${TestConstants.TS_SELENIUM_BASE_URL}/dashboard/#/ide/${namespace}/${workspaceName}`; const pathToJavaFolder: string = `${projectName}/src/main/java/org/springframework/samples/petclinic`; +const pathToChangedJavaFileFolder: string = `${projectName}/src/main/java/org/springframework/samples/petclinic/system`; const javaFileName: string = 'PetClinicApplication.java'; +const changedJavaFileName: string = 'CrashController.java'; +const textForErrorMessageChange: string = 'HHHHHHHHHHHHH'; +const codeNavigationClassName: string = 'SpringApplication.class'; const pathToYamlFolder: string = projectName; const yamlFileName: string = 'devfile.yaml'; -const expectedGithubChanges: string = '_remote.repositories %3F/.m2/repository/antlr/antlr/2.7.7\n' + 'U'; -const springTitleLocator: By = By.xpath('//div[@class=\'container-fluid\']//h2[text()=\'Welcome\']'); +const SpringAppLocators = { + springTitleLocator: By.xpath('//div[@class=\'container-fluid\']//h2[text()=\'Welcome\']'), + springMenuButtonLocator: By.css('button[data-target=\'#main-navbar\']'), + springErrorButtonLocator: By.xpath('//div[@id=\'main-navbar\']//span[text()=\'Error\']'), + springErrorMessageLocator: By.xpath('//p[text()=\'Expected: controller used to ' + + `showcase what happens when an exception is thrown${textForErrorMessageChange}\']`) +}; -suite('Ide checks', async () => { - test('Build application', async () => { + +suite('Validation of workspace start, build and run', async () => { + test('Open workspace', async () => { await driverHelper.navigateTo(workspaceUrl); + }); + + test('The \"#13681\" bug workaround', async () => { + await waitGwtIdeLaunching(); + + await driverHelper.getDriver().navigate().refresh(); await ide.waitWorkspaceAndIde(namespace, workspaceName); + }); + + test.skip('Wait workspace running state', async () => { + await ide.waitWorkspaceAndIde(namespace, workspaceName); + }); + + test('Wait until project is imported', async () => { await projectTree.openProjectTreeContainer(); await projectTree.waitProjectImported(projectName, 'src'); await projectTree.expandItem(`/${projectName}`); - await topMenu.waitTopMenu(); - await ide.closeAllNotifications(); - await topMenu.clickOnTopMenuButton('Terminal'); - await topMenu.clickOnSubmenuItem('Run Task...'); + }); + + test('Build application', async () => { + await topMenu.selectOption('Terminal', 'Run Task...'); await quickOpenContainer.clickOnContainerItem('che: build-file-output'); await projectTree.expandPathAndOpenFile(projectName, 'build-output.txt'); - await editor.waitEditorAvailable('build-output.txt'); - await editor.clickOnTab('build-output.txt'); - await editor.waitTabFocused('build-output.txt'); await editor.followAndWaitForText('build-output.txt', '[INFO] BUILD SUCCESS', 180000, 5000); }); test('Run application', async () => { - await topMenu.waitTopMenu(); - await ide.closeAllNotifications(); - await topMenu.clickOnTopMenuButton('Terminal'); - await topMenu.clickOnSubmenuItem('Run Task...'); + await topMenu.selectOption('Terminal', 'Run Task...'); await quickOpenContainer.clickOnContainerItem('che: run'); - await ide.waitNotification('A new process is now listening on port 8080', 120000); - await ide.clickOnNotificationButton('A new process is now listening on port 8080', 'yes'); + await ide.waitNotificationAndConfirm('A new process is now listening on port 8080', 120000); + await ide.waitNotificationAndOpenLink('Redirect is now enabled on port 8080', 120000); + }); + + test('Check the running application', async () => { + await previewWidget.waitContentAvailable(SpringAppLocators.springTitleLocator, 60000, 10000); + }); - await ide.waitNotification('Redirect is now enabled on port 8080', 120000); - await ide.clickOnNotificationButton('Redirect is now enabled on port 8080', 'Open Link'); - await previewWidget.waitContentAvailable(springTitleLocator, 60000, 10000); + test('Close preview widget', async () => { await rightToolbar.clickOnToolIcon('Preview'); await previewWidget.waitPreviewWidgetAbsence(); }); + test('Close the terminal running tasks', async () => { + await terminal.closeTerminalTab('build-file-output'); + await terminal.rejectTerminalProcess('run'); + await terminal.closeTerminalTab('run'); + + await warningDialog.waitAndCloseIfAppear(); + }); +}); + +suite('Language server validation', async () => { test('Java LS initialization', async () => { await projectTree.expandPathAndOpenFile(pathToJavaFolder, javaFileName); - await editor.waitEditorAvailable(javaFileName); - await editor.clickOnTab(javaFileName); - await editor.waitTabFocused(javaFileName); - await ide.waitStatusBarTextAbcence('Starting Java Language Server', 360000); + await editor.selectTab(javaFileName); + + await ide.checkLsInitializationStart('Starting Java Language Server'); + await ide.waitStatusBarTextAbsence('Starting Java Language Server', 360000); + await checkJavaPathCompletion(); + await ide.waitStatusBarTextAbsence('Building workspace', 360000); + }); + + test('Error highlighting', async () => { + await editor.type(javaFileName, 'textForErrorHighlighting', 30); + await editor.waitErrorInLine(30); + await editor.performKeyCombination(javaFileName, Key.chord(Key.CONTROL, 'z')); + await editor.waitErrorInLineDisappearance(30); + }); + + test('Autocomplete', async () => { + await editor.moveCursorToLineAndChar(javaFileName, 32, 17); + await editor.pressControlSpaceCombination(javaFileName); + await editor.waitSuggestionContainer(); + await editor.waitSuggestion(javaFileName, 'SpringApplication - org.springframework.boot'); + }); + + test('Suggestion', async () => { await editor.moveCursorToLineAndChar(javaFileName, 32, 27); await editor.pressControlSpaceCombination(javaFileName); - await editor.waitSuggestion(javaFileName, 'run(Class primarySource, String... args) : ConfigurableApplicationContext', 40000); + await editor.waitSuggestion(javaFileName, 'run(Class primarySource, String... args) : ConfigurableApplicationContext'); + }); + + test('Codenavigation', async () => { + await editor.moveCursorToLineAndChar(javaFileName, 32, 17); + await editor.performKeyCombination(javaFileName, Key.chord(Key.CONTROL, Key.F12)); + await editor.waitEditorAvailable(codeNavigationClassName); }); test.skip('Yaml LS initialization', async () => { @@ -99,11 +158,131 @@ suite('Ide checks', async () => { await editor.waitTabFocused(yamlFileName); await ide.waitStatusBarContains('Starting Yaml Language Server'); await ide.waitStatusBarContains('100% Starting Yaml Language Server'); - await ide.waitStatusBarTextAbcence('Starting Yaml Language Server'); + await ide.waitStatusBarTextAbsence('Starting Yaml Language Server'); }); +}); + +suite('Display source code changes in the running application', async () => { + test('Change source code', async () => { + await projectTree.expandPathAndOpenFile(pathToChangedJavaFileFolder, changedJavaFileName); + await editor.waitEditorAvailable(changedJavaFileName); + await editor.clickOnTab(changedJavaFileName); + await editor.waitTabFocused(changedJavaFileName); - test.skip('Github plugin initialization', async () => { - await githubPlugin.openGitHubPluginContainer(); - await githubPlugin.waitChangesPresence(expectedGithubChanges); + await editor.moveCursorToLineAndChar(changedJavaFileName, 34, 55); + await editor.performKeyCombination(changedJavaFileName, textForErrorMessageChange); + await editor.performKeyCombination(changedJavaFileName, Key.chord(Key.CONTROL, 's')); + }); + + test('Build application with changes', async () => { + await topMenu.selectOption('Terminal', 'Run Task...'); + await quickOpenContainer.clickOnContainerItem('che: build'); + + await projectTree.expandPathAndOpenFile(projectName, 'build.txt'); + await editor.waitEditorAvailable('build.txt'); + await editor.clickOnTab('build.txt'); + await editor.waitTabFocused('build.txt'); + await editor.followAndWaitForText('build.txt', '[INFO] BUILD SUCCESS', 180000, 5000); + }); + + test('Run application with changes', async () => { + await topMenu.selectOption('Terminal', 'Run Task...'); + await quickOpenContainer.clickOnContainerItem('che: run-with-changes'); + + await ide.waitNotificationAndConfirm('A new process is now listening on port 8080', 120000); + await ide.waitNotificationAndOpenLink('Redirect is now enabled on port 8080', 120000); + }); + + test('Check changes are displayed', async () => { + await previewWidget.waitContentAvailable(SpringAppLocators.springTitleLocator, 60000, 10000); + await previewWidget.waitAndSwitchToWidgetFrame(); + await previewWidget.waitAndClick(SpringAppLocators.springMenuButtonLocator); + await previewWidget.waitAndClick(SpringAppLocators.springErrorButtonLocator); + await previewWidget.waitVisibility(SpringAppLocators.springErrorMessageLocator); + await previewWidget.switchBackToIdeFrame(); + }); + + test('Close preview widget', async () => { + await rightToolbar.clickOnToolIcon('Preview'); + await previewWidget.waitPreviewWidgetAbsence(); + }); + + test('Close running terminal processes and tabs', async () => { + await terminal.rejectTerminalProcess('run-with-changes'); + await terminal.closeTerminalTab('run-with-changes'); + + await warningDialog.waitAndCloseIfAppear(); }); }); + +suite('Validation of debug functionality', async () => { + test('Open file and activate breakpoint', async () => { + await projectTree.expandPathAndOpenFile(pathToJavaFolder, javaFileName); + await editor.selectTab(javaFileName); + await editor.moveCursorToLineAndChar(javaFileName, 34, 1); + await editor.activateBreakpoint(javaFileName, 32); + }); + + test('Launch debug', async () => { + await topMenu.selectOption('Terminal', 'Run Task...'); + await quickOpenContainer.clickOnContainerItem('che: run-debug'); + + await ide.waitNotificationAndConfirm('A new process is now listening on port 8080', 120000); + await ide.waitNotificationAndOpenLink('Redirect is now enabled on port 8080', 120000); + }); + + test('Check content of the launched application', async () => { + await previewWidget.waitContentAvailable(SpringAppLocators.springErrorMessageLocator, 60000, 10000); + }); + + test('Open debug configuration file', async () => { + await topMenu.selectOption('Debug', 'Open Configurations'); + await editor.waitEditorAvailable('launch.json'); + await editor.selectTab('launch.json'); + }); + + test('Add debug configuration options', async () => { + await editor.moveCursorToLineAndChar('launch.json', 5, 22); + await editor.performKeyCombination('launch.json', Key.chord(Key.CONTROL, Key.SPACE)); + await editor.clickOnSuggestion('Java: Launch Program in Current File'); + await editor.waitTabWithUnsavedStatus('launch.json'); + await editor.waitText('launch.json', '\"name\": \"Debug (Launch) - Current File\"'); + await editor.performKeyCombination('launch.json', Key.chord(Key.CONTROL, 's')); + await editor.waitTabWithSavedStatus('launch.json'); + }); + + test('Run debug and check application stop in the breakpoint', async () => { + await editor.selectTab(javaFileName); + await topMenu.selectOption('View', 'Debug'); + await ide.waitRightToolbarButton(RightToolbarButton.Debug); + await debugView.clickOnDebugConfigurationDropDown(); + await debugView.clickOnDebugConfigurationItem('Debug (Launch) - Current File'); + await debugView.clickOnRunDebugButton(); + + await previewWidget.refreshPage(); + await editor.waitStoppedDebugBreakpoint(javaFileName, 32); + }); +}); + +async function checkJavaPathCompletion() { + if (await ide.isNotificationPresent('Classpath is incomplete. Only syntax errors will be reported')) { + throw new Error('Known issue: https://github.com/eclipse/che/issues/13427 \n' + + '\"Java LS \"Classpath is incomplete\" warning when loading petclinic\"'); + } +} + +async function waitGwtIdeLaunching(timeout: number = TestConstants.TS_SELENIUM_START_WORKSPACE_TIMEOUT) { + const launchedGwtIdeLocator: By = By.xpath('//div[@id=\'gwt-debug-consolesPanel\']//td[text()=\'Your workspace is ready to be used\']'); + + await ide.waitAndSwitchToIdeFrame(timeout); + try { + await driverHelper.waitVisibility(launchedGwtIdeLocator, timeout); + } catch (err) { + if (err instanceof error.TimeoutError) { + throw new error.TimeoutError('This failure probably happened, because bug #13681 has been fixed. ' + + 'If that\'s really the case, please remove this workaround too.'); + } + + throw err; + } +} diff --git a/e2e/utils/DriverHelper.ts b/e2e/utils/DriverHelper.ts index aa8cbde5a3f..d8aa8376e82 100644 --- a/e2e/utils/DriverHelper.ts +++ b/e2e/utils/DriverHelper.ts @@ -158,7 +158,7 @@ export class DriverHelper { } } - public async waitDisappearanceTestWithTimeout(elementLocator: By, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + public async waitDisappearanceWithTimeout(elementLocator: By, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { await this.getDriver().wait(async () => { const isVisible: boolean = await this.isVisible(elementLocator); @@ -232,6 +232,32 @@ export class DriverHelper { throw new Error(`Exceeded maximum gettin of the '${attribute}' attribute attempts, from the '${elementLocator}' element`); } + public async waitAndGetCssValue(elementLocator: By, + cssAttribute: string, + visibilityTimeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT): Promise { + + const attempts: number = TestConstants.TS_SELENIUM_DEFAULT_ATTEMPTS; + const polling: number = TestConstants.TS_SELENIUM_DEFAULT_POLLING; + + for (let i = 0; i < attempts; i++) { + const element: WebElement = await this.waitVisibility(elementLocator, visibilityTimeout); + + try { + const cssAttributeValue = await element.getCssValue(cssAttribute); + return cssAttributeValue; + } catch (err) { + if (err instanceof error.StaleElementReferenceError) { + await this.wait(polling); + continue; + } + + throw err; + } + } + + throw new Error(`Exceeded maximum gettin of the '${cssAttribute}' css attribute attempts, from the '${elementLocator}' element`); + } + public async waitAttributeValue(elementLocator: By, attribute: string, expectedValue: string, @@ -269,6 +295,29 @@ export class DriverHelper { throw new Error(`Exceeded maximum typing attempts, to the '${elementLocator}' element`); } + public async typeToInvisible(elementLocator: By, text: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + const attempts: number = TestConstants.TS_SELENIUM_DEFAULT_ATTEMPTS; + const polling: number = TestConstants.TS_SELENIUM_DEFAULT_POLLING; + + for (let i = 0; i < attempts; i++) { + const element: WebElement = await this.waitPresence(elementLocator, timeout); + + try { + await element.sendKeys(text); + return; + } catch (err) { + if (err instanceof error.StaleElementReferenceError) { + await this.wait(polling); + continue; + } + + throw err; + } + } + + throw new Error(`Exceeded maximum typing attempts, to the '${elementLocator}' element`); + } + public async clear(elementLocator: By, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { const attempts: number = TestConstants.TS_SELENIUM_DEFAULT_ATTEMPTS; const polling: number = TestConstants.TS_SELENIUM_DEFAULT_POLLING; @@ -342,6 +391,18 @@ export class DriverHelper { public async navigateTo(url: string) { await this.driver.navigate().to(url); + await this.waitURL(url); + } + + public async waitURL(expectedUrl: string, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) { + await this.getDriver().wait(async () => { + const currentUrl: string = await this.getDriver().getCurrentUrl(); + const urlEquals: boolean = currentUrl === expectedUrl; + + if (urlEquals) { + return true; + } + }); } public async scrollTo(elementLocator: By, timeout: number = TestConstants.TS_SELENIUM_DEFAULT_TIMEOUT) {