Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0be620e
Log every simulator tap of ten selection test runs
ellatrix Oct 12, 2026
68ccb25
Serve WordPress with native PHP instead of Playground
ellatrix Oct 12, 2026
a85ec8b
Print the PHP server log when the editor does not load
ellatrix Oct 12, 2026
de67405
Launch Safari once during setup and time each setup step
ellatrix Oct 12, 2026
e7a0f77
Remove the touch diagnostics and update the iOS test docs
ellatrix Oct 12, 2026
22f223e
Match the setup-php version comment to its tag
ellatrix Oct 12, 2026
3afd497
Stop when Safari does not open the warm-up page
ellatrix Oct 12, 2026
d1718f4
Download WordPress from GitHub, the same as the end to end tests
ellatrix Oct 12, 2026
dc0cc72
Block outside requests in the iOS test site and stop the server's wor…
ellatrix Oct 12, 2026
fe99e90
Clone WordPress instead of downloading the tarball
ellatrix Oct 12, 2026
ed73791
Set up the iOS test site with WP-CLI instead of the blueprint
ellatrix Oct 12, 2026
c3e604c
Add the blueprint's two steps to run.sh with WP-CLI
ellatrix Oct 12, 2026
e5aefcd
Time the first editor load in CI (temporary)
ellatrix Oct 13, 2026
abedec7
Compare the first editor load with and without cron (temporary)
ellatrix Oct 13, 2026
05a6f20
Sample the stuck editor load (temporary)
ellatrix Oct 13, 2026
b31b4eb
Sample the first editor load in run.sh (temporary)
ellatrix Oct 13, 2026
b3b3757
Turn off the JIT for the iOS test server and remove the probe
ellatrix Oct 13, 2026
d9c2275
Turn off the JIT for WP-CLI too
ellatrix Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ios-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,11 @@ jobs:
name: ios-build-output
path: build

# The runner's Node is older than the repository requires, and
# npx enforces that requirement when it starts Playground.
- name: Use desired version of Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- name: Set up PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
node-version-file: .nvmrc
package-manager-cache: false
php-version: '8.4'
coverage: none

- name: Install xcodegen
run: brew install xcodegen
Expand All @@ -105,5 +103,7 @@ jobs:
if: ${{ always() }}
with:
name: ios-test-results
path: test/ios/build/results.xcresult
path: |
test/ios/build/results.xcresult
test/ios/build/server.log
if-no-files-found: ignore
8 changes: 4 additions & 4 deletions test/ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ XCUITest suites that drive Safari on an iOS simulator, for behavior only
touch gestures show, such as selecting text with the selection handles.

