Skip to content

Commit

Permalink
feat: add navigationHistory.getEntryAtIndex(int index) method (#41577)
Browse files Browse the repository at this point in the history
  • Loading branch information
alicelovescake committed Mar 21, 2024
1 parent 1036d82 commit 00e3445
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 9 deletions.
29 changes: 29 additions & 0 deletions docs/api/navigation-history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Class: NavigationHistory

> Manage a list of navigation entries, representing the user's browsing history within the application.
Process: [Main](../glossary.md#main-process)<br />
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._

Each navigation entry corresponds to a specific page. The indexing system follows a sequential order, where the first available navigation entry is at index 0, representing the earliest visited page, and the latest navigation entry is at index N, representing the most recent page. Maintaining this ordered list of navigation entries enables seamless navigation both backward and forward through the user's browsing history.

### Instance Methods

#### `navigationHistory.getActiveIndex()`

Returns `Integer` - The index of the current page, from which we would go back/forward or reload.

#### `navigationHistory.getEntryAtIndex(index)`

* `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.

If index is out of bounds (greater than history length or less than 0), null will be returned.

#### `navigationHistory.length()`

Returns `Integer` - History length.
4 changes: 4 additions & 0 deletions docs/api/web-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -2223,6 +2223,10 @@ A `Integer` representing the unique ID of this WebContents. Each ID is unique am

A [`Session`](session.md) used by this webContents.

#### `contents.navigationHistory` _Readonly_

A [`NavigationHistory`](navigation-history.md) used by this webContents.

#### `contents.hostWebContents` _Readonly_

A [`WebContents`](web-contents.md) instance that might own this `WebContents`.
Expand Down
1 change: 1 addition & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ auto_filenames = {
"docs/api/message-port-main.md",
"docs/api/native-image.md",
"docs/api/native-theme.md",
"docs/api/navigation-history.md",
"docs/api/net-log.md",
"docs/api/net.md",
"docs/api/notification.md",
Expand Down
11 changes: 11 additions & 0 deletions lib/browser/api/web-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,17 @@ WebContents.prototype._init = function () {
enumerable: true
});

// Add navigationHistory property which handles session history,
// maintaining a list of navigation entries for backward and forward navigation.
Object.defineProperty(this, 'navigationHistory', {
value: {
getActiveIndex: this._getActiveIndex.bind(this),
length: this._historyLength.bind(this),
getEntryAtIndex: this._getNavigationEntryAtIndex.bind(this)
},
writable: false
});

// Dispatch IPC messages to the ipc module.
this.on('-ipc-message' as any, function (this: Electron.WebContents, event: Electron.IpcMainEvent, internal: boolean, channel: string, args: any[]) {
addSenderToEvent(event, this);
Expand Down
25 changes: 23 additions & 2 deletions shell/browser/api/electron_api_web_contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,20 @@ struct Converter<scoped_refptr<content::DevToolsAgentHost>> {
}
};

template <>
struct Converter<content::NavigationEntry*> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
content::NavigationEntry* entry) {
if (!entry) {
return v8::Null(isolate);
}
gin_helper::Dictionary dict(isolate, v8::Object::New(isolate));
dict.Set("url", entry->GetURL().spec());
dict.Set("title", entry->GetTitleForDisplay());
return dict.GetHandle();
}
};

} // namespace gin

namespace electron::api {
Expand Down Expand Up @@ -2560,6 +2574,11 @@ int WebContents::GetActiveIndex() const {
return web_contents()->GetController().GetCurrentEntryIndex();
}

content::NavigationEntry* WebContents::GetNavigationEntryAtIndex(
int index) const {
return web_contents()->GetController().GetEntryAtIndex(index);
}

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
Expand Down Expand Up @@ -4353,9 +4372,11 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("goToOffset", &WebContents::GoToOffset)
.SetMethod("canGoToIndex", &WebContents::CanGoToIndex)
.SetMethod("goToIndex", &WebContents::GoToIndex)
.SetMethod("getActiveIndex", &WebContents::GetActiveIndex)
.SetMethod("_getActiveIndex", &WebContents::GetActiveIndex)
.SetMethod("_getNavigationEntryAtIndex",
&WebContents::GetNavigationEntryAtIndex)
.SetMethod("_historyLength", &WebContents::GetHistoryLength)
.SetMethod("clearHistory", &WebContents::ClearHistory)
.SetMethod("length", &WebContents::GetHistoryLength)
.SetMethod("isCrashed", &WebContents::IsCrashed)
.SetMethod("forcefullyCrashRenderer",
&WebContents::ForcefullyCrashRenderer)
Expand Down
1 change: 1 addition & 0 deletions shell/browser/api/electron_api_web_contents.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class WebContents : public ExclusiveAccessContext,
bool CanGoToIndex(int index) const;
void GoToIndex(int index);
int GetActiveIndex() const;
content::NavigationEntry* GetNavigationEntryAtIndex(int index) const;
void ClearHistory();
int GetHistoryLength() const;
const std::string GetWebRTCIPHandlingPolicy() const;
Expand Down
88 changes: 88 additions & 0 deletions spec/api-web-contents-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,94 @@ describe('webContents module', () => {
});
});

describe('navigationHistory', () => {
let w: BrowserWindow;

beforeEach(async () => {
w = new BrowserWindow({ show: false });
});
afterEach(closeAllWindows);
describe('navigationHistory.getEntryAtIndex(index) API ', () => {
it('should fetch default navigation entry when no urls are loaded', async () => {
const result = w.webContents.navigationHistory.getEntryAtIndex(0);
expect(result).to.deep.equal({ url: '', title: '' });
});
it('should fetch navigation entry given a valid index', async () => {
await w.loadURL('https://www.google.com');
w.webContents.on('did-finish-load', async () => {
const result = w.webContents.navigationHistory.getEntryAtIndex(0);
expect(result).to.deep.equal({ url: 'https://www.google.com/', title: 'Google' });
});
});
it('should return null given an invalid index larger than history length', async () => {
await w.loadURL('https://www.google.com');
w.webContents.on('did-finish-load', async () => {
const result = w.webContents.navigationHistory.getEntryAtIndex(5);
expect(result).to.be.null();
});
});
it('should return null given an invalid negative index', async () => {
await w.loadURL('https://www.google.com');
w.webContents.on('did-finish-load', async () => {
const result = w.webContents.navigationHistory.getEntryAtIndex(-1);
expect(result).to.be.null();
});
});
});

describe('navigationHistory.getActiveIndex() API', () => {
it('should return valid active index after a single page visit', async () => {
await w.loadURL('https://www.google.com');
w.webContents.on('did-finish-load', async () => {
expect(w.webContents.navigationHistory.getActiveIndex()).to.equal(0);
});
});

it('should return valid active index after a multiple page visits', async () => {
const loadPromise = once(w.webContents, 'did-finish-load');
await w.loadURL('https://www.github.com');
await loadPromise;
await w.loadURL('https://www.google.com');
await loadPromise;
await w.loadURL('about:blank');
await loadPromise;

expect(w.webContents.navigationHistory.getActiveIndex()).to.equal(2);
});

it('should return valid active index given no page visits', async () => {
expect(w.webContents.navigationHistory.getActiveIndex()).to.equal(0);
});
});

describe('navigationHistory.length() API', () => {
it('should return valid history length after a single page visit', async () => {
await w.loadURL('https://www.google.com');
w.webContents.on('did-finish-load', async () => {
expect(w.webContents.navigationHistory.length()).to.equal(1);
});
});

it('should return valid history length after a multiple page visits', async () => {
const loadPromise = once(w.webContents, 'did-finish-load');
await w.loadURL('https://www.github.com');
await loadPromise;
await w.loadURL('https://www.google.com');
await loadPromise;
await w.loadURL('about:blank');
await loadPromise;

expect(w.webContents.navigationHistory.length()).to.equal(3);
});

it('should return valid history length given no page visits', async () => {
// Note: Even if no navigation has committed, the history list will always start with an initial navigation entry
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:ceontent/public/browser/navigation_controller.h;l=381
expect(w.webContents.navigationHistory.length()).to.equal(1);
});
});
});

describe('getFocusedWebContents() API', () => {
afterEach(closeAllWindows);

Expand Down
6 changes: 3 additions & 3 deletions spec/chromium-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2068,13 +2068,13 @@ describe('chromium features', () => {
const w = new BrowserWindow({ show: false });
await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
// History should have current page by now.
expect(w.webContents.length()).to.equal(1);
expect(w.webContents.navigationHistory.length()).to.equal(1);

const waitCommit = once(w.webContents, 'navigation-entry-committed');
w.webContents.executeJavaScript('window.history.pushState({}, "")');
await waitCommit;
// Initial page + pushed state.
expect(w.webContents.length()).to.equal(2);
expect(w.webContents.navigationHistory.length()).to.equal(2);
});
});

Expand Down Expand Up @@ -2102,7 +2102,7 @@ describe('chromium features', () => {
});
await w.webContents.mainFrame.frames[0].executeJavaScript('window.history.back()');
expect(await w.webContents.executeJavaScript('window.history.state')).to.equal(1);
expect(w.webContents.getActiveIndex()).to.equal(1);
expect(w.webContents.navigationHistory.getActiveIndex()).to.equal(1);
});
});
});
Expand Down
8 changes: 4 additions & 4 deletions typings/internal-electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ 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;
_getActiveIndex(): number;
_historyLength(): number;
canGoToIndex(index: number): boolean;
getActiveIndex(): number;
length(): number;
destroy(): void;
// <webview>
attachToIframe(embedderWebContents: Electron.WebContents, embedderFrameId: number): void;
Expand All @@ -112,7 +113,7 @@ declare namespace Electron {
embedder?: Electron.WebContents;
type?: 'backgroundPage' | 'window' | 'browserView' | 'remote' | 'webview' | 'offscreen';
}

type CreateWindowFunction = (options: BrowserWindowConstructorOptions) => WebContents;

interface Menu {
Expand Down Expand Up @@ -165,7 +166,6 @@ declare namespace Electron {
_replyChannel: ReplyChannel;
}


// Deprecated / undocumented BrowserWindow methods
interface BrowserWindow {
getURL(): string;
Expand Down

0 comments on commit 00e3445

Please sign in to comment.