Browse Source

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
Samuel Maddock 4 years ago
parent
commit
43d27cc4d1

+ 6 - 0
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.

+ 29 - 0
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<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)

+ 2 - 0
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<WebFrameMain>,
   int ProcessID() const;
   int RoutingID() const;
   GURL URL() const;
+  blink::mojom::PageVisibilityState VisibilityState() const;
 
   content::RenderFrameHost* Top() const;
   content::RenderFrameHost* Parent() const;

+ 15 - 0
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 } });

+ 46 - 0
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<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);
+  });
+}