feat: add WebFrameMain.visibilityState (#28706)

* feat: add WebFrameMain.visibilityState

* docs: mention other page visibility APIs

* test: delay visibilityState check after hiding

* test: add waitForTrue to avoid flaky visibilityState test

* refactor: waitForTrue -> waitUntil
This commit is contained in:
Samuel Maddock 2021-04-22 12:00:58 -04:00 committed by GitHub
parent 93311c8686
commit 43d27cc4d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 0 deletions

View file

@ -182,3 +182,9 @@ This is not the same as the OS process ID; to read that use `frame.osProcessId`.
An `Integer` representing the unique frame id in the current renderer process.
Distinct `WebFrameMain` instances that refer to the same underlying frame will
have the same `routingId`.
#### `frame.visibilityState` _Readonly_
A `string` representing the [visibility state](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState) of the frame.
See also how the [Page Visibility API](browser-window.md#page-visibility) is affected by other Electron APIs.

View file

@ -30,6 +30,28 @@
#include "shell/common/node_includes.h"
#include "shell/common/v8_value_serializer.h"
namespace gin {
template <>
struct Converter<blink::mojom::PageVisibilityState> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
blink::mojom::PageVisibilityState val) {
std::string visibility;
switch (val) {
case blink::mojom::PageVisibilityState::kVisible:
visibility = "visible";
break;
case blink::mojom::PageVisibilityState::kHidden:
case blink::mojom::PageVisibilityState::kHiddenButPainting:
visibility = "hidden";
break;
}
return gin::ConvertToV8(isolate, visibility);
}
};
} // namespace gin
namespace electron {
namespace api {
@ -228,6 +250,12 @@ GURL WebFrameMain::URL() const {
return render_frame_->GetLastCommittedURL();
}
blink::mojom::PageVisibilityState WebFrameMain::VisibilityState() const {
if (!CheckRenderFrame())
return blink::mojom::PageVisibilityState::kHidden;
return render_frame_->GetVisibilityState();
}
content::RenderFrameHost* WebFrameMain::Top() const {
if (!CheckRenderFrame())
return nullptr;
@ -331,6 +359,7 @@ v8::Local<v8::ObjectTemplate> WebFrameMain::FillObjectTemplate(
.SetProperty("processId", &WebFrameMain::ProcessID)
.SetProperty("routingId", &WebFrameMain::RoutingID)
.SetProperty("url", &WebFrameMain::URL)
.SetProperty("visibilityState", &WebFrameMain::VisibilityState)
.SetProperty("top", &WebFrameMain::Top)
.SetProperty("parent", &WebFrameMain::Parent)
.SetProperty("frames", &WebFrameMain::Frames)

View file

@ -15,6 +15,7 @@
#include "gin/wrappable.h"
#include "shell/common/gin_helper/constructible.h"
#include "shell/common/gin_helper/pinnable.h"
#include "third_party/blink/public/mojom/page/page_visibility_state.mojom-forward.h"
class GURL;
@ -95,6 +96,7 @@ class WebFrameMain : public gin::Wrappable<WebFrameMain>,
int ProcessID() const;
int RoutingID() const;
GURL URL() const;
blink::mojom::PageVisibilityState VisibilityState() const;
content::RenderFrameHost* Top() const;
content::RenderFrameHost* Parent() const;

View file

@ -6,6 +6,7 @@ import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain } from 'electron/mai
import { closeAllWindows } from './window-helpers';
import { emittedOnce, emittedNTimes } from './events-helpers';
import { AddressInfo } from 'net';
import { waitUntil } from './spec-helpers';
describe('webFrameMain module', () => {
const fixtures = path.resolve(__dirname, '..', 'spec-main', 'fixtures');
@ -135,6 +136,20 @@ describe('webFrameMain module', () => {
});
});
describe('WebFrame.visibilityState', () => {
it('should match window state', async () => {
const w = new BrowserWindow({ show: true });
await w.loadURL('about:blank');
const webFrame = w.webContents.mainFrame;
expect(webFrame.visibilityState).to.equal('visible');
w.hide();
await expect(
waitUntil(() => webFrame.visibilityState === 'hidden')
).to.eventually.be.fulfilled();
});
});
describe('WebFrame.executeJavaScript', () => {
it('can inject code into any subframe', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });

View file

@ -86,3 +86,49 @@ export async function startRemoteControlApp () {
defer(() => { appProcess.kill('SIGINT'); });
return new RemoteControlApp(appProcess, port);
}
export function waitUntil (
callback: () => boolean,
opts: { rate?: number, timeout?: number } = {}
) {
const { rate = 10, timeout = 10000 } = opts;
return new Promise<void>((resolve, reject) => {
let intervalId: NodeJS.Timeout | undefined; // eslint-disable-line prefer-const
let timeoutId: NodeJS.Timeout | undefined;
const cleanup = () => {
if (intervalId) clearInterval(intervalId);
if (timeoutId) clearTimeout(timeoutId);
};
const check = () => {
let result;
try {
result = callback();
} catch (e) {
cleanup();
reject(e);
return;
}
if (result === true) {
cleanup();
resolve();
return true;
}
};
if (check()) {
return;
}
intervalId = setInterval(check, rate);
timeoutId = setTimeout(() => {
timeoutId = undefined;
cleanup();
reject(new Error(`waitUntil timed out after ${timeout}ms`));
}, timeout);
});
}