Browse Source

feat: extend navigationHistory API (#42014)

* feat: extend navigationHistory API

* refactor: simplify index checking

* refactor: rename 'getHistory' and 'replaceHistory' methods of navigationHistory

* refactor: rename delete*() methods to remove*()

* feat: remove navigationHistory.replaceHistory()

* tests: add tests for removeEntryAtIndex and getAllEntries
Vít Černý 8 months ago
parent
commit
189675575c

+ 13 - 4
docs/api/navigation-history.md

@@ -35,10 +35,7 @@ Returns `Integer` - The index of the current page, from which we would go back/f
 
 * `index` Integer
 
-Returns `Object`:
-
-* `url` string - The URL of the navigation entry at the given index.
-* `title` string - The page title of the navigation entry at the given index.
+Returns [`NavigationEntry`](structures/navigation-entry.md) - Navigation entry at the given index.
 
 If index is out of bounds (greater than history length or less than 0), null will be returned.
 
@@ -65,3 +62,15 @@ Navigates to the specified offset from the current entry.
 #### `navigationHistory.length()`
 
 Returns `Integer` - History length.
+
+#### `navigationHistory.removeEntryAtIndex(index)`
+
+* `index` Integer
+
+Removes the navigation entry at the given index. Can't remove entry at the "current active index".
+
+Returns `boolean` - Whether the navigation entry was removed from the webContents history.
+
+#### `navigationHistory.getAllEntries()`
+
+Returns [`NavigationEntry[]`](structures/navigation-entry.md) - WebContents complete history.

+ 4 - 0
docs/api/structures/navigation-entry.md

@@ -0,0 +1,4 @@
+# NavigationEntry Object
+
+* `url` string
+* `title` string

+ 1 - 0
filenames.auto.gni

@@ -105,6 +105,7 @@ auto_filenames = {
     "docs/api/structures/mime-typed-buffer.md",
     "docs/api/structures/mouse-input-event.md",
     "docs/api/structures/mouse-wheel-input-event.md",
+    "docs/api/structures/navigation-entry.md",
     "docs/api/structures/notification-action.md",
     "docs/api/structures/notification-response.md",
     "docs/api/structures/open-external-permission-request.md",

+ 3 - 1
lib/browser/api/web-contents.ts

@@ -595,7 +595,9 @@ WebContents.prototype._init = function () {
       goToOffset: this._goToOffset.bind(this),
       getActiveIndex: this._getActiveIndex.bind(this),
       length: this._historyLength.bind(this),
-      getEntryAtIndex: this._getNavigationEntryAtIndex.bind(this)
+      getEntryAtIndex: this._getNavigationEntryAtIndex.bind(this),
+      removeEntryAtIndex: this._removeNavigationEntryAtIndex.bind(this),
+      getAllEntries: this._getHistory.bind(this)
     },
     writable: false,
     enumerable: true

+ 28 - 0
shell/browser/api/electron_api_web_contents.cc

@@ -2496,6 +2496,31 @@ content::NavigationEntry* WebContents::GetNavigationEntryAtIndex(
   return web_contents()->GetController().GetEntryAtIndex(index);
 }
 
+bool WebContents::RemoveNavigationEntryAtIndex(int index) {
+  if (!CanGoToIndex(index))
+    return false;
+
+  return web_contents()->GetController().RemoveEntryAtIndex(index);
+}
+
+std::vector<content::NavigationEntry*> WebContents::GetHistory() const {
+  const int history_length = GetHistoryLength();
+  auto& controller = web_contents()->GetController();
+
+  // If the history is empty, it contains only one entry and that is
+  // "InitialEntry"
+  if (history_length == 1 && controller.GetEntryAtIndex(0)->IsInitialEntry())
+    return std::vector<content::NavigationEntry*>();
+
+  std::vector<content::NavigationEntry*> history;
+  history.reserve(history_length);
+
+  for (int i = 0; i < history_length; i++)
+    history.push_back(controller.GetEntryAtIndex(i));
+
+  return history;
+}
+
 void WebContents::ClearHistory() {
   // In some rare cases (normally while there is no real history) we are in a
   // state where we can't prune navigation entries
@@ -4295,6 +4320,9 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
       .SetMethod("_getNavigationEntryAtIndex",
                  &WebContents::GetNavigationEntryAtIndex)
       .SetMethod("_historyLength", &WebContents::GetHistoryLength)
+      .SetMethod("_removeNavigationEntryAtIndex",
+                 &WebContents::RemoveNavigationEntryAtIndex)
+      .SetMethod("_getHistory", &WebContents::GetHistory)
       .SetMethod("_clearHistory", &WebContents::ClearHistory)
       .SetMethod("isCrashed", &WebContents::IsCrashed)
       .SetMethod("forcefullyCrashRenderer",

+ 2 - 0
shell/browser/api/electron_api_web_contents.h

@@ -204,6 +204,8 @@ class WebContents : public ExclusiveAccessContext,
   void GoToIndex(int index);
   int GetActiveIndex() const;
   content::NavigationEntry* GetNavigationEntryAtIndex(int index) const;
+  bool RemoveNavigationEntryAtIndex(int index);
+  std::vector<content::NavigationEntry*> GetHistory() const;
   void ClearHistory();
   int GetHistoryLength() const;
   const std::string GetWebRTCIPHandlingPolicy() const;

+ 51 - 0
spec/api-web-contents-spec.ts

@@ -567,6 +567,39 @@ describe('webContents module', () => {
       w = new BrowserWindow({ show: false });
     });
     afterEach(closeAllWindows);
+    describe('navigationHistory.removeEntryAtIndex(index) API', () => {
+      it('should remove a navigation entry given a valid index', async () => {
+        await w.loadURL(urlPage1);
+        await w.loadURL(urlPage2);
+        await w.loadURL(urlPage3);
+        const initialLength = w.webContents.navigationHistory.length();
+        const wasRemoved = w.webContents.navigationHistory.removeEntryAtIndex(1); // Attempt to remove the second entry
+        const newLength = w.webContents.navigationHistory.length();
+        expect(wasRemoved).to.be.true();
+        expect(newLength).to.equal(initialLength - 1);
+      });
+
+      it('should not remove the current active navigation entry', async () => {
+        await w.loadURL(urlPage1);
+        await w.loadURL(urlPage2);
+        const activeIndex = w.webContents.navigationHistory.getActiveIndex();
+        const wasRemoved = w.webContents.navigationHistory.removeEntryAtIndex(activeIndex);
+        expect(wasRemoved).to.be.false();
+      });
+
+      it('should return false given an invalid index larger than history length', async () => {
+        await w.loadURL(urlPage1);
+        const wasRemoved = w.webContents.navigationHistory.removeEntryAtIndex(5); // Index larger than history length
+        expect(wasRemoved).to.be.false();
+      });
+
+      it('should return false given an invalid negative index', async () => {
+        await w.loadURL(urlPage1);
+        const wasRemoved = w.webContents.navigationHistory.removeEntryAtIndex(-1); // Negative index
+        expect(wasRemoved).to.be.false();
+      });
+    });
+
     describe('navigationHistory.canGoBack and navigationHistory.goBack API', () => {
       it('should not be able to go back if history is empty', async () => {
         expect(w.webContents.navigationHistory.canGoBack()).to.be.false();
@@ -706,6 +739,24 @@ describe('webContents module', () => {
         expect(w.webContents.navigationHistory.length()).to.equal(1);
       });
     });
+
+    describe('navigationHistory.getAllEntries() API', () => {
+      it('should return all navigation entries as an array of NavigationEntry objects', async () => {
+        await w.loadURL(urlPage1);
+        await w.loadURL(urlPage2);
+        await w.loadURL(urlPage3);
+        const entries = w.webContents.navigationHistory.getAllEntries();
+        expect(entries.length).to.equal(3);
+        expect(entries[0]).to.deep.equal({ url: urlPage1, title: 'Page 1' });
+        expect(entries[1]).to.deep.equal({ url: urlPage2, title: 'Page 2' });
+        expect(entries[2]).to.deep.equal({ url: urlPage3, title: 'Page 3' });
+      });
+
+      it('should return an empty array when there is no navigation history', async () => {
+        const entries = w.webContents.navigationHistory.getAllEntries();
+        expect(entries.length).to.equal(0);
+      });
+    });
   });
 
   describe('getFocusedWebContents() API', () => {

+ 3 - 1
typings/internal-electron.d.ts

@@ -87,7 +87,7 @@ declare namespace Electron {
     _print(options: any, callback?: (success: boolean, failureReason: string) => void): void;
     _getPrintersAsync(): Promise<Electron.PrinterInfo[]>;
     _init(): void;
-    _getNavigationEntryAtIndex(index: number): Electron.EntryAtIndex | null;
+    _getNavigationEntryAtIndex(index: number): Electron.NavigationEntry | null;
     _getActiveIndex(): number;
     _historyLength(): number;
     _canGoBack(): boolean;
@@ -97,6 +97,8 @@ declare namespace Electron {
     _goForward(): void;
     _goToOffset(index: number): void;
     _goToIndex(index: number): void;
+    _removeNavigationEntryAtIndex(index: number): boolean;
+    _getHistory(): Electron.NavigationEntry[];
     _clearHistory():void
     canGoToIndex(index: number): boolean;
     destroy(): void;