Run them on macOS with Xcode and [xcodegen](https://github.com/yonaskolb/XcodeGen)
installed, on the Node version in `.nvmrc`, after building the plugin:
installed and PHP 8 with SQLite, after building the plugin:

```bash
nvm use
npm run build
./test/ios/run.sh
```

The script serves the checkout with WordPress Playground, boots a simulator
and runs `xcodebuild test`. Screenshots of failures are in
`test/ios/build/results.xcresult`.
The script installs WordPress with SQLite, serves it and the checkout with
PHP's built-in server, boots a simulator and runs `xcodebuild test`.
Screenshots of failures are in `test/ios/build/results.xcresult`.

The tests read the software keyboard, so the simulator must show one. A
simulator that has ever had a hardware keyboard connected keeps hiding the
Expand Down
4 changes: 2 additions & 2 deletions test/ios/SafariTests/SelectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ final class SelectionTests: XCTestCase {
}

func testHandleExtendsSelectionIntoPreviousParagraph() throws {
// A new post starting with this content: the blueprint passes the
// content argument of post-new.php through unescaped.
// A new post starting with this content: run.sh passes the content
// argument of post-new.php through unescaped.
let base = ProcessInfo.processInfo.environment[ "WP_BASE_URL" ] ?? "http://127.0.0.1:9400"
var url = URLComponents( string: base + "/wp-admin/post-new.php" )!
url.queryItems = [ URLQueryItem(
Expand Down
15 changes: 0 additions & 15 deletions test/ios/blueprint.json

This file was deleted.

112 changes: 90 additions & 22 deletions test/ios/run.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Runs the Safari tests against the plugin in this checkout on an iOS
# simulator. Needs macOS with Xcode, xcodegen, the Node version in
# .nvmrc and a built plugin (npm run build). Set WP_PORT to serve
# WordPress on another port and SIMULATOR_UDID to pick a device.
# simulator. Needs macOS with Xcode, xcodegen, PHP with SQLite and a
# built plugin (npm run build). Set WP_PORT to serve WordPress on
# another port and SIMULATOR_UDID to pick a device.
set -euo pipefail

# Steps print like the performance tests: elapsed time, then the step.
Expand Down Expand Up @@ -37,34 +37,92 @@ print( devices[ -1 ][ "udid" ] )
step "Booting simulator $UDID"
# The boot goes on in the background while WordPress starts.
xcrun simctl boot "$UDID" 2>/dev/null || true
# WordPress runs in Playground: PHP compiled to WebAssembly, no Docker.
# WordPress runs on PHP's built-in server with SQLite, set up fresh on
# every run.
step "Starting WordPress"
PLAYGROUND_LOG=$( mktemp )
# npx runs outside the checkout: the checkout's npm settings refuse
# Playground's install scripts.
ROOT="$PWD"
( cd "$( mktemp -d )" && exec npx --yes @wp-playground/cli@3.1.53 server \
--auto-mount="$ROOT" \
--blueprint="$ROOT/test/ios/blueprint.json" \
--port "$PORT" ) > "$PLAYGROUND_LOG" 2>&1 &
PLAYGROUND_PID=$!
trap 'kill "$PLAYGROUND_PID" 2>/dev/null || true' EXIT
# Playground prints "Ready!" once it listens and the blueprint has run; the
# server answers earlier, before the plugin is active.
until grep -q "Ready!" "$PLAYGROUND_LOG"; do
kill -0 "$PLAYGROUND_PID" 2>/dev/null || { cat "$PLAYGROUND_LOG"; echo "WordPress did not start"; exit 1; }
CACHE="$ROOT/test/ios/build/cache"
WP_DIR="$ROOT/test/ios/build/wordpress"
mkdir -p "$CACHE"
step "Downloading WP-CLI and SQLite"
# A failed download leaves no file behind, so the next run tries again.
fetch() { [ -f "$2" ] || { curl -sSLf -o "$2.part" "$1" && mv "$2.part" "$2"; }; }
fetch https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar "$CACHE/wp-cli.phar"
fetch https://downloads.wordpress.org/plugin/sqlite-database-integration.latest-stable.zip "$CACHE/sqlite.zip"
# Without the JIT: compiling WordPress takes longer than running it.
wp() { php -d memory_limit=512M -d opcache.jit=disable "$CACHE/wp-cli.phar" --path="$WP_DIR" --quiet "$@"; }
rm -rf "$WP_DIR"
# The same WordPress as the end-to-end tests: a GitHub repository, with
# an optional #branch, from .wp-env.test.json.
CORE=$( python3 -c '
import json, sys
repo, _, branch = json.load( open( ".wp-env.test.json" ) )[ "core" ].partition( "#" )
if repo.count( "/" ) != 1:
sys.exit( "Unsupported core in .wp-env.test.json: " + repo )
print( repo, branch )
' )
read -r REPO BRANCH <<< "$CORE"
step "Downloading WordPress from $REPO"
git clone --quiet --depth 1 ${BRANCH:+--branch "$BRANCH"} "https://github.com/$REPO.git" "$WP_DIR"
unzip -q "$CACHE/sqlite.zip" -d "$WP_DIR/wp-content/plugins"
SQLITE="$WP_DIR/wp-content/plugins/sqlite-database-integration"
sed -e "s#{SQLITE_IMPLEMENTATION_FOLDER_PATH}#$SQLITE#" -e "s#{SQLITE_PLUGIN}#sqlite-database-integration/load.php#" \
"$SQLITE/db.copy" > "$WP_DIR/wp-content/db.php"
step "Installing WordPress"
wp config create --dbname=wordpress --dbuser=wordpress --dbpass=wordpress --skip-check
# No requests to other sites: a fresh site's first admin page otherwise
# checks wordpress.org for updates.
wp config set WP_HTTP_BLOCK_EXTERNAL true --raw
wp core install --url="$WP_BASE_URL" --title="iOS tests" --admin_user=admin --admin_password=password --admin_email=admin@example.com --skip-email
step "Activating the plugin"
ln -s "$ROOT" "$WP_DIR/wp-content/plugins/gutenberg"
wp plugin activate gutenberg
# The editor opens without the welcome guide or fullscreen mode.
wp user meta update 1 wp_persisted_preferences --format=json \
"{\"core/edit-post\":{\"welcomeGuide\":false,\"fullscreenMode\":false},\"_modified\":\"$( date -u +%Y-%m-%dT%H:%M:%S+00:00 )\"}"
mkdir -p "$WP_DIR/wp-content/mu-plugins"
# A new post starts with the content argument of post-new.php, unescaped.
cat > "$WP_DIR/wp-content/mu-plugins/new-post-content.php" <<'PHP'
<?php
add_filter( 'default_content', function ( $content ) {
return isset( $_GET['content'] ) ? wp_unslash( $_GET['content'] ) : $content;
} );
PHP
# Every request is the administrator's.
cat > "$WP_DIR/wp-content/mu-plugins/login.php" <<'PHP'
<?php
add_action( 'init', function () {
if ( is_user_logged_in() ) {
return;
}
// The admin checks this request's cookies, so set them here too.
$expiration = time() + DAY_IN_SECONDS;
$_COOKIE[ AUTH_COOKIE ] = wp_generate_auth_cookie( 1, $expiration, 'auth' );
$_COOKIE[ LOGGED_IN_COOKIE ] = wp_generate_auth_cookie( 1, $expiration, 'logged_in' );
wp_set_auth_cookie( 1, true );
wp_set_current_user( 1 );
}, 0 );
PHP
step "Starting the server"
# Without the JIT: compiling WordPress on the first request takes longer
# than the request's 30 second limit on the CI runner.
PHP_CLI_SERVER_WORKERS=8 php -d memory_limit=512M -d opcache.jit=disable -S "127.0.0.1:$PORT" -t "$WP_DIR" > "$ROOT/test/ios/build/server.log" 2>&1 &
SERVER_PID=$!
# The server's workers are its child processes: stop them too.
trap 'pkill -P "$SERVER_PID" 2>/dev/null; kill "$SERVER_PID" 2>/dev/null || true' EXIT
until curl -sf -o /dev/null "$WP_BASE_URL/wp-login.php"; do
kill -0 "$SERVER_PID" 2>/dev/null || { cat "$ROOT/test/ios/build/server.log"; echo "WordPress did not start"; exit 1; }
sleep 1
done
cat "$PLAYGROUND_LOG"

# The editor page shows whether the plugin replaces core's bundles, which
# it only does when its build exists. Its first request also does the
# one-time work of a fresh site before the test's clock starts.
step "Loading the editor once"
# Playground logs the request in through a redirect, so cookies must be
# kept between the hops: -b "" holds them in memory.
# -b "" keeps the login cookie between redirects, in memory.
if ! EDITOR_HTML=$( curl -sSL --fail -b "" "$WP_BASE_URL/wp-admin/post-new.php" ); then
echo "The editor did not load from $WP_BASE_URL"
tail -40 "$ROOT/test/ios/build/server.log"
exit 1
fi
if ! grep -q "/build/scripts/rich-text/" <<< "$EDITOR_HTML"; then
Expand All @@ -77,8 +135,18 @@ xcrun simctl bootstatus "$UDID" -b
# Safari's tips cover the page on a fresh simulator, and a tap then only
# closes the tip.
xcrun simctl spawn "$UDID" defaults write com.apple.mobilesafari com.apple.TipKit.HideAllTips -bool YES
# Playground logs a browser in once and remembers that in a cookie, which
# outlives the server: start Safari without cookies from an earlier run.
# Safari's first launch on a fresh simulator takes most of a minute, and
# the simulator then loses the test's first tap. Launch it once here.
step "Starting Safari once"
xcrun simctl openurl "$UDID" "$WP_BASE_URL/wp-login.php?warm-up"
for _ in $( seq 1 120 ); do
grep -q "wp-login.php?warm-up" "$ROOT/test/ios/build/server.log" && break
sleep 1
done
grep -q "wp-login.php?warm-up" "$ROOT/test/ios/build/server.log" ||
{ echo "Safari did not open the page"; exit 1; }
# The login is remembered in a cookie, which outlives the server: start
# Safari without cookies from an earlier run.
xcrun simctl terminate "$UDID" com.apple.mobilesafari 2>/dev/null || true
SAFARI_DATA=$( xcrun simctl get_app_container "$UDID" com.apple.mobilesafari data )
if [[ -n "$SAFARI_DATA" && -d "$SAFARI_DATA" ]]; then
Expand Down
Loading