Skip to content

Commit

Permalink
feat: add ability to manually create a newTab
Browse files Browse the repository at this point in the history
Closes #268
  • Loading branch information
blakebyrnes committed Jun 13, 2024
1 parent fa241bd commit 4f1f836
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 15 deletions.
5 changes: 5 additions & 0 deletions client/lib/CoreSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export default class CoreSession
return this.commandQueue.run('Session.getHeroMeta');
}

public async newTab(): Promise<CoreTab> {
const meta = await this.commandQueue.run<ISessionMeta>('Session.newTab');
return this.addTab(meta);
}

public async getTabs(): Promise<CoreTab[]> {
const tabSessionMetas = await this.commandQueue.run<ISessionMeta[]>('Session.getTabs');
for (const tabMeta of tabSessionMetas) {
Expand Down
35 changes: 21 additions & 14 deletions client/lib/Hero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ export default class Hero extends AwaitedEventTarget<IHeroEvents> {
}));
}

public async newTab(): Promise<Tab> {
const coreTab = this.#getCoreSessionOrReject().then(x => x.newTab());
const tab = createTab(this, coreTab, this.#callsiteLocator);
this.#tabs.push(tab);
return tab;
}

public async closeTab(tab: Tab): Promise<void> {
const tabIdx = this.#tabs.indexOf(tab);
this.#tabs.splice(tabIdx, 1);
Expand All @@ -322,6 +329,12 @@ export default class Hero extends AwaitedEventTarget<IHeroEvents> {
await coreTab.close();
}

public async focusTab(tab: Tab): Promise<void> {
const coreTab = await getCoreTab(tab);
await coreTab.focusTab();
this.#activeTab = tab;
}

public async findResource(
filter: IResourceFilterProperties,
options?: { sinceCommandId: number },
Expand All @@ -336,12 +349,6 @@ export default class Hero extends AwaitedEventTarget<IHeroEvents> {
return await this.activeTab.findResources(filter, options);
}

public async focusTab(tab: Tab): Promise<void> {
const coreTab = await getCoreTab(tab);
await coreTab.focusTab();
this.#activeTab = tab;
}

public async getSnippet<T = any>(key: string): Promise<T> {
const coreSession = await this.#getCoreSessionOrReject();
const sessionId = this.#options.replaySessionId ?? (await this.sessionId);
Expand Down Expand Up @@ -373,8 +380,8 @@ export default class Hero extends AwaitedEventTarget<IHeroEvents> {
},
): Promise<void> {
let coreFrame = await getCoreFrameEnvironmentForPosition(mousePosition);
coreFrame ??= await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol]
.coreFramePromise;
coreFrame ??=
await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol].coreFramePromise;
let interaction: IInteraction = { click: mousePosition };
if (!isMousePositionXY(mousePosition)) {
interaction = {
Expand All @@ -396,21 +403,21 @@ export default class Hero extends AwaitedEventTarget<IHeroEvents> {
public async interact(...interactions: IInteractions): Promise<void> {
if (!interactions.length) return;
let coreFrame = await getCoreFrameForInteractions(interactions);
coreFrame ??= await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol]
.coreFramePromise;
coreFrame ??=
await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol].coreFramePromise;
await Interactor.run(coreFrame, interactions);
}

public async scrollTo(mousePosition: IMousePositionXY | ISuperElement): Promise<void> {
let coreFrame = await getCoreFrameEnvironmentForPosition(mousePosition);
coreFrame ??= await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol]
.coreFramePromise;
coreFrame ??=
await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol].coreFramePromise;
await Interactor.run(coreFrame, [{ [Command.scroll]: mousePosition }]);
}

public async type(...typeInteractions: ITypeInteraction[]): Promise<void> {
const coreFrame = await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol]
.coreFramePromise;
const coreFrame =
await this.activeTab.mainFrameEnvironment[InternalPropertiesSymbol].coreFramePromise;
await Interactor.run(
coreFrame,
typeInteractions.map(t => ({ type: t })),
Expand Down
5 changes: 5 additions & 0 deletions core/lib/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ export default class Session
return this.tabsById.get(id);
}

public newTab(): Promise<Tab> {
return this.createTab();
}

public getTabs(): Promise<Tab[]> {
return Promise.resolve([...this.tabsById.values()].filter(x => !x.isClosing));
}
Expand Down Expand Up @@ -602,6 +606,7 @@ export default class Session
this.close,
this.flush,
this.exportUserProfile,
this.newTab,
this.getTabs,
this.getHeroMeta,
this.addRemoteEventListener,
Expand Down
10 changes: 10 additions & 0 deletions docs/basic-client/hero.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,16 @@ Executes a series of mouse and keyboard interactions.

Refer to the [Interactions page](./interactions.md) for details on how to construct an interaction.

### hero.newTab*()* {#new-tab}

Create a new tab. This will reuse the same Hero session, which can be useful for certain use cases, but should not be used for "different user activities". A Hero session is meant to emulate the behavior of a single user action, not as a platform for multiple activities. YMMV.

NOTE: this tab will not be focused until you call [hero.focusTab](./hero.md#focus-tab).

#### **Returns**: `Promise<Tab>`

Refer to the [Interactions page](./interactions.md) for details on how to construct an interaction.

### hero.setSnippet _(key, value)_ {#setSnippet}

Stores a JSON-able value in the session database that can be retrieved later with [HeroReplay](./hero-replay.md).
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/core-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Modify any value in the object to change it session-wide.
#### **Returns** `void`
### onNewBrowser<em>(browser, launchArgs)</em> *optional*
### onNewBrowser<em>(browser, userConfig)</em> *optional*
This is called every time a new browser engine is started, which may not be every session. A Core Plugin can add to the launch arguments that will sent to the process creation of the browser:
- For Chrome, a list can be found [here](https://peter.sh/experiments/chromium-command-line-switches/)
Expand Down
17 changes: 17 additions & 0 deletions end-to-end/test/tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ describe('Multi-tab scenarios', () => {
expect(hero.activeTab).toBe(tab1);
});

it('can open a new tab programatically', async () => {
const hero = new Hero();
Helpers.needsClosing.push(hero);

await hero.goto(`${koaServer.baseUrl}/tabTest`);
await hero.waitForPaintingStable();
expect(await hero.tabs).toHaveLength(1);
expect(await hero.activeTab.url).toBe(`${koaServer.baseUrl}/tabTest`);

const tab2 = await hero.newTab();
expect(await hero.tabs).toHaveLength(2);
expect(await tab2.url).toBe('about:blank');
await tab2.goto(`${koaServer.baseUrl}/newTab`);
await tab2.waitForLoad('AllContentLoaded');
expect(await tab2.url).toBe(`${koaServer.baseUrl}/newTab`);
});

it('can wait for resources in each tab', async () => {
koaServer.get('/logo.png', ctx => {
ctx.set('Content-Type', 'image/png');
Expand Down
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8541,6 +8541,18 @@ tar@^6.0.2, tar@^6.1.0, tar@^6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"

tar@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^5.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"

temp-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d"
Expand Down Expand Up @@ -9253,6 +9265,11 @@ ws@^7.5.9:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==

ws@^8.17.0:
version "8.17.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea"
integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==

xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
Expand Down

0 comments on commit 4f1f836

Please sign in to comment.