Repository: appium/appium-flutter-driver
Pull request #797: feat: New Assertions methods and better usability
Author: login: Zubairahmdfaraz
Labels: name: size:M
description: contribution size: M
Created: 2025-05-16T12:24:05Z | Merged: 2025-05-23T05:49:11Z

This PR adds support for custom visibility assertions and utility actions in the appium-flutter-driver, including:

✅ Added Custom Commands:

flutter:assertVisible
flutter:assertNotVisible
flutter:assertTappable
flutter:tap
flutter:click
flutter:getText
flutter:pageBack
flutter:clear

These commands allow clients to:
Verify widget visibility and tappability using Flutter finders (e.g., by key, text, or label, encoded in base64)
Interact with UI elements via tap, click, and clear
Navigate back in the app (pageBack)
Retrieve widget text content (getText)

All commands have been locally tested and validated on real devices with a variety of Flutter widget trees to ensure correct behavior and compatibility..


Diff:
diff --git a/README.md b/README.md
index 2adf9ee6..07706182 100644
--- a/README.md
+++ b/README.md
@@ -307,6 +307,44 @@ Please replace them properly with your client.
 | -                                                                                                                                  | :ok: | (Ruby) `driver.execute_script 'flutter:launchApp', 'bundleId', {arguments: ['arg1'], environment: {ENV1: 'env'}}` | Flutter Driver    |
 | dragAndDropWithCommandExtension                                                                                                    | :ok: | (Python) `driver.execute_script('flutter:dragAndDropWithCommandExtension', payload)` | Command Extension |
 
+# Flutter Visibility Assertions for Appium
+
+This module extends the `appium-flutter-driver` with custom visibility-related commands using `driver.execute()`.
+
+## ✅ Supported Commands
+| **Command**         | **Status** | **Example Usage**                                                                                                         | **Target** |
+|---------------------|------------|--------------------------------------------------------------------------------------------------------------------------|------------|
+| `assertVisible`     | ✅         | `driver.execute('flutter:assertVisible', { key: 'myKey' })`<br>`driver.execute('flutter:assertVisible', { text: 'Login' })` | Widget     |
+| `assertNotVisible`  | ✅         | `driver.execute('flutter:assertNotVisible', { key: 'hiddenWidget' })`                                                   | Widget     |
+| `assertTappable`    | ✅         | `driver.execute('flutter:assertTappable', { label: 'Submit' })`                                                          | Widget     |
+| `tap`               | ✅         | `driver.execute('flutter:tap', [{ key: 'submit_button' }])`                                                              | Widget     |
+| `click`             | ✅         | `driver.execute('flutter:click', { text: 'Continue' })`                                                                  | Widget     |
+ `getText`           | ✅         | `driver.execute('flutter:getText', { key: 'counterText' })`                             | Widget      |
+| `pageBack`          | ✅         | `await driver.execute('flutter:pageBack')`                                              | Navigation  |
+| `clear`             | ✅         | `await driver.execute('flutter:clear', { key: 'emailInput' })`                          | Input Field |
+
+
+
+## 🔍 Input Formats
+
+Each assertion supports the following input formats:
+
+- `{ key: 'valueKey' }`
+- `{ text: 'Text on widget' }`
+- `{ label: 'Tooltip text' }`
+
+These map to Flutter finders:
+- `byValueKey`
+- `byText`
+- `byTooltip`
+
+## 📦 Integration
+
+These commands are typically invoked using a client helper method like:
+
+```ts
+await assertVisible(driver, { key: 'submit_button' });
+
 **NOTE**
 >`flutter:launchApp` launches an app via instrument service. `mobile:activateApp` and `driver.activate_app` are via XCTest API. They are a bit different.
 
diff --git a/driver/README.md b/driver/README.md
index 2aa7f252..278f15de 100644
--- a/driver/README.md
+++ b/driver/README.md
@@ -309,6 +309,47 @@ Please replace them properly with your client.
 | - | :ok: | (Ruby) `driver.execute_script 'flutter:connectObservatoryWsUrl'` | Flutter Driver |
 | - | :ok: | (Ruby) `driver.execute_script 'flutter:launchApp', 'bundleId', {arguments: ['arg1'], environment: {ENV1: 'env'}}` | Flutter Driver |
 
+## 📌 Additional Flutter Assertions (Custom)
+
+# Flutter Visibility Assertions for Appium
+
+This module extends the `appium-flutter-driver` with custom visibility-related commands using `driver.execute()`.
+
+✅ Supported Commands
+
+| **Command**         | **Status** | **Example Usage**                                                                                                         | **Target** |
+|---------------------|------------|--------------------------------------------------------------------------------------------------------------------------|------------|
+| `assertVisible`     | ✅         | `driver.execute('flutter:assertVisible', { key: 'myKey' })`<br>`driver.execute('flutter:assertVisible', { text: 'Login' })` | Widget     |
+| `assertNotVisible`  | ✅         | `driver.execute('flutter:assertNotVisible', { key: 'hiddenWidget' })`                                                   | Widget     |
+| `assertTappable`    | ✅         | `driver.execute('flutter:assertTappable', { label: 'Submit' })`                                                          | Widget     |
+| `tap`               | ✅         | `driver.execute('flutter:tap', [{ key: 'submit_button' }])`                                                              | Widget     |
+| `click`             | ✅         | `driver.execute('flutter:click', { text: 'Continue' })`                                                                  | Widget     |
+| `getText`           | ✅         | `driver.execute('flutter:getText', { key: 'counterText' })`                             | Widget      |
+| `pageBack`          | ✅         | `await driver.execute('flutter:pageBack')`                                              | Navigation  |
+| `clear`             | ✅         | `await driver.execute('flutter:clear', { key: 'emailInput' })`                          | Input Field |
+
+## 🔍 Input Formats
+
+Each assertion supports the following input formats:
+
+- `{ key: 'valueKey' }`
+- `{ text: 'Text on widget' }`
+- `{ label: 'Tooltip text' }`
+
+These map to Flutter finders:
+- `byValueKey`
+- `byText`
+- `byTooltip`
+
+## 📦 Integration
+
+These commands are typically invoked using a client helper method like:
+
+```ts
+await assertVisible(driver, { key: 'submit_button' });
+Flutter API	Status	WebDriver example (JavaScript, WebDriverIO)	Scope
+```  
+
 > **NOTE**
 > `flutter:launchApp` launches an app via instrument service. `mobile:activateApp` and `driver.activate_app` are via XCTest API. They are a bit different.
 
diff --git a/driver/lib/commands/assertions.ts b/driver/lib/commands/assertions.ts
new file mode 100644
index 00000000..d1e90577
--- /dev/null
+++ b/driver/lib/commands/assertions.ts
@@ -0,0 +1,90 @@
+import { FlutterDriver } from '../driver';
+import { byValueKey, byText, byTooltip } from 'appium-flutter-finder';
+import type { SerializableFinder } from 'appium-flutter-finder';
+
+export type FinderInput =
+  | { key: string }
+  | { text: string }
+  | { label: string }
+  | SerializableFinder
+  | string
+  | { getRawFinder: () => SerializableFinder }; // FlutterElement-like input
+
+// Serialize a finder to base64
+const serializeFinder = (finder: SerializableFinder): string =>
+  Buffer.from(JSON.stringify(finder)).toString('base64');
+
+// Type guards
+const isRawFinder = (input: any): input is SerializableFinder =>
+  input && typeof input === 'object' && typeof input.finderType === 'string';
+
+const isFlutterElementLike = (input: any): input is { getRawFinder: () => SerializableFinder } =>
+  input && typeof input === 'object' && typeof input.getRawFinder === 'function';
+
+// Convert FinderInput to base64 string
+function getFinderBase64(input: FinderInput): string {
+  if (typeof input === 'string') {
+    return input; // already base64
+  }
+
+  if (isFlutterElementLike(input)) {
+    return serializeFinder(input.getRawFinder());
+  }
+
+  if (isRawFinder(input)) {
+    return serializeFinder(input);
+  }
+
+  if ('key' in input) {
+    return serializeFinder(byValueKey(input.key));
+  }
+
+  if ('text' in input) {
+    return serializeFinder(byText(input.text));
+  }
+
+  if ('label' in input) {
+    return serializeFinder(byTooltip(input.label));
+  }
+
+  throw new Error('Invalid finder input: must provide key, text, label, raw finder, or FlutterElement');
+}
+
+// Generic helper to wrap assert commands
+async function executeAssertion(
+  driver: FlutterDriver,
+  command: string,
+  input: FinderInput,
+  timeout = 5000,
+  extraArgs: object = {}
+): Promise<void> {
+  const base64 = getFinderBase64(input);
+  try {
+    await driver.executeElementCommand(command, base64, { timeout, ...extraArgs });
+  } catch (err) {
+    throw new Error(`Assertion failed on command "${command}" within ${timeout}ms\n${err}`);
+  }
+}
+
+// Exported assertion commands
+export const assertVisible = async (
+  driver: FlutterDriver,
+  input: FinderInput,
+  timeout = 5000
+): Promise<void> =>
+  await executeAssertion(driver, 'waitFor', input, timeout, { visible: true });
+
+export const assertNotVisible = async (
+  driver: FlutterDriver,
+  input: FinderInput,
+  timeout = 5000
+): Promise<void> =>
+  await executeAssertion(driver, 'waitForAbsent', input, timeout);
+
+export const assertTappable = async (
+  driver: FlutterDriver,
+  input: FinderInput,
+  timeout = 5000
+): Promise<void> =>
+  await executeAssertion(driver, 'waitForTappable', input, timeout);
+
diff --git a/driver/lib/commands/execute.ts b/driver/lib/commands/execute.ts
index 00b7d227..7578956f 100644
--- a/driver/lib/commands/execute.ts
+++ b/driver/lib/commands/execute.ts
@@ -1,254 +1,203 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import type { FlutterDriver } from '../driver';
 import { reConnectFlutterDriver } from '../sessions/session';
-import { longTap, scroll, scrollIntoView, scrollUntilVisible, scrollUntilTapable } from './execute/scroll';
-import { waitFor, waitForAbsent, waitForTappable } from './execute/wait';
+import {
+  longTap,
+  scroll,
+  scrollIntoView,
+  scrollUntilVisible,
+  scrollUntilTapable,
+  pageBack
+} from './execute/scroll';
+import {
+  waitFor,
+  waitForAbsent,
+  waitForTappable
+} from './execute/wait';
+import { clear, getText } from './element';
+import { tap, click } from './gesture';
+import {
+  assertVisible,
+  assertNotVisible,
+  assertTappable,
+  type FinderInput
+} from './assertions';
+
 import { launchApp } from './../ios/app';
 import B from 'bluebird';
 
-
 const flutterCommandRegex = /^[\s]*flutter[\s]*:(.+)/;
 
-export const execute = async function(
-  this: FlutterDriver,
-  rawCommand: string,
-  args: any[],
-) {
-  // flutter
-  const matching = rawCommand.match(flutterCommandRegex);
-  if (!matching) {
-    throw new Error(`Command not support: "${rawCommand}"`);
-  }
 
-  const command = matching[1].trim();
-  switch (command) {
-    case `launchApp`:
-      return await flutterLaunchApp(this, args[0], args[1]);
-    case `connectObservatoryWsUrl`:
-      return await connectObservatoryWsUrl(this);
-    case `getVMInfo`:
-      return await getVMInfo(this);
-    case `setIsolateId`:
-      return await setIsolateId(this, args[0]);
-    case `getIsolate`:
-      return await getIsolate(this, args[0]);
-    case `checkHealth`:
-      return await checkHealth(this);
-    case `clearTimeline`:
-      return await clearTimeline(this);
-    case `forceGC`:
-      return await forceGC(this);
-    case `getRenderTree`:
-      return await getRenderTree(this);
-    case `getBottomLeft`:
-      return await getOffset(this, args[0], { offsetType: `bottomLeft` });
-    case `getBottomRight`:
-      return await getOffset(this, args[0], { offsetType: `bottomRight` });
-    case `getCenter`:
-      return await getOffset(this, args[0], { offsetType: `center` });
-    case `getTopLeft`:
-      return await getOffset(this, args[0], { offsetType: `topLeft` });
-    case `getTopRight`:
-      return await getOffset(this, args[0], { offsetType: `topRight` });
-    case `getRenderObjectDiagnostics`:
-      return await getRenderObjectDiagnostics(this, args[0], args[1]);
-    case `getWidgetDiagnostics`:
-      return await getWidgetDiagnostics(this, args[0], args[1]);
-    case `getSemanticsId`:
-      return await getSemanticsId(this, args[0]);
-    case `waitForAbsent`:
-      return await waitForAbsent(this, args[0], args[1]);
-    case `waitFor`:
-      return await waitFor(this, args[0], args[1]);
-    case `waitForTappable`:
-      return await waitForTappable(this, args[0], args[1]);
-    case `scroll`:
-      return await scroll(this, args[0], args[1]);
-    case `scrollUntilVisible`:
-      return await scrollUntilVisible(this, args[0], args[1]);
-    case `scrollUntilTapable`:
-      return await scrollUntilTapable(this, args[0], args[1]);
-    case `scrollIntoView`:
-      return await scrollIntoView(this, args[0], args[1]);
-    case `setTextEntryEmulation`:
-      return await setTextEntryEmulation(this, args[0]);
-    case `enterText`:
-      return await enterText(this, args[0]);
-    case `requestData`:
-      return await requestData(this, args[0]);
-    case `longTap`:
-      return await longTap(this, args[0], args[1]);
-    case `waitForFirstFrame`:
-      return await waitForCondition(this, { conditionName: `FirstFrameRasterizedCondition`});
-    case `setFrameSync`:
-      return await setFrameSync(this, args[0], args[1]);
-    case `clickElement`:
-      return await clickElement(this, args[0], args[1]);
-    case `dragAndDropWithCommandExtension`:
-      return await dragAndDropWithCommandExtension(this, args[0]);
-    case `getTextWithCommandExtension`:
-      return await getTextWithCommandExtension(this, args[0]);
-    default:
-      throw new Error(`Command not support: "${rawCommand}"`);
-  }
-};
 
-const flutterLaunchApp = async (
-  self: FlutterDriver, appId: string, opts
-) => {
-  const { arguments: args = [], environment: env = {}} = opts;
-  await launchApp(self.internalCaps.udid!, appId, args, env);
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
+// Define types for better type safety
+type CommandHandler = (driver: FlutterDriver, ...args: any[]) => Promise<any>;
+type CommandMap = Record<string, CommandHandler>;
 
-const connectObservatoryWsUrl = async (self: FlutterDriver) => {
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
-
-const checkHealth = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_health`)).status;
-
-const getVMInfo = async (self: FlutterDriver) =>
-  (await self.executeGetVMCommand());
+interface DragAndDropParams {
+  startX: string;
+  startY: string;
+  endX: string;
+  endY: string;
+  duration: string;
+}
 
