Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .github/workflows/ci_e2e_tests_pos_android.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: e2e-tests-pos-android

permissions:
contents: read

on:
workflow_dispatch:
push:
branches:
- main
- develop
paths:
- 'dapps/pos-app/**'
pull_request:
paths:
- 'dapps/pos-app/**'

jobs:
e2e-tests:
name: Maestro E2E Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup
uses: ./.github/actions/ci-setup
with:
root-path: dapps/pos-app
package-manager: npm

- name: Install Java 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
architecture: x86_64

- name: Create env file
run: |
if [ -n "${{ vars.POS_DEV_ENV_FILE }}" ]; then
echo "${{ vars.POS_DEV_ENV_FILE }}" > dapps/pos-app/.env 2>/dev/null
fi

- name: Expo Prebuild
run: |
cd dapps/pos-app
npm run prebuild

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-pos-e2e-${{ hashFiles('dapps/pos-app/package.json', 'dapps/pos-app/app.json') }}
restore-keys: |
${{ runner.os }}-gradle-pos-e2e-

- name: Build Release APK
id: build
run: |
cd dapps/pos-app/android
./gradlew assembleRelease

- 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

- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH

- name: Run Maestro E2E Tests
id: maestro
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
arch: x86_64
profile: pixel_6
heap-size: 512M
ram-size: 4096M
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
disable-animations: true
script: |
adb install dapps/pos-app/android/app/build/outputs/apk/release/app-release.apk
$HOME/.maestro/bin/maestro test dapps/pos-app/e2e/ --format junit --output maestro-report.xml
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The maestro command outputs to 'maestro-report.xml' without specifying a directory, which means it will be created in the current working directory. Since the script runs in the emulator context and the working directory is not explicitly set, the output file location may be unpredictable. Consider using an absolute path or explicitly setting the working directory, for example: '$HOME/.maestro/bin/maestro test dapps/pos-app/e2e/ --format junit --output ${{ github.workspace }}/maestro-report.xml'

Suggested change
$HOME/.maestro/bin/maestro test dapps/pos-app/e2e/ --format junit --output maestro-report.xml
$HOME/.maestro/bin/maestro test dapps/pos-app/e2e/ --format junit --output "${{ github.workspace }}/maestro-report.xml"

Copilot uses AI. Check for mistakes.

- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-test-results
path: maestro-report.xml

- name: Send Slack notification
if: always() && !cancelled()
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "POS App E2E Test Report",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "🧪 POS App E2E Test Report" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Branch:*\n`${{ github.ref_name }}`" },
{ "type": "mrkdwn", "text": "*Triggered by:*\n`${{ github.actor }}`" }
]
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Build:*\n`${{ steps.build.outcome == 'success' && '✅ Success' || '❌ Failed' }}`" },
{ "type": "mrkdwn", "text": "*E2E Tests:*\n`${{ steps.maestro.outcome == 'success' && '✅ Passed' || '❌ Failed' }}`" }
]
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Overall Status:*\n`${{ job.status == 'success' && '✅ Success' || '❌ Failed' }}`" }
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View Workflow Run" },
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
]
}
]
}
15 changes: 15 additions & 0 deletions dapps/pos-app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,10 +624,25 @@ const apiKey = await secureStorage.getItem(SECURE_STORAGE_KEYS.MERCHANT_API_KEY)
- Use ESLint and Prettier for consistent formatting
- Prefer functional components with hooks
- Use TypeScript types/interfaces for all props and data structures
- **Minimal comments**: Do not add comments unless absolutely necessary to understand the code. Code should be self-documenting through clear naming and structure. Avoid explanatory comments that describe what the code does - the code itself should be clear enough.
- **No unused variables**: Ensure code changes do not leave unused variables, imports, or functions. ESLint will flag these - fix them before committing.
- **No trailing whitespace**: New code must not have trailing whitespace at the end of lines. Most editors can be configured to remove trailing whitespace on save.
- **Run lint after changing code**: Always run `npm run lint` after making code changes to ensure code quality and catch any formatting or linting issues before committing.
- **Check TypeScript errors**: Always run `npx tsc --noEmit` after making code changes to check for TypeScript errors. Fix any TypeScript errors in files you've modified before committing. Note: Pre-existing TypeScript errors in other files can be ignored if they're unrelated to your changes.

