feat: add step instrumentation for flow methods

- Introduced a new `step` decorator to enhance method reporting in flows.
- Removed the old step instrumentation logic from `step-instrumentation.ts`.
- Updated various flow classes to utilize the new `@step()` decorator for method tracking.
- Simplified page manager by removing the instrumentWithSteps function.
- Adjusted tests to ensure compatibility with the new step reporting mechanism.
This commit is contained in:
Bob Qiu committed 2026-10-30 17:40:37 +08:00
1 parent 8e856242c0
commit e32d8cb4e1
28 files changed
+353 -315

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
{
"private": true,
"type": "module",
"name": "insomnia-data",
"license": "Apache-2.0",
"version": "13.3.1",
+9
View File
@@ -8,6 +8,7 @@ import {
trackHiddenWindowCoverage,
} from "../misc/coverage";
import { launchInsomniaElectron } from "../misc/launch";
import { step } from "../misc/step";
import { BaseFlow } from "./base.flow";
export class AppFlow extends BaseFlow {
@@ -18,6 +19,7 @@ export class AppFlow extends BaseFlow {
* `flowManager`/`pageManager` onto the new app in place, so the same
* `user.flowManager`/`user.pageManager` instances keep working.
*/
@step()
async restart(): Promise<Page> {
const config = this.flowManager.launchConfig;
if (!config) {
@@ -61,6 +63,7 @@ export class AppFlow extends BaseFlow {
* `setImmediate` before returning, so the error has been fully
* handled by the time this resolves.
*/
@step()
async emitStdoutEpipeError(): Promise<void> {
await this.flowManager.electronApp!.evaluate(() => {
process.nextTick(() => {
@@ -79,6 +82,7 @@ export class AppFlow extends BaseFlow {
* event on the main process's `process.stdout` (e.g. `"error"`,
* `"close"`, `"drain"`).
*/
@step()
async getStdoutListenerCount(event: string): Promise<number> {
return this.flowManager.electronApp!.evaluate(
(_electron, evt) => process.stdout.listenerCount(evt),
@@ -91,6 +95,7 @@ export class AppFlow extends BaseFlow {
* trivial `evaluate()` call. If the main process had crashed, this
* call itself would reject with a "Target closed" error.
*/
@step()
async isAlive(): Promise<boolean> {
return this.flowManager.electronApp!.evaluate(() => true);
}
@@ -99,6 +104,7 @@ export class AppFlow extends BaseFlow {
* Reads the main process's `userData` directory — the same path
* Insomnia backs up its NeDB `.db` files under (`<dataPath>/backups`).
*/
@step()
async getDataPath(): Promise<string> {
return this.flowManager.electronApp!.evaluate(({ app }) =>
app.getPath("userData"),
@@ -112,6 +118,7 @@ export class AppFlow extends BaseFlow {
* "check for remote changes" logic on a real focus change, without
* actually needing to blur/refocus the Electron window from the OS.
*/
@step()
async simulateMainWindowFocusChange(): Promise<void> {
await this.flowManager.electronApp!.evaluate(({ BrowserWindow }) => {
for (const win of BrowserWindow.getAllWindows()) {
@@ -127,6 +134,7 @@ export class AppFlow extends BaseFlow {
* them in order. Only lines written during this call are captured —
* nothing from before it started.
*/
@step()
async captureMainProcessLog(action: () => Promise<void>): Promise<string[]> {
const lines: string[] = [];
const proc = this.flowManager.electronApp!.process();
@@ -153,6 +161,7 @@ export class AppFlow extends BaseFlow {
* to run.
* @throws If no window with that title is currently open
*/
@step()
async closeHiddenScriptWindow(): Promise<void> {
for (const win of await this.flowManager.electronApp!.windows()) {
if ((await win.title().catch(() => "")) === "Hidden Browser Window") {
@@ -1,3 +1,4 @@
import { step } from "../misc/step";
import type { ClientCertificate } from "../models/client-certificate";
import type { Collection } from "../models/collection";
import type { EventStreamRequest } from "../models/event-stream-request";
@@ -25,6 +26,7 @@ export class CertificatesFlow extends BaseFlow {
* Certificates" dialog from `item`'s location. Overwrites any CA
* certificate already set.
*/
@step()
async setCaCertificate(item: CertificateTarget, path: string): Promise<void> {
await this.openTarget(item);
@@ -38,6 +40,7 @@ export class CertificatesFlow extends BaseFlow {
* Adds a client certificate to `item`'s workspace, opening the "Manage
* Certificates" dialog from `item`'s location.
*/
@step()
async addClientCertificate(
item: CertificateTarget,
certificate: ClientCertificate,
@@ -55,6 +58,7 @@ export class CertificatesFlow extends BaseFlow {
* its `host`, opening the "Manage Certificates" dialog from `item`'s
* location.
*/
@step()
async setClientCertificateEnabled(
item: CertificateTarget,
host: string,
@@ -78,6 +82,7 @@ export class CertificatesFlow extends BaseFlow {
* Toggles `item`'s workspace-wide CA Certificate's Enabled/Disabled
* state, opening the "Manage Certificates" dialog from `item`'s location.
*/
@step()
async setCaCertificateEnabled(
item: CertificateTarget,
enabled: boolean,
@@ -94,6 +99,7 @@ export class CertificatesFlow extends BaseFlow {
* Toggles an MCP Client's "SSL Certificate Validation" switch, opening
* the "Manage Certificates" dialog from the client's location.
*/
@step()
async setSslValidationEnabled(
client: McpClient,
enabled: boolean,
@@ -1,4 +1,5 @@
import { DeleteMode } from "../enums/delete-mode";
import { step } from "../misc/step";
import type { Project } from "../models/project";
import { BaseFlow } from "./base.flow";
@@ -8,6 +9,7 @@ export class CloudSyncFlow extends BaseFlow {
* where synced, local-only, and unsynced remote files are all shown as
* cards side by side.
*/
@step()
async openProjectDashboard(project: Project): Promise<void> {
const { workspacePage, cloudSyncPage } = this.pageManager;
const node = await workspacePage.requireItemNode({ name: project.name });
@@ -19,6 +21,7 @@ export class CloudSyncFlow extends BaseFlow {
* Fetches a workspace that only exists on the Cloud Sync backend into
* the local sidebar and navigates into it.
*/
@step()
async fetch(name: string): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.backToAllProjects();
@@ -29,6 +32,7 @@ export class CloudSyncFlow extends BaseFlow {
* Discards every uncommitted local change on the current Cloud-synced
* workspace.
*/
@step()
async discardAllChanges(): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.discardAllChanges();
@@ -37,6 +41,7 @@ export class CloudSyncFlow extends BaseFlow {
/**
* Stages `requestName`, commits with `message`, then pushes.
*/
@step()
async commitAndPush(requestName: string, message: string): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.openCommitDialog();
@@ -46,6 +51,7 @@ export class CloudSyncFlow extends BaseFlow {
/**
* Stages `requestName` and commits with `message`, without pushing.
*/
@step()
async commit(requestName: string, message: string): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.openCommitDialog();
@@ -56,6 +62,7 @@ export class CloudSyncFlow extends BaseFlow {
* Restores the snapshot with commit message `message` from the current
* workspace's History.
*/
@step()
async restoreSnapshot(message: string): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.openHistory();
@@ -67,6 +74,7 @@ export class CloudSyncFlow extends BaseFlow {
* Creates a new branch named `branch` off whichever branch is
* currently checked out on the current Cloud-synced workspace.
*/
@step()
async createBranch(branch: string): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.openBranchesDialog();
@@ -78,6 +86,7 @@ export class CloudSyncFlow extends BaseFlow {
* Merges `branch` into whichever branch is currently checked out on
* the current Cloud-synced workspace.
*/
@step()
async mergeBranch(branch: string): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.openBranchesDialog();
@@ -89,6 +98,7 @@ export class CloudSyncFlow extends BaseFlow {
* Merges `branch` into whichever branch is currently checked out,
* expecting conflicts, and resolves all of them by taking the same side.
*/
@step()
async mergeBranchResolvingConflicts(
branch: string,
version: "Current" | "Incoming",
@@ -107,6 +117,7 @@ export class CloudSyncFlow extends BaseFlow {
* afterward) or "Delete Permanently" (removed both locally and on the
* backend).
*/
@step()
async delete(name: string, mode: DeleteMode): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.openDeleteWorkspaceDialog(name);
@@ -119,6 +130,7 @@ export class CloudSyncFlow extends BaseFlow {
* archiving the backend project by its own id (no local pull required).
* Requires `openProjectDashboard()` to have already navigated there.
*/
@step()
async deleteUnsyncedFile(name: string): Promise<void> {
const { cloudSyncPage } = this.pageManager;
await cloudSyncPage.openFileDeleteDialog(name);
@@ -1,3 +1,4 @@
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import {
Environment,
@@ -31,6 +32,7 @@ export class EnvironmentFlow extends BaseFlow {
* Creates/renames the workspace's Base Environment under `parent`, then
* applies `environment.kvPairData`.
*/
@step()
async createBaseEnvironment(parent: Project, environment: Environment): Promise<Environment> {
await this.workspaceFlow.createEnvironment(parent, environment);
if (environment.name !== BASE_ENVIRONMENT_NAME) {
@@ -47,6 +49,7 @@ export class EnvironmentFlow extends BaseFlow {
* Creates a sub-environment nested under `parent`, then applies
* `environment.kvPairData`.
*/
@step()
async createSubEnvironment(parent: Environment, environment: Environment): Promise<Environment> {
await this.pageManager.environmentPage.createSubEnvironment(
environment.name,
@@ -59,6 +62,7 @@ export class EnvironmentFlow extends BaseFlow {
/**
* Finds an environment by name/id and reads its variables back.
*/
@step()
async get(
item: string | { name: string; id?: string },
): Promise<Environment | undefined> {
@@ -88,6 +92,7 @@ export class EnvironmentFlow extends BaseFlow {
* Links a project Environment into `item` via the "Select a Project
* Environment" picker.
*/
@step()
async link(
item:
| Collection
@@ -123,6 +128,7 @@ export class EnvironmentFlow extends BaseFlow {
* Distinct from `link()`, which instead attaches a separate Project
* Environment; both can be active on the same collection at once.
*/
@step()
async activate(collection: Collection, name: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.resolveNode(collection);
@@ -134,6 +140,7 @@ export class EnvironmentFlow extends BaseFlow {
* Opens one environment picker on `collection`'s toolbar and reports
* which picker popovers are open, then dismisses it.
*/
@step()
async getOpenPickersAfterOpening(
collection: Collection,
picker: "project" | "collection",
@@ -153,6 +160,7 @@ export class EnvironmentFlow extends BaseFlow {
* Creates a Project Environment from `collection`'s project picker and
* reads the picker back, without linking the new environment.
*/
@step()
async addProjectEnvironmentFromPicker(
collection: Collection,
name: string,
@@ -170,6 +178,7 @@ export class EnvironmentFlow extends BaseFlow {
* Creates a shared or private sub-environment inside `collection`'s own
* "Manage Environments" modal and reports whether it is marked private.
*/
@step()
async createCollectionSubEnvironment(
collection: Collection,
environment: Environment,
@@ -2,6 +2,7 @@ import type { RequestAuthentication } from "insomnia-data";
import { ContextMenuItem } from "../enums/context-menu-items";
import { TreeNodeType } from "../enums/tree-node-types";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import { EventStreamRequest } from "../models/event-stream-request";
import type { Folder } from "../models/folder";
@@ -15,6 +16,7 @@ export class EventStreamRequestFlow extends BaseFlow {
* Creates an Event Stream request under `parent` via the standalone
* project tree, then verifies it was created.
*/
@step()
async create(
parent: Collection | Folder,
request: EventStreamRequest,
@@ -45,6 +47,7 @@ export class EventStreamRequestFlow extends BaseFlow {
* Reads back an existing Event Stream request's fields via the
* standalone project tree.
*/
@step()
async get(
item: string | { name: string; id?: string },
parent?: Collection,
@@ -71,6 +74,7 @@ export class EventStreamRequestFlow extends BaseFlow {
* Opens `request`, clicks its Auth tab's token-fetch button and reads
* the resulting tokens back.
*/
@step()
async fetchOAuth2Tokens(request: EventStreamRequest): Promise<OAuth2Tokens> {
const workspace = this.pageManager.workspacePage;
const page = this.pageManager.eventStreamRequestPage;
@@ -86,6 +90,7 @@ export class EventStreamRequestFlow extends BaseFlow {
/**
* Opens `request` and applies `authentication` on its Auth tab.
*/
@step()
async setAuthentication(
request: EventStreamRequest,
authentication: RequestAuthentication,
@@ -103,6 +108,7 @@ export class EventStreamRequestFlow extends BaseFlow {
* Connects the event stream for `request` and reads back the resulting
* response via the standalone project tree.
*/
@step()
async send(
request: EventStreamRequest,
options?: { expectConnected?: boolean },
@@ -1,6 +1,7 @@
import type { ApiSpecExportFormat } from "../enums/api-spec-export-format";
import { ContextMenuItem } from "../enums/context-menu-items";
import type { ExportFormat } from "../enums/export-format";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type { Project } from "../models/project";
import { BaseFlow } from "./base.flow";
@@ -11,6 +12,7 @@ export class ExportFlow extends BaseFlow {
* "Export" context-menu item, confirming the request-selection step
* (every request stays checked).
*/
@step()
async exportCollection(item: Collection, format: ExportFormat, path: string): Promise<void> {
const { workspacePage, exportPage } = this.pageManager;
const node = await workspacePage.resolveNode(item);
@@ -28,6 +30,7 @@ export class ExportFlow extends BaseFlow {
* each) via Preferences -> Data, selecting `format`. Skips the
* request-selection step, which only Collection-scoped exports show.
*/
@step()
async exportProject(item: Project, format: ExportFormat, path: string): Promise<void> {
const { workspacePage, preferencesPage, exportPage } = this.pageManager;
await workspacePage.clickNode(await workspacePage.resolveNode(item));
@@ -47,6 +50,7 @@ export class ExportFlow extends BaseFlow {
* workspace) via Preferences -> Data. Always writes as Insomnia v5 —
* this entry point has no request-selection or format-selection step.
*/
@step()
async exportAllData(directoryPath: string): Promise<void> {
const { preferencesPage, exportPage } = this.pageManager;
await exportPage.stubSaveDirectoryLocation(directoryPath);
@@ -67,6 +71,7 @@ export class ExportFlow extends BaseFlow {
* so this rejects with that alert's trimmed text in that case; dismiss
* it with `workspaceFlow.closeDialog()` before continuing.
*/
@step()
async exportOpenApiSpec(
item: Collection,
format: ApiSpecExportFormat,
+23 -68
View File
@@ -1,6 +1,5 @@
import type { ElectronApplication } from "@playwright/test";
import { instrumentWithSteps } from "../misc/step-instrumentation";
import type { PageManager } from "../pages/page-manager";
import { AppFlow } from "./app.flow";
import { CertificatesFlow } from "./certificates.flow";
@@ -89,140 +88,96 @@ export class FlowManager implements FlowContext {
}
get appFlow(): AppFlow {
return (this._appFlow ??= instrumentWithSteps(
new AppFlow(this, this.pageManager),
));
return (this._appFlow ??= new AppFlow(this, this.pageManager));
}
get certificatesFlow(): CertificatesFlow {
return (this._certificatesFlow ??= instrumentWithSteps(
new CertificatesFlow(this, this.pageManager),
));
return (this._certificatesFlow ??= new CertificatesFlow(this, this.pageManager));
}
get cloudSyncFlow(): CloudSyncFlow {
return (this._cloudSyncFlow ??= instrumentWithSteps(
new CloudSyncFlow(this, this.pageManager),
));
return (this._cloudSyncFlow ??= new CloudSyncFlow(this, this.pageManager));
}
get cookieFlow(): CookieFlow {
return (this._cookieFlow ??= instrumentWithSteps(
new CookieFlow(this, this.pageManager),
));
return (this._cookieFlow ??= new CookieFlow(this, this.pageManager));
}
get environmentFlow(): EnvironmentFlow {
return (this._environmentFlow ??= instrumentWithSteps(
new EnvironmentFlow(this, this.pageManager, this.workspaceFlow),
));
return (this._environmentFlow ??= new EnvironmentFlow(this, this.pageManager, this.workspaceFlow));
}
get eventStreamRequestFlow(): EventStreamRequestFlow {
return (this._eventStreamRequestFlow ??= instrumentWithSteps(
new EventStreamRequestFlow(this, this.pageManager),
));
return (this._eventStreamRequestFlow ??= new EventStreamRequestFlow(this, this.pageManager));
}
get exportFlow(): ExportFlow {
return (this._exportFlow ??= instrumentWithSteps(
new ExportFlow(this, this.pageManager),
));
return (this._exportFlow ??= new ExportFlow(this, this.pageManager));
}
get folderFlow(): FolderFlow {
return (this._folderFlow ??= instrumentWithSteps(
new FolderFlow(this, this.pageManager),
));
return (this._folderFlow ??= new FolderFlow(this, this.pageManager));
}
get gitSyncFlow(): GitSyncFlow {
return (this._gitSyncFlow ??= instrumentWithSteps(
new GitSyncFlow(this, this.pageManager),
));
return (this._gitSyncFlow ??= new GitSyncFlow(this, this.pageManager));
}
get graphqlRequestFlow(): GraphQLRequestFlow {
return (this._graphqlRequestFlow ??= instrumentWithSteps(
new GraphQLRequestFlow(this, this.pageManager),
));
return (this._graphqlRequestFlow ??= new GraphQLRequestFlow(this, this.pageManager));
}
get grpcRequestFlow(): GrpcRequestFlow {
return (this._grpcRequestFlow ??= instrumentWithSteps(
new GrpcRequestFlow(this, this.pageManager),
));
return (this._grpcRequestFlow ??= new GrpcRequestFlow(this, this.pageManager));
}
get httpRequestFlow(): HttpRequestFlow {
return (this._httpRequestFlow ??= instrumentWithSteps(
new HttpRequestFlow(this, this.pageManager),
));
return (this._httpRequestFlow ??= new HttpRequestFlow(this, this.pageManager));
}
get importFlow(): ImportFlow {
return (this._importFlow ??= instrumentWithSteps(
new ImportFlow(this, this.pageManager),
));
return (this._importFlow ??= new ImportFlow(this, this.pageManager));
}
get mcpClientFlow(): McpClientFlow {
return (this._mcpClientFlow ??= instrumentWithSteps(
new McpClientFlow(this, this.pageManager, this.workspaceFlow),
));
return (this._mcpClientFlow ??= new McpClientFlow(this, this.pageManager, this.workspaceFlow));
}
get mockServerFlow(): MockServerFlow {
return (this._mockServerFlow ??= instrumentWithSteps(
new MockServerFlow(this, this.pageManager),
));
return (this._mockServerFlow ??= new MockServerFlow(this, this.pageManager));
}
get organizationFlow(): OrganizationFlow {
return (this._organizationFlow ??= instrumentWithSteps(
new OrganizationFlow(this, this.pageManager),
));
return (this._organizationFlow ??= new OrganizationFlow(this, this.pageManager));
}
get preferencesFlow(): PreferencesFlow {
return (this._preferencesFlow ??= instrumentWithSteps(
new PreferencesFlow(this, this.pageManager),
));
return (this._preferencesFlow ??= new PreferencesFlow(this, this.pageManager));
}
get socketIoRequestFlow(): SocketIORequestFlow {
return (this._socketIoRequestFlow ??= instrumentWithSteps(
new SocketIORequestFlow(this, this.pageManager),
));
return (this._socketIoRequestFlow ??= new SocketIORequestFlow(this, this.pageManager));
}
get templateTagFlow(): TemplateTagFlow {
return (this._templateTagFlow ??= instrumentWithSteps(
new TemplateTagFlow(this, this.pageManager),
));
return (this._templateTagFlow ??= new TemplateTagFlow(this, this.pageManager));
}
get variableSourceFlow(): VariableSourceFlow {
return (this._variableSourceFlow ??= instrumentWithSteps(
new VariableSourceFlow(this, this.pageManager),
));
return (this._variableSourceFlow ??= new VariableSourceFlow(this, this.pageManager));
}
get webSocketRequestFlow(): WebSocketRequestFlow {
return (this._webSocketRequestFlow ??= instrumentWithSteps(
new WebSocketRequestFlow(this, this.pageManager),
));
return (this._webSocketRequestFlow ??= new WebSocketRequestFlow(this, this.pageManager));
}
get workspaceFlow(): WorkspaceFlow {
return (this._workspaceFlow ??= instrumentWithSteps(
new WorkspaceFlow(
return (this._workspaceFlow ??= new WorkspaceFlow(
this,
this.pageManager,
this.preferencesFlow,
this,
this.gitRepoUrl,
),
));
));
}
}
@@ -1,5 +1,6 @@
import { ContextMenuItem } from "../enums/context-menu-items";
import { TreeNodeType } from "../enums/tree-node-types";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import { Folder } from "../models/folder";
import type { OAuth2Tokens } from "../pages/auth-tab.page";
@@ -14,6 +15,7 @@ export class FolderFlow extends BaseFlow {
* request (see `HttpRequestFlow.create()`), just with a different
* context-menu item and a plain naming dialog instead of a rename step.
*/
@step()
async create(parent: Collection | Folder, folder: Folder): Promise<Folder> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.resolveNode(parent);
@@ -44,6 +46,7 @@ export class FolderFlow extends BaseFlow {
* Docs) via its "Open in New Tab" context-menu item — not the same as
* its "Settings" item, which only opens a rename/move dialog.
*/
@step()
async open(folder: Folder): Promise<FolderPage> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.findItemNode(
@@ -65,6 +68,7 @@ export class FolderFlow extends BaseFlow {
* token exists, "Refresh Token" once one does), then reads back the
* resulting Refresh/Identity/Access Token fields in one call.
*/
@step()
async fetchOAuth2Tokens(folder: Folder): Promise<OAuth2Tokens> {
const folderPage = await this.open(folder);
await folderPage.auth.fetchOAuth2Tokens();
@@ -76,6 +80,7 @@ export class FolderFlow extends BaseFlow {
* scripts (see `ScriptTabComponent.setScripts()`) — these run for
* every descendant request before/after that request's own script.
*/
@step()
async setScripts(folder: Folder, scripts: RequestScripts): Promise<void> {
const folderPage = await this.open(folder);
await folderPage.scripts.setScripts(scripts);
@@ -86,6 +91,7 @@ export class FolderFlow extends BaseFlow {
* override the collection's base and sub-environment values for every
* descendant request.
*/
@step()
async setEnvironmentVariables(folder: Folder, variables: Record<string, string>): Promise<void> {
const folderPage = await this.open(folder);
await folderPage.setEnvironmentVariables(variables);
@@ -1,5 +1,6 @@
import { GitChangeList } from "../enums/git-change-list";
import { GitDiffView } from "../enums/git-diff-view";
import { step } from "../misc/step";
import { Commit } from "../models/commit";
import { BaseFlow } from "./base.flow";
@@ -7,6 +8,7 @@ export class GitSyncFlow extends BaseFlow {
/**
* Checks out `branch` directly from the Git Sync dropdown's branch list.
*/
@step()
async switchBranch(branch: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.switchBranch(branch);
@@ -16,6 +18,7 @@ export class GitSyncFlow extends BaseFlow {
* Stages all changes, commits with `message` (no push), and reads back
* the resulting commit's id from History.
*/
@step()
async commit(message: string): Promise<Commit> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -37,6 +40,7 @@ export class GitSyncFlow extends BaseFlow {
* Stages all changes, commits with `message`, then pushes, and reads
* back the resulting commit's id from History.
*/
@step()
async commitAndPush(message: string): Promise<Commit> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -58,6 +62,7 @@ export class GitSyncFlow extends BaseFlow {
* Reads the file names currently listed as staged and unstaged in the
* Commit modal, then closes it.
*/
@step()
async getChanges(): Promise<{ staged: string[]; unstaged: string[] }> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -71,6 +76,7 @@ export class GitSyncFlow extends BaseFlow {
* Stages each of `fileNames` individually from the Commit modal, then
* closes it.
*/
@step()
async stageChanges(fileNames: string[]): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -84,6 +90,7 @@ export class GitSyncFlow extends BaseFlow {
* Unstages each of `fileNames` individually from the Commit modal, then
* closes it.
*/
@step()
async unstageChanges(fileNames: string[]): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -98,6 +105,7 @@ export class GitSyncFlow extends BaseFlow {
* closes the Commit modal if it's still open, and reads the resulting
* commit back from History.
*/
@step()
async commitStaged(message: string): Promise<Commit> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -122,6 +130,7 @@ export class GitSyncFlow extends BaseFlow {
/**
* Discards every uncommitted local change.
*/
@step()
async discardAllChanges(): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -132,6 +141,7 @@ export class GitSyncFlow extends BaseFlow {
* Creates `branch` from the current branch, which switches to it
* immediately.
*/
@step()
async createBranch(branch: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openBranchesDialog();
@@ -143,6 +153,7 @@ export class GitSyncFlow extends BaseFlow {
* Attempts to create `branch` from the Branches modal and reads back the
* validation error the app shows instead, then closes the modal.
*/
@step()
async createBranchExpectingError(branch: string): Promise<string> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openBranchesDialog();
@@ -156,6 +167,7 @@ export class GitSyncFlow extends BaseFlow {
* Reads the local and remote-only branch lists from the Branches modal,
* then closes it.
*/
@step()
async getBranches(): Promise<{ local: string[]; remote: string[] }> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openBranchesDialog();
@@ -169,6 +181,7 @@ export class GitSyncFlow extends BaseFlow {
* Fetches `branch` (a branch that exists only on the remote) and checks
* it out via the Branches modal.
*/
@step()
async checkoutRemoteBranch(branch: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openBranchesDialog();
@@ -179,6 +192,7 @@ export class GitSyncFlow extends BaseFlow {
/**
* Fetches from the remote via the Git Sync dropdown.
*/
@step()
async fetch(): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.fetch();
@@ -187,6 +201,7 @@ export class GitSyncFlow extends BaseFlow {
/**
* Pulls the current branch from the remote via the Git Sync dropdown.
*/
@step()
async pull(): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.pull();
@@ -196,6 +211,7 @@ export class GitSyncFlow extends BaseFlow {
* Pushes the current branch's commits to the remote via the Git Sync
* dropdown's standalone "Push" action.
*/
@step()
async push(): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.push();
@@ -204,6 +220,7 @@ export class GitSyncFlow extends BaseFlow {
/**
* Merges `branch` into whichever branch is currently checked out.
*/
@step()
async mergeBranch(branch: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openBranchesDialog();
@@ -215,6 +232,7 @@ export class GitSyncFlow extends BaseFlow {
* Merges `branch` into the current branch, expecting conflicts, and
* resolves all of them by taking the same side.
*/
@step()
async mergeBranchResolvingConflicts(
branch: string,
version: "Current" | "Incoming",
@@ -230,6 +248,7 @@ export class GitSyncFlow extends BaseFlow {
* Checks out `switchToBranch` (since a branch can't be deleted while
* checked out) and deletes `branch`.
*/
@step()
async deleteBranch(branch: string, switchToBranch = "master"): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openBranchesDialog();
@@ -243,6 +262,7 @@ export class GitSyncFlow extends BaseFlow {
* clicking Commit, continues past the "Unrecognized Fields" confirmation
* before reading the resulting commit back from History.
*/
@step()
async commitStagedConfirmingUnknownFields(message: string): Promise<Commit> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -270,6 +290,7 @@ export class GitSyncFlow extends BaseFlow {
* waiting for the file to leave the unstaged list (so it also works for a
* change that comes straight back), then closes the modal.
*/
@step()
async discardChangeAfterReview(fileName: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await gitSyncPage.openCommitDialog();
@@ -283,6 +304,7 @@ export class GitSyncFlow extends BaseFlow {
* Opens the Commit modal (if it isn't already open), selects a file in
* the given list and switches its diff pane to the Visual (Beta) view.
*/
@step()
async openVisualDiff(list: GitChangeList, fileName: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
if (!(await gitSyncPage.isCommitDialogOpen())) {
@@ -295,6 +317,7 @@ export class GitSyncFlow extends BaseFlow {
/**
* Stages one entity from its card in the Visual view of an unstaged file.
*/
@step()
async stageEntity(fileName: string, entityName: string, typeLabel?: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await this.openVisualDiff(GitChangeList.Unstaged, fileName);
@@ -304,6 +327,7 @@ export class GitSyncFlow extends BaseFlow {
/**
* Unstages one entity from its card in the Visual view of a staged file.
*/
@step()
async unstageEntity(fileName: string, entityName: string, typeLabel?: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await this.openVisualDiff(GitChangeList.Staged, fileName);
@@ -314,6 +338,7 @@ export class GitSyncFlow extends BaseFlow {
* Discards one entity from its card in the Visual view of an unstaged
* file, confirming the prompt.
*/
@step()
async discardEntity(fileName: string, entityName: string, typeLabel?: string): Promise<void> {
const { gitSyncPage } = this.pageManager;
await this.openVisualDiff(GitChangeList.Unstaged, fileName);
@@ -2,6 +2,7 @@ import type { RequestAuthentication } from "insomnia-data";
import { ContextMenuItem } from "../enums/context-menu-items";
import { TreeNodeType } from "../enums/tree-node-types";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type { Folder } from "../models/folder";
import { GraphQLRequest } from "../models/graphql-request";
@@ -15,6 +16,7 @@ export class GraphQLRequestFlow extends BaseFlow {
* Creates a GraphQL request under `parent` via the standalone project
* tree, then verifies it was created.
*/
@step()
async create(
parent: Collection | Folder,
request: GraphQLRequest,
@@ -41,6 +43,7 @@ export class GraphQLRequestFlow extends BaseFlow {
* Reads back an existing GraphQL request's fields via the standalone
* project tree.
*/
@step()
async get(
item: string | { name: string; id?: string },
parent?: Collection,
@@ -63,6 +66,7 @@ export class GraphQLRequestFlow extends BaseFlow {
* Opens `request`, clicks its Auth tab's token-fetch button and reads
* the resulting tokens back.
*/
@step()
async fetchOAuth2Tokens(request: GraphQLRequest): Promise<OAuth2Tokens> {
const workspace = this.pageManager.workspacePage;
const page = this.pageManager.graphqlRequestPage;
@@ -78,6 +82,7 @@ export class GraphQLRequestFlow extends BaseFlow {
/**
* Opens `request` and applies `authentication` on its Auth tab.
*/
@step()
async setAuthentication(
request: GraphQLRequest,
authentication: RequestAuthentication,
@@ -95,6 +100,7 @@ export class GraphQLRequestFlow extends BaseFlow {
* Sends `request` and reads back the resulting response via the
* standalone project tree.
*/
@step()
async send(request: GraphQLRequest): Promise<ResponseView> {
const workspace = this.pageManager.workspacePage;
const graphqlRequestPage = this.pageManager.graphqlRequestPage;
@@ -1,5 +1,6 @@
import { ContextMenuItem } from "../enums/context-menu-items";
import { TreeNodeType } from "../enums/tree-node-types";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type { Folder } from "../models/folder";
import { GrpcRequest } from "../models/grpc-request";
@@ -17,6 +18,7 @@ export class GrpcRequestFlow extends BaseFlow {
* Creates a gRPC request under `parent` via the standalone project tree,
* then verifies it was created.
*/
@step()
async create(parent: Collection | Folder, request: GrpcRequest): Promise<GrpcRequest> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.resolveNode(parent);
@@ -40,6 +42,7 @@ export class GrpcRequestFlow extends BaseFlow {
* Reads back an existing gRPC request's fields via the standalone
* project tree.
*/
@step()
async get(
item: string | { name: string; id?: string },
parent?: Collection,
@@ -63,6 +66,7 @@ export class GrpcRequestFlow extends BaseFlow {
* server-streaming call, or an open client/bidi-streaming call — then
* reads back the resulting gRPC status.
*/
@step()
async cancel(request: GrpcRequest): Promise<GrpcResponse> {
const grpcRequestPage = await this.openRequest(request);
await grpcRequestPage.cancel();
@@ -74,6 +78,7 @@ export class GrpcRequestFlow extends BaseFlow {
* bidi-streaming call by clicking "Commit", then reads back the
* resulting gRPC status and message(s).
*/
@step()
async commit(request: GrpcRequest): Promise<GrpcResponse> {
const grpcRequestPage = await this.openRequest(request);
await grpcRequestPage.commit();
@@ -88,6 +93,7 @@ export class GrpcRequestFlow extends BaseFlow {
* response arrives until `commit()` closes the client's half of the
* stream.
*/
@step()
async send(request: GrpcRequest): Promise<GrpcResponse> {
const grpcRequestPage = await this.openRequest(request);
await grpcRequestPage.send();
@@ -99,6 +105,7 @@ export class GrpcRequestFlow extends BaseFlow {
* Unlike `send()`, this does not wait for a response — none arrives
* until `commit()` closes the client's half of the stream.
*/
@step()
async start(request: GrpcRequest): Promise<void> {
const grpcRequestPage = await this.openRequest(request);
await grpcRequestPage.send();
@@ -109,6 +116,7 @@ export class GrpcRequestFlow extends BaseFlow {
* on disk and in the request pane's loader data, so a following
* `send()` can't go out with the previous body.
*/
@step()
async setBody(request: GrpcRequest, body: string): Promise<void> {
if (!request.id) {
throw new Error(
@@ -126,6 +134,7 @@ export class GrpcRequestFlow extends BaseFlow {
* bidi-streaming call. Can be called repeatedly with different bodies
* before calling `commit()`.
*/
@step()
async streamMessage(request: GrpcRequest, body: string): Promise<void> {
if (!request.id) {
throw new Error(
@@ -1,6 +1,7 @@
import { ContextMenuItem } from "../enums/context-menu-items";
import type { ResponseExportOption } from "../enums/response-export-option";
import { TreeNodeType } from "../enums/tree-node-types";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type { Folder } from "../models/folder";
import type { RequestAuthentication, RequestParameter } from "../models/http-request";
@@ -15,6 +16,7 @@ export class HttpRequestFlow extends BaseFlow {
* Creates a new HTTP request under `parent`, naming it via the rename
* flow and applying its fields on the request page.
*/
@step()
async create(parent: Collection | Folder, request: HttpRequest): Promise<HttpRequest> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.resolveNode(parent);
@@ -39,6 +41,7 @@ export class HttpRequestFlow extends BaseFlow {
* editor. Use after `create()` for requests with many params, where
* per-row editing is prone to a stale-row-list save race.
*/
@step()
async setParamsBulk(request: HttpRequest, params: RequestParameter[]): Promise<void> {
const workspace = this.pageManager.workspacePage;
const httpRequestPage = this.pageManager.httpRequestPage;
@@ -53,6 +56,7 @@ export class HttpRequestFlow extends BaseFlow {
* Reads back an existing request's fields by name or identity, looking
* it up in the standalone project tree.
*/
@step()
async get(
item: string | { name: string; id?: string },
): Promise<HttpRequest | undefined> {
@@ -74,6 +78,7 @@ export class HttpRequestFlow extends BaseFlow {
* Sends `request` and returns the resulting response, locating it in the
* standalone project tree.
*/
@step()
async send(request: HttpRequest): Promise<ResponseView> {
const workspace = this.pageManager.workspacePage;
const httpRequestPage = this.pageManager.httpRequestPage;
@@ -96,6 +101,7 @@ export class HttpRequestFlow extends BaseFlow {
* any token exists, "Refresh Token" once one does), then reads back
* the resulting Refresh/Identity/Access Token fields in one call.
*/
@step()
async fetchOAuth2Tokens(request: HttpRequest): Promise<OAuth2Tokens> {
const workspace = this.pageManager.workspacePage;
const httpRequestPage = this.pageManager.httpRequestPage;
@@ -112,6 +118,7 @@ export class HttpRequestFlow extends BaseFlow {
* Exports the response currently shown for the last-sent request to
* `filePath` via the response pane's "Preview" dropdown.
*/
@step()
async exportResponse(option: ResponseExportOption, filePath: string): Promise<void> {
await this.pageManager.responsePage.exportResponse(option, filePath);
}
@@ -120,6 +127,7 @@ export class HttpRequestFlow extends BaseFlow {
* Saves the large response currently shown for the last-sent request to
* `filePath` via the response pane's "Save To File" button.
*/
@step()
async saveResponseToFile(filePath: string): Promise<void> {
await this.pageManager.responsePage.saveResponseToFile(filePath);
}
@@ -128,6 +136,7 @@ export class HttpRequestFlow extends BaseFlow {
* Reads an export menu item's label and disabled state for the response
* currently shown.
*/
@step()
async getResponseExportOptionState(
option: ResponseExportOption,
): Promise<{ text: string; disabled: boolean }> {
@@ -1,5 +1,6 @@
import { ContextMenuItem } from "../enums/context-menu-items";
import { ImportSource } from "../enums/import-sources";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type { CurlCommand } from "../models/curl-command";
import type { Project } from "../models/project";
@@ -10,6 +11,7 @@ export class ImportFlow extends BaseFlow {
* Opens the Import dialog from `project`'s context menu, scans clipboard
* content pasted into the import source, then imports it into the project.
*/
@step()
async importClipboard(project: Project, content: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
const projectNode = (await workspace.getTree())
@@ -32,6 +34,7 @@ export class ImportFlow extends BaseFlow {
* Opens the Import dialog from `project`'s context menu, scans a raw curl
* command, then imports it into the given collection within that project.
*/
@step()
async importCurl(
project: Project,
collection: Collection,
@@ -60,6 +63,7 @@ export class ImportFlow extends BaseFlow {
* Opens the Import dialog from `project`'s context menu, scans a local
* file, then imports it into the project.
*/
@step()
async importFile(project: Project, filePath: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
const projectNode = (await workspace.getTree())
@@ -82,6 +86,7 @@ export class ImportFlow extends BaseFlow {
* Opens the Import dialog from `project`'s context menu, scans an MCP
* server URL, then imports it into the project.
*/
@step()
async importMcp(project: Project, url: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
const projectNode = (await workspace.getTree())
@@ -104,6 +109,7 @@ export class ImportFlow extends BaseFlow {
* Opens the Import dialog from `project`'s context menu, scans content
* fetched from a URL, then imports it into the project.
*/
@step()
async importUrl(project: Project, url: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
const projectNode = (await workspace.getTree())
@@ -2,6 +2,7 @@ import { expect } from "@playwright/test";
import { type McpCapabilityType, McpListAction } from "../enums/mcp-capability";
import { DEFAULT_TIMEOUT } from "../misc/constants";
import { step } from "../misc/step";
import { McpClient } from "../models/mcp-client";
import type { Project } from "../models/project";
import type { ResponseView } from "../models/response";
@@ -27,6 +28,7 @@ export class McpClientFlow extends BaseFlow {
* triggering/awaiting side effects the tool call depends on) before
* reading the resulting response.
*/
@step()
async callTool(
client: McpClient,
tool: string,
@@ -61,6 +63,7 @@ export class McpClientFlow extends BaseFlow {
* Opens the given MCP Client node and clicks its disconnect action,
* waiting for the connection to close.
*/
@step()
async disconnect(client: McpClient): Promise<void> {
const workspace = this.pageManager.workspacePage;
const mcpClientPage = this.pageManager.mcpClientPage;
@@ -75,6 +78,7 @@ export class McpClientFlow extends BaseFlow {
* Opens `client`, clicks its Auth tab's token-fetch button and reads the
* resulting tokens back.
*/
@step()
async fetchOAuth2Tokens(client: McpClient): Promise<OAuth2Tokens> {
const workspace = this.pageManager.workspacePage;
const mcpClientPage = this.pageManager.mcpClientPage;
@@ -90,6 +94,7 @@ export class McpClientFlow extends BaseFlow {
/**
* Opens the given MCP Client node and clicks its connect action.
*/
@step()
async connect(
client: McpClient,
options?: { expectConnected?: boolean },
@@ -107,6 +112,7 @@ export class McpClientFlow extends BaseFlow {
* Creates an MCP Client node under `parent` (delegating the tree creation
* to `workspaceFlow.createMcpClient`), then opens it and sets its URL.
*/
@step()
async create(parent: Project, client: McpClient): Promise<McpClient> {
await this.workspaceFlow.createMcpClient(parent, client);
@@ -139,6 +145,7 @@ export class McpClientFlow extends BaseFlow {
* Finds the MCP Client node matching `item` in the workspace tree, opens
* it, and reads back its configured URL.
*/
@step()
async get(
item: string | { name: string; id?: string },
): Promise<McpClient | undefined> {
@@ -161,6 +168,7 @@ export class McpClientFlow extends BaseFlow {
/**
* Opens the given MCP Client node and lists the names of its available tools.
*/
@step()
async getTools(client: McpClient): Promise<string[]> {
const workspace = this.pageManager.workspacePage;
const mcpClientPage = this.pageManager.mcpClientPage;
@@ -175,6 +183,7 @@ export class McpClientFlow extends BaseFlow {
* Opens the given MCP Client node and reads the error panel of one of its
* sidebar capability items, waiting for it to appear.
*/
@step()
async getListError(client: McpClient, type: McpCapabilityType): Promise<McpCapabilityError> {
await this.open(client);
return this.pageManager.mcpClientPage.getCapabilityError(type);
@@ -184,6 +193,7 @@ export class McpClientFlow extends BaseFlow {
* Opens the given MCP Client node and collapses or expands one of its
* sidebar capability items.
*/
@step()
async toggleCapability(client: McpClient, type: McpCapabilityType): Promise<void> {
await this.open(client);
await this.pageManager.mcpClientPage.toggleCapability(type);
@@ -193,6 +203,7 @@ export class McpClientFlow extends BaseFlow {
* Opens the given MCP Client node and re-runs one capability item's list
* request via its actions menu, re-deriving its entries and error.
*/
@step()
async refresh(client: McpClient, type: McpCapabilityType): Promise<void> {
await this.open(client);
await this.pageManager.mcpClientPage.clickCapabilityAction(type, McpListAction.Reload);
@@ -203,6 +214,7 @@ export class McpClientFlow extends BaseFlow {
* item's list via its actions menu (offered once the server returned a
* `nextCursor`).
*/
@step()
async loadMore(client: McpClient, type: McpCapabilityType): Promise<void> {
await this.open(client);
await this.pageManager.mcpClientPage.clickCapabilityAction(type, McpListAction.LoadMore);
@@ -212,6 +224,7 @@ export class McpClientFlow extends BaseFlow {
* Opens the given MCP Client node and reads which list action one of its
* capability items currently offers.
*/
@step()
async getListAction(client: McpClient, type: McpCapabilityType): Promise<McpListAction> {
await this.open(client);
return this.pageManager.mcpClientPage.getCapabilityActionName(type);
@@ -220,6 +233,7 @@ export class McpClientFlow extends BaseFlow {
/**
* Opens the given MCP Client node and filters its sidebar primitives by name.
*/
@step()
async filterCapabilities(client: McpClient, text: string): Promise<void> {
await this.open(client);
await this.pageManager.mcpClientPage.setCapabilityFilter(text);
@@ -2,6 +2,7 @@ import { expect } from "@playwright/test";
import { ContextMenuItem } from "../enums/context-menu-items";
import { DEFAULT_TIMEOUT } from "../misc/constants";
import { step } from "../misc/step";
import type { MockRoute, MockRouteHistoryEntry } from "../models/mock-route";
import { MockServer } from "../models/mock-server";
import type { Project } from "../models/project";
@@ -13,6 +14,7 @@ export class MockServerFlow extends BaseFlow {
* Creates a self-hosted Mock Server under `parent` through the project's
* create menu and the create dialog, then re-reads it from the tree.
*/
@step()
async create(parent: Project, server: MockServer): Promise<MockServer> {
const workspace = this.pageManager.workspacePage;
const mockServerPage = this.pageManager.mockServerPage;
@@ -30,6 +32,7 @@ export class MockServerFlow extends BaseFlow {
/**
* Finds a Mock Server's tree node by name/id and returns it as a model.
*/
@step()
async get(item: string | { name: string; id?: string }): Promise<MockServer> {
const identity = typeof item === "string" ? { name: item } : item;
const node = await this.pageManager.workspacePage.findItemNode(identity);
@@ -41,6 +44,7 @@ export class MockServerFlow extends BaseFlow {
* Duplicates `server` through its context menu's "Duplicate / Move"
* dialog.
*/
@step()
async duplicate(
server: MockServer,
newName: string,
@@ -62,6 +66,7 @@ export class MockServerFlow extends BaseFlow {
* Exports `server` (as an Insomnia v5 YAML file) through its context
* menu's "Export" item, writing it to `path`.
*/
@step()
async export(server: MockServer, path: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
const exportPage = this.pageManager.exportPage;
@@ -75,6 +80,7 @@ export class MockServerFlow extends BaseFlow {
/**
* Opens `server`'s Mock Server view.
*/
@step()
async open(server: MockServer): Promise<void> {
const workspace = this.pageManager.workspacePage;
const mockServerPage = this.pageManager.mockServerPage;
@@ -89,6 +95,7 @@ export class MockServerFlow extends BaseFlow {
* then, if it carries any, configures its response (status code/text,
* body type, body and headers).
*/
@step()
async createRoute(server: MockServer, route: MockRoute): Promise<MockRoute> {
const mockServerPage = this.pageManager.mockServerPage;
@@ -105,6 +112,7 @@ export class MockServerFlow extends BaseFlow {
* status code, status text, body type, body and headers — then moves
* focus off the last edited editor so it pushes to the server.
*/
@step()
async setResponse(route: MockRoute): Promise<void> {
const mockServerPage = this.pageManager.mockServerPage;
@@ -140,6 +148,7 @@ export class MockServerFlow extends BaseFlow {
* Opens `route` and edits its path and/or method through its action
* menu's "Edit" dialog.
*/
@step()
async editRoute(
route: MockRoute,
changes: Pick<MockRoute, "path" | "method">,
@@ -153,6 +162,7 @@ export class MockServerFlow extends BaseFlow {
/**
* Deletes `route` through its action menu and the confirmation dialog.
*/
@step()
async deleteRoute(route: MockRoute): Promise<void> {
await this.pageManager.mockServerPage.deleteRoute(route.id!);
}
@@ -160,6 +170,7 @@ export class MockServerFlow extends BaseFlow {
/**
* Opens `server` and lists the paths of the routes in its sidebar.
*/
@step()
async getRoutePaths(server: MockServer): Promise<string[]> {
await this.open(server);
return this.pageManager.mockServerPage.getRoutePaths();
@@ -168,6 +179,7 @@ export class MockServerFlow extends BaseFlow {
/**
* Opens `route` and reads the method and path its URL bar shows.
*/
@step()
async getRoute(route: MockRoute): Promise<{ method: string; path: string }> {
const mockServerPage = this.pageManager.mockServerPage;
@@ -179,6 +191,7 @@ export class MockServerFlow extends BaseFlow {
* Opens `route` and reads its full URL on the mock server out of the
* "Show URL" dialog.
*/
@step()
async getUrl(route: MockRoute): Promise<string> {
const mockServerPage = this.pageManager.mockServerPage;
@@ -190,6 +203,7 @@ export class MockServerFlow extends BaseFlow {
* Opens `route`, clicks "Test" and reads the response the mock server
* answered with.
*/
@step()
async test(route: MockRoute): Promise<ResponseView> {
const mockServerPage = this.pageManager.mockServerPage;
const responsePage = this.pageManager.responsePage;
@@ -204,6 +218,7 @@ export class MockServerFlow extends BaseFlow {
* On the response `request` currently shows, opens the "→ Mock" tab and
* creates a new Mock Route on `server` from it.
*/
@step()
async createRouteFromResponse(
server: MockServer,
path?: string,
@@ -222,6 +237,7 @@ export class MockServerFlow extends BaseFlow {
* On the response the request currently shows, opens the "→ Mock" tab
* and overwrites `route` with it.
*/
@step()
async overwriteRouteFromResponse(
server: MockServer,
route: MockRoute,
@@ -236,6 +252,7 @@ export class MockServerFlow extends BaseFlow {
/**
* Opens `route` and reads the hits its History tab has logged.
*/
@step()
async getHistory(route: MockRoute): Promise<MockRouteHistoryEntry[]> {
const mockServerPage = this.pageManager.mockServerPage;
@@ -1,4 +1,5 @@
import type { CollaboratorRole } from "../enums/collaborator-role";
import { step } from "../misc/step";
import { BaseFlow } from "./base.flow";
/**
@@ -28,6 +29,7 @@ export class OrganizationFlow extends BaseFlow {
* doesn't already match, rather than always clicking through the role
* menu.
*/
@step()
async invite(...collaborators: Collaborator[]): Promise<void> {
const { invitePage } = this.pageManager;
const alreadyListed = (c: Collaborator) =>
@@ -2,6 +2,7 @@ import { expect } from "@playwright/test";
import type { ScriptSandboxRuleGroup } from "../enums/script-sandbox-rule-group";
import { DEFAULT_TIMEOUT } from "../misc/constants";
import { step } from "../misc/step";
import type { GitCredential } from "../models/git-credential";
import type { AiUrlBackendSettings} from "../models/settings";
import { Settings } from "../models/settings";
@@ -14,6 +15,7 @@ export class PreferencesFlow extends BaseFlow {
* closes it. Only settings that are defined on `settings` are applied
* (e.g. `validateSSL` is skipped when undefined).
*/
@step()
async set(settings: Settings): Promise<Settings> {
const preferencesPage = this.pageManager.preferencesPage;
let aiUrlBackendResult: AiUrlBackendSettings | undefined;
@@ -167,6 +169,7 @@ export class PreferencesFlow extends BaseFlow {
* `.rejects.toThrow(...)`. If accepted, the modal closes on its own and
* this closes Preferences to finish.
*/
@step()
async createPlugin(name: string): Promise<void> {
const preferencesPage = this.pageManager.preferencesPage;
await preferencesPage.open();
@@ -187,6 +190,7 @@ export class PreferencesFlow extends BaseFlow {
* Opens Preferences -> Scripting and toggles a single rule group's
* sandbox switch, then closes Preferences.
*/
@step()
async toggleScriptSandboxRule(
group: ScriptSandboxRuleGroup,
enabled: boolean,
@@ -203,6 +207,7 @@ export class PreferencesFlow extends BaseFlow {
* Credentials, so it's available to select when cloning/connecting a Git
* Sync project.
*/
@step()
async addGitCredential(credential: GitCredential): Promise<void> {
const { preferencesPage } = this.pageManager;
await preferencesPage.open();
@@ -217,6 +222,7 @@ export class PreferencesFlow extends BaseFlow {
* Preferences. Does NOT include bundled plugins (see
* `PreferencesPage.getBundlePlugins()`), which never appear on this tab.
*/
@step()
async getInstalledPlugins(): Promise<string[]> {
const { preferencesPage } = this.pageManager;
await preferencesPage.open();
@@ -232,6 +238,7 @@ export class PreferencesFlow extends BaseFlow {
* salt exists yet for this session (a fresh session, or one whose vault
* state was just reset — see `resetVaultKey()`).
*/
@step()
async generateVaultKey(): Promise<string> {
const { preferencesPage } = this.pageManager;
await preferencesPage.open();
@@ -249,6 +256,7 @@ export class PreferencesFlow extends BaseFlow {
* there's no dialog to surface via `.rejects.toThrow()`); returns `null`
* on success.
*/
@step()
async enterVaultKey(key: string): Promise<string | null> {
const { preferencesPage } = this.pageManager;
await preferencesPage.open();
@@ -267,6 +275,7 @@ export class PreferencesFlow extends BaseFlow {
* Preferences. Requires a vault salt to already exist for this session
* (e.g. via the `vaultSalt` fixture override).
*/
@step()
async resetVaultKey(): Promise<string> {
const { preferencesPage } = this.pageManager;
await preferencesPage.open();
@@ -1,6 +1,7 @@
import { ContextMenuItem } from "../enums/context-menu-items";
import { TreeNodeType } from "../enums/tree-node-types";
import { DEFAULT_TIMEOUT } from "../misc/constants";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type { Folder } from "../models/folder";
import type { ResponseView } from "../models/response";
@@ -13,6 +14,7 @@ export class SocketIORequestFlow extends BaseFlow {
* Opens the Socket.IO connection for `request`, locating it via the
* standalone project tree before connecting.
*/
@step()
async connect(request: SocketIORequest): Promise<void> {
const workspace = this.pageManager.workspacePage;
const socketIoRequestPage = this.pageManager.socketIoRequestPage;
@@ -27,6 +29,7 @@ export class SocketIORequestFlow extends BaseFlow {
* Creates a new Socket.IO request under `parent`, naming it via the
* rename flow and applying its fields on the request page.
*/
@step()
async create(
parent: Collection | Folder,
request: SocketIORequest,
@@ -55,6 +58,7 @@ export class SocketIORequestFlow extends BaseFlow {
* returns the resulting response, locating it via the standalone project
* tree.
*/
@step()
async disconnect(
request: SocketIORequest,
callback: () => Promise<void> | void = () => {},
@@ -79,6 +83,7 @@ export class SocketIORequestFlow extends BaseFlow {
* Reads back an existing request's fields by name or identity via the
* standalone project tree.
*/
@step()
async get(
item: string | { name: string; id?: string },
parent?: Collection,
@@ -102,6 +107,7 @@ export class SocketIORequestFlow extends BaseFlow {
* connection and returns the resulting response, locating it via the
* standalone project tree.
*/
@step()
async sendMessage(request: SocketIORequest): Promise<ResponseView> {
const workspace = this.pageManager.workspacePage;
const socketIoRequestPage = this.pageManager.socketIoRequestPage;
@@ -1,4 +1,5 @@
import { TreeNodeType } from "../enums/tree-node-types";
import { step } from "../misc/step";
import type { HttpRequest } from "../models/http-request";
import type { ResponseView } from "../models/response";
import type { TemplateTag, TemplateTagName } from "../models/template-tag";
@@ -16,6 +17,7 @@ export class TemplateTagFlow extends BaseFlow {
* arguments too, since those have dedicated getters), and confirms via
* "Done".
*/
@step()
async get(template: string): Promise<TemplateTag> {
const templateTag = this.pageManager.templateTagPage;
const tag = template.match(/\{%.*?%\}/)?.[0] ?? template;
@@ -47,6 +49,7 @@ export class TemplateTagFlow extends BaseFlow {
* `PreferencesFlow.addCloudCredentials()`). No other tag is supported
* today, since these are the only two with dedicated argument setters.
*/
@step()
async edit(template: string, target: TemplateTag): Promise<TemplateTag> {
const templateTag = this.pageManager.templateTagPage;
const tag = template.match(/\{%.*?%\}/)?.[0] ?? template;
@@ -80,6 +83,7 @@ export class TemplateTagFlow extends BaseFlow {
* clicks "Clear" to wipe the value cached under its storage key, and
* confirms via "Done".
*/
@step()
async clearCachedPrompt(template: string): Promise<void> {
const templateTag = this.pageManager.templateTagPage;
const tag = template.match(/\{%.*?%\}/)?.[0] ?? template;
@@ -93,6 +97,7 @@ export class TemplateTagFlow extends BaseFlow {
* value), answers the resulting prompt dialog, and reads back the
* response.
*/
@step()
async sendAnsweringPrompt(
request: HttpRequest,
label: string,
@@ -1,3 +1,4 @@
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type {
VariableSourceModal,
@@ -20,6 +21,7 @@ export class VariableSourceFlow extends BaseFlow {
* tag matching `snippet` to open its editor modal, and reads its Source
* row plus Live Preview once the preview settles on `expectedPreview`.
*/
@step()
async inspectVariableModal(
snippet: string,
expectedPreview: string,
@@ -35,6 +37,7 @@ export class VariableSourceFlow extends BaseFlow {
* environment page's editor to open its tag editor modal, and reads its
* Source row plus Live Preview once the preview settles.
*/
@step()
async inspectEnvironmentPageVariable(
snippet: string,
expectedPreview: string,
@@ -48,6 +51,7 @@ export class VariableSourceFlow extends BaseFlow {
* Hovers the template tag matching `snippet` in the currently open
* environment page's editor until its variable-source tooltip shows.
*/
@step()
async hoverEnvironmentPageVariable(
snippet: string,
): Promise<{ tooltip: string; chip: string }> {
@@ -59,6 +63,7 @@ export class VariableSourceFlow extends BaseFlow {
* template tag matching `snippet` until its variable-source tooltip
* shows.
*/
@step()
async hoverVariable(
snippet: string,
): Promise<{ tooltip: string; chip: string }> {
@@ -71,6 +76,7 @@ export class VariableSourceFlow extends BaseFlow {
* Hovers the template tag matching `snippet` inside the open Manage
* Environments modal until its variable-source tooltip shows.
*/
@step()
async hoverVariableInModal(
snippet: string,
scope: VariableSourceScope = "modal",
@@ -82,6 +88,7 @@ export class VariableSourceFlow extends BaseFlow {
* Hovers the tag matching `snippet` on the Body tab, then hovers the
* tooltip's own content and reports whether it stayed visible.
*/
@step()
async hoverTooltipContent(snippet: string): Promise<boolean> {
const page = this.pageManager.variableSourcePage;
await page.navigate();
@@ -94,6 +101,7 @@ export class VariableSourceFlow extends BaseFlow {
* the tooltip text after each hover (the tooltip swaps rather than
* vanishing when the pointer moves between tags).
*/
@step()
async hoverBetweenVariables(
first: string,
firstExpected: string,
@@ -116,6 +124,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Switches the open tag editor modal's selected variable.
*/
@step()
async selectModalVariable(
option: string | { label: string },
expectedPreview?: string,
@@ -131,6 +140,7 @@ export class VariableSourceFlow extends BaseFlow {
* Types a custom template into the open tag editor modal and reads the
* re-derived Source row and Live Preview.
*/
@step()
async setModalTemplate(
template: string,
expectedPreview?: string,
@@ -145,6 +155,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Clicks the open tag editor modal's Source "Open" button.
*/
@step()
async openSourceFromModal(): Promise<void> {
await this.pageManager.variableSourcePage.clickModalSourceOpen();
}
@@ -152,6 +163,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Clicks the visible variable-source tooltip's "Open" button.
*/
@step()
async openSourceFromTooltip(): Promise<void> {
await this.pageManager.variableSourcePage.clickTooltipOpen();
}
@@ -159,6 +171,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Opens `collection` and picks an imported project environment for it.
*/
@step()
async selectProjectEnvironment(
collection: Collection,
optionName: string,
@@ -171,6 +184,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Opens the collection's Manage Environments modal.
*/
@step()
async openManageEnvironmentsModal(): Promise<void> {
await this.pageManager.variableSourcePage.openManageEnvironmentsModal();
}
@@ -178,6 +192,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Selects an environment row inside the open Manage Environments modal.
*/
@step()
async selectModalEnvironment(name: string): Promise<void> {
await this.pageManager.variableSourcePage.selectModalEnvironment(name);
}
@@ -185,6 +200,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Reads where an "Open" action landed.
*/
@step()
async getNavigation(
landing: VariableSourceLanding,
environmentRow?: string,
@@ -213,6 +229,7 @@ export class VariableSourceFlow extends BaseFlow {
/**
* Waits for the Manage Environments modal to close.
*/
@step()
async waitForManageEnvironmentsModalClosed(): Promise<void> {
await this.pageManager.variableSourcePage.waitForManageEnvironmentsModalHidden();
}
@@ -3,6 +3,7 @@ import type { RequestAuthentication } from "insomnia-data";
import { ContextMenuItem } from "../enums/context-menu-items";
import { TreeNodeType } from "../enums/tree-node-types";
import { DEFAULT_TIMEOUT } from "../misc/constants";
import { step } from "../misc/step";
import type { Collection } from "../models/collection";
import type { Folder } from "../models/folder";
import type { ResponseView } from "../models/response";
@@ -19,6 +20,7 @@ export class WebSocketRequestFlow extends BaseFlow {
* Creates a new WebSocket request under `parent`, naming it via the
* rename flow and applying its fields on the request page.
*/
@step()
async create(
parent: Collection | Folder,
request: WebSocketRequest,
@@ -51,6 +53,7 @@ export class WebSocketRequestFlow extends BaseFlow {
* returns the resulting response, locating it via the standalone
* project tree.
*/
@step()
async disconnect(
request: WebSocketRequest,
callback: () => Promise<void> | void = () => {},
@@ -75,6 +78,7 @@ export class WebSocketRequestFlow extends BaseFlow {
* Reads back an existing request's fields by name or identity via the
* standalone project tree.
*/
@step()
async get(
item: string | { name: string; id?: string },
parent?: Collection,
@@ -97,6 +101,7 @@ export class WebSocketRequestFlow extends BaseFlow {
* Opens the WebSocket connection for `request` and returns the
* resulting response, locating it via the standalone project tree.
*/
@step()
async send(
request: WebSocketRequest,
options?: { expectConnected?: boolean },
@@ -119,6 +124,7 @@ export class WebSocketRequestFlow extends BaseFlow {
/**
* Opens `request` and applies `authentication` on its Auth tab.
*/
@step()
async setAuthentication(
request: WebSocketRequest,
authentication: RequestAuthentication,
@@ -138,6 +144,7 @@ export class WebSocketRequestFlow extends BaseFlow {
* `timeout` ms elapses), and returns the resulting response, locating it
* via the standalone project tree.
*/
@step()
async sendMessage(
request: WebSocketRequest,
body: WebSocketRequestBody,
@@ -5,6 +5,7 @@ import { LintSeverity } from '../enums/lint-severity';
import { ProjectType } from '../enums/project-types';
import { TreeNodeType } from '../enums/tree-node-types';
import { DEFAULT_TIMEOUT } from '../misc/constants';
import { step } from "../misc/step";
import { Collection, TestSuite, UnitTest } from '../models/collection';
import { Environment } from '../models/environment';
import type { EventStreamRequest } from '../models/event-stream-request';
@@ -102,6 +103,7 @@ export class WorkspaceFlow extends BaseFlow {
* already a repo) — instead of cloning from a remote URL: the "Open
* local folder" mode of the create-project dialog's Git sub-form.
*/
@step()
async openLocalGitFolder(item: Project): Promise<Project> {
if (item.folderPath === undefined) {
throw new Error(`openLocalGitFolder(): Project "${item.name}" has no folderPath to open`);
@@ -123,6 +125,7 @@ export class WorkspaceFlow extends BaseFlow {
* Preferences -> Credentials) rather than relying on whichever
* credential the form defaults to.
*/
@step()
async cloneGitProject(
item: Project,
credentialName: string,
@@ -213,6 +216,7 @@ export class WorkspaceFlow extends BaseFlow {
* naming dialog. Authors `collection.spec` into the spec editor
* afterward, if given.
*/
@step()
async createInEmptyState(parent: Project, collection: Collection): Promise<Collection> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.resolveNode(parent);
@@ -231,6 +235,7 @@ export class WorkspaceFlow extends BaseFlow {
* an action decorated with `@throwOnDialog`) by clicking its "Ok"
* button, so a test can continue with further steps afterward.
*/
@step()
async closeDialog(): Promise<void> {
await this.pageManager.workspacePage.closeDialog();
}
@@ -239,6 +244,7 @@ export class WorkspaceFlow extends BaseFlow {
* Reads whether the one-time "Welcome to focus mode" onboarding popover
* is currently shown, then dismisses it if so.
*/
@step()
async dismissFocusModePrompt(): Promise<boolean> {
const { workspacePage } = this.pageManager;
const shown = await workspacePage.isFocusModePromptShown();
@@ -252,6 +258,7 @@ export class WorkspaceFlow extends BaseFlow {
* interaction via `pageManager.projectSettingsPage` (e.g. reading the
* current git repo path, relocating it).
*/
@step()
async openSettings(project: Project): Promise<void> {
const { workspacePage, projectSettingsPage } = this.pageManager;
const node = await workspacePage.resolveNode(project);
@@ -265,6 +272,7 @@ export class WorkspaceFlow extends BaseFlow {
* Project has no "Rename" context-menu item, so this is the only way
* to rename one.
*/
@step()
async renameProject(project: Project, newName: string): Promise<Project> {
const { projectSettingsPage } = this.pageManager;
await this.openSettings(project);
@@ -279,6 +287,7 @@ export class WorkspaceFlow extends BaseFlow {
* Right-clicks the tree node matching `item` and deletes it via the
* context menu. Does nothing if no matching node is found.
*/
@step()
async delete(
item:
| Project
@@ -304,6 +313,7 @@ export class WorkspaceFlow extends BaseFlow {
* Opens the command palette, searches for `query`, and reads back the
* matching result names.
*/
@step()
async search(query: string): Promise<string[]> {
const { commandPalettePage } = this.pageManager;
await commandPalettePage.open();
@@ -316,6 +326,7 @@ export class WorkspaceFlow extends BaseFlow {
* generated code snippet via "Generate Code" — available for regular
* requests, not gRPC.
*/
@step()
async generateCode(
request: HttpRequest | GraphQLRequest | EventStreamRequest | SocketIORequest | WebSocketRequest,
options?: { target?: string; client?: string },
@@ -335,6 +346,7 @@ export class WorkspaceFlow extends BaseFlow {
* optionally moving the copy into `targetProject`, then re-fetches it as
* its full model.
*/
@step()
async duplicateCollection(item: Collection, newName: string, targetProject?: Project): Promise<Collection> {
await this.duplicateViaContextMenu(item, newName, targetProject);
return this.assertCreated(await this.getCollection(newName), newName);
@@ -345,6 +357,7 @@ export class WorkspaceFlow extends BaseFlow {
* optionally moving the copy into `targetProject`, then re-fetches it as
* its full model.
*/
@step()
async duplicateMcpClient(item: McpClient, newName: string, targetProject?: Project): Promise<McpClient> {
await this.duplicateViaContextMenu(item, newName, targetProject);
return this.assertCreated(await this.getMcpClient(newName), newName);
@@ -355,6 +368,7 @@ export class WorkspaceFlow extends BaseFlow {
* optionally moving the copy into `targetProject`, then re-fetches it as
* its full model.
*/
@step()
async duplicateEnvironment(item: Environment, newName: string, targetProject?: Project): Promise<Environment> {
await this.duplicateViaContextMenu(item, newName, targetProject);
@@ -374,6 +388,7 @@ export class WorkspaceFlow extends BaseFlow {
/**
* Duplicates a request in place via its dedicated "Duplicate" dialog.
*/
@step()
async duplicateRequest(item: RequestItem, newName: string): Promise<{ id: string; name: string }> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.resolveNode(item);
@@ -405,6 +420,7 @@ export class WorkspaceFlow extends BaseFlow {
* Right-clicks the tree node matching `request` and pins it via the
* context menu's "Pin" action.
*/
@step()
async pin(
request: HttpRequest | GraphQLRequest | GrpcRequest | EventStreamRequest | SocketIORequest | WebSocketRequest,
): Promise<void> {
@@ -415,6 +431,7 @@ export class WorkspaceFlow extends BaseFlow {
* Right-clicks the tree node matching `request` and unpins it via the
* context menu's "Unpin" action.
*/
@step()
async unpin(
request: HttpRequest | GraphQLRequest | GrpcRequest | EventStreamRequest | SocketIORequest | WebSocketRequest,
): Promise<void> {
@@ -438,6 +455,7 @@ export class WorkspaceFlow extends BaseFlow {
* Renames a Collection via its context menu's "Rename" action, then
* re-fetches it as its full model under the new name.
*/
@step()
async renameCollection(item: Collection, newName: string): Promise<Collection> {
await this.renameViaContextMenu(item, newName);
return this.assertCreated(await this.getCollection(newName), newName);
@@ -447,6 +465,7 @@ export class WorkspaceFlow extends BaseFlow {
* Renames an McpClient via its context menu's "Rename" action, then
* re-fetches it as its full model under the new name.
*/
@step()
async renameMcpClient(item: McpClient, newName: string): Promise<McpClient> {
await this.renameViaContextMenu(item, newName);
return this.assertCreated(await this.getMcpClient(newName), newName);
@@ -456,6 +475,7 @@ export class WorkspaceFlow extends BaseFlow {
* Renames an Environment via its context menu's "Rename" action, then
* re-fetches it as its full model under the new name.
*/
@step()
async renameEnvironment(item: Environment, newName: string): Promise<Environment> {
await this.renameViaContextMenu(item, newName);
return this.assertCreated(await this.getEnvironment(newName), newName);
@@ -464,6 +484,7 @@ export class WorkspaceFlow extends BaseFlow {
/**
* Renames a request in place via its context menu's "Rename" action.
*/
@step()
async renameRequest(item: RequestItem, newName: string): Promise<{ id: string; name: string }> {
await this.renameViaContextMenu(item, newName);
const newNode = await this.pageManager.workspacePage.findNode(newName, TreeNodeType.Request);
@@ -492,6 +513,7 @@ export class WorkspaceFlow extends BaseFlow {
* with the Runner (e.g. Cancel/Skip) while it's in flight, rather than
* waiting for it to finish.
*/
@step()
async openRunner(collection: Collection, options?: RunnerOptions): Promise<void> {
const workspace = this.pageManager.workspacePage;
const runner = this.pageManager.runnerPage;
@@ -517,6 +539,7 @@ export class WorkspaceFlow extends BaseFlow {
* request before returning the collected test-result count and
* per-iteration results.
*/
@step()
async run(collection: Collection, options?: RunnerOptions): Promise<RunnerRunResult> {
const runner = this.pageManager.runnerPage;
await this.openRunner(collection, options);
@@ -580,6 +603,7 @@ export class WorkspaceFlow extends BaseFlow {
* doesn't carry a spec; use `getCollectionSpec()` instead for one that
* does (or might).
*/
@step()
async getCollection(item: string | { name: string; id?: string }): Promise<Collection | undefined> {
const identity = typeof item === 'string' ? { name: item } : item;
const node = await this.pageManager.workspacePage.findItemNode(identity);
@@ -596,6 +620,7 @@ export class WorkspaceFlow extends BaseFlow {
* call `getLintState()` separately — it polls for the lint pass to
* settle, which not every caller needs.
*/
@step()
async getCollectionSpec(item: string | { name: string; id?: string }): Promise<Collection | undefined> {
const identity = typeof item === 'string' ? { name: item } : item;
const workspace = this.pageManager.workspacePage;
@@ -622,6 +647,7 @@ export class WorkspaceFlow extends BaseFlow {
* creation), this can be called at any later point — e.g. filling in a
* collection that started out with no spec.
*/
@step()
async fillSpecification(item: Collection, spec: NonNullable<Collection['spec']>): Promise<void> {
const workspace = this.pageManager.workspacePage;
const node = await workspace.resolveNode(item);
@@ -646,6 +672,7 @@ export class WorkspaceFlow extends BaseFlow {
* renames it — the two nearly always happen together, since a fresh
* suite is always named "New Suite".
*/
@step()
async createTestSuite(item: string | { name: string; id?: string }, name: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
await this.openTests(item);
@@ -659,6 +686,7 @@ export class WorkspaceFlow extends BaseFlow {
* the two nearly always happen together, since a fresh test is always
* named "Returns 200".
*/
@step()
async createUnitTest(testSuite: TestSuite, testName: string): Promise<void> {
const workspace = this.pageManager.workspacePage;
await this.openTests(testSuite.collection);
@@ -672,6 +700,7 @@ export class WorkspaceFlow extends BaseFlow {
* Opens `item`'s Tests tab, then deletes `suiteName`'s test suite via its
* confirm modal (`WorkspacePage.deleteTestSuite()`).
*/
@step()
async deleteTestSuite(item: string | { name: string; id?: string }, suiteName: string): Promise<void> {
await this.openTests(item);
await this.pageManager.workspacePage.deleteTestSuite(suiteName);
@@ -688,6 +717,7 @@ export class WorkspaceFlow extends BaseFlow {
* already on the collection's page after `create()`/`getCollectionSpec()`/
* `WorkspacePage.setSpecification()`.
*/
@step()
async getLintState(minLintErrors = 0): Promise<NonNullable<Collection['lint']>> {
const workspace = this.pageManager.workspacePage;
@@ -729,6 +759,7 @@ export class WorkspaceFlow extends BaseFlow {
* (`WorkspacePage.getUnitTestNames()`) and a reference back to `item`,
* so the suite can be traced back to where it lives.
*/
@step()
async getTestSuite(item: string | { name: string; id?: string }, testSuiteName: string): Promise<TestSuite> {
const workspace = this.pageManager.workspacePage;
const node = await this.openTests(item);
@@ -746,6 +777,7 @@ export class WorkspaceFlow extends BaseFlow {
* Opens `item`'s Tests tab and reads back every suite name currently
* listed in the Test Suites sidebar (`WorkspacePage.getTestSuiteNames()`).
*/
@step()
async getTestSuiteNames(item: string | { name: string; id?: string }): Promise<string[]> {
await this.openTests(item);
return this.pageManager.workspacePage.getTestSuiteNames();
@@ -756,6 +788,7 @@ export class WorkspaceFlow extends BaseFlow {
* via its confirm modal (`WorkspacePage.removeRuleset()`), reverting to
* the default OAS ruleset.
*/
@step()
async removeRuleset(item: string | { name: string; id?: string }): Promise<void> {
const identity = typeof item === 'string' ? { name: item } : item;
const workspace = this.pageManager.workspacePage;
@@ -770,6 +803,7 @@ export class WorkspaceFlow extends BaseFlow {
* Opens `testSuite`'s Tests tab, selects `testSuite`, runs every test in
* it, and reads back the resulting pass/fail summary and per-test rows.
*/
@step()
async runAllTests(testSuite: TestSuite): Promise<UnitTestRunResult> {
const workspace = this.pageManager.workspacePage;
await this.openTests(testSuite.collection);
@@ -792,6 +826,7 @@ export class WorkspaceFlow extends BaseFlow {
* resulting "Invalid Spectral Ruleset" error dialog and returns
* `"invalid"` instead, leaving the default ruleset active.
*/
@step()
async uploadRuleset(filePath: string, expectCode?: string): Promise<string[] | 'invalid'> {
const workspace = this.pageManager.workspacePage;
const result = await workspace.uploadRuleset(filePath);
@@ -859,6 +894,7 @@ export class WorkspaceFlow extends BaseFlow {
/**
* Finds the tree node matching `item` and returns it as a Project model.
*/
@step()
async getProject(item: string | { name: string; id?: string }): Promise<Project | undefined> {
const identity = typeof item === 'string' ? { name: item } : item;
const node = await this.pageManager.workspacePage.findItemNode(identity);
@@ -961,6 +997,7 @@ export class WorkspaceFlow extends BaseFlow {
* Deletes the tree node matching `item` with the sidebar's Delete shortcut
* and confirms the dialog. Does nothing if no matching node is found.
*/
@step()
async deleteViaShortcut(
item:
| Collection
@@ -985,6 +1022,7 @@ export class WorkspaceFlow extends BaseFlow {
* reads the "Unknown Fields Detected" warning off the post-scan screen
* before confirming the clone.
*/
@step()
async cloneWithScanReport(
item: Project,
credentialName: string,
-2
View File
@@ -18,7 +18,6 @@ import {
trackHiddenWindowCoverage,
} from "./coverage";
import { launchInsomniaElectron, SESSION_ID } from "./launch";
import { closeOpenStepGroup } from "./step-instrumentation";
export * from "./constants";
export { launchInsomniaElectron, SESSION_ID };
@@ -326,7 +325,6 @@ export const test = base.extend<Fixtures>({
await pageManager.workspacePage.navigate();
await disableFocusModeDefault(flowManager);
await use({ page: window, pageManager, flowManager });
await closeOpenStepGroup();
// A test that called AppFlow.restart() leaves the relaunched app
// behind — the `insomnia`/`window` fixtures only know the original
@@ -1,241 +0,0 @@
import { test } from "@playwright/test";
const step = test.step.bind(test);
const EXCLUDED_METHODS = new Set([
"setContext",
"switchTab",
"fillOneLineEditor",
"readCodeMirror",
"setKeyValuePairs",
"readKeyValuePairs",
"createProject",
"createCollection",
"createDocument",
"createMcpClient",
"createEnvironment",
"rows",
"fillField",
"editCookie",
"caCertificateRow",
"clientCertificateRow",
"fileCard",
"dragRow",
"invitationRow",
"oAuth1FieldEditor",
"oauth2FieldEditor",
"oauth2TokenInput",
"projectTypeTile",
"isToggleOn",
"setToggle",
"tab",
"openAddTabMenu",
"toAiUrlBackendSettings",
]);
const GET_METHOD_PATTERN = /^get[A-Z]/;
function humanize(name: string): string {
const words = name
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
return words
.replace(/Graph Ql/g, "GraphQL")
.replace(/Socket Io/g, "Socket.IO")
.replace(/\bMcp\b/g, "MCP")
.replace(/\bGrpc\b/g, "gRPC");
}
const BARE_VERB_PATTERN = /^[a-z]+$/;
const CLASS_SUFFIX_PATTERN = /(Flow|Page)$/;
interface IdentifiedObjectArg {
pos: number;
identifier: string;
typeLabel?: string;
}
function describeArgs(className: string, key: string, args: unknown[]): string {
let stringArg: string | undefined;
let stringPos = -1;
const objectArgs: IdentifiedObjectArg[] = [];
args.forEach((arg, pos) => {
if (typeof arg === "string") {
if (stringArg === undefined) {
stringArg = arg;
stringPos = pos;
}
return;
}
if (!arg || typeof arg !== "object" || Array.isArray(arg)) return;
const candidate =
(arg as { name?: unknown; id?: unknown }).name ??
(arg as { name?: unknown; id?: unknown }).id;
if (typeof candidate !== "string") return;
const ctorName = (arg as object).constructor?.name;
objectArgs.push({
pos,
identifier: candidate,
typeLabel:
ctorName && ctorName !== "Object" ? humanize(ctorName) : undefined,
});
});
const preposition = key.toLowerCase().startsWith("create") ? "in" : "to";
const last = objectArgs[objectArgs.length - 1];
const domain = humanize(className.replace(CLASS_SUFFIX_PATTERN, ""));
let subject = last?.typeLabel ?? "";
if (subject === domain) subject = "";
if (!subject && BARE_VERB_PATTERN.test(key) && domain) subject = domain;
const primitives = [
stringArg !== undefined ? { value: stringArg, pos: stringPos } : null,
last ? { value: last.identifier, pos: last.pos } : null,
]
.filter((p): p is { value: string; pos: number } => p !== null)
.sort((a, b) => a.pos - b.pos)
.map((p) => p.value);
let detail = primitives.length ? ` (${primitives.join(", ")})` : "";
for (const target of objectArgs.slice(0, -1)) {
detail += ` ${preposition}${target.typeLabel ? ` ${target.typeLabel}` : ""} (${target.identifier})`;
}
return (subject ? ` ${subject}` : "") + detail;
}
interface QueueItem {
childTitle: string;
fn: () => unknown;
resolve: (v: unknown) => void;
reject: (e: unknown) => void;
}
interface PageGroup {
instance: object;
run: (childTitle: string, fn: () => unknown) => Promise<unknown>;
close: () => Promise<void>;
}
let isProcessingGroupItem = false;
function createPageGroup(instance: object, title: string): PageGroup {
const queue: QueueItem[] = [];
let wake: (() => void) | null = null;
let closed = false;
const finished = step(title, async () => {
for (;;) {
const item = queue.shift();
if (item) {
isProcessingGroupItem = true;
try {
item.resolve(await step(item.childTitle, item.fn));
} catch (e) {
item.reject(e);
} finally {
isProcessingGroupItem = false;
}
continue;
}
if (closed) return;
await new Promise<void>((resolve) => (wake = resolve));
}
});
return {
instance,
run(childTitle, fn) {
if (isProcessingGroupItem) return Promise.resolve(step(childTitle, fn));
return new Promise((resolve, reject) => {
queue.push({ childTitle, fn, resolve, reject });
wake?.();
wake = null;
});
},
async close() {
closed = true;
wake?.();
wake = null;
await finished;
},
};
}
let currentPageGroup: PageGroup | null = null;
async function getPageGroup(
instance: object,
humanClassName: string,
): Promise<PageGroup> {
if (currentPageGroup?.instance === instance) return currentPageGroup;
if (isProcessingGroupItem && currentPageGroup) return currentPageGroup;
if (currentPageGroup) {
const previous = currentPageGroup;
currentPageGroup = null;
await previous.close();
}
const group = createPageGroup(instance, humanClassName);
currentPageGroup = group;
return group;
}
export async function closeOpenStepGroup(): Promise<void> {
if (!currentPageGroup) return;
const group = currentPageGroup;
currentPageGroup = null;
await group.close();
}
export function instrumentWithSteps<T extends object>(instance: T): T {
const className = instance.constructor.name;
const isPageClass = className.endsWith("Page");
const humanClassName = humanize(className);
const seen = new Set<string>(["constructor"]);
let proto = Object.getPrototypeOf(instance);
while (proto && proto !== Object.prototype) {
for (const key of Object.getOwnPropertyNames(proto)) {
if (seen.has(key) || EXCLUDED_METHODS.has(key)) continue;
seen.add(key);
if (isPageClass && GET_METHOD_PATTERN.test(key)) continue;
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
if (!descriptor || typeof descriptor.value !== "function") continue;
const original = (instance as any)[key] as (
...args: unknown[]
) => unknown;
const childTitle = (args: unknown[]) =>
`${humanize(key)}${describeArgs(className, key, args)}`;
(instance as any)[key] = isPageClass
? async function (...args: unknown[]) {
const group = await getPageGroup(instance, humanClassName);
return group.run(childTitle(args), () =>
original.apply(instance, args),
);
}
: async function (...args: unknown[]) {
await closeOpenStepGroup();
try {
return await step(`${humanClassName}: ${childTitle(args)}`, () =>
original.apply(instance, args),
);
} finally {
await closeOpenStepGroup();
}
};
}
proto = Object.getPrototypeOf(proto);
}
return instance;
}
+104
View File
@@ -0,0 +1,104 @@
import { test } from "@playwright/test";
function humanize(name: string): string {
const words = name
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
return words
.replace(/Graph Ql/g, "GraphQL")
.replace(/Socket Io/g, "Socket.IO")
.replace(/\bMcp\b/g, "MCP")
.replace(/\bGrpc\b/g, "gRPC");
}
const BARE_VERB_PATTERN = /^[a-z]+$/;
const CLASS_SUFFIX_PATTERN = /(Flow|Page)$/;
interface IdentifiedObjectArg {
pos: number;
identifier: string;
typeLabel?: string;
}
function describeArgs(className: string, key: string, args: unknown[]): string {
let stringArg: string | undefined;
let stringPos = -1;
const objectArgs: IdentifiedObjectArg[] = [];
args.forEach((arg, pos) => {
if (typeof arg === "string") {
if (stringArg === undefined) {
stringArg = arg;
stringPos = pos;
}
return;
}
if (!arg || typeof arg !== "object" || Array.isArray(arg)) return;
const candidate =
(arg as { name?: unknown; id?: unknown }).name ??
(arg as { name?: unknown; id?: unknown }).id;
if (typeof candidate !== "string") return;
const ctorName = (arg as object).constructor?.name;
objectArgs.push({
pos,
identifier: candidate,
typeLabel:
ctorName && ctorName !== "Object" ? humanize(ctorName) : undefined,
});
});
const preposition = key.toLowerCase().startsWith("create") ? "in" : "to";
const last = objectArgs[objectArgs.length - 1];
const domain = humanize(className.replace(CLASS_SUFFIX_PATTERN, ""));
let subject = last?.typeLabel ?? "";
if (subject === domain) subject = "";
if (!subject && BARE_VERB_PATTERN.test(key) && domain) subject = domain;
const primitives = [
stringArg !== undefined ? { value: stringArg, pos: stringPos } : null,
last ? { value: last.identifier, pos: last.pos } : null,
]
.filter((p): p is { value: string; pos: number } => p !== null)
.sort((a, b) => a.pos - b.pos)
.map((p) => p.value);
let detail = primitives.length ? ` (${primitives.join(", ")})` : "";
for (const target of objectArgs.slice(0, -1)) {
detail += ` ${preposition}${target.typeLabel ? ` ${target.typeLabel}` : ""} (${target.identifier})`;
}
return (subject ? ` ${subject}` : "") + detail;
}
/**
* Method decorator that reports a Flow method as a `test.step()`.
*
* Without a `title` the step is named after the method and its arguments
* (e.g. `Http Request Flow: Create (Request name)`); pass a `title` to
* override that.
*/
export function step(title?: string) {
return function <This extends object, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Promise<Return>,
context: ClassMethodDecoratorContext<
This,
(this: This, ...args: Args) => Promise<Return>
>,
) {
const key = String(context.name);
return function (this: This, ...args: Args): Promise<Return> {
const className = this.constructor.name;
const stepTitle =
title ?? `${humanize(key)}${describeArgs(className, key, args)}`;
return test.step(`${humanize(className)}: ${stepTitle}`, () =>
target.call(this, ...args),
);
};
};
}
+1 -3
View File
@@ -1,7 +1,6 @@
import type { ElectronApplication } from "@playwright/test";
import type { Page } from "playwright-core";
import { instrumentWithSteps } from "../misc/step-instrumentation";
import type { BasePage } from "./base.page";
import { CertificatesPage } from "./certificates.page";
import { CloudSyncPage } from "./cloud-sync.page";
@@ -282,8 +281,7 @@ export class PageManager {
}
private getOrCreate<T extends object>(key: string, factory: () => T): T {
if (!this.instances.has(key))
this.instances.set(key, instrumentWithSteps(factory()));
if (!this.instances.has(key)) this.instances.set(key, factory());
return this.instances.get(key) as T;
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ const INSOMNIA_SRC_PACKAGE = path.resolve(import.meta.dirname, "..", "insomnia")
ensureCerts();
// The suite is serial by design: misc/git-fixtures.ts (`currentGitRepo`),
// misc/step-instrumentation.ts (`currentPageGroup`) and the mock servers'
// module-level state and the mock servers'
// shared state all assume one test at a time. Fail fast instead of letting
// `--workers=N` produce cross-test interference.
const workersIdx = process.argv.findIndex((arg) => arg === "--workers" || arg.startsWith("--workers=") || arg === "-j");