-const getRenderTree = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_render_tree`)).tree;
+interface DiagnosticsOptions {
+  subtreeDepth?: number;
+  includeProperties?: boolean;
+}
 
-const getOffset = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  offsetType: {offsetType: string},
-) => await self.executeElementCommand(`get_offset`, elementBase64, offsetType);
 
-const waitForCondition = async (
-  self: FlutterDriver,
-  conditionName: {conditionName: string},
-) => await self.executeElementCommand(`waitForCondition`, ``, conditionName);
-
-const forceGC = async (self: FlutterDriver) => {
-  const response = await self.socket!.call(`_collectAllGarbage`, {
-    isolateId: self.socket!.isolateId,
-  }) as { type: string };
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
 
-const setIsolateId = async (self: FlutterDriver, isolateId: string) => {
-  self.socket!.isolateId = isolateId;
-  return await self.socket!.call(`getIsolate`, {
-    isolateId: `${isolateId}`,
-  });
-};
+interface LongTapOptions {
+  durationMilliseconds: number;
+  frequency?: number;
+}
 
-const getIsolate = async (
-  self: FlutterDriver, isolateId: string|undefined
-) => await self.executeGetIsolateCommand(isolateId || self.socket!.isolateId);
-
-const clearTimeline = async (self: FlutterDriver) => {
-  // @todo backward compatible, need to cleanup later
-  const call1: Promise<any> = self.socket!.call(`_clearVMTimeline`);
-  const call2: Promise<any> = self.socket!.call(`clearVMTimeline`);
-  const response = await B.any([call1, call2]);
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
+interface OffsetOptions {
+  offsetType: 'bottomLeft' | 'bottomRight' | 'center' | 'topLeft' | 'topRight';
+}
 
-const getRenderObjectDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+// Extract command handlers into a separate object for better organization
+const commandHandlers: CommandMap = {
+  launchApp: async (driver, appId: string, opts = {}) => {
+    const { arguments: args = [], environment: env = {} } = opts;
+    await launchApp(driver.internalCaps.udid!, appId, args, env);
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  connectObservatoryWsUrl: async (driver) => {
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  checkHealth: async (driver) => (await driver.executeElementCommand('get_health')).status,
+  getVMInfo: async (driver) => await driver.executeGetVMCommand(),
+  getRenderTree: async (driver) => (await driver.executeElementCommand('get_render_tree')).tree,
+  getOffset: async (driver, elementBase64: string, options: OffsetOptions) =>
+    await driver.executeElementCommand('get_offset', elementBase64, options),
+  waitForCondition: async (driver, conditionName: string) =>
+    await driver.executeElementCommand('waitForCondition', '', { conditionName }),
+  forceGC: async (driver) => {
+    const response = await driver.socket!.call('_collectAllGarbage', {
+      isolateId: driver.socket!.isolateId,
+    }) as { type: string };
+    if (response.type !== 'Success') {
+      throw new Error(`Could not forceGC, response was ${JSON.stringify(response)}`);
+    }
+  },
+  setIsolateId: async (driver, isolateId: string) => {
+    driver.socket!.isolateId = isolateId;
+    return await driver.socket!.call('getIsolate', { isolateId });
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `renderObject`,
+  getIsolate: async (driver, isolateId?: string) =>
+    await driver.executeGetIsolateCommand(isolateId || driver.socket!.isolateId!),
+  clearTimeline: async (driver) => {
+    const call1 = driver.socket!.call('_clearVMTimeline');
+    const call2 = driver.socket!.call('clearVMTimeline');
+    const response = await B.any([call1, call2]);
+    if (response.type !== 'Success') {
+      throw new Error(`Could not clear timeline, response was ${JSON.stringify(response)}`);
+    }
+  },
+  getRenderObjectDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'renderObject',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getWidgetDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+    });
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `widget`,
+  getWidgetDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'widget',
       includeProperties,
       subtreeDepth,
-    },
-  );
+    });
+  },
+  getSemanticsId: async (driver, elementBase64: string) =>
+    (await driver.executeElementCommand('get_semantics_id', elementBase64)).id,
+  waitForAbsent: async (driver, finder: string, timeout?: number) =>
+    await waitForAbsent(driver, finder, timeout),
+  waitFor: async (driver, finder: string, timeout?: number) =>
+    await waitFor(driver, finder, timeout),
+  waitForTappable: async (driver, finder: string, timeout?: number) =>
+    await waitForTappable(driver, finder, timeout),
+  scroll: async (driver, finder: string, opts: any) =>
+    await scroll(driver, finder, opts),
+  scrollUntilVisible: async (driver, finder: string, opts: any) =>
+    await scrollUntilVisible(driver, finder, opts),
+  scrollUntilTapable: async (driver, finder: string, opts: any) =>
+    await scrollUntilTapable(driver, finder, opts),
+  scrollIntoView: async (driver, finder: string, opts: any) =>
+    await scrollIntoView(driver, finder, opts),
+  setTextEntryEmulation: async (driver, enabled: boolean) =>
+    await driver.socket!.executeSocketCommand({ command: 'set_text_entry_emulation', enabled }),
+  enterText: async (driver, text: string) =>
+    await driver.socket!.executeSocketCommand({ command: 'enter_text', text }),
+  requestData: async (driver, message: string) =>
+    await driver.socket!.executeSocketCommand({ command: 'request_data', message }),
+  longTap: async (driver, finder: string, durationOrOptions?: LongTapOptions) =>
+    await longTap(driver, finder, durationOrOptions),
+  waitForFirstFrame: async (driver) =>
+    await driver.executeElementCommand('waitForCondition', '', { conditionName: 'FirstFrameRasterizedCondition' }),
+  setFrameSync: async (driver, enabled: boolean, durationMilliseconds: number) =>
+    await driver.socket!.executeSocketCommand({
+      command: 'set_frame_sync',
+      enabled,
+      timeout: durationMilliseconds,
+    }),
+  clickElement: async (driver, finder: string, opts: { timeout?: number } = {}) => {
+    const { timeout = 1000 } = opts;
+    return await driver.executeElementCommand('tap', finder, { timeout });
+  },
+  dragAndDropWithCommandExtension: async (driver, params: DragAndDropParams) =>
+    await driver.socket!.executeSocketCommand({
+      command: 'dragAndDropWithCommandExtension',
+      ...params,
+    }),
+  assertVisible: async (driver, input: FinderInput, timeout = 5000) =>
+    await assertVisible(driver, input, timeout),
+
+  assertNotVisible: async (driver, input: FinderInput, timeout = 5000) =>
+    await assertNotVisible(driver, input, timeout),
+
+  assertTappable: async (driver, input: FinderInput, timeout = 5000) =>
+    await assertTappable(driver, input, timeout),
+
+  tap: async (driver, gestures: Record<string, any>[], longPress: boolean = false) =>
+    await tap.call(driver, gestures, longPress),
+
+  click: async (driver, input: FinderInput) =>
+    await click.call(driver, input),
+  getText: async (driver, element: string) =>
+    await getText.call(driver, element),
+  pageBack: async (driver) =>
+    await pageBack.call(driver),
+  clear: async (driver, input: FinderInput) =>
+    await clear.call(driver, input),
+
+  getTextWithCommandExtension: async (driver, params: { findBy: string }) =>
+    await driver.socket!.executeSocketCommand({
+      command: 'getTextWithCommandExtension',
+      findBy: params.findBy,
+    }),
 };
+export const execute = async function (
+  this: FlutterDriver,
+  rawCommand: string,
+  args: any[],
+) {
+  const matching = rawCommand.match(flutterCommandRegex);
+  if (!matching) {
+    throw new Error(`Command not supported: "${rawCommand}"`);
+  }
 
-const getSemanticsId = async (self: FlutterDriver, elementBase64: string) =>
-  (await self.executeElementCommand(`get_semantics_id`, elementBase64)).id;
-
-const enterText = async (self: FlutterDriver, text: string) =>
-  await self.socket!.executeSocketCommand({ command: `enter_text`, text });
-
-const requestData = async (self: FlutterDriver, message: string) =>
-  await self.socket!.executeSocketCommand({ command: `request_data`, message });
-
-const setFrameSync = async (self, bool: boolean, durationMilliseconds: number) =>
-  await self.socket!.executeSocketCommand({
-    command: `set_frame_sync`,
-    enabled: bool,
-    timeout: durationMilliseconds,
-  });
-
-const setTextEntryEmulation = async (self: FlutterDriver, enabled: boolean) =>
-  await self.socket!.executeSocketCommand({ command: `set_text_entry_emulation`, enabled });
-
-const clickElement = async (self:FlutterDriver, elementBase64: string, opts) => {
-  const {timeout = 1000} = opts;
-  return await self.executeElementCommand(`tap`, elementBase64, {
-        timeout
-  });
-};
+  const command = matching[1].trim();
+  const handler = commandHandlers[command];
 
-const dragAndDropWithCommandExtension = async (
-  self: FlutterDriver,
-  params: {
-    startX: string;
-    startY: string;
-    endX: string;
-    endY: string;
-    duration: string;
+  if (!handler) {
+    throw new Error(`Command not supported: "${rawCommand}"`);
   }
-) => {
-  const { startX, startY, endX, endY, duration } = params;
-  const commandPayload = {
-    command: 'dragAndDropWithCommandExtension',
-    startX,
-    startY,
-    endX,
-    endY,
-    duration,
-  };
-  return await self.socket!.executeSocketCommand(commandPayload);
-};
 
-const getTextWithCommandExtension = async (self: FlutterDriver, params: { findBy: string; }) => {
-  const payload = {
-      command: 'getTextWithCommandExtension',
-      findBy: params.findBy,
-  };
-  return await self.socket!.executeSocketCommand(payload);
+  return await handler(this, ...args);
 };
+
diff --git a/driver/lib/commands/execute/scroll.ts b/driver/lib/commands/execute/scroll.ts
index bec3d0c7..8806479f 100644
--- a/driver/lib/commands/execute/scroll.ts
+++ b/driver/lib/commands/execute/scroll.ts
@@ -43,26 +43,24 @@ export const scroll = async (
 export const longTap = async (
   self: FlutterDriver,
   elementBase64: string,
-  opts: {
+  durationOrOptions: number | {
     durationMilliseconds: number;
     frequency?: number;
-  },
+  } = 1000, // Default to 1000ms if nothing provided
 ) => {
-  const { durationMilliseconds, frequency = 60 } = opts;
+  const options = typeof durationOrOptions === 'number'
+    ? { durationMilliseconds: durationOrOptions }
+    : durationOrOptions;
 
-  if (
-    typeof durationMilliseconds !== `number` ||
-    typeof frequency !== `number`
-  ) {
-    // @todo BaseDriver's errors.InvalidArgumentError();
-    throw new Error(`${opts} is not a valid options`);
+  const { durationMilliseconds, frequency = 60 } = options;
+
+  if (typeof durationMilliseconds !== 'number' || typeof frequency !== 'number') {
+    throw new Error(`Invalid longTap options: ${JSON.stringify(options)}`);
   }
 
-  return await self.executeElementCommand(`scroll`, elementBase64, {
+  return await self.executeElementCommand('scroll', elementBase64, {
     dx: 0,
     dy: 0,
-    // 'scroll' expects microseconds
-    // https://github.com/flutter/flutter/blob/master/packages/flutter_driver/lib/src/common/gesture.dart#L33-L38
     duration: durationMilliseconds * 1000,
     frequency,
   });
@@ -206,3 +204,13 @@ export const scrollIntoView = async (
 
   return await self.executeElementCommand(`scrollIntoView`, elementBase64, args);
 };
+
+
+export const pageBack = async function (this: FlutterDriver): Promise<void> {
+  if (this.proxydriver && typeof this.proxydriver.back === 'function') {
+    return await this.proxydriver.back(); // use platform driver
+  }
+
+  // fallback
+  throw new Error(`Back navigation is not supported on this platform.`);
+};
\ No newline at end of file
diff --git a/driver/package.json b/driver/package.json
index f2bc4ca8..885d2e3e 100644
--- a/driver/package.json
+++ b/driver/package.json
@@ -69,8 +69,9 @@
     "typescript": "~5.8"
   },
   "dependencies": {
-    "appium-ios-device": "^2.7.18",
     "appium-android-driver": "^10.0.0",
+    "appium-flutter-finder": "^0.2.0",
+    "appium-ios-device": "^2.7.18",
     "appium-uiautomator2-driver": "^4.2.3",
     "appium-xcuitest-driver": "^9.2.3",
     "asyncbox": "^3.0.0",
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..99b2c94b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+  "dependencies": {
+    "appium-flutter-finder": "^0.2.0"
+  },
+  "devDependencies": {
+    "@types/node": "^22.15.18"
+  }
+}


Reviews:
user: login: KazuCocoa
body: And need to fix eslint
https://github.com/appium/appium-flutter-driver/actions/runs/15068366656/job/42358321998?pr=797
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T21:35:37Z
user: login: Zubairahmdfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T07:13:58Z
user: login: Zubairahmdfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T07:14:35Z
user: login: Zubairahmdfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T07:24:10Z
user: login: Zubairahmdfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T07:44:51Z
user: login: Zubairahmdfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T08:01:18Z
user: login: KazuCocoa
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T05:48:19Z
user: login: KazuCocoa
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T05:48:29Z

Review comments:
diff_hunk: @@ -1,254 +1,197 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import type { FlutterDriver } from '../driver';
 import { reConnectFlutterDriver } from '../sessions/session';
-import { longTap, scroll, scrollIntoView, scrollUntilVisible, scrollUntilTapable } from './execute/scroll';
-import { waitFor, waitForAbsent, waitForTappable } from './execute/wait';
+import {
+  longTap,
+  scroll,
+  scrollIntoView,
+  scrollUntilVisible,
+  scrollUntilTapable
+} from './execute/scroll';
+import {
+  waitFor,
+  waitForAbsent,
+  waitForTappable
+} from './execute/wait';
+import { getText } from './element';
+import { tap, click } from './gesture';
+import {
+  assertVisible,
+  assertNotVisible,
+  assertTappable,
+  type FinderInput
+} from './assertions';
+
 import { launchApp } from './../ios/app';
 import B from 'bluebird';
 
-
 const flutterCommandRegex = /^[\s]*flutter[\s]*:(.+)/;
 
-export const execute = async function(
-  this: FlutterDriver,
-  rawCommand: string,
-  args: any[],
-) {
-  // flutter
-  const matching = rawCommand.match(flutterCommandRegex);
-  if (!matching) {
-    throw new Error(`Command not support: "${rawCommand}"`);
-  }
-
-  const command = matching[1].trim();
-  switch (command) {
-    case `launchApp`:
-      return await flutterLaunchApp(this, args[0], args[1]);
-    case `connectObservatoryWsUrl`:
-      return await connectObservatoryWsUrl(this);
-    case `getVMInfo`:
-      return await getVMInfo(this);
-    case `setIsolateId`:
-      return await setIsolateId(this, args[0]);
-    case `getIsolate`:
-      return await getIsolate(this, args[0]);
-    case `checkHealth`:
-      return await checkHealth(this);
-    case `clearTimeline`:
-      return await clearTimeline(this);
-    case `forceGC`:
-      return await forceGC(this);
-    case `getRenderTree`:
-      return await getRenderTree(this);
-    case `getBottomLeft`:
-      return await getOffset(this, args[0], { offsetType: `bottomLeft` });
-    case `getBottomRight`:
-      return await getOffset(this, args[0], { offsetType: `bottomRight` });
-    case `getCenter`:
-      return await getOffset(this, args[0], { offsetType: `center` });
-    case `getTopLeft`:
-      return await getOffset(this, args[0], { offsetType: `topLeft` });
-    case `getTopRight`:
-      return await getOffset(this, args[0], { offsetType: `topRight` });
-    case `getRenderObjectDiagnostics`:
-      return await getRenderObjectDiagnostics(this, args[0], args[1]);
-    case `getWidgetDiagnostics`:
-      return await getWidgetDiagnostics(this, args[0], args[1]);
-    case `getSemanticsId`:
-      return await getSemanticsId(this, args[0]);
-    case `waitForAbsent`:
-      return await waitForAbsent(this, args[0], args[1]);
-    case `waitFor`:
-      return await waitFor(this, args[0], args[1]);
-    case `waitForTappable`:
-      return await waitForTappable(this, args[0], args[1]);
-    case `scroll`:
-      return await scroll(this, args[0], args[1]);
-    case `scrollUntilVisible`:
-      return await scrollUntilVisible(this, args[0], args[1]);
-    case `scrollUntilTapable`:
-      return await scrollUntilTapable(this, args[0], args[1]);
-    case `scrollIntoView`:
-      return await scrollIntoView(this, args[0], args[1]);
-    case `setTextEntryEmulation`:
-      return await setTextEntryEmulation(this, args[0]);
-    case `enterText`:
-      return await enterText(this, args[0]);
-    case `requestData`:
-      return await requestData(this, args[0]);
-    case `longTap`:
-      return await longTap(this, args[0], args[1]);
-    case `waitForFirstFrame`:
-      return await waitForCondition(this, { conditionName: `FirstFrameRasterizedCondition`});
-    case `setFrameSync`:
-      return await setFrameSync(this, args[0], args[1]);
-    case `clickElement`:
-      return await clickElement(this, args[0], args[1]);
-    case `dragAndDropWithCommandExtension`:
-      return await dragAndDropWithCommandExtension(this, args[0]);
-    case `getTextWithCommandExtension`:
-      return await getTextWithCommandExtension(this, args[0]);
-    default:
-      throw new Error(`Command not support: "${rawCommand}"`);
-  }
-};
-
-const flutterLaunchApp = async (
-  self: FlutterDriver, appId: string, opts
-) => {
-  const { arguments: args = [], environment: env = {}} = opts;
-  await launchApp(self.internalCaps.udid!, appId, args, env);
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const connectObservatoryWsUrl = async (self: FlutterDriver) => {
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const checkHealth = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_health`)).status;
+// Define types for better type safety
+type CommandHandler = (driver: FlutterDriver, ...args: any[]) => Promise<any>;
+type CommandMap = Record<string, CommandHandler>;
 
