From 4898c2f4a110736afc259df9b80b31b9319e5f95 Mon Sep 17 00:00:00 2001 From: Dominik Jelinek Date: Tue, 21 Nov 2023 09:50:39 +0100 Subject: [PATCH] issue-931: moveCursor fails on file with tabs Signed-off-by: Dominik Jelinek tmp Signed-off-by: Dominik Jelinek tmp Signed-off-by: Dominik Jelinek --- docs/TextEditor.md | 19 +- .../src/components/editor/TextEditor.ts | 202 ++++++++++++++++-- .../resources/file-with-spaces.ts | 3 + .../test-project/resources/file-with-tabs.ts | 3 + .../src/test/editor/textEditor.test.ts | 66 ++++-- 5 files changed, 255 insertions(+), 38 deletions(-) create mode 100644 tests/test-project/resources/file-with-spaces.ts create mode 100644 tests/test-project/resources/file-with-tabs.ts diff --git a/docs/TextEditor.md b/docs/TextEditor.md index 6cd9a43d0..4cee812f3 100644 --- a/docs/TextEditor.md +++ b/docs/TextEditor.md @@ -1,6 +1,7 @@ ![editor](https://user-images.githubusercontent.com/4181232/56643754-81b9ee00-667a-11e9-9c7a-de39f342d676.png) #### Lookup + ```typescript import { TextEditor, EditorView } from 'vscode-extension-tester'; ... @@ -11,7 +12,9 @@ const editor1 = await new EditorView().openEditor('package.json'); ``` #### Text Retrieval + Note: Most text retrieval and editing actions will make use of the clipboard. + ```typescript // get all text const text = await editor.getText(); @@ -22,6 +25,7 @@ const numberOfLines = await editor.getNumberOfLines(); ``` #### Editing Text + ```typescript // replace all text with a string await editor.setText('my fabulous text'); @@ -33,11 +37,19 @@ 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 +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 + ```typescript // find if the editor has changes const hasChanges = await editor.isDirty(); @@ -48,11 +60,13 @@ const prompt = await editor.saveAs(); ``` #### Get Document File Path + ```typescript const path = await editor.getFilePath(); ``` #### Content Assist + ```typescript // open content assist at current position const contentAssist = await editor.toggleContentAssist(true); @@ -61,6 +75,7 @@ await editor.toggleContentAssist(false); ``` #### Search for Text + ```typescript // get line number that contains some text const lineNum = await editor.getLineOfText('some text'); @@ -75,12 +90,14 @@ const find = await editor.openFindWidget(); ``` #### Breakpoints + ```typescript // toggle breakpoint on a line with given number await editor.toggleBreakpoint(1); ``` #### CodeLenses + ```typescript // get a code lens by (partial) text const lens = await editor.getCodeLens('my code lens text'); @@ -95,4 +112,4 @@ await lens.click(); // or just get the text const text = await lens.getText(); const tooltip = await lens.getTooltip(); -``` \ No newline at end of file +``` diff --git a/packages/page-objects/src/components/editor/TextEditor.ts b/packages/page-objects/src/components/editor/TextEditor.ts index 6d740a0ad..545d273f9 100644 --- a/packages/page-objects/src/components/editor/TextEditor.ts +++ b/packages/page-objects/src/components/editor/TextEditor.ts @@ -215,7 +215,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); @@ -316,6 +316,30 @@ 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): Promise { + const input = await new Workbench().openCommandPrompt(); + await input.setText(`:${line},${column}`); + await input.confirm(); + await this.waitForCursorPositionAt(line, column); + } + + /** + * 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 @@ -329,34 +353,166 @@ export class TextEditor extends Editor { 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 this.moveCursorToLine(inputarea, line); + await this.moveCursorToColumn(inputarea, 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; + let nextLine = coordinates[0]; + + for (let i = 0; i < Math.abs(lineGap); i++) { + if(await this.isLine(line)) break; + await inputArea.sendKeys(lineKey); + await inputArea.getDriver().sleep(150); + if(await this.isLine(line)) break; + + switch (lineKey) { + case Key.UP: + nextLine = nextLine - 1; + break; + case Key.DOWN: + nextLine = nextLine + 1; + break; } - 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); - let actualCoordinates = (await this.getCoordinates())[0]; - if (actualCoordinates != coordinates[0]) { - throw new Error(`Column number ${column} is not accessible on line ${line}`); + try { + await this.waitForCursorPositionAtLine(nextLine); + } catch (error) { + if(await this.isLine(line)) break; + await inputArea.sendKeys(lineKey); + await inputArea.getDriver().sleep(250); + if(await this.isLine(line)) break; + + if(lineKey == Key.DOWN && nextLine < line) { + await this.waitForCursorPositionAtLine(nextLine); + } else { + await this.waitForCursorPositionAtLine(line); } } + } - await this.getDriver().wait(async () => { + } + + /** + * (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; + let nextCol = coordinates[1]; + + for (let i = 0; i < Math.abs(columnGap); i++) { + if(await this.isColumn(column)) break; + await inputArea.sendKeys(columnKey); + await inputArea.getDriver().sleep(150); + if(await this.isColumn(column)) break; + if ((await this.getCoordinates())[0] != coordinates[0]) { + throw new Error(`Column number ${column} is not accessible on line ${coordinates[0]}`); + } + + switch (columnKey) { + case Key.LEFT: + nextCol = nextCol - 1; + break; + case Key.RIGHT: + nextCol = nextCol + 1; + break; + } + + try { + await this.waitForCursorPositionAtColumn(nextCol); + } catch (error) { + if(await this.isColumn(column)) break; + await inputArea.sendKeys(columnKey); + await inputArea.getDriver().sleep(250); + if(await this.isColumn(column)) break; + if ((await this.getCoordinates())[0] != coordinates[0]) { + throw new Error(`Column number ${column} is not accessible on line ${coordinates[0]}`); + } + + if(columnKey === Key.RIGHT && nextCol < column) { + await this.waitForCursorPositionAtColumn(nextCol); + } else { + await this.waitForCursorPositionAtColumn(column); + } + } + + } + } + + /** + * (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; + } + 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): Promise { + await this.waitForCursorPositionAtLine(line) && await this.waitForCursorPositionAtColumn(column); + } + + /** + * (private) Dynamic waiting for cursor position movements at line + * @param line line number to wait + * @param timeout default timeout is wait for 1.5s + */ + private async waitForCursorPositionAtLine(line: number, timeout: number = 1_500): Promise { + return await this.getDriver().wait(async () => { + const coor = await this.getCoordinates(); + return coor[0] === line; + }, timeout, `Unable to set cursor at line ${line}`); + } + + /** + * (private) Dynamic waiting for cursor position movements at column + * @param column column number to wait + * @param timeout default timeout is wait for 1.5s + */ + private async waitForCursorPositionAtColumn(column: number, timeout: number = 1_500): Promise { + return await this.getDriver().wait(async () => { const coor = await this.getCoordinates(); - return coor[0] === line && coor[1] === column; - }, 10000, `Unable to set cursor at position ${column}:${line}`); + return coor[1] === column; + }, timeout, `Unable to set cursor at column ${column}`); } /** 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 501eba171..f28ccf7d1 100644 --- a/tests/test-project/src/test/editor/textEditor.test.ts +++ b/tests/test-project/src/test/editor/textEditor.test.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { expect } from 'chai'; -import { TextEditor, EditorView, StatusBar, InputBox, ContentAssist, Workbench, FindWidget, VSBrowser, Notification, 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; @@ -30,6 +30,7 @@ describe('ContentAssist', async function () { }); beforeEach(async function () { + this.timeout(15000); assist = await editor.toggleContentAssist(true) as ContentAssist; await new Promise(res => setTimeout(res, 2000)); }); @@ -71,7 +72,7 @@ describe('TextEditor', function () { const testText = process.platform === 'win32' ? `line1\r\nline2\r\nline3` : `line1\nline2\nline3`; before(async function () { - this.timeout(8000); + this.timeout(15000); await new Workbench().executeCommand('Create: New File...'); await (await InputBox.create()).selectQuickPick('Text File'); await new Promise((res) => { setTimeout(res, 1000); }); @@ -102,14 +103,14 @@ 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); expect(await editor.getCoordinates()).to.deep.equal([1, 1]); @@ -143,9 +144,53 @@ describe('TextEditor', function () { expect(await editor.formatDocument()).not.to.throw; }); + describe('move/set cursor', function () { + + const params = [ + { file: 'file-with-spaces.ts', spaces: 'spaces'}, + { file: 'file-with-tabs.ts', spaces: 'tabs'} + ]; + + params.forEach(param => describe(`file using ${param.spaces}`, 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; + }); + + afterEach(async function() { + await new EditorView().closeEditor(param.file); + }); + + [[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.spaces === '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'); }); @@ -175,7 +220,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); @@ -272,7 +317,7 @@ describe('TextEditor', 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)); @@ -323,14 +368,7 @@ describe('TextEditor', function () { await lens.click(); await lens.getDriver().sleep(1000); const notifications = await new Workbench().getNotifications(); - let notification: Notification; - - for (const not of notifications) { - if ((await not.getMessage()).startsWith('CodeLens action clicked')) { - notification = not; - break; - } - } + const notification = notifications.find(async notification => { return (await notification.getMessage()).startsWith('Codelens action clicked'); }); expect(notification).not.undefined; }); });