From 58323f5313cd0ed005670c489a1ad5457f6cd50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= Date: Wed, 30 Aug 2023 16:35:13 +0200 Subject: [PATCH] (core) Adding testId to the widget iframe once it receives the ready message Summary: Iframe with custom widget is marked with a test class `test-custom-widget-ready` when it receives the `ready` message from the rendered widget. Test Plan: Added and updated. Existing test should pass. Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: georgegevoian Differential Revision: https://phab.getgrist.com/D4023 --- app/client/components/Importer.ts | 4 +-- app/client/components/WidgetFrame.ts | 21 +++++++++++---- app/client/lib/domUtils.ts | 9 +++++++ test/nbrowser/CustomView.ts | 38 ++++++++++++++++++++++++++++ test/nbrowser/CustomWidgetsConfig.ts | 16 +++++------- 5 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 app/client/lib/domUtils.ts diff --git a/app/client/components/Importer.ts b/app/client/components/Importer.ts index d8941e1f..da779d6c 100644 --- a/app/client/components/Importer.ts +++ b/app/client/components/Importer.ts @@ -7,6 +7,7 @@ import {GristDoc} from 'app/client/components/GristDoc'; import {buildParseOptionsForm, ParseOptionValues} from 'app/client/components/ParseOptions'; import {PluginScreen} from 'app/client/components/PluginScreen'; +import {makeTestId} from 'app/client/lib/domUtils'; import {FocusLayer} from 'app/client/lib/FocusLayer'; import {ImportSourceElement} from 'app/client/lib/ImportSourceElement'; import {makeT} from 'app/client/lib/localization'; @@ -46,7 +47,6 @@ import {byteString, not} from 'app/common/gutil'; import {FetchUrlOptions, UploadResult} from 'app/common/uploads'; import {ParseOptions, ParseOptionSchema} from 'app/plugin/FileParserAPI'; import { - BindableValue, Computed, Disposable, dom, @@ -65,7 +65,7 @@ import debounce = require('lodash/debounce'); const t = makeT('Importer'); // Custom testId that can be appended conditionally. -const testId = (id: string, obs?: BindableValue) => dom.cls('test-importer-' + id, obs ?? true); +const testId = makeTestId('test-importer-'); // We expect a function for creating the preview GridView, to avoid the need to require the diff --git a/app/client/components/WidgetFrame.ts b/app/client/components/WidgetFrame.ts index f4b5b40f..4d4a1f3c 100644 --- a/app/client/components/WidgetFrame.ts +++ b/app/client/components/WidgetFrame.ts @@ -2,6 +2,7 @@ import BaseView from 'app/client/components/BaseView'; import {GristDoc} from 'app/client/components/GristDoc'; import {hooks} from 'app/client/Hooks'; import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; +import {makeTestId} from 'app/client/lib/domUtils'; import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel'; import {AccessLevel, isSatisfied} from 'app/common/CustomWidget'; import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; @@ -16,6 +17,9 @@ import debounce = require('lodash/debounce'); import isEqual = require('lodash/isEqual'); import flatMap = require('lodash/flatMap'); +const testId = makeTestId('test-custom-widget-'); + + /** * This file contains a WidgetFrame and all its components. * @@ -62,6 +66,8 @@ export class WidgetFrame extends DisposableWithEvents { private _rpc: Rpc; // Created iframe element, used to receive and post messages via Rpc private _iframe: HTMLIFrameElement | null; + // If widget called ready() method, this will be set to true. + private _readyCalled = Observable.create(this, false); constructor(private _options: WidgetFrameOptions) { super(); @@ -155,10 +161,14 @@ export class WidgetFrame extends DisposableWithEvents { const fullUrl = urlWithAccess(this._options.url); const onElem = this._options.onElem ?? ((el: HTMLIFrameElement) => el); return onElem( - (this._iframe = dom('iframe', dom.cls('clipboard_focus'), dom.cls('custom_view'), { - src: fullUrl, - ...hooks.iframeAttributes, - })) + (this._iframe = dom('iframe', + dom.cls('clipboard_focus'), + dom.cls('custom_view'), { + src: fullUrl, + ...hooks.iframeAttributes, + }, + testId('ready', this._readyCalled), + )) ); } @@ -177,6 +187,7 @@ export class WidgetFrame extends DisposableWithEvents { } if (event.data.mtype === MsgType.Ready) { this.trigger('ready', this); + this._readyCalled.set(true); } this._rpc.receiveMessage(event.data); } @@ -513,7 +524,7 @@ export class RecordNotifier extends BaseEventSource { } /** - * Notifies about options position change. Exposed in the API as a onOptions handler. + * Notifies about options change. Exposed in the API as a onOptions handler. */ export class ConfigNotifier extends BaseEventSource { private _currentConfig: Computed; diff --git a/app/client/lib/domUtils.ts b/app/client/lib/domUtils.ts new file mode 100644 index 00000000..578ebfe2 --- /dev/null +++ b/app/client/lib/domUtils.ts @@ -0,0 +1,9 @@ +import {BindableValue, dom} from 'grainjs'; + +/** + * Version of makeTestId that can be appended conditionally. + * TODO: update grainjs typings, as this is already supported there. + */ +export function makeTestId(prefix: string) { + return (id: string, obs?: BindableValue) => dom.cls(prefix + id, obs ?? true); +} diff --git a/test/nbrowser/CustomView.ts b/test/nbrowser/CustomView.ts index f9fa7dac..1292bcd5 100644 --- a/test/nbrowser/CustomView.ts +++ b/test/nbrowser/CustomView.ts @@ -39,6 +39,38 @@ describe('CustomView', function() { } }); + // This tests if test id works. Feels counterintuitive to "test the test" but grist-widget repository test suite + // depends on this. + it('informs about ready called', async () => { + // Add a custom inline widget to a new doc. + const session = await gu.session().teamSite.login(); + await session.tempNewDoc(cleanup); + await gu.addNewSection('Custom', 'Table1'); + + // Create an inline widget that will call ready message. + await inFrame(async () => { + const customWidget = ` + + + `; + await driver.executeScript("document.write(`" + customWidget + "`);"); + }); + + // We should have a single iframe. + assert.equal(await driver.findAll('iframe').then(f => f.length), 1); + + // But without test ready class. + assert.isFalse(await driver.find("iframe.test-custom-widget-ready").isPresent()); + + // Now invoke ready. + await inFrame(async () => { + await driver.find('button').click(); + }); + + // And we should have a test ready class. + assert.isTrue(await driver.findWait("iframe.test-custom-widget-ready", 100).isDisplayed()); + }); + for (const access of ['none', 'read table', 'full'] as const) { function withAccess(obj: any, fallback: any) { @@ -478,3 +510,9 @@ describe('CustomView', function() { assert.equal(opinions['A'][0], 'do not zap plz'); }); }); + +async function inFrame(op: () => Promise) { + await driver.switchTo().frame(driver.find("iframe")); + await op(); + await driver.switchTo().defaultContent(); +} diff --git a/test/nbrowser/CustomWidgetsConfig.ts b/test/nbrowser/CustomWidgetsConfig.ts index cefdea0a..e480b243 100644 --- a/test/nbrowser/CustomWidgetsConfig.ts +++ b/test/nbrowser/CustomWidgetsConfig.ts @@ -220,14 +220,10 @@ describe('CustomWidgetsConfig', function () { // Poor man widget rpc. Class that invokes various parts in the tester widget. class Widget { - constructor(public frameSelector = 'iframe') {} + constructor() {} // Wait for a frame. public async waitForFrame() { - await driver.wait(() => driver.find(this.frameSelector).isPresent(), 3000); - const iframe = driver.find(this.frameSelector); - await driver.switchTo().frame(iframe); - await driver.wait(async () => (await driver.find('#ready').getText()) === 'ready', 3000); - await driver.switchTo().defaultContent(); + await driver.findWait(`iframe.test-custom-widget-ready`, 1000); } public async content() { return await this._read('body'); @@ -254,11 +250,11 @@ describe('CustomWidgetsConfig', function () { } // Wait for frame to close. public async waitForClose() { - await driver.wait(async () => !(await driver.find(this.frameSelector).isPresent()), 3000); + await driver.wait(async () => !(await driver.find('iframe').isPresent()), 3000); } // Wait for the onOptions event, and return its value. public async onOptions() { - const iframe = driver.find(this.frameSelector); + const iframe = driver.find('iframe'); await driver.switchTo().frame(iframe); // Wait for options to get filled, initially this div is empty, // as first message it should get at least null as an options. @@ -296,7 +292,7 @@ describe('CustomWidgetsConfig', function () { // serialized to the #output div. public async invokeOnWidget(name: string, input?: any[]) { // Switch to frame. - const iframe = driver.find(this.frameSelector); + const iframe = driver.find('iframe'); await driver.switchTo().frame(iframe); // Clear input box that holds arguments. await driver.find('#input').click(); @@ -331,7 +327,7 @@ describe('CustomWidgetsConfig', function () { } private async _read(selector: string) { - const iframe = driver.find(this.frameSelector); + const iframe = driver.find('iframe'); await driver.switchTo().frame(iframe); const text = await driver.find(selector).getText(); await driver.switchTo().defaultContent();