diff --git a/test/e2e/infra/index.ts b/test/e2e/infra/index.ts index d0e9adb62bd..19166e9ebc9 100644 --- a/test/e2e/infra/index.ts +++ b/test/e2e/infra/index.ts @@ -36,6 +36,7 @@ export * from '../pages/extensions'; export * from '../pages/editors'; export * from '../pages/settings'; export * from '../pages/debug'; +export * from '../pages/problems'; // fixtures export * from './fixtures/userSettings'; diff --git a/test/e2e/infra/test-runner/test-tags.ts b/test/e2e/infra/test-runner/test-tags.ts index c450d9bef60..cee4cbfc87a 100644 --- a/test/e2e/infra/test-runner/test-tags.ts +++ b/test/e2e/infra/test-runner/test-tags.ts @@ -46,6 +46,7 @@ export enum TestTags { TOP_ACTION_BAR = '@:top-action-bar', VARIABLES = '@:variables', WELCOME = '@:welcome', + PROBLEMS = '@:problems', // platform tags WEB = '@:web', diff --git a/test/e2e/infra/workbench.ts b/test/e2e/infra/workbench.ts index 06fdd7f6a22..783a143e4c8 100644 --- a/test/e2e/infra/workbench.ts +++ b/test/e2e/infra/workbench.ts @@ -33,6 +33,7 @@ import { Extensions } from '../pages/extensions'; import { Settings } from '../pages/settings'; import { Debug } from '../pages/debug'; import { EditorActionBar } from '../pages/editorActionBar'; +import { Problems } from '../pages/problems'; export interface Commands { runCommand(command: string, options?: { exactLabelMatch?: boolean }): Promise; @@ -69,6 +70,7 @@ export class Workbench { readonly settings: Settings; readonly debug: Debug; readonly editorActionBar: EditorActionBar; + readonly problems: Problems; constructor(code: Code) { @@ -102,6 +104,7 @@ export class Workbench { this.settings = new Settings(code, this.editors, this.editor, this.quickaccess); this.debug = new Debug(code); this.editorActionBar = new EditorActionBar(code.driver.page, this.viewer, this.quickaccess); + this.problems = new Problems(code, this.quickaccess); } } diff --git a/test/e2e/pages/editor.ts b/test/e2e/pages/editor.ts index f67630fb9c7..b2e4150ccbb 100644 --- a/test/e2e/pages/editor.ts +++ b/test/e2e/pages/editor.ts @@ -10,6 +10,8 @@ import { Code } from '../infra/code'; const EDITOR = (filename: string) => `.monaco-editor[data-uri$="${filename}"]`; const CURRENT_LINE = '.view-overlays .current-line'; const PLAY_BUTTON = '.codicon-play'; +const VIEW_LINES = (filename: string) => `${EDITOR(filename)} .view-lines`; +const LINE_NUMBERS = (filename: string) => `${EDITOR(filename)} .margin .margin-view-overlays .line-numbers`; const OUTER_FRAME = '.webview'; const INNER_FRAME = '#active-frame'; @@ -134,4 +136,70 @@ export class Editor { }).toPass(); } + async clickOnTerm(filename: string, term: string, line: number, doubleClick: boolean = false): Promise { + const selector = await this.getSelector(filename, term, line); + if (doubleClick) { + await this.code.driver.page.locator(selector).dblclick(); + } else { + await this.code.driver.page.locator(selector).click(); + } + } + + private async getSelector(filename: string, term: string, line: number): Promise { + const lineIndex = await this.getViewLineIndex(filename, line); + + // Get class names and the correct term index + const { classNames, termIndex } = await this.getClassSelectors(filename, term, lineIndex); + + return `${VIEW_LINES(filename)}>:nth-child(${lineIndex}) span span.${classNames[0]}:nth-of-type(${termIndex + 1})`; + } + + private async getViewLineIndex(filename: string, line: number): Promise { + const allElements = await this.code.driver.page.locator(LINE_NUMBERS(filename)).all(); + + // Resolve textContent for all elements first + const elementsWithText = await Promise.all(allElements.map(async (el, index) => ({ + el, + text: await el.textContent(), + index + }))); + + // Find the first element matching the line number + const matchingElement = elementsWithText.find(({ text }) => text === `${line}`); + + if (!matchingElement) { + throw new Error(`Line ${line} not found in file ${filename}`); + } + + // Return the 1-based index + return matchingElement.index + 1; + } + + private async getClassSelectors(filename: string, term: string, viewline: number): Promise<{ classNames: string[], termIndex: number }> { + // Locate all spans inside the line + const allElements = await this.code.driver.page.locator(`${VIEW_LINES(filename)}>:nth-child(${viewline}) span span`).all(); + + // Resolve all textContent values before filtering + const elementsWithText = await Promise.all(allElements.map(async (el, index) => ({ + el, + text: await el.textContent(), + index + }))); + + // Find the first element that contains the term + const matchingElement = elementsWithText.find(({ text }) => text === term); + + if (!matchingElement) { + throw new Error(`No elements found with term "${term}" in file "${filename}"`); + } + + // Get the class names of the matching span + const className = await matchingElement.el.evaluate(el => (el as HTMLElement).className) as string; + const classNames = className.split(/\s+/); + + // Find the index of this span among all spans + const termIndex = elementsWithText.filter(({ text }) => text !== null).map(({ el }) => el).indexOf(matchingElement.el); + + return { classNames, termIndex }; + } } diff --git a/test/e2e/pages/problems.ts b/test/e2e/pages/problems.ts new file mode 100644 index 00000000000..8ccb1bdc08a --- /dev/null +++ b/test/e2e/pages/problems.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { expect } from '@playwright/test'; +import { Code } from '../infra/code'; +import { QuickAccess } from './quickaccess'; + +export const enum ProblemSeverity { + WARNING = 0, + ERROR = 1 +} + +export class Problems { + + static PROBLEMS_VIEW_SELECTOR = '.panel .markers-panel'; + + constructor(private code: Code, private quickaccess: QuickAccess) { } + + async showProblemsView(): Promise { + await this.quickaccess.runCommand('workbench.panel.markers.view.focus'); + await this.waitForProblemsView(); + } + + async waitForProblemsView(): Promise { + await expect(this.code.driver.page.locator(Problems.PROBLEMS_VIEW_SELECTOR)).toBeVisible(); + } + + + static getSelectorInProblemsView(problemType: ProblemSeverity): string { + const selector = problemType === ProblemSeverity.WARNING ? 'codicon-warning' : 'codicon-error'; + return `div[id="workbench.panel.markers"] .monaco-tl-contents .marker-icon .${selector}`; + } + + static getSelectorInEditor(problemType: ProblemSeverity): string { + const selector = problemType === ProblemSeverity.WARNING ? 'squiggly-warning' : 'squiggly-error'; + return `.view-overlays .cdr.${selector}`; + } +} diff --git a/test/e2e/tests/problems/problems.test.ts b/test/e2e/tests/problems/problems.test.ts new file mode 100644 index 00000000000..769ecb1c2ab --- /dev/null +++ b/test/e2e/tests/problems/problems.test.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from '@playwright/test'; +import { Problems, ProblemSeverity } from '../../infra'; +import { test, tags } from '../_test.setup'; +import { join } from 'path'; + +test.use({ + suiteId: __filename +}); + +test.describe('Problems', { + tag: [tags.DEBUG, tags.WEB, tags.WIN] +}, () => { + + test('Python - Verify Problems Functionality', { tag: [tags.WIN, tags.WEB, tags.PROBLEMS] }, async function ({ app, python, openFile }) { + + await test.step('Open file and replace "rows" on line 9 with exclamation point', async () => { + await openFile(join('workspaces', 'chinook-db-py', 'chinook-sqlite.py')); + + await app.workbench.editor.clickOnTerm('chinook-sqlite.py', 'rows', 9, true); + + await app.code.driver.page.keyboard.type('!'); + }); + + await test.step('Verify File Squiggly', async () => { + const fileSquiggly = Problems.getSelectorInEditor(ProblemSeverity.ERROR); + await expect(app.code.driver.page.locator(fileSquiggly)).toBeVisible(); + }); + + const errorsSelector = Problems.getSelectorInProblemsView(ProblemSeverity.ERROR); + + await app.workbench.problems.showProblemsView(); + + await test.step('Verify Problems Count', async () => { + const errorLocators = await app.code.driver.page.locator(errorsSelector).all(); + + expect(errorLocators.length).toBe(4); + }); + + await test.step('Revert error', async () => { + await app.code.driver.page.keyboard.press(process.platform === 'darwin' ? 'Meta+Z' : 'Control+Z'); + + }); + + await test.step('Verify File Squiggly Is Gone', async () => { + const fileSquiggly = Problems.getSelectorInEditor(ProblemSeverity.ERROR); + await expect(app.code.driver.page.locator(fileSquiggly)).not.toBeVisible(); + }); + + await test.step('Verify Problems Count is 0', async () => { + + await expect(async () => { + const errorLocators = await app.code.driver.page.locator(errorsSelector).all(); + expect(errorLocators.length).toBe(0); + }).toPass({ timeout: 20000 }); + + }); + + }); + + test('R - Verify Problems Functionality', { tag: [tags.WIN, tags.WEB, tags.PROBLEMS] }, async function ({ app, r, openFile }) { + + await test.step('Open file and replace "albums" on line 5 with exclamation point', async () => { + await openFile(join('workspaces', 'chinook-db-r', 'chinook-sqlite.r')); + + await app.workbench.editor.clickOnTerm('chinook-sqlite.r', 'albums', 5, true); + + await app.code.driver.page.keyboard.type('!'); + }); + + await test.step('Verify File Squiggly', async () => { + const fileSquiggly = Problems.getSelectorInEditor(ProblemSeverity.ERROR); + await expect(app.code.driver.page.locator(fileSquiggly)).toBeVisible(); + }); + + const errorsSelector = Problems.getSelectorInProblemsView(ProblemSeverity.ERROR); + + await app.workbench.problems.showProblemsView(); + + await test.step('Verify Problems Count', async () => { + const errorLocators = await app.code.driver.page.locator(errorsSelector).all(); + + expect(errorLocators.length).toBe(1); + }); + + await test.step('Revert error', async () => { + await app.code.driver.page.keyboard.press(process.platform === 'darwin' ? 'Meta+Z' : 'Control+Z'); + + }); + + await test.step('Verify File Squiggly Is Gone', async () => { + const fileSquiggly = Problems.getSelectorInEditor(ProblemSeverity.ERROR); + await expect(app.code.driver.page.locator(fileSquiggly)).not.toBeVisible(); + }); + + await test.step('Verify Problems Count is 0', async () => { + + await expect(async () => { + const errorLocators = await app.code.driver.page.locator(errorsSelector).all(); + + expect(errorLocators.length).toBe(0); + }).toPass({ timeout: 20000 }); + + }); + + }); +});