mirror of
https://github.com/Kong/insomnia.git
synced 2026-11-07 21:44:32 -05:00
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:
1 parent
f2a0133c74
commit
1b3c395034
17 files changed
+216
-164
No files matched your search
+2
-2
@@ -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"]`;
|
||||
}
|
||||
+2
-2
@@ -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
-2
@@ -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.
|
||||
+2
-2
@@ -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 });
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
+2
-2
@@ -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,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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user