mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3424bcf2e9 | ||
|
|
d13769a9f2 | ||
|
|
0c8b8179b2 | ||
|
|
533f176e88 | ||
|
|
da72415ef3 | ||
|
|
0109273037 | ||
|
|
33eb8f99e5 | ||
|
|
721c2e34a1 | ||
|
|
e06d92d88f | ||
|
|
42e89122bd | ||
|
|
33fc33910a | ||
|
|
dde504f825 | ||
|
|
f5f3637d01 | ||
|
|
bdade8503d | ||
|
|
57ca1c8083 | ||
|
|
32a41f42d3 | ||
|
|
ad9db5cbff | ||
|
|
99e4bd9614 | ||
|
|
239bf618b5 | ||
|
|
58e52421e9 | ||
|
|
96ffeaa0e2 | ||
|
|
81a0723abd | ||
|
|
6103df86a5 | ||
|
|
b97a877fce | ||
|
|
663f6f0007 | ||
|
|
5477cc039a | ||
|
|
ab8e8d7e1e | ||
|
|
49e57482cc | ||
|
|
5f06c3918c | ||
|
|
96471cf13b | ||
|
|
3575ed9450 | ||
|
|
f181c73a56 | ||
|
|
c85e32c9f4 | ||
|
|
f2b98c6dba | ||
|
|
653d5965ed | ||
|
|
01d15fd24f | ||
|
|
35e44318b9 | ||
|
|
3bdafefb65 | ||
|
|
982a23cafd | ||
|
|
c39e977de5 | ||
|
|
24948cb717 | ||
|
|
6b434346ff | ||
|
|
1827f8eb34 | ||
|
|
9c549c9d00 | ||
|
|
f00cd6a259 | ||
|
|
62931bcc53 | ||
|
|
5622d732e9 | ||
|
|
db3c6b5ba1 | ||
|
|
680d4745ed | ||
|
|
8533bd4f99 | ||
|
|
52975bd715 | ||
|
|
bf226c3794 | ||
|
|
ca09708fd1 | ||
|
|
96d9d54cc9 | ||
|
|
02bd714afd | ||
|
|
e57ee3d446 | ||
|
|
08e2f47187 | ||
|
|
de414f7ed6 | ||
|
|
273051e5f5 | ||
|
|
fa68b2eb53 | ||
|
|
3748a64b10 | ||
|
|
2078388e6f | ||
|
|
ea9bad7cff | ||
|
|
ae0b76442a | ||
|
|
341cdb0e61 | ||
|
|
1c9bbcc383 | ||
|
|
c7eda4735c | ||
|
|
5f5ffdaddb | ||
|
|
083ec958a0 | ||
|
|
2b486cb1ef | ||
|
|
86400b124a | ||
|
|
323797b4ee | ||
|
|
9166b0fb3c | ||
|
|
1f253e9488 | ||
|
|
d22fb8d5d0 | ||
|
|
e0f18713aa | ||
|
|
56d826c508 | ||
|
|
e5492b89c8 | ||
|
|
b50436b1ba | ||
|
|
11e74042e2 | ||
|
|
95580b84ea |
@@ -0,0 +1,92 @@
|
||||
name: iOS simulator test leg
|
||||
description: Boot a simulator, install the app and run one suite of Maestro flows
|
||||
|
||||
inputs:
|
||||
suite:
|
||||
description: Folder under .maestro (signin or features)
|
||||
required: true
|
||||
name:
|
||||
description: Leg name, used for the artifact
|
||||
required: true
|
||||
app_run_id:
|
||||
description: Take the simulator app from this run instead of the current one
|
||||
required: false
|
||||
default: ""
|
||||
flows:
|
||||
description: Comma list of flow names to run instead of the whole suite
|
||||
required: false
|
||||
default: ""
|
||||
playwright:
|
||||
description: Install Playwright for the helper's webmail pairing
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download the simulator app (this run)
|
||||
if: inputs.app_run_id == ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: simulator-app
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Download the simulator app (earlier run)
|
||||
if: inputs.app_run_id != ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: simulator-app
|
||||
run-id: ${{ inputs.app_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Unpack the simulator app
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/app"
|
||||
tar -C "$RUNNER_TEMP/app" -xzf "$RUNNER_TEMP/simulator-app.tar.gz"
|
||||
echo "APP=$RUNNER_TEMP/app/BulwarkMobile.app" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Boot a simulator
|
||||
shell: bash
|
||||
run: bash scripts/e2e/boot-sim.sh "$DEVICE" "$APPEARANCE" "$APP"
|
||||
|
||||
- name: Install Maestro
|
||||
shell: bash
|
||||
run: |
|
||||
curl -fsSL "https://get.maestro.mobile.dev" | bash
|
||||
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Playwright for the helper
|
||||
if: inputs.playwright == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/pw"
|
||||
cd "$RUNNER_TEMP/pw"
|
||||
npm init -y >/dev/null
|
||||
npm install --no-audit --no-fund playwright@1.59.1 >/dev/null
|
||||
npx playwright install chromium >/dev/null
|
||||
echo "PLAYWRIGHT_DIR=$RUNNER_TEMP/pw/node_modules" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Start the test helper
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "$OUT/helper"
|
||||
HELPER_SHOTS="$OUT/helper" nohup node scripts/e2e/helper.mjs > "$OUT/helper/helper.log" 2>&1 &
|
||||
for i in $(seq 1 20); do
|
||||
curl -fs http://127.0.0.1:8787/health >/dev/null && break
|
||||
sleep 0.5
|
||||
done
|
||||
curl -fs http://127.0.0.1:8787/health
|
||||
|
||||
- name: Run the flows
|
||||
shell: bash
|
||||
run: bash scripts/e2e/run-flows.sh "${{ inputs.suite }}" "${{ inputs.flows }}"
|
||||
|
||||
- name: Upload screenshots, recording and logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-ios-${{ inputs.name }}
|
||||
path: e2e-output/
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,349 @@
|
||||
name: iOS simulator tests
|
||||
|
||||
# Builds the app for the iOS Simulator (no signing needed) and runs the
|
||||
# Maestro flows in .maestro/ on iPhone and iPad simulators:
|
||||
#
|
||||
# - signin: every sign-in method (.maestro/signin), on an iPhone and an iPad
|
||||
# - features: the app feature by feature (.maestro/features), on iPhone and
|
||||
# iPad in light and dark, plus the iPad in landscape (split layout)
|
||||
#
|
||||
# Each leg signs in with its own account on the test server (e2e-* accounts
|
||||
# on VM 106, password E2E_PASSWORD), so parallel legs never share a mailbox.
|
||||
# scripts/e2e/helper.mjs runs next to the simulator for what a flow can't do
|
||||
# itself: TOTP codes, fresh webmail pairing links, mail sent by userb.
|
||||
#
|
||||
# Artifacts per leg: screenshots, a screen recording, the app log and a JUnit
|
||||
# report. They are public; see scripts/e2e/run-flows.sh for how secrets are
|
||||
# kept out of them.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
app_run_id:
|
||||
description: "Reuse the simulator app from this earlier run instead of building (run id)"
|
||||
type: string
|
||||
required: false
|
||||
legs:
|
||||
description: "Legs to run: all, signin, features, perf, or a comma list (signin-phone, features-tablet-dark, ...)"
|
||||
type: string
|
||||
default: all
|
||||
flows:
|
||||
description: "Only these flows (comma list of file names without .yaml); empty runs all"
|
||||
type: string
|
||||
required: false
|
||||
perf_base_run_id:
|
||||
description: "perf leg: compare against the simulator app of this run (run id)"
|
||||
type: string
|
||||
required: false
|
||||
perf_rounds:
|
||||
description: "perf leg: rounds (each: 3 cold starts and the interaction flow per app)"
|
||||
type: string
|
||||
default: "5"
|
||||
required: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
concurrency:
|
||||
# Per branch and set of legs: a perf run doesn't cancel a feature run.
|
||||
group: e2e-ios-${{ github.ref }}-${{ inputs.legs || 'all' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
EXPO_NO_TELEMETRY: "1"
|
||||
MAESTRO_CLI_NO_ANALYTICS: "1"
|
||||
# The XCTest driver takes minutes to come up on a cold runner.
|
||||
MAESTRO_DRIVER_STARTUP_TIMEOUT: "600000"
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
signin: ${{ steps.plan.outputs.signin }}
|
||||
features: ${{ steps.plan.outputs.features }}
|
||||
steps:
|
||||
- id: plan
|
||||
env:
|
||||
LEGS: ${{ inputs.legs || 'all' }}
|
||||
run: |
|
||||
node -e '
|
||||
const legs = (process.env.LEGS || "all").split(",").map((s) => s.trim()).filter(Boolean);
|
||||
const want = (name, group) => legs.includes("all") || legs.includes(group) || legs.includes(name);
|
||||
const signin = ["phone", "tablet"]
|
||||
.map((device) => ({ name: `signin-${device}`, device }))
|
||||
.filter((l) => want(l.name, "signin"));
|
||||
const features = [
|
||||
{ name: "features-phone-light", device: "phone", appearance: "light", orientation: "portrait", account: "e2e-phone-light" },
|
||||
{ name: "features-phone-dark", device: "phone", appearance: "dark", orientation: "portrait", account: "e2e-phone-dark" },
|
||||
{ name: "features-tablet-light", device: "tablet", appearance: "light", orientation: "portrait", account: "e2e-tablet-light" },
|
||||
{ name: "features-tablet-dark", device: "tablet", appearance: "dark", orientation: "portrait", account: "e2e-tablet-dark" },
|
||||
{ name: "features-tablet-landscape", device: "tablet", appearance: "light", orientation: "landscape", account: "e2e-tablet-land" },
|
||||
].filter((l) => want(l.name, "features"));
|
||||
// Push on iOS (.maestro/push): only on request, the app needs the
|
||||
// notification extension (plugins/ios-push).
|
||||
if (legs.includes("push")) {
|
||||
features.push({ name: "push-phone", device: "phone", appearance: "light", orientation: "portrait", account: "e2e-perf", suite: "push" });
|
||||
}
|
||||
const fs = require("fs");
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT,
|
||||
`signin=${JSON.stringify({ include: signin })}\nfeatures=${JSON.stringify({ include: features })}\n`);
|
||||
console.log({ signin: signin.map((l) => l.name), features: features.map((l) => l.name) });
|
||||
'
|
||||
|
||||
build:
|
||||
if: inputs.app_run_id == ''
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Cache CocoaPods
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/CocoaPods
|
||||
~/.cocoapods/repos
|
||||
key: pods-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: pods-${{ runner.os }}-
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate the iOS project
|
||||
env:
|
||||
# Simulator device tokens belong to the APNs sandbox.
|
||||
APS_ENVIRONMENT: development
|
||||
run: npx expo prebuild --platform ios --clean
|
||||
|
||||
- name: Build for the simulator
|
||||
env:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
# The bundle times each module's evaluation for the perf leg, and
|
||||
# Metro writes which id is which module (metro.config.js; a branch
|
||||
# without that support ignores both).
|
||||
BULWARK_MODULE_TIMINGS: '1'
|
||||
BULWARK_MODULE_MAP: ${{ runner.temp }}/module-map.json
|
||||
run: |
|
||||
WORKSPACE=$(ls -d ios/*.xcworkspace | head -1)
|
||||
SCHEME=$(basename "$WORKSPACE" .xcworkspace)
|
||||
mkdir -p e2e-build
|
||||
# Release bundles the JS into the app, so no Metro server is needed.
|
||||
# arm64 only: the runner is Apple silicon, and a second simulator
|
||||
# architecture would double the build time.
|
||||
if ! xcodebuild build \
|
||||
-workspace "$WORKSPACE" \
|
||||
-scheme "$SCHEME" \
|
||||
-configuration Release \
|
||||
-sdk iphonesimulator \
|
||||
-destination 'generic/platform=iOS Simulator' \
|
||||
-derivedDataPath build \
|
||||
ARCHS=arm64 \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
CODE_SIGN_IDENTITY=- \
|
||||
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
|
||||
PROVISIONING_PROFILE_SPECIFIER= \
|
||||
> e2e-build/xcodebuild.log 2>&1; then
|
||||
grep -E "error:|fatal error" e2e-build/xcodebuild.log | head -n 50 || true
|
||||
tail -n 80 e2e-build/xcodebuild.log
|
||||
exit 1
|
||||
fi
|
||||
# Signed ad hoc ("Sign to Run Locally"): no certificate needed, and
|
||||
# Xcode embeds the simulator entitlements the keychain needs. An
|
||||
# unsigned build has none, and every expo-secure-store call fails
|
||||
# with "A required entitlement isn't present".
|
||||
APP_PATH=build/Build/Products/Release-iphonesimulator/BulwarkMobile.app
|
||||
echo "Simulated entitlements:"
|
||||
xcrun otool -X -s __TEXT __entitlements "$APP_PATH/BulwarkMobile" | head -20 || true
|
||||
codesign -d --entitlements - "$APP_PATH" 2>&1 | head -20 || true
|
||||
# Packed as a tar: artifacts drop the executable bit, which would
|
||||
# leave a reused app unable to launch.
|
||||
tar -C build/Build/Products/Release-iphonesimulator -cf "$RUNNER_TEMP/simulator-app.tar" BulwarkMobile.app
|
||||
if [ -f "$RUNNER_TEMP/module-map.json" ]; then
|
||||
tar -C "$RUNNER_TEMP" -rf "$RUNNER_TEMP/simulator-app.tar" module-map.json
|
||||
else
|
||||
echo "no module map (branch without module timings)"
|
||||
fi
|
||||
gzip -f "$RUNNER_TEMP/simulator-app.tar"
|
||||
ls -la "$RUNNER_TEMP/simulator-app.tar.gz"
|
||||
|
||||
- name: Upload the simulator app
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: simulator-app
|
||||
path: ${{ runner.temp }}/simulator-app.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload the build log
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xcodebuild-log
|
||||
path: e2e-build/
|
||||
|
||||
signin:
|
||||
needs: [plan, build]
|
||||
if: >-
|
||||
always() && !cancelled() &&
|
||||
(needs.build.result == 'success' || needs.build.result == 'skipped') &&
|
||||
fromJson(needs.plan.outputs.signin).include[0] != null
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 150
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.plan.outputs.signin) }}
|
||||
env:
|
||||
OUT: ${{ github.workspace }}/e2e-output
|
||||
DEVICE: ${{ matrix.device }}
|
||||
APPEARANCE: light
|
||||
ORIENTATION: portrait
|
||||
E2E_SERVER: ${{ vars.E2E_SERVER }}
|
||||
E2E_WEBMAIL: https://bulwark.internal.bulwarkmail.org
|
||||
E2E_ACCOUNT: e2e-login-${{ matrix.device }}
|
||||
E2E_ACCOUNT2: e2e-second-${{ matrix.device }}
|
||||
E2E_TOTP_ACCOUNT: e2e-totp-${{ matrix.device }}
|
||||
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
|
||||
E2E_USERB_PASSWORD: ${{ secrets.E2E_USERB_PASSWORD }}
|
||||
E2E_TOTP_SECRET: ${{ secrets.E2E_TOTP_SECRET }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/e2e-ios-leg
|
||||
with:
|
||||
suite: signin
|
||||
app_run_id: ${{ inputs.app_run_id }}
|
||||
flows: ${{ inputs.flows }}
|
||||
playwright: "true"
|
||||
name: ${{ matrix.name }}
|
||||
|
||||
features:
|
||||
needs: [plan, build]
|
||||
if: >-
|
||||
always() && !cancelled() &&
|
||||
(needs.build.result == 'success' || needs.build.result == 'skipped') &&
|
||||
fromJson(needs.plan.outputs.features).include[0] != null
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 120
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.plan.outputs.features) }}
|
||||
env:
|
||||
OUT: ${{ github.workspace }}/e2e-output
|
||||
DEVICE: ${{ matrix.device }}
|
||||
APPEARANCE: ${{ matrix.appearance }}
|
||||
ORIENTATION: ${{ matrix.orientation }}
|
||||
E2E_SERVER: ${{ vars.E2E_SERVER }}
|
||||
E2E_WEBMAIL: https://bulwark.internal.bulwarkmail.org
|
||||
E2E_ACCOUNT: ${{ matrix.account }}
|
||||
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
|
||||
E2E_USERB_PASSWORD: ${{ secrets.E2E_USERB_PASSWORD }}
|
||||
E2E_TOTP_SECRET: ${{ secrets.E2E_TOTP_SECRET }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/e2e-ios-leg
|
||||
with:
|
||||
suite: ${{ matrix.suite || 'features' }}
|
||||
app_run_id: ${{ inputs.app_run_id }}
|
||||
flows: ${{ inputs.flows }}
|
||||
playwright: "false"
|
||||
name: ${{ matrix.name }}
|
||||
|
||||
# Part 2 of docs/ios-test-2026-10.md: timing marks from the app
|
||||
# (src/lib/perf-marks.ts), read by scripts/e2e/perf.mjs. Compares the app
|
||||
# of this run (or app_run_id) with the app of perf_base_run_id, alternating
|
||||
# them on one simulator so both sides share the runner.
|
||||
perf:
|
||||
needs: [plan, build]
|
||||
if: >-
|
||||
always() && !cancelled() &&
|
||||
(needs.build.result == 'success' || needs.build.result == 'skipped') &&
|
||||
contains(inputs.legs, 'perf')
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 120
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
env:
|
||||
OUT: ${{ github.workspace }}/e2e-output
|
||||
DEVICE: phone
|
||||
APPEARANCE: light
|
||||
ORIENTATION: portrait
|
||||
E2E_SERVER: ${{ vars.E2E_SERVER }}
|
||||
E2E_ACCOUNT: e2e-bulk
|
||||
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download the app under test (this run)
|
||||
if: inputs.app_run_id == ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: simulator-app
|
||||
path: ${{ runner.temp }}/cand
|
||||
- name: Download the app under test (earlier run)
|
||||
if: inputs.app_run_id != ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: simulator-app
|
||||
run-id: ${{ inputs.app_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: ${{ runner.temp }}/cand
|
||||
- name: Download the baseline app
|
||||
if: inputs.perf_base_run_id != ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: simulator-app
|
||||
run-id: ${{ inputs.perf_base_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: ${{ runner.temp }}/base
|
||||
|
||||
- name: Unpack
|
||||
run: |
|
||||
for side in cand base; do
|
||||
[ -f "$RUNNER_TEMP/$side/simulator-app.tar.gz" ] || continue
|
||||
mkdir -p "$RUNNER_TEMP/$side/app"
|
||||
tar -C "$RUNNER_TEMP/$side/app" -xzf "$RUNNER_TEMP/$side/simulator-app.tar.gz"
|
||||
done
|
||||
echo "APP=$RUNNER_TEMP/cand/app/BulwarkMobile.app" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Boot a simulator
|
||||
run: bash scripts/e2e/boot-sim.sh "$DEVICE" "$APPEARANCE" "$APP"
|
||||
|
||||
- name: Install Maestro
|
||||
run: |
|
||||
curl -fsSL "https://get.maestro.mobile.dev" | bash
|
||||
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Sign in once
|
||||
run: |
|
||||
mkdir -p "$OUT/screenshots"
|
||||
cd "$OUT/screenshots"
|
||||
# Warm-up for the driver, then the sign-in (keychain survives reinstalls).
|
||||
maestro --device "$UDID" test "$GITHUB_WORKSPACE/.maestro/launch.yaml"
|
||||
maestro --device "$UDID" test -e "SERVER=$E2E_SERVER" -e "PASSWORD=$E2E_PASSWORD" \
|
||||
-e "ACCOUNT=$E2E_ACCOUNT" -e "ORIENTATION=portrait" "$GITHUB_WORKSPACE/.maestro/perf/sign-in.yaml"
|
||||
|
||||
- name: Measure
|
||||
run: |
|
||||
APPS=(--app "cand=$RUNNER_TEMP/cand/app/BulwarkMobile.app")
|
||||
if [ -d "$RUNNER_TEMP/base/app/BulwarkMobile.app" ]; then
|
||||
APPS=(--app "base=$RUNNER_TEMP/base/app/BulwarkMobile.app" "${APPS[@]}")
|
||||
fi
|
||||
node scripts/e2e/perf.mjs "${APPS[@]}" --rounds "${{ inputs.perf_rounds || 5 }}" --cold 5 \
|
||||
--flows "$(ls "$GITHUB_WORKSPACE"/.maestro/perf/[0-9]-*.yaml | paste -sd, -)" --out "$OUT"
|
||||
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-ios-perf
|
||||
path: e2e-output/
|
||||
if-no-files-found: ignore
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release Android APK
|
||||
name: Release Android
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
ORG_GRADLE_PROJECT_BULWARK_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease --no-daemon
|
||||
./gradlew assembleGithubRelease --no-daemon
|
||||
|
||||
- name: Put apksigner on the PATH
|
||||
run: |
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
# Oldest signer first: apksigner uses it for v2, and the rotated key
|
||||
# for v3 from API 28. Without --rotation-min-sdk-version it would
|
||||
# only rotate on Android 13+ (v3.1) and leave 9-12L on the debug key.
|
||||
APK=android/app/build/outputs/apk/release/app-release.apk
|
||||
APK=android/app/build/outputs/apk/github/release/app-github-release.apk
|
||||
apksigner sign \
|
||||
--v4-signing-enabled false \
|
||||
--ks android/app/debug.keystore --ks-key-alias androiddebugkey \
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
ROTATED: ${{ steps.rotation.outputs.rotated }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK=android/app/build/outputs/apk/release/app-release.apk
|
||||
APK=android/app/build/outputs/apk/github/release/app-github-release.apk
|
||||
apksigner verify --verbose --print-certs "$APK"
|
||||
# build-tools 36 prints "Signer #1 certificate SHA-256 digest: …",
|
||||
# 37 prints "V3.0 Signer: certificate …" and "V2 Signer: …".
|
||||
@@ -248,12 +248,12 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||
SRC=android/app/build/outputs/apk/release/app-release.apk
|
||||
SRC=android/app/build/outputs/apk/github/release/app-github-release.apk
|
||||
DEST="bulwark-mobile-${VERSION}-${GITHUB_SHA::7}.apk"
|
||||
cp "$SRC" "$DEST"
|
||||
sha256sum "$DEST" > "$DEST.sha256"
|
||||
|
||||
MAPPING_SRC=android/app/build/outputs/mapping/release/mapping.txt
|
||||
MAPPING_SRC=android/app/build/outputs/mapping/githubRelease/mapping.txt
|
||||
MAPPING=""
|
||||
if [ -f "$MAPPING_SRC" ]; then
|
||||
MAPPING="bulwark-mobile-${VERSION}-${GITHUB_SHA::7}-mapping.txt"
|
||||
@@ -299,12 +299,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# A manual run with no tag only builds: it must not replace the APK
|
||||
# of the latest release with one from another commit.
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
elif [ -n "${{ github.event.inputs.attach_to_release_tag }}" ]; then
|
||||
TAG="${{ github.event.inputs.attach_to_release_tag }}"
|
||||
else
|
||||
TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' || true)
|
||||
TAG="${{ github.event.inputs.attach_to_release_tag }}"
|
||||
fi
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
if [ -n "$TAG" ]; then
|
||||
@@ -319,3 +319,148 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ steps.target_tag.outputs.tag }}
|
||||
files: ${{ steps.artifact.outputs.files }}
|
||||
|
||||
# The Google Play bundle (docs/android-release.md, "Google Play"). It is
|
||||
# signed with the release key, which Play knows as the upload key; Play
|
||||
# re-signs what it delivers with its own app signing key. A separate job so
|
||||
# the GitHub APK isn't held up by the three-ABI native build.
|
||||
play-bundle:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 75
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
steps:
|
||||
- name: Require the release signing secrets
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
missing=""
|
||||
[ -n "$KEYSTORE_BASE64" ] || missing="$missing ANDROID_KEYSTORE_BASE64"
|
||||
[ -n "$KEYSTORE_PASSWORD" ] || missing="$missing ANDROID_KEYSTORE_PASSWORD"
|
||||
[ -n "$KEY_ALIAS" ] || missing="$missing ANDROID_KEY_ALIAS"
|
||||
[ -n "$KEY_PASSWORD" ] || missing="$missing ANDROID_KEY_PASSWORD"
|
||||
if [ -n "$missing" ]; then
|
||||
echo "::error title=Release signing is not configured::Missing repository secrets:$missing. See docs/android-release.md."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- uses: android-actions/setup-android@v3
|
||||
with:
|
||||
packages: platform-tools
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Decode and check the upload key
|
||||
id: key
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
KEYSTORE="$RUNNER_TEMP/release.keystore"
|
||||
CERT="$RUNNER_TEMP/release-cert.der"
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > "$KEYSTORE"
|
||||
keytool -exportcert -keystore "$KEYSTORE" -alias "$KEY_ALIAS" \
|
||||
-storepass:env KEYSTORE_PASSWORD -file "$CERT"
|
||||
CERT_SHA256=$(sha256sum "$CERT" | cut -d' ' -f1)
|
||||
if [ "$CERT_SHA256" = "$DEBUG_CERT_SHA256" ]; then
|
||||
echo "::error title=Upload key is a debug key::ANDROID_KEYSTORE_BASE64 holds the Android debug key."
|
||||
exit 1
|
||||
fi
|
||||
echo "store_file=$KEYSTORE" >> "$GITHUB_OUTPUT"
|
||||
echo "cert_sha256=$CERT_SHA256" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Play splits a bundle per ABI, so the extra architectures cost a phone
|
||||
# nothing and let 32-bit ARM phones and x86_64 Chromebooks install it.
|
||||
- name: Build the Play bundle
|
||||
working-directory: android
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_BULWARK_RELEASE_STORE_FILE: ${{ steps.key.outputs.store_file }}
|
||||
ORG_GRADLE_PROJECT_BULWARK_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ORG_GRADLE_PROJECT_BULWARK_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ORG_GRADLE_PROJECT_BULWARK_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
./gradlew bundlePlayRelease --no-daemon -PreactNativeArchitectures=armeabi-v7a,arm64-v8a,x86_64
|
||||
|
||||
# Play rejects a bundle signed with another key than the registered
|
||||
# upload key, and its policies reject a self-updater and the media
|
||||
# permission. Catch all three here instead of in the Play Console.
|
||||
- name: Check the bundle
|
||||
env:
|
||||
RELEASE_CERT_SHA256: ${{ steps.key.outputs.cert_sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AAB=android/app/build/outputs/bundle/playRelease/app-play-release.aab
|
||||
SIGNER=$(keytool -printcert -jarfile "$AAB" | sed -n 's/^[[:space:]]*SHA256: //p' | head -n 1 | tr -d ':' | tr 'A-F' 'a-f')
|
||||
echo "Bundle signer SHA-256: $SIGNER"
|
||||
if [ "$SIGNER" != "$RELEASE_CERT_SHA256" ]; then
|
||||
echo "::error title=Wrong bundle signer::Expected the upload key $RELEASE_CERT_SHA256, got '$SIGNER'."
|
||||
exit 1
|
||||
fi
|
||||
MANIFEST=$(unzip -p "$AAB" base/manifest/AndroidManifest.xml | tr -c '[:print:]' '\n')
|
||||
if ! grep -q 'org.bulwarkmail.mobile' <<< "$MANIFEST"; then
|
||||
echo "::error title=Wrong package::The bundle is not org.bulwarkmail.mobile."
|
||||
exit 1
|
||||
fi
|
||||
for permission in REQUEST_INSTALL_PACKAGES READ_MEDIA_IMAGES READ_MEDIA_VIDEO RECORD_AUDIO; do
|
||||
if grep -q "android.permission.$permission" <<< "$MANIFEST"; then
|
||||
echo "::error title=Permission not allowed on Play::The Play bundle declares $permission."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Name the bundle and R8 mapping with version
|
||||
id: artifact
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||
AAB="bulwark-mobile-play-${VERSION}-${GITHUB_SHA::7}.aab"
|
||||
MAPPING="bulwark-mobile-play-${VERSION}-${GITHUB_SHA::7}-mapping.txt"
|
||||
cp android/app/build/outputs/bundle/playRelease/app-play-release.aab "$AAB"
|
||||
cp android/app/build/outputs/mapping/playRelease/mapping.txt "$MAPPING"
|
||||
echo "aab=$AAB" >> "$GITHUB_OUTPUT"
|
||||
echo "mapping=$MAPPING" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.artifact.outputs.aab }}
|
||||
path: |
|
||||
${{ steps.artifact.outputs.aab }}
|
||||
${{ steps.artifact.outputs.mapping }}
|
||||
if-no-files-found: error
|
||||
|
||||
# Runs once PLAY_SERVICE_ACCOUNT_JSON is set. Play only accepts API
|
||||
# uploads after the first bundle was uploaded by hand in the Console, and
|
||||
# releases of an app that was never published must be drafts; switch
|
||||
# `status` to `completed` after the first production release.
|
||||
- name: Upload to Google Play (internal testing, draft)
|
||||
if: env.PLAY_SERVICE_ACCOUNT_JSON != ''
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
packageName: org.bulwarkmail.mobile
|
||||
releaseFiles: ${{ steps.artifact.outputs.aab }}
|
||||
mappingFile: ${{ steps.artifact.outputs.mapping }}
|
||||
track: internal
|
||||
status: draft
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Mailbox list and folders: the drawer (or docked sidebar) lists the role
|
||||
# folders and custom ones; opening a folder shows its messages.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Folders
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- takeScreenshot: folders-01-inbox
|
||||
- assertVisible: ".*Your verification code.*"
|
||||
- runFlow: ../subflows/open-drawer.yaml
|
||||
- takeScreenshot: folders-02-drawer
|
||||
- assertVisible: "Inbox([ ,].*)?"
|
||||
- assertVisible: "Sent([ ,].*)?"
|
||||
- assertVisible: "Drafts([ ,].*)?"
|
||||
- assertVisible: "Archive([ ,].*)?"
|
||||
# Parent folders read "Collapse, Projects" (the chevron comes first).
|
||||
- assertVisible: ".*Projects.*"
|
||||
- tapOn: "Archive([ ,].*)?"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Invoice 2025-12.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: folders-03-archive
|
||||
- runFlow: ../subflows/open-drawer.yaml
|
||||
# Projects/Alpha holds the seeded thread.
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: "Alpha([ ,].*)?"
|
||||
commands:
|
||||
- tapOn: "Expand, Projects.*"
|
||||
- tapOn: "Alpha([ ,].*)?"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Project Alpha kickoff.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: folders-04-alpha
|
||||
- runFlow: ../subflows/go-to-inbox.yaml
|
||||
@@ -0,0 +1,75 @@
|
||||
# Opening messages: plain text, HTML with blocked remote content, a thread,
|
||||
# and a PDF attachment in the in-app preview.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Read messages
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
|
||||
# Plain text
|
||||
- runFlow:
|
||||
file: ../subflows/open-message.yaml
|
||||
env:
|
||||
SUBJECT: Grüße aus München
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Jürgen.*"
|
||||
timeout: 10000
|
||||
- takeScreenshot: read-01-plain
|
||||
- runFlow: ../subflows/close-message.yaml
|
||||
|
||||
# HTML newsletter
|
||||
- runFlow:
|
||||
file: ../subflows/open-message.yaml
|
||||
env:
|
||||
SUBJECT: Weekly digest
|
||||
- extendedWaitUntil:
|
||||
visible: ".*5 new articles.*|Weekly digest"
|
||||
timeout: 20000
|
||||
- takeScreenshot: read-02-html
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Load images"
|
||||
commands:
|
||||
- tapOn: "Load images"
|
||||
- takeScreenshot: read-03-html-images
|
||||
- runFlow: ../subflows/close-message.yaml
|
||||
|
||||
# Attachment and its preview
|
||||
- runFlow:
|
||||
file: ../subflows/open-message.yaml
|
||||
env:
|
||||
SUBJECT: Q3 report
|
||||
- extendedWaitUntil:
|
||||
visible: ".*q3-report.pdf.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: read-04-attachment
|
||||
# The message's attachment chip, labelled with the file name alone (on the
|
||||
# iPad the list row next to it mentions the name in its longer label).
|
||||
- tapOn: "q3-report.pdf"
|
||||
- extendedWaitUntil:
|
||||
visible: "Share"
|
||||
timeout: 30000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: read-05-attachment-preview
|
||||
- assertVisible: "q3-report.pdf"
|
||||
- tapOn: "Close"
|
||||
# Back on the message (the iPad pane has a quick reply instead of Reply All).
|
||||
- extendedWaitUntil:
|
||||
notVisible: "Share"
|
||||
timeout: 10000
|
||||
- assertVisible: ".*q3-report.pdf.*"
|
||||
- runFlow: ../subflows/close-message.yaml
|
||||
|
||||
# A thread (Projects/Alpha)
|
||||
- runFlow:
|
||||
file: ../subflows/go-to-folder.yaml
|
||||
env:
|
||||
FOLDER: Alpha
|
||||
- runFlow:
|
||||
file: ../subflows/open-message.yaml
|
||||
env:
|
||||
SUBJECT: Project Alpha kickoff
|
||||
- takeScreenshot: read-06-thread
|
||||
- runFlow: ../subflows/go-to-inbox.yaml
|
||||
@@ -0,0 +1,101 @@
|
||||
# Compose a new message and send it to userb; userb's mailbox (checked over
|
||||
# JMAP) must get it.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Compose and send
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- evalScript: ${output.subject = 'E2E compose ' + DEVICE + '-' + APPEARANCE + '-' + ORIENTATION + ' ' + Date.now()}
|
||||
- tapOn: "Compose"
|
||||
- extendedWaitUntil:
|
||||
visible: "New Message"
|
||||
timeout: 15000
|
||||
- tapOn: "Recipient email addresses"
|
||||
- inputText: "userb@example.org"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
text: "Subject"
|
||||
index: 1
|
||||
- inputText: ${output.subject}
|
||||
- runFlow:
|
||||
file: ../subflows/compose-body.yaml
|
||||
env:
|
||||
FROM_SUBJECT: "yes"
|
||||
BODY: "Hello from the iOS simulator test."
|
||||
- takeScreenshot: compose-01-filled
|
||||
|
||||
# The attach sheet offers its options one by one (it used to be a single
|
||||
# accessibility element, and picking one opened no picker on iOS).
|
||||
- tapOn: "Attach"
|
||||
- extendedWaitUntil:
|
||||
visible: "Photos & Videos"
|
||||
timeout: 10000
|
||||
- assertVisible: "Camera"
|
||||
- assertVisible: "Files"
|
||||
- takeScreenshot: compose-02-attach-sheet
|
||||
- evalScript: ${output.pickPhoto = ORIENTATION != 'landscape'}
|
||||
# The system photo picker runs in another process: XCTest sees none of its
|
||||
# elements, but taps by position reach it. Pick the first photo (the app
|
||||
# icon, added by boot-sim.sh) and confirm with the check mark. The iPad in
|
||||
# landscape lays the picker out differently, so it only closes the sheet.
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${!output.pickPhoto}
|
||||
commands:
|
||||
- tapOn: "Cancel"
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${output.pickPhoto}
|
||||
commands:
|
||||
- tapOn: "Photos & Videos"
|
||||
- extendedWaitUntil:
|
||||
notVisible: "Draft saved|New Message"
|
||||
timeout: 10000
|
||||
optional: true
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: compose-03-photo-picker
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE == 'tablet'}
|
||||
commands:
|
||||
- tapOn:
|
||||
point: "44%,33%"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 2000
|
||||
- tapOn:
|
||||
point: "90%,26%"
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE != 'tablet'}
|
||||
commands:
|
||||
- tapOn:
|
||||
point: "16%,25%"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 2000
|
||||
- tapOn:
|
||||
point: "90%,13%"
|
||||
- extendedWaitUntil:
|
||||
visible: "New Message"
|
||||
timeout: 20000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: compose-04-with-photo
|
||||
|
||||
- tapOn: "Send"
|
||||
- extendedWaitUntil:
|
||||
visible: "Email sent successfully"
|
||||
timeout: 20000
|
||||
optional: true
|
||||
- runFlow:
|
||||
file: ../subflows/assert-delivered.yaml
|
||||
env:
|
||||
SUBJECT: ${output.subject}
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${output.pickPhoto}
|
||||
commands:
|
||||
- assertTrue:
|
||||
condition: ${output.helper.attachments.length >= 1}
|
||||
label: "${output.subject} carries the picked photo"
|
||||
@@ -0,0 +1,72 @@
|
||||
# Incoming mail while the app is open (sent by userb over JMAP), then reply
|
||||
# and forward it; userb receives both, the forward with the attachment.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Incoming, reply, forward
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- evalScript: ${output.incoming = 'E2E incoming ' + DEVICE + '-' + APPEARANCE + '-' + ORIENTATION + ' ' + Date.now()}
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /send?to=${ACCOUNT}&subject=${encodeURIComponent(output.incoming)}&attach=1&html=1
|
||||
# Arrives without a pull to refresh (push over EventSource).
|
||||
- extendedWaitUntil:
|
||||
visible: ".*${output.incoming}.*"
|
||||
timeout: 90000
|
||||
- takeScreenshot: incoming-01-arrived
|
||||
- runFlow:
|
||||
file: ../subflows/open-message.yaml
|
||||
env:
|
||||
SUBJECT: ${output.incoming}
|
||||
- takeScreenshot: incoming-02-open
|
||||
|
||||
# Reply
|
||||
- tapOn:
|
||||
text: "Reply"
|
||||
index: 0
|
||||
- extendedWaitUntil:
|
||||
visible: "Re: ${output.incoming}"
|
||||
timeout: 15000
|
||||
- runFlow:
|
||||
file: ../subflows/compose-body.yaml
|
||||
env:
|
||||
BODY: "Reply from the iOS simulator test."
|
||||
- takeScreenshot: incoming-03-reply
|
||||
- tapOn: "Send"
|
||||
# The toast is gone after a few seconds; delivery is checked below.
|
||||
- extendedWaitUntil:
|
||||
visible: "Email sent successfully"
|
||||
timeout: 20000
|
||||
optional: true
|
||||
- runFlow:
|
||||
file: ../subflows/assert-delivered.yaml
|
||||
env:
|
||||
SUBJECT: "Re: ${output.incoming}"
|
||||
|
||||
# Forward with the attachment
|
||||
- extendedWaitUntil:
|
||||
visible: "Forward"
|
||||
timeout: 15000
|
||||
- tapOn:
|
||||
text: "Forward"
|
||||
index: 0
|
||||
- extendedWaitUntil:
|
||||
visible: "Fwd: ${output.incoming}"
|
||||
timeout: 15000
|
||||
- tapOn: "Recipient email addresses"
|
||||
- inputText: "userb@example.org"
|
||||
- pressKey: Enter
|
||||
- takeScreenshot: incoming-04-forward
|
||||
- tapOn: "Send"
|
||||
- extendedWaitUntil:
|
||||
visible: "Email sent successfully"
|
||||
timeout: 20000
|
||||
optional: true
|
||||
- runFlow:
|
||||
file: ../subflows/assert-delivered.yaml
|
||||
env:
|
||||
SUBJECT: "Fwd: ${output.incoming}"
|
||||
ATTACHMENT: e2e-attachment.pdf
|
||||
- runFlow: ../subflows/close-message.yaml
|
||||
@@ -0,0 +1,27 @@
|
||||
# Search: a hit, a miss, clearing.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Search
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- tapOn: "Search mail..."
|
||||
- inputText: "verification"
|
||||
- pressKey: Enter
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Your verification code.*"
|
||||
timeout: 30000
|
||||
- assertNotVisible: ".*Weekly digest.*"
|
||||
- takeScreenshot: search-01-hit
|
||||
- tapOn: "Clear search"
|
||||
- tapOn: "Search mail..."
|
||||
- inputText: "zzqx-no-such-mail"
|
||||
- pressKey: Enter
|
||||
- extendedWaitUntil:
|
||||
visible: "No results found"
|
||||
timeout: 30000
|
||||
- takeScreenshot: search-02-miss
|
||||
- tapOn: "Clear"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Weekly digest.*"
|
||||
timeout: 30000
|
||||
@@ -0,0 +1,41 @@
|
||||
# Swipe actions with undo: swipe left archives (undo brings it back), swipe
|
||||
# right toggles read.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Swipe actions and undo
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- scrollUntilVisible:
|
||||
element: ".*Grüße aus München.*"
|
||||
direction: DOWN
|
||||
centerElement: true
|
||||
timeout: 20000
|
||||
- swipe:
|
||||
from: ".*Grüße aus München.*"
|
||||
direction: LEFT
|
||||
duration: 400
|
||||
# The undo bar shows for 5 s; when it goes, the Compose button slides into
|
||||
# its place, and a late tap on UNDO opens the composer. So tap at once and
|
||||
# check the rest afterwards.
|
||||
- tapOn:
|
||||
text: "UNDO"
|
||||
waitToSettleTimeoutMs: 500
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Grüße aus München.*"
|
||||
timeout: 15000
|
||||
- takeScreenshot: swipe-02-undone
|
||||
|
||||
- swipe:
|
||||
from: ".*Grüße aus München.*"
|
||||
direction: RIGHT
|
||||
duration: 400
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- takeScreenshot: swipe-03-read-toggled
|
||||
- swipe:
|
||||
from: ".*Grüße aus München.*"
|
||||
direction: RIGHT
|
||||
duration: 400
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
@@ -0,0 +1,85 @@
|
||||
# Message actions with undo: move to a folder, delete to Trash, select and
|
||||
# star from the list.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Move, delete, select
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
|
||||
# Move via More → Move to...
|
||||
- runFlow:
|
||||
file: ../subflows/open-message.yaml
|
||||
env:
|
||||
SUBJECT: Weekly digest
|
||||
- tapOn: "more"
|
||||
- extendedWaitUntil:
|
||||
visible: "More actions"
|
||||
timeout: 10000
|
||||
- takeScreenshot: actions-01-more
|
||||
- tapOn: "Move to..."
|
||||
- extendedWaitUntil:
|
||||
visible: "Move to..."
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "Projects"
|
||||
# The undo bar shows for 5 s: don't spend them waiting for the screen to settle.
|
||||
waitToSettleTimeoutMs: 500
|
||||
- takeScreenshot: actions-02-moved
|
||||
# Tap UNDO at once (see 06-swipe-undo: a late tap opens the composer).
|
||||
- tapOn:
|
||||
text: "UNDO"
|
||||
# The undo bar shows for 5 s: don't spend them waiting for the screen to settle.
|
||||
waitToSettleTimeoutMs: 500
|
||||
- runFlow: ../subflows/close-message.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Weekly digest.*"
|
||||
timeout: 15000
|
||||
|
||||
# Delete (to Trash) and undo
|
||||
- runFlow:
|
||||
file: ../subflows/open-message.yaml
|
||||
env:
|
||||
SUBJECT: Weekly digest
|
||||
- takeScreenshot: actions-03a-before-delete
|
||||
- tapOn:
|
||||
text: "Delete"
|
||||
# The undo bar shows for 5 s: don't spend them waiting for the screen to settle.
|
||||
waitToSettleTimeoutMs: 500
|
||||
- takeScreenshot: actions-03-deleted
|
||||
- tapOn:
|
||||
text: "UNDO"
|
||||
# The undo bar shows for 5 s: don't spend them waiting for the screen to settle.
|
||||
waitToSettleTimeoutMs: 500
|
||||
- runFlow: ../subflows/close-message.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Weekly digest.*"
|
||||
timeout: 15000
|
||||
|
||||
# Select from the list and star, then unstar. On a phone the row can be
|
||||
# below the fold (earlier runs left mail on top).
|
||||
- scrollUntilVisible:
|
||||
element: ".*Grüße aus München.*"
|
||||
direction: DOWN
|
||||
centerElement: true
|
||||
timeout: 20000
|
||||
- longPressOn: ".*Grüße aus München.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Clear selection"
|
||||
timeout: 10000
|
||||
- takeScreenshot: actions-04-selected
|
||||
# Star (or Unstar, when an earlier run left it starred), then toggle back.
|
||||
- tapOn: "Star|Unstar"
|
||||
# A bulk action ends selection mode by itself.
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Clear selection"
|
||||
commands:
|
||||
- tapOn: "Clear selection"
|
||||
- longPressOn: ".*Grüße aus München.*"
|
||||
- tapOn: "Unstar|Star"
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Clear selection"
|
||||
commands:
|
||||
- tapOn: "Clear selection"
|
||||
@@ -0,0 +1,58 @@
|
||||
# Scheduled send: schedule a message for later, find it under Scheduled,
|
||||
# cancel it.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Scheduled send
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- evalScript: ${output.subject = 'E2E scheduled ' + DEVICE + '-' + APPEARANCE + '-' + ORIENTATION + ' ' + Date.now()}
|
||||
- tapOn: "Compose"
|
||||
- extendedWaitUntil:
|
||||
visible: "New Message"
|
||||
timeout: 15000
|
||||
- tapOn: "Recipient email addresses"
|
||||
- inputText: "userb@example.org"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
text: "Subject"
|
||||
index: 1
|
||||
- inputText: ${output.subject}
|
||||
- runFlow:
|
||||
file: ../subflows/compose-body.yaml
|
||||
env:
|
||||
FROM_SUBJECT: "yes"
|
||||
BODY: "Scheduled from the iOS simulator test."
|
||||
- tapOn: "Schedule send"
|
||||
- extendedWaitUntil:
|
||||
visible: "Tomorrow morning.*"
|
||||
timeout: 10000
|
||||
- takeScreenshot: scheduled-01-options
|
||||
- tapOn: "Tomorrow morning.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Scheduled"
|
||||
timeout: 30000
|
||||
- takeScreenshot: scheduled-02-confirmed
|
||||
- tapOn: "OK"
|
||||
|
||||
- runFlow:
|
||||
file: ../subflows/go-to-folder.yaml
|
||||
env:
|
||||
FOLDER: Scheduled
|
||||
- extendedWaitUntil:
|
||||
visible: ".*${output.subject}.*"
|
||||
timeout: 30000
|
||||
- takeScreenshot: scheduled-03-list
|
||||
- tapOn: ".*${output.subject}.*"
|
||||
- tapOn: "Cancel send"
|
||||
- extendedWaitUntil:
|
||||
visible: "Keep scheduled"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "Cancel send"
|
||||
below: "Keep scheduled|.*will not be delivered.*"
|
||||
- extendedWaitUntil:
|
||||
notVisible: ".*${output.subject}.*"
|
||||
timeout: 30000
|
||||
- takeScreenshot: scheduled-04-cancelled
|
||||
- runFlow: ../subflows/go-to-inbox.yaml
|
||||
@@ -0,0 +1,126 @@
|
||||
# Calendar: the views, create an event with a reminder, edit it, delete it.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Calendar
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- evalScript: ${output.event = 'E2E event ' + DEVICE + ' ' + Date.now() % 100000}
|
||||
- tapOn:
|
||||
id: tab-calendar
|
||||
# The first visit asks for notification permission (event reminders).
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Would Like to Send You Notifications.*|Go to today"
|
||||
timeout: 30000
|
||||
- runFlow: ../subflows/allow-notifications.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "Go to today"
|
||||
timeout: 30000
|
||||
- takeScreenshot: calendar-01-month
|
||||
|
||||
- tapOn: "Open menu"
|
||||
- tapOn: "Schedule"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: calendar-02-schedule
|
||||
- tapOn: "Open menu"
|
||||
- tapOn: "Day"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: calendar-03-day
|
||||
- tapOn: "Open menu"
|
||||
- tapOn: "Week"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: calendar-04-week
|
||||
|
||||
# Create
|
||||
- tapOn: "New event"
|
||||
- extendedWaitUntil:
|
||||
visible: "Save"
|
||||
timeout: 10000
|
||||
- tapOn: "Title"
|
||||
- inputText: ${output.event}
|
||||
# Close the keyboard first: a tap while it slides away lands on a form that
|
||||
# is still moving.
|
||||
- pressKey: Enter
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- scrollUntilVisible:
|
||||
element: "Add reminder"
|
||||
direction: DOWN
|
||||
timeout: 10000
|
||||
- repeat:
|
||||
while:
|
||||
notVisible: "15 minutes before"
|
||||
times: 2
|
||||
commands:
|
||||
- tapOn: "Add reminder"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- scrollUntilVisible:
|
||||
element: "15 minutes before"
|
||||
direction: DOWN
|
||||
timeout: 10000
|
||||
- tapOn: "15 minutes before"
|
||||
- takeScreenshot: calendar-05-editor
|
||||
- tapOn: "Save"
|
||||
# The editor closes (its Save goes; the FAB is labelled "New event" too).
|
||||
- extendedWaitUntil:
|
||||
notVisible: "Save"
|
||||
timeout: 20000
|
||||
|
||||
# Find it in the schedule and open it
|
||||
- tapOn: "Open menu"
|
||||
- tapOn: "Schedule"
|
||||
- tapOn: "Go to today"
|
||||
- scrollUntilVisible:
|
||||
element: ".*${output.event}.*"
|
||||
direction: DOWN
|
||||
timeout: 30000
|
||||
- tapOn: ".*${output.event}.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Edit"
|
||||
timeout: 10000
|
||||
- takeScreenshot: calendar-06-detail
|
||||
- assertVisible: ".*15 minutes before.*"
|
||||
|
||||
# Edit: add a location (an empty field, so no cursor games in the title)
|
||||
- tapOn: "Edit"
|
||||
- extendedWaitUntil:
|
||||
visible: "Edit event"
|
||||
timeout: 10000
|
||||
- scrollUntilVisible:
|
||||
element: "Place or address"
|
||||
direction: DOWN
|
||||
timeout: 10000
|
||||
- tapOn: "Place or address"
|
||||
- inputText: "Room 42"
|
||||
- tapOn: "Save"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Room 42.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: calendar-07-edited
|
||||
|
||||
# Delete (the detail sheet is still open after saving)
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: "Delete"
|
||||
commands:
|
||||
- tapOn: ".*${output.event}.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Delete"
|
||||
timeout: 10000
|
||||
- tapOn: "Delete"
|
||||
- extendedWaitUntil:
|
||||
visible: "Delete this event.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "Delete"
|
||||
below: "Delete this event.*"
|
||||
- extendedWaitUntil:
|
||||
notVisible: ".*${output.event}.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: calendar-08-deleted
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
@@ -0,0 +1,70 @@
|
||||
# Contacts: the list, a contact's detail, create one and delete it again.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Contacts
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- evalScript: ${output.last = 'Tester' + Date.now() % 100000}
|
||||
- tapOn:
|
||||
id: tab-contacts
|
||||
- extendedWaitUntil:
|
||||
visible: "All Contacts"
|
||||
timeout: 30000
|
||||
- takeScreenshot: contacts-01-list
|
||||
- tapOn: ".*Alice.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Email"
|
||||
timeout: 10000
|
||||
- takeScreenshot: contacts-02-detail
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Back"
|
||||
commands:
|
||||
- tapOn: "Back"
|
||||
|
||||
- tapOn: "New Contact"
|
||||
- extendedWaitUntil:
|
||||
visible: "Save"
|
||||
timeout: 10000
|
||||
- runFlow:
|
||||
file: ../subflows/type.yaml
|
||||
env:
|
||||
FIELD: contact-first-name
|
||||
TEXT: E2E
|
||||
# Return moves to Middle name, then Last name: tapping the field would hit
|
||||
# the keyboard covering it.
|
||||
- pressKey: Enter
|
||||
- pressKey: Enter
|
||||
- runFlow:
|
||||
file: ../subflows/type.yaml
|
||||
env:
|
||||
FIELD: contact-last-name
|
||||
TAP: "no"
|
||||
TEXT: ${output.last}
|
||||
- takeScreenshot: contacts-03-form
|
||||
- tapOn: "Save"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*${output.last}.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: contacts-04-created
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: "More actions"
|
||||
commands:
|
||||
- tapOn: ".*E2E ${output.last}.*"
|
||||
- tapOn: "More actions"
|
||||
- tapOn: "Delete"
|
||||
- extendedWaitUntil:
|
||||
visible: "Delete contact"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "Delete"
|
||||
below: "Are you sure.*"
|
||||
- extendedWaitUntil:
|
||||
notVisible: ".*E2E ${output.last}.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: contacts-05-deleted
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
@@ -0,0 +1,65 @@
|
||||
# Files: open a folder, preview a text and a PDF file, create and delete a
|
||||
# folder.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Files
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- evalScript: ${output.folder = 'E2E folder ' + Date.now() % 100000}
|
||||
- tapOn:
|
||||
id: tab-files
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Documents.*"
|
||||
timeout: 30000
|
||||
- takeScreenshot: files-01-root
|
||||
- tapOn: ".*Documents.*"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*readme.txt.*"
|
||||
timeout: 20000
|
||||
- tapOn: ".*readme.txt.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "readme.txt"
|
||||
timeout: 20000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: files-02-text-preview
|
||||
- tapOn: "Close"
|
||||
- tapOn: ".*q3-report.pdf.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "q3-report.pdf"
|
||||
timeout: 20000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: files-03-pdf-preview
|
||||
- tapOn: "Close"
|
||||
- tapOn: "Back"
|
||||
|
||||
- tapOn: "New Folder"
|
||||
- tapOn: "Folder name"
|
||||
- inputText: ${output.folder}
|
||||
- tapOn: "Create"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*${output.folder}.*"
|
||||
timeout: 20000
|
||||
- takeScreenshot: files-04-folder-created
|
||||
- longPressOn: ".*${output.folder}.*"
|
||||
- repeat:
|
||||
while:
|
||||
notVisible: "Delete this item.*"
|
||||
times: 3
|
||||
commands:
|
||||
- tapOn: "Delete"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- extendedWaitUntil:
|
||||
visible: "Delete this item.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "Delete"
|
||||
below: "Delete this item.*"
|
||||
- extendedWaitUntil:
|
||||
notVisible: ".*${output.folder}.*"
|
||||
timeout: 20000
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
@@ -0,0 +1,38 @@
|
||||
# Every settings page opens and shows its content.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Settings pages
|
||||
tags:
|
||||
- features
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
- tapOn:
|
||||
id: tab-settings
|
||||
- extendedWaitUntil:
|
||||
visible: "Search settings"
|
||||
timeout: 20000
|
||||
- takeScreenshot: settings-00-list
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: account, ROW: ".*@example.org, .*", PROOF: "Logged-in accounts|Display Name"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: language, ROW: "Language, Region & Time, .*", Q: "Language", PROOF: "Date Format"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: notifications, ROW: "Notifications, .*", Q: "Notifications", PROOF: "Email Notifications"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: appearance, ROW: "Appearance, .*", Q: "Appearance", PROOF: "Choose your preferred color scheme"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: layout, ROW: "Layout, .*", Q: "Layout", PROOF: "Tune the email list interactions for mobile."}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: reading, ROW: "Reading, .*", Q: "Reading", PROOF: "Mark as Read"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: composing, ROW: "Composing, .*", Q: "Composing", PROOF: "Composer"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: identities, ROW: "Identities, .*", Q: "Identities", PROOF: "Sending Identities"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: vacation, ROW: "Out of Office, .*", Q: "Office", PROOF: "Reply automatically"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: filters, ROW: "Filters, .*", Q: "Filters", PROOF: "Email Filters"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: templates, ROW: "Templates, .*", Q: "Templates", PROOF: "Email Templates"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: folders, ROW: "Folders, .*", Q: "Folders", PROOF: ".*Inbox.*"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: tags, ROW: "Tags, .*", Q: "Tags", PROOF: "Email Tags"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: downloads, ROW: "Downloads, .*", Q: "Downloads", PROOF: "Email .*eml.* filename"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: security, ROW: "Security, .*", Q: "Security", PROOF: "Account Security|Detecting server capabilities.*"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: content, ROW: "Content & Senders, .*", Q: "Content", PROOF: "External Content"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: calendar, ROW: "Calendar, .*", Q: "Calendar", PROOF: "Default view"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: contacts, ROW: "Contacts, .*", Q: "Contacts", PROOF: "Group by first letter"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: files, ROW: "Files, .*", Q: "Files", PROOF: "Default View"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: sidebar-apps, ROW: "Sidebar Apps, .*", Q: "Sidebar", PROOF: "Keep Apps Loaded"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: themes, ROW: "Themes, .*", Q: "Themes", PROOF: "The default light and dark palettes."}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: storage, ROW: "Storage & backup, .*", Q: "Storage", PROOF: "Offline mail.*"}}
|
||||
- runFlow: {file: ../subflows/settings-page.yaml, env: {NAME: about, ROW: "About, .*", Q: "About", PROOF: "This device"}}
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
@@ -0,0 +1,30 @@
|
||||
# Fresh install: the app starts and shows the sign-in choices, and typing
|
||||
# into a field works. Needs no server, so it runs before every suite as the
|
||||
# warm-up for the XCTest driver. It types only a made-up address, so its
|
||||
# debug log is safe to print.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Launch to sign-in
|
||||
tags:
|
||||
- smoke
|
||||
---
|
||||
- launchApp:
|
||||
clearState: true
|
||||
clearKeychain: true
|
||||
# A cold start of a release build on a CI simulator can be slow.
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually"
|
||||
timeout: 120000
|
||||
- takeScreenshot: warmup-01-sign-in-choices
|
||||
- tapOn: "Enter server details manually"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: login-server-input
|
||||
timeout: 10000
|
||||
- runFlow: subflows/dismiss-keyboard-tip.yaml
|
||||
- runFlow:
|
||||
file: subflows/type.yaml
|
||||
env:
|
||||
FIELD: login-server-input
|
||||
TAP: "no"
|
||||
TEXT: typing-probe.example.net
|
||||
- takeScreenshot: warmup-02-typed
|
||||
@@ -0,0 +1,27 @@
|
||||
# Perf: open a message and go back, five times. The app is already running
|
||||
# with -perfMarks YES (no launchApp here: it would drop the launch argument).
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Perf open message
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: email-row
|
||||
timeout: 60000
|
||||
- repeat:
|
||||
times: 5
|
||||
commands:
|
||||
- tapOn:
|
||||
id: email-row
|
||||
index: 1
|
||||
- extendedWaitUntil:
|
||||
visible: "Reply All"
|
||||
timeout: 30000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Back"
|
||||
commands:
|
||||
- tapOn: "Back"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
@@ -0,0 +1,33 @@
|
||||
# Perf: switch through the tabs, three times.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Perf tab switches
|
||||
---
|
||||
- repeat:
|
||||
times: 3
|
||||
commands:
|
||||
- tapOn:
|
||||
id: tab-calendar
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ".*Would Like to Send You Notifications.*"
|
||||
commands:
|
||||
- tapOn: "Allow"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- tapOn:
|
||||
id: tab-contacts
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- takeScreenshot: perf-contacts
|
||||
- tapOn:
|
||||
id: tab-files
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- tapOn:
|
||||
id: tab-settings
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
@@ -0,0 +1,23 @@
|
||||
# Perf: open the composer and close it untouched, five times.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Perf compose open
|
||||
---
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
- repeat:
|
||||
times: 5
|
||||
commands:
|
||||
- tapOn: "Compose"
|
||||
- extendedWaitUntil:
|
||||
visible: "New Message"
|
||||
timeout: 20000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- tapOn: "Close"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Discard"
|
||||
commands:
|
||||
- tapOn: "Discard"
|
||||
@@ -0,0 +1,28 @@
|
||||
# Perf: fling through the long inbox (thousands of messages), then back up.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Perf list scroll
|
||||
---
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: email-row
|
||||
timeout: 30000
|
||||
- repeat:
|
||||
times: 8
|
||||
commands:
|
||||
- swipe:
|
||||
start: 50%,80%
|
||||
end: 50%,20%
|
||||
duration: 250
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 4000
|
||||
- repeat:
|
||||
times: 8
|
||||
commands:
|
||||
- swipe:
|
||||
start: 50%,20%
|
||||
end: 50%,80%
|
||||
duration: 250
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 4000
|
||||
@@ -0,0 +1,11 @@
|
||||
# Performance runs: sign in once (ACCOUNT, the bulk account with thousands of
|
||||
# messages). Later installs of other builds keep the data and the keychain.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Perf sign-in
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/password-sign-in.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
INBOX_TEXT: ".*Bulk.*"
|
||||
@@ -0,0 +1,79 @@
|
||||
# Push on iOS: the relay's payload carries only ids and a placeholder text.
|
||||
# A tap opens the message, with the app running and from a cold start. The
|
||||
# helper sends the message as userb and pushes the relay's payload with
|
||||
# simctl, so this needs no APNs key.
|
||||
#
|
||||
# simctl push hands the payload to the simulator as a local notification
|
||||
# request: the notification service extension (plugins/ios-push), which
|
||||
# puts sender and subject in, never runs that way (the log shows no
|
||||
# NotificationService process). So the banner shows the placeholder here;
|
||||
# the extension needs a real APNs push to be tested.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Push notification on iOS
|
||||
tags:
|
||||
- push
|
||||
---
|
||||
- runFlow: ../subflows/signed-in.yaml
|
||||
# Without notification permission iOS drops the push silently, and the
|
||||
# launch-time grant doesn't reach it on these simulators: let the app ask
|
||||
# (the Calendar tab does, for event reminders) and allow.
|
||||
- tapOn:
|
||||
id: tab-calendar
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Would Like to Send You Notifications.*|Go to today"
|
||||
timeout: 30000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ".*Would Like to Send You Notifications.*"
|
||||
commands:
|
||||
- tapOn:
|
||||
text: "Allow"
|
||||
optional: true
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- pressKey: Home
|
||||
|
||||
# 1. The banner shows (and what it says).
|
||||
- evalScript: ${output.subject0 = 'E2E push banner ' + Date.now()}
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /simpush?to=${ACCOUNT}&subject=${encodeURIComponent(output.subject0)}
|
||||
- extendedWaitUntil:
|
||||
visible: ".*${output.subject0}.*|.*New message.*"
|
||||
timeout: 30000
|
||||
- takeScreenshot: push-01-banner
|
||||
# Let it go, so the next tap can't hit this one.
|
||||
- extendedWaitUntil:
|
||||
notVisible: ".*${output.subject0}.*|.*New message.*"
|
||||
timeout: 20000
|
||||
|
||||
# 2. A tap opens the message, app in the background. The banner shows for a
|
||||
# few seconds and finding it takes Maestro about as long; taps on the lock
|
||||
# screen list do nothing on these simulators. So tap where the banner
|
||||
# appears, right after the push (push-and-tap.yaml).
|
||||
- evalScript: ${output.subject = 'E2E push ' + Date.now()}
|
||||
- runFlow:
|
||||
file: ../subflows/push-and-tap.yaml
|
||||
env:
|
||||
SUBJECT: ${output.subject}
|
||||
- extendedWaitUntil:
|
||||
visible: "Reply All"
|
||||
timeout: 60000
|
||||
- assertVisible: ".*${output.subject}.*"
|
||||
- takeScreenshot: push-02-opened-warm
|
||||
|
||||
# 3. The same from a cold start: the app isn't running.
|
||||
- stopApp
|
||||
- evalScript: ${output.subject2 = 'E2E push cold ' + Date.now()}
|
||||
- runFlow:
|
||||
file: ../subflows/push-and-tap.yaml
|
||||
env:
|
||||
SUBJECT: ${output.subject2}
|
||||
- extendedWaitUntil:
|
||||
visible: "Reply All"
|
||||
timeout: 90000
|
||||
- assertVisible: ".*${output.subject2}.*"
|
||||
- takeScreenshot: push-04-opened-cold
|
||||
@@ -0,0 +1,8 @@
|
||||
// Calls the test helper (scripts/e2e/helper.mjs) on the runner.
|
||||
// env CALL: path and query, e.g. "/totp" or "/pair?user=x&deliver=openurl".
|
||||
// The answer lands in output.helper.
|
||||
const response = http.get(HELPER + CALL);
|
||||
if (!response.ok) {
|
||||
throw new Error('helper ' + CALL.split('?')[0] + ' failed: ' + response.status + ' ' + response.body);
|
||||
}
|
||||
output.helper = json(response.body);
|
||||
@@ -0,0 +1,32 @@
|
||||
# Password sign-in as the first account, sign out, sign in again.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Password, sign out, sign in again
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- takeScreenshot: password-01-choices
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${SERVER}
|
||||
- takeScreenshot: password-02-confirm
|
||||
- tapOn: "Sign in with a password instead"
|
||||
- runFlow:
|
||||
file: ../subflows/password-form.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: password-03-inbox
|
||||
|
||||
- runFlow: ../subflows/sign-out.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually"
|
||||
timeout: 30000
|
||||
- takeScreenshot: password-04-signed-out
|
||||
|
||||
- runFlow:
|
||||
file: ../subflows/password-sign-in.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- takeScreenshot: password-05-signed-in-again
|
||||
@@ -0,0 +1,58 @@
|
||||
# A wrong password and an unreachable server: the app says what went wrong
|
||||
# and stays on the form.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Password errors
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${SERVER}
|
||||
- tapOn: "Sign in with a password instead"
|
||||
- runFlow:
|
||||
file: ../subflows/password-form.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
PASSWORD: wrong-password-123
|
||||
- extendedWaitUntil:
|
||||
visible: "That didn't work"
|
||||
timeout: 30000
|
||||
- takeScreenshot: errors-01-wrong-password
|
||||
- assertVisible:
|
||||
id: login-password-input
|
||||
|
||||
# Back to the server step, then a host that doesn't resolve.
|
||||
- tapOn: "Back"
|
||||
- tapOn: "Back"
|
||||
- runFlow:
|
||||
file: ../subflows/type.yaml
|
||||
env:
|
||||
FIELD: login-server-input
|
||||
TAP: "no"
|
||||
ERASE: "yes"
|
||||
TEXT: mail.does-not-exist.invalid
|
||||
- tapOn: "Continue"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
- tapOn: "Sign in with a password instead"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: login-password-input
|
||||
timeout: 10000
|
||||
- runFlow:
|
||||
file: ../subflows/type-secret.yaml
|
||||
env:
|
||||
FIELD: login-password-input
|
||||
# The email is filled in, so the password field focuses itself.
|
||||
TAP: "no"
|
||||
TEXT: ${PASSWORD}
|
||||
- pressKey: Enter
|
||||
- extendedWaitUntil:
|
||||
visible: "Can't reach mail.does-not-exist.invalid|No mail server at mail.does-not-exist.invalid|Sign-in failed"
|
||||
timeout: 60000
|
||||
- takeScreenshot: errors-02-unreachable
|
||||
- assertVisible:
|
||||
id: login-password-input
|
||||
@@ -0,0 +1,24 @@
|
||||
# A TOTP account signs in with a password: Stalwart answers 402 for the right
|
||||
# password, and the app asks for the code.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Password with TOTP
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${SERVER}
|
||||
- tapOn: "Sign in with a password instead"
|
||||
- runFlow:
|
||||
file: ../subflows/password-form.yaml
|
||||
env:
|
||||
LOGIN: ${TOTP_ACCOUNT}@example.org
|
||||
- extendedWaitUntil:
|
||||
visible: "Authentication code"
|
||||
timeout: 30000
|
||||
- takeScreenshot: totp-01-code-asked
|
||||
- runFlow: ../subflows/two-factor.yaml
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: totp-02-inbox
|
||||
@@ -0,0 +1,20 @@
|
||||
# OAuth with Stalwart's own authorization server: the confirm step's
|
||||
# Continue opens an ASWebAuthenticationSession on Stalwart's sign-in page.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: OAuth (Stalwart)
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${SERVER}
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/stalwart-login-page.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: oauth-01-inbox
|
||||
@@ -0,0 +1,39 @@
|
||||
# Backing out of OAuth: Cancel on the system alert, and closing the sheet
|
||||
# halfway. Both leave the user on the confirm step without an error.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: OAuth cancelled
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${SERVER}
|
||||
- tapOn: "Continue"
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Wants to Use.*to Sign In.*"
|
||||
timeout: 30000
|
||||
- tapOn:
|
||||
text: "Cancel"
|
||||
below: ".*Wants to Use.*to Sign In.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
- assertNotVisible: "Sign-in failed"
|
||||
- assertNotVisible: "Sign-in was interrupted"
|
||||
- takeScreenshot: oauth-cancel-01-alert-cancelled
|
||||
|
||||
# Accept the alert, then close the sheet on Stalwart's page.
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "you@example.com"
|
||||
timeout: 60000
|
||||
- tapOn: "Cancel"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
- assertNotVisible: "Sign-in failed"
|
||||
- assertNotVisible: "Sign-in was interrupted"
|
||||
- takeScreenshot: oauth-cancel-02-sheet-closed
|
||||
@@ -0,0 +1,20 @@
|
||||
# Sign in via webmail: the confirm step on a Bulwark webmail address hands
|
||||
# off to the webmail's sign-in page in an auth session.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Webmail hand-off
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${WEBMAIL}
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/webmail-login-page.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: webmail-01-inbox
|
||||
@@ -0,0 +1,24 @@
|
||||
# Webmail hand-off for a TOTP account: the webmail asks for the code; if it
|
||||
# hands back the password, the app finishes with a code of its own. Uses the
|
||||
# beta webmail: the stable one still has 06's session in the browser, which
|
||||
# strands the hand-off (see 20-webmail-session.yaml).
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Webmail hand-off with TOTP
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${WEBMAIL_BETA}
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/webmail-login-page.yaml
|
||||
env:
|
||||
LOGIN: ${TOTP_ACCOUNT}@example.org
|
||||
TOTP: "yes"
|
||||
- runFlow: ../subflows/finish-two-factor-if-asked.yaml
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: webmail-totp-01-inbox
|
||||
@@ -0,0 +1,17 @@
|
||||
# Sign-in link from another app (simctl openurl), password account. The
|
||||
# helper signs in to the test webmail, creates a fresh "Link mobile app" code
|
||||
# and opens its bulwarkmail://pair link on the simulator.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Sign-in link from another app
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /pair?user=${ACCOUNT}&deliver=openurl
|
||||
- runFlow: ../subflows/confirm-link.yaml
|
||||
- runFlow: ../subflows/finish-two-factor-if-asked.yaml
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: link-password-01-inbox
|
||||
@@ -0,0 +1,16 @@
|
||||
# "Paste sign-in link" with a fresh pairing link on the clipboard (simctl
|
||||
# pbcopy), password account.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Paste sign-in link
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /pair?user=${ACCOUNT}&deliver=pbcopy
|
||||
- runFlow: ../subflows/paste-link.yaml
|
||||
- runFlow: ../subflows/finish-two-factor-if-asked.yaml
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: paste-password-01-inbox
|
||||
@@ -0,0 +1,15 @@
|
||||
# Sign-in link from another app for a TOTP account.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Sign-in link, TOTP account
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /pair?user=${TOTP_ACCOUNT}&deliver=openurl
|
||||
- runFlow: ../subflows/confirm-link.yaml
|
||||
- runFlow: ../subflows/finish-two-factor-if-asked.yaml
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: link-totp-01-inbox
|
||||
@@ -0,0 +1,15 @@
|
||||
# "Paste sign-in link" for a TOTP account.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Paste sign-in link, TOTP account
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /pair?user=${TOTP_ACCOUNT}&deliver=pbcopy
|
||||
- runFlow: ../subflows/paste-link.yaml
|
||||
- runFlow: ../subflows/finish-two-factor-if-asked.yaml
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- takeScreenshot: paste-totp-01-inbox
|
||||
@@ -0,0 +1,21 @@
|
||||
# An expired code (codes live 2 minutes): the helper creates one now and
|
||||
# opens it after 130 s. The app says so and stays on the sign-in screen.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Expired sign-in code
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /pair-later?user=${ACCOUNT}&deliver=openurl&delay=130
|
||||
- runFlow:
|
||||
file: ../subflows/confirm-link.yaml
|
||||
env:
|
||||
LINK_WAIT: 200000
|
||||
- extendedWaitUntil:
|
||||
visible: "That code has expired.*"
|
||||
timeout: 30000
|
||||
- takeScreenshot: link-expired-01-notice
|
||||
- assertVisible: "Enter server details manually"
|
||||
@@ -0,0 +1,44 @@
|
||||
# A code that was already used: sign in with it, sign out, open the same link
|
||||
# again (same app process, then after a restart, which asks the server).
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Used sign-in code
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /pair?user=${ACCOUNT}&deliver=openurl
|
||||
- runFlow: ../subflows/confirm-link.yaml
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- runFlow: ../subflows/sign-out.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually"
|
||||
timeout: 30000
|
||||
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /redeliver?user=${ACCOUNT}&deliver=openurl
|
||||
- runFlow: ../subflows/confirm-link.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "That code was already used|That code has expired or was already used"
|
||||
timeout: 30000
|
||||
- takeScreenshot: link-used-01-same-process
|
||||
|
||||
- stopApp
|
||||
- launchApp
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually"
|
||||
timeout: 60000
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /redeliver?user=${ACCOUNT}&deliver=openurl
|
||||
- runFlow: ../subflows/confirm-link.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "That code was already used|That code has expired or was already used"
|
||||
timeout: 30000
|
||||
- takeScreenshot: link-used-02-after-restart
|
||||
- assertVisible: "Enter server details manually"
|
||||
@@ -0,0 +1,32 @@
|
||||
# The QR scanner with camera access denied, then allowed (the simulator has
|
||||
# no camera, so nothing can be scanned). Neither may crash the app.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Scanner without camera
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow:
|
||||
file: ../subflows/start.yaml
|
||||
env:
|
||||
CAMERA: deny
|
||||
- tapOn: "Scan a sign-in code.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Camera access is.*|Scanning a sign-in QR code uses the camera.*"
|
||||
timeout: 15000
|
||||
- takeScreenshot: scanner-01-denied
|
||||
- tapOn: "Close"
|
||||
- assertVisible: "Enter server details manually"
|
||||
|
||||
- runFlow:
|
||||
file: ../subflows/start.yaml
|
||||
env:
|
||||
CAMERA: allow
|
||||
- tapOn: "Scan a sign-in code.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign-in code"
|
||||
timeout: 15000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- takeScreenshot: scanner-02-allowed
|
||||
- tapOn: "Close"
|
||||
- assertVisible: "Enter server details manually"
|
||||
@@ -0,0 +1,23 @@
|
||||
# "Use my email address": example.org has no autodiscovery, so the app moves
|
||||
# on to the server step and names the domain.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Email address discovery
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- tapOn: "Use my email address.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "What's your email address."
|
||||
timeout: 10000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- inputText: ${ACCOUNT}@example.org
|
||||
- assertVisible: ${ACCOUNT}@example.org
|
||||
- tapOn: "Continue"
|
||||
- extendedWaitUntil:
|
||||
visible: "We couldn't find a server for example.org.*"
|
||||
timeout: 60000
|
||||
- takeScreenshot: discovery-01-not-found
|
||||
- assertVisible:
|
||||
id: login-server-input
|
||||
@@ -0,0 +1,29 @@
|
||||
# Second account by password via "Another account on <host>", then switch
|
||||
# between the two.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Add account, password
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/password-sign-in.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow: ../subflows/open-add-account.yaml
|
||||
- takeScreenshot: add-password-01-choices
|
||||
- tapOn: "Another account on .*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
- tapOn: "Sign in with a password instead"
|
||||
- runFlow:
|
||||
file: ../subflows/password-form.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT2}@example.org
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/check-active-account.yaml
|
||||
env:
|
||||
ACTIVE: ${ACCOUNT2}@example.org
|
||||
- takeScreenshot: add-password-02-two-accounts
|
||||
@@ -0,0 +1,41 @@
|
||||
# Second account by OAuth. Adding uses an ephemeral auth session, so the
|
||||
# first account's Stalwart sign-in must not be reused.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Add account, OAuth
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${SERVER}
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/stalwart-login-page.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
|
||||
- runFlow: ../subflows/open-add-account.yaml
|
||||
- tapOn: "Another account on .*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
- tapOn: "Continue"
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ".*Wants to Use.*to Sign In.*"
|
||||
commands:
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/stalwart-login-page.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT2}@example.org
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/check-active-account.yaml
|
||||
env:
|
||||
ACTIVE: ${ACCOUNT2}@example.org
|
||||
- takeScreenshot: add-oauth-01-two-accounts
|
||||
@@ -0,0 +1,43 @@
|
||||
# Second account via the webmail hand-off, on top of a password account.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Add account, webmail
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/password-sign-in.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow: ../subflows/open-add-account.yaml
|
||||
- tapOn: "An account somewhere else.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "What's the other address."
|
||||
timeout: 10000
|
||||
- tapOn: "I know my server address"
|
||||
- runFlow:
|
||||
file: ../subflows/type.yaml
|
||||
env:
|
||||
FIELD: login-server-input
|
||||
TAP: "no"
|
||||
TEXT: ${WEBMAIL}
|
||||
- tapOn: "Continue"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
- tapOn: "Continue"
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ".*Wants to Use.*to Sign In.*"
|
||||
commands:
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/webmail-login-page.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT2}@example.org
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/check-active-account.yaml
|
||||
env:
|
||||
ACTIVE: ${ACCOUNT2}@example.org
|
||||
- takeScreenshot: add-webmail-01-two-accounts
|
||||
@@ -0,0 +1,37 @@
|
||||
# Second account from a sign-in link opened by another app while signed in.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Add account, sign-in link
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/password-sign-in.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /pair?user=${ACCOUNT2}&deliver=openurl
|
||||
- extendedWaitUntil:
|
||||
visible: "Open in .*Bulwark Mobile.*|Sign in with a link."
|
||||
timeout: 60000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Open in .*Bulwark Mobile.*"
|
||||
commands:
|
||||
- tapOn: "Open"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a link."
|
||||
timeout: 20000
|
||||
- takeScreenshot: add-link-01-prompt
|
||||
- assertVisible: "This link adds an account.*"
|
||||
- tapOn:
|
||||
text: "Continue"
|
||||
below: "Sign in with a link."
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/check-active-account.yaml
|
||||
env:
|
||||
ACTIVE: ${ACCOUNT2}@example.org
|
||||
- takeScreenshot: add-link-02-two-accounts
|
||||
@@ -0,0 +1,52 @@
|
||||
# Sign in via webmail while the browser still has a webmail session (from
|
||||
# 06-webmail, or from using the webmail in Safari): the hand-off must still
|
||||
# reach the app. Before the fix, the webmail's login page redirected an
|
||||
# existing session to its own inbox inside the sign-in sheet, and the app
|
||||
# never got the sign-in.
|
||||
appId: org.bulwarkmail.mobile
|
||||
name: Webmail hand-off with an existing webmail session
|
||||
tags:
|
||||
- signin
|
||||
---
|
||||
# Leaves a webmail session in the shared browser cookies.
|
||||
- runFlow: ../subflows/start.yaml
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${WEBMAIL}
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "user@example.com|Hand over to the app|.*Inbox.*"
|
||||
timeout: 60000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "user@example.com"
|
||||
commands:
|
||||
- runFlow:
|
||||
file: ../subflows/webmail-login-page.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Hand over to the app"
|
||||
commands:
|
||||
- tapOn: "Hand over to the app"
|
||||
- runFlow: ../subflows/inbox.yaml
|
||||
- runFlow: ../subflows/sign-out.yaml
|
||||
|
||||
# Again: the webmail now knows the user.
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually"
|
||||
timeout: 30000
|
||||
- runFlow:
|
||||
file: ../subflows/manual-server.yaml
|
||||
env:
|
||||
HOST: ${WEBMAIL}
|
||||
- tapOn: "Continue"
|
||||
- runFlow: ../subflows/allow-auth-session.yaml
|
||||
- extendedWaitUntil:
|
||||
visible: "user@example.com|Hand over to the app|Search mail.*"
|
||||
timeout: 60000
|
||||
- takeScreenshot: webmail-session-01-second-visit
|
||||
- assertNotVisible: "Search mail.*"
|
||||
@@ -0,0 +1,11 @@
|
||||
# iOS asks before an app opens a sign-in session in the system browser:
|
||||
# "“Bulwark Mobile” Wants to Use “<domain>” to Sign In". Accept it.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Wants to Use.*to Sign In.*"
|
||||
timeout: 30000
|
||||
- takeScreenshot: auth-session-alert
|
||||
- tapOn:
|
||||
text: "Continue"
|
||||
below: ".*Wants to Use.*to Sign In.*"
|
||||
@@ -0,0 +1,21 @@
|
||||
# The app asks for notification permission now and then (calendar
|
||||
# reminders, push). The system alert covers the whole app, so XCTest sees
|
||||
# nothing else until it is answered: allow it when it is up. Taps on system
|
||||
# alerts miss in landscape, so answer it in portrait.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ".*Would Like to Send You Notifications.*"
|
||||
commands:
|
||||
- setOrientation: PORTRAIT
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- tapOn:
|
||||
text: "Allow"
|
||||
optional: true
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${ORIENTATION == 'landscape'}
|
||||
commands:
|
||||
- setOrientation: LANDSCAPE_LEFT
|
||||
@@ -0,0 +1,18 @@
|
||||
# The helper checks over JMAP that AS (default userb) received SUBJECT; with
|
||||
# ATTACHMENT set, that the message carries that file.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /find?as=${AS || 'userb'}&subject=${encodeURIComponent(SUBJECT)}&timeout=60
|
||||
- assertTrue:
|
||||
condition: ${output.helper.found}
|
||||
label: "${SUBJECT} delivered to ${AS || 'userb'}"
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${!!ATTACHMENT}
|
||||
commands:
|
||||
- assertTrue:
|
||||
condition: ${output.helper.attachments.indexOf(ATTACHMENT) >= 0}
|
||||
label: "${SUBJECT} carries ${ATTACHMENT}"
|
||||
@@ -0,0 +1,17 @@
|
||||
# The Settings account card names the active account ACTIVE, and the Account
|
||||
# page lists another account to switch to. Goes back to Mail afterwards.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- tapOn:
|
||||
id: tab-settings
|
||||
- extendedWaitUntil:
|
||||
visible: ".*${ACTIVE}.*"
|
||||
timeout: 20000
|
||||
- tapOn: ".*${ACTIVE}.*"
|
||||
- scrollUntilVisible:
|
||||
element: "Switch to this account"
|
||||
direction: DOWN
|
||||
timeout: 20000
|
||||
- takeScreenshot: accounts-${ACTIVE}
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
@@ -0,0 +1,11 @@
|
||||
# Empty the field with id FIELD. A tap puts the cursor where it lands and
|
||||
# eraseText only deletes before the cursor, so erase in passes: each tap
|
||||
# lands in the middle of what is left.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- repeat:
|
||||
times: 4
|
||||
commands:
|
||||
- tapOn:
|
||||
id: ${FIELD}
|
||||
- eraseText: 100
|
||||
@@ -0,0 +1,11 @@
|
||||
# Leave the message view: Back on a phone; the split layout keeps the list.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Back"
|
||||
commands:
|
||||
- tapOn: "Back"
|
||||
# A tap during the transition lands on stale coordinates.
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
@@ -0,0 +1,41 @@
|
||||
# In the composer: type BODY into the editor. The rich editor is a WebView
|
||||
# whose placeholder is CSS, so it may not be exposed; then tap the middle of
|
||||
# the screen, which is the body area below Subject (above the keyboard).
|
||||
# FROM_SUBJECT=yes: the Subject field has the focus; Return there moves it
|
||||
# to the body (an iPad in landscape shows hardly any body with the keyboard
|
||||
# up, so there is nothing to tap).
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${FROM_SUBJECT == 'yes'}
|
||||
commands:
|
||||
- pressKey: Enter
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 2000
|
||||
- inputText: ${BODY}
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${FROM_SUBJECT != 'yes'}
|
||||
commands:
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Write your message.*"
|
||||
commands:
|
||||
- tapOn: "Write your message.*"
|
||||
# A tap on the WebView doesn't always take the focus from the Subject field
|
||||
# (on the iPad the body once went into Subject); a second tap does.
|
||||
# (hideKeyboard fails here: the composer's keyboard has no dismiss key.)
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: "Write your message.*"
|
||||
commands:
|
||||
- tapOn:
|
||||
point: "50%,50%"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 2000
|
||||
- tapOn:
|
||||
point: "50%,50%"
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- inputText: ${BODY}
|
||||
@@ -0,0 +1,20 @@
|
||||
# A sign-in link from another app asks first. Continue.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
# simctl openurl acts like a link from another app: iOS asks first.
|
||||
- extendedWaitUntil:
|
||||
visible: "Open in .*Bulwark Mobile.*|Sign in with a link.|Open the sign-in page of.*"
|
||||
timeout: ${LINK_WAIT || 60000}
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Open in .*Bulwark Mobile.*"
|
||||
commands:
|
||||
- tapOn: "Open"
|
||||
# Right after a sign-out the app can take a while to get to the link.
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a link.|Open the sign-in page of.*"
|
||||
timeout: 60000
|
||||
- takeScreenshot: link-confirm-prompt
|
||||
- tapOn:
|
||||
text: "Continue"
|
||||
below: "Sign in with a link."
|
||||
@@ -0,0 +1,12 @@
|
||||
# iOS shows a one-time slide-to-type tip over the keyboard on a fresh
|
||||
# simulator. The workflow turns it off, but dismiss it if it still shows up.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Speed up your typing by sliding.*"
|
||||
commands:
|
||||
# The sign-in screen has its own Continue button; take the tip's.
|
||||
- tapOn:
|
||||
text: "Continue"
|
||||
below: "Speed up your typing by sliding.*"
|
||||
@@ -0,0 +1,12 @@
|
||||
# Some routes hand the app a password for a TOTP account; the app then asks
|
||||
# for a code itself. Answer it when it shows up.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: "Authentication code|Inbox"
|
||||
timeout: 90000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Authentication code"
|
||||
commands:
|
||||
- runFlow: two-factor.yaml
|
||||
@@ -0,0 +1,9 @@
|
||||
# Open folder FOLDER from the drawer or docked sidebar.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow: open-drawer.yaml
|
||||
# A folder row's label joins its texts: "<name>, <unread>, /, <total>".
|
||||
- tapOn: ${FOLDER}([ ,].*)?
|
||||
- extendedWaitUntil:
|
||||
visible: ${FOLDER}
|
||||
timeout: 15000
|
||||
@@ -0,0 +1,16 @@
|
||||
# Back to the inbox list, from wherever in the Mail tab.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Back"
|
||||
commands:
|
||||
- tapOn: "Back"
|
||||
- runFlow:
|
||||
file: go-to-folder.yaml
|
||||
env:
|
||||
FOLDER: Inbox
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: email-row
|
||||
timeout: 60000
|
||||
@@ -0,0 +1,26 @@
|
||||
# Signed in: the tab bar is there and the inbox shows messages. Dismisses the
|
||||
# one-time prompts on the way.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: tab-mail
|
||||
timeout: 90000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Not now"
|
||||
commands:
|
||||
- tapOn: "Not now"
|
||||
# Adding an account from Settings returns to Settings.
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: email-row
|
||||
timeout: 60000
|
||||
# Rows are one element: sender, subject and preview together. Mail the
|
||||
# tests sent earlier can push the seeded row below the fold.
|
||||
- scrollUntilVisible:
|
||||
element: ${INBOX_TEXT || '.*Your verification code.*'}
|
||||
direction: DOWN
|
||||
timeout: 30000
|
||||
@@ -0,0 +1,31 @@
|
||||
# From the sign-in choices: enter a server address by hand and continue to the
|
||||
# confirm step. env HOST: the address as a user types it.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- tapOn: "Enter server details manually"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: login-server-input
|
||||
timeout: 10000
|
||||
- runFlow:
|
||||
file: type.yaml
|
||||
env:
|
||||
FIELD: login-server-input
|
||||
TAP: "no"
|
||||
TEXT: ${HOST}
|
||||
- tapOn: "Continue"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
optional: true
|
||||
# Once in a while the tap doesn't reach the button (it moves as the keyboard
|
||||
# goes away): tap again if the form is still there.
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
id: login-server-input
|
||||
commands:
|
||||
- tapOn: "Continue"
|
||||
- extendedWaitUntil:
|
||||
visible: "Sign in with a password instead"
|
||||
timeout: 20000
|
||||
@@ -0,0 +1,19 @@
|
||||
# Signed in: Settings → account card → Account page → Add account, up to the
|
||||
# add-mode choices. (On the iPad split layout the Account page is already
|
||||
# open; tapping the card again is harmless.)
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- tapOn:
|
||||
id: tab-settings
|
||||
- extendedWaitUntil:
|
||||
visible: ".*@example.org, .*"
|
||||
timeout: 10000
|
||||
- tapOn: ".*@example.org, .*"
|
||||
- scrollUntilVisible:
|
||||
element: "Add account"
|
||||
direction: DOWN
|
||||
timeout: 20000
|
||||
- tapOn: "Add account"
|
||||
- extendedWaitUntil:
|
||||
visible: "Add another account"
|
||||
timeout: 10000
|
||||
@@ -0,0 +1,24 @@
|
||||
# Show the folder list: the modal drawer, or nothing to do where the sidebar
|
||||
# is docked (iPad landscape).
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Toggle menu"
|
||||
commands:
|
||||
- tapOn: "Toggle menu"
|
||||
- extendedWaitUntil:
|
||||
visible: "Scheduled.*"
|
||||
timeout: 5000
|
||||
optional: true
|
||||
# Once more if the first tap went to a list still settling.
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: "Scheduled.*"
|
||||
commands:
|
||||
- tapOn: "Toggle menu"
|
||||
- extendedWaitUntil:
|
||||
visible: "Scheduled.*"
|
||||
timeout: 10000
|
||||
@@ -0,0 +1,36 @@
|
||||
# From a message list: open the message whose row mentions SUBJECT.
|
||||
# A row near the bottom can count as visible while the phone's tab bar
|
||||
# covers it (the tap then opens another tab): on the phone, scroll it to the
|
||||
# middle. The iPad's list is a column Maestro's scroll (in the middle of the
|
||||
# screen) doesn't reach; its rows fit on screen.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE != 'tablet'}
|
||||
commands:
|
||||
- scrollUntilVisible:
|
||||
element: ".*${SUBJECT}.*"
|
||||
direction: DOWN
|
||||
centerElement: true
|
||||
visibilityPercentage: 100
|
||||
timeout: 30000
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE == 'tablet'}
|
||||
commands:
|
||||
- scrollUntilVisible:
|
||||
element: ".*${SUBJECT}.*"
|
||||
direction: DOWN
|
||||
timeout: 30000
|
||||
# A tap while the list still moves lands on another row.
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- tapOn: ".*${SUBJECT}.*"
|
||||
# The message toolbar's "more" button: "Reply All" shows on the phone, but
|
||||
# the iPad pane has a quick reply box instead for mail to one recipient.
|
||||
- extendedWaitUntil:
|
||||
visible: "more"
|
||||
timeout: 20000
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
@@ -0,0 +1,19 @@
|
||||
# On the password step: fill in LOGIN and PASSWORD and sign in.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: login-username-input
|
||||
timeout: 20000
|
||||
- runFlow:
|
||||
file: type.yaml
|
||||
env:
|
||||
FIELD: login-username-input
|
||||
TEXT: ${LOGIN}
|
||||
- runFlow:
|
||||
file: type-secret.yaml
|
||||
env:
|
||||
FIELD: login-password-input
|
||||
TEXT: ${PASSWORD}
|
||||
# Return on the password field signs in.
|
||||
- pressKey: Enter
|
||||
@@ -0,0 +1,11 @@
|
||||
# From the sign-in choices to the inbox: manual server, password.
|
||||
# env LOGIN: the account's address.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
file: manual-server.yaml
|
||||
env:
|
||||
HOST: ${SERVER}
|
||||
- tapOn: "Sign in with a password instead"
|
||||
- runFlow: password-form.yaml
|
||||
- runFlow: inbox.yaml
|
||||
@@ -0,0 +1,22 @@
|
||||
# The helper has put a sign-in link on the simulator's clipboard: open
|
||||
# "Paste sign-in link", paste it and sign in.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- tapOn: "Paste sign-in link.*"
|
||||
- extendedWaitUntil:
|
||||
visible: "Paste"
|
||||
timeout: 10000
|
||||
- tapOn: "Paste"
|
||||
# iOS asks before an app reads the clipboard.
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Allow Paste"
|
||||
commands:
|
||||
- tapOn: "Allow Paste"
|
||||
- extendedWaitUntil:
|
||||
visible: "bulwarkmail://pair.*"
|
||||
timeout: 10000
|
||||
- takeScreenshot: paste-link-filled
|
||||
- tapOn:
|
||||
text: "Sign in"
|
||||
below: "bulwarkmail://pair.*"
|
||||
@@ -0,0 +1,23 @@
|
||||
# Push a message with SUBJECT to ACCOUNT (helper /simpush) and tap its banner
|
||||
# where it appears, at once (it shows for a few seconds, about as long as
|
||||
# Maestro takes to find it). If the tap came too early or too late, push
|
||||
# again: up to three tries until the message is open.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- repeat:
|
||||
while:
|
||||
notVisible: "Reply All"
|
||||
times: 3
|
||||
commands:
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /simpush?to=${ACCOUNT}&subject=${encodeURIComponent(SUBJECT)}
|
||||
- tapOn:
|
||||
point: "50%,9%"
|
||||
waitToSettleTimeoutMs: 500
|
||||
retryTapIfNoChange: false
|
||||
- extendedWaitUntil:
|
||||
visible: "Reply All"
|
||||
timeout: 30000
|
||||
optional: true
|
||||
@@ -0,0 +1,87 @@
|
||||
# Settings list: find the row ROW (regex on "Title, summary"), open it,
|
||||
# check PROOF shows, screenshot, and go back on a phone.
|
||||
#
|
||||
# Rows count as visible while the phone's tab bar or header covers them or
|
||||
# while they hang half off the iPad's screen, and a tap then misses; the
|
||||
# iPad's list is a left column Maestro's scroll (in the middle of the screen)
|
||||
# doesn't reach. So search for the page (Q, a word of its title): the row
|
||||
# then sits right under the search box. The account card (no Q) is always
|
||||
# at the top.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${!!Q}
|
||||
commands:
|
||||
- tapOn: "Search settings"
|
||||
- eraseText: 40
|
||||
- inputText: ${Q}
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
# Now and then a typed character goes missing: type it again.
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: ${ROW}
|
||||
commands:
|
||||
- tapOn: "Search settings"
|
||||
- eraseText: 40
|
||||
- inputText: ${Q}
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
# A tap while the list still coasts from the swipe only stops it.
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
- tapOn: ${ROW}
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 5000
|
||||
# On the phone the page replaces the list: make sure it opened before
|
||||
# scrolling (else the swipes below scroll the list away).
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE != 'tablet'}
|
||||
commands:
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: "Back"
|
||||
commands:
|
||||
- tapOn: ${ROW}
|
||||
- extendedWaitUntil:
|
||||
visible: "Back"
|
||||
timeout: 10000
|
||||
# PROOF can be further down the page: scroll the page (the right-hand pane
|
||||
# on the iPad) until it shows.
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE == 'tablet'}
|
||||
commands:
|
||||
- repeat:
|
||||
while:
|
||||
notVisible: ${PROOF}
|
||||
times: 6
|
||||
commands:
|
||||
- swipe:
|
||||
start: 75%,80%
|
||||
end: 75%,40%
|
||||
duration: 600
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE != 'tablet'}
|
||||
commands:
|
||||
- repeat:
|
||||
while:
|
||||
notVisible: ${PROOF}
|
||||
times: 6
|
||||
commands:
|
||||
- swipe:
|
||||
start: 50%,80%
|
||||
end: 50%,40%
|
||||
duration: 600
|
||||
- extendedWaitUntil:
|
||||
visible: ${PROOF}
|
||||
timeout: 20000
|
||||
- takeScreenshot: settings-${NAME}
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Back"
|
||||
commands:
|
||||
- tapOn: "Back"
|
||||
@@ -0,0 +1,37 @@
|
||||
# Signed in: sign out of the active account from the end of Settings.
|
||||
# On the iPad the Settings list is the left column; scroll that one.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- tapOn:
|
||||
id: tab-settings
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE == 'tablet'}
|
||||
commands:
|
||||
- repeat:
|
||||
while:
|
||||
notVisible: "Sign out"
|
||||
times: 12
|
||||
commands:
|
||||
- swipe:
|
||||
start: 30%,85%
|
||||
end: 30%,30%
|
||||
duration: 500
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${DEVICE != 'tablet'}
|
||||
commands:
|
||||
- scrollUntilVisible:
|
||||
element: "Sign out"
|
||||
direction: DOWN
|
||||
timeout: 30000
|
||||
- tapOn: "Sign out"
|
||||
- extendedWaitUntil:
|
||||
visible: "The account is removed from this device.*"
|
||||
timeout: 10000
|
||||
# The dialog's button, not the Settings row of the same name behind it.
|
||||
- tapOn:
|
||||
text: "Sign out"
|
||||
rightOf: "Cancel"
|
||||
below: "The account is removed from this device.*"
|
||||
- takeScreenshot: sign-out-tapped
|
||||
@@ -0,0 +1,36 @@
|
||||
# Feature flows start here: the app signed in to ACCOUNT, on the inbox.
|
||||
# Keeps the app's data between flows; signs in only when needed.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- launchApp:
|
||||
permissions:
|
||||
notifications: allow
|
||||
photos: allow
|
||||
camera: allow
|
||||
# Answer the notification prompt (if the app asks) before turning to
|
||||
# landscape: rotated, taps on system alerts miss.
|
||||
- setOrientation: PORTRAIT
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually|Inbox|.*Would Like to Send You Notifications.*"
|
||||
timeout: 120000
|
||||
- runFlow: allow-notifications.yaml
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${ORIENTATION == 'landscape'}
|
||||
commands:
|
||||
- setOrientation: LANDSCAPE_LEFT
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually|Inbox"
|
||||
timeout: 30000
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Enter server details manually"
|
||||
commands:
|
||||
- runFlow:
|
||||
file: password-sign-in.yaml
|
||||
env:
|
||||
LOGIN: ${ACCOUNT}@example.org
|
||||
- runFlow: allow-notifications.yaml
|
||||
- tapOn:
|
||||
id: tab-mail
|
||||
- runFlow: go-to-inbox.yaml
|
||||
@@ -0,0 +1,28 @@
|
||||
# Stalwart's own sign-in page inside the auth session. env LOGIN, TOTP (any
|
||||
# non-empty value: the account needs a one-time code).
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: "you@example.com"
|
||||
timeout: 60000
|
||||
- takeScreenshot: stalwart-login-page
|
||||
- runFlow:
|
||||
file: type-web.yaml
|
||||
env:
|
||||
HINT: "you@example.com"
|
||||
TEXT: ${LOGIN}
|
||||
- tapOn: "••••••••"
|
||||
- inputText: ${PASSWORD}
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${TOTP == 'yes'}
|
||||
commands:
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /totp
|
||||
- tapOn: "000000"
|
||||
- inputText: ${output.helper.code}
|
||||
- tapOn:
|
||||
text: "Sign in"
|
||||
index: 1
|
||||
@@ -0,0 +1,27 @@
|
||||
# Fresh install state, rotated for the landscape leg, on the sign-in choices.
|
||||
# env CAMERA: allow (default) or deny.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
# A system prompt left over from an earlier flow ("Open in …?") stays on
|
||||
# screen across app launches.
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Open in .*"
|
||||
commands:
|
||||
- tapOn: "Cancel"
|
||||
- launchApp:
|
||||
clearState: true
|
||||
clearKeychain: true
|
||||
permissions:
|
||||
notifications: allow
|
||||
camera: ${CAMERA || 'allow'}
|
||||
photos: allow
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${ORIENTATION == 'landscape'}
|
||||
commands:
|
||||
- setOrientation: LANDSCAPE_LEFT
|
||||
# A cold start of a release build on a CI simulator can be slow.
|
||||
- extendedWaitUntil:
|
||||
visible: "Enter server details manually"
|
||||
timeout: 120000
|
||||
@@ -0,0 +1,21 @@
|
||||
# The two-factor field is showing (it focuses itself): type a fresh code from
|
||||
# the helper and sign in.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: "Authentication code"
|
||||
timeout: 30000
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /totp
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- inputText: ${output.helper.code}
|
||||
# The number pad has no return key.
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible: "Sign in"
|
||||
commands:
|
||||
- hideKeyboard
|
||||
- tapOn: "Sign in"
|
||||
@@ -0,0 +1,22 @@
|
||||
# Type a secret into the field with id FIELD (TAP=no for a field that focuses
|
||||
# itself; see type.yaml for why the first character goes alone). The value
|
||||
# can't be checked (a secure field shows dots) and must never be shown, so a
|
||||
# lost keystroke surfaces as a failed sign-in.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- evalScript: ${output.secretHead = TEXT.slice(0, 1); output.secretTail = TEXT.slice(1)}
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${TAP != 'no'}
|
||||
commands:
|
||||
- tapOn:
|
||||
id: ${FIELD}
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- inputText: ${output.secretHead}
|
||||
# A short pause: wait for something that never shows.
|
||||
- extendedWaitUntil:
|
||||
visible: "zz-no-such-element-zz"
|
||||
timeout: 1000
|
||||
optional: true
|
||||
- inputText: ${output.secretTail}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Type TEXT into a web page field found by its placeholder HINT (the sign-in
|
||||
# pages in the auth session), and check it arrived as typed. Now and then a
|
||||
# stray character lands in front of it on the CI simulator; then erase the
|
||||
# field and type again. The caret is still at the end of what was just
|
||||
# typed, so eraseText (which deletes before the caret) empties it; selecting
|
||||
# the text with a long press didn't work and appended instead. SECRET=yes
|
||||
# skips the check (a password field shows dots).
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- tapOn: ${HINT}
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
- inputText: ${TEXT}
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${SECRET != 'yes'}
|
||||
commands:
|
||||
- repeat:
|
||||
while:
|
||||
notVisible: ${TEXT}
|
||||
times: 2
|
||||
commands:
|
||||
- eraseText: 150
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 2000
|
||||
- inputText: ${TEXT}
|
||||
- assertVisible: ${TEXT}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Type TEXT into the field with id FIELD and check it arrived in full.
|
||||
#
|
||||
# iOS drops keystrokes in two situations seen on the CI simulator: while the
|
||||
# edit menu (the "AutoFill" bubble) opens, which happens when a flow taps a
|
||||
# field that already has focus, and right after the first character, while
|
||||
# the field re-renders. So: no tap for a field that focuses itself (TAP=no;
|
||||
# Maestro can't see a React Native field's focus), type the first character,
|
||||
# wait until it shows, then type the rest. Retyped up to twice.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- evalScript: ${output.typeHead = TEXT.slice(0, 1); output.typeTail = TEXT.slice(1)}
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${TAP != 'no'}
|
||||
commands:
|
||||
- tapOn:
|
||||
id: ${FIELD}
|
||||
- waitForAnimationToEnd:
|
||||
timeout: 3000
|
||||
# ERASE=yes: the field already holds text.
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${ERASE == 'yes'}
|
||||
commands:
|
||||
- runFlow: clear-field.yaml
|
||||
- inputText: ${output.typeHead}
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: ${FIELD}
|
||||
text: "${output.typeHead}.*"
|
||||
timeout: 5000
|
||||
optional: true
|
||||
- inputText: ${output.typeTail}
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible:
|
||||
id: ${FIELD}
|
||||
text: ${TEXT_PATTERN || TEXT}
|
||||
commands:
|
||||
- retry:
|
||||
maxRetries: 2
|
||||
commands:
|
||||
- runFlow: clear-field.yaml
|
||||
- inputText: ${output.typeHead}
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: ${FIELD}
|
||||
text: "${output.typeHead}.*"
|
||||
timeout: 5000
|
||||
optional: true
|
||||
- inputText: ${output.typeTail}
|
||||
- assertVisible:
|
||||
id: ${FIELD}
|
||||
text: ${TEXT_PATTERN || TEXT}
|
||||
@@ -0,0 +1,41 @@
|
||||
# The Bulwark webmail's sign-in page inside the auth session (the app's
|
||||
# "Sign in via webmail" hand-off). env LOGIN, TOTP=yes for a TOTP account.
|
||||
appId: org.bulwarkmail.mobile
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: "user@example.com"
|
||||
timeout: 60000
|
||||
- takeScreenshot: webmail-login-page
|
||||
- runFlow:
|
||||
file: type-web.yaml
|
||||
env:
|
||||
HINT: "user@example.com"
|
||||
TEXT: ${LOGIN}
|
||||
- runFlow:
|
||||
file: type-web.yaml
|
||||
env:
|
||||
HINT: "Enter your password"
|
||||
TEXT: ${PASSWORD}
|
||||
SECRET: "yes"
|
||||
- tapOn: "Sign in"
|
||||
- runFlow:
|
||||
when:
|
||||
true: ${TOTP == 'yes'}
|
||||
commands:
|
||||
- extendedWaitUntil:
|
||||
visible: "000000"
|
||||
timeout: 30000
|
||||
- runScript:
|
||||
file: ../scripts/helper.js
|
||||
env:
|
||||
CALL: /totp
|
||||
- tapOn: "000000"
|
||||
- inputText: ${output.helper.code}
|
||||
- tapOn: "Sign in|Verify"
|
||||
|
||||
# Before handing over, the webmail asks whether the app may take the sign-in.
|
||||
- extendedWaitUntil:
|
||||
visible: "Hand over to the app"
|
||||
timeout: 60000
|
||||
- takeScreenshot: webmail-handover-prompt
|
||||
- tapOn: "Hand over to the app"
|
||||
@@ -39,21 +39,18 @@ import {
|
||||
type NotificationTapPayload,
|
||||
} from './src/lib/push-notifications';
|
||||
import { addUnifiedPushEndpointListener } from './src/lib/unified-push';
|
||||
|
||||
// Pause between a mute change and the push-filter update it sends (#78).
|
||||
const MUTES_RESYNC_DELAY_MS = 1_000;
|
||||
import type { MainTabsParamList, RootStackParamList } from './src/navigation/types';
|
||||
import ComposeScreen from './src/screens/ComposeScreen';
|
||||
import EmailThreadScreen from './src/screens/EmailThreadScreen';
|
||||
import EmailSourceScreen from './src/screens/EmailSourceScreen';
|
||||
import LoginScreen from './src/screens/LoginScreen';
|
||||
import MailTab from './src/screens/MailTab';
|
||||
import FilesScreen from './src/screens/FilesScreen';
|
||||
import CalendarScreen from './src/screens/CalendarScreen';
|
||||
import ContactsTab from './src/screens/ContactsTab';
|
||||
import ContactDetailScreen from './src/screens/ContactDetailScreen';
|
||||
import ContactFormScreen from './src/screens/ContactFormScreen';
|
||||
import GroupDetailScreen from './src/screens/GroupDetailScreen';
|
||||
import SettingsScreen from './src/screens/SettingsScreen';
|
||||
import ScheduledScreen from './src/screens/ScheduledScreen';
|
||||
import UnifiedInboxScreen from './src/screens/UnifiedInboxScreen';
|
||||
// Screens that aren't on the way to the inbox load on first visit
|
||||
// (getComponent below), so their modules stay out of the startup work.
|
||||
import { useAccountStore } from './src/stores/account-store';
|
||||
import { useAuthStore } from './src/stores/auth-store';
|
||||
import { useCalendarStore } from './src/stores/calendar-store';
|
||||
@@ -91,6 +88,7 @@ import { useOfflineCacheStore } from './src/stores/offline-cache-store';
|
||||
import { useOutboxStore } from './src/stores/outbox-store';
|
||||
import { runOfflineSync } from './src/lib/offline-sync';
|
||||
import { spacing, typography, type ThemePalette } from './src/theme/tokens';
|
||||
import { perfEndAfterPaint, perfMarksEnabled, perfStart } from './src/lib/perf-marks';
|
||||
import { useColors } from './src/theme/colors';
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
@@ -258,6 +256,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
||||
<PushOnboardingPrompt />
|
||||
<AppIconBadge />
|
||||
<Tab.Navigator
|
||||
screenListeners={perfMarksEnabled() ? ({ route }) => ({
|
||||
tabPress: () => perfStart('tab-switch'),
|
||||
focus: () => perfEndAfterPaint('tab-switch', { to: route.name }),
|
||||
}) : undefined}
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
// Tabs out of view stop re-rendering until they are shown again.
|
||||
@@ -294,6 +296,7 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
||||
name="Mail"
|
||||
options={{
|
||||
title: tabLabels.mail,
|
||||
tabBarButtonTestID: 'tab-mail',
|
||||
tabBarIcon: ({ color, size }) => <Mail size={size} color={color} />,
|
||||
tabBarBadge: inboxUnreadCount > 0 ? (inboxUnreadCount > 99 ? '99+' : inboxUnreadCount) : undefined,
|
||||
tabBarBadgeStyle: {
|
||||
@@ -314,9 +317,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
||||
</Tab.Screen>
|
||||
<Tab.Screen
|
||||
name="Calendar"
|
||||
component={CalendarScreen}
|
||||
getComponent={() => require('./src/screens/CalendarScreen').default}
|
||||
options={{
|
||||
title: tabLabels.calendar,
|
||||
tabBarButtonTestID: 'tab-calendar',
|
||||
tabBarIcon: ({ color, size }) => <Calendar size={size} color={color} />,
|
||||
tabBarItemStyle: hasCalendar ? undefined : disabledTabStyle,
|
||||
tabBarAccessibilityLabel: hasCalendar ? tabLabels.calendar : unavailable(tabLabels.calendar),
|
||||
@@ -329,9 +333,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Contacts"
|
||||
component={ContactsTab}
|
||||
getComponent={() => require('./src/screens/ContactsTab').default}
|
||||
options={{
|
||||
title: tabLabels.contacts,
|
||||
tabBarButtonTestID: 'tab-contacts',
|
||||
tabBarIcon: ({ color, size }) => <BookUser size={size} color={color} />,
|
||||
tabBarItemStyle: hasContacts ? undefined : disabledTabStyle,
|
||||
tabBarAccessibilityLabel: hasContacts ? tabLabels.contacts : unavailable(tabLabels.contacts),
|
||||
@@ -344,9 +349,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Files"
|
||||
component={FilesScreen}
|
||||
getComponent={() => require('./src/screens/FilesScreen').default}
|
||||
options={{
|
||||
title: tabLabels.files,
|
||||
tabBarButtonTestID: 'tab-files',
|
||||
tabBarIcon: ({ color, size }) => <HardDrive size={size} color={color} />,
|
||||
tabBarItemStyle: hasFiles ? undefined : disabledTabStyle,
|
||||
tabBarAccessibilityLabel: hasFiles ? tabLabels.files : unavailable(tabLabels.files),
|
||||
@@ -361,6 +367,7 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
||||
name="Settings"
|
||||
options={{
|
||||
title: tabLabels.settings,
|
||||
tabBarButtonTestID: 'tab-settings',
|
||||
tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />,
|
||||
}}
|
||||
>
|
||||
@@ -684,6 +691,41 @@ export default function App() {
|
||||
};
|
||||
}, [client, isAuthenticated, emailNotificationsEnabled, activeAccountId]);
|
||||
|
||||
// Muting a folder or account changes the server-side push filter (#78),
|
||||
// which the setup above would only send at the next launch. Send it now,
|
||||
// after a short pause so a few taps in a row make one update. Compared by
|
||||
// content, since hydration hands back an equal but new object; the value
|
||||
// seen first after hydration is the baseline, not a change.
|
||||
const notificationMutes = useSettingsStore((s) => s.notificationMutes);
|
||||
const settingsHydrated = useSettingsStore((s) => s.hydrated);
|
||||
const mutesKey = JSON.stringify(notificationMutes);
|
||||
const lastMutesKey = React.useRef<string | null>(null);
|
||||
React.useEffect(() => {
|
||||
if (!settingsHydrated) return;
|
||||
if (lastMutesKey.current === null || lastMutesKey.current === mutesKey) {
|
||||
lastMutesKey.current = mutesKey;
|
||||
return;
|
||||
}
|
||||
lastMutesKey.current = mutesKey;
|
||||
if (!isAuthenticated || !client || !emailNotificationsEnabled) return;
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(async () => {
|
||||
const relayBaseUrl = await getStoredRelayBaseUrl();
|
||||
if (!relayBaseUrl || cancelled) return;
|
||||
try {
|
||||
await resyncPushNotifications({ relayBaseUrl, accountLabel: client.username ?? undefined });
|
||||
} catch (error) {
|
||||
console.warn('[push] mute update failed:', error instanceof Error ? error.message : error);
|
||||
}
|
||||
}, MUTES_RESYNC_DELAY_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
// Only a change of the mutes should send an update; the rest is read live.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mutesKey, settingsHydrated]);
|
||||
|
||||
// Live updates (SSE with polling fallback), re-armed on every account
|
||||
// switch and every re-established session — the singleton `client` object
|
||||
// never changes identity, so it cannot be the dependency on its own.
|
||||
@@ -825,10 +867,10 @@ export default function App() {
|
||||
<Stack.Navigator screenOptions={{ headerShown: false, freezeOnBlur: true }} screenLayout={withHosts}>
|
||||
<Stack.Screen name="MainTabs" component={MainTabsNavigator} />
|
||||
<Stack.Screen name="EmailThread" component={EmailThreadScreen} />
|
||||
<Stack.Screen name="EmailSource" component={EmailSourceScreen} />
|
||||
<Stack.Screen name="EmailSource" getComponent={() => require('./src/screens/EmailSourceScreen').default} />
|
||||
<Stack.Screen
|
||||
name="Compose"
|
||||
component={ComposeScreen}
|
||||
getComponent={() => require('./src/screens/ComposeScreen').default}
|
||||
// A tablet shows the composer as a panel over the screen it came
|
||||
// from, so that screen stays drawn behind the dimmed backdrop.
|
||||
options={isSplit
|
||||
@@ -838,15 +880,15 @@ export default function App() {
|
||||
<Stack.Screen name="ContactDetail" component={ContactDetailScreen} />
|
||||
<Stack.Screen
|
||||
name="ContactForm"
|
||||
component={ContactFormScreen}
|
||||
getComponent={() => require('./src/screens/ContactFormScreen').default}
|
||||
options={{
|
||||
presentation: 'modal',
|
||||
animation: 'slide_from_bottom',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="GroupDetail" component={GroupDetailScreen} />
|
||||
<Stack.Screen name="Scheduled" component={ScheduledScreen} />
|
||||
<Stack.Screen name="UnifiedInbox" component={UnifiedInboxScreen} />
|
||||
<Stack.Screen name="Scheduled" getComponent={() => require('./src/screens/ScheduledScreen').default} />
|
||||
<Stack.Screen name="UnifiedInbox" getComponent={() => require('./src/screens/UnifiedInboxScreen').default} />
|
||||
<Stack.Screen
|
||||
name="AddAccount"
|
||||
options={{
|
||||
|
||||
@@ -90,8 +90,8 @@ react {
|
||||
/* 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"]
|
||||
// The distribution flavors (github, play) need their debug variants listed.
|
||||
debuggableVariants = ["githubDebug", "playDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
@@ -154,13 +154,39 @@ android {
|
||||
versionCode appVersionCode.toInteger()
|
||||
versionName appVersionName
|
||||
|
||||
// Android account type of device sync (#34), derived from the package
|
||||
// name so a later rename needs no other change. The XML in res/xml and
|
||||
// the Kotlin in sync/ read it as @string/device_sync_account_type.
|
||||
resValue "string", "device_sync_account_type", "${applicationId}.account"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the build is distributed. The two are separate apps on a phone:
|
||||
*
|
||||
* - github: the APK on GitHub Releases, sideloaded, updated by the in-app
|
||||
* updater. Keeps the original package name so existing installs update.
|
||||
* - play: the Google Play bundle. Play forbids an app updating itself
|
||||
* outside Play, so this flavor has no installer permission and the JS
|
||||
* hides the updater (BuildConfig.DISTRIBUTION, lib/platform-capabilities).
|
||||
*
|
||||
* See docs/android-release.md.
|
||||
*/
|
||||
flavorDimensions = ["distribution"]
|
||||
productFlavors {
|
||||
github {
|
||||
dimension "distribution"
|
||||
applicationId 'com.anonymous.bulwarkmobile'
|
||||
buildConfigField "String", "DISTRIBUTION", '"github"'
|
||||
}
|
||||
play {
|
||||
dimension "distribution"
|
||||
applicationId 'org.bulwarkmail.mobile'
|
||||
buildConfigField "String", "DISTRIBUTION", '"play"'
|
||||
}
|
||||
}
|
||||
applicationVariants.all { variant ->
|
||||
// Android account type of device sync (#34), derived from the package
|
||||
// name of each flavor. The XML in res/xml and the Kotlin in sync/ read
|
||||
// it as @string/device_sync_account_type.
|
||||
variant.resValue "string", "device_sync_account_type", "${variant.applicationId}.account"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
@@ -245,7 +271,7 @@ def checkReleaseSigning = tasks.register('checkReleaseSigning') {
|
||||
}
|
||||
}
|
||||
tasks.configureEach { task ->
|
||||
if (task.name == 'preReleaseBuild') {
|
||||
if (task.name ==~ /pre\w*ReleaseBuild/) {
|
||||
task.dependsOn(checkReleaseSigning)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "504486613721",
|
||||
"project_id": "bulwark-mobile",
|
||||
"storage_bucket": "bulwark-mobile.firebasestorage.app"
|
||||
"project_number": "752109678859",
|
||||
"project_id": "bulwark-20c72",
|
||||
"storage_bucket": "bulwark-20c72.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:504486613721:android:69a5d7ba9148fb14cb24c8",
|
||||
"mobilesdk_app_id": "1:752109678859:android:952e8373ad508e780cf20e",
|
||||
"android_client_info": {
|
||||
"package_name": "com.anonymous.bulwarkmobile"
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyD4Zyu1BU4YNYy4SOK5te3EhSRHRGoMZno"
|
||||
"current_key": "AIzaSyCpmCG1-Mnlgtme3NgtgRbmp7GztQeT4w4"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
@@ -26,15 +26,15 @@
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:504486613721:android:14c6a69aa49cac3ecb24c8",
|
||||
"mobilesdk_app_id": "1:752109678859:android:2692d26569e5d9950cf20e",
|
||||
"android_client_info": {
|
||||
"package_name": "com.bulwark.bulwarkmobile"
|
||||
"package_name": "org.bulwarkmail.mobile"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyD4Zyu1BU4YNYy4SOK5te3EhSRHRGoMZno"
|
||||
"current_key": "AIzaSyCpmCG1-Mnlgtme3NgtgRbmp7GztQeT4w4"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Debug builds load the bundle from Metro over http, which the main config
|
||||
forbids (a network security config replaces the manifest's
|
||||
usesCleartextTraffic). Same trust anchors as the release config.
|
||||
-->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -0,0 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The in-app updater hands the downloaded release APK to the system
|
||||
installer (src/lib/install-update.ts). Only the sideloaded GitHub build
|
||||
updates itself; the Play build is updated by Play. -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "504486613721",
|
||||
"project_id": "bulwark-mobile",
|
||||
"storage_bucket": "bulwark-mobile.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:504486613721:android:69a5d7ba9148fb14cb24c8",
|
||||
"android_client_info": {
|
||||
"package_name": "com.anonymous.bulwarkmobile"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyD4Zyu1BU4YNYy4SOK5te3EhSRHRGoMZno"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:504486613721:android:14c6a69aa49cac3ecb24c8",
|
||||
"android_client_info": {
|
||||
"package_name": "com.bulwark.bulwarkmobile"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyD4Zyu1BU4YNYy4SOK5te3EhSRHRGoMZno"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -2,9 +2,13 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<!-- REQUEST_INSTALL_PACKAGES (in-app updater) is declared by the github flavor only. -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<!-- Photos are picked with the system photo picker, which needs no media
|
||||
permission; Play's photo and video policy rejects READ_MEDIA_IMAGES for that. -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
|
||||
<!-- expo-camera declares the microphone for video; QR scanning never records audio. -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<!-- Device sync (#34): contacts and calendars in Android's providers, driven by the sync framework. -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
@@ -21,7 +25,7 @@
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules" android:networkSecurityConfig="@xml/network_security_config">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
@@ -110,224 +114,224 @@
|
||||
<receiver android:name=".widget.InboxWidget" android:exported="false" android:label="@string/widgetprovider_inboxwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_inboxwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.TriageWidget" android:exported="false" android:label="@string/widgetprovider_triagewidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_triagewidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.UnreadCountWidget" android:exported="false" android:label="@string/widgetprovider_unreadcountwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_unreadcountwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.LatestMessageWidget" android:exported="false" android:label="@string/widgetprovider_latestmessagewidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_latestmessagewidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.FolderCountsWidget" android:exported="false" android:label="@string/widgetprovider_foldercountswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_foldercountswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.MailShortcutsWidget" android:exported="false" android:label="@string/widgetprovider_mailshortcutswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_mailshortcutswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.AllAccountsWidget" android:exported="false" android:label="@string/widgetprovider_allaccountswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_allaccountswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.StarredWidget" android:exported="false" android:label="@string/widgetprovider_starredwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_starredwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.FavouritePeopleWidget" android:exported="false" android:label="@string/widgetprovider_favouritepeoplewidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_favouritepeoplewidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.MailSearchWidget" android:exported="false" android:label="@string/widgetprovider_mailsearchwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_mailsearchwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.OutboxWidget" android:exported="false" android:label="@string/widgetprovider_outboxwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_outboxwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.TagWidget" android:exported="false" android:label="@string/widgetprovider_tagwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_tagwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.LastDraftWidget" android:exported="false" android:label="@string/widgetprovider_lastdraftwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_lastdraftwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.AgendaWidget" android:exported="false" android:label="@string/widgetprovider_agendawidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_agendawidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.WeekWidget" android:exported="false" android:label="@string/widgetprovider_weekwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_weekwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.MonthWidget" android:exported="false" android:label="@string/widgetprovider_monthwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_monthwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.TodayEventsWidget" android:exported="false" android:label="@string/widgetprovider_todayeventswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_todayeventswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.NextEventWidget" android:exported="false" android:label="@string/widgetprovider_nexteventwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_nexteventwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.DateNextWidget" android:exported="false" android:label="@string/widgetprovider_datenextwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_datenextwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.UpNextWidget" android:exported="false" android:label="@string/widgetprovider_upnextwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_upnextwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.InvitationsWidget" android:exported="false" android:label="@string/widgetprovider_invitationswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_invitationswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.FreeTimeWidget" android:exported="false" android:label="@string/widgetprovider_freetimewidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_freetimewidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.TasksWidget" android:exported="false" android:label="@string/widgetprovider_taskswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_taskswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.TaskProgressWidget" android:exported="false" android:label="@string/widgetprovider_taskprogresswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_taskprogresswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.CountdownWidget" android:exported="false" android:label="@string/widgetprovider_countdownwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_countdownwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.BirthdaysWidget" android:exported="false" android:label="@string/widgetprovider_birthdayswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_birthdayswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.TodayHubWidget" android:exported="false" android:label="@string/widgetprovider_todayhubwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_todayhubwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.MailAndNextWidget" android:exported="false" android:label="@string/widgetprovider_mailandnextwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_mailandnextwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.RecentFilesWidget" android:exported="false" android:label="@string/widgetprovider_recentfileswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_recentfileswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.AttachmentsWidget" android:exported="false" android:label="@string/widgetprovider_attachmentswidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_attachmentswidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.VacationWidget" android:exported="false" android:label="@string/widgetprovider_vacationwidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_vacationwidget"/>
|
||||
</receiver>
|
||||
<receiver android:name=".widget.StorageWidget" android:exported="false" android:label="@string/widgetprovider_storagewidget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
<action android:name="com.anonymous.bulwarkmobile.WIDGET_CLICK"/>
|
||||
<action android:name="${applicationId}.WIDGET_CLICK"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widgetprovider_storagewidget"/>
|
||||
</receiver>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.anonymous.bulwarkmobile
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
|
||||
/**
|
||||
* Facts about this build that JS can't see on its own. `distribution` is the
|
||||
* product flavor (app/build.gradle): "github" for the sideloaded APK with its
|
||||
* own updater, "play" for the Google Play bundle, which must not update
|
||||
* itself (lib/platform-capabilities).
|
||||
*/
|
||||
class BulwarkBuildModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||
override fun getName() = "BulwarkBuild"
|
||||
|
||||
override fun getConstants(): Map<String, Any> = mapOf(
|
||||
"distribution" to BuildConfig.DISTRIBUTION,
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,8 @@ class BulwarkFcmPackage : ReactPackage {
|
||||
add(BulwarkClientCertModule(reactContext))
|
||||
add(BulwarkDeviceSyncModule(reactContext))
|
||||
add(BulwarkWidgetsModule(reactContext))
|
||||
add(BulwarkPdfModule(reactContext))
|
||||
add(BulwarkBuildModule(reactContext))
|
||||
// The bridge registers React Native's own module under this name.
|
||||
if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) {
|
||||
add(BulwarkHeadlessJsTaskSupportModule(reactContext))
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.anonymous.bulwarkmobile
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Renders PDF pages to PNG files for the in-app file preview, with the
|
||||
* platform's own PdfRenderer: Android has no PDF view, and its WebView
|
||||
* doesn't show PDFs either. The preview draws the pages in a WebView of
|
||||
* its own so they zoom, asking for each page as it scrolls into view.
|
||||
*
|
||||
* PdfRenderer is not thread-safe and allows one open page at a time, so
|
||||
* every call runs on one background thread, and the open document is kept
|
||||
* between calls for the file the preview shows.
|
||||
*/
|
||||
class BulwarkPdfModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||
|
||||
private val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "BulwarkPdf").apply { isDaemon = true } }
|
||||
private var openPath: String? = null
|
||||
private var descriptor: ParcelFileDescriptor? = null
|
||||
private var renderer: PdfRenderer? = null
|
||||
|
||||
override fun getName(): String = "BulwarkPdf"
|
||||
|
||||
override fun invalidate() {
|
||||
worker.execute { closeDocument() }
|
||||
worker.shutdown()
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
/** Page count and each page's size in points: `{ pages: [{ width, height }] }`. */
|
||||
@ReactMethod
|
||||
fun open(fileUri: String, promise: Promise) {
|
||||
worker.execute {
|
||||
try {
|
||||
val doc = document(fileUri)
|
||||
val pages = Arguments.createArray()
|
||||
for (i in 0 until doc.pageCount) {
|
||||
doc.openPage(i).use { page ->
|
||||
pages.pushMap(Arguments.createMap().apply {
|
||||
putInt("width", page.width)
|
||||
putInt("height", page.height)
|
||||
})
|
||||
}
|
||||
}
|
||||
promise.resolve(Arguments.createMap().apply { putArray("pages", pages) })
|
||||
} catch (e: SecurityException) {
|
||||
// Password-protected; PdfRenderer can't open those.
|
||||
closeDocument()
|
||||
promise.reject("E_PDF_PROTECTED", e.message, e)
|
||||
} catch (e: Throwable) {
|
||||
closeDocument()
|
||||
promise.reject("E_PDF_OPEN", e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Render one page `width` pixels wide into `outUri` (a PNG). Resolves with the file URI. */
|
||||
@ReactMethod
|
||||
fun renderPage(fileUri: String, index: Int, width: Int, outUri: String, promise: Promise) {
|
||||
worker.execute {
|
||||
try {
|
||||
val doc = document(fileUri)
|
||||
if (index < 0 || index >= doc.pageCount) throw IndexOutOfBoundsException("page $index")
|
||||
doc.openPage(index).use { page ->
|
||||
val w = width.coerceIn(1, MAX_PAGE_PX)
|
||||
val h = (w.toLong() * page.height / page.width.coerceAtLeast(1)).toInt().coerceIn(1, MAX_PAGE_PX * 2)
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
try {
|
||||
// Pages are transparent where nothing is drawn.
|
||||
bitmap.eraseColor(Color.WHITE)
|
||||
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||
val out = fileOf(outUri)
|
||||
// The preview's folder is gone once it closed; a page
|
||||
// still queued then must not bring it back.
|
||||
if (out.parentFile?.isDirectory != true) throw IllegalStateException("preview closed")
|
||||
FileOutputStream(out).use { stream -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) }
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
promise.resolve(outUri)
|
||||
} catch (e: Throwable) {
|
||||
promise.reject("E_PDF_RENDER", e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Close the document the preview showed. */
|
||||
@ReactMethod
|
||||
fun close(fileUri: String, promise: Promise) {
|
||||
worker.execute {
|
||||
if (openPath == fileOf(fileUri).absolutePath) closeDocument()
|
||||
promise.resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun document(fileUri: String): PdfRenderer {
|
||||
val path = fileOf(fileUri).absolutePath
|
||||
renderer?.let { if (openPath == path) return it }
|
||||
closeDocument()
|
||||
val fd = ParcelFileDescriptor.open(File(path), ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
try {
|
||||
val doc = PdfRenderer(fd)
|
||||
descriptor = fd
|
||||
renderer = doc
|
||||
openPath = path
|
||||
return doc
|
||||
} catch (e: Throwable) {
|
||||
fd.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeDocument() {
|
||||
try { renderer?.close() } catch (_: Throwable) {}
|
||||
try { descriptor?.close() } catch (_: Throwable) {}
|
||||
renderer = null
|
||||
descriptor = null
|
||||
openPath = null
|
||||
}
|
||||
|
||||
private fun fileOf(uri: String): File {
|
||||
val path = if (uri.startsWith("file:")) Uri.parse(uri).path else uri
|
||||
return File(path ?: throw IllegalArgumentException("not a file: $uri"))
|
||||
}
|
||||
|
||||
companion object {
|
||||
// A 4096 px page bitmap is 64 MB; keep well below.
|
||||
private const val MAX_PAGE_PX = 2480
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Trust anchors for every connection the app makes (fetch, the client
|
||||
certificate module, the sync adapters). Android 7+ trusts only the system
|
||||
CAs unless the app opts in; a self-hosted server behind a private CA
|
||||
installed under Settings > Security > Encryption & credentials would fail
|
||||
with "Network request failed" otherwise (#3). Cleartext stays off, as it is
|
||||
for an app targeting API 28+ without this file; the debug source set
|
||||
overrides this for Metro.
|
||||
-->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
+41
-3
@@ -3,7 +3,8 @@
|
||||
The release APK is built by
|
||||
[.github/workflows/release-android.yml](../.github/workflows/release-android.yml)
|
||||
on an Ubuntu runner whenever a GitHub release is published, and attached to that
|
||||
release. Installed apps pick it up through the in-app updater.
|
||||
release. Installed apps pick it up through the in-app updater. The same workflow
|
||||
builds the Google Play bundle; see [Google Play](#google-play).
|
||||
|
||||
Every APK up to and including 0.1.62 was signed with `android/app/debug.keystore`,
|
||||
the debug key that ships with the React Native template. Its password is
|
||||
@@ -199,6 +200,42 @@ Once you delete it, every APK is signed with the release key alone:
|
||||
the update. Those users have to uninstall, reinstall and sign in again, and
|
||||
they lose local data. Afterwards their devices trust only the release key.
|
||||
|
||||
## Google Play
|
||||
|
||||
The app has two product flavors (`android/app/build.gradle`). They install
|
||||
side by side as separate apps:
|
||||
|
||||
| Flavor | Package | Output | Updated by | Firebase project |
|
||||
|---|---|---|---|---|
|
||||
| `github` | `com.anonymous.bulwarkmobile` | APK on GitHub Releases | in-app updater | `bulwark-mobile` (`src/github/google-services.json`) |
|
||||
| `play` | `org.bulwarkmail.mobile` | AAB for Google Play | Google Play | `bulwark-20c72` (`google-services.json`) |
|
||||
|
||||
Play forbids an app updating itself outside Play, so the `play` flavor has no
|
||||
`REQUEST_INSTALL_PACKAGES` (only `src/github/AndroidManifest.xml` declares it)
|
||||
and `BuildConfig.DISTRIBUTION` is `"play"`, which the `BulwarkBuild` native
|
||||
module hands to JS. `supportsSideloadUpdates` in `src/lib/platform-capabilities.ts`
|
||||
is false there, which hides the updater. Neither flavor declares
|
||||
`READ_MEDIA_IMAGES`: photos come from the system photo picker, and Play's photo
|
||||
and video policy rejects the permission for that use. `RECORD_AUDIO`, which
|
||||
expo-camera adds, is removed as well: QR scanning never records audio.
|
||||
|
||||
Play App Signing holds the key that signs what Play delivers (generated by
|
||||
Google, certificate SHA-256 `7e:e1:fd:35:…:91:fa:e3`). The workflow signs the
|
||||
bundle with our release key, which Play registered as the upload key with the
|
||||
first upload. The two flavors therefore have different signers too.
|
||||
|
||||
The `play-bundle` job builds `bundlePlayRelease` for arm64-v8a, armeabi-v7a and
|
||||
x86_64 (Play splits the bundle per ABI), checks the signer, the package name and
|
||||
the permissions, and keeps the AAB and its R8 mapping as a workflow artifact.
|
||||
With the `PLAY_SERVICE_ACCOUNT_JSON` secret set (vault: "Google Play – service
|
||||
account bulwark-google-play-api") it also uploads the bundle to the internal
|
||||
testing track as a draft. Play only accepts API uploads once a first bundle has
|
||||
been uploaded by hand in the Play Console.
|
||||
|
||||
The `github` flavor stays on the old Firebase project until the app registers
|
||||
its new FCM token with the relay when the token changes; moving it before that
|
||||
would silence push for every updated install.
|
||||
|
||||
## Local release builds
|
||||
|
||||
CI signs releases, so you normally don't need the release key on your machine.
|
||||
@@ -206,7 +243,8 @@ For a local release build, for profiling or checking R8, opt in to the debug key
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew assembleRelease -PallowDebugSignedRelease=true
|
||||
./gradlew assembleGithubRelease -PallowDebugSignedRelease=true
|
||||
./gradlew bundlePlayRelease -PallowDebugSignedRelease=true
|
||||
```
|
||||
|
||||
```powershell
|
||||
@@ -252,7 +290,7 @@ retrace bulwark-mobile-0.1.63-abc1234-mapping.txt stacktrace.txt
|
||||
|
||||
`retrace` is in the Android SDK command-line tools (`cmdline-tools/latest/bin`).
|
||||
A local build writes its mapping to
|
||||
`android/app/build/outputs/mapping/release/mapping.txt`.
|
||||
`android/app/build/outputs/mapping/<flavor>Release/mapping.txt`.
|
||||
|
||||
## versionCode
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin
|
||||
- What RN does: single tap follows the global `mailAttachmentAction`; `jszip` is already a dependency (`package.json:36`) but unused for mail.
|
||||
- Fix hint: long-press sheet with "Open / Save / Share"; a "Download all" entry building the zip with `jszip` into `Paths.cache` then sharing.
|
||||
|
||||
- [x] **In-app attachment preview (images, PDF, text, audio/video) is absent; iOS "Preview" is just the share sheet — fixed in d2ed27f** — `P3` — `partial`
|
||||
- [x] **In-app attachment preview (images, PDF, text, audio/video) is absent; iOS "Preview" is just the share sheet — fixed in d2ed27f; audio/video, HTML, Markdown, SVG, Android PDFs and ZIPs since 0.1.80-0.1.82** — `P3` — `partial`
|
||||
- What WEB does: `FilePreviewModal` for images/PDF (bytes handed to pdf.js, #871)/text/markdown/audio/video/eml, script-bearing types forced to download (`components/mail/mail-app.tsx:2887-2935`, `lib/file-preview.ts:29-92`).
|
||||
- What RN does: Android hands the file to an external viewer via `ACTION_VIEW`, iOS opens the share sheet (`src/lib/email-export.ts:64-79`, `115-141`); nothing is previewed inside the app.
|
||||
- Fix hint: for images/PDF/text open a modal `WebView` on the cached `file://` (or use `expo-file-system`'s `File.uri` with a QuickLook bridge on iOS); keep the external-viewer path as fallback.
|
||||
|
||||
@@ -110,7 +110,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a
|
||||
- What RN does: `getDocumentAsync({ multiple: false })` (`src/screens/FilesScreen.tsx:357-360`); uploading the same name twice creates two nodes with the same name.
|
||||
- Fix hint: `multiple: true` + sequential upload loop; reuse WEB's `getUniqueName` against `visibleFiles`.
|
||||
|
||||
- [x] **No in-app preview (image/text/markdown/PDF/audio/video/eml)** — `P2` — `missing` — done in 409cdb1 (images, text and source files, `.eml`, and PDFs on iOS open in the reader's preview; PDFs on Android, audio, video, HTML and Markdown still go to another app)
|
||||
- [x] **No in-app preview (image/text/markdown/PDF/audio/video/eml)** — `P2` — `missing` — done in 409cdb1 (images, text and source files, `.eml`, and PDFs on iOS open in the reader's preview); since 0.1.80/0.1.81 also audio, video, SVG/ICO, HTML, Markdown, PDFs on Android (native `PdfRenderer`) and ZIP listings, through the one preview the reader, list, Files and compose share (`src/components/email/use-file-preview.ts`)
|
||||
- What WEB does: `ImagePreviewModal` and `FilePreviewModal` (text, markdown, PDF incl. mobile pdf.js viewer, audio, video, EML) chosen via `getFilePreviewKind` (`lib/file-preview.ts:29-68`; `components/files/file-preview-modal.tsx`, `image-preview-modal.tsx`, `pdf-mobile-viewer.tsx`; `files-app.tsx:487-495`, `690-708`).
|
||||
- What RN does: tapping a file calls `shareAttachment` which downloads to cache and hands off to the OS share/viewer sheet (`src/screens/FilesScreen.tsx:248-258`; `src/lib/email-export.ts` "preview" variant). Works but there is no inline viewer and no "recent files" tracking.
|
||||
- Fix hint: reuse whatever attachment viewer the mail area has (image modal / WebView for text+PDF); N/A if the team decides the OS viewer is the mobile answer - then close this item.
|
||||
|
||||
@@ -6,6 +6,10 @@ import { registerWidgetTaskHandler } from 'react-native-android-widget';
|
||||
import { pushBackgroundTask } from './src/lib/push-background-task';
|
||||
import { widgetTaskHandler } from './src/widgets/task-handler';
|
||||
import { refreshWidgetsInBackground } from './src/widgets/sync';
|
||||
import { perfMark } from './src/lib/perf-marks';
|
||||
|
||||
// Every import above has run: the bundle is loaded (performance harness).
|
||||
perfMark('bundle-loaded');
|
||||
import { markUiStarted } from './src/widgets/ui-presence';
|
||||
|
||||
// Runs in a fresh headless JS runtime when BulwarkPushTaskService is started
|
||||
|
||||
+4506
-3645
File diff suppressed because it is too large
Load Diff
+4506
-3645
File diff suppressed because it is too large
Load Diff
+4506
-3645
File diff suppressed because it is too large
Load Diff
+4506
-3645
File diff suppressed because it is too large
Load Diff
+953
-92
File diff suppressed because it is too large
Load Diff
+951
-90
File diff suppressed because it is too large
Load Diff
+4506
-3645
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4506
-3645
File diff suppressed because it is too large
Load Diff
+4506
-3645
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user