Skip to content

Commit

Permalink
[playwright] Add option preference support and reactive tests
Browse files Browse the repository at this point in the history
Due to the Monaco uplift, we had to temporarily skip a test, because
it affected the default preference regarding auto-save.

To re-enable the test, we now explicitly set the auto save preference to
`off` before the test. This way the preference value is ensured to be
consistent.

In order to do so, we add support for option preferences

See also eclipse-theia#10736

Fixes eclipse-theia#10891

Change-Id: I6ab22a71848e174313a30f9121cb4b3dec363f12
  • Loading branch information
planger committed May 5, 2022
1 parent b9e1949 commit 67b3e78
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 86 deletions.
51 changes: 38 additions & 13 deletions examples/playwright/src/tests/theia-preference-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,44 +38,63 @@ test.describe('Preference View', () => {
const preferences = await app.openPreferences(TheiaPreferenceView);
const preferenceId = PreferenceIds.DiffEditor.MaxComputationTime;

await preferences.resetStringPreferenceById(preferenceId);
await preferences.resetPreferenceById(preferenceId);
expect(await preferences.getStringPreferenceById(preferenceId)).toBe(DefaultPreferences.DiffEditor.MaxComputationTime);

await preferences.setStringPreferenceById(preferenceId, '8000');
await preferences.waitForModified(preferenceId);
expect(await preferences.getStringPreferenceById(preferenceId)).toBe('8000');

await preferences.resetStringPreferenceById(preferenceId);
await preferences.resetPreferenceById(preferenceId);
expect(await preferences.getStringPreferenceById(preferenceId)).toBe(DefaultPreferences.DiffEditor.MaxComputationTime);
});

test('should be able to read, set, and reset Boolean preferences', async () => {
const preferences = await app.openPreferences(TheiaPreferenceView);
const preferenceId = PreferenceIds.Explorer.AutoReveal;

await preferences.resetBooleanPreferenceById(preferenceId);
await preferences.resetPreferenceById(preferenceId);
expect(await preferences.getBooleanPreferenceById(preferenceId)).toBe(DefaultPreferences.Explorer.AutoReveal.Enabled);

await preferences.setBooleanPreferenceById(preferenceId, false);
await preferences.waitForModified(preferenceId);
expect(await preferences.getBooleanPreferenceById(preferenceId)).toBe(false);

await preferences.resetBooleanPreferenceById(preferenceId);
await preferences.resetPreferenceById(preferenceId);
expect(await preferences.getBooleanPreferenceById(preferenceId)).toBe(DefaultPreferences.Explorer.AutoReveal.Enabled);
});

test('should be able to read, set, and reset Options preferences', async () => {
const preferences = await app.openPreferences(TheiaPreferenceView);
const preferenceId = PreferenceIds.Editor.RenderWhitespace;

await preferences.resetPreferenceById(preferenceId);
expect(await preferences.getOptionsPreferenceById(preferenceId)).toBe(DefaultPreferences.Editor.RenderWhitespace.Selection);

await preferences.setOptionsPreferenceById(preferenceId, DefaultPreferences.Editor.RenderWhitespace.Boundary);
await preferences.waitForModified(preferenceId);
expect(await preferences.getOptionsPreferenceById(preferenceId)).toBe(DefaultPreferences.Editor.RenderWhitespace.Boundary);

await preferences.resetPreferenceById(preferenceId);
expect(await preferences.getOptionsPreferenceById(preferenceId)).toBe(DefaultPreferences.Editor.RenderWhitespace.Selection);
});

