mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) In custom widgets show placeholder content until all columns are mapped
Summary: Showing configuration screen when widget is not mapped Test Plan: New test added Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D4192
This commit is contained in:
@@ -40,9 +40,9 @@ describe('AttachedCustomWidget', function () {
|
||||
.header('Content-Type', 'text/html')
|
||||
.send('<html><head><script src="/grist-plugin-api.js"></script></head><body>\n' +
|
||||
(req.query.name || req.query.access) + // send back widget name from query string or access level
|
||||
'</body>'+
|
||||
"<script>grist.ready({requiredAccess: 'full', columns: [{name: 'Content', type: 'Text'}],"+
|
||||
" onEditOptions(){}})</script>"+
|
||||
'</body>' +
|
||||
"<script>grist.ready({requiredAccess: 'full', columns: [{name: 'Content', type: 'Text', optional: true}]," +
|
||||
" onEditOptions(){}})</script>" +
|
||||
'</html>\n')
|
||||
.end()
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {assert, driver, Key} from 'mocha-webdriver';
|
||||
import {addToRepl, assert, driver, Key} from 'mocha-webdriver';
|
||||
import * as gu from 'test/nbrowser/gristUtils';
|
||||
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
|
||||
import {addStatic, serveSomething} from 'test/server/customUtil';
|
||||
@@ -15,6 +15,7 @@ const NORMAL_WIDGET = 'Normal';
|
||||
const READ_WIDGET = 'Read';
|
||||
const FULL_WIDGET = 'Full';
|
||||
const COLUMN_WIDGET = 'COLUMN_WIDGET';
|
||||
const REQUIRED_WIDGET = 'REQUIRED_WIDGET';
|
||||
// Custom URL label in selectbox.
|
||||
const CUSTOM_URL = 'Custom URL';
|
||||
// Holds url for sample widget server.
|
||||
@@ -129,6 +130,9 @@ describe('CustomWidgetsConfig', function () {
|
||||
let mainSession: gu.Session;
|
||||
gu.bigScreen();
|
||||
|
||||
|
||||
addToRepl('getOptions', getOptions);
|
||||
|
||||
before(async function () {
|
||||
if (server.isExternalServer()) {
|
||||
this.skip();
|
||||
@@ -164,9 +168,15 @@ describe('CustomWidgetsConfig', function () {
|
||||
{
|
||||
// Widget with column mapping
|
||||
name: COLUMN_WIDGET,
|
||||
url: createConfigUrl({requiredAccess: AccessLevel.read_table, columns: ['Column']}),
|
||||
url: createConfigUrl({requiredAccess: AccessLevel.read_table, columns: [{name:'Column', optional: true}]}),
|
||||
widgetId: 'tester5',
|
||||
},
|
||||
{
|
||||
// Widget with required column mapping
|
||||
name: REQUIRED_WIDGET,
|
||||
url: createConfigUrl({requiredAccess: AccessLevel.read_table, columns: [{name:'Column', optional: false}]}),
|
||||
widgetId: 'tester6',
|
||||
},
|
||||
]);
|
||||
});
|
||||
addStatic(app);
|
||||
@@ -188,146 +198,6 @@ describe('CustomWidgetsConfig', function () {
|
||||
await server.testingHooks.setWidgetRepositoryUrl('');
|
||||
});
|
||||
|
||||
// Poor man widget rpc. Class that invokes various parts in the tester widget.
|
||||
class Widget {
|
||||
constructor() {}
|
||||
// Wait for a frame.
|
||||
public async waitForFrame() {
|
||||
await driver.findWait(`iframe.test-custom-widget-ready`, 1000);
|
||||
await driver.wait(async () => await driver.find('iframe').isDisplayed(), 1000);
|
||||
await widget.waitForPendingRequests();
|
||||
}
|
||||
public async waitForPendingRequests() {
|
||||
await this._inWidgetIframe(async () => {
|
||||
await driver.executeScript('grist.testWaitForPendingRequests();');
|
||||
});
|
||||
}
|
||||
public async content() {
|
||||
return await this._read('body');
|
||||
}
|
||||
public async readonly() {
|
||||
const text = await this._read('#readonly');
|
||||
return text === 'true';
|
||||
}
|
||||
public async access() {
|
||||
const text = await this._read('#access');
|
||||
return text as AccessLevel;
|
||||
}
|
||||
public async onRecordMappings() {
|
||||
const text = await this._read('#onRecordMappings');
|
||||
return JSON.parse(text || 'null');
|
||||
}
|
||||
public async onRecords() {
|
||||
const text = await this._read('#onRecords');
|
||||
return JSON.parse(text || 'null');
|
||||
}
|
||||
public async onRecord() {
|
||||
const text = await this._read('#onRecord');
|
||||
return JSON.parse(text || 'null');
|
||||
}
|
||||
public async onRecordsMappings() {
|
||||
const text = await this._read('#onRecordsMappings');
|
||||
return JSON.parse(text || 'null');
|
||||
}
|
||||
public async log() {
|
||||
const text = await this._read('#log');
|
||||
return text || '';
|
||||
}
|
||||
// Wait for frame to close.
|
||||
public async waitForClose() {
|
||||
await driver.wait(async () => !(await driver.find('iframe').isPresent()), 3000);
|
||||
}
|
||||
// Wait for the onOptions event, and return its value.
|
||||
public async onOptions() {
|
||||
const text = await this._inWidgetIframe(async () => {
|
||||
// Wait for options to get filled, initially this div is empty,
|
||||
// as first message it should get at least null as an options.
|
||||
await driver.wait(async () => await driver.find('#onOptions').getText(), 3000);
|
||||
return await driver.find('#onOptions').getText();
|
||||
});
|
||||
return JSON.parse(text);
|
||||
}
|
||||
public async wasConfigureCalled() {
|
||||
const text = await this._read('#configure');
|
||||
return text === 'called';
|
||||
}
|
||||
public async setOptions(options: any) {
|
||||
return await this.invokeOnWidget('setOptions', [options]);
|
||||
}
|
||||
public async setOption(key: string, value: any) {
|
||||
return await this.invokeOnWidget('setOption', [key, value]);
|
||||
}
|
||||
public async getOption(key: string) {
|
||||
return await this.invokeOnWidget('getOption', [key]);
|
||||
}
|
||||
public async clearOptions() {
|
||||
return await this.invokeOnWidget('clearOptions');
|
||||
}
|
||||
public async getOptions() {
|
||||
return await this.invokeOnWidget('getOptions');
|
||||
}
|
||||
public async mappings() {
|
||||
return await this.invokeOnWidget('mappings');
|
||||
}
|
||||
public async clearLog() {
|
||||
return await this.invokeOnWidget('clearLog');
|
||||
}
|
||||
// Invoke method on a Custom Widget.
|
||||
// Each method is available as a button with content that is equal to the method name.
|
||||
// It accepts single argument, that we pass by serializing it to #input textbox. Widget invokes
|
||||
// the method and serializes its return value to #output div. When there is an error, it is also
|
||||
// serialized to the #output div.
|
||||
public async invokeOnWidget(name: string, input?: any[]) {
|
||||
// Switch to frame.
|
||||
const iframe = driver.find('iframe');
|
||||
await driver.switchTo().frame(iframe);
|
||||
// Clear input box that holds arguments.
|
||||
await driver.find('#input').click();
|
||||
await gu.clearInput();
|
||||
// Serialize argument to the textbox (or leave empty).
|
||||
if (input !== undefined) {
|
||||
await driver.sendKeys(JSON.stringify(input));
|
||||
}
|
||||
// Find button that is responsible for invoking method.
|
||||
await driver.findContent('button', gu.exactMatch(name)).click();
|
||||
// Wait for the #output div to be filled with a result. Custom Widget will set it to
|
||||
// "waiting..." before invoking the method.
|
||||
await driver.wait(async () => (await driver.find('#output').value()) !== 'waiting...');
|
||||
// Read the result.
|
||||
const text = await driver.find('#output').getText();
|
||||
// Switch back to main window.
|
||||
await driver.switchTo().defaultContent();
|
||||
// If the method was a void method, the output will be "undefined".
|
||||
if (text === 'undefined') {
|
||||
return; // Simulate void method.
|
||||
}
|
||||
// Result will always be parsed json.
|
||||
const parsed = JSON.parse(text);
|
||||
// All exceptions will be serialized to { error : <<Error.message>> }
|
||||
if (parsed?.error) {
|
||||
// Rethrow the error.
|
||||
throw new Error(parsed.error);
|
||||
} else {
|
||||
// Or return result.
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
private async _read(selector: string) {
|
||||
return this._inWidgetIframe(() => driver.find(selector).getText());
|
||||
}
|
||||
|
||||
private async _inWidgetIframe<T>(callback: () => Promise<T>) {
|
||||
const iframe = driver.find('iframe');
|
||||
await driver.switchTo().frame(iframe);
|
||||
const retVal = await callback();
|
||||
await driver.switchTo().defaultContent();
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
// Rpc for main widget (Custom Widget).
|
||||
const widget = new Widget();
|
||||
|
||||
beforeEach(async () => {
|
||||
// Before each test, we will switch to Custom Url (to cleanup the widget)
|
||||
// and then back to the Tester widget.
|
||||
@@ -337,6 +207,47 @@ describe('CustomWidgetsConfig', function () {
|
||||
}
|
||||
await toggleWidgetMenu();
|
||||
await clickOption(TESTER_WIDGET);
|
||||
await widget.waitForFrame();
|
||||
});
|
||||
|
||||
it('should hide widget when some columns are not mapped', async () => {
|
||||
// Reset the widget to the one that has a column mapping requirements.
|
||||
await widget.resetWidget();
|
||||
|
||||
// Since the widget was reset, we don't have .test-custom-widget-ready element.
|
||||
assert.isFalse(await driver.find('.test-custom-widget-ready').isPresent());
|
||||
|
||||
// Now select the widget that requires a column.
|
||||
await toggleWidgetMenu();
|
||||
await clickOption(REQUIRED_WIDGET);
|
||||
await gu.acceptAccessRequest();
|
||||
|
||||
// The widget iframe should be covered with a text explaining that the widget is not configured.
|
||||
assert.isTrue(await driver.findWait('.test-custom-widget-not-mapped', 1000).isDisplayed());
|
||||
|
||||
// The content should at least have those words:
|
||||
assert.include(await driver.find('.test-custom-widget-not-mapped').getText(),
|
||||
"Some required columns aren't mapped");
|
||||
|
||||
// Make sure that the iframe is not displayed.
|
||||
assert.isFalse(await driver.find('.test-custom-widget-ready').isPresent());
|
||||
|
||||
// Now map the column.
|
||||
await toggleDrop(pickerDrop('Column'));
|
||||
|
||||
// Map it to A.
|
||||
await clickOption('A');
|
||||
|
||||
// Make sure that the text is gone.
|
||||
await gu.waitToPass(async () => {
|
||||
assert.isFalse(await driver.find('.test-config-widget-not-mapped').isPresent());
|
||||
});
|
||||
|
||||
// Make sure the widget is now visible.
|
||||
assert.isTrue(await driver.find('.test-custom-widget-ready').isDisplayed());
|
||||
|
||||
// And we see widget with info about mapped columns, Column to A.
|
||||
assert.deepEqual(await widget.onRecordsMappings(), {Column: 'A'});
|
||||
});
|
||||
|
||||
it('should hide mappings when there is no good column', async () => {
|
||||
@@ -346,7 +257,7 @@ describe('CustomWidgetsConfig', function () {
|
||||
}
|
||||
await gu.setWidgetUrl(
|
||||
createConfigUrl({
|
||||
columns: [{name: 'M2', type: 'Date'}],
|
||||
columns: [{name: 'M2', type: 'Date', optional: true}],
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
);
|
||||
@@ -382,7 +293,7 @@ describe('CustomWidgetsConfig', function () {
|
||||
|
||||
// Now expand the drop again and make sure we can't clear it.
|
||||
await toggleDrop(pickerDrop('M2'));
|
||||
assert.deepEqual(await getOptions(), ['NewCol']);
|
||||
assert.deepEqual(await getOptions(), ['NewCol', 'Clear selection']);
|
||||
|
||||
// Now remove the column, and make sure that the drop is disabled again.
|
||||
await driver.sendKeys(Key.ESCAPE);
|
||||
@@ -485,11 +396,8 @@ describe('CustomWidgetsConfig', function () {
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
);
|
||||
await widget.waitForFrame();
|
||||
await gu.acceptAccessRequest();
|
||||
await widget.waitForPendingRequests();
|
||||
// Mappings should be empty
|
||||
assert.isNull(await widget.onRecordsMappings());
|
||||
await widget.waitForPlaceholder();
|
||||
// We should see 4 pickers
|
||||
assert.isTrue(await driver.find(pickerLabel('M1')).isPresent());
|
||||
assert.isTrue(await driver.find(pickerLabel('M2')).isPresent());
|
||||
@@ -508,27 +416,20 @@ describe('CustomWidgetsConfig', function () {
|
||||
// Should be able to select column A for all options
|
||||
await toggleDrop(pickerDrop('M1'));
|
||||
await clickOption('A');
|
||||
await widget.waitForPendingRequests();
|
||||
const empty = {M1: null, M2: null, M3: null, M4: null};
|
||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A'});
|
||||
await toggleDrop(pickerDrop('M2'));
|
||||
await clickOption('A');
|
||||
await widget.waitForPendingRequests();
|
||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A', M2: 'A'});
|
||||
await toggleDrop(pickerDrop('M3'));
|
||||
await clickOption('A');
|
||||
await widget.waitForPendingRequests();
|
||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A', M2: 'A', M3: 'A'});
|
||||
await toggleDrop(pickerDrop('M4'));
|
||||
await clickOption('A');
|
||||
await widget.waitForFrame();
|
||||
await widget.waitForPendingRequests();
|
||||
assert.deepEqual(await widget.onRecordsMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'});
|
||||
// Single record should also receive update.
|
||||
assert.deepEqual(await widget.onRecordMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'});
|
||||
// Undo should revert mappings - there should be only 3 operations to revert to first mapping.
|
||||
await gu.undo(3);
|
||||
await widget.waitForPendingRequests();
|
||||
assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A'});
|
||||
await widget.waitForPlaceholder();
|
||||
// Add another columns, numeric B and any C.
|
||||
await gu.selectSectionByTitle('Table');
|
||||
await gu.addColumn('B');
|
||||
@@ -541,10 +442,6 @@ describe('CustomWidgetsConfig', function () {
|
||||
assert.deepEqual(await getOptions(), ['A', 'B', 'C']);
|
||||
await toggleDrop(pickerDrop('M4'));
|
||||
assert.deepEqual(await getOptions(), ['A', 'C']);
|
||||
await toggleDrop(pickerDrop('M1'));
|
||||
await clickOption('B');
|
||||
await widget.waitForPendingRequests();
|
||||
assert.deepEqual(await widget.onRecordsMappings(), {...empty, M1: 'B'});
|
||||
await revert();
|
||||
});
|
||||
|
||||
@@ -602,8 +499,8 @@ describe('CustomWidgetsConfig', function () {
|
||||
await gu.setWidgetUrl(
|
||||
createConfigUrl({
|
||||
columns: [
|
||||
{name: 'M1', allowMultiple: true},
|
||||
{name: 'M2', type: 'Text', allowMultiple: true},
|
||||
{name: 'M1', allowMultiple: true, optional: true},
|
||||
{name: 'M2', type: 'Text', allowMultiple: true, optional: true},
|
||||
],
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
@@ -686,8 +583,8 @@ describe('CustomWidgetsConfig', function () {
|
||||
await gu.setWidgetUrl(
|
||||
createConfigUrl({
|
||||
columns: [
|
||||
{name: 'M1', type: 'Date,DateTime'},
|
||||
{name: 'M2', type: 'Date, DateTime ', allowMultiple: true},
|
||||
{name: 'M1', type: 'Date,DateTime', optional: true},
|
||||
{name: 'M2', type: 'Date, DateTime ', allowMultiple: true, optional: true},
|
||||
],
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
@@ -747,10 +644,10 @@ describe('CustomWidgetsConfig', function () {
|
||||
await gu.setWidgetUrl(
|
||||
createConfigUrl({
|
||||
columns: [
|
||||
{name: 'Any', type: 'Any', strictType: true},
|
||||
{name: 'Date_Numeric', type: 'Date, Numeric', strictType: true},
|
||||
{name: 'Date_Any', type: 'Date, Any', strictType: true},
|
||||
{name: 'Date', type: 'Date', strictType: true},
|
||||
{name: 'Any', type: 'Any', strictType: true, optional: true},
|
||||
{name: 'Date_Numeric', type: 'Date, Numeric', strictType: true, optional: true},
|
||||
{name: 'Date_Any', type: 'Date, Any', strictType: true, optional: true},
|
||||
{name: 'Date', type: 'Date', strictType: true, optional: true},
|
||||
],
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
@@ -791,7 +688,7 @@ describe('CustomWidgetsConfig', function () {
|
||||
await gu.setWidgetUrl(
|
||||
createConfigUrl({
|
||||
columns: [
|
||||
{name: 'Choice', type: 'Choice', strictType: true},
|
||||
{name: 'Choice', type: 'Choice', strictType: true, optional: true},
|
||||
],
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
@@ -839,7 +736,7 @@ describe('CustomWidgetsConfig', function () {
|
||||
await clickOption(CUSTOM_URL);
|
||||
await gu.setWidgetUrl(
|
||||
createConfigUrl({
|
||||
columns: [{name: 'M1'}, {name: 'M2', allowMultiple: true}],
|
||||
columns: [{name: 'M1', optional: true}, {name: 'M2', allowMultiple: true, optional: true}],
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
);
|
||||
@@ -905,7 +802,7 @@ describe('CustomWidgetsConfig', function () {
|
||||
// Add B column as a new one.
|
||||
await toggleDrop(pickerDrop('M1'));
|
||||
// Make sure it is there to select.
|
||||
assert.deepEqual(await getOptions(), ['A', 'C', 'B']);
|
||||
assert.deepEqual(await getOptions(), ['A', 'C', 'B', 'Clear selection']);
|
||||
await clickOption('B');
|
||||
await widget.waitForPendingRequests();
|
||||
await click(pickerAdd('M2'));
|
||||
@@ -928,7 +825,10 @@ describe('CustomWidgetsConfig', function () {
|
||||
await clickOption(CUSTOM_URL);
|
||||
await gu.setWidgetUrl(
|
||||
createConfigUrl({
|
||||
columns: [{name: 'M1', type: 'Text'}, {name: 'M2', type: 'Text', allowMultiple: true}],
|
||||
columns: [
|
||||
{name: 'M1', type: 'Text', optional: true},
|
||||
{name: 'M2', type: 'Text', allowMultiple: true, optional: true}
|
||||
],
|
||||
requiredAccess: 'read table',
|
||||
})
|
||||
);
|
||||
@@ -1220,3 +1120,152 @@ describe('CustomWidgetsConfig', function () {
|
||||
await refresh();
|
||||
});
|
||||
});
|
||||
|
||||
// Poor man widget rpc. Class that invokes various parts in the tester widget.
|
||||
const widget = {
|
||||
async waitForPlaceholder() {
|
||||
assert.isTrue(await driver.findWait('.test-custom-widget-not-mapped', 1000).isDisplayed());
|
||||
},
|
||||
// Wait for a frame.
|
||||
async waitForFrame() {
|
||||
await driver.findWait(`iframe.test-custom-widget-ready`, 1000);
|
||||
await driver.wait(async () => await driver.find('iframe').isDisplayed(), 1000);
|
||||
await widget.waitForPendingRequests();
|
||||
},
|
||||
async waitForPendingRequests() {
|
||||
await this._inWidgetIframe(async () => {
|
||||
await driver.executeScript('grist.testWaitForPendingRequests();');
|
||||
});
|
||||
},
|
||||
async content() {
|
||||
return await this._read('body');
|
||||
},
|
||||
async readonly() {
|
||||
const text = await this._read('#readonly');
|
||||
return text === 'true';
|
||||
},
|
||||
async access() {
|
||||
const text = await this._read('#access');
|
||||
return text as AccessLevel;
|
||||
},
|
||||
async onRecordMappings() {
|
||||
const text = await this._read('#onRecordMappings');
|
||||
return JSON.parse(text || 'null');
|
||||
},
|
||||
async onRecords() {
|
||||
const text = await this._read('#onRecords');
|
||||
return JSON.parse(text || 'null');
|
||||
},
|
||||
async onRecord() {
|
||||
const text = await this._read('#onRecord');
|
||||
return JSON.parse(text || 'null');
|
||||
},
|
||||
/**
|
||||
* Reads last mapping parameter received by the widget as part of onRecords call.
|
||||
*/
|
||||
async onRecordsMappings() {
|
||||
const text = await this._read('#onRecordsMappings');
|
||||
return JSON.parse(text || 'null');
|
||||
},
|
||||
async log() {
|
||||
const text = await this._read('#log');
|
||||
return text || '';
|
||||
},
|
||||
// Wait for frame to close.
|
||||
async waitForClose() {
|
||||
await driver.wait(async () => !(await driver.find('iframe').isPresent()), 3000);
|
||||
},
|
||||
// Wait for the onOptions event, and return its value.
|
||||
async onOptions() {
|
||||
const text = await this._inWidgetIframe(async () => {
|
||||
// Wait for options to get filled, initially this div is empty,
|
||||
// as first message it should get at least null as an options.
|
||||
await driver.wait(async () => await driver.find('#onOptions').getText(), 3000);
|
||||
return await driver.find('#onOptions').getText();
|
||||
});
|
||||
return JSON.parse(text);
|
||||
},
|
||||
async wasConfigureCalled() {
|
||||
const text = await this._read('#configure');
|
||||
return text === 'called';
|
||||
},
|
||||
async setOptions(options: any) {
|
||||
return await this.invokeOnWidget('setOptions', [options]);
|
||||
},
|
||||
async setOption(key: string, value: any) {
|
||||
return await this.invokeOnWidget('setOption', [key, value]);
|
||||
},
|
||||
async getOption(key: string) {
|
||||
return await this.invokeOnWidget('getOption', [key]);
|
||||
},
|
||||
async clearOptions() {
|
||||
return await this.invokeOnWidget('clearOptions');
|
||||
},
|
||||
async getOptions() {
|
||||
return await this.invokeOnWidget('getOptions');
|
||||
},
|
||||
async mappings() {
|
||||
return await this.invokeOnWidget('mappings');
|
||||
},
|
||||
async clearLog() {
|
||||
return await this.invokeOnWidget('clearLog');
|
||||
},
|
||||
// Invoke method on a Custom Widget.
|
||||
// Each method is available as a button with content that is equal to the method name.
|
||||
// It accepts single argument, that we pass by serializing it to #input textbox. Widget invokes
|
||||
// the method and serializes its return value to #output div. When there is an error, it is also
|
||||
// serialized to the #output div.
|
||||
async invokeOnWidget(name: string, input?: any[]) {
|
||||
// Switch to frame.
|
||||
const iframe = driver.find('iframe');
|
||||
await driver.switchTo().frame(iframe);
|
||||
// Clear input box that holds arguments.
|
||||
await driver.find('#input').click();
|
||||
await gu.clearInput();
|
||||
// Serialize argument to the textbox (or leave empty).
|
||||
if (input !== undefined) {
|
||||
await driver.sendKeys(JSON.stringify(input));
|
||||
}
|
||||
// Find button that is responsible for invoking method.
|
||||
await driver.findContent('button', gu.exactMatch(name)).click();
|
||||
// Wait for the #output div to be filled with a result. Custom Widget will set it to
|
||||
// "waiting..." before invoking the method.
|
||||
await driver.wait(async () => (await driver.find('#output').value()) !== 'waiting...');
|
||||
// Read the result.
|
||||
const text = await driver.find('#output').getText();
|
||||
// Switch back to main window.
|
||||
await driver.switchTo().defaultContent();
|
||||
// If the method was a void method, the output will be "undefined".
|
||||
if (text === 'undefined') {
|
||||
return; // Simulate void method.
|
||||
}
|
||||
// Result will always be parsed json.
|
||||
const parsed = JSON.parse(text);
|
||||
// All exceptions will be serialized to { error : <<Error.message>> }
|
||||
if (parsed?.error) {
|
||||
// Rethrow the error.
|
||||
throw new Error(parsed.error);
|
||||
} else {
|
||||
// Or return result.
|
||||
return parsed;
|
||||
}
|
||||
},
|
||||
async _read(selector: string) {
|
||||
return this._inWidgetIframe(() => driver.find(selector).getText());
|
||||
},
|
||||
async _inWidgetIframe<T>(callback: () => Promise<T>) {
|
||||
const iframe = driver.find('iframe');
|
||||
await driver.switchTo().frame(iframe);
|
||||
const retVal = await callback();
|
||||
await driver.switchTo().defaultContent();
|
||||
return retVal;
|
||||
},
|
||||
/**
|
||||
* Resets the widget by first selecting Custom URL option from the menu, which clearOptions
|
||||
* any existing widget state (even if the Custom URL was already selected).
|
||||
*/
|
||||
async resetWidget() {
|
||||
await toggleWidgetMenu();
|
||||
await clickOption(CUSTOM_URL);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,6 +259,8 @@ describe('GridViewNewColumnMenu', function () {
|
||||
await gu.waitForServer();
|
||||
//discard rename menu
|
||||
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
||||
// Wait for the sidepanel animation.
|
||||
await gu.waitForSidePanel();
|
||||
//check if right menu is opened on column section
|
||||
assert.isTrue(await driver.findWait('.test-right-tab-field', 1000).isDisplayed());
|
||||
await gu.toggleSidePanel("right", "close");
|
||||
|
||||
@@ -3393,7 +3393,7 @@ export async function hasAccessPrompt() {
|
||||
* Accepts new access level.
|
||||
*/
|
||||
export async function acceptAccessRequest() {
|
||||
await driver.find('.test-config-widget-access-accept').click();
|
||||
await driver.findWait('.test-config-widget-access-accept', 1000).click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user