diff --git a/packages/locators/lib/1.37.0.ts b/packages/locators/lib/1.37.0.ts index 579f2a797..8435efc51 100644 --- a/packages/locators/lib/1.37.0.ts +++ b/packages/locators/lib/1.37.0.ts @@ -170,7 +170,17 @@ const editor = { checkboxSetting: By.className('setting-value-checkbox'), checkboxChecked: 'aria-checked', linkButton: By.className('edit-in-settings-button'), - itemCount: By.className('settings-count-widget') + itemCount: By.className('settings-count-widget'), + arraySetting: By.className('setting-item-control'), + arrayRoot: By.xpath(`.//div[@role='list' and contains(@class, 'setting-list-widget')]`), + arrayRow: By.className('setting-list-row'), + arrayRowValue: By.className('setting-list-value'), + arrayNewRow: By.className('setting-list-new-row'), + arrayEditRow: By.className('setting-list-edit-row'), + arrayBtnConstructor: (label: string) => By.xpath(`.//a[contains(@role, 'button') and @aria-label='${label}']`), + arraySettingItem: { + btnConstructor: (label: string) => By.xpath(`.//a[contains(@role, 'button') and text()='${label}']`) + } }, DiffEditor: { originalEditor: By.className('original-in-monaco-diff-editor'), diff --git a/packages/page-objects/src/components/editor/SettingsEditor.ts b/packages/page-objects/src/components/editor/SettingsEditor.ts index 136eab219..050f54741 100644 --- a/packages/page-objects/src/components/editor/SettingsEditor.ts +++ b/packages/page-objects/src/components/editor/SettingsEditor.ts @@ -107,7 +107,7 @@ export class SettingsEditor extends Editor { * Context menu is disabled in this editor, throw an error */ async openContextMenu(): Promise { - throw new Error('Operation not supported'); + throw new Error('Operation not supported!'); } private async createSetting(element: WebElement, title: string, category: string): Promise { @@ -132,7 +132,13 @@ export class SettingsEditor extends Editor { await element.findElement(SettingsEditor.locators.SettingsEditor.linkButton); return new LinkSetting(SettingsEditor.locators.SettingsEditor.settingConstructor(title, category), this); } catch (err) { - throw new Error('Setting type not supported'); + // try array setting + try { + await element.findElement(SettingsEditor.locators.SettingsEditor.arraySetting); + return new ArraySetting(SettingsEditor.locators.SettingsEditor.settingConstructor(title, category), this); + } catch (err) { + throw new Error('Setting type not supported!'); + } } } } @@ -142,7 +148,7 @@ export class SettingsEditor extends Editor { /** * Abstract item representing a Setting with title, description and - * an input element (combo/textbox/checkbox/link) + * an input element (combo/textbox/checkbox/link/array) */ export abstract class Setting extends AbstractElement { @@ -274,11 +280,11 @@ export class CheckboxSetting extends Setting { export class LinkSetting extends Setting { async getValue(): Promise { - throw new Error('Method getValue is not available for LinkSetting'); + throw new Error('Method getValue is not available for LinkSetting!'); } async setValue(value: string | boolean): Promise { - throw new Error('Method setValue is not available for LinkSetting'); + throw new Error('Method setValue is not available for LinkSetting!'); } /** @@ -290,3 +296,188 @@ export class LinkSetting extends Setting { await link.click(); } } + +/** + * TODO + */ +export class ArraySetting extends Setting { + + /** + * @deprecated Method 'getValue' is not available for ArraySetting! + */ + async getValue(): Promise { + throw new Error('Method \'getValue\' is not available for ArraySetting!'); + } + + /** + * @deprecated Method 'setValue' is not available for ArraySetting! + */ + async setValue(value: string | boolean): Promise { + throw new Error('Method \'setValue\' is not available for ArraySetting!'); + } + + /** + * TODO + */ + async select(item: string): Promise { + const toSelect = await this.getItem(item); + await toSelect?.select() + } + + /** + * TODO + */ + async getItem(item: string | number): Promise { + const row = await this.findRow(item); + if(row) { + return new ArraySettingItem(row, this); + } + return undefined; + } + + /** + * TODO + */ + async getItems(): Promise { + const listRows = await this.getRows(); + let items: ArraySettingItem[] = []; + for(const row of listRows) { + items.push(new ArraySettingItem(row, this)); + } + return items; + } + + /** + * TODO + */ + async getValues(): Promise { + const items = await this.getItems(); + let values: string[] = []; + for(const item of items) { + values.push(await item.getValue()); + } + return values; + } + + /** + * TODO + */ + async add(): Promise { + // click 'Add Item' button + const button = await this.findElement(SettingsEditor.locators.SettingsEditor.arrayNewRow).findElement(By.className('monaco-button')); + await button.click(); + + // get item row switched to 'edit' mode + const list = await this.getListRootElement(); + const editRow = await list.findElement(SettingsEditor.locators.SettingsEditor.arrayEditRow); + return new ArraySettingItem(editRow, this); + } + + /** + * TODO + */ + async edit(item: string | number): Promise { + const row = await this.findRow(item); + if(row) { + // select item row + const toEdit = new ArraySettingItem(row, this); + await toEdit.select(); + + // click 'Edit Item' button + const edit = await toEdit.findElement(SettingsEditor.locators.SettingsEditor.arrayBtnConstructor('Edit Item')); + await edit.click(); + + // get item row switched to 'edit' mode + const list = await this.getListRootElement(); + const editRow = await list.findElement(SettingsEditor.locators.SettingsEditor.arrayEditRow); + return new ArraySettingItem(editRow, this); + } + return undefined; + } + + private async getListRootElement(): Promise { + return await this.findElement(SettingsEditor.locators.SettingsEditor.arrayRoot); + } + + private async getRows(): Promise { + const list = await this.getListRootElement(); + return await list.findElements(SettingsEditor.locators.SettingsEditor.arrayRow); + } + + private async findRow(item: string | number): Promise { + const listRows = await this.getRows(); + if(Number.isInteger(item)) { + const index = +item; + if (index < 0 || index > listRows.length - 1) { + throw Error(`Index '${index}' is of bounds! Found items have length = ${listRows.length}.`); + } + return listRows[index]; + } else { + for(const row of listRows) { + const li = await row.findElement(SettingsEditor.locators.SettingsEditor.arrayRowValue); + if(await li.getText() === item) { + return row; + } + } + } + return undefined; + } +} + +/** + * TODO + */ +export class ArraySettingItem extends AbstractElement { + + constructor(element: WebElement, setting: ArraySetting) { + super(element, setting); + } + + /** + * TODO + */ + async select(): Promise { + await this.click(); + } + + /** + * TODO + */ + async getValue(): Promise { + return await this.getText(); + } + + /** + * TODO + */ + async setValue(value: string): Promise { + const input = await this.findElement(SettingsEditor.locators.SettingsEditor.textSetting); + await input.clear(); + await input.sendKeys(value); + } + + /** + * TODO + */ + async remove(): Promise { + await this.select(); + const remove = await this.findElement(SettingsEditor.locators.SettingsEditor.arrayBtnConstructor('Remove Item')); + await remove.click(); + } + + /** + * TODO + */ + async ok(): Promise { + const ok = await this.findElement(SettingsEditor.locators.SettingsEditor.arraySettingItem.btnConstructor('OK')); + await ok.click(); + } + + /** + * TODO + */ + async cancel(): Promise { + const cancel = await this.findElement(SettingsEditor.locators.SettingsEditor.arraySettingItem.btnConstructor('Cancel')); + await cancel.click(); + } +} diff --git a/packages/page-objects/src/locators/locators.ts b/packages/page-objects/src/locators/locators.ts index 0660d1d76..0607d888f 100644 --- a/packages/page-objects/src/locators/locators.ts +++ b/packages/page-objects/src/locators/locators.ts @@ -176,6 +176,16 @@ export interface Locators { checkboxChecked: string linkButton: By itemCount: By + arraySetting: By + arrayRoot: By + arrayRow: By + arrayRowValue: By + arrayNewRow: By + arrayEditRow: By + arrayBtnConstructor: (label: string) => By + arraySettingItem: { + btnConstructor: (label: string) => By + } } DiffEditor: { originalEditor: By diff --git a/tests/test-project/package.json b/tests/test-project/package.json index 69eb77fbd..7eb3d3fe3 100644 --- a/tests/test-project/package.json +++ b/tests/test-project/package.json @@ -141,7 +141,7 @@ } ], "configuration": { - "title": "Test Project", + "title": "ExTester Tests", "properties": { "testProject.general.helloWorld": { "type": "boolean", @@ -151,6 +151,19 @@ "testProject.enableCodeLens": { "type": "boolean", "default": false + }, + "testProject.general.helloWorldArray": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + }, + "additionalProperties": false, + "markdownDescription": "This is an example array of strings", + "default": [ + "Hello World", + "Hello ExTester" + ] } } }, diff --git a/tests/test-project/src/test/editor/settingsEditor.test.ts b/tests/test-project/src/test/editor/settingsEditor.test.ts index 8b05df6d7..66bc2633d 100644 --- a/tests/test-project/src/test/editor/settingsEditor.test.ts +++ b/tests/test-project/src/test/editor/settingsEditor.test.ts @@ -1,12 +1,13 @@ import { expect } from 'chai'; -import { SettingsEditor, Workbench, EditorView, ComboSetting, TextSetting, CheckboxSetting } from 'vscode-extension-tester'; +import { SettingsEditor, Workbench, EditorView, ComboSetting, TextSetting, CheckboxSetting, ArraySetting } from 'vscode-extension-tester'; -describe('Settings Editor', function () { +describe.only('Settings Editor', function () { let editor: SettingsEditor; before(async function () { - this.timeout(10000); + this.timeout(30000); editor = await new Workbench().openSettings(); + await new Promise(t => setTimeout(t, 5_000)); // wait to be sure settings editor is loaded properly }); after(async function () { @@ -116,4 +117,115 @@ describe('Settings Editor', function () { await setting.setValue(true); }); }); + + describe('array setting', function () { + let setting: ArraySetting; + + before(async function () { + this.timeout(15000); + setting = await editor.findSetting('Hello World Array', 'Test Project', 'General') as ArraySetting; + }); + + it('getItem works - using index', async function () { + const item = await setting.getItem(1); + expect(item).is.not.undefined; + const value = await item.getValue(); + expect(value).is.equal('Hello ExTester'); + }); + + it('getItem works - using label', async function () { + const item = await setting.getItem('Hello World'); + expect(item).is.not.undefined; + const value = await item.getValue(); + expect(value).is.equal('Hello World'); + }); + + it('getItems works', async function () { + const items = await setting.getItems(); + expect(items).is.not.empty; + expect(items.length).is.equal(2); + }); + + it('getValues works', async function () { + const values = await setting.getValues(); + expect(values).contains.members(['Hello World', 'Hello ExTester']); + }); + + it('addItem works', async function () { + const add1 = await setting.add(); + await add1.setValue('Add Item 1'); + await add1.ok(); + await waitUntilItemExists('Add Item 1'); + + const add2 = await setting.add(); + await add2.setValue('Add Item 2'); + await add2.ok(); + await waitUntilItemExists('Add Item 2'); + + const add3 = await setting.add(); + await add3.setValue('Add Item 3'); + await add3.ok(); + await waitUntilItemExists('Add Item 3'); + + const newValue = await setting.getItem('Add Item 1'); + expect(await newValue.getValue()).is.equal('Add Item 1'); + }); + + it('removeItem works - using label', async function () { + const toRemove = await setting.getItem('Hello ExTester'); + await toRemove.remove(); + await waitUntilItemNotExists('Hello ExTester'); + + const values = await setting.getValues(); + expect(values.length).is.lessThan(5); + expect(values).not.includes('Hello ExTester'); + }); + + it('removeItem works - using index', async function () { + const toRemove = await setting.getItem(1); + await toRemove.remove(); + await waitUntilItemNotExists('Add Item 1'); + + const values = await setting.getValues(); + expect(values.length).is.lessThan(4); + expect(values).not.includes('Add Item 1'); + }); + + it('editItem works - using label', async function () { + const toEdit = await setting.edit('Hello World'); + await toEdit.setValue('Edit Item Label'); + await toEdit.ok(); + await waitUntilItemExists('Edit Item Label'); + + const values = await setting.getValues(); + expect(values).includes('Edit Item Label'); + }); + + it('editItem works - using index', async function () { + const toEdit = await setting.edit(1); + await toEdit.setValue('Edit Item Index'); + await toEdit.ok(); + await waitUntilItemExists('Edit Item Index'); + + const values = await setting.getValues(); + expect(values).includes('Edit Item Index'); + }); + + async function waitUntilItemExists(item: string, timeout: number = 5_000): Promise { + let values = []; + await setting.getDriver().wait(async function () { + values = await setting.getValues(); + return values.includes(item); + }, timeout, `Expected item - '${item}' was not found in list of: ${values}`); + } + + async function waitUntilItemNotExists(item: string, timeout: number = 5_000): Promise { + let values = []; + await setting.getDriver().wait(async function () { + values = await setting.getValues(); + return !values.includes(item); + }, timeout, `Expected item - '${item}' was found in list of: ${values}`); + } + }); + });