diff --git a/docs/TextEditor.md b/docs/TextEditor.md index 8eb107f5b..5a74e6ed1 100644 --- a/docs/TextEditor.md +++ b/docs/TextEditor.md @@ -37,8 +37,16 @@ await editor.typeText("I have the best text"); await editor.typeTextAt(1, 3, " absolutely"); // format the whole document with built-in tools await editor.formatDocument(); +// move the cursor to the given coordinates +await editor.moveCursor(3, 6); +// set cursor to given position using command prompt :Ln,Col +// Note: the setCursor approach is not properly working for tabs indentation in VS Code text editor, see https://github.com/microsoft/vscode/issues/198780 +await editor.setCursor(2, 5); // get the current cursor coordinates as number array [x,y] const coords = await editor.getCoordinates(); +// get the current indentation of opened editor +const indent = await editor.getIndentation(); +console.log(`indentation label = ${indent.label} and value = ${indent.value}`); ``` #### Save Changes diff --git a/packages/page-objects/src/components/editor/TextEditor.ts b/packages/page-objects/src/components/editor/TextEditor.ts index 17928c96f..6c2bf3d5b 100644 --- a/packages/page-objects/src/components/editor/TextEditor.ts +++ b/packages/page-objects/src/components/editor/TextEditor.ts @@ -235,7 +235,7 @@ export class TextEditor extends Editor { * Find and select a given text. Not usable for multi line selection. * * @param text text to select - * @param occurrence specify which onccurrence of text to select if multiple are present in the document + * @param occurrence specify which occurrence of text to select if multiple are present in the document */ async selectText(text: string, occurrence = 1): Promise { const lineNum = await this.getLineOfText(text, occurrence); @@ -336,50 +336,172 @@ export class TextEditor extends Editor { await inputarea.sendKeys(text); } + /** + * Set cursor to given position using command prompt :Ln,Col + * @param line line number to set to + * @param column column number to set to + * @returns Promise resolving when the cursor has reached the given coordinates + */ + async setCursor(line: number, column: number, timeout: number = 2_500): Promise { + const input = await new Workbench().openCommandPrompt(); + await input.setText(`:${line},${column}`); + await input.confirm(); + await this.waitForCursorPositionAt(line, column, timeout); + } + + /** + * Get indentation from the status bar for the currently opened text editor + * @returns \{ string, number \} object which contains label and value of indentation + */ + async getIndentation(): Promise<{ label: string; value: number }> { + const indentation = await new StatusBar().getCurrentIndentation(); + const value = Number(indentation.match(/\d+/g)?.at(0)); + const label = indentation.match(/^[a-zA-Z\s]+/g)?.at(0) as string; + return { label, value }; + } + /** * Move the cursor to the given coordinates * @param line line number to move to * @param column column number to move to * @returns Promise resolving when the cursor has reached the given coordinates */ - async moveCursor(line: number, column: number): Promise { + async moveCursor(line: number, column: number, timeout: number = 10_000): Promise { if (line < 1 || line > (await this.getNumberOfLines())) { throw new Error(`Line number ${line} does not exist`); } if (column < 1) { throw new Error(`Column number ${column} does not exist`); } - if (process.platform === 'darwin') { - const input = await new Workbench().openCommandPrompt(); - await input.setText(`:${line},${column}`); - await input.confirm(); - } else { - const inputarea = await this.findElement(TextEditor.locators.Editor.inputArea); - let coordinates = await this.getCoordinates(); - const lineGap = coordinates[0] - line; - const lineKey = lineGap >= 0 ? Key.UP : Key.DOWN; - for (let i = 0; i < Math.abs(lineGap); i++) { - await inputarea.sendKeys(lineKey); + const inputarea = await this.findElement(TextEditor.locators.Editor.inputArea); + await inputarea.getDriver().wait( + async () => { + await this.moveCursorToLine(inputarea, line); + return await this.isLine(line); + }, + timeout, + `Unable to move cursor to line: ${line}`, + ); + await inputarea.getDriver().wait( + async () => { + await this.moveCursorToColumn(inputarea, column); + return await this.isColumn(column); + }, + timeout, + `Unable to move cursor to column: ${column}`, + ); + } + + /** + * (private) Move the cursor to the given line coordinate + * @param inputArea WebElement of an editor input area + * @param line line number to move to + * @returns Promise resolving when the cursor has reached the given line coordinate + */ + private async moveCursorToLine(inputArea: WebElement, line: number): Promise { + const coordinates = await this.getCoordinates(); + const lineGap = coordinates[0] - line; + const lineKey = lineGap >= 0 ? Key.UP : Key.DOWN; + + for (let i = 0; i < Math.abs(lineGap); i++) { + if (await this.isLine(line)) { + break; } + await inputArea.getDriver().actions().clear(); + await inputArea.sendKeys(lineKey); + await inputArea.getDriver().sleep(500); + } + } - coordinates = await this.getCoordinates(); - const columnGap = coordinates[1] - column; - const columnKey = columnGap >= 0 ? Key.LEFT : Key.RIGHT; - for (let i = 0; i < Math.abs(columnGap); i++) { - await inputarea.sendKeys(columnKey); - const actualCoordinates = (await this.getCoordinates())[0]; - if (actualCoordinates !== coordinates[0]) { - throw new Error(`Column number ${column} is not accessible on line ${line}`); - } + /** + * (private) Move the cursor to the given column coordinate + * @param inputArea WebElement of an editor input area + * @param column column number to move to + * @returns Promise resolving when the cursor has reached the given column coordinate + */ + private async moveCursorToColumn(inputArea: WebElement, column: number): Promise { + const coordinates = await this.getCoordinates(); + const columnGap = coordinates[1] - column; + const columnKey = columnGap >= 0 ? Key.LEFT : Key.RIGHT; + + for (let i = 0; i < Math.abs(columnGap); i++) { + if (await this.isColumn(column)) { + break; } + await inputArea.getDriver().actions().clear(); + await inputArea.sendKeys(columnKey); + await inputArea.getDriver().sleep(500); + if ((await this.getCoordinates())[0] !== coordinates[0]) { + throw new Error(`Column number ${column} is not accessible on line ${coordinates[0]}`); + } + } + } + + /** + * (private) Check if the cursor is already on requested line + * @param line line number to check against current cursor position + * @returns true / false + */ + private async isLine(line: number): Promise { + const actualCoordinates = await this.getCoordinates(); + if (actualCoordinates[0] === line) { + return true; } - await this.getDriver().wait( + return false; + } + + /** + * (private) Check if the cursor is already on requested column + * @param column column number to check against current cursor position + * @returns true / false + */ + private async isColumn(column: number): Promise { + const actualCoordinates = await this.getCoordinates(); + if (actualCoordinates[1] === column) { + return true; + } + return false; + } + + /** + * (private) Dynamic waiting for cursor position movements + * @param line line number to wait + * @param column column number to wait + * @param timeout default timeout + */ + private async waitForCursorPositionAt(line: number, column: number, timeout: number = 2_500): Promise { + (await this.waitForCursorPositionAtLine(line, timeout)) && (await this.waitForCursorPositionAtColumn(column, timeout)); + } + + /** + * (private) Dynamic waiting for cursor position movements at line + * @param line line number to wait + * @param timeout + */ + private async waitForCursorPositionAtLine(line: number, timeout: number): Promise { + return await this.getDriver().wait( + async () => { + return await this.isLine(line); + }, + timeout, + `Unable to set cursor at line ${line}`, + 500, + ); + } + + /** + * (private) Dynamic waiting for cursor position movements at column + * @param column column number to wait + * @param timeout + */ + private async waitForCursorPositionAtColumn(column: number, timeout: number): Promise { + return await this.getDriver().wait( async () => { - const coor = await this.getCoordinates(); - return coor[0] === line && coor[1] === column; + return await this.isColumn(column); }, - 10000, - `Unable to set cursor at position ${column}:${line}`, + timeout, + `Unable to set cursor at column ${column}`, + 500, ); } diff --git a/tests/test-project/resources/file-with-spaces.ts b/tests/test-project/resources/file-with-spaces.ts new file mode 100644 index 000000000..03d3de4b0 --- /dev/null +++ b/tests/test-project/resources/file-with-spaces.ts @@ -0,0 +1,3 @@ +first row + second row + third row diff --git a/tests/test-project/resources/file-with-tabs.ts b/tests/test-project/resources/file-with-tabs.ts new file mode 100644 index 000000000..322cb7900 --- /dev/null +++ b/tests/test-project/resources/file-with-tabs.ts @@ -0,0 +1,3 @@ +first row + second row + third row diff --git a/tests/test-project/src/test/editor/textEditor.test.ts b/tests/test-project/src/test/editor/textEditor.test.ts index 7f4448348..84f6082a9 100644 --- a/tests/test-project/src/test/editor/textEditor.test.ts +++ b/tests/test-project/src/test/editor/textEditor.test.ts @@ -17,7 +17,20 @@ import * as path from 'path'; import { expect } from 'chai'; -import { TextEditor, EditorView, StatusBar, InputBox, ContentAssist, Workbench, FindWidget, VSBrowser, after, before } from 'vscode-extension-tester'; +import { + TextEditor, + EditorView, + StatusBar, + InputBox, + ContentAssist, + Workbench, + FindWidget, + VSBrowser, + after, + before, + afterEach, + beforeEach, +} from 'vscode-extension-tester'; describe('ContentAssist', async function () { let assist: ContentAssist; @@ -49,20 +62,21 @@ describe('ContentAssist', async function () { ); }); - beforeEach(async function () { + beforeEach(async () => { + this.timeout(15000); assist = (await editor.toggleContentAssist(true)) as ContentAssist; await new Promise((res) => setTimeout(res, 2000)); }); - after(async function () { - await new EditorView().closeAllEditors(); - }); - afterEach(async function () { await editor.toggleContentAssist(false); await new Promise((res) => setTimeout(res, 1000)); }); + after(async function () { + await new EditorView().closeAllEditors(); + }); + it('getItems retrieves the suggestions', async function () { const items = await assist.getItems(); expect(items).not.empty; @@ -91,7 +105,7 @@ describe('TextEditor', function () { const testText = process.platform === 'win32' ? `line1\r\nline2\r\nline3` : `line1\nline2\nline3`; before(async () => { - this.timeout(8000); + this.timeout(15000); await new Workbench().executeCommand('Create: New File...'); await (await InputBox.create()).selectQuickPick('Text File'); await new Promise((res) => { @@ -124,22 +138,22 @@ describe('TextEditor', function () { }); it('can type text at given coordinates', async function () { - this.timeout(5000); + this.timeout(10000); await editor.typeTextAt(1, 6, '1'); const line = await editor.getTextAtLine(1); expect(line).has.string('line11'); }); it('getCoordinates works', async function () { - this.timeout(15000); + this.timeout(20000); - await editor.moveCursor(1, 1); + await editor.setCursor(1, 1); expect(await editor.getCoordinates()).to.deep.equal([1, 1]); const lineCount = await editor.getNumberOfLines(); const lastLine = await editor.getTextAtLine(lineCount); - await editor.moveCursor(lineCount, lastLine.length); + await editor.setCursor(lineCount, lastLine.length); expect(await editor.getCoordinates()).to.deep.equal([lineCount, lastLine.length]); }); @@ -165,8 +179,61 @@ describe('TextEditor', function () { expect(await editor.formatDocument()).not.to.throw; }); + describe('move/set cursor', function () { + const params = [ + { file: 'file-with-spaces.ts', indent: 'spaces' }, + { file: 'file-with-tabs.ts', indent: 'tabs' }, + ]; + + params.forEach((param) => + describe(`file using ${param.indent}`, function () { + let editor: TextEditor; + let ew: EditorView; + + beforeEach(async function () { + await VSBrowser.instance.openResources(path.resolve(__dirname, '..', '..', '..', 'resources', param.file)); + ew = new EditorView(); + await ew.getDriver().wait( + async function () { + return (await ew.getOpenEditorTitles()).includes(param.file); + }, + 10_000, + `Unable to find opened editor with title '${param.file}'`, + ); + editor = (await ew.openEditor(param.file)) as TextEditor; + }); + + [ + [2, 5], + [3, 9], + ].forEach((coor) => + it(`move cursor to position [Ln ${coor[0]}, Col ${coor[1]}]`, async function () { + this.timeout(30000); + await editor.moveCursor(coor[0], coor[1]); + expect(await editor.getCoordinates()).to.deep.equal(coor); + }), + ); + + // set cursor using command prompt is not working properly for tabs indentation in VS Code, see https://github.com/microsoft/vscode/issues/198780 + [ + [2, 12], + [3, 15], + ].forEach((coor) => + (param.indent === 'tabs' ? it.skip : it)(`set cursor to position [Ln ${coor[0]}, Col ${coor[1]}]`, async function () { + this.timeout(30000); + await editor.setCursor(coor[0], coor[1]); + expect(await editor.getCoordinates()).to.deep.equal(coor); + }), + ); + }), + ); + }); + describe('searching', function () { before(async function () { + const ew = new EditorView(); + const editors = await ew.getOpenEditorTitles(); + editor = (await ew.openEditor(editors[0])) as TextEditor; await editor.setText('aline\nbline\ncline\ndline\nnope\neline1 eline2\n'); }); @@ -196,7 +263,7 @@ describe('TextEditor', function () { expect(await editor.getSelectedText()).equals(text); }); - it('selectText errors if given text doesnt exist', async function () { + it("selectText errors if given text doesn't exist", async function () { const text = 'wat'; try { await editor.selectText(text); @@ -296,7 +363,7 @@ describe('TextEditor', function () { describe('CodeLens', function () { before(async function () { await new Workbench().executeCommand('enable codelens'); - // older versions of vscode dont fire the update event immediately, give it some encouragement + // older versions of vscode don't fire the update event immediately, give it some encouragement // otherwise the lenses end up empty await new Workbench().executeCommand('enable codelens'); await new Promise((res) => setTimeout(res, 1000)); @@ -304,11 +371,9 @@ describe('TextEditor', function () { after(async function () { await new Workbench().executeCommand('disable codelens'); - }); - - it('getCodeLenses works', async function () { - const lenses = await editor.getCodeLenses(); - expect(lenses.length).is.equal(7); + const nc = await new Workbench().openNotificationsCenter(); + await nc.clearAllNotifications(); + await nc.close(); }); it('getCodeLens works with index', async function () { @@ -345,17 +410,10 @@ describe('TextEditor', function () { this.timeout(20000); const lens = await editor.getCodeLens(2); await lens?.click(); - await lens?.getDriver().sleep(1000); + await lens?.getDriver().sleep(2_000); const notifications = await new Workbench().getNotifications(); - - let notification = undefined; - for (const not of notifications) { - if ((await not.getMessage()).startsWith('CodeLens action clicked')) { - notification = not; - break; - } - } - expect(notification).not.undefined; + const message = await notifications[0].getMessage(); + expect(message).to.includes('CodeLens action clicked'); }); }); });