diff --git a/client/lib/CoreSession.ts b/client/lib/CoreSession.ts index d49a8b480..990cdef16 100644 --- a/client/lib/CoreSession.ts +++ b/client/lib/CoreSession.ts @@ -111,6 +111,11 @@ export default class CoreSession return this.commandQueue.run('Session.getHeroMeta'); } + public async newTab(): Promise { + const meta = await this.commandQueue.run('Session.newTab'); + return this.addTab(meta); + } + public async getTabs(): Promise { const tabSessionMetas = await this.commandQueue.run('Session.getTabs'); for (const tabMeta of tabSessionMetas) { diff --git a/client/lib/Hero.ts b/client/lib/Hero.ts index 08e3d555c..a70b2029e 100644 --- a/client/lib/Hero.ts +++ b/client/lib/Hero.ts @@ -312,6 +312,13 @@ export default class Hero extends AwaitedEventTarget { })); } + public async newTab(): Promise { + 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 { const tabIdx = this.#tabs.indexOf(tab); this.#tabs.splice(tabIdx, 1); @@ -322,6 +329,12 @@ export default class Hero extends AwaitedEventTarget { await coreTab.close(); } + public async focusTab(tab: Tab): Promise { + const coreTab = await getCoreTab(tab); + await coreTab.focusTab(); + this.#activeTab = tab; + } + public async findResource( filter: IResourceFilterProperties, options?: { sinceCommandId: number }, @@ -336,12 +349,6 @@ export default class Hero extends AwaitedEventTarget { return await this.activeTab.findResources(filter, options); } - public async focusTab(tab: Tab): Promise { - const coreTab = await getCoreTab(tab); - await coreTab.focusTab(); - this.#activeTab = tab; - } - public async getSnippet(key: string): Promise { const coreSession = await this.#getCoreSessionOrReject(); const sessionId = this.#options.replaySessionId ?? (await this.sessionId); @@ -373,8 +380,8 @@ export default class Hero extends AwaitedEventTarget { }, ): Promise { 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 = { @@ -396,21 +403,21 @@ export default class Hero extends AwaitedEventTarget { public async interact(...interactions: IInteractions): Promise { 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 { 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 { - 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 })), diff --git a/core/lib/Session.ts b/core/lib/Session.ts index d1a7c4951..a7aa91b40 100644 --- a/core/lib/Session.ts +++ b/core/lib/Session.ts @@ -193,6 +193,10 @@ export default class Session return this.tabsById.get(id); } + public newTab(): Promise { + return this.createTab(); + } + public getTabs(): Promise { return Promise.resolve([...this.tabsById.values()].filter(x => !x.isClosing)); } @@ -602,6 +606,7 @@ export default class Session this.close, this.flush, this.exportUserProfile, + this.newTab, this.getTabs, this.getHeroMeta, this.addRemoteEventListener, diff --git a/docs/basic-client/hero.md b/docs/basic-client/hero.md index 0c6d4269c..6f0230520 100644 --- a/docs/basic-client/hero.md +++ b/docs/basic-client/hero.md @@ -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` + +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). diff --git a/docs/plugins/core-plugins.md b/docs/plugins/core-plugins.md index 052407576..3d654f1fc 100644 --- a/docs/plugins/core-plugins.md +++ b/docs/plugins/core-plugins.md @@ -108,7 +108,7 @@ Modify any value in the object to change it session-wide. #### **Returns** `void` -### onNewBrowser(browser, launchArgs) *optional* +### onNewBrowser(browser, userConfig) *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/) diff --git a/end-to-end/test/tab.test.ts b/end-to-end/test/tab.test.ts index c17ce78ca..c451a0cf6 100644 --- a/end-to-end/test/tab.test.ts +++ b/end-to-end/test/tab.test.ts @@ -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'); diff --git a/yarn.lock b/yarn.lock index f9d076a1f..7251b8134 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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"