-const getVMInfo = async (self: FlutterDriver) =>
-  (await self.executeGetVMCommand());
+interface DragAndDropParams {
+  startX: string;
+  startY: string;
+  endX: string;
+  endY: string;
+  duration: string;
+}
 
-const getRenderTree = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_render_tree`)).tree;
+interface DiagnosticsOptions {
+  subtreeDepth?: number;
+  includeProperties?: boolean;
+}
 
-const getOffset = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  offsetType: {offsetType: string},
-) => await self.executeElementCommand(`get_offset`, elementBase64, offsetType);
 
-const waitForCondition = async (
-  self: FlutterDriver,
-  conditionName: {conditionName: string},
-) => await self.executeElementCommand(`waitForCondition`, ``, conditionName);
 
-const forceGC = async (self: FlutterDriver) => {
-  const response = await self.socket!.call(`_collectAllGarbage`, {
-    isolateId: self.socket!.isolateId,
-  }) as { type: string };
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
+interface LongTapOptions {
+  durationMilliseconds: number;
+  frequency?: number;
+}
 
-const setIsolateId = async (self: FlutterDriver, isolateId: string) => {
-  self.socket!.isolateId = isolateId;
-  return await self.socket!.call(`getIsolate`, {
-    isolateId: `${isolateId}`,
-  });
-};
+interface OffsetOptions {
+  offsetType: 'bottomLeft' | 'bottomRight' | 'center' | 'topLeft' | 'topRight';
+}
 
-const getIsolate = async (
-  self: FlutterDriver, isolateId: string|undefined
-) => await self.executeGetIsolateCommand(isolateId || self.socket!.isolateId);
-
-const clearTimeline = async (self: FlutterDriver) => {
-  // @todo backward compatible, need to cleanup later
-  const call1: Promise<any> = self.socket!.call(`_clearVMTimeline`);
-  const call2: Promise<any> = self.socket!.call(`clearVMTimeline`);
-  const response = await B.any([call1, call2]);
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
-
-const getRenderObjectDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+// Extract command handlers into a separate object for better organization
+const commandHandlers: CommandMap = {
+  launchApp: async (driver, appId: string, opts = {}) => {
+    const { arguments: args = [], environment: env = {} } = opts;
+    await launchApp(driver.internalCaps.udid!, appId, args, env);
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  connectObservatoryWsUrl: async (driver) => {
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  checkHealth: async (driver) => (await driver.executeElementCommand('get_health')).status,
+  getVMInfo: async (driver) => await driver.executeGetVMCommand(),
+  getRenderTree: async (driver) => (await driver.executeElementCommand('get_render_tree')).tree,
+  getOffset: async (driver, elementBase64: string, options: OffsetOptions) => 
+    await driver.executeElementCommand('get_offset', elementBase64, options),
+  waitForCondition: async (driver, conditionName: string) => 
+    await driver.executeElementCommand('waitForCondition', '', { conditionName }),
+  forceGC: async (driver) => {
+    const response = await driver.socket!.call('_collectAllGarbage', {
+      isolateId: driver.socket!.isolateId,
+    }) as { type: string };
+    if (response.type !== 'Success') {
+      throw new Error(`Could not forceGC, response was ${JSON.stringify(response)}`);
+    }
+  },
+  setIsolateId: async (driver, isolateId: string) => {
+    driver.socket!.isolateId = isolateId;
+    return await driver.socket!.call('getIsolate', { isolateId });
+  },
+  getIsolate: async (driver, isolateId?: string) => 
+    await driver.executeGetIsolateCommand(isolateId || driver.socket!.isolateId!),
+  clearTimeline: async (driver) => {
+    const call1 = driver.socket!.call('_clearVMTimeline');
+    const call2 = driver.socket!.call('clearVMTimeline');
+    const response = await B.any([call1, call2]);
+    if (response.type !== 'Success') {
+      throw new Error(`Could not clear timeline, response was ${JSON.stringify(response)}`);
+    }
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `renderObject`,
+  getRenderObjectDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'renderObject',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getWidgetDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+    });
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `widget`,
+  getWidgetDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'widget',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getSemanticsId = async (self: FlutterDriver, elementBase64: string) =>
-  (await self.executeElementCommand(`get_semantics_id`, elementBase64)).id;
-
-const enterText = async (self: FlutterDriver, text: string) =>
-  await self.socket!.executeSocketCommand({ command: `enter_text`, text });
-
-const requestData = async (self: FlutterDriver, message: string) =>
-  await self.socket!.executeSocketCommand({ command: `request_data`, message });
-
-const setFrameSync = async (self, bool: boolean, durationMilliseconds: number) =>
-  await self.socket!.executeSocketCommand({
-    command: `set_frame_sync`,
-    enabled: bool,
-    timeout: durationMilliseconds,
-  });
-
-const setTextEntryEmulation = async (self: FlutterDriver, enabled: boolean) =>
-  await self.socket!.executeSocketCommand({ command: `set_text_entry_emulation`, enabled });
-
-const clickElement = async (self:FlutterDriver, elementBase64: string, opts) => {
-  const {timeout = 1000} = opts;
-  return await self.executeElementCommand(`tap`, elementBase64, {
-        timeout
-  });
+    });
+  },
+  getSemanticsId: async (driver, elementBase64: string) => 
+    (await driver.executeElementCommand('get_semantics_id', elementBase64)).id,
+  waitForAbsent: async (driver, finder: string, timeout?: number) => 
+    await waitForAbsent(driver, finder, timeout),
+  waitFor: async (driver, finder: string, timeout?: number) => 
+    await waitFor(driver, finder, timeout),
+  waitForTappable: async (driver, finder: string, timeout?: number) => 
+    await waitForTappable(driver, finder, timeout),
+  scroll: async (driver, finder: string, opts: any) => 
+    await scroll(driver, finder, opts),
+  scrollUntilVisible: async (driver, finder: string, opts: any) => 
+    await scrollUntilVisible(driver, finder, opts),
+  scrollUntilTapable: async (driver, finder: string, opts: any) => 
+    await scrollUntilTapable(driver, finder, opts),
+  scrollIntoView: async (driver, finder: string, opts: any) => 
+    await scrollIntoView(driver, finder, opts),
+  setTextEntryEmulation: async (driver, enabled: boolean) => 
+    await driver.socket!.executeSocketCommand({ command: 'set_text_entry_emulation', enabled }),
+  enterText: async (driver, text: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'enter_text', text }),
+  requestData: async (driver, message: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'request_data', message }),
+  longTap: async (driver, finder: string, durationOrOptions?: number | LongTapOptions) => 
+  await longTap(driver, finder, durationOrOptions),   
path: driver/lib/commands/execute.ts
user: login: KazuCocoa
body: Can you use only `LongTapOptions`? (and spaces)
```suggestion
  longTap: async (driver, finder: string, durationOrOptions?: LongTapOptions) => 
    await longTap(driver, finder, durationOrOptions),   
