Skip to content

Commit 36f6211

Browse files
author
GitLab CI
committed
feat: export recorded tests as Playwright/Cypress/Selenium/XCUITest/Espresso
1 parent 56c0a58 commit 36f6211

File tree

1 file changed

+232
-4
lines changed

1 file changed

+232
-4
lines changed

lib/src/cli/server.dart

Lines changed: 232 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)