From c6338596664d4c6fd1042b6bdae02c8e5d9059e4 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 24 Aug 2026 23:05:04 -0700 Subject: [PATCH] feat: convert CommonJS to ESM --- README.md | 2 ++ driver/CHANGELOG.md | 3 ++ driver/lib/commands/assertions.ts | 14 +++------- driver/lib/commands/clipboard.ts | 2 +- driver/lib/commands/context.ts | 4 +-- driver/lib/commands/element.ts | 2 +- driver/lib/commands/execute.ts | 12 ++++---- driver/lib/commands/execute/scroll.ts | 4 +-- driver/lib/commands/execute/wait.ts | 2 +- driver/lib/commands/gesture.ts | 4 +-- driver/lib/commands/screen.ts | 4 +-- driver/lib/driver.ts | 28 +++++++++---------- driver/lib/ios/app.ts | 2 +- driver/lib/sessions/android.ts | 10 +++---- driver/lib/sessions/ios.ts | 12 ++++---- driver/lib/sessions/observatory.ts | 8 +++--- driver/lib/sessions/session.ts | 8 +++--- driver/package.json | 5 ++-- ...ssertions.test.cjs => assertions.test.mjs} | 14 +++++++--- 19 files changed, 73 insertions(+), 67 deletions(-) rename driver/test/{assertions.test.cjs => assertions.test.mjs} (56%) diff --git a/README.md b/README.md index 0eb6f2cd..0260ca80 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ If you'd like to test a release app, which can be released from app store as-is, Appium Flutter Driver version `3.0.0` requires Appium 3. +The driver package is ESM-only. Appium loads it as an extension automatically; programmatic Node.js consumers must use `import` or dynamic `import()` instead of `require()`. + ``` appium driver install --source=npm appium-flutter-driver ``` diff --git a/driver/CHANGELOG.md b/driver/CHANGELOG.md index 4686cb31..54d4b608 100644 --- a/driver/CHANGELOG.md +++ b/driver/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Next +- Convert the driver package from CommonJS to ESM + ## 3.10.1 - Fix loading the ESM-only `appium-flutter-finder` dependency from the CommonJS driver ([#923](https://github.com/appium/appium-flutter-driver/issues/923)) diff --git a/driver/lib/commands/assertions.ts b/driver/lib/commands/assertions.ts index bf578836..1177af97 100644 --- a/driver/lib/commands/assertions.ts +++ b/driver/lib/commands/assertions.ts @@ -1,6 +1,6 @@ -import type {SerializableFinder} from 'appium-flutter-finder'; +import {byValueKey, byText, byTooltip, type SerializableFinder} from 'appium-flutter-finder'; -import type {FlutterDriver} from '../driver'; +import type {FlutterDriver} from '../driver.js'; export type FinderInput = | {key: string} @@ -10,10 +10,6 @@ export type FinderInput = | string | {getRawFinder: () => SerializableFinder}; // FlutterElement-like input -let finderModule: Promise | undefined; - -const loadFinder = () => (finderModule ??= import('appium-flutter-finder')); - // Serialize a finder to base64 const serializeFinder = (finder: SerializableFinder): string => Buffer.from(JSON.stringify(finder)).toString('base64'); @@ -25,7 +21,7 @@ const isFlutterElementLike = (input: any): input is {getRawFinder: () => Seriali input && typeof input === 'object' && typeof input.getRawFinder === 'function'; // Convert FinderInput to base64 string -async function getFinderBase64(input: FinderInput): Promise { +function getFinderBase64(input: FinderInput): string { if (typeof input === 'string') { return input; // already base64 } @@ -38,8 +34,6 @@ async function getFinderBase64(input: FinderInput): Promise { return serializeFinder(input); } - const {byValueKey, byText, byTooltip} = await loadFinder(); - if ('key' in input) { return byValueKey(input.key); } @@ -63,7 +57,7 @@ async function executeAssertion( timeout = 5000, extraArgs: object = {}, ): Promise { - const base64 = await getFinderBase64(input); + const base64 = getFinderBase64(input); try { await driver.executeElementCommand(command, base64, { timeout, diff --git a/driver/lib/commands/clipboard.ts b/driver/lib/commands/clipboard.ts index 5f170213..bb7cf1c6 100644 --- a/driver/lib/commands/clipboard.ts +++ b/driver/lib/commands/clipboard.ts @@ -1,4 +1,4 @@ -import type {FlutterDriver} from '../driver'; +import type {FlutterDriver} from '../driver.js'; /** * Set clipboard content via each native app driver diff --git a/driver/lib/commands/context.ts b/driver/lib/commands/context.ts index d0df76d4..e74185d1 100644 --- a/driver/lib/commands/context.ts +++ b/driver/lib/commands/context.ts @@ -1,5 +1,5 @@ -import type {FlutterDriver} from '../driver'; -import {log} from '../logger'; +import type {FlutterDriver} from '../driver.js'; +import {log} from '../logger.js'; export const FLUTTER_CONTEXT_NAME = `FLUTTER`; export const NATIVE_CONTEXT_NAME = `NATIVE_APP`; diff --git a/driver/lib/commands/element.ts b/driver/lib/commands/element.ts index ba62f382..4e95dfeb 100755 --- a/driver/lib/commands/element.ts +++ b/driver/lib/commands/element.ts @@ -1,4 +1,4 @@ -import type {FlutterDriver} from '../driver'; +import type {FlutterDriver} from '../driver.js'; export const getText = async function (this: FlutterDriver, el: string): Promise { const response = await this.executeElementCommand(`get_text`, el); diff --git a/driver/lib/commands/execute.ts b/driver/lib/commands/execute.ts index 54568257..47a2373f 100644 --- a/driver/lib/commands/execute.ts +++ b/driver/lib/commands/execute.ts @@ -1,12 +1,12 @@ import B from 'bluebird'; /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import type {FlutterDriver} from '../driver'; -import {reConnectFlutterDriver} from '../sessions/session'; -import {launchApp} from './../ios/app'; -import {assertVisible, assertNotVisible, assertTappable, type FinderInput} from './assertions'; -import {longTap, scroll, scrollIntoView, scrollUntilVisible, scrollUntilTapable} from './execute/scroll'; -import {waitFor, waitForAbsent, waitForTappable} from './execute/wait'; +import type {FlutterDriver} from '../driver.js'; +import {reConnectFlutterDriver} from '../sessions/session.js'; +import {launchApp} from './../ios/app.js'; +import {assertVisible, assertNotVisible, assertTappable, type FinderInput} from './assertions.js'; +import {longTap, scroll, scrollIntoView, scrollUntilVisible, scrollUntilTapable} from './execute/scroll.js'; +import {waitFor, waitForAbsent, waitForTappable} from './execute/wait.js'; const flutterCommandRegex = /^[\s]*flutter[\s]*:(.+)/; diff --git a/driver/lib/commands/execute/scroll.ts b/driver/lib/commands/execute/scroll.ts index f18daeb8..2520876b 100644 --- a/driver/lib/commands/execute/scroll.ts +++ b/driver/lib/commands/execute/scroll.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; -import type {FlutterDriver} from '../../driver'; -import {waitFor, waitForTappable} from './wait'; +import type {FlutterDriver} from '../../driver.js'; +import {waitFor, waitForTappable} from './wait.js'; export const scroll = async ( self: FlutterDriver, diff --git a/driver/lib/commands/execute/wait.ts b/driver/lib/commands/execute/wait.ts index 6620dfee..3610d2d3 100644 --- a/driver/lib/commands/execute/wait.ts +++ b/driver/lib/commands/execute/wait.ts @@ -1,4 +1,4 @@ -import type {FlutterDriver} from '../../driver'; +import type {FlutterDriver} from '../../driver.js'; const waitForConstructor = (command: `waitForAbsent` | `waitFor` | `waitForTappable`) => diff --git a/driver/lib/commands/gesture.ts b/driver/lib/commands/gesture.ts index c73e4299..adf8b374 100755 --- a/driver/lib/commands/gesture.ts +++ b/driver/lib/commands/gesture.ts @@ -1,5 +1,5 @@ -import type {FlutterDriver} from '../driver'; -import {longTap as longClick} from './execute/scroll'; +import type {FlutterDriver} from '../driver.js'; +import {longTap as longClick} from './execute/scroll.js'; export const click = async function (this: FlutterDriver, el: string) { const retVal = await this.tapEl(el, false); diff --git a/driver/lib/commands/screen.ts b/driver/lib/commands/screen.ts index ce89c957..00f98ba8 100644 --- a/driver/lib/commands/screen.ts +++ b/driver/lib/commands/screen.ts @@ -1,5 +1,5 @@ -import type {FlutterDriver} from '../driver'; -import type {IsolateSocket} from '../sessions/isolate_socket'; +import type {FlutterDriver} from '../driver.js'; +import type {IsolateSocket} from '../sessions/isolate_socket.js'; export const getScreenshot = async function (this: FlutterDriver) { const response = (await (this.socket as IsolateSocket).call(`_flutter.screenshot`)) as any; diff --git a/driver/lib/driver.ts b/driver/lib/driver.ts index 921c4966..5503ad75 100644 --- a/driver/lib/driver.ts +++ b/driver/lib/driver.ts @@ -10,11 +10,11 @@ import type { } from '@appium/types'; import type {AndroidUiautomator2Driver} from 'appium-uiautomator2-driver'; import {XCUITestDriver} from 'appium-xcuitest-driver'; -import {BaseDriver} from 'appium/driver'; +import {BaseDriver} from 'appium/driver.js'; // @ts-ignore: no 'errors' export module import _ from 'lodash'; -import {getClipboard, setClipboard} from './commands/clipboard'; +import {getClipboard, setClipboard} from './commands/clipboard.js'; import { driverShouldDoProxyCmd, FLUTTER_CONTEXT_NAME, @@ -22,18 +22,18 @@ import { getCurrentContext, NATIVE_CONTEXT_NAME, setContext, -} from './commands/context'; -import {clear, getText, setValue} from './commands/element'; -import {execute} from './commands/execute'; -import {click, longTap, performTouch, tap, tapEl} from './commands/gesture'; -import {getScreenshot} from './commands/screen'; -import {desiredCapConstraints} from './desired-caps'; -import {log as logger} from './logger'; -import {PLATFORM} from './platform'; -import type {IsolateSocket} from './sessions/isolate_socket'; -import type {LogMonitor} from './sessions/log-monitor'; -import {executeElementCommand, executeGetVMCommand, executeGetIsolateCommand} from './sessions/observatory'; -import {createSession, reConnectFlutterDriver} from './sessions/session'; +} from './commands/context.js'; +import {clear, getText, setValue} from './commands/element.js'; +import {execute} from './commands/execute.js'; +import {click, longTap, performTouch, tap, tapEl} from './commands/gesture.js'; +import {getScreenshot} from './commands/screen.js'; +import {desiredCapConstraints} from './desired-caps.js'; +import {log as logger} from './logger.js'; +import {PLATFORM} from './platform.js'; +import type {IsolateSocket} from './sessions/isolate_socket.js'; +import type {LogMonitor} from './sessions/log-monitor.js'; +import {executeElementCommand, executeGetVMCommand, executeGetIsolateCommand} from './sessions/observatory.js'; +import {createSession, reConnectFlutterDriver} from './sessions/session.js'; type FluttertDriverConstraints = typeof desiredCapConstraints; // Need to not proxy in WebView context diff --git a/driver/lib/ios/app.ts b/driver/lib/ios/app.ts index 87dcce26..fa5a90d7 100644 --- a/driver/lib/ios/app.ts +++ b/driver/lib/ios/app.ts @@ -1,6 +1,6 @@ import {services, INSTRUMENT_CHANNEL} from 'appium-ios-device'; -import {log} from './../logger'; +import {log} from './../logger.js'; /** * Launch the given bundle id via instrument service. diff --git a/driver/lib/sessions/android.ts b/driver/lib/sessions/android.ts index f3542ed1..13de68b3 100644 --- a/driver/lib/sessions/android.ts +++ b/driver/lib/sessions/android.ts @@ -1,11 +1,11 @@ import type {InitialOpts, StringRecord} from '@appium/types'; import type {AndroidUiautomator2Driver} from 'appium-uiautomator2-driver'; -import type {FlutterDriver} from '../driver'; -import type {IsolateSocket} from './isolate_socket'; -import {LogMonitor} from './log-monitor'; -import type {LogEntry} from './log-monitor'; -import {connectSocket, extractObservatoryUrl, OBSERVATORY_URL_PATTERN} from './observatory'; +import type {FlutterDriver} from '../driver.js'; +import type {IsolateSocket} from './isolate_socket.js'; +import {LogMonitor} from './log-monitor.js'; +import type {LogEntry} from './log-monitor.js'; +import {connectSocket, extractObservatoryUrl, OBSERVATORY_URL_PATTERN} from './observatory.js'; const VM_SERVICE_PORT_EXTRA = `vm-service-port`; const DISABLE_SERVICE_AUTH_CODES_EXTRA = `disable-service-auth-codes`; diff --git a/driver/lib/sessions/ios.ts b/driver/lib/sessions/ios.ts index 35ad34b3..feedb04c 100644 --- a/driver/lib/sessions/ios.ts +++ b/driver/lib/sessions/ios.ts @@ -2,15 +2,15 @@ import net from 'node:net'; import {utilities} from 'appium-ios-device'; import {XCUITestDriver} from 'appium-xcuitest-driver'; -import type {XCUITestDriverOpts} from 'appium-xcuitest-driver/build/lib/driver'; +import type {XCUITestDriverOpts} from 'appium-xcuitest-driver/build/lib/driver.js'; import B from 'bluebird'; import {checkPortStatus} from 'portscanner'; -import type {FlutterDriver} from '../driver'; -import type {IsolateSocket} from './isolate_socket'; -import {LogMonitor} from './log-monitor'; -import type {LogEntry} from './log-monitor'; -import {connectSocket, extractObservatoryUrl, OBSERVATORY_URL_PATTERN} from './observatory'; +import type {FlutterDriver} from '../driver.js'; +import type {IsolateSocket} from './isolate_socket.js'; +import {LogMonitor} from './log-monitor.js'; +import type {LogEntry} from './log-monitor.js'; +import {connectSocket, extractObservatoryUrl, OBSERVATORY_URL_PATTERN} from './observatory.js'; const LOCALHOST = `127.0.0.1`; diff --git a/driver/lib/sessions/observatory.ts b/driver/lib/sessions/observatory.ts index 440909ab..a7552d68 100644 --- a/driver/lib/sessions/observatory.ts +++ b/driver/lib/sessions/observatory.ts @@ -3,10 +3,10 @@ import {URL} from 'node:url'; import {retryInterval} from 'asyncbox'; import _ from 'lodash'; -import type {FlutterDriver} from '../driver'; -import {decode} from './base64url'; -import {IsolateSocket} from './isolate_socket'; -import type {LogEntry} from './log-monitor'; +import type {FlutterDriver} from '../driver.js'; +import {decode} from './base64url.js'; +import {IsolateSocket} from './isolate_socket.js'; +import type {LogEntry} from './log-monitor.js'; const truncateLength = 500; // https://github.com/flutter/flutter/blob/f90b019c68edf4541a4c8273865a2b40c2c01eb3/dev/devicelab/lib/framework/runner.dart#L183 diff --git a/driver/lib/sessions/session.ts b/driver/lib/sessions/session.ts index eb7fe5bc..f153bb9c 100644 --- a/driver/lib/sessions/session.ts +++ b/driver/lib/sessions/session.ts @@ -2,10 +2,10 @@ import type {AndroidUiautomator2Driver} from 'appium-uiautomator2-driver'; import type {XCUITestDriver} from 'appium-xcuitest-driver'; import _ from 'lodash'; -import type {FlutterDriver} from '../driver'; -import {PLATFORM} from '../platform'; -import {startAndroidSession, connectAndroidSession} from './android'; -import {startIOSSession, connectIOSSession} from './ios'; +import type {FlutterDriver} from '../driver.js'; +import {PLATFORM} from '../platform.js'; +import {startAndroidSession, connectAndroidSession} from './android.js'; +import {startIOSSession, connectIOSSession} from './ios.js'; export const reConnectFlutterDriver = async function (this: FlutterDriver, caps: Record) { // setup proxies - if platformName is not empty, make it less case sensitive diff --git a/driver/package.json b/driver/package.json index 771c4a4c..3bc08851 100644 --- a/driver/package.json +++ b/driver/package.json @@ -24,8 +24,9 @@ "npm-shrinkwrap.json", "scripts" ], + "type": "module", "main": "./build/lib/driver.js", - "types": "./build/lib/index.d.ts", + "types": "./build/lib/driver.d.ts", "scripts": { "build": "tsc -b", "dev": "npm run build -- --watch", @@ -36,7 +37,7 @@ "format:check": "oxfmt -c oxfmt.config.mjs --check .", "prepublishOnly": "cp ../README.md ../LICENSE ./", "prepare": "npm run clean && npm run build", - "test": "npm run build && node --test test/*.test.cjs", + "test": "npm run build && node --test test/*.test.mjs", "clean-dependency": "rm -rf node_modules && rm -f package-lock.json" }, "dependencies": { diff --git a/driver/test/assertions.test.cjs b/driver/test/assertions.test.mjs similarity index 56% rename from driver/test/assertions.test.cjs rename to driver/test/assertions.test.mjs index cf051e21..58fc945e 100644 --- a/driver/test/assertions.test.cjs +++ b/driver/test/assertions.test.mjs @@ -1,9 +1,9 @@ -const assert = require('node:assert/strict'); -const {it} = require('node:test'); +import assert from 'node:assert/strict'; +import {it} from 'node:test'; -const {assertVisible} = require('../build/lib/commands/assertions.js'); +import {assertVisible} from '../build/lib/commands/assertions.js'; -it('loads the ESM finder from the CommonJS driver', async () => { +it('loads the ESM finder from the ESM driver', async () => { let command; let finder; let options; @@ -23,3 +23,9 @@ it('loads the ESM finder from the CommonJS driver', async () => { }); assert.deepEqual(options, {timeout: 5000, visible: true}); }); + +it('loads the driver entry point as ESM', async () => { + const {FlutterDriver} = await import('../build/lib/driver.js'); + + assert.equal(typeof FlutterDriver, 'function'); +});