```
created_at: 2025-05-16T20:54:50Z
author_association: MEMBER
diff_hunk: @@ -7,6 +7,7 @@
     "outDir": "build",
     "types": ["node"],
     "checkJs": true
+
path: driver/tsconfig.json
user: login: KazuCocoa
body: ```suggestion
```
created_at: 2025-05-16T21:05:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,8 @@
+{
path: package.json
user: login: KazuCocoa
body: Please remove unused package.json
created_at: 2025-05-16T21:05:49Z
author_association: MEMBER
diff_hunk: @@ -307,6 +307,38 @@ Please replace them properly with your client.
 | -                                                                                                                                  | :ok: | (Ruby) `driver.execute_script 'flutter:launchApp', 'bundleId', {arguments: ['arg1'], environment: {ENV1: 'env'}}` | Flutter Driver    |
 | dragAndDropWithCommandExtension                                                                                                    | :ok: | (Python) `driver.execute_script('flutter:dragAndDropWithCommandExtension', payload)` | Command Extension |
 
+# Flutter Visibility Assertions for Appium
+
+This module extends the `appium-flutter-driver` with custom visibility-related commands using `driver.execute()`.
+
+## ✅ Supported Commands
+
+| **Command**         | **Status** | **Example Usage**                                                                                                         | **Target** |
+|---------------------|------------|--------------------------------------------------------------------------------------------------------------------------|------------|
+| `assertVisible`     | ✅         | `driver.execute('flutter:assertVisible', { key: 'myKey' })`<br>`driver.execute('flutter:assertVisible', { text: 'Login' })` | Widget     |
+| `assertNotVisible`  | ✅         | `driver.execute('flutter:assertNotVisible', { key: 'hiddenWidget' })`                                                   | Widget     |
+| `assertTappable`    | ✅         | `driver.execute('flutter:assertTappable', { label: 'Submit' })`                                                          | Widget     |
+
+## 🔍 Input Formats
+
+Each assertion supports the following input formats:
+
+- `{ key: 'valueKey' }`
+- `{ text: 'Text on widget' }`
+- `{ label: 'Tooltip text' }`
+
+These map to Flutter finders:
+- `byValueKey`
+- `byText`
+- `byTooltip`
+
+## 📦 Integration
+
+These commands are typically invoked using a client helper method like:
+
+```ts
+await assertVisible(driver, { key: 'submit_button' });
path: README.md
user: login: KazuCocoa
body: missing <code>```</code>
created_at: 2025-05-16T21:31:14Z
author_association: MEMBER
diff_hunk: @@ -1,254 +1,197 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import type { FlutterDriver } from '../driver';
 import { reConnectFlutterDriver } from '../sessions/session';
-import { longTap, scroll, scrollIntoView, scrollUntilVisible, scrollUntilTapable } from './execute/scroll';
-import { waitFor, waitForAbsent, waitForTappable } from './execute/wait';
+import {
+  longTap,
+  scroll,
+  scrollIntoView,
+  scrollUntilVisible,
+  scrollUntilTapable
+} from './execute/scroll';
+import {
+  waitFor,
+  waitForAbsent,
+  waitForTappable
+} from './execute/wait';
+import { getText } from './element';
+import { tap, click } from './gesture';
+import {
+  assertVisible,
+  assertNotVisible,
+  assertTappable,
+  type FinderInput
+} from './assertions';
+
 import { launchApp } from './../ios/app';
 import B from 'bluebird';
 
-
 const flutterCommandRegex = /^[\s]*flutter[\s]*:(.+)/;
 
-export const execute = async function(
-  this: FlutterDriver,
-  rawCommand: string,
-  args: any[],
-) {
-  // flutter
-  const matching = rawCommand.match(flutterCommandRegex);
-  if (!matching) {
-    throw new Error(`Command not support: "${rawCommand}"`);
-  }
-
-  const command = matching[1].trim();
-  switch (command) {
-    case `launchApp`:
-      return await flutterLaunchApp(this, args[0], args[1]);
-    case `connectObservatoryWsUrl`:
-      return await connectObservatoryWsUrl(this);
-    case `getVMInfo`:
-      return await getVMInfo(this);
-    case `setIsolateId`:
-      return await setIsolateId(this, args[0]);
-    case `getIsolate`:
-      return await getIsolate(this, args[0]);
-    case `checkHealth`:
-      return await checkHealth(this);
-    case `clearTimeline`:
-      return await clearTimeline(this);
-    case `forceGC`:
-      return await forceGC(this);
-    case `getRenderTree`:
-      return await getRenderTree(this);
-    case `getBottomLeft`:
-      return await getOffset(this, args[0], { offsetType: `bottomLeft` });
-    case `getBottomRight`:
-      return await getOffset(this, args[0], { offsetType: `bottomRight` });
-    case `getCenter`:
-      return await getOffset(this, args[0], { offsetType: `center` });
-    case `getTopLeft`:
-      return await getOffset(this, args[0], { offsetType: `topLeft` });
-    case `getTopRight`:
-      return await getOffset(this, args[0], { offsetType: `topRight` });
-    case `getRenderObjectDiagnostics`:
-      return await getRenderObjectDiagnostics(this, args[0], args[1]);
-    case `getWidgetDiagnostics`:
-      return await getWidgetDiagnostics(this, args[0], args[1]);
-    case `getSemanticsId`:
-      return await getSemanticsId(this, args[0]);
-    case `waitForAbsent`:
-      return await waitForAbsent(this, args[0], args[1]);
-    case `waitFor`:
-      return await waitFor(this, args[0], args[1]);
-    case `waitForTappable`:
-      return await waitForTappable(this, args[0], args[1]);
-    case `scroll`:
-      return await scroll(this, args[0], args[1]);
-    case `scrollUntilVisible`:
-      return await scrollUntilVisible(this, args[0], args[1]);
-    case `scrollUntilTapable`:
-      return await scrollUntilTapable(this, args[0], args[1]);
-    case `scrollIntoView`:
-      return await scrollIntoView(this, args[0], args[1]);
-    case `setTextEntryEmulation`:
-      return await setTextEntryEmulation(this, args[0]);
-    case `enterText`:
-      return await enterText(this, args[0]);
-    case `requestData`:
-      return await requestData(this, args[0]);
-    case `longTap`:
-      return await longTap(this, args[0], args[1]);
-    case `waitForFirstFrame`:
-      return await waitForCondition(this, { conditionName: `FirstFrameRasterizedCondition`});
-    case `setFrameSync`:
-      return await setFrameSync(this, args[0], args[1]);
-    case `clickElement`:
-      return await clickElement(this, args[0], args[1]);
-    case `dragAndDropWithCommandExtension`:
-      return await dragAndDropWithCommandExtension(this, args[0]);
-    case `getTextWithCommandExtension`:
-      return await getTextWithCommandExtension(this, args[0]);
-    default:
-      throw new Error(`Command not support: "${rawCommand}"`);
-  }
-};
-
-const flutterLaunchApp = async (
-  self: FlutterDriver, appId: string, opts
-) => {
-  const { arguments: args = [], environment: env = {}} = opts;
-  await launchApp(self.internalCaps.udid!, appId, args, env);
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const connectObservatoryWsUrl = async (self: FlutterDriver) => {
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const checkHealth = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_health`)).status;
+// Define types for better type safety
+type CommandHandler = (driver: FlutterDriver, ...args: any[]) => Promise<any>;
+type CommandMap = Record<string, CommandHandler>;
 
