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
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ Notes:
- `BACKEND=local` uses local Electrum (default).
- `BACKEND=regtest` sets network Electrum against regtest.

### Test fixtures (images for profile avatar, etc.)

Images in `test/fixtures/` can be pushed to a running Android emulator (`/sdcard/Pictures/`) and iOS Simulator (Photos) via:

```bash
./scripts/push-fixture-media-to-devices.sh
```

See `docs/pubky-profile-manual-e2e.md` (preconditions).

## Running Tests

**Important:** The `BACKEND` env var controls which infrastructure the tests use for deposits/mining:
Expand Down
218 changes: 218 additions & 0 deletions docs/pubky-profile-manual-e2e.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions scripts/adb-reverse.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash

for d in $(adb devices | awk 'NR>1 && $2=="device" {print $1}'); do
echo "Setting reverse ports for $d"

adb -s "$d" reverse tcp:60001 tcp:60001
adb -s "$d" reverse tcp:9735 tcp:9735
adb -s "$d" reverse tcp:30001 tcp:30001
adb -s "$d" reverse tcp:6288 tcp:6288
done

echo "Done."
135 changes: 135 additions & 0 deletions scripts/push-fixture-media-to-devices.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# Push image fixtures into Android emulator storage and iOS Simulator Photos so tests
# can pick them (e.g. profile avatar during Pubky profile creation).
#
# Android: copies to /sdcard/Pictures/ and triggers MEDIA_SCANNER_SCAN_FILE.
# iOS: xcrun simctl addmedia (Photos library on the target simulator).
#
# Usage (from bitkit-e2e-tests repo root):
# ./scripts/push-fixture-media-to-devices.sh
# ./scripts/push-fixture-media-to-devices.sh ./test/fixtures/bob.jpg ./test/fixtures/alice.png
#
# Environment:
# ANDROID_SERIAL — if set, passed to adb -s (when multiple devices/emulators).
# SIMCTL_DEVICE — iOS device name or UDID for simctl (default: booted).
# SKIP_ANDROID=1 / SKIP_IOS=1 — skip one platform.
#
# Requirements: adb (Android), booted emulator; Xcode simctl (iOS), booted simulator.
set -euo pipefail

E2E_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FIXTURES_DIR="${FIXTURES_DIR:-$E2E_ROOT/test/fixtures}"

adb_cmd() {
if [[ -n "${ANDROID_SERIAL:-}" ]]; then
adb -s "$ANDROID_SERIAL" "$@"
else
adb "$@"
fi
}

