diff --git a/.github/workflows/discord-ci-notify.yml b/.github/workflows/discord-ci-notify.yml index 5286e33..e1b27d0 100644 --- a/.github/workflows/discord-ci-notify.yml +++ b/.github/workflows/discord-ci-notify.yml @@ -10,6 +10,7 @@ on: - CI - Expo versions - Gryt Git notification + - Phone flows - Release Android - Release iOS - Release Mobile diff --git a/.github/workflows/phone-flows.yml b/.github/workflows/phone-flows.yml new file mode 100644 index 0000000..0b5eef9 --- /dev/null +++ b/.github/workflows/phone-flows.yml @@ -0,0 +1,117 @@ +name: Phone flows + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + pull_request: + paths: + - ".maestro/**" + - ".github/workflows/phone-flows.yml" + +concurrency: + group: phone-flows + cancel-in-progress: true + +jobs: + flows: + name: Tap through the app + runs-on: xcode-27 + timeout-minutes: 60 + env: + LANG: en_US.UTF-8 + LC_ALL: en_US.UTF-8 + MAESTRO_CLI_NO_ANALYTICS: "1" + MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000" + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: yarn + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Select Xcode 27.1 + run: sudo xcode-select -s /Applications/Xcode_27.1.app + + - run: yarn install --frozen-lockfile + + - name: Install Maestro + run: | + curl -fsSL --retry 5 --retry-all-errors -o maestro.zip https://github.com/mobile-dev-inc/maestro/releases/download/cli-2.12.0/maestro.zip + echo "a6044a84542f522f434c5dc3d609e60c7446d423f3ed1b2ae513f5c48663ec5e maestro.zip" | shasum -a 256 -c - + unzip -q maestro.zip -d "$HOME/maestro-cli" + echo "$HOME/maestro-cli/maestro/bin" >> "$GITHUB_PATH" + + - uses: actions/checkout@v4 + with: + repository: Gryt-chat/server + path: test-server + + # There is no Docker on macOS runners, so the server runs from source. + - name: Start a throwaway Gryt server + working-directory: test-server + env: + PORT: "5077" + HOST: 127.0.0.1 + DATA_DIR: ${{ runner.temp }}/gryt-data + DISABLE_S3: "true" + GRYT_IDENTITY_TIERS: local,account + run: | + yarn install --frozen-lockfile + export JWT_SECRET=$(openssl rand -hex 32) + start() { nohup node -r ts-node/register src/index.ts > "$RUNNER_TEMP/server.log" 2>&1 & echo $! > "$RUNNER_TEMP/server.pid"; } + ready() { for _ in $(seq 1 60); do curl -sf http://127.0.0.1:5077/info > /dev/null && return 0; sleep 2; done; return 1; } + start + ready + # A new server only takes invites, and no env var changes that. /info reads it at boot. + sqlite3 "$DATA_DIR/gryt.db" "UPDATE server_config SET join_policy='open', display_name='Maestro test', is_configured=1" + kill "$(cat "$RUNNER_TEMP/server.pid")" + sleep 2 + start + ready + curl -s http://127.0.0.1:5077/info | grep -q '"joinPolicy":"open"' + + # Named devices differ between runner images, so take an iPhone on the newest runtime it has. + - name: Pick a simulator + run: | + DEVICE=$(xcrun simctl list devices available -j | node -e ' + const { devices } = JSON.parse(require("fs").readFileSync(0, "utf8")); + const version = (k) => k.split("iOS-")[1].split("-").map(Number); + const newest = Object.keys(devices).filter((k) => k.includes("iOS-") && devices[k].some((d) => d.name.startsWith("iPhone"))) + .sort((a, b) => version(b)[0] - version(a)[0] || version(b)[1] - version(a)[1])[0]; + console.log(devices[newest].find((d) => d.name.startsWith("iPhone")).udid); + ') + test -n "$DEVICE" + echo "DEVICE=$DEVICE" >> "$GITHUB_ENV" + + - name: Build for the simulator + run: | + set -o pipefail + yarn expo prebuild --platform ios --no-install + (cd ios && pod install) + xcodebuild -workspace ios/Gryt.xcworkspace -scheme Gryt -configuration Release \ + -destination "platform=iOS Simulator,id=$DEVICE" -derivedDataPath build/sim \ + CODE_SIGN_IDENTITY=- CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM=8883W2XTQ8 PROVISIONING_PROFILE_SPECIFIER= \ + build | tail -20 + + - name: Run the flows + run: | + xcrun simctl boot "$DEVICE" + xcrun simctl bootstatus "$DEVICE" + xcrun simctl install "$DEVICE" build/sim/Build/Products/Release-iphonesimulator/Gryt.app + maestro test -e GRYT_TEST_SERVER=127.0.0.1:5077 .maestro + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: maestro-debug + path: | + ~/.maestro/tests + ${{ runner.temp }}/server.log diff --git a/.maestro/add-server-from-drawer.yaml b/.maestro/add-server-from-drawer.yaml new file mode 100644 index 0000000..73b399a --- /dev/null +++ b/.maestro/add-server-from-drawer.yaml @@ -0,0 +1,20 @@ +appId: chat.gryt.app +name: Add a server from the server drawer +--- +- runFlow: subflows/join.yaml +- tapOn: "Maestro test.*" +- assertVisible: "Your servers" +# Outside the drawer, which has to close it. +- tapOn: + point: "90%,50%" +- assertNotVisible: "Your servers" +- tapOn: "Maestro test.*" +- tapOn: "Add a server" +# It froze here once: the sheet never showed and no touch got through. +- extendedWaitUntil: + visible: "Invite link or server address" + timeout: 5000 +- inputText: "127.0.0.1:5077" +- extendedWaitUntil: + visible: "Maestro test" + timeout: 10000 diff --git a/.maestro/config.yaml b/.maestro/config.yaml new file mode 100644 index 0000000..f1896ff --- /dev/null +++ b/.maestro/config.yaml @@ -0,0 +1,4 @@ +flows: + - "*.yaml" +excludeTags: + - subflow diff --git a/.maestro/message-long-press.yaml b/.maestro/message-long-press.yaml new file mode 100644 index 0000000..8fc8274 --- /dev/null +++ b/.maestro/message-long-press.yaml @@ -0,0 +1,17 @@ +appId: chat.gryt.app +name: Send a message and long-press it +--- +- runFlow: subflows/join.yaml +- tapOn: "General" +- tapOn: "Message #General.*" +- inputText: "**bold** hello" +- tapOn: "Send" +- tapOn: "Agree" +- extendedWaitUntil: + visible: ".*bold hello. Hold for actions" + timeout: 5000 +- longPressOn: ".*bold hello. Hold for actions" +- assertVisible: "Reply" +- assertVisible: "Copy text" +- assertVisible: ".*bold hello.*" +- assertNotVisible: ".*\\*\\*bold.*" diff --git a/.maestro/subflows/join.yaml b/.maestro/subflows/join.yaml new file mode 100644 index 0000000..964a6e5 --- /dev/null +++ b/.maestro/subflows/join.yaml @@ -0,0 +1,28 @@ +appId: chat.gryt.app +tags: + - subflow +--- +- launchApp: + clearState: true + clearKeychain: true +- extendedWaitUntil: + visible: "Get started" + timeout: 15000 +- tapOn: "Get started" +- tapOn: "Add a server" +- inputText: ${GRYT_TEST_SERVER} +# iOS's first-run keyboard tip, which covers the bottom half of a fresh simulator. +- runFlow: + when: + visible: "Continue" + commands: + - tapOn: "Continue" +- extendedWaitUntil: + visible: "Add Maestro test" + timeout: 10000 +- tapOn: "Add Maestro test" +- tapOn: "Not now" +- tapOn: "Skip" +- extendedWaitUntil: + visible: "General" + timeout: 20000 diff --git a/README.md b/README.md index 54b2d52..e3eabd6 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,16 @@ CocoaPods reading its own path, not anything about this project: LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 npx expo prebuild --platform ios ``` +## Tapping through it: Maestro + +The flows in `.maestro/` go through the app on a simulator the way a person would. CI runs them every night and on any PR that changes them. To run them yourself you need Java 17, the [Maestro CLI](https://github.com/mobile-dev-inc/maestro), a Release simulator build and a Gryt server called "Maestro test" with open joining. The flows are in `.github/workflows/phone-flows.yml` step by step if you need the server setup. + +```sh +maestro test -e GRYT_TEST_SERVER=127.0.0.1:5077 .maestro +``` + +A failed run leaves its screenshots in `~/.maestro/tests`. Every flow starts from a wiped app and joins the server again. + ## Giving it to somebody else: TestFlight `expo run:ios --device` above installs onto a phone you're holding. This is the diff --git a/package.json b/package.json index 95e4110..095a716 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@gryt/owl": "^0.7.0", "@gryt/theme": "^0.13.0", "@gryt/ui": "^0.46.0", - "@gryt/ui-native": "^0.18.1", + "@gryt/ui-native": "^0.18.2", "@gryt/voice": "0.9.1", "@noble/ciphers": "^2.4.0", "@noble/curves": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index e4e3f53..687f399 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1357,10 +1357,10 @@ resolved "https://registry.yarnpkg.com/@gryt/theme/-/theme-0.13.0.tgz#640f3049b53cfe427f0067cfc5faeffbd04d36aa" integrity sha512-7CW/wBKZFL1bfk5xQcJtvdoD+rbCvmHeA0U2uTQMQ9gfXAvIbPx7GL+TjMYYouTtLdJ/OKFARK+NXuS0RaBveg== -"@gryt/ui-native@^0.18.1": - version "0.18.1" - resolved "https://registry.yarnpkg.com/@gryt/ui-native/-/ui-native-0.18.1.tgz#a2c7eb1097951330fc719c41b179ac3dd11513d5" - integrity sha512-5KLrUEY2gMIaapTy7LRyX2V7cLbU5Uf03dyoz0KVmW5kIfqEUu8AXinEliczUUREEL6Isbh7GIM/hrMSDtDCxQ== +"@gryt/ui-native@^0.18.2": + version "0.18.2" + resolved "https://registry.yarnpkg.com/@gryt/ui-native/-/ui-native-0.18.2.tgz#ee468e0af96e93b90c06ddb44b0617dd8c5c6fb7" + integrity sha512-6QN/ZesYfsvbTPq2sibguPIoCavY9M3eJiCAqXdoNy8G2BLsJGLJi7Bcaf/gmnwsy3/0NcYnlhxgfGDkQZn47w== dependencies: "@gryt/owl" "^0.7.0" "@gryt/theme" "^0.13.0"