openuispec 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/cli/index.ts +80 -2
- package/cli/init.ts +23 -8
- package/docs/cli.md +42 -7
- package/drift/index.ts +41 -15
- package/mcp-server/index.ts +247 -117
- package/mcp-server/screenshot-android.ts +185 -44
- package/mcp-server/screenshot-ios.ts +242 -30
- package/mcp-server/screenshot.ts +96 -1
- package/package.json +5 -2
- package/prepare/index.ts +16 -0
- package/scripts/take-all-screenshots.ts +507 -0
- package/status/index.ts +2 -2
- package/examples/social-app/.mcp.json +0 -10
- package/examples/social-app/AGENTS.md +0 -124
- package/examples/social-app/CLAUDE.md +0 -124
- package/examples/social-app/backend/.gitkeep +0 -1
- package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +0 -3
- package/examples/social-app/generated/android/social-app/app/build.gradle.kts +0 -94
- package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +0 -26
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +0 -20
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +0 -35
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +0 -13
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +0 -98
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +0 -19
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +0 -68
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +0 -15
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +0 -34
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +0 -390
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +0 -234
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +0 -641
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +0 -113
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +0 -212
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +0 -113
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +0 -137
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +0 -180
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +0 -169
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +0 -85
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +0 -74
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +0 -293
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +0 -116
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +0 -161
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +0 -164
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +0 -95
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +0 -123
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +0 -33
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +0 -41
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +0 -20
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +0 -82
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +0 -60
- package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +0 -9
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -5
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -5
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +0 -91
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +0 -10
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +0 -79
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +0 -79
- package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +0 -23
- package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +0 -34
- package/examples/social-app/generated/android/social-app/build.gradle.kts +0 -7
- package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +0 -50
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +0 -8
- package/examples/social-app/generated/android/social-app/gradle.properties +0 -11
- package/examples/social-app/generated/android/social-app/gradlew +0 -248
- package/examples/social-app/generated/android/social-app/settings.gradle.kts +0 -27
- package/examples/social-app/generated/web/social-app/index.html +0 -12
- package/examples/social-app/generated/web/social-app/package-lock.json +0 -2517
- package/examples/social-app/generated/web/social-app/package.json +0 -27
- package/examples/social-app/generated/web/social-app/src/app/App.tsx +0 -58
- package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +0 -259
- package/examples/social-app/generated/web/social-app/src/components/cards.tsx +0 -317
- package/examples/social-app/generated/web/social-app/src/components/ui.tsx +0 -340
- package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +0 -86
- package/examples/social-app/generated/web/social-app/src/i18n.tsx +0 -59
- package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +0 -85
- package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +0 -70
- package/examples/social-app/generated/web/social-app/src/lib/utils.ts +0 -97
- package/examples/social-app/generated/web/social-app/src/locales/en.json +0 -67
- package/examples/social-app/generated/web/social-app/src/locales/ru.json +0 -67
- package/examples/social-app/generated/web/social-app/src/locales/uz.json +0 -67
- package/examples/social-app/generated/web/social-app/src/main.tsx +0 -16
- package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +0 -90
- package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +0 -86
- package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +0 -57
- package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +0 -103
- package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +0 -52
- package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +0 -41
- package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +0 -115
- package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +0 -57
- package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +0 -76
- package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +0 -96
- package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +0 -79
- package/examples/social-app/generated/web/social-app/src/state/store.ts +0 -592
- package/examples/social-app/generated/web/social-app/src/styles.css +0 -125
- package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +0 -1
- package/examples/social-app/generated/web/social-app/tsconfig.json +0 -22
- package/examples/social-app/generated/web/social-app/tsconfig.node.json +0 -13
- package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +0 -1
- package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +0 -1
- package/examples/social-app/generated/web/social-app/vite.config.d.ts +0 -2
- package/examples/social-app/generated/web/social-app/vite.config.js +0 -6
- package/examples/social-app/generated/web/social-app/vite.config.ts +0 -7
- package/examples/social-app/package.json +0 -13
- package/examples/social-app/take-web-screenshots.ts +0 -97
- package/examples/taskflow/.codex/config.toml +0 -4
- package/examples/taskflow/.mcp.json +0 -10
- package/examples/taskflow/AGENTS.md +0 -124
- package/examples/taskflow/CLAUDE.md +0 -124
- package/examples/taskflow/backend/.gitkeep +0 -1
- package/examples/taskflow/generated/android/TaskFlow/README.md +0 -43
- package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +0 -76
- package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +0 -1
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +0 -21
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +0 -19
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +0 -283
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +0 -106
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +0 -57
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +0 -109
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +0 -112
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +0 -61
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +0 -82
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +0 -111
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +0 -77
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +0 -30
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +0 -86
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +0 -57
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +0 -155
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +0 -4
- package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +0 -5
- package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +0 -12
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/examples/taskflow/generated/android/TaskFlow/gradle.properties +0 -4
- package/examples/taskflow/generated/android/TaskFlow/gradlew +0 -18
- package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +0 -12
- package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +0 -18
- package/examples/taskflow/generated/ios/TaskFlow/README.md +0 -21
- package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +0 -115
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +0 -24
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +0 -150
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +0 -220
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +0 -122
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +0 -21
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +0 -201
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +0 -48
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +0 -59
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +0 -63
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +0 -85
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +0 -219
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +0 -320
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +0 -41
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +0 -26
- package/examples/taskflow/generated/web/TaskFlow/README.md +0 -19
- package/examples/taskflow/generated/web/TaskFlow/index.html +0 -12
- package/examples/taskflow/generated/web/TaskFlow/package-lock.json +0 -1908
- package/examples/taskflow/generated/web/TaskFlow/package.json +0 -24
- package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +0 -58
- package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +0 -55
- package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +0 -82
- package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +0 -191
- package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +0 -41
- package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +0 -131
- package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +0 -25
- package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +0 -39
- package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +0 -111
- package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +0 -13
- package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +0 -111
- package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +0 -82
- package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +0 -132
- package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +0 -105
- package/examples/taskflow/generated/web/TaskFlow/src/store.ts +0 -216
- package/examples/taskflow/generated/web/TaskFlow/src/styles.css +0 -617
- package/examples/taskflow/generated/web/TaskFlow/src/types.ts +0 -64
- package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +0 -78
- package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +0 -21
- package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +0 -6
- package/examples/todo-orbit/.codex/config.toml +0 -4
- package/examples/todo-orbit/.mcp.json +0 -10
- package/examples/todo-orbit/AGENTS.md +0 -124
- package/examples/todo-orbit/CLAUDE.md +0 -124
- package/examples/todo-orbit/backend/.gitkeep +0 -1
- package/examples/todo-orbit/generated/android/Todo Orbit/README.md +0 -14
- package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +0 -58
- package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +0 -1
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +0 -20
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +0 -14
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +0 -345
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +0 -231
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +0 -169
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +0 -8
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +0 -236
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +0 -193
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +0 -102
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +0 -347
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +0 -347
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +0 -59
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +0 -149
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +0 -155
- package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +0 -4
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +0 -4
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +0 -248
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +0 -93
- package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +0 -18
- package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +0 -36
- package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +0 -29
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +0 -119
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +0 -119
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +0 -50
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +0 -204
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +0 -126
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +0 -70
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +0 -126
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +0 -61
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +0 -238
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +0 -94
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +0 -76
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +0 -364
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +0 -324
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +0 -400
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +0 -25
- package/examples/todo-orbit/generated/web/Todo Orbit/index.html +0 -16
- package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +0 -1087
- package/examples/todo-orbit/generated/web/Todo Orbit/package.json +0 -24
- package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +0 -2167
- package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +0 -13
- package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +0 -926
- package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +0 -19
- package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +0 -6
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./screenshot-shared.js";
|
|
18
18
|
|
|
19
19
|
const exec = promisify(execCb);
|
|
20
|
+
const androidScreenshotQueues = new Map<string, Promise<void>>();
|
|
20
21
|
|
|
21
22
|
// ── types ───────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -31,6 +32,21 @@ export interface AndroidScreenshotOptions {
|
|
|
31
32
|
module?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
export interface AndroidBatchCapture {
|
|
36
|
+
screen: string;
|
|
37
|
+
route?: string;
|
|
38
|
+
nav?: string[];
|
|
39
|
+
wait_for?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AndroidScreenshotBatchOptions {
|
|
43
|
+
captures: AndroidBatchCapture[];
|
|
44
|
+
theme?: "light" | "dark";
|
|
45
|
+
output_dir?: string;
|
|
46
|
+
project_dir?: string;
|
|
47
|
+
module?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
// ── constants ───────────────────────────────────────────────────────
|
|
35
51
|
|
|
36
52
|
const ADB_SCREENSHOT_PATH = "/sdcard/openuispec_screenshot.png";
|
|
@@ -282,6 +298,27 @@ export async function installAndLaunch(
|
|
|
282
298
|
}
|
|
283
299
|
}
|
|
284
300
|
|
|
301
|
+
export async function launchInstalledApp(
|
|
302
|
+
adb: string,
|
|
303
|
+
serial: string,
|
|
304
|
+
appInfo: AppInfo,
|
|
305
|
+
route?: string,
|
|
306
|
+
): Promise<void> {
|
|
307
|
+
await adbShell(adb, serial, `am force-stop ${appInfo.applicationId}`);
|
|
308
|
+
// Clear saved nav state so deep links route correctly
|
|
309
|
+
try { await adbShell(adb, serial, `pm clear ${appInfo.applicationId}`); } catch { /* ignore */ }
|
|
310
|
+
if (route) {
|
|
311
|
+
await adbShell(
|
|
312
|
+
adb,
|
|
313
|
+
serial,
|
|
314
|
+
`am start -W -a android.intent.action.VIEW -d '${route}' ` +
|
|
315
|
+
`${appInfo.applicationId}/${appInfo.launchActivity}`,
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
await adbShell(adb, serial, `am start -W -n ${appInfo.applicationId}/${appInfo.launchActivity}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
285
322
|
// ── theme control ───────────────────────────────────────────────────
|
|
286
323
|
|
|
287
324
|
export async function setTheme(adb: string, serial: string, theme: "light" | "dark"): Promise<void> {
|
|
@@ -358,9 +395,12 @@ export async function captureScreenshot(
|
|
|
358
395
|
serial: string,
|
|
359
396
|
localPath: string,
|
|
360
397
|
): Promise<void> {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
398
|
+
try {
|
|
399
|
+
await exec(`${adb} -s ${serial} exec-out screencap -p > "${localPath}"`, { timeout: 60_000, shell: "/bin/bash" });
|
|
400
|
+
} catch (err: any) {
|
|
401
|
+
const output = ((err.stderr ?? "") + "\n" + (err.stdout ?? "")).trim();
|
|
402
|
+
throw new Error(`Android screenshot capture failed${output ? `:\n${output}` : "."}`);
|
|
403
|
+
}
|
|
364
404
|
}
|
|
365
405
|
|
|
366
406
|
// ── wait for app ready ──────────────────────────────────────────────
|
|
@@ -395,6 +435,47 @@ async function waitForAppReady(
|
|
|
395
435
|
await new Promise(r => setTimeout(r, waitMs));
|
|
396
436
|
}
|
|
397
437
|
|
|
438
|
+
async function takeSingleAndroidCapture(
|
|
439
|
+
adb: string,
|
|
440
|
+
serial: string,
|
|
441
|
+
androidDir: string,
|
|
442
|
+
appInfo: AppInfo,
|
|
443
|
+
capture: AndroidBatchCapture,
|
|
444
|
+
theme: "light" | "dark" | undefined,
|
|
445
|
+
defaultOutputDir: string | undefined,
|
|
446
|
+
): Promise<{ screen: string; path: string; data: string }> {
|
|
447
|
+
await launchInstalledApp(adb, serial, appInfo, capture.route);
|
|
448
|
+
await waitForAppReady(adb, serial, appInfo.applicationId, capture.wait_for ?? 3000);
|
|
449
|
+
|
|
450
|
+
if (capture.nav && capture.nav.length > 0) {
|
|
451
|
+
await navigateByTaps(adb, serial, capture.nav);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const themeLabel = theme ?? "default";
|
|
455
|
+
const filename = `${capture.screen}_${themeLabel}.png`;
|
|
456
|
+
const tmpPath = join(androidDir, `.openuispec-screenshot-${capture.screen}.png`);
|
|
457
|
+
await captureScreenshot(adb, serial, tmpPath);
|
|
458
|
+
|
|
459
|
+
let savedPath = filename;
|
|
460
|
+
if (defaultOutputDir) {
|
|
461
|
+
const outDir = resolve(androidDir, defaultOutputDir);
|
|
462
|
+
mkdirSync(outDir, { recursive: true });
|
|
463
|
+
savedPath = join(outDir, filename);
|
|
464
|
+
copyFileSync(tmpPath, savedPath);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const data = readFileSync(tmpPath).toString("base64");
|
|
469
|
+
return {
|
|
470
|
+
screen: capture.screen,
|
|
471
|
+
path: savedPath,
|
|
472
|
+
data,
|
|
473
|
+
};
|
|
474
|
+
} finally {
|
|
475
|
+
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
398
479
|
// ── main entry point ────────────────────────────────────────────────
|
|
399
480
|
|
|
400
481
|
export async function takeAndroidScreenshot(
|
|
@@ -420,61 +501,121 @@ export async function takeAndroidScreenshot(
|
|
|
420
501
|
const adb = findAdb();
|
|
421
502
|
const serial = await getConnectedEmulator(adb);
|
|
422
503
|
|
|
423
|
-
|
|
424
|
-
|
|
504
|
+
const previousRun = androidScreenshotQueues.get(serial) ?? Promise.resolve();
|
|
505
|
+
let releaseQueue: (() => void) | undefined;
|
|
506
|
+
const currentRun = new Promise<void>((resolve) => {
|
|
507
|
+
releaseQueue = resolve;
|
|
508
|
+
});
|
|
509
|
+
androidScreenshotQueues.set(serial, previousRun.then(() => currentRun));
|
|
425
510
|
|
|
426
|
-
|
|
427
|
-
const apkPath = await buildApk(androidDir, appInfo.moduleName);
|
|
511
|
+
await previousRun;
|
|
428
512
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
await
|
|
432
|
-
}
|
|
513
|
+
try {
|
|
514
|
+
// 3. Free emulator storage before build/install
|
|
515
|
+
await cleanEmulatorStorage(adb, serial);
|
|
433
516
|
|
|
434
|
-
|
|
435
|
-
|
|
517
|
+
// 4. Build APK
|
|
518
|
+
const apkPath = await buildApk(androidDir, appInfo.moduleName);
|
|
436
519
|
|
|
437
|
-
|
|
438
|
-
|
|
520
|
+
// 5. Set theme if requested
|
|
521
|
+
if (theme) {
|
|
522
|
+
await setTheme(adb, serial, theme);
|
|
523
|
+
}
|
|
439
524
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
525
|
+
// 6. Install fresh once, then capture
|
|
526
|
+
await installAndLaunch(adb, serial, apkPath, appInfo, route);
|
|
527
|
+
|
|
528
|
+
const snapshot = await takeSingleAndroidCapture(
|
|
529
|
+
adb,
|
|
530
|
+
serial,
|
|
531
|
+
androidDir,
|
|
532
|
+
appInfo,
|
|
533
|
+
{ screen: screen ?? "main", route, nav, wait_for },
|
|
534
|
+
theme,
|
|
535
|
+
output_dir,
|
|
536
|
+
);
|
|
444
537
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
538
|
+
return buildScreenshotResponse([snapshot], (s) => ({
|
|
539
|
+
screen: s.screen,
|
|
540
|
+
path: snapshot.path ?? null,
|
|
541
|
+
emulator: serial,
|
|
542
|
+
theme: theme ?? "default",
|
|
543
|
+
applicationId: appInfo.applicationId,
|
|
544
|
+
}));
|
|
545
|
+
} finally {
|
|
546
|
+
releaseQueue?.();
|
|
547
|
+
if (androidScreenshotQueues.get(serial) === currentRun) {
|
|
548
|
+
androidScreenshotQueues.delete(serial);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
451
552
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
553
|
+
export async function takeAndroidScreenshotBatch(
|
|
554
|
+
projectCwd: string,
|
|
555
|
+
options: AndroidScreenshotBatchOptions,
|
|
556
|
+
): Promise<ScreenshotResult> {
|
|
557
|
+
const { captures, theme, output_dir, project_dir, module } = options;
|
|
558
|
+
if (captures.length === 0) {
|
|
559
|
+
return {
|
|
560
|
+
content: [{ type: "text", text: "No Android captures specified." }],
|
|
561
|
+
isError: true,
|
|
562
|
+
};
|
|
459
563
|
}
|
|
460
564
|
|
|
461
|
-
|
|
565
|
+
const androidDir = findAndroidAppDir(projectCwd, project_dir);
|
|
566
|
+
const appInfo = extractAppInfo(androidDir, module);
|
|
567
|
+
const adb = findAdb();
|
|
568
|
+
const serial = await getConnectedEmulator(adb);
|
|
569
|
+
|
|
570
|
+
const previousRun = androidScreenshotQueues.get(serial) ?? Promise.resolve();
|
|
571
|
+
let releaseQueue: (() => void) | undefined;
|
|
572
|
+
const currentRun = new Promise<void>((resolve) => {
|
|
573
|
+
releaseQueue = resolve;
|
|
574
|
+
});
|
|
575
|
+
androidScreenshotQueues.set(serial, previousRun.then(() => currentRun));
|
|
576
|
+
|
|
577
|
+
await previousRun;
|
|
578
|
+
|
|
462
579
|
try {
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
screen: screenLabel,
|
|
466
|
-
path: savedPath ?? filename,
|
|
467
|
-
data,
|
|
468
|
-
}];
|
|
580
|
+
await cleanEmulatorStorage(adb, serial);
|
|
581
|
+
const apkPath = await buildApk(androidDir, appInfo.moduleName);
|
|
469
582
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
583
|
+
if (theme) {
|
|
584
|
+
await setTheme(adb, serial, theme);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
await installAndLaunch(adb, serial, apkPath, appInfo);
|
|
588
|
+
|
|
589
|
+
// Pre-create output dir once
|
|
590
|
+
if (output_dir) mkdirSync(resolve(androidDir, output_dir), { recursive: true });
|
|
591
|
+
|
|
592
|
+
const snapshots = [];
|
|
593
|
+
for (let index = 0; index < captures.length; index += 1) {
|
|
594
|
+
const capture = captures[index];
|
|
595
|
+
snapshots.push(
|
|
596
|
+
await takeSingleAndroidCapture(
|
|
597
|
+
adb,
|
|
598
|
+
serial,
|
|
599
|
+
androidDir,
|
|
600
|
+
appInfo,
|
|
601
|
+
capture,
|
|
602
|
+
theme,
|
|
603
|
+
output_dir,
|
|
604
|
+
),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return buildScreenshotResponse(snapshots, (snapshot) => ({
|
|
609
|
+
screen: snapshot.screen,
|
|
610
|
+
path: snapshot.path,
|
|
473
611
|
emulator: serial,
|
|
474
|
-
theme:
|
|
612
|
+
theme: theme ?? "default",
|
|
475
613
|
applicationId: appInfo.applicationId,
|
|
476
614
|
}));
|
|
477
615
|
} finally {
|
|
478
|
-
|
|
616
|
+
releaseQueue?.();
|
|
617
|
+
if (androidScreenshotQueues.get(serial) === currentRun) {
|
|
618
|
+
androidScreenshotQueues.delete(serial);
|
|
619
|
+
}
|
|
479
620
|
}
|
|
480
621
|
}
|
|
@@ -233,6 +233,45 @@ async function setAppearance(udid: string, theme: "light" | "dark"): Promise<voi
|
|
|
233
233
|
const UITEST_TARGET = "ScreenshotUITests";
|
|
234
234
|
const UITEST_DIR = ".screenshot-uitest";
|
|
235
235
|
|
|
236
|
+
export function generateUITestTargetYml(
|
|
237
|
+
appInfo: IOSAppInfo,
|
|
238
|
+
sourcePath: string,
|
|
239
|
+
includeProductName = false,
|
|
240
|
+
): string {
|
|
241
|
+
const productLines = includeProductName
|
|
242
|
+
? `\n PRODUCT_NAME: ${UITEST_TARGET}\n PRODUCT_MODULE_NAME: ${UITEST_TARGET}`
|
|
243
|
+
: "";
|
|
244
|
+
return ` ${UITEST_TARGET}:
|
|
245
|
+
type: bundle.ui-testing
|
|
246
|
+
platform: iOS
|
|
247
|
+
deploymentTarget: "${appInfo.deploymentTarget}"
|
|
248
|
+
sources:
|
|
249
|
+
- path: ${sourcePath}
|
|
250
|
+
dependencies:
|
|
251
|
+
- target: ${appInfo.schemeName}
|
|
252
|
+
embed: false
|
|
253
|
+
settings:
|
|
254
|
+
base:${productLines}
|
|
255
|
+
TEST_TARGET_NAME: ${appInfo.schemeName}
|
|
256
|
+
PRODUCT_BUNDLE_IDENTIFIER: ${appInfo.bundleId}.uitests
|
|
257
|
+
GENERATE_INFOPLIST_FILE: YES`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function insertUITestTarget(yml: string, targetYml: string): string {
|
|
261
|
+
if (yml.includes("\nschemes:")) {
|
|
262
|
+
return yml.replace("\nschemes:", `\n${targetYml}\nschemes:`);
|
|
263
|
+
}
|
|
264
|
+
return yml + "\n" + targetYml + "\n";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function ensureInfoPlistFlag(yml: string): string {
|
|
268
|
+
if (yml.includes("GENERATE_INFOPLIST_FILE")) return yml;
|
|
269
|
+
return yml.replace(
|
|
270
|
+
/(PRODUCT_BUNDLE_IDENTIFIER:[^\n]+\n)/,
|
|
271
|
+
"$1 GENERATE_INFOPLIST_FILE: YES\n",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
236
275
|
function generateUITestSwift(
|
|
237
276
|
bundleId: string,
|
|
238
277
|
navSteps: string[],
|
|
@@ -322,40 +361,13 @@ async function runXCUITest(
|
|
|
322
361
|
|
|
323
362
|
if (appInfo.hasXcodegen) {
|
|
324
363
|
originalProjectYml = readFileSync(projectYmlPath, "utf-8");
|
|
364
|
+
let modifiedYml = ensureInfoPlistFlag(originalProjectYml);
|
|
365
|
+
modifiedYml = insertUITestTarget(modifiedYml, generateUITestTargetYml(appInfo, `${UITEST_DIR}/Sources`));
|
|
366
|
+
writeFileSync(projectYmlPath, modifiedYml);
|
|
325
367
|
|
|
326
|
-
// Ensure main target has GENERATE_INFOPLIST_FILE and append UI test target
|
|
327
|
-
let modifiedYml = originalProjectYml;
|
|
328
|
-
if (!modifiedYml.includes("GENERATE_INFOPLIST_FILE")) {
|
|
329
|
-
// Add after the first PRODUCT_BUNDLE_IDENTIFIER line
|
|
330
|
-
modifiedYml = modifiedYml.replace(
|
|
331
|
-
/(PRODUCT_BUNDLE_IDENTIFIER:[^\n]+\n)/,
|
|
332
|
-
"$1 GENERATE_INFOPLIST_FILE: YES\n",
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const uitestConfig = `
|
|
337
|
-
${UITEST_TARGET}:
|
|
338
|
-
type: bundle.ui-testing
|
|
339
|
-
platform: iOS
|
|
340
|
-
deploymentTarget: "${appInfo.deploymentTarget}"
|
|
341
|
-
sources:
|
|
342
|
-
- path: ${UITEST_DIR}/Sources
|
|
343
|
-
dependencies:
|
|
344
|
-
- target: ${appInfo.schemeName}
|
|
345
|
-
embed: false
|
|
346
|
-
settings:
|
|
347
|
-
base:
|
|
348
|
-
TEST_TARGET_NAME: ${appInfo.schemeName}
|
|
349
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${appInfo.bundleId}.uitests
|
|
350
|
-
GENERATE_INFOPLIST_FILE: YES
|
|
351
|
-
`;
|
|
352
|
-
writeFileSync(projectYmlPath, modifiedYml + uitestConfig);
|
|
353
|
-
|
|
354
|
-
// Regenerate Xcode project
|
|
355
368
|
try {
|
|
356
369
|
await exec(`xcodegen generate`, { cwd: iosDir, timeout: 30_000 });
|
|
357
370
|
} catch (err: any) {
|
|
358
|
-
// Restore original project.yml
|
|
359
371
|
writeFileSync(projectYmlPath, originalProjectYml);
|
|
360
372
|
throw new Error(`xcodegen failed: ${((err.stderr ?? "") + (err.stdout ?? "")).slice(-300)}`);
|
|
361
373
|
}
|
|
@@ -539,3 +551,203 @@ export async function takeIOSScreenshot(
|
|
|
539
551
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
540
552
|
}
|
|
541
553
|
}
|
|
554
|
+
|
|
555
|
+
// ── batch types ──────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
export interface IOSBatchCapture {
|
|
558
|
+
screen: string;
|
|
559
|
+
nav?: string[];
|
|
560
|
+
wait_for?: number;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export interface IOSScreenshotBatchOptions {
|
|
564
|
+
captures: IOSBatchCapture[];
|
|
565
|
+
device?: string;
|
|
566
|
+
theme?: "light" | "dark";
|
|
567
|
+
output_dir?: string;
|
|
568
|
+
project_dir?: string;
|
|
569
|
+
scheme?: string;
|
|
570
|
+
bundle_id?: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ── batch screenshot ─────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
export async function takeIOSScreenshotBatch(
|
|
576
|
+
projectCwd: string,
|
|
577
|
+
options: IOSScreenshotBatchOptions,
|
|
578
|
+
): Promise<ScreenshotResult> {
|
|
579
|
+
const { captures, device, theme, output_dir, project_dir, scheme, bundle_id } = options;
|
|
580
|
+
|
|
581
|
+
if (captures.length === 0) {
|
|
582
|
+
return { content: [{ type: "text", text: "No iOS captures specified." }], isError: true };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const iosDir = findIOSAppDir(projectCwd, project_dir);
|
|
586
|
+
const appInfo = extractAppInfo(iosDir, { scheme, bundle_id });
|
|
587
|
+
const sim = findSimulator(device);
|
|
588
|
+
await ensureSimulatorBooted(sim.udid);
|
|
589
|
+
|
|
590
|
+
if (theme) {
|
|
591
|
+
await setAppearance(sim.udid, theme);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const themeLabel = theme ?? "default";
|
|
595
|
+
const snapshots: Array<{ screen: string; path: string; data: string }> = [];
|
|
596
|
+
|
|
597
|
+
// Separate captures: no-nav (simctl screenshot) vs nav (XCUITest batch)
|
|
598
|
+
const noNavCaptures = captures.filter((c) => !c.nav || c.nav.length === 0);
|
|
599
|
+
const navCaptures = captures.filter((c) => c.nav && c.nav.length > 0);
|
|
600
|
+
|
|
601
|
+
// Build + install once for all captures
|
|
602
|
+
const appBundlePath = await buildApp(iosDir, appInfo, sim.udid);
|
|
603
|
+
await installAndLaunch(sim.udid, appBundlePath, appInfo.bundleId);
|
|
604
|
+
|
|
605
|
+
// Pre-create output dir once
|
|
606
|
+
if (output_dir) mkdirSync(resolve(iosDir, output_dir), { recursive: true });
|
|
607
|
+
|
|
608
|
+
// No-nav captures: relaunch, wait, simctl screenshot
|
|
609
|
+
for (const capture of noNavCaptures) {
|
|
610
|
+
// Relaunch without reinstalling
|
|
611
|
+
try { await exec(`xcrun simctl terminate ${sim.udid} ${appInfo.bundleId}`); } catch { /* not running */ }
|
|
612
|
+
await exec(`xcrun simctl launch ${sim.udid} ${appInfo.bundleId}`, { timeout: 30_000 });
|
|
613
|
+
await waitForAppReady(sim.udid, appInfo.bundleId, capture.wait_for ?? 3000);
|
|
614
|
+
|
|
615
|
+
const filename = `${capture.screen}_${themeLabel}.png`;
|
|
616
|
+
const tmpPath = join(iosDir, `.openuispec-screenshot-${capture.screen}.png`);
|
|
617
|
+
await captureScreenshot(sim.udid, tmpPath);
|
|
618
|
+
|
|
619
|
+
if (!existsSync(tmpPath)) continue;
|
|
620
|
+
|
|
621
|
+
let savedPath = filename;
|
|
622
|
+
if (output_dir) {
|
|
623
|
+
savedPath = join(resolve(iosDir, output_dir), filename);
|
|
624
|
+
copyFileSync(tmpPath, savedPath);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
snapshots.push({ screen: capture.screen, path: savedPath, data: readFileSync(tmpPath).toString("base64") });
|
|
628
|
+
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Nav captures: batch into a single XCUITest run
|
|
632
|
+
if (navCaptures.length > 0) {
|
|
633
|
+
const uitestDir = join(iosDir, ".screenshot-uitest");
|
|
634
|
+
const sourcesDir = join(uitestDir, "Sources");
|
|
635
|
+
mkdirSync(sourcesDir, { recursive: true });
|
|
636
|
+
|
|
637
|
+
// Build output paths map
|
|
638
|
+
const outputPaths: Record<string, string> = {};
|
|
639
|
+
for (const capture of navCaptures) {
|
|
640
|
+
const filename = `${capture.screen}_${themeLabel}.png`;
|
|
641
|
+
if (output_dir) {
|
|
642
|
+
const outDir = resolve(iosDir, output_dir);
|
|
643
|
+
mkdirSync(outDir, { recursive: true });
|
|
644
|
+
outputPaths[capture.screen] = join(outDir, filename);
|
|
645
|
+
} else {
|
|
646
|
+
outputPaths[capture.screen] = join(iosDir, `.openuispec-screenshot-${capture.screen}.png`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Generate multi-test Swift file
|
|
651
|
+
const testCases = navCaptures.map((capture, i) => {
|
|
652
|
+
const taps = (capture.nav ?? []).map((step, j) => {
|
|
653
|
+
const escaped = step.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
654
|
+
return `
|
|
655
|
+
let target_${i}_${j} = app.descendants(matching: .any).matching(NSPredicate(format: "label ==[c] %@ OR title ==[c] %@", "${escaped}", "${escaped}")).firstMatch
|
|
656
|
+
if target_${i}_${j}.waitForExistence(timeout: 5) {
|
|
657
|
+
target_${i}_${j}.tap()
|
|
658
|
+
Thread.sleep(forTimeInterval: 0.8)
|
|
659
|
+
}`;
|
|
660
|
+
}).join("\n");
|
|
661
|
+
|
|
662
|
+
const outPath = outputPaths[capture.screen].replace(/"/g, '\\"');
|
|
663
|
+
return `
|
|
664
|
+
func test_${String(i + 1).padStart(2, "0")}_${capture.screen}() {
|
|
665
|
+
let app = XCUIApplication()
|
|
666
|
+
app.launchArguments = ["-AppleLanguages", "(en)"]
|
|
667
|
+
app.launch()
|
|
668
|
+
Thread.sleep(forTimeInterval: ${((capture.wait_for ?? 3000) / 1000).toFixed(1)})
|
|
669
|
+
${taps}
|
|
670
|
+
Thread.sleep(forTimeInterval: 0.5)
|
|
671
|
+
let screenshot = XCUIScreen.main.screenshot()
|
|
672
|
+
try! screenshot.pngRepresentation.write(to: URL(fileURLWithPath: "${outPath}"))
|
|
673
|
+
}`;
|
|
674
|
+
}).join("\n");
|
|
675
|
+
|
|
676
|
+
writeFileSync(join(sourcesDir, "ScreenshotUITest.swift"),
|
|
677
|
+
`import XCTest\n\nfinal class ScreenshotUITest: XCTestCase {\n${testCases}\n}\n`);
|
|
678
|
+
|
|
679
|
+
// Set up xcodegen
|
|
680
|
+
const UITEST_TARGET = "ScreenshotUITests";
|
|
681
|
+
const hasXcodegen = existsSync(join(iosDir, "project.yml"));
|
|
682
|
+
const projectYmlPath = join(iosDir, "project.yml");
|
|
683
|
+
let originalProjectYml: string | null = null;
|
|
684
|
+
const buildDir = join(iosDir, ".build", "screenshot");
|
|
685
|
+
|
|
686
|
+
if (hasXcodegen) {
|
|
687
|
+
originalProjectYml = readFileSync(projectYmlPath, "utf-8");
|
|
688
|
+
let modifiedYml = ensureInfoPlistFlag(originalProjectYml);
|
|
689
|
+
modifiedYml = insertUITestTarget(modifiedYml, generateUITestTargetYml(appInfo, ".screenshot-uitest/Sources", true));
|
|
690
|
+
writeFileSync(projectYmlPath, modifiedYml);
|
|
691
|
+
await exec(`xcodegen generate`, { cwd: iosDir, timeout: 30_000 });
|
|
692
|
+
} else {
|
|
693
|
+
writeFileSync(join(uitestDir, "project.yml"), `name: ${UITEST_TARGET}
|
|
694
|
+
targets:
|
|
695
|
+
${UITEST_TARGET}:
|
|
696
|
+
type: bundle.ui-testing
|
|
697
|
+
platform: iOS
|
|
698
|
+
deploymentTarget: "${appInfo.deploymentTarget}"
|
|
699
|
+
sources:
|
|
700
|
+
- path: Sources
|
|
701
|
+
settings:
|
|
702
|
+
base:
|
|
703
|
+
TEST_TARGET_NAME: ${appInfo.schemeName}
|
|
704
|
+
PRODUCT_BUNDLE_IDENTIFIER: ${appInfo.bundleId}.uitests
|
|
705
|
+
GENERATE_INFOPLIST_FILE: YES
|
|
706
|
+
`);
|
|
707
|
+
await exec(`xcodegen generate`, { cwd: uitestDir, timeout: 30_000 });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const testProjectFlag = hasXcodegen
|
|
711
|
+
? (appInfo.xcodeproj ? `-project "${join(iosDir, appInfo.xcodeproj)}"` : "")
|
|
712
|
+
: `-project "${join(uitestDir, `${UITEST_TARGET}.xcodeproj`)}"`;
|
|
713
|
+
const testCwd = hasXcodegen ? iosDir : uitestDir;
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
await exec(
|
|
717
|
+
`xcodebuild test ${testProjectFlag} -scheme "${UITEST_TARGET}" -destination "id=${sim.udid}" -derivedDataPath "${buildDir}" -only-testing:${UITEST_TARGET}/ScreenshotUITest 2>&1`,
|
|
718
|
+
{ cwd: testCwd, timeout: 300_000 },
|
|
719
|
+
);
|
|
720
|
+
} catch {
|
|
721
|
+
// Tests may "fail" but still produce screenshots
|
|
722
|
+
} finally {
|
|
723
|
+
if (originalProjectYml) {
|
|
724
|
+
writeFileSync(projectYmlPath, originalProjectYml);
|
|
725
|
+
try { await exec(`xcodegen generate`, { cwd: iosDir, timeout: 30_000 }); } catch { /* best effort */ }
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Collect results
|
|
730
|
+
for (const capture of navCaptures) {
|
|
731
|
+
const outPath = outputPaths[capture.screen];
|
|
732
|
+
if (existsSync(outPath)) {
|
|
733
|
+
snapshots.push({
|
|
734
|
+
screen: capture.screen,
|
|
735
|
+
path: output_dir ? outPath : `${capture.screen}_${themeLabel}.png`,
|
|
736
|
+
data: readFileSync(outPath).toString("base64"),
|
|
737
|
+
});
|
|
738
|
+
if (!output_dir) { try { unlinkSync(outPath); } catch { /* ignore */ } }
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (snapshots.length === 0) {
|
|
744
|
+
return { content: [{ type: "text", text: "No screenshots were captured. Check Xcode and Simulator output." }], isError: true };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const content: ScreenshotResult["content"] = [];
|
|
748
|
+
for (const s of snapshots) {
|
|
749
|
+
content.push({ type: "image" as const, data: s.data, mimeType: "image/png" });
|
|
750
|
+
content.push({ type: "text" as const, text: JSON.stringify({ screen: s.screen, path: s.path, simulator: sim.name, theme: themeLabel, bundleId: appInfo.bundleId }, null, 2) });
|
|
751
|
+
}
|
|
752
|
+
return { content };
|
|
753
|
+
}
|