From f927a84eee86f5778330f96faae83a7bca6ae965 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 18:40:00 +0530 Subject: [PATCH 01/17] feat: implement createFlow function and FlowConfig interface --- src/execution-flow/flows/create-flow.ts | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/execution-flow/flows/create-flow.ts diff --git a/src/execution-flow/flows/create-flow.ts b/src/execution-flow/flows/create-flow.ts new file mode 100644 index 0000000..3f336be --- /dev/null +++ b/src/execution-flow/flows/create-flow.ts @@ -0,0 +1,61 @@ +import { spinner } from "@clack/prompts"; +import type { BaseFlowParam } from "@type/base-flow.types"; +import type { FlowStep } from "@type/workflow-config.types"; +import chalk from "chalk"; +import type { StepKeys } from "../flow-registry"; + +export interface FlowConfig { + /** The action name (e.g., 'clickOn', 'input') */ + action: StepKeys; + + /** Generate descriptive message for spinner */ + getMessage: (step: T) => string; + + /** Core execution logic */ + execute: (params: BaseFlowParam) => Promise; + + /** Custom error handler */ + onError?: (error: unknown, step: T) => Error; + + /** Custom error message (defaults to getMessage) */ + getErrorMessage?: (step: T) => string; + + /** Custom success message (defaults to getMessage) */ + getSuccessMessage?: (step: T) => string; +} + +export function createFlow({ + action, + execute, + getMessage, + getSuccessMessage, + onError, + getErrorMessage, +}: FlowConfig) { + return async (params: BaseFlowParam): Promise => { + const { step, page } = params; + const flowSpinner = spinner(); + const message = getMessage(step); + + flowSpinner.start(message); + + try { + await execute({ step, page }); + + const successMsg = getSuccessMessage ? getSuccessMessage(step) : message; + + flowSpinner.stop(`${chalk.green("✓")} ${successMsg}`); + } catch (error) { + const errorMsg = getErrorMessage + ? getErrorMessage(step) + : `${action}: ${message}`; + flowSpinner.stop(`${chalk.red("✖")} ${errorMsg}`); + + if (onError) { + throw onError(error, step); + } + + throw error; + } + }; +} From ae7344c2640a4ecfbafdfb7c815706fbbae96372 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 18:40:27 +0530 Subject: [PATCH 02/17] feat: refactor clickonFlow to use createFlow --- src/execution-flow/flows/clickon-flow.ts | 67 +++++++++++++++--------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/execution-flow/flows/clickon-flow.ts b/src/execution-flow/flows/clickon-flow.ts index 2a9b2a2..9c46ca1 100644 --- a/src/execution-flow/flows/clickon-flow.ts +++ b/src/execution-flow/flows/clickon-flow.ts @@ -1,38 +1,53 @@ -import { spinner } from "@clack/prompts"; import { ElementNotFoundError } from "@src/errors/workflow-error"; import type { BaseFlowParam } from "@type/base-flow.types"; import type { ClickAction } from "@type/workflow-config.types"; -import chalk from "chalk"; import type { Locator } from "playwright"; +import { createFlow } from "./create-flow"; -export default async function clickonFlow({ - step: clickOnStep, - page, -}: BaseFlowParam): Promise { - const clickSpinner = spinner(); - - let targetLocator: Locator; +function clickOnDescription({ + step, +}: Pick, "step">) { let targetDescription: `text: ${string}` | `selector: ${string}`; - - if (typeof clickOnStep.clickOn === "string") { - targetLocator = page.getByText(clickOnStep.clickOn).first(); - targetDescription = `text: ${clickOnStep.clickOn}`; + if (typeof step.clickOn === "string") { + targetDescription = `text: ${step.clickOn}`; } else { - targetLocator = page.locator(clickOnStep.clickOn.selector); - targetDescription = `selector: ${clickOnStep.clickOn.selector}`; + targetDescription = `selector: ${step.clickOn.selector}`; } + return targetDescription; +} + +export default createFlow({ + action: "clickOn", + + getMessage: (step) => { + const targetDescription = clickOnDescription({ step }); + return `Clicking on element with ${targetDescription}`; + }, + + execute: async ({ step: clickOnStep, page }) => { + let targetLocator: Locator; - clickSpinner.start(`Clicking on element with ${targetDescription}`); + if (typeof clickOnStep.clickOn === "string") { + targetLocator = page.getByText(clickOnStep.clickOn).first(); + } else { + targetLocator = page.locator(clickOnStep.clickOn.selector); + } - try { await targetLocator.click(); - clickSpinner.stop( - `${chalk.green("✓")} Clicked on element with ${targetDescription}`, - ); - } catch { - clickSpinner.stop( - `${chalk.red("✖")} clickOn: Unable to find element with ${targetDescription}`, - ); + }, + + getSuccessMessage: (step) => { + const targetDescription = clickOnDescription({ step }); + return `Clicked on element with ${targetDescription}`; + }, + + getErrorMessage: (step) => { + const targetDescription = clickOnDescription({ step }); + return `clickOn: Unable to find element with ${targetDescription}`; + }, + + onError: (_, step) => { + const targetDescription = clickOnDescription({ step }); throw new ElementNotFoundError(`Element missing: ${targetDescription}`); - } -} + }, +}); From 7e6a2bd1958275985ab8cb4e2584e3c5b0124984 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 18:47:52 +0530 Subject: [PATCH 03/17] feat: refactor clickOnFlow to extract clickOnLocator function --- src/execution-flow/flows/clickon-flow.ts | 26 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/execution-flow/flows/clickon-flow.ts b/src/execution-flow/flows/clickon-flow.ts index 9c46ca1..ddc0a83 100644 --- a/src/execution-flow/flows/clickon-flow.ts +++ b/src/execution-flow/flows/clickon-flow.ts @@ -16,7 +16,19 @@ function clickOnDescription({ return targetDescription; } -export default createFlow({ +function clickOnLocator({ page, step }: BaseFlowParam): Locator { + let targetLocator: Locator; + + if (typeof step.clickOn === "string") { + targetLocator = page.getByText(step.clickOn).first(); + } else { + targetLocator = page.locator(step.clickOn.selector); + } + + return targetLocator; +} + +const clickOnFlow = createFlow({ action: "clickOn", getMessage: (step) => { @@ -24,14 +36,8 @@ export default createFlow({ return `Clicking on element with ${targetDescription}`; }, - execute: async ({ step: clickOnStep, page }) => { - let targetLocator: Locator; - - if (typeof clickOnStep.clickOn === "string") { - targetLocator = page.getByText(clickOnStep.clickOn).first(); - } else { - targetLocator = page.locator(clickOnStep.clickOn.selector); - } + execute: async (params) => { + const targetLocator = clickOnLocator(params); await targetLocator.click(); }, @@ -51,3 +57,5 @@ export default createFlow({ throw new ElementNotFoundError(`Element missing: ${targetDescription}`); }, }); + +export default clickOnFlow; From 6dbc866c2fb34a227bbc93eeab3d899714d14efd Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 18:48:55 +0530 Subject: [PATCH 04/17] feat: update FlowConfig interface comments for clarity on error and success messages --- src/execution-flow/flows/create-flow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/execution-flow/flows/create-flow.ts b/src/execution-flow/flows/create-flow.ts index 3f336be..182b966 100644 --- a/src/execution-flow/flows/create-flow.ts +++ b/src/execution-flow/flows/create-flow.ts @@ -17,10 +17,10 @@ export interface FlowConfig { /** Custom error handler */ onError?: (error: unknown, step: T) => Error; - /** Custom error message (defaults to getMessage) */ + /** Custom error message for stop spinner (defaults to getMessage) */ getErrorMessage?: (step: T) => string; - /** Custom success message (defaults to getMessage) */ + /** Custom success message for stop spinner (defaults to getMessage) */ getSuccessMessage?: (step: T) => string; } From 6391b7ecf4fed96743cb353639d22741bbf8d2e2 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 19:12:46 +0530 Subject: [PATCH 05/17] feat: refactor clickOnDescription and isDisableDescription for improved parameter handling --- src/execution-flow/flows/clickon-flow.ts | 12 ++-- src/execution-flow/flows/is-disabled-flow.ts | 68 +++++++++++++------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/execution-flow/flows/clickon-flow.ts b/src/execution-flow/flows/clickon-flow.ts index ddc0a83..1d82648 100644 --- a/src/execution-flow/flows/clickon-flow.ts +++ b/src/execution-flow/flows/clickon-flow.ts @@ -4,9 +4,7 @@ import type { ClickAction } from "@type/workflow-config.types"; import type { Locator } from "playwright"; import { createFlow } from "./create-flow"; -function clickOnDescription({ - step, -}: Pick, "step">) { +function clickOnDescription(step: ClickAction) { let targetDescription: `text: ${string}` | `selector: ${string}`; if (typeof step.clickOn === "string") { targetDescription = `text: ${step.clickOn}`; @@ -32,7 +30,7 @@ const clickOnFlow = createFlow({ action: "clickOn", getMessage: (step) => { - const targetDescription = clickOnDescription({ step }); + const targetDescription = clickOnDescription(step); return `Clicking on element with ${targetDescription}`; }, @@ -43,17 +41,17 @@ const clickOnFlow = createFlow({ }, getSuccessMessage: (step) => { - const targetDescription = clickOnDescription({ step }); + const targetDescription = clickOnDescription(step); return `Clicked on element with ${targetDescription}`; }, getErrorMessage: (step) => { - const targetDescription = clickOnDescription({ step }); + const targetDescription = clickOnDescription(step); return `clickOn: Unable to find element with ${targetDescription}`; }, onError: (_, step) => { - const targetDescription = clickOnDescription({ step }); + const targetDescription = clickOnDescription(step); throw new ElementNotFoundError(`Element missing: ${targetDescription}`); }, }); diff --git a/src/execution-flow/flows/is-disabled-flow.ts b/src/execution-flow/flows/is-disabled-flow.ts index 016288a..b7fdb42 100644 --- a/src/execution-flow/flows/is-disabled-flow.ts +++ b/src/execution-flow/flows/is-disabled-flow.ts @@ -1,40 +1,58 @@ -import { spinner } from "@clack/prompts"; import { FailedAssertionError } from "@src/errors/workflow-error"; import type { BaseFlowParam } from "@type/base-flow.types"; import type { IsDisabledAction } from "@type/workflow-config.types"; -import chalk from "chalk"; import type { Locator } from "playwright"; import { expect } from "playwright/test"; +import { createFlow } from "./create-flow"; -export default async function isDisabledFlow({ - step: isDisabledStep, +function isDisableDescription(step: IsDisabledAction): string { + let targetDescription: `text: ${string}` | `selector: ${string}`; + + if (typeof step.isDisabled === "string") { + targetDescription = `text: ${step.isDisabled}`; + } else { + targetDescription = `selector: ${step.isDisabled.selector}`; + } + return targetDescription; +} + +function isDisabledLocator({ page, -}: BaseFlowParam): Promise { - const isDisabledSpinner = spinner(); + step, +}: BaseFlowParam): Locator { let targetLocator: Locator; - let targetDescription: `text: ${string}` | `selector: ${string}`; - if (typeof isDisabledStep.isDisabled === "string") { - targetLocator = page.getByText(isDisabledStep.isDisabled).first(); - targetDescription = `text: ${isDisabledStep.isDisabled}`; + if (typeof step.isDisabled === "string") { + targetLocator = page.getByText(step.isDisabled).first(); } else { - targetLocator = page.locator(isDisabledStep.isDisabled.selector); - targetDescription = `selector: ${isDisabledStep.isDisabled.selector}`; + targetLocator = page.locator(step.isDisabled.selector); } - isDisabledSpinner.start( - `Checking visibility of element with ${targetDescription}`, - ); + return targetLocator; +} - try { +const isDisabledFlow = createFlow({ + action: "isDisabled", + getMessage(step) { + const targetDescription = isDisableDescription(step); + return `Checking visibility of element with ${targetDescription}`; + }, + async execute(params) { + const targetLocator = isDisabledLocator(params); await expect(targetLocator).toBeDisabled(); - isDisabledSpinner.stop( - `${chalk.green("✓")} Element is disabled with ${targetDescription}`, - ); - } catch { - isDisabledSpinner.stop( - `${chalk.red("✖")} isDisabled: Element is not disabled with ${targetDescription}`, - ); + }, + getSuccessMessage(step) { + const targetDescription = isDisableDescription(step); + return `Element is disabled with ${targetDescription}`; + }, + getErrorMessage(step) { + const targetDescription = isDisableDescription(step); + return `isDisabled: Element is not disabled with ${targetDescription}`; + }, + onError(_, step) { + const targetDescription = isDisableDescription(step); throw new FailedAssertionError(`Assertion failed: ${targetDescription}`); - } -} + }, +}); + +export default isDisabledFlow; From 24ac0f4f510c5597ad67d5e7bf76f9df53a77437 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 20:50:41 +0530 Subject: [PATCH 06/17] feat: refactor isTitleFlow to use createFlow for improved structure and readability --- src/execution-flow/flows/istitle-flow.ts | 52 ++++++++++++------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/execution-flow/flows/istitle-flow.ts b/src/execution-flow/flows/istitle-flow.ts index e198edc..37b533c 100644 --- a/src/execution-flow/flows/istitle-flow.ts +++ b/src/execution-flow/flows/istitle-flow.ts @@ -1,34 +1,32 @@ -import { spinner } from "@clack/prompts"; import { FailedAssertionError } from "@src/errors/workflow-error"; -import type { BaseFlowParam } from "@type/base-flow.types"; import type { IsTitleAction } from "@type/workflow-config.types"; import regexOrStringMaker from "@utils/regex-or-string-maker"; -import chalk from "chalk"; import { expect } from "playwright/test"; +import { createFlow } from "./create-flow"; -export default async function isTitleFlow({ - step: isTitleStep, - page, -}: BaseFlowParam) { - const isTitleSpinner = spinner(); - - const { isRegex, value: titleOrRegex } = regexOrStringMaker( - isTitleStep.isTitle, - ); - - const LocatorDescription = isRegex ? "Regex:" : "Text:"; - - isTitleSpinner.start(`Checking visibility of page title ${titleOrRegex}`); - - try { +const isTitleFlow = createFlow({ + action: "isTitle", + async execute({ page, step }) { + const { value: titleOrRegex } = regexOrStringMaker(step.isTitle); await expect(page).toHaveTitle(titleOrRegex); - isTitleSpinner.stop( - `${chalk.green("✓")} Page title is visible with ${LocatorDescription} "${titleOrRegex}"`, - ); - } catch { - isTitleSpinner.stop( - `${chalk.red("✖")} isTitle: Page title is not visible with ${LocatorDescription} "${titleOrRegex}"`, - ); + }, + getMessage(step) { + const { value: titleOrRegex } = regexOrStringMaker(step.isTitle); + return `Checking visibility of page title ${titleOrRegex}`; + }, + getSuccessMessage(step) { + const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isTitle); + const LocatorDescription = isRegex ? "Regex:" : "Text:"; + return `Page title is visible with ${LocatorDescription} "${titleOrRegex}"`; + }, + getErrorMessage(step) { + const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isTitle); + const LocatorDescription = isRegex ? "Regex:" : "Text:"; + return `isTitle: Page title is not visible with ${LocatorDescription} "${titleOrRegex}"`; + }, + onError(_, step) { + const { value: titleOrRegex } = regexOrStringMaker(step.isTitle); throw new FailedAssertionError(`Assertion failed: ${titleOrRegex}`); - } -} + }, +}); +export default isTitleFlow; From 8ad9d46f25aa9ffb84ec2afb943d0aea24f65fd4 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 21:00:58 +0530 Subject: [PATCH 07/17] feat: refactor isURLFlow to utilize createFlow for improved structure and readability --- src/execution-flow/flows/create-flow.ts | 3 +- src/execution-flow/flows/isurl-flow.ts | 51 ++++++++++++++----------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/execution-flow/flows/create-flow.ts b/src/execution-flow/flows/create-flow.ts index 182b966..3fc4ac7 100644 --- a/src/execution-flow/flows/create-flow.ts +++ b/src/execution-flow/flows/create-flow.ts @@ -32,8 +32,7 @@ export function createFlow({ onError, getErrorMessage, }: FlowConfig) { - return async (params: BaseFlowParam): Promise => { - const { step, page } = params; + return async ({ page, step }: BaseFlowParam): Promise => { const flowSpinner = spinner(); const message = getMessage(step); diff --git a/src/execution-flow/flows/isurl-flow.ts b/src/execution-flow/flows/isurl-flow.ts index 69842e2..7374b45 100644 --- a/src/execution-flow/flows/isurl-flow.ts +++ b/src/execution-flow/flows/isurl-flow.ts @@ -1,32 +1,39 @@ -import { spinner } from "@clack/prompts"; import { FailedAssertionError } from "@src/errors/workflow-error"; -import type { BaseFlowParam } from "@type/base-flow.types"; import type { IsURLAction } from "@type/workflow-config.types"; import regexOrStringMaker from "@utils/regex-or-string-maker"; -import chalk from "chalk"; import { expect } from "playwright/test"; +import { createFlow } from "./create-flow"; -export default async function isURLFlow({ - step: isURLStep, - page, -}: BaseFlowParam) { - const isTitleSpinner = spinner(); +const isURLFlow = createFlow({ + action: "isURL", - const { isRegex, value: titleOrRegex } = regexOrStringMaker(isURLStep.isURL); + getMessage(step) { + const { value: titleOrRegex } = regexOrStringMaker(step.isURL); + return `Checking visibility of page URL ${titleOrRegex}`; + }, - const LocatorDescription = isRegex ? "Regex:" : "URL:"; + async execute({ page, step }) { + const { value: titleOrRegex } = regexOrStringMaker(step.isURL); + await expect(page).toHaveURL(titleOrRegex); + }, - isTitleSpinner.start(`Checking visibility of page URL ${titleOrRegex}`); + getSuccessMessage(step) { + const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isURL); + const LocatorDescription = isRegex ? "Regex:" : "URL:"; - try { - await expect(page).toHaveURL(titleOrRegex); - isTitleSpinner.stop( - `${chalk.green("✓")} Page URL is visible with ${LocatorDescription} "${titleOrRegex}"`, - ); - } catch { - isTitleSpinner.stop( - `${chalk.red("✖")} isTitle: Page URL is not visible with ${LocatorDescription} "${titleOrRegex}"`, - ); + return `Page URL is visible with ${LocatorDescription} "${titleOrRegex}"`; + }, + + getErrorMessage(step) { + const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isURL); + const LocatorDescription = isRegex ? "Regex:" : "URL:"; + return `isURL: Page URL is not visible with ${LocatorDescription} "${titleOrRegex}"`; + }, + + onError(_, step) { + const { value: titleOrRegex } = regexOrStringMaker(step.isURL); throw new FailedAssertionError(`Assertion failed: ${titleOrRegex}`); - } -} + }, +}); + +export default isURLFlow; From 8803fa61739d0184cb421190b0cf7a105fb2cd01 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 21:35:32 +0530 Subject: [PATCH 08/17] feat: refactor isVisibleFlow to improve structure and readability --- src/execution-flow/flows/isvisible-flow.ts | 68 +++++++++++++--------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/src/execution-flow/flows/isvisible-flow.ts b/src/execution-flow/flows/isvisible-flow.ts index 5fd9ea0..258c24b 100644 --- a/src/execution-flow/flows/isvisible-flow.ts +++ b/src/execution-flow/flows/isvisible-flow.ts @@ -1,41 +1,57 @@ -import { spinner } from "@clack/prompts"; import { FailedAssertionError } from "@src/errors/workflow-error"; import type { BaseFlowParam } from "@type/base-flow.types"; import type { IsVisibleAction } from "@type/workflow-config.types"; -import chalk from "chalk"; import type { Locator } from "playwright"; import { expect } from "playwright/test"; +import { createFlow } from "./create-flow"; -export default async function isVisibleFlow({ - step: isVisibleStep, - page, -}: BaseFlowParam): Promise { - const isVisibleSpiner = spinner(); +function isVisisbleDescription(step: IsVisibleAction): string { + let targetDescription: `text: ${string}` | `selector: ${string}`; + if (typeof step.isVisible === "string") { + targetDescription = `text: ${step.isVisible}`; + } else { + targetDescription = `selector: ${step.isVisible.selector}`; + } + return targetDescription; +} +function isVisibleLocator({ + page, + step, +}: BaseFlowParam): Locator { let targetLocator: Locator; - let targetDescription: `text: ${string}` | `selector: ${string}`; - if (typeof isVisibleStep.isVisible === "string") { - targetLocator = page.getByText(isVisibleStep.isVisible).first(); - targetDescription = `text: ${isVisibleStep.isVisible}`; + if (typeof step.isVisible === "string") { + targetLocator = page.getByText(step.isVisible).first(); } else { - targetLocator = page.locator(isVisibleStep.isVisible.selector); - targetDescription = `selector: ${isVisibleStep.isVisible.selector}`; + targetLocator = page.locator(step.isVisible.selector); } - isVisibleSpiner.start( - `Checking visibility of element with ${targetDescription}`, - ); + return targetLocator; +} - try { +const isVisibleFlow = createFlow({ + action: "isVisible", + setLoadingMessage(step) { + const targetDescription = isVisisbleDescription(step); + return `Checking visibility of element with ${targetDescription}`; + }, + async execute(params) { + const targetLocator = isVisibleLocator(params); await expect(targetLocator).toBeVisible(); - isVisibleSpiner.stop( - `${chalk.green("✓")} Element is visible with ${targetDescription}`, - ); - } catch { - isVisibleSpiner.stop( - `${chalk.red("✖")} isVisible: Element is not visible with ${targetDescription}`, - ); + }, + getSuccessMessage(step) { + const targetDescription = isVisisbleDescription(step); + return ` Element is visible with ${targetDescription}`; + }, + getErrorMessage(step) { + const targetDescription = isVisisbleDescription(step); + return `isVisible: Element is not visible with ${targetDescription}`; + }, + onError(_, step) { + const targetDescription = isVisisbleDescription(step); throw new FailedAssertionError(`Assertion failed: ${targetDescription}`); - } -} + }, +}); + +export default isVisibleFlow; From 678cb9e77e339172ac313757da9864e1ee2fa662 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 21:35:42 +0530 Subject: [PATCH 09/17] feat: rename getMessage to setLoadingMessage for consistency across flows --- src/execution-flow/flows/clickon-flow.ts | 2 +- src/execution-flow/flows/create-flow.ts | 4 ++-- src/execution-flow/flows/is-disabled-flow.ts | 2 +- src/execution-flow/flows/istitle-flow.ts | 2 +- src/execution-flow/flows/isurl-flow.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/execution-flow/flows/clickon-flow.ts b/src/execution-flow/flows/clickon-flow.ts index 1d82648..8ae4ddf 100644 --- a/src/execution-flow/flows/clickon-flow.ts +++ b/src/execution-flow/flows/clickon-flow.ts @@ -29,7 +29,7 @@ function clickOnLocator({ page, step }: BaseFlowParam): Locator { const clickOnFlow = createFlow({ action: "clickOn", - getMessage: (step) => { + setLoadingMessage: (step) => { const targetDescription = clickOnDescription(step); return `Clicking on element with ${targetDescription}`; }, diff --git a/src/execution-flow/flows/create-flow.ts b/src/execution-flow/flows/create-flow.ts index 3fc4ac7..b5fce34 100644 --- a/src/execution-flow/flows/create-flow.ts +++ b/src/execution-flow/flows/create-flow.ts @@ -9,7 +9,7 @@ export interface FlowConfig { action: StepKeys; /** Generate descriptive message for spinner */ - getMessage: (step: T) => string; + setLoadingMessage: (step: T) => string; /** Core execution logic */ execute: (params: BaseFlowParam) => Promise; @@ -27,7 +27,7 @@ export interface FlowConfig { export function createFlow({ action, execute, - getMessage, + setLoadingMessage: getMessage, getSuccessMessage, onError, getErrorMessage, diff --git a/src/execution-flow/flows/is-disabled-flow.ts b/src/execution-flow/flows/is-disabled-flow.ts index b7fdb42..b1ca589 100644 --- a/src/execution-flow/flows/is-disabled-flow.ts +++ b/src/execution-flow/flows/is-disabled-flow.ts @@ -33,7 +33,7 @@ function isDisabledLocator({ const isDisabledFlow = createFlow({ action: "isDisabled", - getMessage(step) { + setLoadingMessage(step) { const targetDescription = isDisableDescription(step); return `Checking visibility of element with ${targetDescription}`; }, diff --git a/src/execution-flow/flows/istitle-flow.ts b/src/execution-flow/flows/istitle-flow.ts index 37b533c..be50287 100644 --- a/src/execution-flow/flows/istitle-flow.ts +++ b/src/execution-flow/flows/istitle-flow.ts @@ -10,7 +10,7 @@ const isTitleFlow = createFlow({ const { value: titleOrRegex } = regexOrStringMaker(step.isTitle); await expect(page).toHaveTitle(titleOrRegex); }, - getMessage(step) { + setLoadingMessage(step) { const { value: titleOrRegex } = regexOrStringMaker(step.isTitle); return `Checking visibility of page title ${titleOrRegex}`; }, diff --git a/src/execution-flow/flows/isurl-flow.ts b/src/execution-flow/flows/isurl-flow.ts index 7374b45..86a1552 100644 --- a/src/execution-flow/flows/isurl-flow.ts +++ b/src/execution-flow/flows/isurl-flow.ts @@ -7,7 +7,7 @@ import { createFlow } from "./create-flow"; const isURLFlow = createFlow({ action: "isURL", - getMessage(step) { + setLoadingMessage(step) { const { value: titleOrRegex } = regexOrStringMaker(step.isURL); return `Checking visibility of page URL ${titleOrRegex}`; }, From 5a032521d0c04373938c61955ed54f20ccc72880 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 21:36:41 +0530 Subject: [PATCH 10/17] feat: rename getSuccessMessage and getErrorMessage to setSuccessMessage and setErrorMessage --- src/execution-flow/flows/clickon-flow.ts | 4 ++-- src/execution-flow/flows/create-flow.ts | 8 ++++---- src/execution-flow/flows/is-disabled-flow.ts | 4 ++-- src/execution-flow/flows/istitle-flow.ts | 4 ++-- src/execution-flow/flows/isurl-flow.ts | 4 ++-- src/execution-flow/flows/isvisible-flow.ts | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/execution-flow/flows/clickon-flow.ts b/src/execution-flow/flows/clickon-flow.ts index 8ae4ddf..2d191c8 100644 --- a/src/execution-flow/flows/clickon-flow.ts +++ b/src/execution-flow/flows/clickon-flow.ts @@ -40,12 +40,12 @@ const clickOnFlow = createFlow({ await targetLocator.click(); }, - getSuccessMessage: (step) => { + setSuccessMessage: (step) => { const targetDescription = clickOnDescription(step); return `Clicked on element with ${targetDescription}`; }, - getErrorMessage: (step) => { + setErrorMessage: (step) => { const targetDescription = clickOnDescription(step); return `clickOn: Unable to find element with ${targetDescription}`; }, diff --git a/src/execution-flow/flows/create-flow.ts b/src/execution-flow/flows/create-flow.ts index b5fce34..f50da68 100644 --- a/src/execution-flow/flows/create-flow.ts +++ b/src/execution-flow/flows/create-flow.ts @@ -18,19 +18,19 @@ export interface FlowConfig { onError?: (error: unknown, step: T) => Error; /** Custom error message for stop spinner (defaults to getMessage) */ - getErrorMessage?: (step: T) => string; + setErrorMessage?: (step: T) => string; /** Custom success message for stop spinner (defaults to getMessage) */ - getSuccessMessage?: (step: T) => string; + setSuccessMessage?: (step: T) => string; } export function createFlow({ action, execute, setLoadingMessage: getMessage, - getSuccessMessage, + setSuccessMessage: getSuccessMessage, onError, - getErrorMessage, + setErrorMessage: getErrorMessage, }: FlowConfig) { return async ({ page, step }: BaseFlowParam): Promise => { const flowSpinner = spinner(); diff --git a/src/execution-flow/flows/is-disabled-flow.ts b/src/execution-flow/flows/is-disabled-flow.ts index b1ca589..acb659d 100644 --- a/src/execution-flow/flows/is-disabled-flow.ts +++ b/src/execution-flow/flows/is-disabled-flow.ts @@ -41,11 +41,11 @@ const isDisabledFlow = createFlow({ const targetLocator = isDisabledLocator(params); await expect(targetLocator).toBeDisabled(); }, - getSuccessMessage(step) { + setSuccessMessage(step) { const targetDescription = isDisableDescription(step); return `Element is disabled with ${targetDescription}`; }, - getErrorMessage(step) { + setErrorMessage(step) { const targetDescription = isDisableDescription(step); return `isDisabled: Element is not disabled with ${targetDescription}`; }, diff --git a/src/execution-flow/flows/istitle-flow.ts b/src/execution-flow/flows/istitle-flow.ts index be50287..0e27dca 100644 --- a/src/execution-flow/flows/istitle-flow.ts +++ b/src/execution-flow/flows/istitle-flow.ts @@ -14,12 +14,12 @@ const isTitleFlow = createFlow({ const { value: titleOrRegex } = regexOrStringMaker(step.isTitle); return `Checking visibility of page title ${titleOrRegex}`; }, - getSuccessMessage(step) { + setSuccessMessage(step) { const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isTitle); const LocatorDescription = isRegex ? "Regex:" : "Text:"; return `Page title is visible with ${LocatorDescription} "${titleOrRegex}"`; }, - getErrorMessage(step) { + setErrorMessage(step) { const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isTitle); const LocatorDescription = isRegex ? "Regex:" : "Text:"; return `isTitle: Page title is not visible with ${LocatorDescription} "${titleOrRegex}"`; diff --git a/src/execution-flow/flows/isurl-flow.ts b/src/execution-flow/flows/isurl-flow.ts index 86a1552..7690dde 100644 --- a/src/execution-flow/flows/isurl-flow.ts +++ b/src/execution-flow/flows/isurl-flow.ts @@ -17,14 +17,14 @@ const isURLFlow = createFlow({ await expect(page).toHaveURL(titleOrRegex); }, - getSuccessMessage(step) { + setSuccessMessage(step) { const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isURL); const LocatorDescription = isRegex ? "Regex:" : "URL:"; return `Page URL is visible with ${LocatorDescription} "${titleOrRegex}"`; }, - getErrorMessage(step) { + setErrorMessage(step) { const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isURL); const LocatorDescription = isRegex ? "Regex:" : "URL:"; return `isURL: Page URL is not visible with ${LocatorDescription} "${titleOrRegex}"`; diff --git a/src/execution-flow/flows/isvisible-flow.ts b/src/execution-flow/flows/isvisible-flow.ts index 258c24b..11d4976 100644 --- a/src/execution-flow/flows/isvisible-flow.ts +++ b/src/execution-flow/flows/isvisible-flow.ts @@ -40,11 +40,11 @@ const isVisibleFlow = createFlow({ const targetLocator = isVisibleLocator(params); await expect(targetLocator).toBeVisible(); }, - getSuccessMessage(step) { + setSuccessMessage(step) { const targetDescription = isVisisbleDescription(step); return ` Element is visible with ${targetDescription}`; }, - getErrorMessage(step) { + setErrorMessage(step) { const targetDescription = isVisisbleDescription(step); return `isVisible: Element is not visible with ${targetDescription}`; }, From 77509e34951499c65010f07bcc667f59b8f75b02 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sat, 17 Jan 2026 21:40:47 +0530 Subject: [PATCH 11/17] feat: refactor keyboardFlow to use createFlow for improved structure and readability --- src/execution-flow/flows/keyboard-flow.ts | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/execution-flow/flows/keyboard-flow.ts b/src/execution-flow/flows/keyboard-flow.ts index 915bc07..7f22a84 100644 --- a/src/execution-flow/flows/keyboard-flow.ts +++ b/src/execution-flow/flows/keyboard-flow.ts @@ -1,16 +1,17 @@ -import { spinner } from "@clack/prompts"; -import type { BaseFlowParam } from "@type/base-flow.types"; import type { KeyboardAction } from "@type/workflow-config.types"; -import chalk from "chalk"; +import { createFlow } from "./create-flow"; -export default async function keyboardFlow({ - step: keyboardStep, - page, -}: BaseFlowParam): Promise { - const keyboardSpinner = spinner(); - keyboardSpinner.start(`Pressing key: ${keyboardStep.keyboard.key}`); - await page.keyboard.press(keyboardStep.keyboard.key); - keyboardSpinner.stop( - `${chalk.green("✓")} Pressed key: ${keyboardStep.keyboard.key}`, - ); -} +const keyboardFlow = createFlow({ + action: "keyboard", + setLoadingMessage(step) { + return `Pressing key: ${step.keyboard.key}`; + }, + async execute({ page, step }) { + await page.keyboard.press(step.keyboard.key); + }, + setSuccessMessage(step) { + return `Pressed key: ${step.keyboard.key}`; + }, +}); + +export default keyboardFlow; From 4309ca1cbb734af86f6b7f8b98974f4fc1d330bb Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Sun, 18 Jan 2026 22:32:10 +0530 Subject: [PATCH 12/17] feat: refactor waitforFlow to use createFlow for improved structure and readability --- src/execution-flow/flows/waitfor-flow.ts | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/execution-flow/flows/waitfor-flow.ts b/src/execution-flow/flows/waitfor-flow.ts index 86a3d0a..1d192f5 100644 --- a/src/execution-flow/flows/waitfor-flow.ts +++ b/src/execution-flow/flows/waitfor-flow.ts @@ -1,16 +1,17 @@ -import { spinner } from "@clack/prompts"; -import type { BaseFlowParam } from "@type/base-flow.types"; import type { WaitForAction } from "@type/workflow-config.types"; -import chalk from "chalk"; +import { createFlow } from "./create-flow"; -export default async function waitforFlow({ - step: waitforStep, - page, -}: BaseFlowParam): Promise { - const waitForSpinner = spinner(); - waitForSpinner.start(`Waiting for: ${waitforStep.waitFor.duration}ms`); - await page.waitForTimeout(waitforStep.waitFor.duration); - waitForSpinner.stop( - `${chalk.green("✓")} Waited for: ${waitforStep.waitFor.duration}ms`, - ); -} +const waitforFlow = createFlow({ + action: "waitFor", + setLoadingMessage(step) { + return `Waiting for: ${step.waitFor.duration}ms`; + }, + async execute({ step, page }) { + await page.waitForTimeout(step.waitFor.duration); + }, + setSuccessMessage(step) { + return `Waited for: ${step.waitFor.duration}ms`; + }, +}); + +export default waitforFlow; From 6437691edbf786e653ef58e7586d2c32604b1874 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Mon, 19 Jan 2026 01:57:22 +0530 Subject: [PATCH 13/17] feat: refactor inputFlow to improve structure and readability --- src/execution-flow/flows/input-flow.ts | 163 ++++++++++++++++--------- 1 file changed, 102 insertions(+), 61 deletions(-) diff --git a/src/execution-flow/flows/input-flow.ts b/src/execution-flow/flows/input-flow.ts index f28f726..3f6f93d 100644 --- a/src/execution-flow/flows/input-flow.ts +++ b/src/execution-flow/flows/input-flow.ts @@ -1,68 +1,109 @@ -import { spinner } from "@clack/prompts"; import type { BaseFlowParam } from "@type/base-flow.types"; import type { Role } from "@type/role.types"; import type { InputAction } from "@type/workflow-config.types"; -import chalk from "chalk"; +import type { Locator } from "playwright"; +import { createFlow } from "./create-flow"; -export default async function inputFlow({ - step: inputStep, - page, -}: BaseFlowParam): Promise { - const inputSelector = inputStep.input.selector; - const inputSpinner = spinner(); - inputSpinner.start(`Filling input: ${inputSelector}`); - if (inputSelector.startsWith("label:")) { - const labelText = inputSelector.replace("label:", "").trim(); - await page - .getByLabel(labelText) - .fill(inputStep.input.value) - .then(() => { - inputSpinner.stop( - `${chalk.green("✓")} Filled input for label: ${labelText}`, - ); - }); - } else if (inputSelector.startsWith("testID:")) { - const id = inputSelector.replace("testID:", "").trim(); - await page - .getByTestId(id) - .fill(inputStep.input.value) - .then(() => { - inputSpinner.stop(`${chalk.green("✓")}Filled input for testID: ${id}`); - }); - } else if (inputSelector.startsWith("placeholder:")) { - const placeholderText = inputSelector.replace("placeholder:", "").trim(); - await page - .getByPlaceholder(placeholderText) - .fill(inputStep.input.value) - .then(() => { - inputSpinner.stop( - `${chalk.green( - "✓", - )} Filled input for placeholder: ${placeholderText}`, - ); - }); - } else if (inputSelector.startsWith("role:")) { - const roleAndName = inputSelector.replace("role:", "").trim(); - const [role, ...nameParts] = roleAndName.split(" "); - const name = nameParts.join(" ").replace('name="', "").replace('"', ""); - await page - .getByRole(role as Role, { name }) - .fill(inputStep.input.value) - .then(() => { - inputSpinner.stop( - `${chalk.green( - "✓", - )} Filled input for role: ${role} and name: ${name}`, - ); - }); - } else { - await page - .locator(inputSelector) - .fill(inputStep.input.value) - .then(() => { - inputSpinner.stop( - `${chalk.green("✓")} Filled input for selector: ${inputSelector}`, - ); +type Action = "label" | "testID" | "placeholder" | "role" | "selector"; + +type InputValueProp = { + action: Action; + step: InputAction; +}; + +type InputValue = + | { action: Exclude; value: string } + | { + action: Extract; + value: { role: string; name: string }; + }; + +function inputValue({ action, step }: InputValueProp): InputValue { + const inputSelector = step.input.selector; + + switch (action) { + case "label": { + const labelText = inputSelector.replace("label:", "").trim(); + return { action: "label", value: labelText }; + } + case "placeholder": { + const placeholderText = inputSelector.replace("placeholder:", "").trim(); + return { action: "placeholder", value: placeholderText }; + } + case "role": { + const roleAndName = inputSelector.replace("role:", "").trim(); + const [role, ...nameParts] = roleAndName.split(" "); + const name = nameParts.join(" ").replace('name="', "").replace('"', ""); + return { action: "role", value: { name, role } }; + } + case "selector": { + return { action: "selector", value: inputSelector }; + } + case "testID": { + const id = inputSelector.replace("testID:", "").trim(); + return { action: "testID", value: id }; + } + default: + return { action: "selector", value: inputSelector }; + } +} + +function getActionType(selector: string): Action { + if (selector.startsWith("label:")) return "label"; + if (selector.startsWith("testID:")) return "testID"; + if (selector.startsWith("placeholder:")) return "placeholder"; + if (selector.startsWith("role:")) return "role"; + return "selector"; +} + +function inputDescription(step: InputAction): string { + const actionType = getActionType(step.input.selector); + const result = inputValue({ action: actionType, step }); + + switch (result.action) { + case "label": + return `label: ${result.value}`; + case "testID": + return `testID: ${result.value}`; + case "placeholder": + return `placeholder: ${result.value}`; + case "role": + return `role: ${result.value.role} and name: ${result.value.name}`; + default: + return `selector: ${result.value}`; + } +} + +function inputLocator({ page, step }: BaseFlowParam): Locator { + const actionType = getActionType(step.input.selector); + const result = inputValue({ action: actionType, step }); + + switch (result.action) { + case "label": + return page.getByLabel(result.value); + case "testID": + return page.getByTestId(result.value); + case "placeholder": + return page.getByPlaceholder(result.value); + case "role": + return page.getByRole(result.value.role as Role, { + name: result.value.name, }); + default: + return page.locator(result.value); } } + +export const inputFlow = createFlow({ + action: "input", + async execute({ page, step }) { + const locator = inputLocator({ page, step }); + await locator.fill(step.input.value); + }, + setLoadingMessage(step) { + const target = inputDescription(step); + return `Filling input for ${target}`; + }, +}); + +export default inputFlow; From ad2b575d844c4b8f35ef6e18fcf681356129e2bb Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Mon, 19 Jan 2026 02:25:41 +0530 Subject: [PATCH 14/17] feat: update error handling in flows to return errors instead of throwing --- src/execution-flow/flows/clickon-flow.ts | 2 +- src/execution-flow/flows/is-disabled-flow.ts | 2 +- src/execution-flow/flows/is-not-visible-flow.ts | 2 +- src/execution-flow/flows/istitle-flow.ts | 2 +- src/execution-flow/flows/isurl-flow.ts | 2 +- src/execution-flow/flows/isvisible-flow.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/execution-flow/flows/clickon-flow.ts b/src/execution-flow/flows/clickon-flow.ts index 2d191c8..caa590f 100644 --- a/src/execution-flow/flows/clickon-flow.ts +++ b/src/execution-flow/flows/clickon-flow.ts @@ -52,7 +52,7 @@ const clickOnFlow = createFlow({ onError: (_, step) => { const targetDescription = clickOnDescription(step); - throw new ElementNotFoundError(`Element missing: ${targetDescription}`); + return new ElementNotFoundError(`Element missing: ${targetDescription}`); }, }); diff --git a/src/execution-flow/flows/is-disabled-flow.ts b/src/execution-flow/flows/is-disabled-flow.ts index acb659d..b3fc48b 100644 --- a/src/execution-flow/flows/is-disabled-flow.ts +++ b/src/execution-flow/flows/is-disabled-flow.ts @@ -51,7 +51,7 @@ const isDisabledFlow = createFlow({ }, onError(_, step) { const targetDescription = isDisableDescription(step); - throw new FailedAssertionError(`Assertion failed: ${targetDescription}`); + return new FailedAssertionError(`Assertion failed: ${targetDescription}`); }, }); diff --git a/src/execution-flow/flows/is-not-visible-flow.ts b/src/execution-flow/flows/is-not-visible-flow.ts index 5834b1b..59e17e7 100644 --- a/src/execution-flow/flows/is-not-visible-flow.ts +++ b/src/execution-flow/flows/is-not-visible-flow.ts @@ -36,6 +36,6 @@ export default async function isNotVisibleFlow({ isNotVisibleSpiner.stop( `${chalk.red("✖")} isNotVisible: Element is visible with ${targetDescription}`, ); - throw new FailedAssertionError(`Assertion failed: ${targetDescription}`); + return new FailedAssertionError(`Assertion failed: ${targetDescription}`); } } diff --git a/src/execution-flow/flows/istitle-flow.ts b/src/execution-flow/flows/istitle-flow.ts index 0e27dca..725b44d 100644 --- a/src/execution-flow/flows/istitle-flow.ts +++ b/src/execution-flow/flows/istitle-flow.ts @@ -26,7 +26,7 @@ const isTitleFlow = createFlow({ }, onError(_, step) { const { value: titleOrRegex } = regexOrStringMaker(step.isTitle); - throw new FailedAssertionError(`Assertion failed: ${titleOrRegex}`); + return new FailedAssertionError(`Assertion failed: ${titleOrRegex}`); }, }); export default isTitleFlow; diff --git a/src/execution-flow/flows/isurl-flow.ts b/src/execution-flow/flows/isurl-flow.ts index 7690dde..450a046 100644 --- a/src/execution-flow/flows/isurl-flow.ts +++ b/src/execution-flow/flows/isurl-flow.ts @@ -32,7 +32,7 @@ const isURLFlow = createFlow({ onError(_, step) { const { value: titleOrRegex } = regexOrStringMaker(step.isURL); - throw new FailedAssertionError(`Assertion failed: ${titleOrRegex}`); + return new FailedAssertionError(`Assertion failed: ${titleOrRegex}`); }, }); diff --git a/src/execution-flow/flows/isvisible-flow.ts b/src/execution-flow/flows/isvisible-flow.ts index 11d4976..a643760 100644 --- a/src/execution-flow/flows/isvisible-flow.ts +++ b/src/execution-flow/flows/isvisible-flow.ts @@ -50,7 +50,7 @@ const isVisibleFlow = createFlow({ }, onError(_, step) { const targetDescription = isVisisbleDescription(step); - throw new FailedAssertionError(`Assertion failed: ${targetDescription}`); + return new FailedAssertionError(`Assertion failed: ${targetDescription}`); }, }); From 5683c13361858058b6bbfa971b0fffa3b16c6266 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Mon, 19 Jan 2026 02:29:39 +0530 Subject: [PATCH 15/17] feat: fix typo in isVisibleDescription function name and update references --- src/execution-flow/flows/isvisible-flow.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/execution-flow/flows/isvisible-flow.ts b/src/execution-flow/flows/isvisible-flow.ts index a643760..104a269 100644 --- a/src/execution-flow/flows/isvisible-flow.ts +++ b/src/execution-flow/flows/isvisible-flow.ts @@ -5,7 +5,7 @@ import type { Locator } from "playwright"; import { expect } from "playwright/test"; import { createFlow } from "./create-flow"; -function isVisisbleDescription(step: IsVisibleAction): string { +function isVisibleDescription(step: IsVisibleAction): string { let targetDescription: `text: ${string}` | `selector: ${string}`; if (typeof step.isVisible === "string") { targetDescription = `text: ${step.isVisible}`; @@ -33,7 +33,7 @@ function isVisibleLocator({ const isVisibleFlow = createFlow({ action: "isVisible", setLoadingMessage(step) { - const targetDescription = isVisisbleDescription(step); + const targetDescription = isVisibleDescription(step); return `Checking visibility of element with ${targetDescription}`; }, async execute(params) { @@ -41,15 +41,15 @@ const isVisibleFlow = createFlow({ await expect(targetLocator).toBeVisible(); }, setSuccessMessage(step) { - const targetDescription = isVisisbleDescription(step); + const targetDescription = isVisibleDescription(step); return ` Element is visible with ${targetDescription}`; }, setErrorMessage(step) { - const targetDescription = isVisisbleDescription(step); + const targetDescription = isVisibleDescription(step); return `isVisible: Element is not visible with ${targetDescription}`; }, onError(_, step) { - const targetDescription = isVisisbleDescription(step); + const targetDescription = isVisibleDescription(step); return new FailedAssertionError(`Assertion failed: ${targetDescription}`); }, }); From eae818b6d15d71a5839faebecc2871d5652173c6 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Mon, 19 Jan 2026 02:32:07 +0530 Subject: [PATCH 16/17] feat: update error message in setErrorMessage for clarity --- src/execution-flow/flows/isurl-flow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/execution-flow/flows/isurl-flow.ts b/src/execution-flow/flows/isurl-flow.ts index 450a046..a36be72 100644 --- a/src/execution-flow/flows/isurl-flow.ts +++ b/src/execution-flow/flows/isurl-flow.ts @@ -27,7 +27,7 @@ const isURLFlow = createFlow({ setErrorMessage(step) { const { isRegex, value: titleOrRegex } = regexOrStringMaker(step.isURL); const LocatorDescription = isRegex ? "Regex:" : "URL:"; - return `isURL: Page URL is not visible with ${LocatorDescription} "${titleOrRegex}"`; + return `isURL: Page URL does not match ${LocatorDescription} "${titleOrRegex}"`; }, onError(_, step) { From 245acfe4b105ccd3af929dcea0b77ae24224d7b5 Mon Sep 17 00:00:00 2001 From: Sawan Bhattacharya Date: Mon, 19 Jan 2026 02:37:38 +0530 Subject: [PATCH 17/17] feat: refactor isNotVisibleFlow for improved structure and readability --- .../flows/is-not-visible-flow.ts | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/src/execution-flow/flows/is-not-visible-flow.ts b/src/execution-flow/flows/is-not-visible-flow.ts index 59e17e7..bc5dbca 100644 --- a/src/execution-flow/flows/is-not-visible-flow.ts +++ b/src/execution-flow/flows/is-not-visible-flow.ts @@ -1,41 +1,59 @@ -import { spinner } from "@clack/prompts"; import { FailedAssertionError } from "@src/errors/workflow-error"; import type { BaseFlowParam } from "@type/base-flow.types"; import type { IsNotVisibleAction } from "@type/workflow-config.types"; -import chalk from "chalk"; import type { Locator } from "playwright"; import { expect } from "playwright/test"; +import { createFlow } from "./create-flow"; -export default async function isNotVisibleFlow({ - step: isNotVisibleStep, +function isNotVisibleDescription( + step: IsNotVisibleAction, +): `text: ${string}` | `selector: ${string}` { + if (typeof step.isNotVisible === "string") { + return `text: ${step.isNotVisible}`; + } + return `selector: ${step.isNotVisible.selector}`; +} + +function isNotVisibleLocator({ page, -}: BaseFlowParam) { - const isNotVisibleSpiner = spinner(); - - let targetLocator: Locator; - let targetDescription: `text: ${string}` | `selector: ${string}`; - - if (typeof isNotVisibleStep.isNotVisible === "string") { - targetLocator = page.getByText(isNotVisibleStep.isNotVisible).first(); - targetDescription = `text: ${isNotVisibleStep.isNotVisible}`; - } else { - targetLocator = page.locator(isNotVisibleStep.isNotVisible.selector); - targetDescription = `selector: ${isNotVisibleStep.isNotVisible.selector}`; + step, +}: BaseFlowParam): Locator { + if (typeof step.isNotVisible === "string") { + return page.getByText(step.isNotVisible).first(); } + return page.locator(step.isNotVisible.selector); +} - isNotVisibleSpiner.start( - `Checking non-visibility of element with ${targetDescription}`, - ); +export const isNotVisibleFlow = createFlow({ + action: "isNotVisible", - try { - await expect(targetLocator).toBeHidden(); - isNotVisibleSpiner.stop( - `${chalk.green("✓")} Element is not visible with ${targetDescription}`, - ); - } catch { - isNotVisibleSpiner.stop( - `${chalk.red("✖")} isNotVisible: Element is visible with ${targetDescription}`, + setLoadingMessage: (step) => { + const targetDescription = isNotVisibleDescription(step); + return `Checking non-visibility of element with ${targetDescription}`; + }, + + execute: async ({ page, step }) => { + const locator = isNotVisibleLocator({ page, step }); + // Usually expect(...).toBeHidden() is what we want for "isNotVisible" + await expect(locator).toBeHidden(); + }, + + setSuccessMessage: (step) => { + const targetDescription = isNotVisibleDescription(step); + return `Element is not visible with ${targetDescription}`; + }, + + setErrorMessage: (step) => { + const targetDescription = isNotVisibleDescription(step); + return `isNotVisible: Element is visible with ${targetDescription}`; + }, + + onError: (error, step) => { + const targetDescription = isNotVisibleDescription(step); + throw new FailedAssertionError( + `Assertion failed: ${targetDescription}. Reason: ${error instanceof Error ? error.message : "unknown"}`, ); - return new FailedAssertionError(`Assertion failed: ${targetDescription}`); - } -} + }, +}); + +export default isNotVisibleFlow;