2020-03-20 20:28:31 +00:00
|
|
|
import { expect } from 'chai';
|
2023-06-15 14:42:27 +00:00
|
|
|
import * as path from 'node:path';
|
2022-03-28 16:47:08 +00:00
|
|
|
import { BrowserView, BrowserWindow, screen, webContents } from 'electron/main';
|
2023-01-25 21:01:25 +00:00
|
|
|
import { closeWindow } from './lib/window-helpers';
|
|
|
|
import { defer, ifit, startRemoteControlApp } from './lib/spec-helpers';
|
|
|
|
import { areColorsSimilar, captureScreen, getPixelColor } from './lib/screen-helpers';
|
2023-06-15 14:42:27 +00:00
|
|
|
import { once } from 'node:events';
|
2019-08-06 17:27:33 +00:00
|
|
|
|
2017-10-27 00:11:12 +00:00
|
|
|
describe('BrowserView module', () => {
|
2022-08-16 19:23:13 +00:00
|
|
|
const fixtures = path.resolve(__dirname, 'fixtures');
|
2018-11-27 01:39:03 +00:00
|
|
|
|
2020-03-20 20:28:31 +00:00
|
|
|
let w: BrowserWindow;
|
|
|
|
let view: BrowserView;
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
beforeEach(() => {
|
2023-12-13 21:01:03 +00:00
|
|
|
expect(webContents.getAllWebContents().length).to.equal(0, 'expected no webContents to exist');
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
w = new BrowserWindow({
|
|
|
|
show: false,
|
|
|
|
width: 400,
|
|
|
|
height: 400,
|
|
|
|
webPreferences: {
|
|
|
|
backgroundThrottling: false
|
|
|
|
}
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
|
|
|
});
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2019-07-24 15:44:24 +00:00
|
|
|
afterEach(async () => {
|
2023-02-23 23:53:53 +00:00
|
|
|
const p = once(w.webContents, 'destroyed');
|
2020-03-20 20:28:31 +00:00
|
|
|
await closeWindow(w);
|
2020-07-09 15:48:39 +00:00
|
|
|
w = null as any;
|
|
|
|
await p;
|
2019-10-04 00:30:44 +00:00
|
|
|
|
2022-09-01 00:40:02 +00:00
|
|
|
if (view && view.webContents) {
|
2023-02-23 23:53:53 +00:00
|
|
|
const p = once(view.webContents, 'destroyed');
|
2022-12-14 21:07:38 +00:00
|
|
|
view.webContents.destroy();
|
2020-07-09 15:48:39 +00:00
|
|
|
view = null as any;
|
|
|
|
await p;
|
2017-04-12 21:52:07 +00:00
|
|
|
}
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
expect(webContents.getAllWebContents().length).to.equal(0, 'expected no webContents to exist');
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2018-03-15 07:15:56 +00:00
|
|
|
|
2023-07-10 09:49:20 +00:00
|
|
|
it('sets the correct class name on the prototype', () => {
|
|
|
|
expect(BrowserView.prototype.constructor.name).to.equal('BrowserView');
|
|
|
|
});
|
|
|
|
|
2020-12-15 23:52:43 +00:00
|
|
|
it('can be created with an existing webContents', async () => {
|
2023-02-16 14:41:41 +00:00
|
|
|
const wc = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
|
2020-12-15 23:52:43 +00:00
|
|
|
await wc.loadURL('about:blank');
|
|
|
|
|
|
|
|
view = new BrowserView({ webContents: wc } as any);
|
2023-12-13 21:01:03 +00:00
|
|
|
expect(view.webContents === wc).to.be.true('view.webContents === wc');
|
2020-12-15 23:52:43 +00:00
|
|
|
|
|
|
|
expect(view.webContents.getURL()).to.equal('about:blank');
|
|
|
|
});
|
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
it('has type browserView', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
expect(view.webContents.getType()).to.equal('browserView');
|
|
|
|
});
|
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
describe('BrowserView.setBackgroundColor()', () => {
|
|
|
|
it('does not throw for valid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
view.setBackgroundColor('#000');
|
|
|
|
});
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
// We now treat invalid args as "no background".
|
|
|
|
it('does not throw for invalid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
2018-06-17 22:56:04 +00:00
|
|
|
expect(() => {
|
2023-12-13 21:01:03 +00:00
|
|
|
view.setBackgroundColor({} as any);
|
|
|
|
}).not.to.throw();
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2022-03-28 16:47:08 +00:00
|
|
|
|
|
|
|
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
|
2022-11-21 15:24:26 +00:00
|
|
|
ifit(process.platform === 'darwin' && process.arch === 'x64')('sets the background color to transparent if none is set', async () => {
|
2022-03-28 16:47:08 +00:00
|
|
|
const display = screen.getPrimaryDisplay();
|
|
|
|
const WINDOW_BACKGROUND_COLOR = '#55ccbb';
|
|
|
|
|
|
|
|
w.show();
|
|
|
|
w.setBounds(display.bounds);
|
|
|
|
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
|
|
|
|
await w.loadURL('about:blank');
|
|
|
|
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setBounds(display.bounds);
|
|
|
|
w.setBrowserView(view);
|
|
|
|
await view.webContents.loadURL('data:text/html,hello there');
|
|
|
|
|
|
|
|
const screenCapture = await captureScreen();
|
|
|
|
const centerColor = getPixelColor(screenCapture, {
|
|
|
|
x: display.size.width / 2,
|
|
|
|
y: display.size.height / 2
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(areColorsSimilar(centerColor, WINDOW_BACKGROUND_COLOR)).to.be.true();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
|
2022-11-21 15:24:26 +00:00
|
|
|
ifit(process.platform === 'darwin' && process.arch === 'x64')('successfully applies the background color', async () => {
|
2022-03-28 16:47:08 +00:00
|
|
|
const WINDOW_BACKGROUND_COLOR = '#55ccbb';
|
|
|
|
const VIEW_BACKGROUND_COLOR = '#ff00ff';
|
|
|
|
const display = screen.getPrimaryDisplay();
|
|
|
|
|
|
|
|
w.show();
|
|
|
|
w.setBounds(display.bounds);
|
|
|
|
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
|
|
|
|
await w.loadURL('about:blank');
|
|
|
|
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setBounds(display.bounds);
|
|
|
|
w.setBrowserView(view);
|
|
|
|
w.setBackgroundColor(VIEW_BACKGROUND_COLOR);
|
|
|
|
await view.webContents.loadURL('data:text/html,hello there');
|
|
|
|
|
|
|
|
const screenCapture = await captureScreen();
|
|
|
|
const centerColor = getPixelColor(screenCapture, {
|
|
|
|
x: display.size.width / 2,
|
|
|
|
y: display.size.height / 2
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(areColorsSimilar(centerColor, VIEW_BACKGROUND_COLOR)).to.be.true();
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
describe('BrowserView.setAutoResize()', () => {
|
|
|
|
it('does not throw for valid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({});
|
|
|
|
view.setAutoResize({ width: true, height: false });
|
|
|
|
});
|
2017-04-12 11:40:31 +00:00
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
it('throws for invalid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
2018-06-17 22:56:04 +00:00
|
|
|
expect(() => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view.setAutoResize(null as any);
|
2023-12-13 21:01:03 +00:00
|
|
|
}).to.throw(/Invalid auto resize options/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not resize when the BrowserView has no AutoResize', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 400, height: 200 });
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 400,
|
|
|
|
height: 200
|
|
|
|
});
|
|
|
|
w.setSize(800, 400);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 400,
|
|
|
|
height: 200
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('resizes horizontally when the window is resized horizontally', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({ width: true, height: false });
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 400, height: 200 });
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 400,
|
|
|
|
height: 200
|
|
|
|
});
|
|
|
|
w.setSize(800, 400);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 800,
|
|
|
|
height: 200
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('resizes vertically when the window is resized vertically', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({ width: false, height: true });
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 200, height: 400 });
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 200,
|
|
|
|
height: 400
|
|
|
|
});
|
|
|
|
w.setSize(400, 800);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 200,
|
|
|
|
height: 800
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('resizes both vertically and horizontally when the window is resized', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({ width: true, height: true });
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 400, height: 400 });
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 400,
|
|
|
|
height: 400
|
|
|
|
});
|
|
|
|
w.setSize(800, 800);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 800,
|
|
|
|
height: 800
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('resizes proportionally', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({ width: true, height: false });
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 200, height: 100 });
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 200,
|
|
|
|
height: 100
|
|
|
|
});
|
|
|
|
w.setSize(800, 400);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 600,
|
|
|
|
height: 100
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not move x if horizontal: false', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({ width: true });
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 200, y: 0, width: 200, height: 100 });
|
|
|
|
w.setSize(800, 400);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 200,
|
|
|
|
y: 0,
|
|
|
|
width: 600,
|
|
|
|
height: 100
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('moves x if horizontal: true', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({ horizontal: true });
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 200, y: 0, width: 200, height: 100 });
|
|
|
|
w.setSize(800, 400);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 400,
|
|
|
|
y: 0,
|
|
|
|
width: 400,
|
|
|
|
height: 100
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('moves x if horizontal: true width: true', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
view.setAutoResize({ horizontal: true, width: true });
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 200, y: 0, width: 200, height: 100 });
|
|
|
|
w.setSize(800, 400);
|
|
|
|
expect(view.getBounds()).to.deep.equal({
|
|
|
|
x: 400,
|
|
|
|
y: 0,
|
|
|
|
width: 400,
|
|
|
|
height: 100
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
|
|
|
});
|
2017-04-12 11:40:31 +00:00
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
describe('BrowserView.setBounds()', () => {
|
|
|
|
it('does not throw for valid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 1, height: 1 });
|
|
|
|
});
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
it('throws for invalid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
2018-06-17 22:56:04 +00:00
|
|
|
expect(() => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view.setBounds(null as any);
|
|
|
|
}).to.throw(/conversion failure/);
|
2018-06-17 22:56:04 +00:00
|
|
|
expect(() => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view.setBounds({} as any);
|
|
|
|
}).to.throw(/conversion failure/);
|
|
|
|
});
|
2023-08-23 13:55:31 +00:00
|
|
|
|
|
|
|
it('can set bounds after view is added to window', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
|
|
|
|
const bounds = { x: 0, y: 0, width: 50, height: 50 };
|
|
|
|
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds(bounds);
|
|
|
|
|
|
|
|
expect(view.getBounds()).to.deep.equal(bounds);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can set bounds before view is added to window', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
|
|
|
|
const bounds = { x: 0, y: 0, width: 50, height: 50 };
|
|
|
|
|
|
|
|
view.setBounds(bounds);
|
|
|
|
w.addBrowserView(view);
|
|
|
|
|
|
|
|
expect(view.getBounds()).to.deep.equal(bounds);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can update bounds', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
w.addBrowserView(view);
|
|
|
|
|
|
|
|
const bounds1 = { x: 0, y: 0, width: 50, height: 50 };
|
|
|
|
view.setBounds(bounds1);
|
|
|
|
expect(view.getBounds()).to.deep.equal(bounds1);
|
|
|
|
|
|
|
|
const bounds2 = { x: 0, y: 150, width: 50, height: 50 };
|
|
|
|
view.setBounds(bounds2);
|
|
|
|
expect(view.getBounds()).to.deep.equal(bounds2);
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2019-07-30 02:43:05 +00:00
|
|
|
describe('BrowserView.getBounds()', () => {
|
2023-07-06 07:50:08 +00:00
|
|
|
it('returns the current bounds', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
const bounds = { x: 10, y: 20, width: 30, height: 40 };
|
|
|
|
view.setBounds(bounds);
|
|
|
|
expect(view.getBounds()).to.deep.equal(bounds);
|
|
|
|
});
|
2023-08-23 13:55:31 +00:00
|
|
|
|
|
|
|
it('does not changer after being added to a window', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
const bounds = { x: 10, y: 20, width: 30, height: 40 };
|
|
|
|
view.setBounds(bounds);
|
|
|
|
expect(view.getBounds()).to.deep.equal(bounds);
|
|
|
|
|
|
|
|
w.addBrowserView(view);
|
|
|
|
expect(view.getBounds()).to.deep.equal(bounds);
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2019-07-30 02:43:05 +00:00
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
describe('BrowserWindow.setBrowserView()', () => {
|
|
|
|
it('does not throw for valid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
w.setBrowserView(view);
|
|
|
|
});
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
it('does not throw if called multiple times with same view', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
w.setBrowserView(view);
|
|
|
|
w.setBrowserView(view);
|
|
|
|
w.setBrowserView(view);
|
|
|
|
});
|
|
|
|
});
|
2017-06-21 23:21:28 +00:00
|
|
|
|
2017-10-27 18:44:41 +00:00
|
|
|
describe('BrowserWindow.getBrowserView()', () => {
|
|
|
|
it('returns the set view', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
w.setBrowserView(view);
|
2018-06-17 22:56:04 +00:00
|
|
|
|
2020-03-20 20:28:31 +00:00
|
|
|
const view2 = w.getBrowserView();
|
|
|
|
expect(view2!.webContents.id).to.equal(view.webContents.id);
|
|
|
|
});
|
2017-10-27 18:44:41 +00:00
|
|
|
|
|
|
|
it('returns null if none is set', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
const view = w.getBrowserView();
|
|
|
|
expect(view).to.be.null('view');
|
|
|
|
});
|
|
|
|
});
|
2017-10-27 18:44:41 +00:00
|
|
|
|
2018-12-22 01:49:26 +00:00
|
|
|
describe('BrowserWindow.addBrowserView()', () => {
|
|
|
|
it('does not throw for valid args', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
const view1 = new BrowserView();
|
2022-12-14 21:07:38 +00:00
|
|
|
defer(() => view1.webContents.destroy());
|
2020-03-20 20:28:31 +00:00
|
|
|
w.addBrowserView(view1);
|
2020-07-09 15:48:39 +00:00
|
|
|
defer(() => w.removeBrowserView(view1));
|
2020-03-20 20:28:31 +00:00
|
|
|
const view2 = new BrowserView();
|
2022-12-14 21:07:38 +00:00
|
|
|
defer(() => view2.webContents.destroy());
|
2020-03-20 20:28:31 +00:00
|
|
|
w.addBrowserView(view2);
|
2020-07-09 15:48:39 +00:00
|
|
|
defer(() => w.removeBrowserView(view2));
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2020-08-26 03:04:13 +00:00
|
|
|
|
2018-12-22 01:49:26 +00:00
|
|
|
it('does not throw if called multiple times with same view', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
w.addBrowserView(view);
|
|
|
|
w.addBrowserView(view);
|
|
|
|
w.addBrowserView(view);
|
|
|
|
});
|
2020-08-26 03:04:13 +00:00
|
|
|
|
2021-11-15 07:24:22 +00:00
|
|
|
it('does not crash if the BrowserView webContents are destroyed prior to window addition', () => {
|
2020-08-26 03:04:13 +00:00
|
|
|
expect(() => {
|
|
|
|
const view1 = new BrowserView();
|
2022-12-14 21:07:38 +00:00
|
|
|
view1.webContents.destroy();
|
2020-08-26 03:04:13 +00:00
|
|
|
w.addBrowserView(view1);
|
|
|
|
}).to.not.throw();
|
|
|
|
});
|
2021-01-05 00:34:22 +00:00
|
|
|
|
2021-11-15 07:24:22 +00:00
|
|
|
it('does not crash if the webContents is destroyed after a URL is loaded', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
expect(async () => {
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 400, height: 300 });
|
|
|
|
await view.webContents.loadURL('data:text/html,hello there');
|
|
|
|
view.webContents.destroy();
|
|
|
|
}).to.not.throw();
|
|
|
|
});
|
|
|
|
|
2021-01-05 00:34:22 +00:00
|
|
|
it('can handle BrowserView reparenting', async () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.webContents.loadURL('about:blank');
|
2023-02-23 23:53:53 +00:00
|
|
|
await once(view.webContents, 'did-finish-load');
|
2021-01-05 00:34:22 +00:00
|
|
|
|
|
|
|
const w2 = new BrowserWindow({ show: false });
|
|
|
|
w2.addBrowserView(view);
|
|
|
|
|
|
|
|
w.close();
|
|
|
|
|
|
|
|
view.webContents.loadURL(`file://${fixtures}/pages/blank.html`);
|
2023-02-23 23:53:53 +00:00
|
|
|
await once(view.webContents, 'did-finish-load');
|
2021-01-05 00:34:22 +00:00
|
|
|
|
|
|
|
// Clean up - the afterEach hook assumes the webContents on w is still alive.
|
|
|
|
w = new BrowserWindow({ show: false });
|
|
|
|
w2.close();
|
|
|
|
w2.destroy();
|
|
|
|
});
|
2023-12-13 21:01:03 +00:00
|
|
|
|
|
|
|
it('does not cause a crash when used for view with destroyed web contents', async () => {
|
|
|
|
const w2 = new BrowserWindow({ show: false });
|
|
|
|
const view = new BrowserView();
|
|
|
|
view.webContents.close();
|
|
|
|
w2.addBrowserView(view);
|
|
|
|
w2.webContents.loadURL('about:blank');
|
|
|
|
await once(w2.webContents, 'did-finish-load');
|
|
|
|
w2.close();
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2018-12-22 01:49:26 +00:00
|
|
|
|
|
|
|
describe('BrowserWindow.removeBrowserView()', () => {
|
|
|
|
it('does not throw if called multiple times with same view', () => {
|
2020-08-26 03:04:13 +00:00
|
|
|
expect(() => {
|
|
|
|
view = new BrowserView();
|
|
|
|
w.addBrowserView(view);
|
|
|
|
w.removeBrowserView(view);
|
|
|
|
w.removeBrowserView(view);
|
|
|
|
}).to.not.throw();
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2023-06-21 19:20:54 +00:00
|
|
|
|
2023-09-04 10:33:29 +00:00
|
|
|
it('can be called on a BrowserView with a destroyed webContents', async () => {
|
2023-06-21 19:20:54 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
w.addBrowserView(view);
|
2023-09-04 10:33:29 +00:00
|
|
|
await view.webContents.loadURL('data:text/html,hello there');
|
|
|
|
const destroyed = once(view.webContents, 'destroyed');
|
|
|
|
view.webContents.close();
|
|
|
|
await destroyed;
|
|
|
|
w.removeBrowserView(view);
|
2023-06-21 19:20:54 +00:00
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2018-12-22 01:49:26 +00:00
|
|
|
|
|
|
|
describe('BrowserWindow.getBrowserViews()', () => {
|
|
|
|
it('returns same views as was added', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
const view1 = new BrowserView();
|
2022-12-14 21:07:38 +00:00
|
|
|
defer(() => view1.webContents.destroy());
|
2020-03-20 20:28:31 +00:00
|
|
|
w.addBrowserView(view1);
|
2020-07-09 15:48:39 +00:00
|
|
|
defer(() => w.removeBrowserView(view1));
|
2020-03-20 20:28:31 +00:00
|
|
|
const view2 = new BrowserView();
|
2022-12-14 21:07:38 +00:00
|
|
|
defer(() => view2.webContents.destroy());
|
2020-03-20 20:28:31 +00:00
|
|
|
w.addBrowserView(view2);
|
2020-07-09 15:48:39 +00:00
|
|
|
defer(() => w.removeBrowserView(view2));
|
2020-03-20 20:28:31 +00:00
|
|
|
|
|
|
|
const views = w.getBrowserViews();
|
|
|
|
expect(views).to.have.lengthOf(2);
|
|
|
|
expect(views[0].webContents.id).to.equal(view1.webContents.id);
|
|
|
|
expect(views[1].webContents.id).to.equal(view2.webContents.id);
|
|
|
|
});
|
2023-07-11 09:01:30 +00:00
|
|
|
|
|
|
|
it('persists ordering by z-index', () => {
|
|
|
|
const view1 = new BrowserView();
|
|
|
|
defer(() => view1.webContents.destroy());
|
|
|
|
w.addBrowserView(view1);
|
|
|
|
defer(() => w.removeBrowserView(view1));
|
|
|
|
const view2 = new BrowserView();
|
|
|
|
defer(() => view2.webContents.destroy());
|
|
|
|
w.addBrowserView(view2);
|
|
|
|
defer(() => w.removeBrowserView(view2));
|
|
|
|
w.setTopBrowserView(view1);
|
|
|
|
|
|
|
|
const views = w.getBrowserViews();
|
|
|
|
expect(views).to.have.lengthOf(2);
|
|
|
|
expect(views[0].webContents.id).to.equal(view2.webContents.id);
|
|
|
|
expect(views[1].webContents.id).to.equal(view1.webContents.id);
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2018-12-22 01:49:26 +00:00
|
|
|
|
2021-02-10 07:23:35 +00:00
|
|
|
describe('BrowserWindow.setTopBrowserView()', () => {
|
|
|
|
it('should throw an error when a BrowserView is not attached to the window', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
expect(() => {
|
|
|
|
w.setTopBrowserView(view);
|
|
|
|
}).to.throw(/is not attached/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw an error when a BrowserView is attached to some other window', () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
|
|
|
|
const win2 = new BrowserWindow();
|
|
|
|
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({ x: 0, y: 0, width: 100, height: 100 });
|
|
|
|
win2.addBrowserView(view);
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
w.setTopBrowserView(view);
|
|
|
|
}).to.throw(/is not attached/);
|
|
|
|
|
|
|
|
win2.close();
|
|
|
|
win2.destroy();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-10-27 00:05:15 +00:00
|
|
|
describe('BrowserView.webContents.getOwnerBrowserWindow()', () => {
|
|
|
|
it('points to owning window', () => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
expect(view.webContents.getOwnerBrowserWindow()).to.be.null('owner browser window');
|
2018-06-17 22:56:04 +00:00
|
|
|
|
2020-03-20 20:28:31 +00:00
|
|
|
w.setBrowserView(view);
|
|
|
|
expect(view.webContents.getOwnerBrowserWindow()).to.equal(w);
|
2018-06-17 22:56:04 +00:00
|
|
|
|
2020-03-20 20:28:31 +00:00
|
|
|
w.setBrowserView(null);
|
|
|
|
expect(view.webContents.getOwnerBrowserWindow()).to.be.null('owner browser window');
|
|
|
|
});
|
|
|
|
});
|
2017-07-24 03:32:30 +00:00
|
|
|
|
2020-07-09 15:48:39 +00:00
|
|
|
describe('shutdown behavior', () => {
|
|
|
|
it('does not crash on exit', async () => {
|
|
|
|
const rc = await startRemoteControlApp();
|
|
|
|
await rc.remotely(() => {
|
|
|
|
const { BrowserView, app } = require('electron');
|
2023-05-25 01:09:17 +00:00
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
new BrowserView({});
|
2020-07-09 15:48:39 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
app.quit();
|
|
|
|
});
|
|
|
|
});
|
2023-02-23 23:53:53 +00:00
|
|
|
const [code] = await once(rc.process, 'exit');
|
2020-07-09 15:48:39 +00:00
|
|
|
expect(code).to.equal(0);
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2018-11-08 15:57:28 +00:00
|
|
|
|
2020-07-09 15:48:39 +00:00
|
|
|
it('does not crash on exit if added to a browser window', async () => {
|
|
|
|
const rc = await startRemoteControlApp();
|
|
|
|
await rc.remotely(() => {
|
|
|
|
const { app, BrowserView, BrowserWindow } = require('electron');
|
|
|
|
const bv = new BrowserView();
|
|
|
|
bv.webContents.loadURL('about:blank');
|
|
|
|
const bw = new BrowserWindow({ show: false });
|
|
|
|
bw.addBrowserView(bv);
|
|
|
|
setTimeout(() => {
|
|
|
|
app.quit();
|
|
|
|
});
|
|
|
|
});
|
2023-02-23 23:53:53 +00:00
|
|
|
const [code] = await once(rc.process, 'exit');
|
2020-03-20 20:28:31 +00:00
|
|
|
expect(code).to.equal(0);
|
|
|
|
});
|
2023-03-01 10:35:06 +00:00
|
|
|
|
|
|
|
it('emits the destroyed event when webContents.close() is called', async () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
w.setBrowserView(view);
|
|
|
|
await view.webContents.loadFile(path.join(fixtures, 'pages', 'a.html'));
|
|
|
|
|
|
|
|
view.webContents.close();
|
|
|
|
await once(view.webContents, 'destroyed');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('emits the destroyed event when window.close() is called', async () => {
|
|
|
|
view = new BrowserView();
|
|
|
|
w.setBrowserView(view);
|
|
|
|
await view.webContents.loadFile(path.join(fixtures, 'pages', 'a.html'));
|
|
|
|
|
|
|
|
view.webContents.executeJavaScript('window.close()');
|
|
|
|
await once(view.webContents, 'destroyed');
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|
2018-11-27 01:39:03 +00:00
|
|
|
|
|
|
|
describe('window.open()', () => {
|
2020-11-10 17:06:03 +00:00
|
|
|
it('works in BrowserView', (done) => {
|
2020-03-20 20:28:31 +00:00
|
|
|
view = new BrowserView();
|
|
|
|
w.setBrowserView(view);
|
2020-11-10 17:06:03 +00:00
|
|
|
view.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
|
|
|
expect(url).to.equal('http://host/');
|
|
|
|
expect(frameName).to.equal('host');
|
|
|
|
done();
|
|
|
|
return { action: 'deny' };
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
view.webContents.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
|
|
|
|
});
|
|
|
|
});
|
2022-03-21 23:38:03 +00:00
|
|
|
|
|
|
|
describe('BrowserView.capturePage(rect)', () => {
|
|
|
|
it('returns a Promise with a Buffer', async () => {
|
|
|
|
view = new BrowserView({
|
|
|
|
webPreferences: {
|
|
|
|
backgroundThrottling: false
|
|
|
|
}
|
|
|
|
});
|
|
|
|
w.addBrowserView(view);
|
|
|
|
view.setBounds({
|
|
|
|
...w.getBounds(),
|
|
|
|
x: 0,
|
|
|
|
y: 0
|
|
|
|
});
|
|
|
|
const image = await view.webContents.capturePage({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 100,
|
|
|
|
height: 100
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(image.isEmpty()).to.equal(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
xit('resolves after the window is hidden and capturer count is non-zero', async () => {
|
|
|
|
view = new BrowserView({
|
|
|
|
webPreferences: {
|
|
|
|
backgroundThrottling: false
|
|
|
|
}
|
|
|
|
});
|
|
|
|
w.setBrowserView(view);
|
|
|
|
view.setBounds({
|
|
|
|
...w.getBounds(),
|
|
|
|
x: 0,
|
|
|
|
y: 0
|
|
|
|
});
|
|
|
|
await view.webContents.loadFile(path.join(fixtures, 'pages', 'a.html'));
|
|
|
|
|
|
|
|
const image = await view.webContents.capturePage();
|
|
|
|
expect(image.isEmpty()).to.equal(false);
|
|
|
|
});
|
|
|
|
});
|
2020-03-20 20:28:31 +00:00
|
|
|
});
|