refactor(insomnia-test): move shared tab panes and preferences dialog to components

Address review feedback: auth-tab, script-tab and tab-panel are reused by
several pages, and the Preferences dialog is reachable from any page, so
they belong in the component layer rather than pages.

- TabPanelPage -> TabPanelComponent (RequestPage now composes it)
- AuthTabComponent / ScriptTabComponent move to components/
- Preferences{,Tab,Credentials,Plugins,Ai}Page -> *Component, registered
  in PageManager under preferences / preferencesCredentials / ...

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Bob QiuandClaude Sonnet 5.5 committed 2026-11-05 13:18:03 +08:00
1 parent f2a0133c74
commit 1b3c395034
17 files changed
+216 -164

No files matched your search

@@ -12,7 +12,7 @@ import type {
AuthTypeSingleToken,
} from "insomnia-data";
import { TabPanelPage } from "./tab-panel.page";
import { TabPanelComponent } from "./tab-panel.component";
type OAuth1TextField = keyof Omit<
AuthTypeOAuth1,
@@ -111,7 +111,7 @@ interface AuthFieldEdit {
* Constructed with the owning page's `pane` selector (its tab-bar's outer
* container), since that's the only thing that differs between them.
*/
export class AuthTabComponent extends TabPanelPage {
export class AuthTabComponent extends TabPanelComponent {
protected get AUTH_TYPE_BUTTON(): string {
return `${this.PANE} [aria-label="Change Authentication type"]`;
}
@@ -1,7 +1,7 @@
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import { expect } from "@playwright/test";
import { PreferencesTabPage } from "./preferences-tab.page";
import { PreferencesTabComponent } from "./preferences-tab.component";
export interface LlmUrlBackendFormFields {
url: string;
@@ -15,7 +15,7 @@ export interface LlmUrlBackendFormFields {
/**
* The Preferences dialog's AI Settings tab: LLM backends and Smart Commits.
*/
export class PreferencesAiPage extends PreferencesTabPage {
export class PreferencesAiComponent extends PreferencesTabComponent {
/**
* Switches the open Preferences dialog to the "AI Settings" tab. Only
* rendered once the bundled AI plugin is present and the user is logged
@@ -2,7 +2,7 @@ import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import type { CloudCredential } from "@package/models/settings";
import { expect } from "@playwright/test";
import { PreferencesTabPage } from "./preferences-tab.page";
import { PreferencesTabComponent } from "./preferences-tab.component";
type AwsCloudCredential = Extract<CloudCredential, { provider: "aws" }>;
type GcpCloudCredential = Extract<CloudCredential, { provider: "gcp" }>;
@@ -15,7 +15,7 @@ type HashiCorpCloudCredential = Extract<
* The Preferences dialog's Credentials tab: Git credentials and cloud
* (vault provider) credentials.
*/
export class PreferencesCredentialsPage extends PreferencesTabPage {
export class PreferencesCredentialsComponent extends PreferencesTabComponent {
/**
* Switches the open Preferences dialog to the Credentials tab, where Git
* credentials are added/managed.
@@ -1,13 +1,13 @@
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import { expect } from "@playwright/test";
import { PreferencesTabPage } from "./preferences-tab.page";
import { PreferencesTabComponent } from "./preferences-tab.component";
/**
* The Preferences dialog's Plugins and Scripting tabs: plugin install /
* generate / reload, and the template-tag and script sandbox switches.
*/
export class PreferencesPluginsPage extends PreferencesTabPage {
export class PreferencesPluginsComponent extends PreferencesTabComponent {
/**
* Switches the open Preferences dialog to the Scripting tab, where the
* template-tag sandbox toggle and per-rule-group sandbox switches live.
@@ -0,0 +1,33 @@
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import type { Locator, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { BaseComponent } from "./base.component";
/**
* Shared root for the Preferences dialog's components: the dialog locator
* that every tab is scoped to. One subclass per tab (or tab area), all of
* which assume `PreferencesComponent.open()` has already shown the dialog.
* The dialog is global — it can be opened from any page — so it lives in
* the component layer rather than being tied to one page.
*/
export abstract class PreferencesTabComponent extends BaseComponent {
constructor(page: Page) {
super(
page,
page.getByRole("dialog").filter({ hasText: "Insomnia Preferences" }),
);
}
protected get dialog(): Locator {
return this.root;
}
/**
* Confirms the Preferences dialog is open by waiting for it to become
* visible.
*/
async waitUntilOpen(): Promise<void> {
await expect(this.dialog).toBeVisible({ timeout: DEFAULT_TIMEOUT });
}
}
@@ -1,14 +1,14 @@
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import { expect } from "@playwright/test";
import { PreferencesTabPage } from "./preferences-tab.page";
import { PreferencesTabComponent } from "./preferences-tab.component";
/**
* The Preferences dialog itself (open / close) and its General, Data and
* Proxy tabs. Other tabs live in `PreferencesCredentialsPage`,
* `PreferencesPluginsPage`, `PreferencesAiPage` and `PreferencesVaultPage`.
* Proxy tabs. Other tabs live in `PreferencesCredentialsComponent`,
* `PreferencesPluginsComponent`, `PreferencesAiComponent` and `PreferencesVaultComponent`.
*/
export class PreferencesPage extends PreferencesTabPage {
export class PreferencesComponent extends PreferencesTabComponent {
/**
* Closes the Preferences dialog via its close button and waits for it
* to be hidden. More reliable than Escape, whose dismiss behavior can be
@@ -28,7 +28,7 @@ export class PreferencesPage extends PreferencesTabPage {
*/
async open(): Promise<void> {
await this.page.getByTestId("settings-button").click();
await this.navigate();
await this.waitUntilOpen();
}
/**
@@ -1,6 +1,6 @@
import { expect } from "@playwright/test";
import { TabPanelPage } from "./tab-panel.page";
import { TabPanelComponent } from "./tab-panel.component";
export interface RequestScripts {
preRequest?: string;
@@ -16,7 +16,7 @@ export interface RequestScripts {
* `pane` selector (its tab-bar's outer container), since that's the only
* thing that differs between them.
*/
export class ScriptTabComponent extends TabPanelPage {
export class ScriptTabComponent extends TabPanelComponent {
protected readonly AFTER_RESPONSE_EDITOR = "[role='tab'][data-key='after-response']";
protected readonly PRE_REQUEST_EDITOR = "[role='tab'][data-key='pre-request']";
@@ -0,0 +1,78 @@
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import { findDocs } from "@package/misc/db";
import type { Locator, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { BaseComponent } from "./base.component";
import { CodeEditorComponent } from "./code-editor.component";
import { TabsComponent } from "./tabs.component";
/**
* Shared plumbing for any component scoped to one tab-bar pane (a
* request's own pane, or a Folder's "Open in New Tab" pane) — both render
* the exact same tab-bar component (`["Auth", "Headers", "Scripts",
* "Environment", "Docs"]`), just with a different outer container
* selector. Subclasses/composed components take that container selector
* (`pane`) through the constructor instead of declaring it as an abstract
* field, so the same component class can be instantiated once per owning
* page with that page's own `PANE`.
*/
export class TabPanelComponent extends BaseComponent {
constructor(
page: Page,
readonly PANE: string,
) {
super(page, page.locator(PANE));
}
/**
* Waits for this pane to become visible, confirming it's open and active.
*/
async waitUntilVisible(): Promise<void> {
await expect(this.root).toBeVisible({ timeout: DEFAULT_TIMEOUT });
}
/** The pane's tab bar. */
get tabs(): TabsComponent {
return new TabsComponent(this.page, this.root);
}
get TABPANEL(): string {
return `${this.PANE} [role="tabpanel"]`;
}
/**
* Clicks the tab within this pane identified by its data-key.
*/
async switchTab(tab: string): Promise<void> {
await this.tabs.tab(tab).click();
}
/**
* Reads a CodeMirror editor's current value. See `CodeEditorComponent`.
*/
protected async readCodeMirror(locator: Locator): Promise<string> {
return CodeEditorComponent.fromCodeMirror(this.page, locator).getValue();
}
/**
* Sets a CodeMirror editor's value and confirms it holds. See
* `CodeEditorComponent.setValue`.
*/
protected async setCodeMirrorValue(
cmLocator: Locator,
text: string,
): Promise<void> {
await CodeEditorComponent.fromCodeMirror(this.page, cmLocator).setValue(
text,
);
}
/**
* Reads every live document of the model stored in `dbFile` from the
* app's database.
*/
protected async readPersistedDocs(dbFile: string): Promise<any[]> {
return findDocs(this.page, dbFile);
}
}
@@ -5,7 +5,7 @@ import { BaseComponent } from "./base.component";
/**
* A react-aria `Tabs` widget (`basic-components/tabs.tsx`, request/response
* pane tabs). `root` is the container that holds both the tab list and the
* `tabpanel`s, e.g. a request pane. Replaces `TabPanelPage`'s tab logic.
* `tabpanel`s, e.g. a request pane.
*/
export class TabsComponent extends BaseComponent {
/** A tab by its `data-key`. */
+1 -1
View File
@@ -1,7 +1,7 @@
import type { AuthTabComponent } from "@package/components/auth-tab.component";
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import type { FlowContext } from "@package/misc/flow-context";
import type { RequestAuthentication } from "@package/models/authentication";
import type { AuthTabComponent } from "@package/pages/auth-tab.page";
import type { PageManager } from "@package/pages/page-manager";
import { expect } from "@playwright/test";
@@ -1,8 +1,8 @@
import type { LlmUrlBackendFormFields } from "@package/components/preferences-ai.component";
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import { step } from "@package/misc/step";
import type { AiUrlBackendSettings} from "@package/models/settings";
import { Settings } from "@package/models/settings";
import type { LlmUrlBackendFormFields } from "@package/pages/preferences-ai.page";
import { expect } from "@playwright/test";
import { BaseFlow } from "./base.flow";
@@ -15,57 +15,57 @@ export class PreferencesFlow extends BaseFlow {
*/
@step()
async set(settings: Settings): Promise<Settings> {
const { preferencesPage, preferencesCredentialsPage, preferencesPluginsPage, preferencesAiPage } = this.pageManager;
const { preferences, preferencesCredentials, preferencesPlugins, preferencesAi } = this.pageManager;
let aiUrlBackendResult: AiUrlBackendSettings | undefined;
await preferencesPage.open();
await preferences.open();
if (settings.validateSSL !== undefined) {
await preferencesPage.setValidateSSL(settings.validateSSL);
await preferences.setValidateSSL(settings.validateSSL);
}
if (settings.sidebarFocusForCollections !== undefined) {
await preferencesPage.setSidebarFocusForCollections(
await preferences.setSidebarFocusForCollections(
settings.sidebarFocusForCollections,
);
}
if (settings.showLegacyUnitTests !== undefined) {
await preferencesPage.setShowLegacyUnitTests(
await preferences.setShowLegacyUnitTests(
settings.showLegacyUnitTests,
);
}
if (settings.hideSecretValuesInPreviewAndConsole !== undefined) {
await preferencesPage.setHideSecretValuesInPreviewAndConsole(
await preferences.setHideSecretValuesInPreviewAndConsole(
settings.hideSecretValuesInPreviewAndConsole,
);
}
if (settings.dataFolders !== undefined) {
for (const path of settings.dataFolders) {
await preferencesPage.addDataFolder(path);
await preferences.addDataFolder(path);
}
}
if (settings.pluginSandboxEnabled !== undefined) {
await preferencesPluginsPage.openScriptingTab();
await preferencesPluginsPage.setTemplateTagSandbox(
await preferencesPlugins.openScriptingTab();
await preferencesPlugins.setTemplateTagSandbox(
settings.pluginSandboxEnabled,
);
}
if (settings.filterResponsesByEnv !== undefined) {
await preferencesPage.setFilterResponsesByEnvironment(
await preferences.setFilterResponsesByEnvironment(
settings.filterResponsesByEnv,
);
}
if (settings.cloudCredentials !== undefined) {
await preferencesCredentialsPage.openCredentialsTab();
await preferencesCredentials.openCredentialsTab();
for (const credential of settings.cloudCredentials) {
if (credential.provider === "aws") {
await preferencesCredentialsPage.createAwsCloudCredential(credential);
await preferencesCredentials.createAwsCloudCredential(credential);
} else if (credential.provider === "gcp") {
await preferencesCredentialsPage.createGcpCloudCredential(credential);
await preferencesCredentials.createGcpCloudCredential(credential);
} else {
await preferencesCredentialsPage.createHashiCorpCloudCredential(credential);
await preferencesCredentials.createHashiCorpCloudCredential(credential);
}
}
}
if (settings.timeout !== undefined) {
await preferencesPage.setRequestTimeout(settings.timeout);
await preferences.setRequestTimeout(settings.timeout);
}
if (
settings.proxyEnabled !== undefined ||
@@ -73,74 +73,74 @@ export class PreferencesFlow extends BaseFlow {
settings.httpsProxy !== undefined ||
settings.noProxy !== undefined
) {
await preferencesPage.openProxyTab();
await preferences.openProxyTab();
if (settings.proxyEnabled !== undefined) {
await preferencesPage.setProxyEnabled(settings.proxyEnabled);
await preferences.setProxyEnabled(settings.proxyEnabled);
}
if (settings.httpProxy !== undefined) {
await preferencesPage.setHttpProxy(settings.httpProxy);
await preferences.setHttpProxy(settings.httpProxy);
}
if (settings.httpsProxy !== undefined) {
await preferencesPage.setHttpsProxy(settings.httpsProxy);
await preferences.setHttpsProxy(settings.httpsProxy);
}
if (settings.noProxy !== undefined) {
await preferencesPage.setNoProxy(settings.noProxy);
await preferences.setNoProxy(settings.noProxy);
}
}
if (settings.aiUrlBackend !== undefined) {
await preferencesAiPage.openAiSettingsTab();
await preferencesAi.openAiSettingsTab();
if (settings.aiUrlBackend === null) {
const fields =
await preferencesAiPage.getUrlBackendFormValues("LLM URL Active");
await preferencesAiPage.clickDeactivateAiBackend();
await preferencesAi.getUrlBackendFormValues("LLM URL Active");
await preferencesAi.clickDeactivateAiBackend();
let isActive = true;
await expect(async () => {
isActive = await preferencesAiPage.isAiBackendActive("LLM URL");
isActive = await preferencesAi.isAiBackendActive("LLM URL");
expect(isActive).toBe(false);
}).toPass({ timeout: DEFAULT_TIMEOUT });
aiUrlBackendResult = this.toAiUrlBackendSettings(fields, isActive);
} else {
const { url, model, apiKey, temperature, topP, maxTokens } =
settings.aiUrlBackend;
await preferencesAiPage.clickAiBackendNav("LLM URL");
await preferencesAiPage.setAiUrlBackendUrl(url);
await preferencesAi.clickAiBackendNav("LLM URL");
await preferencesAi.setAiUrlBackendUrl(url);
if (apiKey !== undefined) {
await preferencesAiPage.setAiUrlBackendApiToken(apiKey);
await preferencesAi.setAiUrlBackendApiToken(apiKey);
}
await preferencesAiPage.clickLoadAiModels();
await preferencesAiPage.selectAiModel(model);
await preferencesAi.clickLoadAiModels();
await preferencesAi.selectAiModel(model);
if (
temperature !== undefined ||
topP !== undefined ||
maxTokens !== undefined
) {
await preferencesAiPage.openAiAdvancedOptions();
await preferencesAi.openAiAdvancedOptions();
if (temperature !== undefined) {
await preferencesAiPage.setAiAdvancedOptionValue(
await preferencesAi.setAiAdvancedOptionValue(
"Temperature (0-2):",
String(temperature),
);
}
if (topP !== undefined) {
await preferencesAiPage.setAiAdvancedOptionValue(
await preferencesAi.setAiAdvancedOptionValue(
"Top P (0-1):",
String(topP),
);
}
if (maxTokens !== undefined) {
await preferencesAiPage.setAiAdvancedOptionValue(
await preferencesAi.setAiAdvancedOptionValue(
"Max Tokens (1-128000):",
String(maxTokens),
);
}
}
await preferencesAiPage.clickActivateAiBackend();
await preferencesAi.clickActivateAiBackend();
const fields =
await preferencesAiPage.getUrlBackendFormValues("LLM URL Active");
await preferencesAi.getUrlBackendFormValues("LLM URL Active");
aiUrlBackendResult = this.toAiUrlBackendSettings(fields, true);
}
}
await preferencesPage.close();
await preferences.close();
return new Settings({ aiUrlBackend: aiUrlBackendResult });
}
+1 -1
View File
@@ -123,7 +123,7 @@ export const test = base.extend<Fixtures>({
// Its own fixture, same shape as `vaultKey` above — pre-seeds
// `userSession.vaultSalt`, which alone is enough to make Preferences'
// Vault Key panel show "Enter Vault Key" instead of "Generate Vault Key"
// (see `pages/preferences.page.ts`'s vault key methods), without needing
// (see `components/preferences.component.ts`'s vault key methods), without needing
// a `vaultKey` override too. `misc/mock-api.ts` runs a real SRP exchange
// against whatever salt/verifier a spec's own `preferencesFlow` calls
// establish — this fixture only ever seeds the renderer's *local* belief
+1 -1
View File
@@ -8,7 +8,7 @@ import type { Settings as AppSettings } from "insomnia-data/common";
// Each provider's real `credentials` field (`CloudProviderCredential` in
// insomnia-data) is itself a union of every auth mode that provider
// supports (e.g. AWS: temporary/file/SSO) — narrowed here to just the one
// `PreferencesPage` actually has a create method for (AWS: "Credential
// `PreferencesComponent` actually has a create method for (AWS: "Credential
// File"; HashiCorp: on-prem AppRole), so a caller can't hand in a shape
// (e.g. AWS temporary credentials) no create method reads. `azure` is
// left out entirely: its credential only ever comes from a real OAuth
+15 -13
View File
@@ -1,13 +1,14 @@
import type { BaseComponent } from "@package/components/base.component";
import { PreferencesComponent } from "@package/components/preferences.component";
import { PreferencesAiComponent } from "@package/components/preferences-ai.component";
import { PreferencesCredentialsComponent } from "@package/components/preferences-credentials.component";
import { PreferencesPluginsComponent } from "@package/components/preferences-plugins.component";
import { findDocs } from "@package/misc/db";
import type { Page } from "playwright-core";
import type { BasePage } from "./base.page";
import { EnvironmentPage } from "./environment.page";
import { HttpRequestPage } from "./http-request.page";
import { PreferencesPage } from "./preferences.page";
import { PreferencesAiPage } from "./preferences-ai.page";
import { PreferencesCredentialsPage } from "./preferences-credentials.page";
import { PreferencesPluginsPage } from "./preferences-plugins.page";
import { ProjectPage } from "./project.page";
import { ResponsePage } from "./response.page";
import { SpecPage } from "./spec.page";
@@ -18,23 +19,24 @@ const pageRegistry = {
new EnvironmentPage(page),
httpRequestPage: (page: Page): HttpRequestPage =>
new HttpRequestPage(page),
preferencesPage: (page: Page): PreferencesPage =>
new PreferencesPage(page),
// Global dialogs reachable from any page, so components rather than pages.
preferences: (page: Page): PreferencesComponent =>
new PreferencesComponent(page),
responsePage: (page: Page): ResponsePage =>
new ResponsePage(page),
preferencesCredentialsPage: (page: Page): PreferencesCredentialsPage =>
new PreferencesCredentialsPage(page),
preferencesPluginsPage: (page: Page): PreferencesPluginsPage =>
new PreferencesPluginsPage(page),
preferencesAiPage: (page: Page): PreferencesAiPage =>
new PreferencesAiPage(page),
preferencesCredentials: (page: Page): PreferencesCredentialsComponent =>
new PreferencesCredentialsComponent(page),
preferencesPlugins: (page: Page): PreferencesPluginsComponent =>
new PreferencesPluginsComponent(page),
preferencesAi: (page: Page): PreferencesAiComponent =>
new PreferencesAiComponent(page),
projectPage: (page: Page): ProjectPage =>
new ProjectPage(page),
specPage: (page: Page): SpecPage =>
new SpecPage(page),
workspacePage: (page: Page): WorkspacePage =>
new WorkspacePage(page),
} satisfies Record<string, (page: Page) => BasePage>;
} satisfies Record<string, (page: Page) => BasePage | BaseComponent>;
type PageName = Extract<keyof typeof pageRegistry, string>;
@@ -1,28 +0,0 @@
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import type { Locator } from "@playwright/test";
import { expect } from "@playwright/test";
import { BasePage } from "./base.page";
/**
* Shared root for the Preferences dialog's pages: the dialog locator that
* every tab is scoped to. One subclass per tab (or tab area), all of which
* assume `PreferencesPage.open()` has already shown the dialog.
*/
export abstract class PreferencesTabPage extends BasePage {
// A getter, not a field: `AppFlow.restart()` swaps `this.page` on the
// retained page object, so the locator must follow the current window.
protected get dialog(): Locator {
return this.page
.getByRole("dialog")
.filter({ hasText: "Insomnia Preferences" });
}
/**
* Confirms the Preferences dialog is open by waiting for it to become
* visible.
*/
async navigate(): Promise<void> {
await expect(this.dialog).toBeVisible({ timeout: DEFAULT_TIMEOUT });
}
}
+34 -17
View File
@@ -1,6 +1,9 @@
import { AuthTabComponent } from "@package/components/auth-tab.component";
import { DropdownComponent } from "@package/components/dropdown.component";
import { KeyValueEditorComponent } from "@package/components/key-value-editor.component";
import { ScriptTabComponent } from "@package/components/script-tab.component";
import { SelectComponent } from "@package/components/select.component";
import { TabPanelComponent } from "@package/components/tab-panel.component";
import type { HttpMethod } from "@package/enums/http-method";
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import { throwOnDialog } from "@package/misc/decorators";
@@ -12,9 +15,7 @@ import type {
} from "insomnia-data";
import type { Page } from "playwright-core";
import { AuthTabComponent } from "./auth-tab.page";
import { ScriptTabComponent } from "./script-tab.page";
import { TabPanelPage } from "./tab-panel.page";
import { BasePage } from "./base.page";
const REQUEST_PANE = '[data-testid="request-pane"]';
@@ -24,7 +25,9 @@ interface KeyValuePair {
disabled?: boolean;
}
export abstract class RequestPage extends TabPanelPage {
export abstract class RequestPage extends BasePage {
protected readonly PANE = REQUEST_PANE;
protected readonly METHOD_BUTTON =
'[data-testid="request-pane"] button[aria-label="Request Method"]';
/** The key/value row editor in the active tab panel (params, headers, body params…). */
@@ -60,26 +63,40 @@ export abstract class RequestPage extends TabPanelPage {
"body.params",
];
// Getters, not fields: the components are bound to the current window, so
// building them on access keeps them following `this.page`.
/** The tab-bar pane of this request. */
protected get panel(): TabPanelComponent {
return new TabPanelComponent(this.page, this.PANE);
}
protected get TABPANEL(): string {
return this.panel.TABPANEL;
}
/** The Auth tab component for this request's own pane. */
readonly auth: AuthTabComponent;
get auth(): AuthTabComponent {
return new AuthTabComponent(this.page, this.PANE);
}
/** The Scripts tab component for this request's own pane. */
readonly scripts: ScriptTabComponent;
constructor(page: Page) {
super(page, REQUEST_PANE);
this.auth = new AuthTabComponent(page, REQUEST_PANE);
this.scripts = new ScriptTabComponent(page, REQUEST_PANE);
get scripts(): ScriptTabComponent {
return new ScriptTabComponent(this.page, this.PANE);
}
/**
* Also rewires the composed Auth/Scripts tab components onto the new
* window.
* Waits for this request's pane to become visible, confirming it's open
* and active.
*/
override setContext(page: Page): void {
super.setContext(page);
this.auth.setContext(page);
this.scripts.setContext(page);
async navigate(): Promise<void> {
await this.panel.waitUntilVisible();
}
/**
* Clicks the tab within this request's pane identified by its data-key.
*/
protected async switchTab(tab: string): Promise<void> {
await this.panel.switchTab(tab);
}
/**
@@ -1,50 +0,0 @@
import { TabsComponent } from "@package/components/tabs.component";
import { DEFAULT_TIMEOUT } from "@package/misc/constants";
import { expect } from "@playwright/test";
import type { Page } from "playwright-core";
import { BasePage } from "./base.page";
/**
* Shared plumbing for any component scoped to one tab-bar pane (a
* request's own pane, or a Folder's "Open in New Tab" pane) — both render
* the exact same tab-bar component (`["Auth", "Headers", "Scripts",
* "Environment", "Docs"]`), just with a different outer container
* selector. Subclasses/composed components take that container selector
* (`pane`) through the constructor instead of declaring it as an abstract
* field, so the same component class can be instantiated once per owning
* page with that page's own `PANE`.
*/
export abstract class TabPanelPage extends BasePage {
constructor(
page: Page,
protected readonly PANE: string,
) {
super(page);
}
/**
* Waits for this pane to become visible, confirming it's open and active.
*/
async navigate(): Promise<void> {
await expect(this.page.locator(this.PANE)).toBeVisible({
timeout: DEFAULT_TIMEOUT,
});
}
/** The pane's tab bar. */
protected get tabs(): TabsComponent {
return new TabsComponent(this.page, this.page.locator(this.PANE));
}
protected get TABPANEL(): string {
return `${this.PANE} [role="tabpanel"]`;
}
/**
* Clicks the tab within this pane identified by its data-key.
*/
protected async switchTab(tab: string): Promise<void> {
await this.tabs.tab(tab).click();
}
}