@gjsify/iframe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,323 @@
1
+ // Tests for @gjsify/iframe — HTMLIFrameElement, IFrameWindowProxy, IFrameWidget
2
+ // Reference: refs/happy-dom/packages/happy-dom/test/nodes/html-iframe-element/
3
+
4
+ import { describe, it, expect } from '@gjsify/unit';
5
+
6
+ // Import index.ts to trigger side-effect registration (Document.registerElementFactory)
7
+ import { HTMLIFrameElement, IFrameWindowProxy } from './index.js';
8
+ import { Document } from '@gjsify/dom-elements';
9
+ import { HTMLElement, Element, Node } from '@gjsify/dom-elements';
10
+ import { MessageEvent } from '@gjsify/dom-events';
11
+
12
+ export default async () => {
13
+ // -- HTMLIFrameElement DOM properties --
14
+
15
+ await describe('HTMLIFrameElement', async () => {
16
+ await it('should be an instance of HTMLElement, Element, and Node', async () => {
17
+ const iframe = new HTMLIFrameElement();
18
+ expect(iframe instanceof HTMLElement).toBe(true);
19
+ expect(iframe instanceof Element).toBe(true);
20
+ expect(iframe instanceof Node).toBe(true);
21
+ });
22
+
23
+ await it('should have correct tagName and localName', async () => {
24
+ const iframe = new HTMLIFrameElement();
25
+ expect(iframe.tagName).toBe('IFRAME');
26
+ expect(iframe.localName).toBe('iframe');
27
+ });
28
+
29
+ await it('should have correct [Symbol.toStringTag]', async () => {
30
+ const iframe = new HTMLIFrameElement();
31
+ expect(Object.prototype.toString.call(iframe)).toBe('[object HTMLIFrameElement]');
32
+ });
33
+
34
+ // -- src --
35
+
36
+ await it('should get/set src', async () => {
37
+ const iframe = new HTMLIFrameElement();
38
+ expect(iframe.src).toBe('');
39
+ iframe.setAttribute('src', 'https://example.com');
40
+ expect(iframe.src).toBe('https://example.com');
41
+ });
42
+
43
+ await it('should reflect src to attribute', async () => {
44
+ const iframe = new HTMLIFrameElement();
45
+ iframe.src = 'https://example.com';
46
+ expect(iframe.getAttribute('src')).toBe('https://example.com');
47
+ });
48
+
49
+ // -- srcdoc --
50
+
51
+ await it('should get/set srcdoc', async () => {
52
+ const iframe = new HTMLIFrameElement();
53
+ expect(iframe.srcdoc).toBe('');
54
+ iframe.setAttribute('srcdoc', '<h1>Hello</h1>');
55
+ expect(iframe.srcdoc).toBe('<h1>Hello</h1>');
56
+ });
57
+
58
+ await it('should reflect srcdoc to attribute', async () => {
59
+ const iframe = new HTMLIFrameElement();
60
+ iframe.srcdoc = '<p>test</p>';
61
+ expect(iframe.getAttribute('srcdoc')).toBe('<p>test</p>');
62
+ });
63
+
64
+ // -- name --
65
+
66
+ await it('should get/set name', async () => {
67
+ const iframe = new HTMLIFrameElement();
68
+ expect(iframe.name).toBe('');
69
+ iframe.name = 'my-frame';
70
+ expect(iframe.name).toBe('my-frame');
71
+ expect(iframe.getAttribute('name')).toBe('my-frame');
72
+ });
73
+
74
+ // -- sandbox --
75
+
76
+ await it('should get/set sandbox', async () => {
77
+ const iframe = new HTMLIFrameElement();
78
+ expect(iframe.sandbox).toBe('');
79
+ iframe.sandbox = 'allow-scripts allow-same-origin';
80
+ expect(iframe.sandbox).toBe('allow-scripts allow-same-origin');
81
+ expect(iframe.getAttribute('sandbox')).toBe('allow-scripts allow-same-origin');
82
+ });
83
+
84
+ // -- allow --
85
+
86
+ await it('should get/set allow', async () => {
87
+ const iframe = new HTMLIFrameElement();
88
+ expect(iframe.allow).toBe('');
89
+ iframe.allow = 'fullscreen; autoplay';
90
+ expect(iframe.allow).toBe('fullscreen; autoplay');
91
+ expect(iframe.getAttribute('allow')).toBe('fullscreen; autoplay');
92
+ });
93
+
94
+ // -- referrerPolicy --
95
+
96
+ await it('should get/set referrerPolicy', async () => {
97
+ const iframe = new HTMLIFrameElement();
98
+ expect(iframe.referrerPolicy).toBe('');
99
+ iframe.referrerPolicy = 'no-referrer';
100
+ expect(iframe.referrerPolicy).toBe('no-referrer');
101
+ expect(iframe.getAttribute('referrerpolicy')).toBe('no-referrer');
102
+ });
103
+
104
+ // -- loading --
105
+
106
+ await it('should get/set loading with valid values', async () => {
107
+ const iframe = new HTMLIFrameElement();
108
+ expect(iframe.loading).toBe('eager');
109
+ iframe.loading = 'lazy';
110
+ expect(iframe.loading).toBe('lazy');
111
+ iframe.loading = 'eager';
112
+ expect(iframe.loading).toBe('eager');
113
+ });
114
+
115
+ await it('should default loading to "eager" for invalid values', async () => {
116
+ const iframe = new HTMLIFrameElement();
117
+ iframe.loading = 'invalid';
118
+ expect(iframe.loading).toBe('eager');
119
+ });
120
+
121
+ // -- width / height --
122
+
123
+ await it('should get/set width as string', async () => {
124
+ const iframe = new HTMLIFrameElement();
125
+ expect(iframe.width).toBe('');
126
+ iframe.width = '300';
127
+ expect(iframe.width).toBe('300');
128
+ expect(iframe.getAttribute('width')).toBe('300');
129
+ });
130
+
131
+ await it('should get/set height as string', async () => {
132
+ const iframe = new HTMLIFrameElement();
133
+ expect(iframe.height).toBe('');
134
+ iframe.height = '200';
135
+ expect(iframe.height).toBe('200');
136
+ expect(iframe.getAttribute('height')).toBe('200');
137
+ });
138
+
139
+ // -- contentWindow / contentDocument --
140
+
141
+ await it('should return null for contentWindow without backing widget', async () => {
142
+ const iframe = new HTMLIFrameElement();
143
+ expect(iframe.contentWindow).toBeNull();
144
+ });
145
+
146
+ await it('should always return null for contentDocument', async () => {
147
+ const iframe = new HTMLIFrameElement();
148
+ expect(iframe.contentDocument).toBeNull();
149
+ });
150
+
151
+ // -- Events --
152
+
153
+ await it('should dispatch load event via _onLoad()', async () => {
154
+ const iframe = new HTMLIFrameElement();
155
+ let loaded = false;
156
+ iframe.addEventListener('load', () => { loaded = true; });
157
+ iframe._onLoad();
158
+ expect(loaded).toBe(true);
159
+ });
160
+
161
+ await it('should dispatch error event via _onError()', async () => {
162
+ const iframe = new HTMLIFrameElement();
163
+ let errored = false;
164
+ iframe.addEventListener('error', () => { errored = true; });
165
+ iframe._onError();
166
+ expect(errored).toBe(true);
167
+ });
168
+
169
+ await it('should support onload property handler', async () => {
170
+ const iframe = new HTMLIFrameElement();
171
+ let called = false;
172
+ iframe.onload = () => { called = true; };
173
+ iframe._onLoad();
174
+ expect(called).toBe(true);
175
+ });
176
+
177
+ await it('should support onerror property handler', async () => {
178
+ const iframe = new HTMLIFrameElement();
179
+ let called = false;
180
+ iframe.onerror = () => { called = true; };
181
+ iframe._onError();
182
+ expect(called).toBe(true);
183
+ });
184
+
185
+ // -- Clone --
186
+
187
+ await it('should clone without widget reference', async () => {
188
+ const iframe = new HTMLIFrameElement();
189
+ iframe.src = 'https://example.com';
190
+ iframe.name = 'test-frame';
191
+ const clone = iframe.cloneNode(false);
192
+ expect(clone instanceof HTMLIFrameElement).toBe(true);
193
+ expect(clone.getAttribute('src')).toBe('https://example.com');
194
+ expect(clone.getAttribute('name')).toBe('test-frame');
195
+ expect(clone.contentWindow).toBeNull();
196
+ });
197
+
198
+ // -- getSVGDocument --
199
+
200
+ await it('should return null for getSVGDocument()', async () => {
201
+ const iframe = new HTMLIFrameElement();
202
+ expect(iframe.getSVGDocument()).toBeNull();
203
+ });
204
+ });
205
+
206
+ // -- Document.createElement('iframe') --
207
+
208
+ await describe('Document.registerElementFactory', async () => {
209
+ await it('should create HTMLIFrameElement via document.createElement', async () => {
210
+ // The factory is registered as a side-effect in index.ts
211
+ // which is imported by the test runner
212
+ const doc = new Document();
213
+ const iframe = doc.createElement('iframe');
214
+ expect(iframe instanceof HTMLIFrameElement).toBe(true);
215
+ expect(iframe.tagName).toBe('IFRAME');
216
+ });
217
+ });
218
+
219
+ // -- IFrameWindowProxy (unit tests without WebView) --
220
+
221
+ await describe('IFrameWindowProxy', async () => {
222
+ await it('should have correct [Symbol.toStringTag]', async () => {
223
+ // Create a minimal mock bridge for unit testing
224
+ const mockBridge = {
225
+ sendToWebView(_data: unknown, _targetOrigin: string) {},
226
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
227
+ };
228
+ const proxy = new IFrameWindowProxy(mockBridge as any);
229
+ expect(Object.prototype.toString.call(proxy)).toBe('[object IFrameWindowProxy]');
230
+ });
231
+
232
+ await it('should return globalThis as parent', async () => {
233
+ const mockBridge = {
234
+ sendToWebView(_data: unknown, _targetOrigin: string) {},
235
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
236
+ };
237
+ const proxy = new IFrameWindowProxy(mockBridge as any);
238
+ expect(proxy.parent).toBe(globalThis);
239
+ });
240
+
241
+ await it('should return globalThis as top', async () => {
242
+ const mockBridge = {
243
+ sendToWebView(_data: unknown, _targetOrigin: string) {},
244
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
245
+ };
246
+ const proxy = new IFrameWindowProxy(mockBridge as any);
247
+ expect(proxy.top).toBe(globalThis);
248
+ });
249
+
250
+ await it('should return self references', async () => {
251
+ const mockBridge = {
252
+ sendToWebView(_data: unknown, _targetOrigin: string) {},
253
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
254
+ };
255
+ const proxy = new IFrameWindowProxy(mockBridge as any);
256
+ expect(proxy.self).toBe(proxy);
257
+ expect(proxy.window).toBe(proxy);
258
+ });
259
+
260
+ await it('should report closed status', async () => {
261
+ const mockBridge = {
262
+ sendToWebView(_data: unknown, _targetOrigin: string) {},
263
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
264
+ };
265
+ const proxy = new IFrameWindowProxy(mockBridge as any);
266
+ expect(proxy.closed).toBe(false);
267
+ proxy._close();
268
+ expect(proxy.closed).toBe(true);
269
+ });
270
+
271
+ await it('should delegate postMessage to bridge', async () => {
272
+ let sentData: unknown;
273
+ let sentOrigin: string | undefined;
274
+ const mockBridge = {
275
+ sendToWebView(data: unknown, targetOrigin: string) {
276
+ sentData = data;
277
+ sentOrigin = targetOrigin;
278
+ },
279
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
280
+ };
281
+ const proxy = new IFrameWindowProxy(mockBridge as any);
282
+ proxy.postMessage({ hello: 'world' }, 'https://example.com');
283
+ expect((sentData as any).hello).toBe('world');
284
+ expect(sentOrigin).toBe('https://example.com');
285
+ });
286
+
287
+ await it('should not send message when closed', async () => {
288
+ let called = false;
289
+ const mockBridge = {
290
+ sendToWebView() { called = true; },
291
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
292
+ };
293
+ const proxy = new IFrameWindowProxy(mockBridge as any);
294
+ proxy._close();
295
+ proxy.postMessage('test');
296
+ expect(called).toBe(false);
297
+ });
298
+
299
+ await it('should return location from bridge', async () => {
300
+ const mockBridge = {
301
+ sendToWebView() {},
302
+ getLocation() { return { href: 'https://example.com/page', origin: 'https://example.com' }; },
303
+ };
304
+ const proxy = new IFrameWindowProxy(mockBridge as any);
305
+ expect(proxy.location.href).toBe('https://example.com/page');
306
+ expect(proxy.location.origin).toBe('https://example.com');
307
+ });
308
+
309
+ await it('should support addEventListener for message events', async () => {
310
+ const mockBridge = {
311
+ sendToWebView() {},
312
+ getLocation() { return { href: 'about:blank', origin: 'null' }; },
313
+ };
314
+ const proxy = new IFrameWindowProxy(mockBridge as any);
315
+ let received: unknown;
316
+ proxy.addEventListener('message', (event: Event) => {
317
+ received = (event as MessageEvent).data;
318
+ });
319
+ proxy.dispatchEvent(new MessageEvent('message', { data: 'hello' }));
320
+ expect(received).toBe('hello');
321
+ });
322
+ });
323
+ };
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // HTMLIFrameElement for GJS — backed by WebKit.WebView
2
+ // Reference: refs/happy-dom/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts
3
+ // Reference: refs/map-editor/packages/message-channel-gjs/ (GJS ↔ WebView communication)
4
+
5
+ export { HTMLIFrameElement } from './html-iframe-element.js';
6
+ export { IFrameWidget } from './iframe-widget.js';
7
+ export { IFrameWindowProxy } from './iframe-window-proxy.js';
8
+ export { MessageBridge } from './message-bridge.js';
9
+ export type { IFrameWidgetOptions, IFrameReadyCallback, IFrameMessageData } from './types/index.js';
10
+
11
+ // Side-effect: register DOM globals on import.
12
+ // Same pattern as @gjsify/dom-elements and @gjsify/canvas2d.
13
+ import { Document } from '@gjsify/dom-elements';
14
+ import { HTMLIFrameElement } from './html-iframe-element.js';
15
+
16
+ // Register so that document.createElement('iframe') works
17
+ Document.registerElementFactory('iframe', () => new HTMLIFrameElement());
18
+
19
+ // Register global constructor
20
+ Object.defineProperty(globalThis, 'HTMLIFrameElement', {
21
+ value: HTMLIFrameElement,
22
+ writable: true,
23
+ configurable: true,
24
+ });
@@ -0,0 +1,175 @@
1
+ // MessageBridge for GJS — postMessage bridge between GJS and WebKit.WebView
2
+ // Adapted from refs/map-editor/packages/message-channel-gjs/src/rpc-endpoint.ts
3
+ // Copyright (c) PixelRPG contributors. MIT license.
4
+ // Modifications: Simplified to standard postMessage semantics (no JSON-RPC layer)
5
+
6
+ import Gio from 'gi://Gio?version=2.0';
7
+ import WebKit from 'gi://WebKit?version=6.0';
8
+ import JavaScriptCore from 'gi://JavaScriptCore?version=6.0';
9
+ import { MessageEvent } from '@gjsify/dom-events';
10
+
11
+ // Promisify evaluate_javascript so it returns a Promise in GJS
12
+ Gio._promisify(WebKit.WebView.prototype, 'evaluate_javascript', 'evaluate_javascript_finish');
13
+
14
+ import type { IFrameWindowProxy } from './iframe-window-proxy.js';
15
+ import type { IFrameMessageData } from './types/index.js';
16
+
17
+ const CHANNEL_NAME = 'gjsify-iframe';
18
+
19
+ /**
20
+ * Bootstrap script injected into every WebView page at document start.
21
+ * Provides the `window.parent.postMessage()` bridge from WebView content back to GJS.
22
+ *
23
+ * The script:
24
+ * 1. Gets the WebKit message handler registered under CHANNEL_NAME
25
+ * 2. Creates a parent proxy with a postMessage() that sends via the WebKit handler
26
+ * 3. Overrides window.parent to point to the proxy
27
+ */
28
+ const BOOTSTRAP_SCRIPT = `(function() {
29
+ var handler = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers['${CHANNEL_NAME}'];
30
+ if (!handler) return;
31
+ function bridgePostMessage(data, targetOrigin) {
32
+ handler.postMessage(JSON.stringify({
33
+ data: data,
34
+ targetOrigin: targetOrigin || '*',
35
+ origin: location.origin
36
+ }));
37
+ }
38
+ // In a WebKit.WebView loaded via srcdoc, window.parent === window (no real iframe nesting).
39
+ // window.parent is [LegacyUnforgeable] — cannot be redefined with defineProperty.
40
+ // Instead, override window.postMessage directly. Since window.parent === window,
41
+ // calls to window.parent.postMessage() will use our override.
42
+ var origPostMessage = window.postMessage;
43
+ window.postMessage = function(data, targetOrigin) {
44
+ bridgePostMessage(data, targetOrigin);
45
+ };
46
+ // Also expose on a safe namespace for explicit use
47
+ window.__gjsifyBridge = { postMessage: bridgePostMessage, origPostMessage: origPostMessage };
48
+ })();`;
49
+
50
+ /**
51
+ * Manages bidirectional postMessage communication between GJS and a WebKit.WebView.
52
+ *
53
+ * Direction 1 — GJS → WebView:
54
+ * Uses webView.evaluate_javascript() to dispatch a MessageEvent on the WebView's window.
55
+ *
56
+ * Direction 2 — WebView → GJS:
57
+ * Bootstrap script overrides window.parent.postMessage to call
58
+ * webkit.messageHandlers[CHANNEL_NAME].postMessage(), which triggers
59
+ * the UserContentManager 'script-message-received' signal in GJS.
60
+ */
61
+ export class MessageBridge {
62
+ private _webView: WebKit.WebView;
63
+ private _userContentManager: WebKit.UserContentManager;
64
+ private _windowProxy: IFrameWindowProxy | null = null;
65
+ private _currentUri = 'about:blank';
66
+ private _signalId: number | null = null;
67
+
68
+ constructor(webView: WebKit.WebView) {
69
+ this._webView = webView;
70
+ this._userContentManager = webView.get_user_content_manager();
71
+ this._setupReceiver();
72
+ this._injectBootstrapScript();
73
+ }
74
+
75
+ /** Connect the IFrameWindowProxy that will receive messages from the WebView */
76
+ setWindowProxy(proxy: IFrameWindowProxy): void {
77
+ this._windowProxy = proxy;
78
+ }
79
+
80
+ /** Update current URI (called by IFrameWidget on load-changed) */
81
+ updateUri(uri: string): void {
82
+ this._currentUri = uri;
83
+ }
84
+
85
+ /** Get current location info for the IFrameWindowProxy */
86
+ getLocation(): { href: string; origin: string } {
87
+ let origin: string;
88
+ try {
89
+ const url = new URL(this._currentUri);
90
+ origin = url.origin;
91
+ } catch {
92
+ origin = 'null';
93
+ }
94
+ return { href: this._currentUri, origin };
95
+ }
96
+
97
+ /**
98
+ * Send a message from GJS to the WebView content.
99
+ * Dispatches a standard MessageEvent on the WebView's window object.
100
+ */
101
+ sendToWebView(data: unknown, _targetOrigin: string): void {
102
+ const serialized = JSON.stringify(data);
103
+ const origin = JSON.stringify('gjsify');
104
+ // Note: do not pass `source` — WebKit's MessageEvent constructor throws TypeError
105
+ // if source is not a valid MessageEventSource (Window/MessagePort/ServiceWorker)
106
+ const script = `window.dispatchEvent(new MessageEvent('message', { data: JSON.parse(${JSON.stringify(serialized)}), origin: ${origin} }));`;
107
+
108
+ // evaluate_javascript is async in WebKit 6.0 — fire and forget
109
+ this._webView.evaluate_javascript(
110
+ script,
111
+ -1, // length (-1 = null-terminated)
112
+ null, // world name (null = default)
113
+ null, // source URI
114
+ null, // cancellable
115
+ ).catch(() => {
116
+ // Ignore errors for fire-and-forget message dispatch
117
+ });
118
+ }
119
+
120
+ /** Clean up signal handlers */
121
+ destroy(): void {
122
+ if (this._signalId !== null) {
123
+ this._userContentManager.disconnect(this._signalId);
124
+ this._signalId = null;
125
+ }
126
+ this._userContentManager.unregister_script_message_handler(CHANNEL_NAME, null);
127
+ this._windowProxy = null;
128
+ }
129
+
130
+ /**
131
+ * Set up the receiver for messages coming from the WebView.
132
+ * Registers a script message handler and connects to the signal.
133
+ */
134
+ private _setupReceiver(): void {
135
+ this._userContentManager.register_script_message_handler(CHANNEL_NAME, null);
136
+
137
+ this._signalId = this._userContentManager.connect(
138
+ `script-message-received::${CHANNEL_NAME}`,
139
+ (_ucm: WebKit.UserContentManager, jsValue: JavaScriptCore.Value) => {
140
+ if (!this._windowProxy) return;
141
+
142
+ try {
143
+ // The bootstrap script sends JSON.stringify({data, targetOrigin, origin})
144
+ // so jsValue is a JSC string. Use to_string() to get the raw JSON.
145
+ const json = jsValue.to_string();
146
+ const envelope: IFrameMessageData = JSON.parse(json);
147
+
148
+ // Dispatch MessageEvent on the IFrameWindowProxy
149
+ const event = new MessageEvent('message', {
150
+ data: envelope.data,
151
+ origin: envelope.origin,
152
+ });
153
+ this._windowProxy.dispatchEvent(event);
154
+ } catch (error) {
155
+ console.error('[IFrame MessageBridge] Error processing message:', error);
156
+ }
157
+ },
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Inject the bootstrap script into the WebView so that
163
+ * window.parent.postMessage() bridges back to GJS.
164
+ */
165
+ private _injectBootstrapScript(): void {
166
+ const script = new WebKit.UserScript(
167
+ BOOTSTRAP_SCRIPT,
168
+ WebKit.UserContentInjectedFrames.ALL_FRAMES,
169
+ WebKit.UserScriptInjectionTime.START,
170
+ null, // allow list (null = all)
171
+ null, // block list (null = none)
172
+ );
173
+ this._userContentManager.add_script(script);
174
+ }
175
+ }
@@ -0,0 +1,11 @@
1
+ // Iframe-specific property symbols
2
+ // Follows pattern from packages/dom/dom-elements/src/property-symbol.ts
3
+
4
+ // IFrameWidget (GTK WebView backing)
5
+ export const iframeWidget = Symbol('iframeWidget');
6
+
7
+ // IFrameWindowProxy (contentWindow)
8
+ export const windowProxy = Symbol('windowProxy');
9
+
10
+ // Loading state
11
+ export const loaded = Symbol('loaded');
package/src/test.mts ADDED
@@ -0,0 +1,6 @@
1
+
2
+ import { run } from '@gjsify/unit';
3
+
4
+ import testSuite from './index.spec.js';
5
+
6
+ run({ testSuite });
@@ -0,0 +1,19 @@
1
+ // Shared interfaces for @gjsify/iframe
2
+
3
+ /** Options passed to IFrameWidget constructor */
4
+ export interface IFrameWidgetOptions {
5
+ /** Enable developer extras (Web Inspector). Default: true */
6
+ enableDeveloperExtras?: boolean;
7
+ /** Enable JavaScript execution in the WebView. Default: true */
8
+ enableJavascript?: boolean;
9
+ }
10
+
11
+ /** Data structure for messages crossing the GJS/WebView boundary */
12
+ export interface IFrameMessageData {
13
+ data: unknown;
14
+ targetOrigin: string;
15
+ origin: string;
16
+ }
17
+
18
+ /** Callback for when the IFrameWidget is ready */
19
+ export type IFrameReadyCallback = (iframe: globalThis.HTMLIFrameElement) => void;