push_android() {
local files=("$@")
if ((${#files[@]} == 0)); then
echo "push-fixture-media: no image files for Android (skipped)."
return 0
fi
if ! command -v adb >/dev/null 2>&1; then
echo "push-fixture-media: adb not found; skip Android." >&2
return 0
fi
if ! adb_cmd shell echo ok >/dev/null 2>&1; then
echo "push-fixture-media: no Android device/emulator; skip Android." >&2
return 0
fi

local f base dest
for f in "${files[@]}"; do
base="$(basename "$f")"
dest="/sdcard/Pictures/$base"
echo "push-fixture-media: Android adb push '$f' -> $dest"
adb_cmd push "$f" "$dest"
adb_cmd shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d "file://$dest" >/dev/null \
|| true
done
echo "push-fixture-media: Android done (${#files[@]} file(s))."
}

push_ios() {
local files=("$@")
if ((${#files[@]} == 0)); then
echo "push-fixture-media: no image files for iOS (skipped)."
return 0
fi
if ! command -v xcrun >/dev/null 2>&1; then
echo "push-fixture-media: xcrun not found; skip iOS." >&2
return 0
fi

local device="${SIMCTL_DEVICE:-booted}"
# addmedia requires a booted simulator when using "booted"
if ! xcrun simctl list devices | grep -q Booted; then
echo "push-fixture-media: no booted iOS Simulator; skip iOS." >&2
return 0
fi

echo "push-fixture-media: iOS simctl addmedia $device ${files[*]}"
xcrun simctl addmedia "$device" "${files[@]}"
echo "push-fixture-media: iOS done (${#files[@]} file(s))."
}

default_fixture_files() {
local dir="$1"
local -a out=()
local f
shopt -s nullglob
for f in "$dir"/*.{jpg,jpeg,png,heic,webp}; do
[[ -f "$f" ]] || continue
out+=("$f")
done
shopt -u nullglob
printf '%s\n' "${out[@]}"
}

main() {
local -a files=()
if (($# > 0)); then
files=("$@")
else
while IFS= read -r line; do
[[ -n "$line" ]] && files+=("$line")
done < <(default_fixture_files "$FIXTURES_DIR")
fi

if ((${#files[@]} == 0)); then
echo "push-fixture-media: no images under $FIXTURES_DIR (add .jpg/.png or pass paths)." >&2
exit 1
fi

for f in "${files[@]}"; do
if [[ ! -f "$f" ]]; then
echo "push-fixture-media: not a file: $f" >&2
exit 1
fi
done

local abs=()
local p
for p in "${files[@]}"; do
abs+=("$(cd "$(dirname "$p")" && pwd)/$(basename "$p")")
done

if [[ "${SKIP_ANDROID:-}" != "1" ]]; then
push_android "${abs[@]}"
else
echo "push-fixture-media: SKIP_ANDROID=1"
fi

if [[ "${SKIP_IOS:-}" != "1" ]]; then
push_ios "${abs[@]}"
else
echo "push-fixture-media: SKIP_IOS=1"
fi
}

main "$@"
Binary file added test/fixtures/alice.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/bob.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 21 additions & 3 deletions test/helpers/actions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Buffer } from 'node:buffer';

import type { ChainablePromiseElement } from 'webdriverio';
import { reinstallApp } from './setup';
import { deposit, mineBlocks } from './regtest';
Expand Down Expand Up @@ -393,6 +395,17 @@ export async function multiTap(testId: string, count: number) {
}
}

/**
* Reads the device clipboard as UTF-8 text (Appium returns base64-encoded content).
*/
export async function getClipboardPlaintext(): Promise<string> {
const b64 = await driver.getClipboard('plaintext');
if (!b64 || b64.length === 0) {
return '';
}
return Buffer.from(b64, 'base64').toString('utf8');
}

export async function pasteIOSText(testId: string, text: string) {
if (!driver.isIOS) {
throw new Error('pasteIOSText can only be used on iOS devices');
Expand Down Expand Up @@ -1010,6 +1023,7 @@ export async function getReceiveAddress(which: addressType = 'bitcoin'): Promise
return getAddressFromQRCode(which);
}

/** Reads the encoded string for the `QRCode` element (receive invoice/address URI, profile pubky, etc.). */
export async function getUriFromQRCode(): Promise<string> {
const qrCode = await elementById('QRCode');
await qrCode.waitForDisplayed();
Expand All @@ -1030,7 +1044,7 @@ export async function getUriFromQRCode(): Promise<string> {
timeoutMsg: `Timed out after ${waitTimeoutMs}ms waiting for QR code URI`,
}
);
console.info({ uri });
console.info('→ QR code URI:', uri);
return uri;
}

Expand Down Expand Up @@ -1159,8 +1173,12 @@ export type ToastId =
| 'DevModeEnabledToast'
| 'DevModeDisabledToast'
| 'InsufficientSpendingToast'
| 'InsufficientSavingsToast';
| 'InsufficientSavingsToast'
| 'ProfilePubkyCopiedToast'
| 'ProfileUpdatedToast';

/** Wait for a toast by test id. Prefer `waitToDisappear` for iOS: success toasts live in a separate
* window, so swipe-dismiss (`dismiss: true`) often uses wrong coordinates and blocks later UI. */
export async function waitForToast(
toastId: ToastId,
{
Expand All @@ -1171,7 +1189,7 @@ export async function waitForToast(
) {
await elementById(toastId).waitForDisplayed({ timeout });
if (waitToDisappear) {
await elementById(toastId).waitForDisplayed({ reverse: true });
await elementById(toastId).waitForDisplayed({ reverse: true, timeout });
return;
}
if (dismiss) {
Expand Down
54 changes: 54 additions & 0 deletions test/helpers/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

const REPO_ROOT = path.resolve(__dirname, '..', '..');
export const FIXTURES_DIR = path.join(REPO_ROOT, 'test', 'fixtures');
const SCRIPT_PATH = path.join(REPO_ROOT, 'scripts', 'push-fixture-media-to-devices.sh');

export type PushFixtureMediaOptions = {
/** Specific files to push (absolute or relative to repo root). Defaults to all images in `test/fixtures/`. */
files?: string[];
/** Force-skip Android even when current driver is Android. */
skipAndroid?: boolean;
/** Force-skip iOS even when current driver is iOS. */
skipIos?: boolean;
};

/**
* Push image fixtures into the currently active platform (Android emulator or iOS Simulator)
* so the OS image picker can use them — e.g. for Pubky profile avatar selection.
*
* Wraps `scripts/push-fixture-media-to-devices.sh` so the OS-specific logic stays in one place.
*
* Notes:
* - Best called once per spec from `before(...)`, after `reinstallApp()` / `completeOnboarding()`.
* - Photos library is global per simulator/emulator; re-running is safe but may add duplicates.
* - On iOS, the Bitkit picker may need Photos permission granted (handled by setup helpers).
*/
export function pushFixtureMedia(options: PushFixtureMediaOptions = {}) {
if (!fs.existsSync(SCRIPT_PATH)) {
throw new Error(`pushFixtureMedia: script not found at ${SCRIPT_PATH}`);
}

const env: NodeJS.ProcessEnv = { ...process.env };
// Restrict to the platform of the active driver unless caller explicitly overrides.
const isAndroid = typeof driver !== 'undefined' && driver.isAndroid;
const isIOS = typeof driver !== 'undefined' && driver.isIOS;
if (options.skipAndroid || (typeof driver !== 'undefined' && !isAndroid)) {
env.SKIP_ANDROID = '1';
}
if (options.skipIos || (typeof driver !== 'undefined' && !isIOS)) {
env.SKIP_IOS = '1';
}

const args = (options.files ?? []).map((f) => (path.isAbsolute(f) ? f : path.join(REPO_ROOT, f)));

try {
execFileSync(SCRIPT_PATH, args, { stdio: 'inherit', env });
console.info('→ pushFixtureMedia: done');
} catch (error) {
console.warn('⚠ pushFixtureMedia failed', error);
throw error;
}
}
20 changes: 20 additions & 0 deletions test/helpers/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type SettingsTab = 'general' | 'security' | 'advanced';
export async function openSettings(tab: SettingsTab = 'general') {
await tap('HeaderMenu');
await tap('DrawerSettings');
await sleep(500);
if (tab !== 'general') {
await tap(`Tab-${tab}`);
await sleep(300);
Expand All @@ -21,6 +22,25 @@ export async function openSettings(tab: SettingsTab = 'general') {
export async function openSupport() {
await tap('HeaderMenu');
await tap('DrawerSupport');
await sleep(500);
}

/**
* Opens the Contacts entry from the drawer menu.
*/
export async function openContacts() {
await tap('HeaderMenu');
await tap('DrawerContacts');
await sleep(500);
}

/**
* Opens the Profile entry from the drawer menu.
*/
export async function openProfile() {
await tap('HeaderMenu');
await tap('DrawerProfile');
await sleep(500);
}

/**
Expand Down
Loading