@@ -160,6 +160,10 @@ class FlutterMcpServer {
160160 String ? _autoConnectUrl;
161161 int ? _autoConnectCdpPort;
162162
163+ // Last known connection info for auto-reconnect
164+ String ? _lastConnectionUri;
165+ int ? _lastConnectionPort;
166+
163167 // Legacy single client support (for backward compatibility)
164168 AppDriver ? get _client => _activeSessionId != null
165169 ? _clients[_activeSessionId]
@@ -6025,17 +6029,241 @@ if (mounted) {
60256029 /// Export recorded steps as Playwright test
60266030 String _exportPlaywright () {
60276031 final buf = StringBuffer ();
6028- buf.writeln ("import { test, expect } from '@playwright/test';" );
6032+ buf.writeln ("const { test, expect } = require( '@playwright/test') ;" );
60296033 buf.writeln ("" );
6030- buf.writeln ("test('recorded flow ', async ({ page }) => {" );
6034+ buf.writeln ("test('recorded test ', async ({ page }) => {" );
60316035 for (final step in _recordedSteps) {
6032- final tool = step['tool' ];
6036+ final tool = step['tool' ] as String ;
60336037 final params = step['params' ] as Map <String , dynamic >? ?? {};
6034- buf.writeln (" // $tool : ${jsonEncode (params )}" );
6038+ final key = params['key' ] as String ? ;
6039+ final text = params['text' ] as String ? ;
6040+ final selector = key != null ? '[data-testid="$key "]' : null ;
6041+ switch (tool) {
6042+ case 'tap' :
6043+ if (selector != null ) {
6044+ buf.writeln (" await page.click('$selector ');" );
6045+ } else if (text != null ) {
6046+ buf.writeln (" await page.click('text=$text ');" );
6047+ }
6048+ break ;
6049+ case 'enter_text' :
6050+ final value = params['value' ] as String ? ?? params['text' ] as String ? ?? '' ;
6051+ if (selector != null ) {
6052+ buf.writeln (" await page.fill('$selector ', '${_escapeJs (value )}');" );
6053+ }
6054+ break ;
6055+ case 'swipe' :
6056+ buf.writeln (" // swipe: ${jsonEncode (params )}" );
6057+ break ;
6058+ case 'screenshot' :
6059+ buf.writeln (" await page.screenshot({ path: 'screenshot.png' });" );
6060+ break ;
6061+ case 'scroll' :
6062+ final dx = params['dx' ] ?? 0 ;
6063+ final dy = params['dy' ] ?? 0 ;
6064+ buf.writeln (" await page.mouse.wheel($dx , $dy );" );
6065+ break ;
6066+ default :
6067+ buf.writeln (" // $tool : ${jsonEncode (params )}" );
6068+ }
60356069 }
60366070 buf.writeln ("});" );
60376071 return buf.toString ();
60386072 }
6073+
6074+ /// Export recorded steps as Cypress test
6075+ String _exportCypress () {
6076+ final buf = StringBuffer ();
6077+ buf.writeln ("describe('recorded test', () => {" );
6078+ buf.writeln (" it('should complete flow', () => {" );
6079+ for (final step in _recordedSteps) {
6080+ final tool = step['tool' ] as String ;
6081+ final params = step['params' ] as Map <String , dynamic >? ?? {};
6082+ final key = params['key' ] as String ? ;
6083+ final text = params['text' ] as String ? ;
6084+ final selector = key != null ? '[data-testid="$key "]' : null ;
6085+ switch (tool) {
6086+ case 'tap' :
6087+ if (selector != null ) {
6088+ buf.writeln (" cy.get('$selector ').click();" );
6089+ } else if (text != null ) {
6090+ buf.writeln (" cy.contains('$text ').click();" );
6091+ }
6092+ break ;
6093+ case 'enter_text' :
6094+ final value = params['value' ] as String ? ?? params['text' ] as String ? ?? '' ;
6095+ if (selector != null ) {
6096+ buf.writeln (" cy.get('$selector ').type('${_escapeJs (value )}');" );
6097+ }
6098+ break ;
6099+ case 'swipe' :
6100+ buf.writeln (" // swipe: ${jsonEncode (params )}" );
6101+ break ;
6102+ case 'screenshot' :
6103+ buf.writeln (" cy.screenshot();" );
6104+ break ;
6105+ case 'scroll' :
6106+ final dy = params['dy' ] ?? 0 ;
6107+ buf.writeln (" cy.scrollTo(0, $dy );" );
6108+ break ;
6109+ default :
6110+ buf.writeln (" // $tool : ${jsonEncode (params )}" );
6111+ }
6112+ }
6113+ buf.writeln (" });" );
6114+ buf.writeln ("});" );
6115+ return buf.toString ();
6116+ }
6117+
6118+ /// Export recorded steps as Selenium (Python) test
6119+ String _exportSelenium () {
6120+ final buf = StringBuffer ();
6121+ buf.writeln ("from selenium import webdriver" );
6122+ buf.writeln ("from selenium.webdriver.common.by import By" );
6123+ buf.writeln ("from selenium.webdriver.common.keys import Keys" );
6124+ buf.writeln ("" );
6125+ buf.writeln ("driver = webdriver.Chrome()" );
6126+ buf.writeln ("" );
6127+ buf.writeln ("try:" );
6128+ for (final step in _recordedSteps) {
6129+ final tool = step['tool' ] as String ;
6130+ final params = step['params' ] as Map <String , dynamic >? ?? {};
6131+ final key = params['key' ] as String ? ;
6132+ final text = params['text' ] as String ? ;
6133+ final selector = key != null ? '[data-testid="$key "]' : null ;
6134+ switch (tool) {
6135+ case 'tap' :
6136+ if (selector != null ) {
6137+ buf.writeln (" driver.find_element(By.CSS_SELECTOR, '$selector ').click()" );
6138+ } else if (text != null ) {
6139+ buf.writeln (" driver.find_element(By.XPATH, '//*[text()=\" ${_escapePy (text )}\" ]').click()" );
6140+ }
6141+ break ;
6142+ case 'enter_text' :
6143+ final value = params['value' ] as String ? ?? params['text' ] as String ? ?? '' ;
6144+ if (selector != null ) {
6145+ buf.writeln (" el = driver.find_element(By.CSS_SELECTOR, '$selector ')" );
6146+ buf.writeln (" el.clear()" );
6147+ buf.writeln (" el.send_keys('${_escapePy (value )}')" );
6148+ }
6149+ break ;
6150+ case 'swipe' :
6151+ buf.writeln (" # swipe: ${jsonEncode (params )}" );
6152+ break ;
6153+ case 'screenshot' :
6154+ buf.writeln (" driver.save_screenshot('screenshot.png')" );
6155+ break ;
6156+ case 'scroll' :
6157+ final dy = params['dy' ] ?? 0 ;
6158+ buf.writeln (" driver.execute_script('window.scrollBy(0, $dy )')" );
6159+ break ;
6160+ default :
6161+ buf.writeln (" # $tool : ${jsonEncode (params )}" );
6162+ }
6163+ }
6164+ buf.writeln ("finally:" );
6165+ buf.writeln (" driver.quit()" );
6166+ return buf.toString ();
6167+ }
6168+
6169+ /// Export recorded steps as XCUITest (Swift)
6170+ String _exportXCUITest () {
6171+ final buf = StringBuffer ();
6172+ buf.writeln ("import XCTest" );
6173+ buf.writeln ("" );
6174+ buf.writeln ("class RecordedTest: XCTestCase {" );
6175+ buf.writeln (" func testRecordedFlow() {" );
6176+ buf.writeln (" let app = XCUIApplication()" );
6177+ buf.writeln (" app.launch()" );
6178+ buf.writeln ("" );
6179+ for (final step in _recordedSteps) {
6180+ final tool = step['tool' ] as String ;
6181+ final params = step['params' ] as Map <String , dynamic >? ?? {};
6182+ final key = params['key' ] as String ? ;
6183+ final text = params['text' ] as String ? ;
6184+ final identifier = key ?? text ?? 'unknown' ;
6185+ switch (tool) {
6186+ case 'tap' :
6187+ buf.writeln (' app.buttons["$identifier "].tap()' );
6188+ break ;
6189+ case 'enter_text' :
6190+ final value = params['value' ] as String ? ?? params['text' ] as String ? ?? '' ;
6191+ buf.writeln (' let ${_swiftVar (identifier )}Field = app.textFields["$identifier "]' );
6192+ buf.writeln (' ${_swiftVar (identifier )}Field.tap()' );
6193+ buf.writeln (' ${_swiftVar (identifier )}Field.typeText("${_escapeSwift (value )}")' );
6194+ break ;
6195+ case 'swipe' :
6196+ final direction = params['direction' ] as String ? ?? 'up' ;
6197+ buf.writeln (' app.swipe${direction [0 ].toUpperCase ()}${direction .substring (1 )}()' );
6198+ break ;
6199+ case 'screenshot' :
6200+ buf.writeln (' let screenshot = XCUIScreen.main.screenshot()' );
6201+ buf.writeln (' let attachment = XCTAttachment(screenshot: screenshot)' );
6202+ buf.writeln (' add(attachment)' );
6203+ break ;
6204+ default :
6205+ buf.writeln (' // $tool : ${jsonEncode (params )}' );
6206+ }
6207+ }
6208+ buf.writeln (" }" );
6209+ buf.writeln ("}" );
6210+ return buf.toString ();
6211+ }
6212+
6213+ /// Export recorded steps as Espresso (Kotlin)
6214+ String _exportEspresso () {
6215+ final buf = StringBuffer ();
6216+ buf.writeln ("import androidx.test.ext.junit.runners.AndroidJUnit4" );
6217+ buf.writeln ("import androidx.test.espresso.Espresso.onView" );
6218+ buf.writeln ("import androidx.test.espresso.action.ViewActions.*" );
6219+ buf.writeln ("import androidx.test.espresso.matcher.ViewMatchers.*" );
6220+ buf.writeln ("import org.junit.Test" );
6221+ buf.writeln ("import org.junit.runner.RunWith" );
6222+ buf.writeln ("" );
6223+ buf.writeln ("@RunWith(AndroidJUnit4::class)" );
6224+ buf.writeln ("class RecordedTest {" );
6225+ buf.writeln (" @Test" );
6226+ buf.writeln (" fun testRecordedFlow() {" );
6227+ for (final step in _recordedSteps) {
6228+ final tool = step['tool' ] as String ;
6229+ final params = step['params' ] as Map <String , dynamic >? ?? {};
6230+ final key = params['key' ] as String ? ;
6231+ final text = params['text' ] as String ? ;
6232+ switch (tool) {
6233+ case 'tap' :
6234+ if (key != null ) {
6235+ buf.writeln (' onView(withContentDescription("$key ")).perform(click())' );
6236+ } else if (text != null ) {
6237+ buf.writeln (' onView(withText("$text ")).perform(click())' );
6238+ }
6239+ break ;
6240+ case 'enter_text' :
6241+ final value = params['value' ] as String ? ?? params['text' ] as String ? ?? '' ;
6242+ if (key != null ) {
6243+ buf.writeln (' onView(withContentDescription("$key ")).perform(replaceText("${_escapeKotlin (value )}"))' );
6244+ }
6245+ break ;
6246+ case 'swipe' :
6247+ final direction = params['direction' ] as String ? ?? 'up' ;
6248+ buf.writeln (' onView(withId(android.R.id.content)).perform(swipe${direction [0 ].toUpperCase ()}${direction .substring (1 )}())' );
6249+ break ;
6250+ case 'screenshot' :
6251+ buf.writeln (' // Take screenshot via UiAutomator or test rule' );
6252+ break ;
6253+ default :
6254+ buf.writeln (' // $tool : ${jsonEncode (params )}' );
6255+ }
6256+ }
6257+ buf.writeln (" }" );
6258+ buf.writeln ("}" );
6259+ return buf.toString ();
6260+ }
6261+
6262+ String _escapeJs (String s) => s.replaceAll ("\\ " , "\\\\ " ).replaceAll ("'" , "\\ '" );
6263+ String _escapePy (String s) => s.replaceAll ("\\ " , "\\\\ " ).replaceAll ("'" , "\\ '" );
6264+ String _escapeSwift (String s) => s.replaceAll ("\\ " , "\\\\ " ).replaceAll ('"' , '\\ "' );
6265+ String _escapeKotlin (String s) => s.replaceAll ("\\ " , "\\\\ " ).replaceAll ('"' , '\\ "' );
6266+ String _swiftVar (String s) => s.replaceAll (RegExp (r'[^a-zA-Z0-9]' ), '' ).toLowerCase ();
60396267}
60406268
60416269// ==================== Lock Management ====================
0 commit comments