-const getVMInfo = async (self: FlutterDriver) =>
-  (await self.executeGetVMCommand());
+interface DragAndDropParams {
+  startX: string;
+  startY: string;
+  endX: string;
+  endY: string;
+  duration: string;
+}
 
-const getRenderTree = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_render_tree`)).tree;
+interface DiagnosticsOptions {
+  subtreeDepth?: number;
+  includeProperties?: boolean;
+}
 
-const getOffset = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  offsetType: {offsetType: string},
-) => await self.executeElementCommand(`get_offset`, elementBase64, offsetType);
 
-const waitForCondition = async (
-  self: FlutterDriver,
-  conditionName: {conditionName: string},
-) => await self.executeElementCommand(`waitForCondition`, ``, conditionName);
 
-const forceGC = async (self: FlutterDriver) => {
-  const response = await self.socket!.call(`_collectAllGarbage`, {
-    isolateId: self.socket!.isolateId,
-  }) as { type: string };
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
+interface LongTapOptions {
+  durationMilliseconds: number;
+  frequency?: number;
+}
 
-const setIsolateId = async (self: FlutterDriver, isolateId: string) => {
-  self.socket!.isolateId = isolateId;
-  return await self.socket!.call(`getIsolate`, {
-    isolateId: `${isolateId}`,
-  });
-};
+interface OffsetOptions {
+  offsetType: 'bottomLeft' | 'bottomRight' | 'center' | 'topLeft' | 'topRight';
+}
 
-const getIsolate = async (
-  self: FlutterDriver, isolateId: string|undefined
-) => await self.executeGetIsolateCommand(isolateId || self.socket!.isolateId);
-
-const clearTimeline = async (self: FlutterDriver) => {
-  // @todo backward compatible, need to cleanup later
-  const call1: Promise<any> = self.socket!.call(`_clearVMTimeline`);
-  const call2: Promise<any> = self.socket!.call(`clearVMTimeline`);
-  const response = await B.any([call1, call2]);
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
-
-const getRenderObjectDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+// Extract command handlers into a separate object for better organization
+const commandHandlers: CommandMap = {
+  launchApp: async (driver, appId: string, opts = {}) => {
+    const { arguments: args = [], environment: env = {} } = opts;
+    await launchApp(driver.internalCaps.udid!, appId, args, env);
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  connectObservatoryWsUrl: async (driver) => {
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  checkHealth: async (driver) => (await driver.executeElementCommand('get_health')).status,
+  getVMInfo: async (driver) => await driver.executeGetVMCommand(),
+  getRenderTree: async (driver) => (await driver.executeElementCommand('get_render_tree')).tree,
+  getOffset: async (driver, elementBase64: string, options: OffsetOptions) => 
+    await driver.executeElementCommand('get_offset', elementBase64, options),
+  waitForCondition: async (driver, conditionName: string) => 
+    await driver.executeElementCommand('waitForCondition', '', { conditionName }),
+  forceGC: async (driver) => {
+    const response = await driver.socket!.call('_collectAllGarbage', {
+      isolateId: driver.socket!.isolateId,
+    }) as { type: string };
+    if (response.type !== 'Success') {
+      throw new Error(`Could not forceGC, response was ${JSON.stringify(response)}`);
+    }
+  },
+  setIsolateId: async (driver, isolateId: string) => {
+    driver.socket!.isolateId = isolateId;
+    return await driver.socket!.call('getIsolate', { isolateId });
+  },
+  getIsolate: async (driver, isolateId?: string) => 
+    await driver.executeGetIsolateCommand(isolateId || driver.socket!.isolateId!),
+  clearTimeline: async (driver) => {
+    const call1 = driver.socket!.call('_clearVMTimeline');
+    const call2 = driver.socket!.call('clearVMTimeline');
+    const response = await B.any([call1, call2]);
+    if (response.type !== 'Success') {
+      throw new Error(`Could not clear timeline, response was ${JSON.stringify(response)}`);
+    }
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `renderObject`,
+  getRenderObjectDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'renderObject',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getWidgetDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+    });
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `widget`,
+  getWidgetDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'widget',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getSemanticsId = async (self: FlutterDriver, elementBase64: string) =>
-  (await self.executeElementCommand(`get_semantics_id`, elementBase64)).id;
-
-const enterText = async (self: FlutterDriver, text: string) =>
-  await self.socket!.executeSocketCommand({ command: `enter_text`, text });
-
-const requestData = async (self: FlutterDriver, message: string) =>
-  await self.socket!.executeSocketCommand({ command: `request_data`, message });
-
-const setFrameSync = async (self, bool: boolean, durationMilliseconds: number) =>
-  await self.socket!.executeSocketCommand({
-    command: `set_frame_sync`,
-    enabled: bool,
-    timeout: durationMilliseconds,
-  });
-
-const setTextEntryEmulation = async (self: FlutterDriver, enabled: boolean) =>
-  await self.socket!.executeSocketCommand({ command: `set_text_entry_emulation`, enabled });
-
-const clickElement = async (self:FlutterDriver, elementBase64: string, opts) => {
-  const {timeout = 1000} = opts;
-  return await self.executeElementCommand(`tap`, elementBase64, {
-        timeout
-  });
+    });
+  },
+  getSemanticsId: async (driver, elementBase64: string) => 
+    (await driver.executeElementCommand('get_semantics_id', elementBase64)).id,
+  waitForAbsent: async (driver, finder: string, timeout?: number) => 
+    await waitForAbsent(driver, finder, timeout),
+  waitFor: async (driver, finder: string, timeout?: number) => 
+    await waitFor(driver, finder, timeout),
+  waitForTappable: async (driver, finder: string, timeout?: number) => 
+    await waitForTappable(driver, finder, timeout),
+  scroll: async (driver, finder: string, opts: any) => 
+    await scroll(driver, finder, opts),
+  scrollUntilVisible: async (driver, finder: string, opts: any) => 
+    await scrollUntilVisible(driver, finder, opts),
+  scrollUntilTapable: async (driver, finder: string, opts: any) => 
+    await scrollUntilTapable(driver, finder, opts),
+  scrollIntoView: async (driver, finder: string, opts: any) => 
+    await scrollIntoView(driver, finder, opts),
+  setTextEntryEmulation: async (driver, enabled: boolean) => 
+    await driver.socket!.executeSocketCommand({ command: 'set_text_entry_emulation', enabled }),
+  enterText: async (driver, text: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'enter_text', text }),
+  requestData: async (driver, message: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'request_data', message }),
+  longTap: async (driver, finder: string, durationOrOptions?: number | LongTapOptions) => 
+  await longTap(driver, finder, durationOrOptions),   
+  waitForFirstFrame: async (driver) => 
+    await driver.executeElementCommand('waitForCondition', '', { conditionName: 'FirstFrameRasterizedCondition' }),
+  setFrameSync: async (driver, enabled: boolean, durationMilliseconds: number) => 
+    await driver.socket!.executeSocketCommand({
+      command: 'set_frame_sync',
+      enabled,
+      timeout: durationMilliseconds,
+    }),
+  clickElement: async (driver, finder: string, opts: { timeout?: number } = {}) => {
+    const { timeout = 1000 } = opts;
+    return await driver.executeElementCommand('tap', finder, { timeout });
+  },
+  dragAndDropWithCommandExtension: async (driver, params: DragAndDropParams) => 
+    await driver.socket!.executeSocketCommand({
+      command: 'dragAndDropWithCommandExtension',
+      ...params,
+    }),
+  assertVisible: async (driver, input: FinderInput, timeout = 5000) =>
+  await assertVisible(driver, input, timeout),
+
+  assertNotVisible: async (driver, input: FinderInput, timeout = 5000) =>
+    await assertNotVisible(driver, input, timeout),
+
+  assertTappable: async (driver, input: FinderInput, timeout = 5000) =>
+    await assertTappable(driver, input, timeout),
+  
+  tap: async (driver, gestures: Record<string, any>[], longPress: boolean = false) =>
+    await tap.call(driver, gestures, longPress),
+
+  click: async (driver, input: FinderInput) => await click.call(driver, input),
+
+  getText: async (driver: FlutterDriver, finder: string) =>
+    await getText.bind(driver)(finder),
path: driver/lib/commands/execute.ts
user: login: KazuCocoa
body: Can you separate these new commands with the docs update?
Probably `tap`, `click` and `getText` for `flutter:tap`, `flutter:click` and `flutter:getText` command? They haven't addressed in the description.
created_at: 2025-05-16T21:34:51Z
author_association: MEMBER
diff_hunk: @@ -1,254 +1,197 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import type { FlutterDriver } from '../driver';
 import { reConnectFlutterDriver } from '../sessions/session';
-import { longTap, scroll, scrollIntoView, scrollUntilVisible, scrollUntilTapable } from './execute/scroll';
-import { waitFor, waitForAbsent, waitForTappable } from './execute/wait';
+import {
+  longTap,
+  scroll,
+  scrollIntoView,
+  scrollUntilVisible,
+  scrollUntilTapable
+} from './execute/scroll';
+import {
+  waitFor,
+  waitForAbsent,
+  waitForTappable
+} from './execute/wait';
+import { getText } from './element';
+import { tap, click } from './gesture';
+import {
+  assertVisible,
+  assertNotVisible,
+  assertTappable,
+  type FinderInput
+} from './assertions';
+
 import { launchApp } from './../ios/app';
 import B from 'bluebird';
 
-
 const flutterCommandRegex = /^[\s]*flutter[\s]*:(.+)/;
 
-export const execute = async function(
-  this: FlutterDriver,
-  rawCommand: string,
-  args: any[],
-) {
-  // flutter
-  const matching = rawCommand.match(flutterCommandRegex);
-  if (!matching) {
-    throw new Error(`Command not support: "${rawCommand}"`);
-  }
-
-  const command = matching[1].trim();
-  switch (command) {
-    case `launchApp`:
-      return await flutterLaunchApp(this, args[0], args[1]);
-    case `connectObservatoryWsUrl`:
-      return await connectObservatoryWsUrl(this);
-    case `getVMInfo`:
-      return await getVMInfo(this);
-    case `setIsolateId`:
-      return await setIsolateId(this, args[0]);
-    case `getIsolate`:
-      return await getIsolate(this, args[0]);
-    case `checkHealth`:
-      return await checkHealth(this);
-    case `clearTimeline`:
-      return await clearTimeline(this);
-    case `forceGC`:
-      return await forceGC(this);
-    case `getRenderTree`:
-      return await getRenderTree(this);
-    case `getBottomLeft`:
-      return await getOffset(this, args[0], { offsetType: `bottomLeft` });
-    case `getBottomRight`:
-      return await getOffset(this, args[0], { offsetType: `bottomRight` });
-    case `getCenter`:
-      return await getOffset(this, args[0], { offsetType: `center` });
-    case `getTopLeft`:
-      return await getOffset(this, args[0], { offsetType: `topLeft` });
-    case `getTopRight`:
-      return await getOffset(this, args[0], { offsetType: `topRight` });
-    case `getRenderObjectDiagnostics`:
-      return await getRenderObjectDiagnostics(this, args[0], args[1]);
-    case `getWidgetDiagnostics`:
-      return await getWidgetDiagnostics(this, args[0], args[1]);
-    case `getSemanticsId`:
-      return await getSemanticsId(this, args[0]);
-    case `waitForAbsent`:
-      return await waitForAbsent(this, args[0], args[1]);
-    case `waitFor`:
-      return await waitFor(this, args[0], args[1]);
-    case `waitForTappable`:
-      return await waitForTappable(this, args[0], args[1]);
-    case `scroll`:
-      return await scroll(this, args[0], args[1]);
-    case `scrollUntilVisible`:
-      return await scrollUntilVisible(this, args[0], args[1]);
-    case `scrollUntilTapable`:
-      return await scrollUntilTapable(this, args[0], args[1]);
-    case `scrollIntoView`:
-      return await scrollIntoView(this, args[0], args[1]);
-    case `setTextEntryEmulation`:
-      return await setTextEntryEmulation(this, args[0]);
-    case `enterText`:
-      return await enterText(this, args[0]);
-    case `requestData`:
-      return await requestData(this, args[0]);
-    case `longTap`:
-      return await longTap(this, args[0], args[1]);
-    case `waitForFirstFrame`:
-      return await waitForCondition(this, { conditionName: `FirstFrameRasterizedCondition`});
-    case `setFrameSync`:
-      return await setFrameSync(this, args[0], args[1]);
-    case `clickElement`:
-      return await clickElement(this, args[0], args[1]);
-    case `dragAndDropWithCommandExtension`:
-      return await dragAndDropWithCommandExtension(this, args[0]);
-    case `getTextWithCommandExtension`:
-      return await getTextWithCommandExtension(this, args[0]);
-    default:
-      throw new Error(`Command not support: "${rawCommand}"`);
-  }
-};
-
-const flutterLaunchApp = async (
-  self: FlutterDriver, appId: string, opts
-) => {
-  const { arguments: args = [], environment: env = {}} = opts;
-  await launchApp(self.internalCaps.udid!, appId, args, env);
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const connectObservatoryWsUrl = async (self: FlutterDriver) => {
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const checkHealth = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_health`)).status;
+// Define types for better type safety
+type CommandHandler = (driver: FlutterDriver, ...args: any[]) => Promise<any>;
+type CommandMap = Record<string, CommandHandler>;
 
