(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 {SortedRowSet} from 'app/client/models/rowset';
import {closeRegisteredMenu} from 'app/client/ui2018/menus'; import {closeRegisteredMenu} from 'app/client/ui2018/menus';
import {AccessLevel} from 'app/common/CustomWidget'; import {AccessLevel} from 'app/common/CustomWidget';
import {defaultLocale} from 'app/common/gutil';
import {PluginInstance} from 'app/common/PluginInstance'; import {PluginInstance} from 'app/common/PluginInstance';
import {getGristConfig} from 'app/common/urlUtils'; import {getGristConfig} from 'app/common/urlUtils';
import {Events as BackboneEvents} from 'backbone'; import {Events as BackboneEvents} from 'backbone';
@ -213,9 +214,17 @@ export class CustomView extends Disposable {
showAfterReady?: boolean, showAfterReady?: boolean,
}) { }) {
const {baseUrl, access, showAfterReady} = options; const {baseUrl, access, showAfterReady} = options;
const documentSettings = this.gristDoc.docData.docSettings();
return grains.create(WidgetFrame, { return grains.create(WidgetFrame, {
url: baseUrl || this.getEmptyWidgetPage(), url: baseUrl || this.getEmptyWidgetPage(),
access, 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(), readonly: this.gristDoc.isReadonly.get(),
showAfterReady, showAfterReady,
onSettingsInitialized: async () => { onSettingsInitialized: async () => {

@ -73,6 +73,10 @@ export interface WidgetFrameOptions {
* Optional handler to modify the iframe. * Optional handler to modify the iframe.
*/ */
onElem?: (iframe: HTMLIFrameElement) => void; 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); const urlObj = new URL(url);
urlObj.searchParams.append('access', this._options.access); urlObj.searchParams.append('access', this._options.access);
urlObj.searchParams.append('readonly', String(this._options.readonly)); 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; return urlObj.href;
}; };
const fullUrl = urlWithAccess(this._options.url); const fullUrl = urlWithAccess(this._options.url);

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

Loading…
Cancel
Save