(core) passing language as a query parameter to custom widgets

Summary: to allow custom widget having optional translations, lagunage seeted in user profile is passed as query parameter to custom widget

Test Plan: test added to check if query parameter is existing in url when settings is changed in profile

Reviewers: georgegevoian

Reviewed By: georgegevoian

Subscribers: jarek, paulfitz

Differential Revision: https://phab.getgrist.com/D4045
pull/691/head
Jakub Serafin 8 months ago
parent fbae81648c
commit 498ad07d38

@ -25,6 +25,7 @@ import {UserError} from 'app/client/models/errors';
import {SortedRowSet} from 'app/client/models/rowset';
import {closeRegisteredMenu} from 'app/client/ui2018/menus';
import {AccessLevel} from 'app/common/CustomWidget';
import {defaultLocale} from 'app/common/gutil';
import {PluginInstance} from 'app/common/PluginInstance';
import {getGristConfig} from 'app/common/urlUtils';
import {Events as BackboneEvents} from 'backbone';
@ -213,9 +214,17 @@ export class CustomView extends Disposable {
showAfterReady?: boolean,
}) {
const {baseUrl, access, showAfterReady} = options;
const documentSettings = this.gristDoc.docData.docSettings();
return grains.create(WidgetFrame, {
url: baseUrl || this.getEmptyWidgetPage(),
access,
preferences:
{
culture: documentSettings.locale?? defaultLocale,
language: this.gristDoc.appModel.currentUser?.locale ?? defaultLocale,
timeZone: this.gristDoc.docInfo.timezone() ?? "UTC",
currency: documentSettings.currency?? "USD",
},
readonly: this.gristDoc.isReadonly.get(),
showAfterReady,
onSettingsInitialized: async () => {

@ -73,6 +73,10 @@ export interface WidgetFrameOptions {
* Optional handler to modify the iframe.
*/
onElem?: (iframe: HTMLIFrameElement) => void;
/**
* Optional language to use for the widget.
*/
preferences: {language?: string, timeZone?: any, currency?: string, culture?: string};
}
/**
@ -175,6 +179,9 @@ export class WidgetFrame extends DisposableWithEvents {
const urlObj = new URL(url);
urlObj.searchParams.append('access', this._options.access);
urlObj.searchParams.append('readonly', String(this._options.readonly));
// Append user and document preferences to query string.
const settingsParams = new URLSearchParams(this._options.preferences);
settingsParams.forEach((value, key) => urlObj.searchParams.append(key, value));
return urlObj.href;
};
const fullUrl = urlWithAccess(this._options.url);

@ -38,6 +38,11 @@ const widgetFull = fromAccess(AccessLevel.full);
// Holds widgets manifest content.
let widgets: ICustomWidget[] = [];
// Helper function to get iframe with custom widget.
function getCustomWidgetFrame() {
return driver.findWait('iframe', 500);
}
describe('CustomWidgets', function () {
this.timeout(20000);
const cleanup = setupTestSuite();
@ -50,6 +55,8 @@ describe('CustomWidgets', function () {
return server.testingHooks.setWidgetRepositoryUrl(url ? `${widgetServerUrl}${url}` : '');
}
before(async function () {
if (server.isExternalServer()) {
this.skip();
@ -100,8 +107,14 @@ describe('CustomWidgets', function () {
// Add custom section.
await gu.addNewSection(/Custom/, /Table1/, {selectBy: /TABLE1/});
// Override gristConfig to enable widget list.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
});
after(async function() {
await server.testingHooks.setWidgetRepositoryUrl('');
});
after(async function() {
await server.testingHooks.setWidgetRepositoryUrl('');
});
after(async function() {
@ -109,7 +122,7 @@ describe('CustomWidgets', function () {
});
// Open or close widget menu.
const toggle = () => driver.find('.test-config-widget-select .test-select-open').click();
const toggle = async () => await driver.findWait('.test-config-widget-select .test-select-open', 1000).click();
// Get current value from widget menu.
const current = () => driver.find('.test-config-widget-select .test-select-open').getText();
// Get options from widget menu (must be first opened).
@ -121,19 +134,17 @@ describe('CustomWidgets', function () {
};
// Get rendered content from custom section.
const content = async () => {
const iframe = driver.find('iframe');
await driver.switchTo().frame(iframe);
const text = await driver.find('body').getText();
await driver.switchTo().defaultContent();
return text;
return gu.doInIframe(await getCustomWidgetFrame(), async ()=>{
const text = await driver.find('body').getText();
return text;
});
};
async function execute(
op: (table: TableOperations) => Promise<any>,
tableSelector: (grist: any) => TableOperations = (grist) => grist.selectedTable
) {
const iframe = await driver.find('iframe');
await driver.switchTo().frame(iframe);
try {
return gu.doInIframe(await getCustomWidgetFrame(), async ()=> {
const harness = async (done: any) => {
const grist = (window as any).grist;
grist.ready();
@ -157,9 +168,7 @@ describe('CustomWidgets', function () {
const result = await driver.executeAsyncScript(cmd);
// done callback will return null instead of undefined
return result === "__undefined__" ? undefined : result;
} finally {
await driver.switchTo().defaultContent();
}
});
}
// Replace url for the Custom URL widget.
const setUrl = async (url: string) => {
@ -206,453 +215,510 @@ describe('CustomWidgets', function () {
// Rejects new access level.
const reject = () => driver.find(".test-config-widget-access-reject").click();
it('should show widgets in dropdown', async () => {
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-pagewidget').click();
await gu.waitForServer();
await driver.find('.test-config-widget').click();
await gu.waitForServer(); // Wait for widgets to load.
// Selectbox should have select label.
assert.equal(await current(), CUSTOM_URL);
describe('RightWidgetMenu', () => {
beforeEach(async function () {
// Override gristConfig to enable widget list.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
// We need to be sure that widget configuration panel is open all the time.
await gu.toggleSidePanel('right', 'open');
await recreatePanel();
await driver.findWait('.test-right-tab-pagewidget', 100).click();
});
// There should be 3 options (together with Custom URL)
await toggle();
assert.deepEqual(await options(), [CUSTOM_URL, widget1.name, widget2.name]);
await toggle();
});
it('should show widgets in dropdown', async () => {
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-pagewidget').click();
await gu.waitForServer();
await driver.find('.test-config-widget').click();
await gu.waitForServer(); // Wait for widgets to load.
it('should switch between widgets', async () => {
// Test custom URL.
await toggle();
await select(CUSTOM_URL);
assert.equal(await current(), CUSTOM_URL);
assert.equal(await getUrl(), '');
await setUrl('/200');
assert.equal(await content(), 'OK');
// Test first widget.
await toggle();
await select(widget1.name);
assert.equal(await current(), widget1.name);
assert.equal(await content(), widget1.name);
// Test second widget.
await toggle();
await select(widget2.name);
assert.equal(await current(), widget2.name);
assert.equal(await content(), widget2.name);
// Go back to Custom URL.
await toggle();
await select(CUSTOM_URL);
assert.equal(await getUrl(), '');
assert.equal(await current(), CUSTOM_URL);
await setUrl('/200');
assert.equal(await content(), 'OK');
// Clear url and test if message page is shown.
await setUrl('');
assert.equal(await current(), CUSTOM_URL);
assert.isTrue((await content()).startsWith('Custom widget')); // start page
await recreatePanel();
assert.equal(await current(), CUSTOM_URL);
await gu.undo(7);
});
// Selectbox should have select label.
assert.equal(await current(), CUSTOM_URL);
it('should support theme variables', async () => {
widgets = [widgetWithTheme];
await useManifest(manifestEndpoint);
await recreatePanel();
await toggle();
await select(widgetWithTheme.name);
assert.equal(await current(), widgetWithTheme.name);
assert.equal(await content(), widgetWithTheme.name);
const getWidgetColor = async () => {
const iframe = driver.find('iframe');
await driver.switchTo().frame(iframe);
const color = await driver.find('span').getCssValue('color');
await driver.switchTo().defaultContent();
return color;
};
// There should be 3 options (together with Custom URL)
await toggle();
assert.deepEqual(await options(), [CUSTOM_URL, widget1.name, widget2.name]);
await toggle();
});
// Check that the widget is using the text color from the GristLight theme.
assert.equal(await getWidgetColor(), 'rgba(38, 38, 51, 1)');
it('should switch between widgets', async () => {
// Test custom URL.
await toggle();
await select(CUSTOM_URL);
assert.equal(await current(), CUSTOM_URL);
assert.equal(await getUrl(), '');
await setUrl('/200');
assert.equal(await content(), 'OK');
// Switch the theme to GristDark.
await gu.setGristTheme({appearance: 'dark', syncWithOS: false});
await driver.navigate().back();
await gu.waitForDocToLoad();
// Test first widget.
await toggle();
await select(widget1.name);
assert.equal(await current(), widget1.name);
assert.equal(await content(), widget1.name);
// Check that the span is using the text color from the GristDark theme.
assert.equal(await getWidgetColor(), 'rgba(239, 239, 239, 1)');
// Test second widget.
await toggle();
await select(widget2.name);
assert.equal(await current(), widget2.name);
assert.equal(await content(), widget2.name);
// Switch back to GristLight.
await gu.setGristTheme({appearance: 'light', syncWithOS: true});
await driver.navigate().back();
await gu.waitForDocToLoad();
// Go back to Custom URL.
await toggle();
await select(CUSTOM_URL);
assert.equal(await getUrl(), '');
assert.equal(await current(), CUSTOM_URL);
await setUrl('/200');
assert.equal(await content(), 'OK');
// Check that the widget is back to using the GristLight text color.
assert.equal(await getWidgetColor(), 'rgba(38, 38, 51, 1)');
// Clear url and test if message page is shown.
await setUrl('');
assert.equal(await current(), CUSTOM_URL);
assert.isTrue((await content()).startsWith('Custom widget')); // start page
// Re-enable widget repository.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
});
await recreatePanel();
assert.equal(await current(), CUSTOM_URL);
await gu.undo(7);
});
it("should support widgets that don't use the plugin api", async () => {
widgets = [widgetNoPluginApi];
await useManifest(manifestEndpoint);
await recreatePanel();
await toggle();
await select(widgetNoPluginApi.name);
assert.equal(await current(), widgetNoPluginApi.name);
it('should support theme variables', async () => {
widgets = [widgetWithTheme];
await useManifest(manifestEndpoint);
await recreatePanel();
await toggle();
await select(widgetWithTheme.name);
assert.equal(await current(), widgetWithTheme.name);
assert.equal(await content(), widgetWithTheme.name);
const getWidgetColor = async () => {
const iframe = driver.find('iframe');
await driver.switchTo().frame(iframe);
const color = await driver.find('span').getCssValue('color');
await driver.switchTo().defaultContent();
return color;
};
// Check that the widget loaded and its iframe is visible.
assert.equal(await content(), widgetNoPluginApi.name);
assert.isTrue(await driver.find('iframe').isDisplayed());
// Check that the widget is using the text color from the GristLight theme.
assert.equal(await getWidgetColor(), 'rgba(38, 38, 51, 1)');
// Revert to original configuration.
widgets = [widget1, widget2];
await useManifest(manifestEndpoint);
await recreatePanel();
});
// Switch the theme to GristDark.
await gu.setGristTheme({appearance: 'dark', syncWithOS: false});
await driver.navigate().back();
await gu.waitForDocToLoad();
// Check that the span is using the text color from the GristDark theme.
assert.equal(await getWidgetColor(), 'rgba(239, 239, 239, 1)');
it('should show error message for invalid widget url list', async () => {
const testError = async (url: string, error: string) => {
// Switch section to rebuild the creator panel.
await useManifest(url);
// Switch back to GristLight.
await gu.setGristTheme({appearance: 'light', syncWithOS: true});
await driver.navigate().back();
await gu.waitForDocToLoad();
// Check that the widget is back to using the GristLight text color.
assert.equal(await getWidgetColor(), 'rgba(38, 38, 51, 1)');
// Re-enable widget repository.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
});
it("should support widgets that don't use the plugin api", async () => {
widgets = [widgetNoPluginApi];
await useManifest(manifestEndpoint);
await recreatePanel();
assert.include(await getErrorMessage(), error);
await gu.wipeToasts();
// List should contain only a Custom URL.
await toggle();
assert.deepEqual(await options(), [CUSTOM_URL]);
await toggle();
};
await select(widgetNoPluginApi.name);
assert.equal(await current(), widgetNoPluginApi.name);
await testError('/404', "Remote widget list not found");
await testError('/500', "Remote server returned an error");
await testError('/401', "Remote server returned an error");
await testError('/403', "Remote server returned an error");
// Invalid content in a response.
await testError('/200', "Error reading widget list");
// Check that the widget loaded and its iframe is visible.
assert.equal(await content(), widgetNoPluginApi.name);
assert.isTrue(await driver.find('iframe').isDisplayed());
// Reset to valid manifest.
await useManifest(manifestEndpoint);
await recreatePanel();
});
// Revert to original configuration.
widgets = [widget1, widget2];
await useManifest(manifestEndpoint);
await recreatePanel();
});
it('should show widget when it was removed from list', async () => {
// Select widget1 and then remove it from the list.
await toggle();
await select(widget1.name);
widgets = [widget2];
// Invalidate cache.
await useManifest(manifestEndpoint);
// Toggle sections to reset creator panel and fetch list of available widgets.
await recreatePanel();
// But still should be selected with a correct url.
assert.equal(await current(), widget1.name);
assert.equal(await content(), widget1.name);
await gu.undo(1);
});
it('should show error message for invalid widget url list', async () => {
const testError = async (url: string, error: string) => {
// Switch section to rebuild the creator panel.
await useManifest(url);
await recreatePanel();
assert.include(await getErrorMessage(), error);
await gu.wipeToasts();
// List should contain only a Custom URL.
await toggle();
assert.deepEqual(await options(), [CUSTOM_URL]);
await toggle();
};
it('should switch access level to none on new widget', async () => {
await toggle();
await select(widget1.name);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await toggle();
await select(widget2.name);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await toggle();
await select(CUSTOM_URL);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await toggle();
await select(widget2.name);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await gu.undo(8);
});
await testError('/404', "Remote widget list not found");
await testError('/500', "Remote server returned an error");
await testError('/401', "Remote server returned an error");
await testError('/403', "Remote server returned an error");
// Invalid content in a response.
await testError('/200', "Error reading widget list");
it('should prompt for access change', async () => {
widgets = [widget1, widget2, widgetFull, widgetNone, widgetRead];
await useManifest(manifestEndpoint);
await recreatePanel();
// Reset to valid manifest.
await useManifest(manifestEndpoint);
await recreatePanel();
});
const test = async (w: ICustomWidget) => {
// Select widget without desired access level
it('should show widget when it was removed from list', async () => {
// Select widget1 and then remove it from the list.
await toggle();
await select(widget1.name);
widgets = [widget2];
// Invalidate cache.
await useManifest(manifestEndpoint);
// Toggle sections to reset creator panel and fetch list of available widgets.
await recreatePanel();
// But still should be selected with a correct url.
assert.equal(await current(), widget1.name);
assert.equal(await content(), widget1.name);
await gu.undo(1);
});
it('should switch access level to none on new widget', async () => {
widgets = [widget1, widget2];
await recreatePanel();
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
// Select one with desired access level
await toggle();
await select(w.name);
// Access level should be still none (test by content which will display access level from query string)
assert.equal(await content(), AccessLevel.none);
await select(widget2.name);
assert.equal(await access(), AccessLevel.none);
assert.isTrue(await hasPrompt());
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
// Accept, and test if prompt is hidden, and level stays
await accept();
assert.isFalse(await hasPrompt());
assert.equal(await access(), w.accessLevel);
await toggle();
await select(CUSTOM_URL);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
// Do the same, but this time reject
await toggle();
await select(widget2.name);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await gu.undo(8);
});
it('should prompt for access change', async () => {
widgets = [widget1, widget2, widgetFull, widgetNone, widgetRead];
await useManifest(manifestEndpoint);
await recreatePanel();
const test = async (w: ICustomWidget) => {
// Select widget without desired access level
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
// Select one with desired access level
await toggle();
await select(w.name);
// Access level should be still none (test by content which will display access level from query string)
assert.equal(await content(), AccessLevel.none);
assert.equal(await access(), AccessLevel.none);
assert.isTrue(await hasPrompt());
// Accept, and test if prompt is hidden, and level stays
await accept();
assert.isFalse(await hasPrompt());
assert.equal(await access(), w.accessLevel);
// Do the same, but this time reject
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
await toggle();
await select(w.name);
assert.isTrue(await hasPrompt());
assert.equal(await content(), AccessLevel.none);
await reject();
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
};
await test(widgetFull);
await test(widgetRead);
});
it('should auto accept none access level', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
// Switch to one with none access level
await toggle();
await select(w.name);
assert.isTrue(await hasPrompt());
assert.equal(await content(), AccessLevel.none);
await reject();
await select(widgetNone.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
};
await test(widgetFull);
await test(widgetRead);
});
});
it('should auto accept none access level', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
// Switch to one with none access level
await toggle();
await select(widgetNone.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
});
it('should show prompt when user switches sections', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
it('should show prompt when user switches sections', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
// Switch to one with full access level
await toggle();
await select(widgetFull.name);
assert.isTrue(await hasPrompt());
// Switch section, and test if prompt is hidden
await recreatePanel();
assert.isTrue(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
});
// Switch to one with full access level
await toggle();
await select(widgetFull.name);
assert.isTrue(await hasPrompt());
it('should hide prompt when user switches widget', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
// Switch to one with full access level
await toggle();
await select(widgetFull.name);
assert.isTrue(await hasPrompt());
// Switch to another level.
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
});
// Switch section, and test if prompt is hidden
await recreatePanel();
assert.isTrue(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
});
it('should hide prompt when manually changes access level', async () => {
// Select widget with no access level
const selectNone = async () => {
it('should hide prompt when user switches widget', async () => {
// Select widget without access level
await toggle();
await select(widgetNone.name);
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
};
// Selects widget with full access level
const selectFull = async () => {
// Switch to one with full access level
await toggle();
await select(widgetFull.name);
assert.isTrue(await hasPrompt());
assert.equal(await content(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
};
await selectNone();
await selectFull();
// Switch to another level.
await toggle();
await select(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
});
// Select the same level.
await access(AccessLevel.full);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.full);
assert.equal(await content(), AccessLevel.full);
it('should hide prompt when manually changes access level', async () => {
// Select widget with no access level
const selectNone = async () => {
await toggle();
await select(widgetNone.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
};
await selectNone();
await selectFull();
// Selects widget with full access level
const selectFull = async () => {
await toggle();
await select(widgetFull.name);
assert.isTrue(await hasPrompt());
assert.equal(await content(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
};
// Select the normal level, prompt should be still there, as widget needs a higher permission.
await access(AccessLevel.read_table);
assert.isTrue(await hasPrompt());
assert.equal(await access(), AccessLevel.read_table);
assert.equal(await content(), AccessLevel.read_table);
await selectNone();
await selectFull();
await selectNone();
await selectFull();
// Select the same level.
await access(AccessLevel.full);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.full);
assert.equal(await content(), AccessLevel.full);
// Select the none level.
await access(AccessLevel.none);
assert.isTrue(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
});
await selectNone();
await selectFull();
it("should support grist.selectedTable", async () => {
// Open a custom widget with full access.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-config-widget').click();
await gu.waitForServer();
await toggle();
await select(widget1.name);
await access(AccessLevel.full);
// Check an upsert works.
await execute(async (table) => {
await table.upsert({
require: {A: 'hello'},
fields: {A: 'goodbye'}
});
});
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 1, col: 0}).getText(), 'goodbye');
});
// Select the normal level, prompt should be still there, as widget needs a higher permission.
await access(AccessLevel.read_table);
assert.isTrue(await hasPrompt());
assert.equal(await access(), AccessLevel.read_table);
assert.equal(await content(), AccessLevel.read_table);
// Check an update works.
await execute(async table => {
return table.update({
id: 2,
fields: {A: 'farewell'}
});
await selectNone();
await selectFull();
// Select the none level.
await access(AccessLevel.none);
assert.isTrue(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
});
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 2, col: 0}).getText(), 'farewell');
it('should offer only custom url when disabled', async () => {
await toggle();
await select(CUSTOM_URL);
await driver.executeScript('window.gristConfig.enableWidgetRepository = false;');
await recreatePanel();
assert.isTrue(await driver.find('.test-config-widget-url').isDisplayed());
assert.isFalse(await driver.find('.test-config-widget-select').isPresent());
});
});
// Check options are passed along.
await execute(async table => {
return table.upsert({
require: {},
fields: {A: 'goodbyes'}
}, {onMany: 'all', allowEmptyRequire: true});
describe('gristApiSupport', async ()=>{
beforeEach(async function () {
// Override gristConfig to enable widget list.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
// We need to be sure that widget configuration panel is open all the time.
await gu.toggleSidePanel('right', 'open');
await recreatePanel();
await driver.findWait('.test-right-tab-pagewidget', 100).click();
});
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 1, col: 0}).getText(), 'goodbyes');
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 2, col: 0}).getText(), 'goodbyes');
it('should set language in widget url', async () => {
function languageMenu() {
return gu.currentDriver().find('.test-account-page-language .test-select-open');
}
async function language() {
return await gu.doInIframe(await getCustomWidgetFrame(), async ()=>{
const urlText = await driver.executeScript<string>('return document.location.href');
const url = new URL(urlText);
return url.searchParams.get('language');
});
}
async function switchLanguage(lang: string) {
await gu.openProfileSettingsPage();
await gu.waitForServer();
await languageMenu().click();
await driver.findContentWait('.test-select-menu li', lang, 100).click();
await gu.waitForServer();
await driver.navigate().back();
await gu.waitForServer();
}
widgets = [widget1];
await useManifest(manifestEndpoint);
await gu.openWidgetPanel();
await toggle();
await select(widget1.name);
//Switch language to Polish
await switchLanguage('Polski');
//Check if widgets have "pl" in url
assert.equal(await language(), 'pl');
//Switch back to English
await switchLanguage('English');
//Check if widgets have "en" in url
assert.equal(await language(), 'en');
});
// Check a create works.
const {id} = await execute(async table => {
return table.create({
fields: {A: 'partA', B: 'partB'}
it("should support grist.selectedTable", async () => {
// Open a custom widget with full access.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-config-widget').click();
await gu.waitForServer();
await toggle();
await select(widget1.name);
await access(AccessLevel.full);
// Check an upsert works.
await execute(async (table) => {
await table.upsert({
require: {A: 'hello'},
fields: {A: 'goodbye'}
});
});
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 1, col: 0}).getText(), 'goodbye');
});
}) as {id: number};
assert.equal(id, 5);
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id, col: 0}).getText(), 'partA');
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id, col: 1}).getText(), 'partB');
});
// Check a destroy works.
let result = await execute(async table => {
await table.destroy(1);
});
assert.isUndefined(result);
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id - 1, col: 0}).getText(), 'partA');
});
result = await execute(async table => {
await table.destroy([2]);
});
assert.isUndefined(result);
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id - 2, col: 0}).getText(), 'partA');
});
// Check an update works.
await execute(async table => {
return table.update({
id: 2,
fields: {A: 'farewell'}
});
});
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 2, col: 0}).getText(), 'farewell');
});
// Check errors are friendly.
const errMessage = await execute(async table => {
await table.create({fields: {ziggy: 1}});
});
assert.equal(errMessage, 'Invalid column "ziggy"');
});
// Check options are passed along.
await execute(async table => {
return table.upsert({
require: {},
fields: {A: 'goodbyes'}
}, {onMany: 'all', allowEmptyRequire: true});
});
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 1, col: 0}).getText(), 'goodbyes');
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 2, col: 0}).getText(), 'goodbyes');
});
// Check a create works.
const {id} = await execute(async table => {
return table.create({
fields: {A: 'partA', B: 'partB'}
});
}) as {id: number};
assert.equal(id, 5);
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id, col: 0}).getText(), 'partA');
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id, col: 1}).getText(), 'partB');
});
// Check a destroy works.
let result = await execute(async table => {
await table.destroy(1);
});
assert.isUndefined(result);
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id - 1, col: 0}).getText(), 'partA');
});
result = await execute(async table => {
await table.destroy([2]);
});
assert.isUndefined(result);
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: id - 2, col: 0}).getText(), 'partA');
});
it("should support grist.getTable", async () => {
// Check an update on an existing table works.
await execute(async table => {
return table.update({
id: 3,
fields: {A: 'back again'}
// Check errors are friendly.
const errMessage = await execute(async table => {
await table.create({fields: {ziggy: 1}});
});
}, (grist) => grist.getTable('Table1'));
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 1, col: 0}).getText(), 'back again');
assert.equal(errMessage, 'Invalid column "ziggy"');
});
// Check an update on a nonexistent table fails.
assert.match(String(await execute(async table => {
return table.update({
id: 3,
fields: {A: 'back again'}
it("should support grist.getTable", async () => {
// Check an update on an existing table works.
await execute(async table => {
return table.update({
id: 3,
fields: {A: 'back again'}
});
}, (grist) => grist.getTable('Table1'));
await gu.waitToPass(async () => {
assert.equal(await gu.getCell({section: 'TABLE1', rowNum: 1, col: 0}).getText(), 'back again');
});
}, (grist) => grist.getTable('Table2'))), /Table not found/);
});
it("should support grist.getAccessTokens", async () => {
const iframe = await driver.find('iframe');
await driver.switchTo().frame(iframe);
try {
const tokenResult: AccessTokenResult = await driver.executeAsyncScript(
(done: any) => (window as any).grist.getAccessToken().then(done)
);
assert.sameMembers(Object.keys(tokenResult), ['ttlMsecs', 'token', 'baseUrl']);
const result = await fetch(tokenResult.baseUrl + `/tables/Table1/records?auth=${tokenResult.token}`);
assert.sameMembers(Object.keys(await result.json()), ['records']);
} finally {
await driver.switchTo().defaultContent();
}
});
// Check an update on a nonexistent table fails.
assert.match(String(await execute(async table => {
return table.update({
id: 3,
fields: {A: 'back again'}
});
}, (grist) => grist.getTable('Table2'))), /Table not found/);
});
it('should offer only custom url when disabled', async () => {
await toggle();
await select(CUSTOM_URL);
await driver.executeScript('window.gristConfig.enableWidgetRepository = false;');
await recreatePanel();
assert.isTrue(await driver.find('.test-config-widget-url').isDisplayed());
assert.isFalse(await driver.find('.test-config-widget-select').isPresent());
it("should support grist.getAccessTokens", async () => {
return await gu.doInIframe(await getCustomWidgetFrame(), async ()=>{
const tokenResult: AccessTokenResult = await driver.executeAsyncScript(
(done: any) => (window as any).grist.getAccessToken().then(done)
);
assert.sameMembers(Object.keys(tokenResult), ['ttlMsecs', 'token', 'baseUrl']);
const result = await fetch(tokenResult.baseUrl + `/tables/Table1/records?auth=${tokenResult.token}`);
assert.sameMembers(Object.keys(await result.json()), ['records']);
});
});
});
});

@ -859,6 +859,19 @@ export async function importUrlDialog(url: string): Promise<void> {
await driver.switchTo().defaultContent();
}
/**
* Executed passed function in the context of given iframe, and then switching back to original context
*
*/
export async function doInIframe<T>(iframe: WebElement, func: () => Promise<T>) {
try {
await driver.switchTo().frame(iframe);
return await func();
} finally {
await driver.switchTo().defaultContent();
}
}
/**
* Starts or resets the collections of UserActions. This should be followed some time later by
* a call to userActionsVerify() to check which UserActions were sent to the server. If the

Loading…
Cancel
Save