-const getVMInfo = async (self: FlutterDriver) =>
-  (await self.executeGetVMCommand());
+interface DragAndDropParams {
+  startX: string;
+  startY: string;
+  endX: string;
+  endY: string;
+  duration: string;
+}
 
-const getRenderTree = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_render_tree`)).tree;
+interface DiagnosticsOptions {
+  subtreeDepth?: number;
+  includeProperties?: boolean;
+}
 
-const getOffset = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  offsetType: {offsetType: string},
-) => await self.executeElementCommand(`get_offset`, elementBase64, offsetType);
 
-const waitForCondition = async (
-  self: FlutterDriver,
-  conditionName: {conditionName: string},
-) => await self.executeElementCommand(`waitForCondition`, ``, conditionName);
 
-const forceGC = async (self: FlutterDriver) => {
-  const response = await self.socket!.call(`_collectAllGarbage`, {
-    isolateId: self.socket!.isolateId,
-  }) as { type: string };
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
+interface LongTapOptions {
+  durationMilliseconds: number;
+  frequency?: number;
+}
 
-const setIsolateId = async (self: FlutterDriver, isolateId: string) => {
-  self.socket!.isolateId = isolateId;
-  return await self.socket!.call(`getIsolate`, {
-    isolateId: `${isolateId}`,
-  });
-};
+interface OffsetOptions {
+  offsetType: 'bottomLeft' | 'bottomRight' | 'center' | 'topLeft' | 'topRight';
+}
 
-const getIsolate = async (
-  self: FlutterDriver, isolateId: string|undefined
-) => await self.executeGetIsolateCommand(isolateId || self.socket!.isolateId);
-
-const clearTimeline = async (self: FlutterDriver) => {
-  // @todo backward compatible, need to cleanup later
-  const call1: Promise<any> = self.socket!.call(`_clearVMTimeline`);
-  const call2: Promise<any> = self.socket!.call(`clearVMTimeline`);
-  const response = await B.any([call1, call2]);
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
-
-const getRenderObjectDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+// Extract command handlers into a separate object for better organization
+const commandHandlers: CommandMap = {
+  launchApp: async (driver, appId: string, opts = {}) => {
+    const { arguments: args = [], environment: env = {} } = opts;
+    await launchApp(driver.internalCaps.udid!, appId, args, env);
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  connectObservatoryWsUrl: async (driver) => {
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  checkHealth: async (driver) => (await driver.executeElementCommand('get_health')).status,
+  getVMInfo: async (driver) => await driver.executeGetVMCommand(),
+  getRenderTree: async (driver) => (await driver.executeElementCommand('get_render_tree')).tree,
+  getOffset: async (driver, elementBase64: string, options: OffsetOptions) => 
+    await driver.executeElementCommand('get_offset', elementBase64, options),
+  waitForCondition: async (driver, conditionName: string) => 
+    await driver.executeElementCommand('waitForCondition', '', { conditionName }),
+  forceGC: async (driver) => {
+    const response = await driver.socket!.call('_collectAllGarbage', {
+      isolateId: driver.socket!.isolateId,
+    }) as { type: string };
+    if (response.type !== 'Success') {
+      throw new Error(`Could not forceGC, response was ${JSON.stringify(response)}`);
+    }
+  },
+  setIsolateId: async (driver, isolateId: string) => {
+    driver.socket!.isolateId = isolateId;
+    return await driver.socket!.call('getIsolate', { isolateId });
+  },
+  getIsolate: async (driver, isolateId?: string) => 
+    await driver.executeGetIsolateCommand(isolateId || driver.socket!.isolateId!),
+  clearTimeline: async (driver) => {
+    const call1 = driver.socket!.call('_clearVMTimeline');
+    const call2 = driver.socket!.call('clearVMTimeline');
+    const response = await B.any([call1, call2]);
+    if (response.type !== 'Success') {
+      throw new Error(`Could not clear timeline, response was ${JSON.stringify(response)}`);
+    }
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `renderObject`,
+  getRenderObjectDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'renderObject',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getWidgetDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+    });
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `widget`,
+  getWidgetDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'widget',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getSemanticsId = async (self: FlutterDriver, elementBase64: string) =>
-  (await self.executeElementCommand(`get_semantics_id`, elementBase64)).id;
-
-const enterText = async (self: FlutterDriver, text: string) =>
-  await self.socket!.executeSocketCommand({ command: `enter_text`, text });
-
-const requestData = async (self: FlutterDriver, message: string) =>
-  await self.socket!.executeSocketCommand({ command: `request_data`, message });
-
-const setFrameSync = async (self, bool: boolean, durationMilliseconds: number) =>
-  await self.socket!.executeSocketCommand({
-    command: `set_frame_sync`,
-    enabled: bool,
-    timeout: durationMilliseconds,
-  });
-
-const setTextEntryEmulation = async (self: FlutterDriver, enabled: boolean) =>
-  await self.socket!.executeSocketCommand({ command: `set_text_entry_emulation`, enabled });
-
-const clickElement = async (self:FlutterDriver, elementBase64: string, opts) => {
-  const {timeout = 1000} = opts;
-  return await self.executeElementCommand(`tap`, elementBase64, {
-        timeout
-  });
+    });
+  },
+  getSemanticsId: async (driver, elementBase64: string) => 
+    (await driver.executeElementCommand('get_semantics_id', elementBase64)).id,
+  waitForAbsent: async (driver, finder: string, timeout?: number) => 
+    await waitForAbsent(driver, finder, timeout),
+  waitFor: async (driver, finder: string, timeout?: number) => 
+    await waitFor(driver, finder, timeout),
+  waitForTappable: async (driver, finder: string, timeout?: number) => 
+    await waitForTappable(driver, finder, timeout),
+  scroll: async (driver, finder: string, opts: any) => 
+    await scroll(driver, finder, opts),
+  scrollUntilVisible: async (driver, finder: string, opts: any) => 
+    await scrollUntilVisible(driver, finder, opts),
+  scrollUntilTapable: async (driver, finder: string, opts: any) => 
+    await scrollUntilTapable(driver, finder, opts),
+  scrollIntoView: async (driver, finder: string, opts: any) => 
+    await scrollIntoView(driver, finder, opts),
+  setTextEntryEmulation: async (driver, enabled: boolean) => 
+    await driver.socket!.executeSocketCommand({ command: 'set_text_entry_emulation', enabled }),
+  enterText: async (driver, text: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'enter_text', text }),
+  requestData: async (driver, message: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'request_data', message }),
+  longTap: async (driver, finder: string, durationOrOptions?: number | LongTapOptions) => 
+  await longTap(driver, finder, durationOrOptions),   
path: driver/lib/commands/execute.ts
user: login: Zubairahmdfaraz
body: Fixed this as per the suggestion
created_at: 2025-05-21T07:13:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,6 +7,7 @@
     "outDir": "build",
     "types": ["node"],
     "checkJs": true
+
path: driver/tsconfig.json
user: login: Zubairahmdfaraz
body: fixed this as per the suggestion
created_at: 2025-05-21T07:14:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,6 +307,38 @@ Please replace them properly with your client.
 | -                                                                                                                                  | :ok: | (Ruby) `driver.execute_script 'flutter:launchApp', 'bundleId', {arguments: ['arg1'], environment: {ENV1: 'env'}}` | Flutter Driver    |
 | dragAndDropWithCommandExtension                                                                                                    | :ok: | (Python) `driver.execute_script('flutter:dragAndDropWithCommandExtension', payload)` | Command Extension |
 
+# Flutter Visibility Assertions for Appium
+
+This module extends the `appium-flutter-driver` with custom visibility-related commands using `driver.execute()`.
+
+## ✅ Supported Commands
+
+| **Command**         | **Status** | **Example Usage**                                                                                                         | **Target** |
+|---------------------|------------|--------------------------------------------------------------------------------------------------------------------------|------------|
+| `assertVisible`     | ✅         | `driver.execute('flutter:assertVisible', { key: 'myKey' })`<br>`driver.execute('flutter:assertVisible', { text: 'Login' })` | Widget     |
+| `assertNotVisible`  | ✅         | `driver.execute('flutter:assertNotVisible', { key: 'hiddenWidget' })`                                                   | Widget     |
+| `assertTappable`    | ✅         | `driver.execute('flutter:assertTappable', { label: 'Submit' })`                                                          | Widget     |
+
+## 🔍 Input Formats
+
+Each assertion supports the following input formats:
+
+- `{ key: 'valueKey' }`
+- `{ text: 'Text on widget' }`
+- `{ label: 'Tooltip text' }`
+
+These map to Flutter finders:
+- `byValueKey`
+- `byText`
+- `byTooltip`
+
+## 📦 Integration
+
+These commands are typically invoked using a client helper method like:
+
+```ts
+await assertVisible(driver, { key: 'submit_button' });
path: README.md
user: login: Zubairahmdfaraz
body: Fixed as per the suggestion
created_at: 2025-05-21T07:24:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+{
path: package.json
user: login: Zubairahmdfaraz
body: I have deleted the package.json file that you have mentioned.
created_at: 2025-05-21T07:44:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,254 +1,197 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import type { FlutterDriver } from '../driver';
 import { reConnectFlutterDriver } from '../sessions/session';
-import { longTap, scroll, scrollIntoView, scrollUntilVisible, scrollUntilTapable } from './execute/scroll';
-import { waitFor, waitForAbsent, waitForTappable } from './execute/wait';
+import {
+  longTap,
+  scroll,
+  scrollIntoView,
+  scrollUntilVisible,
+  scrollUntilTapable
+} from './execute/scroll';
+import {
+  waitFor,
+  waitForAbsent,
+  waitForTappable
+} from './execute/wait';
+import { getText } from './element';
+import { tap, click } from './gesture';
+import {
+  assertVisible,
+  assertNotVisible,
+  assertTappable,
+  type FinderInput
+} from './assertions';
+
 import { launchApp } from './../ios/app';
 import B from 'bluebird';
 
-
 const flutterCommandRegex = /^[\s]*flutter[\s]*:(.+)/;
 
-export const execute = async function(
-  this: FlutterDriver,
-  rawCommand: string,
-  args: any[],
-) {
-  // flutter
-  const matching = rawCommand.match(flutterCommandRegex);
-  if (!matching) {
-    throw new Error(`Command not support: "${rawCommand}"`);
-  }
-
-  const command = matching[1].trim();
-  switch (command) {
-    case `launchApp`:
-      return await flutterLaunchApp(this, args[0], args[1]);
-    case `connectObservatoryWsUrl`:
-      return await connectObservatoryWsUrl(this);
-    case `getVMInfo`:
-      return await getVMInfo(this);
-    case `setIsolateId`:
-      return await setIsolateId(this, args[0]);
-    case `getIsolate`:
-      return await getIsolate(this, args[0]);
-    case `checkHealth`:
-      return await checkHealth(this);
-    case `clearTimeline`:
-      return await clearTimeline(this);
-    case `forceGC`:
-      return await forceGC(this);
-    case `getRenderTree`:
-      return await getRenderTree(this);
-    case `getBottomLeft`:
-      return await getOffset(this, args[0], { offsetType: `bottomLeft` });
-    case `getBottomRight`:
-      return await getOffset(this, args[0], { offsetType: `bottomRight` });
-    case `getCenter`:
-      return await getOffset(this, args[0], { offsetType: `center` });
-    case `getTopLeft`:
-      return await getOffset(this, args[0], { offsetType: `topLeft` });
-    case `getTopRight`:
-      return await getOffset(this, args[0], { offsetType: `topRight` });
-    case `getRenderObjectDiagnostics`:
-      return await getRenderObjectDiagnostics(this, args[0], args[1]);
-    case `getWidgetDiagnostics`:
-      return await getWidgetDiagnostics(this, args[0], args[1]);
-    case `getSemanticsId`:
-      return await getSemanticsId(this, args[0]);
-    case `waitForAbsent`:
-      return await waitForAbsent(this, args[0], args[1]);
-    case `waitFor`:
-      return await waitFor(this, args[0], args[1]);
-    case `waitForTappable`:
-      return await waitForTappable(this, args[0], args[1]);
-    case `scroll`:
-      return await scroll(this, args[0], args[1]);
-    case `scrollUntilVisible`:
-      return await scrollUntilVisible(this, args[0], args[1]);
-    case `scrollUntilTapable`:
-      return await scrollUntilTapable(this, args[0], args[1]);
-    case `scrollIntoView`:
-      return await scrollIntoView(this, args[0], args[1]);
-    case `setTextEntryEmulation`:
-      return await setTextEntryEmulation(this, args[0]);
-    case `enterText`:
-      return await enterText(this, args[0]);
-    case `requestData`:
-      return await requestData(this, args[0]);
-    case `longTap`:
-      return await longTap(this, args[0], args[1]);
-    case `waitForFirstFrame`:
-      return await waitForCondition(this, { conditionName: `FirstFrameRasterizedCondition`});
-    case `setFrameSync`:
-      return await setFrameSync(this, args[0], args[1]);
-    case `clickElement`:
-      return await clickElement(this, args[0], args[1]);
-    case `dragAndDropWithCommandExtension`:
-      return await dragAndDropWithCommandExtension(this, args[0]);
-    case `getTextWithCommandExtension`:
-      return await getTextWithCommandExtension(this, args[0]);
-    default:
-      throw new Error(`Command not support: "${rawCommand}"`);
-  }
-};
-
-const flutterLaunchApp = async (
-  self: FlutterDriver, appId: string, opts
-) => {
-  const { arguments: args = [], environment: env = {}} = opts;
-  await launchApp(self.internalCaps.udid!, appId, args, env);
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const connectObservatoryWsUrl = async (self: FlutterDriver) => {
-  await reConnectFlutterDriver.bind(self)(self.internalCaps);
-};
 
-const checkHealth = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_health`)).status;
+// Define types for better type safety
+type CommandHandler = (driver: FlutterDriver, ...args: any[]) => Promise<any>;
+type CommandMap = Record<string, CommandHandler>;
 
-const getVMInfo = async (self: FlutterDriver) =>
-  (await self.executeGetVMCommand());
+interface DragAndDropParams {
+  startX: string;
+  startY: string;
+  endX: string;
+  endY: string;
+  duration: string;
+}
 
-const getRenderTree = async (self: FlutterDriver) =>
-  (await self.executeElementCommand(`get_render_tree`)).tree;
+interface DiagnosticsOptions {
+  subtreeDepth?: number;
+  includeProperties?: boolean;
+}
 
-const getOffset = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  offsetType: {offsetType: string},
-) => await self.executeElementCommand(`get_offset`, elementBase64, offsetType);
 
-const waitForCondition = async (
-  self: FlutterDriver,
-  conditionName: {conditionName: string},
-) => await self.executeElementCommand(`waitForCondition`, ``, conditionName);
 
-const forceGC = async (self: FlutterDriver) => {
-  const response = await self.socket!.call(`_collectAllGarbage`, {
-    isolateId: self.socket!.isolateId,
-  }) as { type: string };
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
+interface LongTapOptions {
+  durationMilliseconds: number;
+  frequency?: number;
+}
 
-const setIsolateId = async (self: FlutterDriver, isolateId: string) => {
-  self.socket!.isolateId = isolateId;
-  return await self.socket!.call(`getIsolate`, {
-    isolateId: `${isolateId}`,
-  });
-};
+interface OffsetOptions {
+  offsetType: 'bottomLeft' | 'bottomRight' | 'center' | 'topLeft' | 'topRight';
+}
 
-const getIsolate = async (
-  self: FlutterDriver, isolateId: string|undefined
-) => await self.executeGetIsolateCommand(isolateId || self.socket!.isolateId);
-
-const clearTimeline = async (self: FlutterDriver) => {
-  // @todo backward compatible, need to cleanup later
-  const call1: Promise<any> = self.socket!.call(`_clearVMTimeline`);
-  const call2: Promise<any> = self.socket!.call(`clearVMTimeline`);
-  const response = await B.any([call1, call2]);
-  if (response.type !== `Success`) {
-    throw new Error(`Could not forceGC, response was ${response}`);
-  }
-};
-
-const getRenderObjectDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+// Extract command handlers into a separate object for better organization
+const commandHandlers: CommandMap = {
+  launchApp: async (driver, appId: string, opts = {}) => {
+    const { arguments: args = [], environment: env = {} } = opts;
+    await launchApp(driver.internalCaps.udid!, appId, args, env);
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  connectObservatoryWsUrl: async (driver) => {
+    await reConnectFlutterDriver.bind(driver)(driver.internalCaps);
+  },
+  checkHealth: async (driver) => (await driver.executeElementCommand('get_health')).status,
+  getVMInfo: async (driver) => await driver.executeGetVMCommand(),
+  getRenderTree: async (driver) => (await driver.executeElementCommand('get_render_tree')).tree,
+  getOffset: async (driver, elementBase64: string, options: OffsetOptions) => 
+    await driver.executeElementCommand('get_offset', elementBase64, options),
+  waitForCondition: async (driver, conditionName: string) => 
+    await driver.executeElementCommand('waitForCondition', '', { conditionName }),
+  forceGC: async (driver) => {
+    const response = await driver.socket!.call('_collectAllGarbage', {
+      isolateId: driver.socket!.isolateId,
+    }) as { type: string };
+    if (response.type !== 'Success') {
+      throw new Error(`Could not forceGC, response was ${JSON.stringify(response)}`);
+    }
+  },
+  setIsolateId: async (driver, isolateId: string) => {
+    driver.socket!.isolateId = isolateId;
+    return await driver.socket!.call('getIsolate', { isolateId });
+  },
+  getIsolate: async (driver, isolateId?: string) => 
+    await driver.executeGetIsolateCommand(isolateId || driver.socket!.isolateId!),
+  clearTimeline: async (driver) => {
+    const call1 = driver.socket!.call('_clearVMTimeline');
+    const call2 = driver.socket!.call('clearVMTimeline');
+    const response = await B.any([call1, call2]);
+    if (response.type !== 'Success') {
+      throw new Error(`Could not clear timeline, response was ${JSON.stringify(response)}`);
+    }
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `renderObject`,
+  getRenderObjectDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'renderObject',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getWidgetDiagnostics = async (
-  self: FlutterDriver,
-  elementBase64: string,
-  opts: {
-    subtreeDepth: number;
-    includeProperties: boolean;
+    });
   },
-) => {
-  const { subtreeDepth = 0, includeProperties = true } = opts;
-
-  return await self.executeElementCommand(
-    `get_diagnostics_tree`,
-    elementBase64,
-    {
-      diagnosticsType: `widget`,
+  getWidgetDiagnostics: async (driver, elementBase64: string, opts: DiagnosticsOptions = {}) => {
+    const { subtreeDepth = 0, includeProperties = true } = opts;
+    return await driver.executeElementCommand('get_diagnostics_tree', elementBase64, {
+      diagnosticsType: 'widget',
       includeProperties,
       subtreeDepth,
-    },
-  );
-};
-
-const getSemanticsId = async (self: FlutterDriver, elementBase64: string) =>
-  (await self.executeElementCommand(`get_semantics_id`, elementBase64)).id;
-
-const enterText = async (self: FlutterDriver, text: string) =>
-  await self.socket!.executeSocketCommand({ command: `enter_text`, text });
-
-const requestData = async (self: FlutterDriver, message: string) =>
-  await self.socket!.executeSocketCommand({ command: `request_data`, message });
-
-const setFrameSync = async (self, bool: boolean, durationMilliseconds: number) =>
-  await self.socket!.executeSocketCommand({
-    command: `set_frame_sync`,
-    enabled: bool,
-    timeout: durationMilliseconds,
-  });
-
-const setTextEntryEmulation = async (self: FlutterDriver, enabled: boolean) =>
-  await self.socket!.executeSocketCommand({ command: `set_text_entry_emulation`, enabled });
-
-const clickElement = async (self:FlutterDriver, elementBase64: string, opts) => {
-  const {timeout = 1000} = opts;
-  return await self.executeElementCommand(`tap`, elementBase64, {
-        timeout
-  });
+    });
+  },
+  getSemanticsId: async (driver, elementBase64: string) => 
+    (await driver.executeElementCommand('get_semantics_id', elementBase64)).id,
+  waitForAbsent: async (driver, finder: string, timeout?: number) => 
+    await waitForAbsent(driver, finder, timeout),
+  waitFor: async (driver, finder: string, timeout?: number) => 
+    await waitFor(driver, finder, timeout),
+  waitForTappable: async (driver, finder: string, timeout?: number) => 
+    await waitForTappable(driver, finder, timeout),
+  scroll: async (driver, finder: string, opts: any) => 
+    await scroll(driver, finder, opts),
+  scrollUntilVisible: async (driver, finder: string, opts: any) => 
+    await scrollUntilVisible(driver, finder, opts),
+  scrollUntilTapable: async (driver, finder: string, opts: any) => 
+    await scrollUntilTapable(driver, finder, opts),
+  scrollIntoView: async (driver, finder: string, opts: any) => 
+    await scrollIntoView(driver, finder, opts),
+  setTextEntryEmulation: async (driver, enabled: boolean) => 
+    await driver.socket!.executeSocketCommand({ command: 'set_text_entry_emulation', enabled }),
+  enterText: async (driver, text: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'enter_text', text }),
+  requestData: async (driver, message: string) => 
+    await driver.socket!.executeSocketCommand({ command: 'request_data', message }),
+  longTap: async (driver, finder: string, durationOrOptions?: number | LongTapOptions) => 
+  await longTap(driver, finder, durationOrOptions),   
+  waitForFirstFrame: async (driver) => 
+    await driver.executeElementCommand('waitForCondition', '', { conditionName: 'FirstFrameRasterizedCondition' }),
+  setFrameSync: async (driver, enabled: boolean, durationMilliseconds: number) => 
+    await driver.socket!.executeSocketCommand({
+      command: 'set_frame_sync',
+      enabled,
+      timeout: durationMilliseconds,
+    }),
+  clickElement: async (driver, finder: string, opts: { timeout?: number } = {}) => {
+    const { timeout = 1000 } = opts;
+    return await driver.executeElementCommand('tap', finder, { timeout });
+  },
+  dragAndDropWithCommandExtension: async (driver, params: DragAndDropParams) => 
+    await driver.socket!.executeSocketCommand({
+      command: 'dragAndDropWithCommandExtension',
+      ...params,
+    }),
+  assertVisible: async (driver, input: FinderInput, timeout = 5000) =>
+  await assertVisible(driver, input, timeout),
+
+  assertNotVisible: async (driver, input: FinderInput, timeout = 5000) =>
+    await assertNotVisible(driver, input, timeout),
+
+  assertTappable: async (driver, input: FinderInput, timeout = 5000) =>
+    await assertTappable(driver, input, timeout),
+  
+  tap: async (driver, gestures: Record<string, any>[], longPress: boolean = false) =>
+    await tap.call(driver, gestures, longPress),
+
+  click: async (driver, input: FinderInput) => await click.call(driver, input),
+
+  getText: async (driver: FlutterDriver, finder: string) =>
+    await getText.bind(driver)(finder),
path: driver/lib/commands/execute.ts
user: login: Zubairahmdfaraz
body:  I have updated the readme  with the flutter:tap , flutter:click , flutter:getText method usage. I  will also updated the PR description with this addition.
created_at: 2025-05-21T08:01:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,3 +204,13 @@ export const scrollIntoView = async (
 
   return await self.executeElementCommand(`scrollIntoView`, elementBase64, args);
 };
+
+
+export const pageBack = async function (this: FlutterDriver): Promise<void> {
path: driver/lib/commands/execute/scroll.ts
user: login: KazuCocoa
body: Implementing `driver.back` is a proper way like https://github.com/appium/appium-flutter-driver/blob/main/driver/lib/driver.ts#L165-L172 to achieve back in native for all cases.

I'll adjust some implementation after this merge
created_at: 2025-05-23T05:48:19Z
author_association: MEMBER

PR comments:
user: login: Zubairahmdfaraz
created_at: 2025-05-21T08:08:42Z
body: Please let me know if there are any further modifications

author_association: CONTRIBUTOR
user: login: Zubairahmdfaraz
created_at: 2025-05-21T08:45:41Z
body: Added two more commits because ReadMe changes were not being tracked properly by git. Had to do it manually from github.
author_association: CONTRIBUTOR
user: login: KazuCocoa
created_at: 2025-06-07T17:45:56Z
body: Hi @Zubairahmdfaraz, congrats: the Appium project wants to compensate you for this contribution! Please reply to this comment mentioning @jlipps and @KazuCocoa and sharing your [OpenCollective](https://opencollective.com/) account name, so that we can initiate payment! Or let us know if you decline to receive compensation via OpenCollective. Thank you!
author_association: MEMBER
user: login: Zubairahmdfaraz
created_at: 2025-06-08T08:11:26Z
body: Hi @jlipps and @KazuCocoa, thank you so much — I’m truly honored! I’m genuinely happy to see my contribution recognized and valued by the Appium community. I’d be delighted to receive the compensation. My OpenCollective account name is zubairahmdfaraz (https://opencollective.com/zubairahmdfaraz). Looking forward to the next steps!
author_association: CONTRIBUTOR