### Security Guidelines

- **Never print secrets in CI logs**: When creating or modifying GitHub Actions workflows, ensure that environment variables, API keys, secrets, or any sensitive data are never printed to CI logs. Use output redirection (`2>/dev/null`), avoid `echo` statements that output secrets, and ensure file creation commands don't expose content. Always verify that sensitive values stored in GitHub secrets/variables are not accidentally logged during workflow execution.

### Testing Guidelines

- **Avoid testing mocked components**: Component tests that mock underlying UI primitives (PressableScale, Pressable, QRCodeSkia, etc.) don't provide real value - they just test that mocks work correctly, not actual component behavior. For meaningful component testing, either:
- Use the real components (may require native setup)
- Focus on testing business logic in hooks/utils instead
- Use E2E tests for UI behavior
- **Focus on business logic**: Unit tests should focus on utilities, stores, services, and hooks that contain actual business logic rather than UI rendering.
- **Use testID for E2E tests**: Always use `testID` props to identify components in E2E tests (Maestro). Prefer testID over text strings as text can change and may be localized. Add testID to interactive components (buttons, inputs, etc.) that need to be targeted by E2E tests.

## Troubleshooting

### Printer Issues
Expand Down
1 change: 1 addition & 0 deletions dapps/pos-app/app/amount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export default function AmountScreen() {
)}
/>
<Button
testID="charge-button"
onPress={handleSubmit(onSubmit)}
disabled={!isValid}
style={[
Expand Down
2 changes: 2 additions & 0 deletions dapps/pos-app/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function HomeScreen() {
return (
<View style={styles.container}>
<Button
testID="new-sale-button"
onPress={handleStartPayment}
style={[
styles.actionButton,
Expand All @@ -50,6 +51,7 @@ export default function HomeScreen() {
<ThemedText fontSize={18}>New sale</ThemedText>
</Button>
<Button
testID="settings-button"
onPress={handleSettingsPress}
style={[
styles.actionButton,
Expand Down
15 changes: 10 additions & 5 deletions dapps/pos-app/app/scan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import * as Clipboard from "expo-clipboard";
import { Image } from "expo-image";
import { router, UnknownOutputParams, useLocalSearchParams } from "expo-router";
import React, { useCallback, useEffect, useState } from "react";
import { StyleSheet, View } from "react-native";
import { Linking, StyleSheet, View } from "react-native";
import { v4 as uuidv4 } from "uuid";

interface ScreenParams extends UnknownOutputParams {
Expand Down Expand Up @@ -65,9 +65,13 @@ export default function ScanScreen() {
resetNavigation("/amount");
};

const handleCopyPaymentUrl = async () => {
await Clipboard.setStringAsync(qrUri);
showSuccessToast("Payment URL copied");
const handleLogoPress = async () => {
try {
await Clipboard.setStringAsync(qrUri);
await Linking.openURL(qrUri);
} catch {
showErrorToast("Failed to open payment URL");
Comment on lines +71 to +73
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function behavior has changed from copying the URL with user feedback (via showSuccessToast) to copying the URL and opening it in a browser. This is a breaking change in user experience: users will now have their browser opened automatically when they tap the QR code, rather than just copying the URL. Consider whether this behavior change is intentional and whether it should be documented in the PR description. If automatic browser opening is the desired behavior, the clipboard copy may be unnecessary since the user didn't explicitly request a copy action.

Suggested change
await Linking.openURL(qrUri);
} catch {
showErrorToast("Failed to open payment URL");
showSuccessToast("Payment URL copied to clipboard");
} catch {
showErrorToast("Failed to copy payment URL");

Copilot uses AI. Check for mistakes.
}
};

useEffect(() => {
Expand Down Expand Up @@ -182,10 +186,11 @@ export default function ScanScreen() {
</ThemedText>
</View>
<QRCode
testID="qr-code"
size={300}
uri={qrUri}
logoBorderRadius={100}
onPress={handleCopyPaymentUrl}
onPress={handleLogoPress}
>
<Image source={assets?.[0]} style={styles.logo} />
</QRCode>
Expand Down
9 changes: 8 additions & 1 deletion dapps/pos-app/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ interface Props {
style?: StyleProp<ViewStyle>;
onPress: () => void;
disabled?: boolean;
testID?: string;
}

export const Button: React.FC<Props> = ({
children,
style,
onPress,
disabled,
testID,
}) => {
return (
<PressableScale style={style} onPress={onPress} enabled={!disabled}>
<PressableScale
testID={testID}
style={style}
onPress={onPress}
enabled={!disabled}
>
{children}
</PressableScale>
);
Expand Down
3 changes: 1 addition & 2 deletions dapps/pos-app/components/numeric-keyboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function NumericKeyboardBase({ onKeyPress, style }: NumericKeyboardProps) {
{row.map((key) => (
<Button
key={key}
testID={key === "." ? "key-dot" : `key-${key}`}
onPress={() => handlePress(key)}
style={[
styles.key,
Expand All @@ -41,7 +42,6 @@ function NumericKeyboardBase({ onKeyPress, style }: NumericKeyboardProps) {
>
{key === "erase" ? (
<Image
testID="key-erase"
source={assets?.[0]}
style={[
styles.backspace,
Expand All @@ -55,7 +55,6 @@ function NumericKeyboardBase({ onKeyPress, style }: NumericKeyboardProps) {
/>
) : (
<ThemedText
testID={`key-${key}`}
style={[styles.keyText, { color: Theme["text-primary"] }]}
>
{key}
Expand Down
83 changes: 83 additions & 0 deletions dapps/pos-app/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# E2E Tests with Maestro

This directory contains end-to-end tests using [Maestro](https://maestro.mobile.dev/).

## Prerequisites

1. Install Maestro CLI:
```bash
curl -Ls "https://get.maestro.mobile.dev" | bash
```

2. Build and install the app on your device/emulator:
```bash
npm run android:build
adb install android/app/build/outputs/apk/release/app-release.apk
```

3. Configure merchant settings in the app before running tests

## Running Tests

### Run all tests
```bash
maestro test e2e/
```

### Run specific test
```bash
maestro test e2e/payment-flow.yaml
```

### Run with Maestro Studio (interactive debugging)
```bash
maestro studio
```

## Test Files

| File | Description |
|------|-------------|
| `payment-flow.yaml` | Basic payment flow: New sale → Enter amount → QR code → Copy URL |

## Adding New Tests

1. Create a new `.yaml` file in this directory
2. Start with `appId: com.reown.pos`
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The appId in the example shows 'com.reown.pos' but the actual appId should be 'com.reown.mobilepos' as specified in line 46. This inconsistency could confuse developers adding new tests.

Suggested change
2. Start with `appId: com.reown.pos`
2. Start with `appId: com.reown.mobilepos`

Copilot uses AI. Check for mistakes.
3. Add test steps using Maestro commands

### Common Maestro Commands

```yaml
# Tap on text
- tapOn: "Button Text"

# Tap on element by testID
- tapOn:
id: "my-test-id"

# Assert element is visible
- assertVisible:
text: "Expected Text"

# Wait for element with timeout
- extendedWaitUntil:
visible:
id: "element-id"
timeout: 10000

# Type text
- inputText: "Hello World"

# Scroll
- scroll

# Take screenshot
- takeScreenshot: "screenshot-name"
```
## Notes
- Tests require the app to have merchant credentials configured
- Payment simulation is not yet implemented (pending backend support)
- Run on a real device or emulator with network access
Loading
Loading