enulator test (#1)
* enulator test * enulator fix * fix adb name * fix path for detox * fix bundeling * fix api * fix path * fix path * path * path * path * path * fix: start Metro before emulator-runner to avoid pipe block * add bare-minimum e2e workflow using release build (no Metro) * fix: use debug instrumentation APK against release app build * fix: add Detox androidTest dep and testInstrumentationRunner, use debug build * fix: add version to detox androidTestImplementation * fix: add Detox local Maven repo to allprojects * fix: remove androidTestImplementation detox, not needed with auto-linking * fix: add androidx.test:runner to provide AndroidJUnitRunner in test APK * fix: add proper Detox test class and dependency with exclusiveContent * fix: correct Detox maven coordinate to com.wix:detox * fix: override Detox minSdk 24 requirement for androidTest * remove viewer.e2e.ts, requires launch args bridge not yet wired up * cleanup: remove E2E from build-android workflow, revert release detox config * update github action * enable ABI splits for smaller per-arch release APKs * fix: move abiCodes into closure scope * fix: only split release builds, keep universal debug APK for E2E
This commit is contained in:
47
.detoxrc.js
Normal file
47
.detoxrc.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/** @type {Detox.DetoxConfig} */
|
||||
module.exports = {
|
||||
testRunner: {
|
||||
args: {
|
||||
$0: 'jest',
|
||||
config: 'e2e/jest.config.js',
|
||||
},
|
||||
jest: {
|
||||
setupTimeout: 120000,
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
'android.debug': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
|
||||
testBinaryPath:
|
||||
'android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk',
|
||||
build:
|
||||
'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug --no-daemon',
|
||||
reversePorts: [8081],
|
||||
},
|
||||
'android.release': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
|
||||
build:
|
||||
'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release --no-daemon',
|
||||
},
|
||||
},
|
||||
devices: {
|
||||
emulator: {
|
||||
type: 'android.emulator',
|
||||
// avdName is set by reactivecircus/android-emulator-runner in CI.
|
||||
// For local runs, create an AVD named below or override via DETOX_AVD_NAME.
|
||||
device: {avdName: process.env.DETOX_AVD_NAME ?? 'Pixel_6_API_34'},
|
||||
},
|
||||
},
|
||||
configurations: {
|
||||
'android.emu.debug': {
|
||||
device: 'emulator',
|
||||
app: 'android.debug',
|
||||
},
|
||||
'android.emu.release': {
|
||||
device: 'emulator',
|
||||
app: 'android.release',
|
||||
},
|
||||
},
|
||||
};
|
||||
24
.github/workflows/build-android.yml
vendored
24
.github/workflows/build-android.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build Android APK
|
||||
name: Build Android
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -27,23 +27,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
# Cache node_modules directly — faster than re-running npm ci on every build
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ hashFiles('package-lock.json') }}
|
||||
@@ -58,7 +57,7 @@ jobs:
|
||||
# automatically invalidates the cache and installs the new versions.
|
||||
- name: Cache Android SDK components
|
||||
id: cache-android-sdk
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /home/runner/android-sdk
|
||||
key: android-sdk-${{ hashFiles('android/build.gradle') }}
|
||||
@@ -73,9 +72,8 @@ jobs:
|
||||
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_HOME \
|
||||
"platforms;android-35" "build-tools;35.0.0" "platform-tools"
|
||||
|
||||
# Gradle cache must be restored before the build, not after
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -102,12 +100,12 @@ jobs:
|
||||
|
||||
- name: Build APK
|
||||
working-directory: android
|
||||
run: |
|
||||
BUILD_TYPE=${{ github.event.inputs.build_type || 'release' }}
|
||||
./gradlew assemble$(echo "${BUILD_TYPE^}") --no-daemon
|
||||
run: ./gradlew assemble$(echo "${BUILD_TYPE^}") --no-daemon
|
||||
env:
|
||||
BUILD_TYPE: ${{ github.event.inputs.build_type || 'release' }}
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: LensApp-${{ github.event.inputs.build_type || 'release' }}-${{ github.run_number }}
|
||||
path: android/app/build/outputs/apk/${{ github.event.inputs.build_type || 'release' }}/*.apk
|
||||
|
||||
120
.github/workflows/e2e-android.yml
vendored
Normal file
120
.github/workflows/e2e-android.yml
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
name: E2E Android (Detox)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: Detox E2E (debug)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
ANDROID_HOME: /home/runner/android-sdk
|
||||
ANDROID_SDK_ROOT: /home/runner/android-sdk
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Cache Android SDK
|
||||
id: cache-sdk
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /home/runner/android-sdk
|
||||
key: android-sdk-${{ hashFiles('android/build.gradle') }}
|
||||
|
||||
- name: Install Android SDK
|
||||
if: steps.cache-sdk.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p $ANDROID_HOME/cmdline-tools
|
||||
curl -sSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o cmdline-tools.zip
|
||||
unzip -q cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools
|
||||
mv $ANDROID_HOME/cmdline-tools/cmdline-tools $ANDROID_HOME/cmdline-tools/latest
|
||||
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_HOME \
|
||||
"platforms;android-35" "build-tools;35.0.0" "platform-tools"
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
android/.gradle
|
||||
key: gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/**/*.gradle') }}
|
||||
restore-keys: gradle-
|
||||
|
||||
- name: chmod gradlew
|
||||
run: chmod +x android/gradlew
|
||||
|
||||
# debuggableVariants=[] in build.gradle means JS is bundled into debug APK too,
|
||||
# so no Metro server is needed at runtime.
|
||||
- name: Build APKs
|
||||
working-directory: android
|
||||
run: ./gradlew assembleDebug assembleAndroidTest --no-daemon
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
# Cache the AVD snapshot to speed up subsequent runs
|
||||
- name: Cache AVD snapshot
|
||||
uses: actions/cache@v5
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: avd-34-x86_64-google_apis
|
||||
|
||||
- name: Create AVD snapshot
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 34
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
profile: pixel_6
|
||||
avd-name: Pixel_6_API_34
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: false
|
||||
script: echo "AVD snapshot ready"
|
||||
|
||||
- name: Run Detox E2E
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 34
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
profile: pixel_6
|
||||
avd-name: Pixel_6_API_34
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: npx detox test --configuration android.emu.debug --headless --record-logs failing
|
||||
|
||||
- name: Upload Detox logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: detox-logs-${{ github.run_number }}
|
||||
path: artifacts/
|
||||
retention-days: 7
|
||||
20
App.tsx
20
App.tsx
@@ -1,4 +1,4 @@
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import React, {useState, useCallback, useEffect} from 'react';
|
||||
import {Alert} from 'react-native';
|
||||
import HomeScreen from './src/screens/HomeScreen';
|
||||
import ScanScreen from './src/screens/ScanScreen';
|
||||
@@ -14,6 +14,24 @@ export default function App() {
|
||||
useDocumentStore();
|
||||
|
||||
const [screen, setScreen] = useState<Screen>('home');
|
||||
|
||||
// Seed a fake document when launched with detoxSeedDocument=1 (E2E only)
|
||||
useEffect(() => {
|
||||
if (__DEV__ || process.env.DETOX_SEED) {
|
||||
const args = (global as any).__detox_launch_args ?? {};
|
||||
if (args.detoxSeedDocument === '1') {
|
||||
createDocument([
|
||||
{
|
||||
id: 'seed-page-1',
|
||||
uri: 'https://via.placeholder.com/600x800.jpg',
|
||||
width: 600,
|
||||
height: 800,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const [pendingPages, setPendingPages] = useState<ScannedPage[]>([]);
|
||||
const [viewingDoc, setViewingDoc] = useState<ScannedDocument | null>(null);
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ react {
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
// Empty list means the JS bundle is embedded in ALL variants including debug.
|
||||
// This is required for E2E tests in CI where no Metro server is running.
|
||||
// For local dev, Metro still takes precedence if the packager is running.
|
||||
debuggableVariants = []
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
@@ -81,6 +81,7 @@ android {
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -102,12 +103,38 @@ android {
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
// Only split release builds — debug keeps a single universal APK
|
||||
// so Detox/E2E can find the expected app-debug.apk path.
|
||||
enable gradle.startParameter.taskNames.any { it.toLowerCase().contains("release") }
|
||||
universalApk false
|
||||
include (*project.property("reactNativeArchitectures").split(","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give each per-ABI APK a distinct versionCode so Play Store can serve them
|
||||
// side-by-side. Multiplier leaves plenty of room for the app's own versionCode.
|
||||
android.applicationVariants.all { variant ->
|
||||
def abiCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
||||
variant.outputs.each { output ->
|
||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
if (abi != null) {
|
||||
output.versionCodeOverride = abiCodes.get(abi) * 1048576 + variant.versionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation("com.facebook.react:flipper-integration")
|
||||
androidTestImplementation("androidx.test:runner:1.6.2")
|
||||
androidTestImplementation("androidx.test:rules:1.6.1")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||
androidTestImplementation("com.wix:detox:20.50.1") { transitive = true }
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
|
||||
6
android/app/src/androidTest/AndroidManifest.xml
Normal file
6
android/app/src/androidTest/AndroidManifest.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.wix.detox" />
|
||||
</manifest>
|
||||
30
android/app/src/androidTest/java/com/lensapp/DetoxTest.java
Normal file
30
android/app/src/androidTest/java/com/lensapp/DetoxTest.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.lensapp;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class DetoxTest {
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> mActivityRule =
|
||||
new ActivityTestRule<>(MainActivity.class, false, false);
|
||||
|
||||
@Test
|
||||
public void runDetoxTests() {
|
||||
DetoxConfig detoxConfig = new DetoxConfig();
|
||||
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
|
||||
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
|
||||
detoxConfig.rnContextLoadTimeoutSec = 180;
|
||||
|
||||
Detox.runTests(mActivityRule, detoxConfig);
|
||||
}
|
||||
}
|
||||
@@ -19,3 +19,21 @@ buildscript {
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
// Route com.wix.detox lookups to Detox's bundled local Maven repo so
|
||||
// they bypass JitPack (which RN's Gradle plugin adds as a fallback and
|
||||
// which returns an HTML error page instead of a valid POM).
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
maven {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
}
|
||||
filter {
|
||||
includeModule "com.wix", "detox"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
e2e/home.e2e.ts
Normal file
28
e2e/home.e2e.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {device, element, by, expect as detoxExpect} from 'detox';
|
||||
|
||||
describe('Home Screen', () => {
|
||||
beforeAll(async () => {
|
||||
await device.launchApp({newInstance: true});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await device.reloadReactNative();
|
||||
});
|
||||
|
||||
it('shows the app title', async () => {
|
||||
await detoxExpect(element(by.text('Lens'))).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows empty state when no documents exist', async () => {
|
||||
await detoxExpect(element(by.id('empty-state'))).toBeVisible();
|
||||
await detoxExpect(element(by.text('No scans yet'))).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows the scan FAB', async () => {
|
||||
await detoxExpect(element(by.id('scan-fab'))).toBeVisible();
|
||||
});
|
||||
|
||||
it('scan FAB label reads Scan', async () => {
|
||||
await detoxExpect(element(by.text('Scan'))).toBeVisible();
|
||||
});
|
||||
});
|
||||
12
e2e/jest.config.js
Normal file
12
e2e/jest.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
rootDir: '..',
|
||||
testMatch: ['<rootDir>/e2e/**/*.e2e.ts'],
|
||||
testTimeout: 120000,
|
||||
maxWorkers: 1,
|
||||
globalSetup: 'detox/runners/jest/globalSetup',
|
||||
globalTeardown: 'detox/runners/jest/globalTeardown',
|
||||
reporters: ['detox/runners/jest/reporter'],
|
||||
testEnvironment: 'detox/runners/jest/testEnvironment',
|
||||
verbose: true,
|
||||
};
|
||||
8
e2e/tsconfig.json
Normal file
8
e2e/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["detox", "jest"]
|
||||
},
|
||||
"include": ["**/*.e2e.ts", "jest.config.js"],
|
||||
"exclude": []
|
||||
}
|
||||
3085
package-lock.json
generated
3085
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"test:e2e:build": "detox build --configuration android.emu.debug",
|
||||
"test:e2e": "detox test --configuration android.emu.debug --headless",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -29,11 +31,14 @@
|
||||
"@react-native/typescript-config": "^0.73.1",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"babel-jest": "^29.6.3",
|
||||
"detox": "^20.50.1",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^29.6.3",
|
||||
"jest-circus": "^30.3.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"prettier": "2.8.8",
|
||||
"react-test-renderer": "18.2.0",
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
export default function DocumentCard({document, onPress, onDelete}: Props) {
|
||||
const thumb = document.pages[0]?.uri;
|
||||
return (
|
||||
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.8}>
|
||||
<TouchableOpacity testID={`document-card-${document.id}`} style={styles.card} onPress={onPress} activeOpacity={0.8}>
|
||||
<View style={styles.thumbContainer}>
|
||||
{thumb ? (
|
||||
<Image source={{uri: thumb}} style={styles.thumb} resizeMode="cover" />
|
||||
@@ -32,7 +32,7 @@ export default function DocumentCard({document, onPress, onDelete}: Props) {
|
||||
{document.pages.length} {document.pages.length === 1 ? 'page' : 'pages'}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.deleteBtn} onPress={onDelete} hitSlop={{top: 8, bottom: 8, left: 8, right: 8}}>
|
||||
<TouchableOpacity testID={`delete-card-${document.id}`} style={styles.deleteBtn} onPress={onDelete} hitSlop={{top: 8, bottom: 8, left: 8, right: 8}}>
|
||||
<Text style={styles.deleteText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -53,13 +53,14 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
|
||||
</View>
|
||||
|
||||
{documents.length === 0 ? (
|
||||
<View style={styles.empty}>
|
||||
<View testID="empty-state" style={styles.empty}>
|
||||
<Text style={styles.emptyIcon}>📄</Text>
|
||||
<Text style={styles.emptyTitle}>No scans yet</Text>
|
||||
<Text style={styles.emptyHint}>Tap the button below to scan a document</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
testID="document-list"
|
||||
data={documents}
|
||||
keyExtractor={d => d.id}
|
||||
renderItem={({item}) => (
|
||||
@@ -73,16 +74,17 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={styles.fab} onPress={onScan} activeOpacity={0.85}>
|
||||
<TouchableOpacity testID="scan-fab" style={styles.fab} onPress={onScan} activeOpacity={0.85}>
|
||||
<Text style={styles.fabIcon}>⊕</Text>
|
||||
<Text style={styles.fabLabel}>Scan</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal visible={!!renameTarget} transparent animationType="fade">
|
||||
<View style={styles.modalBg}>
|
||||
<View style={styles.modalBox}>
|
||||
<View testID="rename-modal" style={styles.modalBox}>
|
||||
<Text style={styles.modalTitle}>Rename Document</Text>
|
||||
<TextInput
|
||||
testID="rename-input"
|
||||
style={styles.modalInput}
|
||||
value={renameText}
|
||||
onChangeText={setRenameText}
|
||||
@@ -94,7 +96,7 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
|
||||
<TouchableOpacity onPress={() => setRenameTarget(null)} style={styles.modalBtn}>
|
||||
<Text style={styles.modalCancel}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={submitRename} style={styles.modalBtn}>
|
||||
<TouchableOpacity testID="rename-confirm" onPress={submitRename} style={styles.modalBtn}>
|
||||
<Text style={styles.modalConfirm}>Rename</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
<StatusBar barStyle="light-content" backgroundColor="#000" />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={onBack} style={styles.headerBtn}>
|
||||
<TouchableOpacity testID="viewer-back" onPress={onBack} style={styles.headerBtn}>
|
||||
<Text style={styles.back}>‹ Back</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerCenter}>
|
||||
@@ -78,6 +78,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
testID="share-pdf-btn"
|
||||
onPress={() => sharePdf()}
|
||||
style={styles.headerBtn}
|
||||
disabled={generating}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"extends": "@react-native/typescript-config/tsconfig.json"
|
||||
"extends": "@react-native/typescript-config/tsconfig.json",
|
||||
"exclude": ["e2e"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user