test('should throw an error if we try to read, set, or reset a non-existing preference', async () => {
const preferences = await app.openPreferences(TheiaPreferenceView);

preferences.customTimeout = 500;
try {
await expect(preferences.getBooleanPreferenceById('no.a.real.preference')).rejects.toThrowError();
await expect(preferences.setBooleanPreferenceById('no.a.real.preference', true)).rejects.toThrowError();
await expect(preferences.resetBooleanPreferenceById('no.a.real.preference')).rejects.toThrowError();
await expect(preferences.getBooleanPreferenceById('not.a.real.preference')).rejects.toThrowError();
await expect(preferences.setBooleanPreferenceById('not.a.real.preference', true)).rejects.toThrowError();
await expect(preferences.resetPreferenceById('not.a.real.preference')).rejects.toThrowError();

await expect(preferences.getStringPreferenceById('no.a.real.preference')).rejects.toThrowError();
await expect(preferences.setStringPreferenceById('no.a.real.preference', 'a')).rejects.toThrowError();
await expect(preferences.resetStringPreferenceById('no.a.real.preference')).rejects.toThrowError();
await expect(preferences.getStringPreferenceById('not.a.real.preference')).rejects.toThrowError();
await expect(preferences.setStringPreferenceById('not.a.real.preference', 'a')).rejects.toThrowError();
await expect(preferences.resetPreferenceById('not.a.real.preference')).rejects.toThrowError();

await expect(preferences.getOptionsPreferenceById('not.a.real.preference')).rejects.toThrowError();
await expect(preferences.setOptionsPreferenceById('not.a.real.preference', 'a')).rejects.toThrowError();
await expect(preferences.resetPreferenceById('not.a.real.preference')).rejects.toThrowError();
} finally {
preferences.customTimeout = undefined;
}
Expand All @@ -86,8 +105,14 @@ test.describe('Preference View', () => {
const stringPreference = PreferenceIds.DiffEditor.MaxComputationTime;
const booleanPreference = PreferenceIds.Explorer.AutoReveal;

await expect(preferences.getBooleanPreferenceById(stringPreference)).rejects.toThrowError();
await expect(preferences.setBooleanPreferenceById(stringPreference, true)).rejects.toThrowError();
await expect(preferences.setStringPreferenceById(booleanPreference, 'true')).rejects.toThrowError();
preferences.customTimeout = 500;
try {
await expect(preferences.getBooleanPreferenceById(stringPreference)).rejects.toThrowError();
await expect(preferences.setBooleanPreferenceById(stringPreference, true)).rejects.toThrowError();
await expect(preferences.setStringPreferenceById(booleanPreference, 'true')).rejects.toThrowError();
await expect(preferences.setOptionsPreferenceById(booleanPreference, 'true')).rejects.toThrowError();
} finally {
preferences.customTimeout = undefined;
}
});
});
8 changes: 7 additions & 1 deletion examples/playwright/src/tests/theia-text-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// *****************************************************************************

