feat: Restore webContents navigation history and page state (#45433)
* feat: Working navigationHistory.restore with just title/url * feat: Restore page state, too * chore: Docs, lint, tests * Implement feedback * More magic * Make _awaitNextLoad truly private * Implement API group feedback * One more round of feedback
This commit is contained in:
parent
6fdfca6e49
commit
9f47c9a051
8 changed files with 259 additions and 10 deletions
|
@ -74,3 +74,22 @@ Returns `boolean` - Whether the navigation entry was removed from the webContent
|
||||||
#### `navigationHistory.getAllEntries()`
|
#### `navigationHistory.getAllEntries()`
|
||||||
|
|
||||||
Returns [`NavigationEntry[]`](structures/navigation-entry.md) - WebContents complete history.
|
Returns [`NavigationEntry[]`](structures/navigation-entry.md) - WebContents complete history.
|
||||||
|
|
||||||
|
#### `navigationHistory.restore(options)`
|
||||||
|
|
||||||
|
Restores navigation history and loads the given entry in the in stack. Will make a best effort
|
||||||
|
to restore not just the navigation stack but also the state of the individual pages - for instance
|
||||||
|
including HTML form values or the scroll position. It's recommended to call this API before any
|
||||||
|
navigation entries are created, so ideally before you call `loadURL()` or `loadFile()` on the
|
||||||
|
`webContents` object.
|
||||||
|
|
||||||
|
This API allows you to create common flows that aim to restore, recreate, or clone other webContents.
|
||||||
|
|
||||||
|
* `options` Object
|
||||||
|
* `entries` [NavigationEntry[]](structures/navigation-entry.md) - Result of a prior `getAllEntries()` call
|
||||||
|
* `index` Integer (optional) - Index of the stack that should be loaded. If you set it to `0`, the webContents will load the first (oldest) entry. If you leave it undefined, Electron will automatically load the last (newest) entry.
|
||||||
|
|
||||||
|
Returns `Promise<void>` - the promise will resolve when the page has finished loading the selected navigation entry
|
||||||
|
(see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects
|
||||||
|
if the page fails to load (see
|
||||||
|
[`did-fail-load`](web-contents.md#event-did-fail-load)). A noop rejection handler is already attached, which avoids unhandled rejection errors.
|
||||||
|
|
|
@ -2,3 +2,6 @@
|
||||||
|
|
||||||
* `url` string
|
* `url` string
|
||||||
* `title` string
|
* `title` string
|
||||||
|
* `pageState` string (optional) - A base64 encoded data string containing Chromium page state
|
||||||
|
including information like the current scroll position or form values. It is committed by
|
||||||
|
Chromium before a navigation event and on a regular interval.
|
||||||
|
|
|
@ -69,8 +69,25 @@ if (navigationHistory.canGoToOffset(2)) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Restoring history
|
||||||
|
|
||||||
|
A common flow is that you want to restore the history of a webContents - for instance to implement an "undo close tab" feature. To do so, you can call `navigationHistory.restore({ index, entries })`. This will restore the webContent's navigation history and the webContents location in said history, meaning that `goBack()` and `goForward()` navigate you through the stack as expected.
|
||||||
|
|
||||||
|
```js @ts-type={navigationHistory:Electron.NavigationHistory}
|
||||||
|
|
||||||
|
const firstWindow = new BrowserWindow()
|
||||||
|
|
||||||
|
// Later, you want a second window to have the same history and navigation position
|
||||||
|
async function restore () {
|
||||||
|
const entries = firstWindow.webContents.navigationHistory.getAllEntries()
|
||||||
|
const index = firstWindow.webContents.navigationHistory.getActiveIndex()
|
||||||
|
|
||||||
|
const secondWindow = new BrowserWindow()
|
||||||
|
await secondWindow.webContents.navigationHistory.restore({ index, entries })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Here's a full example that you can open with Electron Fiddle:
|
Here's a full example that you can open with Electron Fiddle:
|
||||||
|
|
||||||
```fiddle docs/fiddles/features/navigation-history
|
```fiddle docs/fiddles/features/navigation-history
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -8,7 +8,7 @@ import * as deprecate from '@electron/internal/common/deprecate';
|
||||||
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
|
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
|
||||||
|
|
||||||
import { app, ipcMain, session, webFrameMain, dialog } from 'electron/main';
|
import { app, ipcMain, session, webFrameMain, dialog } from 'electron/main';
|
||||||
import type { BrowserWindowConstructorOptions, MessageBoxOptions } from 'electron/main';
|
import type { BrowserWindowConstructorOptions, MessageBoxOptions, NavigationEntry } from 'electron/main';
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
|
@ -343,8 +343,8 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
|
||||||
|
|
||||||
type LoadError = { errorCode: number, errorDescription: string, url: string };
|
type LoadError = { errorCode: number, errorDescription: string, url: string };
|
||||||
|
|
||||||
WebContents.prototype.loadURL = function (url, options) {
|
function _awaitNextLoad (this: Electron.WebContents, navigationUrl: string) {
|
||||||
const p = new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const resolveAndCleanup = () => {
|
const resolveAndCleanup = () => {
|
||||||
removeListeners();
|
removeListeners();
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -402,7 +402,7 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||||
// the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT
|
// the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT
|
||||||
// would be more appropriate.
|
// would be more appropriate.
|
||||||
if (!error) {
|
if (!error) {
|
||||||
error = { errorCode: -2, errorDescription: 'ERR_FAILED', url };
|
error = { errorCode: -2, errorDescription: 'ERR_FAILED', url: navigationUrl };
|
||||||
}
|
}
|
||||||
finishListener();
|
finishListener();
|
||||||
};
|
};
|
||||||
|
@ -426,6 +426,10 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||||
this.on('did-stop-loading', stopLoadingListener);
|
this.on('did-stop-loading', stopLoadingListener);
|
||||||
this.on('destroyed', stopLoadingListener);
|
this.on('destroyed', stopLoadingListener);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
WebContents.prototype.loadURL = function (url, options) {
|
||||||
|
const p = _awaitNextLoad.call(this, url);
|
||||||
// Add a no-op rejection handler to silence the unhandled rejection error.
|
// Add a no-op rejection handler to silence the unhandled rejection error.
|
||||||
p.catch(() => {});
|
p.catch(() => {});
|
||||||
this._loadURL(url, options ?? {});
|
this._loadURL(url, options ?? {});
|
||||||
|
@ -611,7 +615,27 @@ WebContents.prototype._init = function () {
|
||||||
length: this._historyLength.bind(this),
|
length: this._historyLength.bind(this),
|
||||||
getEntryAtIndex: this._getNavigationEntryAtIndex.bind(this),
|
getEntryAtIndex: this._getNavigationEntryAtIndex.bind(this),
|
||||||
removeEntryAtIndex: this._removeNavigationEntryAtIndex.bind(this),
|
removeEntryAtIndex: this._removeNavigationEntryAtIndex.bind(this),
|
||||||
getAllEntries: this._getHistory.bind(this)
|
getAllEntries: this._getHistory.bind(this),
|
||||||
|
restore: ({ index, entries }: { index?: number, entries: NavigationEntry[] }) => {
|
||||||
|
if (index === undefined) {
|
||||||
|
index = entries.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || !entries[index]) {
|
||||||
|
throw new Error('Invalid index. Index must be a positive integer and within the bounds of the entries length.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = _awaitNextLoad.call(this, entries[index].url);
|
||||||
|
p.catch(() => {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._restoreHistory(index, entries);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
writable: false,
|
writable: false,
|
||||||
enumerable: true
|
enumerable: true
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
#include "content/public/browser/keyboard_event_processing_result.h"
|
#include "content/public/browser/keyboard_event_processing_result.h"
|
||||||
#include "content/public/browser/navigation_details.h"
|
#include "content/public/browser/navigation_details.h"
|
||||||
#include "content/public/browser/navigation_entry.h"
|
#include "content/public/browser/navigation_entry.h"
|
||||||
|
#include "content/public/browser/navigation_entry_restore_context.h"
|
||||||
#include "content/public/browser/navigation_handle.h"
|
#include "content/public/browser/navigation_handle.h"
|
||||||
#include "content/public/browser/render_frame_host.h"
|
#include "content/public/browser/render_frame_host.h"
|
||||||
#include "content/public/browser/render_process_host.h"
|
#include "content/public/browser/render_process_host.h"
|
||||||
|
@ -363,14 +364,60 @@ struct Converter<scoped_refptr<content::DevToolsAgentHost>> {
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
struct Converter<content::NavigationEntry*> {
|
struct Converter<content::NavigationEntry*> {
|
||||||
|
static bool FromV8(v8::Isolate* isolate,
|
||||||
|
v8::Local<v8::Value> val,
|
||||||
|
content::NavigationEntry** out) {
|
||||||
|
gin_helper::Dictionary dict;
|
||||||
|
if (!gin::ConvertFromV8(isolate, val, &dict))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::string url_str;
|
||||||
|
std::string title;
|
||||||
|
std::string encoded_page_state;
|
||||||
|
GURL url;
|
||||||
|
|
||||||
|
if (!dict.Get("url", &url) || !dict.Get("title", &title))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto entry = content::NavigationEntry::Create();
|
||||||
|
entry->SetURL(url);
|
||||||
|
entry->SetTitle(base::UTF8ToUTF16(title));
|
||||||
|
|
||||||
|
// Handle optional page state
|
||||||
|
if (dict.Get("pageState", &encoded_page_state)) {
|
||||||
|
std::string decoded_page_state;
|
||||||
|
if (base::Base64Decode(encoded_page_state, &decoded_page_state)) {
|
||||||
|
auto restore_context = content::NavigationEntryRestoreContext::Create();
|
||||||
|
|
||||||
|
auto page_state =
|
||||||
|
blink::PageState::CreateFromEncodedData(decoded_page_state);
|
||||||
|
if (!page_state.IsValid())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
entry->SetPageState(std::move(page_state), restore_context.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = entry.release();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
||||||
content::NavigationEntry* entry) {
|
content::NavigationEntry* entry) {
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return v8::Null(isolate);
|
return v8::Null(isolate);
|
||||||
}
|
}
|
||||||
gin_helper::Dictionary dict(isolate, v8::Object::New(isolate));
|
gin_helper::Dictionary dict = gin_helper::Dictionary::CreateEmpty(isolate);
|
||||||
dict.Set("url", entry->GetURL().spec());
|
dict.Set("url", entry->GetURL().spec());
|
||||||
dict.Set("title", entry->GetTitleForDisplay());
|
dict.Set("title", entry->GetTitleForDisplay());
|
||||||
|
|
||||||
|
// Page state saves scroll position and values of any form fields
|
||||||
|
const blink::PageState& page_state = entry->GetPageState();
|
||||||
|
if (page_state.IsValid()) {
|
||||||
|
std::string encoded_data = base::Base64Encode(page_state.ToEncodedData());
|
||||||
|
dict.Set("pageState", encoded_data);
|
||||||
|
}
|
||||||
|
|
||||||
return dict.GetHandle();
|
return dict.GetHandle();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2572,6 +2619,47 @@ std::vector<content::NavigationEntry*> WebContents::GetHistory() const {
|
||||||
return history;
|
return history;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WebContents::RestoreHistory(
|
||||||
|
v8::Isolate* isolate,
|
||||||
|
gin_helper::ErrorThrower thrower,
|
||||||
|
int index,
|
||||||
|
const std::vector<v8::Local<v8::Value>>& entries) {
|
||||||
|
if (!web_contents()
|
||||||
|
->GetController()
|
||||||
|
.GetLastCommittedEntry()
|
||||||
|
->IsInitialEntry()) {
|
||||||
|
thrower.ThrowError(
|
||||||
|
"Cannot restore history on webContents that have previously loaded "
|
||||||
|
"a page.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto navigation_entries = std::make_unique<
|
||||||
|
std::vector<std::unique_ptr<content::NavigationEntry>>>();
|
||||||
|
|
||||||
|
for (const auto& entry : entries) {
|
||||||
|
content::NavigationEntry* nav_entry = nullptr;
|
||||||
|
if (!gin::Converter<content::NavigationEntry*>::FromV8(isolate, entry,
|
||||||
|
&nav_entry) ||
|
||||||
|
!nav_entry) {
|
||||||
|
// Invalid entry, bail out early
|
||||||
|
thrower.ThrowError(
|
||||||
|
"Failed to restore navigation history: Invalid navigation entry at "
|
||||||
|
"index " +
|
||||||
|
std::to_string(index) + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation_entries->push_back(
|
||||||
|
std::unique_ptr<content::NavigationEntry>(nav_entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!navigation_entries->empty()) {
|
||||||
|
web_contents()->GetController().Restore(
|
||||||
|
index, content::RestoreType::kRestored, navigation_entries.get());
|
||||||
|
web_contents()->GetController().LoadIfNecessary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WebContents::ClearHistory() {
|
void WebContents::ClearHistory() {
|
||||||
// In some rare cases (normally while there is no real history) we are in a
|
// In some rare cases (normally while there is no real history) we are in a
|
||||||
// state where we can't prune navigation entries
|
// state where we can't prune navigation entries
|
||||||
|
@ -4397,6 +4485,7 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
|
||||||
&WebContents::RemoveNavigationEntryAtIndex)
|
&WebContents::RemoveNavigationEntryAtIndex)
|
||||||
.SetMethod("_getHistory", &WebContents::GetHistory)
|
.SetMethod("_getHistory", &WebContents::GetHistory)
|
||||||
.SetMethod("_clearHistory", &WebContents::ClearHistory)
|
.SetMethod("_clearHistory", &WebContents::ClearHistory)
|
||||||
|
.SetMethod("_restoreHistory", &WebContents::RestoreHistory)
|
||||||
.SetMethod("isCrashed", &WebContents::IsCrashed)
|
.SetMethod("isCrashed", &WebContents::IsCrashed)
|
||||||
.SetMethod("forcefullyCrashRenderer",
|
.SetMethod("forcefullyCrashRenderer",
|
||||||
&WebContents::ForcefullyCrashRenderer)
|
&WebContents::ForcefullyCrashRenderer)
|
||||||
|
|
|
@ -219,6 +219,10 @@ class WebContents final : public ExclusiveAccessContext,
|
||||||
bool RemoveNavigationEntryAtIndex(int index);
|
bool RemoveNavigationEntryAtIndex(int index);
|
||||||
std::vector<content::NavigationEntry*> GetHistory() const;
|
std::vector<content::NavigationEntry*> GetHistory() const;
|
||||||
void ClearHistory();
|
void ClearHistory();
|
||||||
|
void RestoreHistory(v8::Isolate* isolate,
|
||||||
|
gin_helper::ErrorThrower thrower,
|
||||||
|
int index,
|
||||||
|
const std::vector<v8::Local<v8::Value>>& entries);
|
||||||
int GetHistoryLength() const;
|
int GetHistoryLength() const;
|
||||||
const std::string GetWebRTCIPHandlingPolicy() const;
|
const std::string GetWebRTCIPHandlingPolicy() const;
|
||||||
void SetWebRTCIPHandlingPolicy(const std::string& webrtc_ip_handling_policy);
|
void SetWebRTCIPHandlingPolicy(const std::string& webrtc_ip_handling_policy);
|
||||||
|
|
|
@ -699,12 +699,14 @@ describe('webContents module', () => {
|
||||||
describe('navigationHistory.getEntryAtIndex(index) API ', () => {
|
describe('navigationHistory.getEntryAtIndex(index) API ', () => {
|
||||||
it('should fetch default navigation entry when no urls are loaded', async () => {
|
it('should fetch default navigation entry when no urls are loaded', async () => {
|
||||||
const result = w.webContents.navigationHistory.getEntryAtIndex(0);
|
const result = w.webContents.navigationHistory.getEntryAtIndex(0);
|
||||||
expect(result).to.deep.equal({ url: '', title: '' });
|
expect(result.url).to.equal('');
|
||||||
|
expect(result.title).to.equal('');
|
||||||
});
|
});
|
||||||
it('should fetch navigation entry given a valid index', async () => {
|
it('should fetch navigation entry given a valid index', async () => {
|
||||||
await w.loadURL(urlPage1);
|
await w.loadURL(urlPage1);
|
||||||
const result = w.webContents.navigationHistory.getEntryAtIndex(0);
|
const result = w.webContents.navigationHistory.getEntryAtIndex(0);
|
||||||
expect(result).to.deep.equal({ url: urlPage1, title: 'Page 1' });
|
expect(result.url).to.equal(urlPage1);
|
||||||
|
expect(result.title).to.equal('Page 1');
|
||||||
});
|
});
|
||||||
it('should return null given an invalid index larger than history length', async () => {
|
it('should return null given an invalid index larger than history length', async () => {
|
||||||
await w.loadURL(urlPage1);
|
await w.loadURL(urlPage1);
|
||||||
|
@ -763,7 +765,10 @@ describe('webContents module', () => {
|
||||||
await w.loadURL(urlPage1);
|
await w.loadURL(urlPage1);
|
||||||
await w.loadURL(urlPage2);
|
await w.loadURL(urlPage2);
|
||||||
await w.loadURL(urlPage3);
|
await w.loadURL(urlPage3);
|
||||||
const entries = w.webContents.navigationHistory.getAllEntries();
|
const entries = w.webContents.navigationHistory.getAllEntries().map(entry => ({
|
||||||
|
url: entry.url,
|
||||||
|
title: entry.title
|
||||||
|
}));
|
||||||
expect(entries.length).to.equal(3);
|
expect(entries.length).to.equal(3);
|
||||||
expect(entries[0]).to.deep.equal({ url: urlPage1, title: 'Page 1' });
|
expect(entries[0]).to.deep.equal({ url: urlPage1, title: 'Page 1' });
|
||||||
expect(entries[1]).to.deep.equal({ url: urlPage2, title: 'Page 2' });
|
expect(entries[1]).to.deep.equal({ url: urlPage2, title: 'Page 2' });
|
||||||
|
@ -774,6 +779,92 @@ describe('webContents module', () => {
|
||||||
const entries = w.webContents.navigationHistory.getAllEntries();
|
const entries = w.webContents.navigationHistory.getAllEntries();
|
||||||
expect(entries.length).to.equal(0);
|
expect(entries.length).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create a NavigationEntry with PageState that can be serialized/deserialized with JSON', async () => {
|
||||||
|
await w.loadURL(urlPage1);
|
||||||
|
await w.loadURL(urlPage2);
|
||||||
|
await w.loadURL(urlPage3);
|
||||||
|
|
||||||
|
const entries = w.webContents.navigationHistory.getAllEntries();
|
||||||
|
const serialized = JSON.stringify(entries);
|
||||||
|
const deserialized = JSON.parse(serialized);
|
||||||
|
expect(deserialized).to.deep.equal(entries);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('navigationHistory.restore({ index, entries }) API', () => {
|
||||||
|
let server: http.Server;
|
||||||
|
let serverUrl: string;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
server = http.createServer((req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
res.end('<html><head><title>Form</title></head><body><form><input type="text" value="value" /></form></body></html>');
|
||||||
|
});
|
||||||
|
serverUrl = (await listen(server)).url;
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
if (server) await new Promise(resolve => server.close(resolve));
|
||||||
|
server = null as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore navigation history with PageState', async () => {
|
||||||
|
await w.loadURL(urlPage1);
|
||||||
|
await w.loadURL(urlPage2);
|
||||||
|
await w.loadURL(serverUrl);
|
||||||
|
|
||||||
|
// Fill out the form on the page
|
||||||
|
await w.webContents.executeJavaScript('document.querySelector("input").value = "Hi!";');
|
||||||
|
|
||||||
|
// PageState is committed:
|
||||||
|
// 1) When the page receives an unload event
|
||||||
|
// 2) During periodic serialization of page state
|
||||||
|
// To not wait randomly for the second option, we'll trigger another load
|
||||||
|
await w.loadURL(urlPage3);
|
||||||
|
|
||||||
|
// Save the navigation state
|
||||||
|
const entries = w.webContents.navigationHistory.getAllEntries();
|
||||||
|
|
||||||
|
// Close the window, make a new one
|
||||||
|
w.close();
|
||||||
|
w = new BrowserWindow();
|
||||||
|
|
||||||
|
const formValue = await new Promise<string>(resolve => {
|
||||||
|
w.webContents.once('dom-ready', () => resolve(w.webContents.executeJavaScript('document.querySelector("input").value')));
|
||||||
|
|
||||||
|
// Restore the navigation history
|
||||||
|
return w.webContents.navigationHistory.restore({ index: 2, entries });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(formValue).to.equal('Hi!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid base64 pageState', async () => {
|
||||||
|
await w.loadURL(urlPage1);
|
||||||
|
await w.loadURL(urlPage2);
|
||||||
|
await w.loadURL(urlPage3);
|
||||||
|
|
||||||
|
const brokenEntries = w.webContents.navigationHistory.getAllEntries().map(entry => ({
|
||||||
|
...entry,
|
||||||
|
pageState: 'invalid base64'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Close the window, make a new one
|
||||||
|
w.close();
|
||||||
|
w = new BrowserWindow();
|
||||||
|
await w.webContents.navigationHistory.restore({ index: 2, entries: brokenEntries });
|
||||||
|
|
||||||
|
const entries = w.webContents.navigationHistory.getAllEntries();
|
||||||
|
|
||||||
|
// Check that we used the original url and titles but threw away the broken
|
||||||
|
// pageState
|
||||||
|
entries.forEach((entry, index) => {
|
||||||
|
expect(entry.url).to.equal(brokenEntries[index].url);
|
||||||
|
expect(entry.title).to.equal(brokenEntries[index].title);
|
||||||
|
expect(entry.pageState?.length).to.be.greaterThanOrEqual(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
2
typings/internal-electron.d.ts
vendored
2
typings/internal-electron.d.ts
vendored
|
@ -87,6 +87,7 @@ declare namespace Electron {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebContents {
|
interface WebContents {
|
||||||
|
_awaitNextLoad(expectedUrl: string): Promise<void>;
|
||||||
_loadURL(url: string, options: ElectronInternal.LoadURLOptions): void;
|
_loadURL(url: string, options: ElectronInternal.LoadURLOptions): void;
|
||||||
getOwnerBrowserWindow(): Electron.BrowserWindow | null;
|
getOwnerBrowserWindow(): Electron.BrowserWindow | null;
|
||||||
getLastWebPreferences(): Electron.WebPreferences | null;
|
getLastWebPreferences(): Electron.WebPreferences | null;
|
||||||
|
@ -115,6 +116,7 @@ declare namespace Electron {
|
||||||
_goToIndex(index: number): void;
|
_goToIndex(index: number): void;
|
||||||
_removeNavigationEntryAtIndex(index: number): boolean;
|
_removeNavigationEntryAtIndex(index: number): boolean;
|
||||||
_getHistory(): Electron.NavigationEntry[];
|
_getHistory(): Electron.NavigationEntry[];
|
||||||
|
_restoreHistory(index: number, entries: Electron.NavigationEntry[]): void
|
||||||
_clearHistory():void
|
_clearHistory():void
|
||||||
canGoToIndex(index: number): boolean;
|
canGoToIndex(index: number): boolean;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue