(core) Add custom widget gallery

Summary:
Custom widgets are now shown in a gallery.

The gallery is automatically opened when a new custom widget is
added to a page.

Descriptions, authors, and update times are pulled from the widget
manifest.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D4309
This commit is contained in:
George Gevoian
2024-08-13 19:21:48 -04:00
parent a16d76d25d
commit e70c294e3d
32 changed files with 1672 additions and 785 deletions

View File

@@ -24,13 +24,6 @@ describe('AttachedCustomWidget', function () {
let widgetServerUrl = '';
// Holds widgets manifest content.
let widgets: ICustomWidget[] = [];
// Switches widget manifest url
async function useManifest(url: string) {
await server.testingHooks.setWidgetRepositoryUrl(url ? `${widgetServerUrl}${url}` : '');
await driver.executeAsyncScript(
(done: any) => (window as any).gristApp?.topAppModel.testReloadWidgets().then(done).catch(done) || done()
);
}
async function buildWidgetServer(){
// Create simple widget server that serves manifest.json file, some widgets and some error pages.
@@ -69,12 +62,11 @@ describe('AttachedCustomWidget', function () {
before(async function () {
await buildWidgetServer();
oldEnv = new EnvironmentSnapshot();
process.env.GRIST_WIDGET_LIST_URL = `${widgetServerUrl}${manifestEndpoint}`;
process.env.PERMITTED_CUSTOM_WIDGETS = "calendar";
await server.restart();
await useManifest(manifestEndpoint);
const session = await gu.session().login();
await session.tempDoc(cleanup, 'Hello.grist');
});
after(async function () {

View File

@@ -145,18 +145,6 @@ describe('BehavioralPrompts', function() {
await assertPromptTitle('Editing Card Layout');
});
it('should be shown after adding custom view as a new page', async function() {
await gu.addNewPage('Custom', 'Table1');
await assertPromptTitle('Custom Widgets');
await gu.undo();
});
it('should be shown after adding custom section', async function() {
await gu.addNewSection('Custom', 'Table1');
await assertPromptTitle('Custom Widgets');
await gu.undo();
});
describe('for the Add New button', function() {
it('should not be shown if site is empty', async function() {
session = await gu.session().user('user4').login({showTips: true});

View File

@@ -1,25 +1,12 @@
import {safeJsonParse} from 'app/common/gutil';
import * as chai from 'chai';
import {assert, driver, Key} from 'mocha-webdriver';
import {serveCustomViews, Serving, setAccess} from 'test/nbrowser/customUtil';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import { serveCustomViews, Serving, setAccess } from 'test/nbrowser/customUtil';
import * as chai from 'chai';
chai.config.truncateThreshold = 5000;
async function setCustomWidget() {
// if there is a select widget option
if (await driver.find('.test-config-widget-select').isPresent()) {
const selected = await driver.find('.test-config-widget-select .test-select-open').getText();
if (selected != "Custom URL") {
await driver.find('.test-config-widget-select .test-select-open').click();
await driver.findContent('.test-select-menu li', "Custom URL").click();
await gu.waitForServer();
}
}
}
describe('CustomView', function() {
this.timeout(20000);
gu.bigScreen();
@@ -49,9 +36,8 @@ describe('CustomView', function() {
await gu.addNewSection('Custom', 'Table1');
// Point to a widget that doesn't immediately call ready.
await gu.setCustomWidgetUrl(`${serving.url}/deferred-ready`, {openGallery: false});
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-config-widget-url').click();
await gu.sendKeys(`${serving.url}/deferred-ready`, Key.ENTER);
// We should have a single iframe.
assert.equal(await driver.findAll('iframe').then(f => f.length), 1);
@@ -108,10 +94,8 @@ describe('CustomView', function() {
// Replace the widget with a custom widget that just reads out the data
// as JSON.
await driver.find('.test-config-widget').click();
await setCustomWidget();
await driver.find('.test-config-widget-url').click();
await driver.sendKeys(`${serving.url}/readout`, Key.ENTER);
await gu.setCustomWidgetUrl(`${serving.url}/readout`, {openGallery: false});
await gu.openWidgetPanel();
await setAccess(access);
await gu.waitForServer();
@@ -167,10 +151,8 @@ describe('CustomView', function() {
await gu.waitForServer();
// Choose the custom view that just reads out data as json
await driver.find('.test-config-widget').click();
await setCustomWidget();
await driver.find('.test-config-widget-url').click();
await driver.sendKeys(`${serving.url}/readout`, Key.ENTER);
await gu.setCustomWidgetUrl(`${serving.url}/readout`, {openGallery: false});
await gu.openWidgetPanel();
await setAccess(access);
await gu.waitForServer();
@@ -265,7 +247,7 @@ describe('CustomView', function() {
it('allows switching to custom section by clicking inside it', async function() {
await gu.getCell({section: 'FRIENDS', col: 0, rowNum: 1}).click();
assert.equal(await gu.getActiveSectionTitle(), 'FRIENDS');
assert.equal(await driver.find('.test-config-widget-url').isPresent(), false);
assert.equal(await driver.find('.test-config-widget-open-custom-widget-gallery').isPresent(), false);
const iframe = gu.getSection('Friends custom').find('iframe');
await driver.switchTo().frame(iframe);
@@ -274,24 +256,19 @@ describe('CustomView', function() {
// Check that the right section is active, and its settings visible in the side panel.
await driver.switchTo().defaultContent();
assert.equal(await gu.getActiveSectionTitle(), 'FRIENDS Custom');
assert.equal(await driver.find('.test-config-widget-url').isPresent(), true);
assert.equal(await driver.find('.test-config-widget-open-custom-widget-gallery').isPresent(), true);
// Switch back.
await gu.getCell({section: 'FRIENDS', col: 0, rowNum: 1}).click();
assert.equal(await gu.getActiveSectionTitle(), 'FRIENDS');
assert.equal(await driver.find('.test-config-widget-url').isPresent(), false);
assert.equal(await driver.find('.test-config-widget-open-custom-widget-gallery').isPresent(), false);
});
it('deals correctly with requests that require full access', async function() {
// Choose a custom widget that tries to replace all cells in all user tables with 'zap'.
await gu.getSection('Friends Custom').click();
await driver.find('.test-config-widget').click();
await setAccess("none");
await gu.waitForServer();
await gu.setValue(driver.find('.test-config-widget-url'), '');
await driver.find('.test-config-widget-url').click();
await driver.sendKeys(`${serving.url}/zap`, Key.ENTER);
await gu.setCustomWidgetUrl(`${serving.url}/zap`);
await gu.openWidgetPanel();
await setAccess(access);
await gu.waitForServer();
@@ -329,12 +306,10 @@ describe('CustomView', function() {
// The test doc already has a Custom View widget. It just needs to
// have a URL set.
await gu.getSection('TYPES custom').click();
await driver.find('.test-config-widget').click();
await setCustomWidget();
await gu.setCustomWidgetUrl(`${serving.url}/types`);
// If we needed to change widget to Custom URL, make sure access is read table.
await setAccess("read table");
await driver.find('.test-config-widget-url').click();
await driver.sendKeys(`${serving.url}/types`, Key.ENTER);
await gu.waitForServer();
const iframe = gu.getSection('TYPES custom').find('iframe');
await driver.switchTo().frame(iframe);
@@ -480,10 +455,8 @@ describe('CustomView', function() {
await gu.waitForServer();
// Select a custom widget that tries to replace all cells in all user tables with 'zap'.
await driver.find('.test-config-widget').click();
await setCustomWidget();
await driver.find('.test-config-widget-url').click();
await driver.sendKeys(`${serving.url}/zap`, Key.ENTER);
await gu.setCustomWidgetUrl(`${serving.url}/zap`, {openGallery: false});
await gu.openWidgetPanel();
await setAccess("full");
await gu.waitForServer();
@@ -537,10 +510,10 @@ describe('CustomView', function() {
const doc = await mainSession.tempDoc(cleanup, 'FetchSelectedOptions.grist', {load: false});
await mainSession.loadDoc(`/doc/${doc.id}`);
await gu.toggleSidePanel('right', 'open');
await gu.getSection('TABLE1 Custom').click();
await driver.find('.test-config-widget-url').click();
await gu.sendKeys(`${serving.url}/fetchSelectedOptions`, Key.ENTER);
await gu.setCustomWidgetUrl(`${serving.url}/fetchSelectedOptions`);
await gu.openWidgetPanel();
await setAccess("full");
await gu.waitForServer();
const expected = {
@@ -620,8 +593,10 @@ describe('CustomView', function() {
}
await inFrame(async () => {
const parsed = await getData(12);
assert.deepEqual(parsed, expected);
await gu.waitToPass(async () => {
const parsed = await getData(12);
assert.deepEqual(parsed, expected);
}, 1000);
});
// Change the access level away from 'full'.

View File

@@ -20,20 +20,46 @@ const widgetEndpoint = '/widget';
const CUSTOM_URL = 'Custom URL';
// Create some widgets:
const widget1: ICustomWidget = {widgetId: '1', name: 'W1', url: widgetEndpoint + '?name=W1'};
const widget2: ICustomWidget = {widgetId: '2', name: 'W2', url: widgetEndpoint + '?name=W2'};
const widget1: ICustomWidget = {
widgetId: '1',
name: 'W1',
url: widgetEndpoint + '?name=W1',
description: 'Widget 1 description',
authors: [
{
name: 'Developer 1',
},
{
name: 'Developer 2',
},
],
isGristLabsMaintained: true,
lastUpdatedAt: '2024-07-30T00:13:31-04:00',
};
const widget2: ICustomWidget = {
widgetId: '2',
name: 'W2',
url: widgetEndpoint + '?name=W2',
};
const widgetWithTheme: ICustomWidget = {
widgetId: '3',
name: 'WithTheme',
url: widgetEndpoint + '?name=WithTheme',
isGristLabsMaintained: true,
};
const widgetNoPluginApi: ICustomWidget = {
widgetId: '4',
name: 'NoPluginApi',
url: widgetEndpoint + '?name=NoPluginApi',
isGristLabsMaintained: true,
};
const fromAccess = (level: AccessLevel) =>
({widgetId: level, name: level, url: widgetEndpoint, accessLevel: level}) as ICustomWidget;
const fromAccess = (level: AccessLevel): ICustomWidget => ({
widgetId: level,
name: level,
url: widgetEndpoint,
accessLevel: level,
isGristLabsMaintained: true,
});
const widgetNone = fromAccess(AccessLevel.none);
const widgetRead = fromAccess(AccessLevel.read_table);
const widgetFull = fromAccess(AccessLevel.full);
@@ -51,23 +77,27 @@ describe('CustomWidgets', function () {
gu.bigScreen();
const cleanup = setupTestSuite();
let oldEnv: EnvironmentSnapshot;
// Holds url for sample widget server.
let widgetServerUrl = '';
// Switches widget manifest url
async function useManifest(url: string) {
await server.testingHooks.setWidgetRepositoryUrl(url ? `${widgetServerUrl}${url}` : '');
}
async function reloadWidgets() {
await driver.executeAsyncScript(
(done: any) => (window as any).gristApp?.topAppModel.testReloadWidgets().then(done).catch(done) || done()
);
}
before(async function () {
if (server.isExternalServer()) {
this.skip();
}
// Create simple widget server that serves manifest.json file, some widgets and some error pages.
const widgetServer = await serveSomething(app => {
app.get('/404', (_, res) => res.sendStatus(404).end()); // not found
@@ -105,32 +135,31 @@ describe('CustomWidgets', function () {
cleanup.addAfterAll(widgetServer.shutdown);
widgetServerUrl = widgetServer.url;
// Start with valid endpoint and 2 widgets.
oldEnv = new EnvironmentSnapshot();
process.env.GRIST_WIDGET_LIST_URL = `${widgetServerUrl}${manifestEndpoint}`;
await server.restart();
// Start with 2 widgets.
widgets = [widget1, widget2];
await useManifest(manifestEndpoint);
const session = await gu.session().login();
await session.tempDoc(cleanup, 'Hello.grist');
// Add custom section.
await gu.addNewSection(/Custom/, /Table1/, {selectBy: /TABLE1/});
// Add custom section.
await gu.addNewSection(/Custom/, /Table1/, {customWidget: /Custom URL/, selectBy: /TABLE1/});
});
after(async function() {
await server.testingHooks.setWidgetRepositoryUrl('');
oldEnv.restore();
await server.restart();
});
// Open or close widget menu.
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).
const options = () => driver.findAll('.test-select-menu li', e => e.getText());
// Select widget from the menu.
const select = async (text: string | RegExp) => {
await driver.findContent('.test-select-menu li', text).click();
await gu.waitForServer();
};
afterEach(() => gu.checkForErrors());
// Get available widgets from widget gallery (must be first opened).
const galleryWidgets = () => driver.findAll('.test-custom-widget-gallery-widget-name', e => e.getText());
// Get rendered content from custom section.
const content = async () => {
return gu.doInIframe(await getCustomWidgetFrame(), async ()=>{
@@ -169,19 +198,6 @@ describe('CustomWidgets', function () {
return result === "__undefined__" ? undefined : result;
});
}
// Replace url for the Custom URL widget.
const setUrl = async (url: string) => {
await driver.find('.test-config-widget-url').click();
// First clear textbox.
await gu.sendKeys(await gu.selectAllKey(), Key.DELETE);
if (url) {
await gu.sendKeys(`${widgetServerUrl}${url}`, Key.ENTER);
} else {
await gu.sendKeys(Key.ENTER);
}
};
// Get an URL from the URL textbox.
const getUrl = () => driver.find('.test-config-widget-url').value();
// Get first error message from error toasts.
const getErrorMessage = async () => (await gu.getToasts())[0];
// Changes active section to recreate creator panel.
@@ -215,8 +231,6 @@ describe('CustomWidgets', function () {
const reject = () => driver.find(".test-config-widget-access-reject").click();
async function enableWidgetsAndShowPanel() {
// 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();
@@ -226,68 +240,65 @@ describe('CustomWidgets', function () {
describe('RightWidgetMenu', () => {
beforeEach(enableWidgetsAndShowPanel);
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.
afterEach(() => gu.checkForErrors());
// Selectbox should have select label.
assert.equal(await current(), CUSTOM_URL);
// 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 button to open gallery', async () => {
const button = await driver.find('.test-config-widget-open-custom-widget-gallery');
assert.equal(await button.getText(), 'Custom URL');
await button.click();
assert.isTrue(await driver.find('.test-custom-widget-gallery-container').isDisplayed());
await gu.sendKeys(Key.ESCAPE, Key.ESCAPE);
assert.isFalse(await driver.find('.test-custom-widget-gallery-container').isPresent());
});
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');
// Test Custom URL.
assert.equal(await gu.getCustomWidgetName(), CUSTOM_URL);
assert.isTrue((await content()).startsWith('Custom widget'));
await gu.setCustomWidgetUrl(`${widgetServerUrl}/200`);
assert.equal(await gu.getCustomWidgetName(), CUSTOM_URL);
assert.equal(await content(), 'OK');
// Test first widget.
await toggle();
await select(widget1.name);
assert.equal(await current(), widget1.name);
await gu.setCustomWidget(widget1.name);
assert.equal(await gu.getCustomWidgetName(), widget1.name);
assert.equal(await gu.getCustomWidgetInfo('description'), widget1.description);
assert.equal(await gu.getCustomWidgetInfo('developer'), widget1.authors?.[0].name);
assert.equal(await gu.getCustomWidgetInfo('last-updated'), 'July 30, 2024');
assert.equal(await content(), widget1.name);
// Test second widget.
await toggle();
await select(widget2.name);
assert.equal(await current(), widget2.name);
await gu.setCustomWidget(widget2.name);
assert.equal(await gu.getCustomWidgetName(), widget2.name);
assert.equal(await gu.getCustomWidgetInfo('description'), '');
assert.equal(await gu.getCustomWidgetInfo('developer'), '');
assert.equal(await gu.getCustomWidgetInfo('last-updated'), '');
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');
await gu.setCustomWidget(CUSTOM_URL);
assert.equal(await gu.getCustomWidgetName(), CUSTOM_URL);
assert.isTrue((await content()).startsWith('Custom widget'));
await gu.setCustomWidgetUrl(`${widgetServerUrl}/200`);
assert.equal(await gu.getCustomWidgetName(), CUSTOM_URL);
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 gu.setCustomWidgetUrl('');
assert.equal(await gu.getCustomWidgetName(), CUSTOM_URL);
assert.isTrue((await content()).startsWith('Custom widget'));
await recreatePanel();
assert.equal(await current(), CUSTOM_URL);
await gu.undo(7);
assert.equal(await gu.getCustomWidgetName(), CUSTOM_URL);
await gu.undo(6);
});
it('should support theme variables', async () => {
widgets = [widgetWithTheme];
await useManifest(manifestEndpoint);
await reloadWidgets();
await recreatePanel();
await toggle();
await select(widgetWithTheme.name);
assert.equal(await current(), widgetWithTheme.name);
await gu.setCustomWidget(widgetWithTheme.name);
assert.equal(await gu.getCustomWidgetName(), widgetWithTheme.name);
assert.equal(await content(), widgetWithTheme.name);
const getWidgetColor = async () => {
@@ -316,18 +327,14 @@ describe('CustomWidgets', function () {
// 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 reloadWidgets();
await recreatePanel();
await toggle();
await select(widgetNoPluginApi.name);
assert.equal(await current(), widgetNoPluginApi.name);
await gu.setCustomWidget(widgetNoPluginApi.name);
assert.equal(await gu.getCustomWidgetName(), widgetNoPluginApi.name);
// Check that the widget loaded and its iframe is visible.
assert.equal(await content(), widgetNoPluginApi.name);
@@ -335,7 +342,7 @@ describe('CustomWidgets', function () {
// Revert to original configuration.
widgets = [widget1, widget2];
await useManifest(manifestEndpoint);
await reloadWidgets();
await recreatePanel();
});
@@ -343,13 +350,15 @@ describe('CustomWidgets', function () {
const testError = async (url: string, error: string) => {
// Switch section to rebuild the creator panel.
await useManifest(url);
await reloadWidgets();
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();
// Gallery should only contain the Custom URL widget.
await gu.openCustomWidgetGallery();
assert.deepEqual(await galleryWidgets(), [CUSTOM_URL]);
await gu.wipeToasts();
await gu.sendKeys(Key.ESCAPE);
};
await testError('/404', "Remote widget list not found");
@@ -361,6 +370,7 @@ describe('CustomWidgets', function () {
// Reset to valid manifest.
await useManifest(manifestEndpoint);
await reloadWidgets();
await recreatePanel();
});
@@ -371,15 +381,14 @@ describe('CustomWidgets', function () {
*/
it.skip('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);
await gu.setCustomWidget(widget1.name);
widgets = [widget2];
// Invalidate cache.
await useManifest(manifestEndpoint);
await reloadWidgets();
// 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 gu.getCustomWidgetName(), widget1.name);
assert.equal(await content(), widget1.name);
await gu.undo(1);
});
@@ -387,26 +396,22 @@ describe('CustomWidgets', function () {
it('should switch access level to none on new widget', async () => {
widgets = [widget1, widget2];
await recreatePanel();
await toggle();
await select(widget1.name);
await gu.setCustomWidget(widget1.name);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await toggle();
await select(widget2.name);
await gu.setCustomWidget(widget2.name);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await toggle();
await select(CUSTOM_URL);
await gu.setCustomWidget(CUSTOM_URL);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
await toggle();
await select(widget2.name);
await gu.setCustomWidget(widget2.name);
assert.equal(await access(), AccessLevel.none);
await access(AccessLevel.full);
assert.equal(await access(), AccessLevel.full);
@@ -416,19 +421,18 @@ describe('CustomWidgets', function () {
it('should prompt for access change', async () => {
widgets = [widget1, widget2, widgetFull, widgetNone, widgetRead];
await useManifest(manifestEndpoint);
await reloadWidgets();
await recreatePanel();
const test = async (w: ICustomWidget) => {
// Select widget without desired access level
await toggle();
await select(widget1.name);
await gu.setCustomWidget(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
// Select one with desired access level
await toggle();
await select(w.name);
await gu.setCustomWidget(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);
@@ -440,13 +444,11 @@ describe('CustomWidgets', function () {
assert.equal(await access(), w.accessLevel);
// Do the same, but this time reject
await toggle();
await select(widget1.name);
await gu.setCustomWidget(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
await toggle();
await select(w.name);
await gu.setCustomWidget(w.name);
assert.isTrue(await hasPrompt());
assert.equal(await content(), AccessLevel.none);
@@ -462,14 +464,12 @@ describe('CustomWidgets', function () {
it('should auto accept none access level', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
await gu.setCustomWidget(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);
await gu.setCustomWidget(widgetNone.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
@@ -477,14 +477,12 @@ describe('CustomWidgets', function () {
it('should show prompt when user switches sections', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
await gu.setCustomWidget(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);
await gu.setCustomWidget(widgetFull.name);
assert.isTrue(await hasPrompt());
// Switch section, and test if prompt is hidden
@@ -496,19 +494,16 @@ describe('CustomWidgets', function () {
it('should hide prompt when user switches widget', async () => {
// Select widget without access level
await toggle();
await select(widget1.name);
await gu.setCustomWidget(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);
await gu.setCustomWidget(widgetFull.name);
assert.isTrue(await hasPrompt());
// Switch to another level.
await toggle();
await select(widget1.name);
await gu.setCustomWidget(widget1.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
});
@@ -516,8 +511,7 @@ describe('CustomWidgets', function () {
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);
await gu.setCustomWidget(widgetNone.name);
assert.isFalse(await hasPrompt());
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
@@ -525,8 +519,7 @@ describe('CustomWidgets', function () {
// Selects widget with full access level
const selectFull = async () => {
await toggle();
await select(widgetFull.name);
await gu.setCustomWidget(widgetFull.name);
assert.isTrue(await hasPrompt());
assert.equal(await content(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
@@ -559,26 +552,140 @@ describe('CustomWidgets', function () {
assert.equal(await access(), AccessLevel.none);
assert.equal(await content(), AccessLevel.none);
});
});
it('should offer only custom url when disabled', async () => {
await toggle();
await select(CUSTOM_URL);
describe('gallery', () => {
afterEach(() => gu.checkForErrors());
it('should show available widgets', async () => {
await gu.openCustomWidgetGallery();
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
['Custom URL', 'full', 'none', 'read table', 'W1', 'W2']
);
});
it('should show available metadata', async () => {
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget', (el) =>
el.matches('.test-custom-widget-gallery-widget-custom')),
[true, false, false, false, false, false]
);
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget', (el) =>
el.matches('.test-custom-widget-gallery-widget-grist')),
[false, true, true, true, true, false]
);
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget', (el) =>
el.matches('.test-custom-widget-gallery-widget-community')),
[false, false, false, false, false, true]
);
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-description', (el) => el.getText()),
[
'Add a widget from outside this gallery.',
'(Missing info)',
'(Missing info)',
'(Missing info)',
'Widget 1 description',
'(Missing info)',
]
);
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-developer', (el) => el.getText()),
[
'(Missing info)',
'(Missing info)',
]
);
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-last-updated', (el) => el.getText()),
[
'(Missing info)',
'(Missing info)',
'(Missing info)',
'July 30, 2024',
'(Missing info)',
]
);
});
it('should filter widgets on search', async () => {
await driver.find('.test-custom-widget-gallery-search').click();
await gu.sendKeys('Custom');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
['Custom URL']
);
}, 200);
await gu.sendKeys(await gu.selectAllKey(), Key.DELETE);
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
['Custom URL', 'full', 'none', 'read table', 'W1', 'W2']
);
}, 200);
await gu.sendKeys('W');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
['Custom URL', 'W1', 'W2']
);
}, 200);
await gu.sendKeys(await gu.selectAllKey(), Key.DELETE, 'tab');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
['read table']
);
}, 200);
await gu.sendKeys(await gu.selectAllKey(), Key.DELETE, 'Markdown');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
[]
);
}, 200);
await gu.sendKeys(await gu.selectAllKey(), Key.DELETE, 'Developer 1');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
['W1']
);
}, 200);
});
it('should only show Custom URL widget when repository is disabled', async () => {
await gu.sendKeys(Key.ESCAPE);
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());
await driver.executeAsyncScript(
(done: any) => (window as any).gristApp?.topAppModel.testReloadWidgets().then(done).catch(done) || done()
);
await gu.openCustomWidgetGallery();
assert.deepEqual(
await driver.findAll('.test-custom-widget-gallery-widget-name', (el) => el.getText()),
['Custom URL']
);
await gu.sendKeys(Key.ESCAPE);
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
await driver.executeAsyncScript(
(done: any) => (window as any).gristApp?.topAppModel.testReloadWidgets().then(done).catch(done) || done()
);
});
});
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();
});
afterEach(() => gu.checkForErrors());
it('should set language in widget url', async () => {
function languageMenu() {
return gu.currentDriver().find('.test-account-page-language .test-select-open');
@@ -602,10 +709,9 @@ describe('CustomWidgets', function () {
}
widgets = [widget1];
await useManifest(manifestEndpoint);
await reloadWidgets();
await gu.openWidgetPanel();
await toggle();
await select(widget1.name);
await gu.setCustomWidget(widget1.name);
//Switch language to Polish
await switchLanguage('Polski');
//Check if widgets have "pl" in url
@@ -621,8 +727,6 @@ describe('CustomWidgets', function () {
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.
@@ -735,6 +839,7 @@ describe('CustomWidgets', function () {
});
afterEach(async function() {
await gu.checkForErrors();
oldEnv.restore();
await server.restart();
await gu.reloadDoc();
@@ -745,10 +850,10 @@ describe('CustomWidgets', function () {
// Double-check that using one external widget, we see
// just that widget listed.
widgets = [widget1];
await useManifest(manifestEndpoint);
await reloadWidgets();
await enableWidgetsAndShowPanel();
await toggle();
assert.deepEqual(await options(), [
await gu.openCustomWidgetGallery();
assert.deepEqual(await galleryWidgets(), [
CUSTOM_URL, widget1.name,
]);
@@ -848,13 +953,13 @@ describe('CustomWidgets', function () {
await gu.reloadDoc();
// Continue using one external widget.
await useManifest(manifestEndpoint);
await reloadWidgets();
await enableWidgetsAndShowPanel();
// Check we see one external widget and two bundled ones.
await toggle();
assert.deepEqual(await options(), [
CUSTOM_URL, widget1.name, 'P1 (My Widgets)', 'P2 (My Widgets)',
await gu.openCustomWidgetGallery();
assert.deepEqual(await galleryWidgets(), [
CUSTOM_URL, 'P1 (My Widgets)', 'P2 (My Widgets)', widget1.name,
]);
// Prepare to check content of widgets.
@@ -867,24 +972,22 @@ describe('CustomWidgets', function () {
}
// Check built-in P1 works as expected.
await select(/P1/);
assert.equal(await current(), 'P1 (My Widgets)');
await gu.setCustomWidget(/P1/, {openGallery: false});
assert.equal(await gu.getCustomWidgetName(), 'P1 (My Widgets)');
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'P1');
});
// Check external W1 works as expected.
await toggle();
await select(/W1/);
assert.equal(await current(), 'W1');
await gu.setCustomWidget(/W1/);
assert.equal(await gu.getCustomWidgetName(), 'W1');
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'W1');
});
// Check build-in P2 works as expected.
await toggle();
await select(/P2/);
assert.equal(await current(), 'P2 (My Widgets)');
await gu.setCustomWidget(/P2/);
assert.equal(await gu.getCustomWidgetName(), 'P2 (My Widgets)');
await gu.waitToPass(async () => {
assert.equal(await getWidgetText(), 'P2');
});

View File

@@ -1,8 +1,9 @@
import {AccessLevel} from 'app/common/CustomWidget';
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';
import {AccessLevel} from 'app/common/CustomWidget';
import {EnvironmentSnapshot} from 'test/server/testUtils';
// Valid manifest url.
const manifestEndpoint = '/manifest.json';
@@ -16,7 +17,7 @@ const READ_WIDGET = 'Read';
const FULL_WIDGET = 'Full';
const COLUMN_WIDGET = 'COLUMN_WIDGET';
const REQUIRED_WIDGET = 'REQUIRED_WIDGET';
// Custom URL label in selectbox.
// Custom URL label.
const CUSTOM_URL = 'Custom URL';
// Holds url for sample widget server.
let widgetServerUrl = '';
@@ -27,14 +28,9 @@ function createConfigUrl(ready?: any) {
return ready ? `${widgetServerUrl}/config?ready=` + encodeURI(JSON.stringify(ready)) : `${widgetServerUrl}/config`;
}
// Open or close widget menu.
const click = (selector: string) => driver.find(`${selector}`).click();
const toggleDrop = (selector: string) => click(`${selector} .test-select-open`);
const toggleWidgetMenu = () => toggleDrop('.test-config-widget-select');
const getOptions = () => driver.findAll('.test-select-menu li', el => el.getText());
// Get current value from widget menu.
const currentWidget = () => driver.find('.test-config-widget-select .test-select-open').getText();
// Select widget from the menu.
const clickOption = async (text: string | RegExp) => {
await driver.findContent('.test-select-menu li', text).click();
await gu.waitForServer();
@@ -58,13 +54,11 @@ async function getListItems(col: string) {
.findAll(`.test-config-widget-map-list-for-${col} .test-config-widget-ref-select-label`, el => el.getText());
}
// When refreshing, we need to make sure widget repository is enabled once again.
async function refresh() {
await driver.navigate().refresh();
await gu.waitForDocToLoad();
// Switch section and enable config
await gu.selectSectionByTitle('Table');
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
await gu.selectSectionByTitle('Widget');
}
@@ -130,6 +124,7 @@ describe('CustomWidgetsConfig', function () {
let mainSession: gu.Session;
gu.bigScreen();
let oldEnv: EnvironmentSnapshot;
addToRepl('getOptions', getOptions);
@@ -137,6 +132,12 @@ describe('CustomWidgetsConfig', function () {
if (server.isExternalServer()) {
this.skip();
}
oldEnv = new EnvironmentSnapshot();
// Set to an unused URL so that the client reports that widgets are available.
process.env.GRIST_WIDGET_LIST_URL = 'unused';
await server.restart();
// Create simple widget server that serves manifest.json file, some widgets and some error pages.
const widgetServer = await serveSomething(app => {
app.get('/manifest.json', (_, res) => {
@@ -188,25 +189,23 @@ describe('CustomWidgetsConfig', function () {
mainSession = await gu.session().login();
const doc = await mainSession.tempDoc(cleanup, 'CustomWidget.grist');
docId = doc.id;
// Make sure widgets are enabled.
await driver.executeScript('window.gristConfig.enableWidgetRepository = true;');
await gu.toggleSidePanel('right', 'open');
await gu.selectSectionByTitle('Widget');
});
after(async function() {
await server.testingHooks.setWidgetRepositoryUrl('');
oldEnv.restore();
await server.restart();
});
beforeEach(async () => {
// Before each test, we will switch to Custom Url (to cleanup the widget)
// and then back to the Tester widget.
if ((await currentWidget()) !== CUSTOM_URL) {
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
if ((await gu.getCustomWidgetName()) !== CUSTOM_URL) {
await gu.setCustomWidget(CUSTOM_URL);
}
await toggleWidgetMenu();
await clickOption(TESTER_WIDGET);
await gu.setCustomWidget(TESTER_WIDGET);
await widget.waitForFrame();
});
@@ -218,8 +217,7 @@ describe('CustomWidgetsConfig', function () {
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.setCustomWidget(REQUIRED_WIDGET);
await gu.acceptAccessRequest();
// The widget iframe should be covered with a text explaining that the widget is not configured.
@@ -251,15 +249,11 @@ describe('CustomWidgetsConfig', function () {
});
it('should hide mappings when there is no good column', async () => {
if ((await currentWidget()) !== CUSTOM_URL) {
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
}
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [{name: 'M2', type: 'Date', optional: true}],
requiredAccess: 'read table',
})
}),
);
await widget.waitForFrame();
@@ -307,11 +301,7 @@ describe('CustomWidgetsConfig', function () {
it('should clear optional mapping', async () => {
const revert = await gu.begin();
if ((await currentWidget()) !== CUSTOM_URL) {
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
}
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [{name: 'M2', type: 'Date', optional: true}],
requiredAccess: 'read table',
@@ -356,9 +346,7 @@ describe('CustomWidgetsConfig', function () {
it('should render columns mapping', async () => {
const revert = await gu.begin();
assert.isTrue(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
await toggleWidgetMenu();
// Select widget that has single column configuration.
await clickOption(COLUMN_WIDGET);
await gu.setCustomWidget(COLUMN_WIDGET);
await widget.waitForFrame();
await gu.acceptAccessRequest();
await widget.waitForPendingRequests();
@@ -386,11 +374,9 @@ describe('CustomWidgetsConfig', function () {
it('should render multiple mappings', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
// This is not standard way of creating widgets. The widgets in this test is reading this parameter
// and is using it to invoke the ready method.
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: ['M1', {name: 'M2', optional: true}, {name: 'M3', title: 'T3'}, {name: 'M4', type: 'Text'}],
requiredAccess: 'read table',
@@ -448,8 +434,7 @@ describe('CustomWidgetsConfig', function () {
it('should clear mappings on widget switch', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
await clickOption(COLUMN_WIDGET);
await gu.setCustomWidget(COLUMN_WIDGET);
await widget.waitForFrame();
await gu.acceptAccessRequest();
await widget.waitForPendingRequests();
@@ -466,8 +451,7 @@ describe('CustomWidgetsConfig', function () {
await clickOption('A');
// Now change to a widget without columns
await toggleWidgetMenu();
await clickOption(NORMAL_WIDGET);
await gu.setCustomWidget(NORMAL_WIDGET);
// Picker should disappear and column mappings should be visible
assert.isTrue(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
@@ -481,8 +465,7 @@ describe('CustomWidgetsConfig', function () {
{id: 3, A: 'C'},
]);
// Now go back to the widget with mappings.
await toggleWidgetMenu();
await clickOption(COLUMN_WIDGET);
await gu.setCustomWidget(COLUMN_WIDGET);
await widget.waitForFrame();
await gu.acceptAccessRequest();
await widget.waitForPendingRequests();
@@ -494,9 +477,7 @@ describe('CustomWidgetsConfig', function () {
it('should render multiple options', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [
{name: 'M1', allowMultiple: true, optional: true},
@@ -578,9 +559,7 @@ describe('CustomWidgetsConfig', function () {
it('should support multiple types in mappings', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [
{name: 'M1', type: 'Date,DateTime', optional: true},
@@ -639,9 +618,7 @@ describe('CustomWidgetsConfig', function () {
it('should support strictType setting', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [
{name: 'Any', type: 'Any', strictType: true, optional: true},
@@ -683,9 +660,7 @@ describe('CustomWidgetsConfig', function () {
it('should react to widget options change', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [
{name: 'Choice', type: 'Choice', strictType: true, optional: true},
@@ -731,10 +706,8 @@ describe('CustomWidgetsConfig', function () {
it('should remove mapping when column is deleted', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
// Prepare mappings for single and multiple columns
await clickOption(CUSTOM_URL);
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [{name: 'M1', optional: true}, {name: 'M2', allowMultiple: true, optional: true}],
requiredAccess: 'read table',
@@ -820,10 +793,8 @@ describe('CustomWidgetsConfig', function () {
it('should remove mapping when column type is changed', async () => {
const revert = await gu.begin();
await toggleWidgetMenu();
// Prepare mappings for single and multiple columns
await clickOption(CUSTOM_URL);
await gu.setWidgetUrl(
await gu.setCustomWidgetUrl(
createConfigUrl({
columns: [
{name: 'M1', type: 'Text', optional: true},
@@ -900,10 +871,9 @@ describe('CustomWidgetsConfig', function () {
await gu.undo();
// Add Custom - no section option by default
await gu.addNewSection(/Custom/, /Table1/);
await gu.addNewSection(/Custom/, /Table1/, {customWidget: /Custom URL/});
assert.isFalse(await hasSectionOption());
await toggleWidgetMenu();
await clickOption(TESTER_WIDGET);
await gu.setCustomWidget(TESTER_WIDGET);
assert.isTrue(await hasSectionOption());
await gu.undo(2);
});
@@ -1058,30 +1028,19 @@ describe('CustomWidgetsConfig', function () {
it('should show options action button', async () => {
// Select widget without options
await toggleWidgetMenu();
await clickOption(NORMAL_WIDGET);
await gu.setCustomWidget(NORMAL_WIDGET);
assert.isFalse(await hasSectionOption());
// Select widget with options
await toggleWidgetMenu();
await clickOption(TESTER_WIDGET);
await gu.setCustomWidget(TESTER_WIDGET);
assert.isTrue(await hasSectionOption());
// Select widget without options
await toggleWidgetMenu();
await clickOption(NORMAL_WIDGET);
await gu.setCustomWidget(NORMAL_WIDGET);
assert.isFalse(await hasSectionOption());
});
it('should prompt user for correct access level', async () => {
// Select widget without request
await toggleWidgetMenu();
await clickOption(NORMAL_WIDGET);
await widget.waitForFrame();
assert.isFalse(await gu.hasAccessPrompt());
assert.equal(await gu.widgetAccess(), AccessLevel.none);
assert.equal(await widget.access(), AccessLevel.none);
// Select widget that requests read access.
await toggleWidgetMenu();
await clickOption(READ_WIDGET);
await gu.setCustomWidget(READ_WIDGET);
await widget.waitForFrame();
assert.isTrue(await gu.hasAccessPrompt());
assert.equal(await gu.widgetAccess(), AccessLevel.none);
@@ -1091,8 +1050,7 @@ describe('CustomWidgetsConfig', function () {
assert.equal(await gu.widgetAccess(), AccessLevel.read_table);
assert.equal(await widget.access(), AccessLevel.read_table);
// Select widget that requests full access.
await toggleWidgetMenu();
await clickOption(FULL_WIDGET);
await gu.setCustomWidget(FULL_WIDGET);
await widget.waitForFrame();
assert.isTrue(await gu.hasAccessPrompt());
assert.equal(await gu.widgetAccess(), AccessLevel.none);
@@ -1101,7 +1059,7 @@ describe('CustomWidgetsConfig', function () {
await widget.waitForPendingRequests();
assert.equal(await gu.widgetAccess(), AccessLevel.full);
assert.equal(await widget.access(), AccessLevel.full);
await gu.undo(5);
await gu.undo(4);
});
it('should pass readonly mode to custom widget', async () => {
@@ -1265,7 +1223,6 @@ const widget = {
* any existing widget state (even if the Custom URL was already selected).
*/
async resetWidget() {
await toggleWidgetMenu();
await clickOption(CUSTOM_URL);
await gu.setCustomWidget(CUSTOM_URL);
}
};

View File

@@ -219,16 +219,10 @@ describe('LinkingBidirectional', function() {
});
it('should support custom filters', async function() {
// Add a new custom section with a widget.
// Add a new page with a table and custom widget.
await gu.addNewPage('Table', 'Classes', {});
// Rename this section as Data.
await gu.renameActiveSection('Data');
// Add new custom section with a widget.
await gu.addNewSection('Custom', 'Classes', { selectBy: 'Data' });
// Rename this section as Custom.
await gu.addNewSection('Custom', 'Classes', {customWidget: /Custom URL/, selectBy: 'Data'});
await gu.renameActiveSection('Custom');
// Make sure it can be used as a filter.

View File

@@ -33,22 +33,17 @@ describe('RightPanel', function() {
await gu.undo();
// Add a custom section.
await gu.addNewSection('Custom', 'Table1');
await gu.addNewSection('Custom', 'Table1', { customWidget: /Custom URL/ });
assert.isFalse(await gu.isSidePanelOpen('right'));
await gu.undo();
// Add a custom page.
await gu.addNewPage('Custom', 'Table1');
await gu.addNewPage('Custom', 'Table1', { customWidget: /Custom URL/ });
assert.isFalse(await gu.isSidePanelOpen('right'));
await gu.undo();
// Now open the panel on the column tab.
const columnTab = async () => {
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-field').click();
};
await columnTab();
await gu.openColumnPanel();
// Add a chart section.
await gu.addNewSection('Chart', 'Table1');
@@ -56,7 +51,7 @@ describe('RightPanel', function() {
assert.isTrue(await driver.find('.test-right-widget-title').isDisplayed());
await gu.undo();
await columnTab();
await gu.openColumnPanel();
// Add a chart page.
await gu.addNewPage('Chart', 'Table1');
@@ -64,18 +59,18 @@ describe('RightPanel', function() {
assert.isTrue(await driver.find('.test-right-widget-title').isDisplayed());
await gu.undo();
await columnTab();
await gu.openColumnPanel();
// Add a custom section.
await gu.addNewSection('Custom', 'Table1');
await gu.addNewSection('Custom', 'Table1', { customWidget: /Custom URL/ });
assert.isTrue(await gu.isSidePanelOpen('right'));
assert.isTrue(await driver.find('.test-right-widget-title').isDisplayed());
await gu.undo();
await columnTab();
await gu.openColumnPanel();
// Add a custom page.
await gu.addNewPage('Custom', 'Table1');
await gu.addNewPage('Custom', 'Table1', { customWidget: /Custom URL/ });
assert.isTrue(await gu.isSidePanelOpen('right'));
assert.isTrue(await driver.find('.test-right-widget-title').isDisplayed());
await gu.undo();

View File

@@ -100,7 +100,7 @@ describe("SelectBy", function() {
// Create a page with with charts and custom widget and then check that no linking is offered
await gu.addNewPage(/Chart/, /Table1/);
await gu.addNewSection(/Custom/, /Table2/);
await gu.addNewSection(/Custom/, /Table2/, {customWidget: /Custom URL/});
// open add widget to page
await driver.findWait('.test-dp-add-new', 2000).doClick();

View File

@@ -100,15 +100,15 @@ describe("ViewLayoutCollapse", function() {
// Add custom section.
await gu.addNewPage('Table', 'Companies');
await gu.addNewSection('Custom', 'Companies', { selectBy: 'COMPANIES'});
await gu.addNewSection('Custom', 'Companies', {selectBy: 'COMPANIES'});
// Serve custom widget.
const widgetServer = await serveSomething(app => {
addStatic(app);
});
cleanup.addAfterAll(widgetServer.shutdown);
await gu.setCustomWidgetUrl(widgetServer.url + '/probe/index.html', {openGallery: false});
await gu.openWidgetPanel();
await gu.setWidgetUrl(widgetServer.url + '/probe/index.html');
await gu.widgetAccess(AccessLevel.full);
// Collapse it.
@@ -139,15 +139,15 @@ describe("ViewLayoutCollapse", function() {
// Add custom section.
await gu.addNewPage('Table', 'Companies');
await gu.addNewSection('Custom', 'Companies', { selectBy: 'COMPANIES'});
await gu.addNewSection('Custom', 'Companies', {selectBy: 'COMPANIES'});
// Serve custom widget.
const widgetServer = await serveSomething(app => {
addStatic(app);
});
cleanup.addAfterAll(widgetServer.shutdown);
await gu.setCustomWidgetUrl(widgetServer.url + '/probe/index.html', {openGallery: false});
await gu.openWidgetPanel();
await gu.setWidgetUrl(widgetServer.url + '/probe/index.html');
await gu.widgetAccess(AccessLevel.full);
// Collapse it.

View File

@@ -3242,17 +3242,52 @@ export async function renameActiveTable(name: string) {
await waitForServer();
}
export async function setWidgetUrl(url: string) {
await driver.find('.test-config-widget-url').click();
// First clear textbox.
await clearInput();
if (url) {
await sendKeys(url);
export async function getCustomWidgetName() {
await openWidgetPanel();
return await driver.find('.test-config-widget-open-custom-widget-gallery').getText();
}
export async function getCustomWidgetInfo(info: 'description'|'developer'|'last-updated') {
await openWidgetPanel();
if (await driver.find('.test-config-widget-show-custom-widget-details').isPresent()) {
await driver.find('.test-config-widget-show-custom-widget-details').click();
}
if (!await driver.find(`.test-config-widget-custom-widget-${info}`).isPresent()) {
return '';
}
return await driver.find(`.test-config-widget-custom-widget-${info}`).getText();
}
export async function openCustomWidgetGallery() {
await openWidgetPanel();
await driver.find('.test-config-widget-open-custom-widget-gallery').click();
await waitForServer();
}
interface SetWidgetOptions {
/** Defaults to `true`. */
openGallery?: boolean;
}
export async function setCustomWidgetUrl(url: string, options: SetWidgetOptions = {}) {
const {openGallery = true} = options;
if (openGallery) { await openCustomWidgetGallery(); }
await driver.find('.test-custom-widget-gallery-custom-url').click();
await clearInput();
if (url) { await sendKeys(url); }
await sendKeys(Key.ENTER);
await waitForServer();
}
export async function setCustomWidget(content: string|RegExp, options: SetWidgetOptions = {}) {
const {openGallery = true} = options;
if (openGallery) { await openCustomWidgetGallery(); }
await driver.findContent('.test-custom-widget-gallery-widget', content).click();
await driver.find('.test-custom-widget-gallery-save').click();
await waitForServer();
}
type BehaviorActions = 'Clear and reset' | 'Convert column to data' | 'Clear and make into formula' |
'Convert columns to data';
/**

View File

@@ -99,13 +99,14 @@ export class GristWebDriverUtils {
tableRe: RegExp|string = '',
options: PageWidgetPickerOptions = {}
) {
const {customWidget, dismissTips, dontAdd, selectBy, summarize, tableName} = options;
const driver = this.driver;
if (options.dismissTips) { await this.dismissBehavioralPrompts(); }
if (dismissTips) { await this.dismissBehavioralPrompts(); }
// select right type
await driver.findContent('.test-wselect-type', typeRe).doClick();
if (options.dismissTips) { await this.dismissBehavioralPrompts(); }
if (dismissTips) { await this.dismissBehavioralPrompts(); }
if (tableRe) {
const tableEl = driver.findContent('.test-wselect-table', tableRe);
@@ -118,34 +119,32 @@ export class GristWebDriverUtils {
// let's select table
await tableEl.click();
if (options.dismissTips) { await this.dismissBehavioralPrompts(); }
if (dismissTips) { await this.dismissBehavioralPrompts(); }
const pivotEl = tableEl.find('.test-wselect-pivot');
if (await pivotEl.isPresent()) {
await this.toggleSelectable(pivotEl, Boolean(options.summarize));
await this.toggleSelectable(pivotEl, Boolean(summarize));
}
if (options.summarize) {
if (summarize) {
for (const columnEl of await driver.findAll('.test-wselect-column')) {
const label = await columnEl.getText();
// TODO: Matching cols with regexp calls for trouble and adds no value. I think function should be
// rewritten using string matching only.
const goal = Boolean(options.summarize.find(r => label.match(r)));
const goal = Boolean(summarize.find(r => label.match(r)));
await this.toggleSelectable(columnEl, goal);
}
}
if (options.selectBy) {
if (selectBy) {
// select link
await driver.find('.test-wselect-selectby').doClick();
await driver.findContent('.test-wselect-selectby option', options.selectBy).doClick();
await driver.findContent('.test-wselect-selectby option', selectBy).doClick();
}
}
if (options.dontAdd) {
return;
}
if (dontAdd) { return; }
// add the widget
await driver.find('.test-wselect-addBtn').doClick();
@@ -154,14 +153,20 @@ export class GristWebDriverUtils {
const prompts = await driver.findAll(".test-modal-prompt");
const prompt = prompts[0];
if (prompt) {
if (options.tableName) {
if (tableName) {
await prompt.doClear();
await prompt.click();
await driver.sendKeys(options.tableName);
await driver.sendKeys(tableName);
}
await driver.find(".test-modal-confirm").click();
}
if (customWidget) {
await this.waitForServer();
await driver.findContent('.test-custom-widget-gallery-widget-name', customWidget).click();
await driver.find('.test-custom-widget-gallery-save').click();
}
await this.waitForServer();
}
@@ -269,4 +274,6 @@ export interface PageWidgetPickerOptions {
dontAdd?: boolean;
/** If true, dismiss any tooltips that are shown. */
dismissTips?: boolean;
/** Optional pattern of custom widget name to select in the gallery. */
customWidget?: RegExp|string;
}