import { expect } from '@playwright/test';
import { DefaultPreferences, PreferenceIds, TheiaPreferenceView } from '../theia-preference-view';
import { TheiaApp } from '../theia-app';
import { TheiaTextEditor } from '../theia-text-editor';
import { TheiaWorkspace } from '../theia-workspace';
Expand All @@ -27,6 +28,11 @@ test.describe('Theia Text Editor', () => {
test.beforeAll(async () => {
const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']);
app = await TheiaApp.load(page, ws);

// set auto-save preference to off
const preferenceView = await app.openPreferences(TheiaPreferenceView);
await preferenceView.setOptionsPreferenceById(PreferenceIds.Editor.AutoSave, DefaultPreferences.Editor.AutoSave.Off);
await preferenceView.close();
});

test('should be visible and active after opening "sample.txt"', async () => {
Expand Down Expand Up @@ -164,7 +170,7 @@ test.describe('Theia Text Editor', () => {
await sampleTextEditor.saveAndClose();
});

test.skip('should close without saving', async () => {
test('should close without saving', async () => {
const sampleTextEditor = await app.openEditor('sample.txt', TheiaTextEditor);
await sampleTextEditor.replaceLineWithLineNumber('change again', 1);
expect(await sampleTextEditor.isDirty()).toBe(true);
Expand Down
150 changes: 78 additions & 72 deletions examples/playwright/src/theia-preference-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const TheiaSettingsViewData = {
};

export const PreferenceIds = {
Editor: {
AutoSave: 'files.autoSave',
RenderWhitespace: 'editor.renderWhitespace'
},
Explorer: {
AutoReveal: 'explorer.autoReveal'
},
Expand All @@ -33,6 +37,21 @@ export const PreferenceIds = {
};

export const DefaultPreferences = {
Editor: {
AutoSave: {
Off: 'off',
AfterDelay: 'afterDelay',
OnFocusChange: 'onFocusChange',
OnWindowChange: 'onWindowChange'
},
RenderWhitespace: {
None: 'none',
Boundary: 'boundary',
Selection: 'selection',
Trailing: 'trailing',
All: 'all'
}
},
Explorer: {
AutoReveal: {
Enabled: true
Expand All @@ -50,6 +69,10 @@ export enum TheiaPreferenceScope {

export class TheiaPreferenceView extends TheiaView {
public customTimeout?: number;
protected modificationIndicator = '.theia-mod-item-modified';
protected optionSelectLabel = '.theia-select-component-label';
protected optionSelectDropdown = '.theia-select-component-dropdown';
protected optionSelectDropdownValue = '.theia-select-component-option-value';

constructor(app: TheiaApp) {
super(TheiaSettingsViewData, app);
Expand All @@ -72,98 +95,79 @@ export class TheiaPreferenceView extends TheiaView {
await scopeTab.click();
}

async getBooleanPreferenceByPath(sectionTitle: string, name: string): Promise<boolean> {
const preferenceId = await this.findPreferenceId(sectionTitle, name);
return this.getBooleanPreferenceById(preferenceId);
}

async getBooleanPreferenceById(preferenceId: string): Promise<boolean> {
const element = await this.findPreferenceEditorById(preferenceId);
return element.isChecked();
}

async getBooleanPreferenceByPath(sectionTitle: string, name: string): Promise<boolean> {
async setBooleanPreferenceByPath(sectionTitle: string, name: string, value: boolean): Promise<void> {
const preferenceId = await this.findPreferenceId(sectionTitle, name);
return this.getBooleanPreferenceById(preferenceId);
return this.setBooleanPreferenceById(preferenceId, value);
}

async setBooleanPreferenceById(preferenceId: string, value: boolean): Promise<void> {
const element = await this.findPreferenceEditorById(preferenceId);
return value ? element.check() : element.uncheck();
}

async setBooleanPreferenceByPath(sectionTitle: string, name: string, value: boolean): Promise<void> {
async getStringPreferenceByPath(sectionTitle: string, name: string): Promise<string> {
const preferenceId = await this.findPreferenceId(sectionTitle, name);
return this.setBooleanPreferenceById(preferenceId, value);
return this.getStringPreferenceById(preferenceId);
}

async getStringPreferenceById(preferenceId: string): Promise<string> {
const element = await this.findPreferenceEditorById(preferenceId);
return element.evaluate(e => (e as HTMLInputElement).value);
}

async getStringPreferenceByPath(sectionTitle: string, name: string): Promise<string> {
async setStringPreferenceByPath(sectionTitle: string, name: string, value: string): Promise<void> {
const preferenceId = await this.findPreferenceId(sectionTitle, name);
return this.getStringPreferenceById(preferenceId);
return this.setStringPreferenceById(preferenceId, value);
}

async setStringPreferenceById(preferenceId: string, value: string): Promise<void> {
const element = await this.findPreferenceEditorById(preferenceId);
return element.fill(value);
}

async setStringPreferenceByPath(sectionTitle: string, name: string, value: string): Promise<void> {
async getOptionsPreferenceByPath(sectionTitle: string, name: string): Promise<string> {
const preferenceId = await this.findPreferenceId(sectionTitle, name);
return this.setStringPreferenceById(preferenceId, value);
return this.getOptionsPreferenceById(preferenceId);
}

async waitForModified(preferenceId: string): Promise<void> {
await this.activate();
const viewElement = await this.viewElement();
await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}.theia-mod-item-modified`, { timeout: this.customTimeout });
async getOptionsPreferenceById(preferenceId: string): Promise<string> {
const element = await this.findPreferenceEditorById(preferenceId, this.optionSelectLabel);
return element.evaluate(e => e.textContent ?? '');
}

async resetStringPreferenceById(preferenceId: string): Promise<void> {
const resetPreferenceButton = await this.findPreferenceResetButton(preferenceId);
if (!resetPreferenceButton) {
// preference not modified
return;
}
const previousValue = await this.getStringPreferenceById(preferenceId);
const selector = this.getPreferenceEditorSelector(preferenceId);
const done = await resetPreferenceButton.click();
await this.page.waitForFunction(data => {
const element = document.querySelector(data.selector);
if (!element) {
throw new Error(`Could not find preference element with id "${data.preferenceId}"`);
}
const value = (element as HTMLInputElement).value;
return value !== data.previousValue;
}, { preferenceId, selector, previousValue, done }, { timeout: this.customTimeout });
}

async resetStringPreferenceByPath(sectionTitle: string, name: string): Promise<void> {
async setOptionsPreferenceByPath(sectionTitle: string, name: string, value: string): Promise<void> {
const preferenceId = await this.findPreferenceId(sectionTitle, name);
return this.resetStringPreferenceById(preferenceId);
return this.setOptionsPreferenceById(preferenceId, value);
}

async resetBooleanPreferenceById(preferenceId: string): Promise<void> {
const resetPreferenceButton = await this.findPreferenceResetButton(preferenceId);
if (!resetPreferenceButton) {
// preference not modified
return;
}
const previousValue = await this.getBooleanPreferenceById(preferenceId);
const selector = this.getPreferenceEditorSelector(preferenceId);
const done = await resetPreferenceButton.click();
await this.page.waitForFunction(data => {
const element = document.querySelector(data.selector);
if (!element) {
throw new Error(`Could not find preference element with id "${data.preferenceId}"`);
}
const value = (element as HTMLInputElement).checked;
return value !== data.previousValue;
}, { preferenceId, selector, previousValue, done }, { timeout: this.customTimeout });
}

async resetBooleanPreferenceByPath(sectionTitle: string, name: string): Promise<void> {
async setOptionsPreferenceById(preferenceId: string, value: string): Promise<void> {
const element = await this.findPreferenceEditorById(preferenceId, this.optionSelectLabel);
await element.click();
const option = await this.page.waitForSelector(`${this.optionSelectDropdown} ${this.optionSelectDropdownValue}:has-text("${value}")`);
await option.click();
}

async resetPreferenceByPath(sectionTitle: string, name: string): Promise<void> {
const preferenceId = await this.findPreferenceId(sectionTitle, name);
return this.resetBooleanPreferenceById(preferenceId);
return this.resetPreferenceById(preferenceId);
}

async resetPreferenceById(preferenceId: string): Promise<void> {
// this is just to fail if the preference doesn't exist at all
await this.findPreferenceEditorById(preferenceId, '');
const resetPreferenceButton = await this.findPreferenceResetButton(preferenceId);
await resetPreferenceButton.click();
await this.waitForUnmodified(preferenceId);
}

private async findPreferenceId(sectionTitle: string, name: string): Promise<string> {
Expand All @@ -178,9 +182,9 @@ export class TheiaPreferenceView extends TheiaView {
return preferenceId;
}

private async findPreferenceEditorById(preferenceId: string): Promise<ElementHandle<SVGElement | HTMLElement>> {
private async findPreferenceEditorById(preferenceId: string, elementType: string = 'input'): Promise<ElementHandle<SVGElement | HTMLElement>> {
const viewElement = await this.viewElement();
const element = await viewElement?.waitForSelector(this.getPreferenceEditorSelector(preferenceId), { timeout: this.customTimeout });
const element = await viewElement?.waitForSelector(this.getPreferenceEditorSelector(preferenceId, elementType), { timeout: this.customTimeout });
if (!element) {
throw new Error(`Could not find element with preference id "${preferenceId}"`);
}
Expand All @@ -191,26 +195,13 @@ export class TheiaPreferenceView extends TheiaView {
return `li[data-pref-id="${preferenceId}"]`;
}

private getPreferenceEditorSelector(preferenceId: string): string {
return `${this.getPreferenceSelector(preferenceId)} input`;
private getPreferenceEditorSelector(preferenceId: string, elementType: string): string {
return `${this.getPreferenceSelector(preferenceId)} ${elementType}`;
}

private getPreferenceGutterSelector(preferenceId: string): string {
return `${this.getPreferenceSelector(preferenceId)} .pref-context-gutter`;
}

private async findPreferenceResetButton(preferenceId: string): Promise<ElementHandle<SVGElement | HTMLElement> | undefined> {
private async findPreferenceResetButton(preferenceId: string): Promise<ElementHandle<SVGElement | HTMLElement>> {
await this.activate();
const viewElement = await this.viewElement();
const gutter = await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}`, { timeout: this.customTimeout });
if (!gutter) {
throw new Error(`Could not determine modified state for element with preference id "${preferenceId}"`);
}
const isModified = await gutter.evaluate(e => e.classList.contains('theia-mod-item-modified'));
if (!isModified) {
return undefined;
}

const settingsContextMenuBtn = await viewElement?.waitForSelector(`${this.getPreferenceSelector(preferenceId)} .settings-context-menu-btn`);
if (!settingsContextMenuBtn) {
throw new Error(`Could not find context menu button for element with preference id "${preferenceId}"`);
Expand All @@ -223,4 +214,19 @@ export class TheiaPreferenceView extends TheiaView {
return resetPreferenceButton;
}

async waitForModified(preferenceId: string): Promise<void> {
await this.activate();
const viewElement = await this.viewElement();
await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}${this.modificationIndicator}`, { timeout: this.customTimeout });
}

async waitForUnmodified(preferenceId: string): Promise<void> {
await this.activate();
const viewElement = await this.viewElement();
await viewElement?.waitForSelector(`${this.getPreferenceGutterSelector(preferenceId)}${this.modificationIndicator}`, { state: 'detached', timeout: this.customTimeout });
}

private getPreferenceGutterSelector(preferenceId: string): string {
return `${this.getPreferenceSelector(preferenceId)} .pref-context-gutter`;
}
}

0 comments on commit 67b3e78

Please sign in to comment.