diff --git a/docs/api/web-frame-main.md b/docs/api/web-frame-main.md index c349321a232..b0460c1713b 100644 --- a/docs/api/web-frame-main.md +++ b/docs/api/web-frame-main.md @@ -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. diff --git a/shell/browser/api/electron_api_web_frame_main.cc b/shell/browser/api/electron_api_web_frame_main.cc index 9ef45ea185e..f650abc8354 100644 --- a/shell/browser/api/electron_api_web_frame_main.cc +++ b/shell/browser/api/electron_api_web_frame_main.cc @@ -30,6 +30,28 @@ #include "shell/common/node_includes.h" #include "shell/common/v8_value_serializer.h" +namespace gin { + +template <> +struct Converter { + static v8::Local 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 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) diff --git a/shell/browser/api/electron_api_web_frame_main.h b/shell/browser/api/electron_api_web_frame_main.h index 4d0dc989055..0291c3284ad 100644 --- a/shell/browser/api/electron_api_web_frame_main.h +++ b/shell/browser/api/electron_api_web_frame_main.h @@ -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, int ProcessID() const; int RoutingID() const; GURL URL() const; + blink::mojom::PageVisibilityState VisibilityState() const; content::RenderFrameHost* Top() const; content::RenderFrameHost* Parent() const; diff --git a/spec-main/api-web-frame-main-spec.ts b/spec-main/api-web-frame-main-spec.ts index fa178f3a201..275d992a5ae 100644 --- a/spec-main/api-web-frame-main-spec.ts +++ b/spec-main/api-web-frame-main-spec.ts @@ -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 } }); diff --git a/spec-main/spec-helpers.ts b/spec-main/spec-helpers.ts index 04dc0abcc31..6bcf0949066 100644 --- a/spec-main/spec-helpers.ts +++ b/spec-main/spec-helpers.ts @@ -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((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); + }); +}