From 1b3c39503404e34a426dbcdcb6e4cc8cfd3d3dc0 Mon Sep 17 00:00:00 2001 From: Bob Qiu Date: Thu, 5 Nov 2026 13:18:03 +0800 Subject: [PATCH] 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 --- .../auth-tab.component.ts} | 4 +- .../preferences-ai.component.ts} | 4 +- .../preferences-credentials.component.ts} | 4 +- .../preferences-plugins.component.ts} | 4 +- .../components/preferences-tab.component.ts | 33 ++++++++ .../preferences.component.ts} | 10 +-- .../script-tab.component.ts} | 4 +- .../components/tab-panel.component.ts | 78 +++++++++++++++++++ .../components/tabs.component.ts | 2 +- packages/insomnia-test/flows/base.flow.ts | 2 +- .../insomnia-test/flows/preferences.flow.ts | 74 +++++++++--------- packages/insomnia-test/misc/fixtures.ts | 2 +- packages/insomnia-test/models/settings.ts | 2 +- packages/insomnia-test/pages/page-manager.ts | 28 +++---- .../pages/preferences-tab.page.ts | 28 ------- packages/insomnia-test/pages/request.page.ts | 51 ++++++++---- .../insomnia-test/pages/tab-panel.page.ts | 50 ------------ 17 files changed, 216 insertions(+), 164 deletions(-) rename packages/insomnia-test/{pages/auth-tab.page.ts => components/auth-tab.component.ts} (99%) rename packages/insomnia-test/{pages/preferences-ai.page.ts => components/preferences-ai.component.ts} (98%) rename packages/insomnia-test/{pages/preferences-credentials.page.ts => components/preferences-credentials.component.ts} (97%) rename packages/insomnia-test/{pages/preferences-plugins.page.ts => components/preferences-plugins.component.ts} (90%) create mode 100644 packages/insomnia-test/components/preferences-tab.component.ts rename packages/insomnia-test/{pages/preferences.page.ts => components/preferences.component.ts} (95%) rename packages/insomnia-test/{pages/script-tab.page.ts => components/script-tab.component.ts} (96%) create mode 100644 packages/insomnia-test/components/tab-panel.component.ts delete mode 100644 packages/insomnia-test/pages/preferences-tab.page.ts delete mode 100644 packages/insomnia-test/pages/tab-panel.page.ts diff --git a/packages/insomnia-test/pages/auth-tab.page.ts b/packages/insomnia-test/components/auth-tab.component.ts similarity index 99% rename from packages/insomnia-test/pages/auth-tab.page.ts rename to packages/insomnia-test/components/auth-tab.component.ts index 09f515985a..d92be3de2e 100644 --- a/packages/insomnia-test/pages/auth-tab.page.ts +++ b/packages/insomnia-test/components/auth-tab.component.ts @@ -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"]`; } diff --git a/packages/insomnia-test/pages/preferences-ai.page.ts b/packages/insomnia-test/components/preferences-ai.component.ts similarity index 98% rename from packages/insomnia-test/pages/preferences-ai.page.ts rename to packages/insomnia-test/components/preferences-ai.component.ts index 8c3edb8995..7fc63decd8 100644 --- a/packages/insomnia-test/pages/preferences-ai.page.ts +++ b/packages/insomnia-test/components/preferences-ai.component.ts @@ -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 diff --git a/packages/insomnia-test/pages/preferences-credentials.page.ts b/packages/insomnia-test/components/preferences-credentials.component.ts similarity index 97% rename from packages/insomnia-test/pages/preferences-credentials.page.ts rename to packages/insomnia-test/components/preferences-credentials.component.ts index 2fe7d14675..41a6dd32b1 100644 --- a/packages/insomnia-test/pages/preferences-credentials.page.ts +++ b/packages/insomnia-test/components/preferences-credentials.component.ts @@ -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; type GcpCloudCredential = Extract; @@ -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. diff --git a/packages/insomnia-test/pages/preferences-plugins.page.ts b/packages/insomnia-test/components/preferences-plugins.component.ts similarity index 90% rename from packages/insomnia-test/pages/preferences-plugins.page.ts rename to packages/insomnia-test/components/preferences-plugins.component.ts index 4552400e95..0617baf8dd 100644 --- a/packages/insomnia-test/pages/preferences-plugins.page.ts +++ b/packages/insomnia-test/components/preferences-plugins.component.ts @@ -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. diff --git a/packages/insomnia-test/components/preferences-tab.component.ts b/packages/insomnia-test/components/preferences-tab.component.ts new file mode 100644 index 0000000000..e1e0a90511 --- /dev/null +++ b/packages/insomnia-test/components/preferences-tab.component.ts @@ -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 { + await expect(this.dialog).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + } +} diff --git a/packages/insomnia-test/pages/preferences.page.ts b/packages/insomnia-test/components/preferences.component.ts similarity index 95% rename from packages/insomnia-test/pages/preferences.page.ts rename to packages/insomnia-test/components/preferences.component.ts index 5d0dee36e8..454fc87e6b 100644 --- a/packages/insomnia-test/pages/preferences.page.ts +++ b/packages/insomnia-test/components/preferences.component.ts @@ -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 { await this.page.getByTestId("settings-button").click(); - await this.navigate(); + await this.waitUntilOpen(); } /** diff --git a/packages/insomnia-test/pages/script-tab.page.ts b/packages/insomnia-test/components/script-tab.component.ts similarity index 96% rename from packages/insomnia-test/pages/script-tab.page.ts rename to packages/insomnia-test/components/script-tab.component.ts index 52fa323364..ceae745ced 100644 --- a/packages/insomnia-test/pages/script-tab.page.ts +++ b/packages/insomnia-test/components/script-tab.component.ts @@ -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']"; diff --git a/packages/insomnia-test/components/tab-panel.component.ts b/packages/insomnia-test/components/tab-panel.component.ts new file mode 100644 index 0000000000..a5ae3f5394 --- /dev/null +++ b/packages/insomnia-test/components/tab-panel.component.ts @@ -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 { + 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 { + await this.tabs.tab(tab).click(); + } + + /** + * Reads a CodeMirror editor's current value. See `CodeEditorComponent`. + */ + protected async readCodeMirror(locator: Locator): Promise { + 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 { + 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 { + return findDocs(this.page, dbFile); + } +} diff --git a/packages/insomnia-test/components/tabs.component.ts b/packages/insomnia-test/components/tabs.component.ts index b8bdc962e6..75de49d7bc 100644 --- a/packages/insomnia-test/components/tabs.component.ts +++ b/packages/insomnia-test/components/tabs.component.ts @@ -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`. */ diff --git a/packages/insomnia-test/flows/base.flow.ts b/packages/insomnia-test/flows/base.flow.ts index ce5a6924d9..b584cceeb6 100644 --- a/packages/insomnia-test/flows/base.flow.ts +++ b/packages/insomnia-test/flows/base.flow.ts @@ -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"; diff --git a/packages/insomnia-test/flows/preferences.flow.ts b/packages/insomnia-test/flows/preferences.flow.ts index 9521a3715f..cfc65bf12c 100644 --- a/packages/insomnia-test/flows/preferences.flow.ts +++ b/packages/insomnia-test/flows/preferences.flow.ts @@ -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 { - 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 }); } diff --git a/packages/insomnia-test/misc/fixtures.ts b/packages/insomnia-test/misc/fixtures.ts index 8c9be7c1a9..a1ef671919 100644 --- a/packages/insomnia-test/misc/fixtures.ts +++ b/packages/insomnia-test/misc/fixtures.ts @@ -123,7 +123,7 @@ export const test = base.extend({ // 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 diff --git a/packages/insomnia-test/models/settings.ts b/packages/insomnia-test/models/settings.ts index 5d91450297..2c04c2a67b 100644 --- a/packages/insomnia-test/models/settings.ts +++ b/packages/insomnia-test/models/settings.ts @@ -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 diff --git a/packages/insomnia-test/pages/page-manager.ts b/packages/insomnia-test/pages/page-manager.ts index 4b5aeba1c9..fa41dfbacb 100644 --- a/packages/insomnia-test/pages/page-manager.ts +++ b/packages/insomnia-test/pages/page-manager.ts @@ -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 BasePage>; +} satisfies Record BasePage | BaseComponent>; type PageName = Extract; diff --git a/packages/insomnia-test/pages/preferences-tab.page.ts b/packages/insomnia-test/pages/preferences-tab.page.ts deleted file mode 100644 index 2d347d391e..0000000000 --- a/packages/insomnia-test/pages/preferences-tab.page.ts +++ /dev/null @@ -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 { - await expect(this.dialog).toBeVisible({ timeout: DEFAULT_TIMEOUT }); - } -} diff --git a/packages/insomnia-test/pages/request.page.ts b/packages/insomnia-test/pages/request.page.ts index 442a307b65..4226ee4161 100644 --- a/packages/insomnia-test/pages/request.page.ts +++ b/packages/insomnia-test/pages/request.page.ts @@ -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 { + await this.panel.waitUntilVisible(); + } + + /** + * Clicks the tab within this request's pane identified by its data-key. + */ + protected async switchTab(tab: string): Promise { + await this.panel.switchTab(tab); } /** diff --git a/packages/insomnia-test/pages/tab-panel.page.ts b/packages/insomnia-test/pages/tab-panel.page.ts deleted file mode 100644 index 36b6ef9b91..0000000000 --- a/packages/insomnia-test/pages/tab-panel.page.ts +++ /dev/null @@ -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 { - 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 { - await this.tabs.tab(tab).click(); - } -}