
rent a Figment M-series Mac from $95/month to run a current Safari against your Web Extension, then use the acceptance worksheet below to verify the install, enable and host-permission grant by hand, and add a small safaridriver check for the page-side behaviour. Start the request at /mac-hosting/start and confirm the exact chip, RAM and macOS version in the quote.
Safari's Web Extension permission model does not match Chrome's install-time grant. Apple's Creating a Safari web extension guide describes the manifest-driven model: an extension ships inside a macOS or iOS container app, Safari lists it under Settings > Extensions, and the user decides per-site access. A Chrome-style assumption that host_permissions is granted the moment the extension is installed will silently fail on Safari.
Apple is also explicit about test tooling on the same page: "XCTest doesn't support unit tests for the JavaScript APIs that Safari web extensions use, or UI tests in Safari." That rules out a clean, Apple-supported XCUITest script that clicks around Safari's own Settings window. People do drive Safari with XCUIApplication(bundleIdentifier:), but the Settings pane element identifiers change between point releases and are not part of a public contract, so a committed CI assertion against them breaks on upgrade. Treat that path as exploratory, not as the backbone of your regression suite.
What stays supportable is an honest split: a manual acceptance worksheet for the permission UI that a human runs on the rented Mac, plus a scripted safaridriver run that proves the page-side effect once access has been granted. Apple's Testing with WebDriver in Safari article describes how safaridriver --enable and a standard Selenium client talk to Safari itself (not PlaywrightWebKit, which is a separate WebKit build).
A Safari Web Extension is a Manifest V3 extension packaged inside an Xcode-generated container app. The two manifest fields that drive this test:
{
"manifest_version": 3,
"name": "My Test Extension",
"version": "1.0",
"permissions": ["activeTab", "scripting"],
"host_permissions": [
"http://localhost:3000/*"
],
"action": { "default_title": "My Test Extension" },
"content_scripts": [{
"matches": ["http://localhost:3000/*"],
"js": ["content.js"]
}]
}
Declaring only localhost keeps the fixture deterministic. The content script should mutate a known sentinel element so the WebDriver step has something unambiguous to assert on:
// content.js
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('target');
if (el) el.dataset.extensionActive = 'yes';
});
Build the macOS container the same way you build any Xcode target. Run xcodebuild -list in your project directory first and substitute the real scheme name; the scheme Xcode generates depends on the product name you chose in the Safari Extension App template:
xcodebuild -list # find the real scheme
xcodebuild -scheme "<YOUR-SCHEME>" \
-destination 'platform=macOS' \
-configuration Debug build
Launching the built container app once registers the extension with Safari. From then on, Safari lists it under Settings > Extensions whether or not the container app is still running.
Run this on the rented Mac after the container app has launched at least once. Record one timestamp per row; each row is a checkpoint, and a failing row is the row that documents the regression.
| # | Step | Expected result | Evidence to capture |
|---|---|---|---|
| 1 | Open Safari > Settings > Extensions | "My Test Extension" appears in the left-hand list | Screenshot of the Extensions pane |
| 2 | Tick the checkbox next to the extension | Checkbox becomes enabled; toolbar icon appears | Screenshot showing enabled state |
| 3 | Open the per-site permission popup for the extension | Options include Ask, Deny, Allow for listed sites and Allow on Every Website | Screenshot of popup |
| 4 | Choose Allow on Every Website (or Allow for localhost only) | Setting persists after closing Settings | Screenshot of updated selection |
| 5 | Open http://localhost:3000/testpage.html | Toolbar icon stays enabled; no permission prompt reappears | Screenshot of Safari window |
| 6 | Inspect #target in Web Inspector | data-extension-active="yes" is set | Copy of outer HTML |
| 7 | Quit and relaunch Safari; revisit the URL | Enabled state and host grant survive restart | Screenshot |
Store the screenshots under a run-unique directory. Do not reuse paths like /tmp/test-server.pid across runs or matrix jobs; GITHUB_RUN_ID alone collides across reruns. A safe pattern:
RUN_DIR="$(mktemp -d -t safari-ext-XXXXXX)"
echo "$RUN_DIR" > ./last-run-dir
# ...save screenshots and logs under $RUN_DIR/
Once the worksheet has established the enabled state, lock the page-side assertion into CI. safaridriver does not expose a documented command to install or enable a Safari web extension, so do not try to fake one — rely on the already-granted state from the worksheet. The run below enables the driver once, serves the fixture on a run-unique port, opens the page and checks that the content script wrote its sentinel attribute.
# One-time per host
safaridriver --enable
# Serve the fixture on a run-unique port; clean up on real exit
PORT=$(python3 -c 'import socket;s=socket.socket();s.bind(("",0));print(s.getsockname()[1]);s.close()')
SRV_DIR="$(mktemp -d -t safari-ext-srv-XXXXXX)"
cp -R ./test-fixtures/. "$SRV_DIR/"
( cd "$SRV_DIR" && python3 -m http.server "$PORT" ) >"$SRV_DIR/srv.log" 2>&1 &
SRV_PID=$!
trap 'kill "$SRV_PID" 2>/dev/null; rm -rf "$SRV_DIR"' EXIT INT TERM
PORT="$PORT" python3 - <<'PY'
import os, sys
from selenium import webdriver
from selenium.webdriver.common.by import By
port = os.environ["PORT"]
d = webdriver.Safari()
try:
d.get(f"http://localhost:{port}/testpage.html")
el = d.find_element(By.ID, "target")
if el.get_attribute("data-extension-active") != "yes":
print("extension did not run on target page", file=sys.stderr)
sys.exit(1)
print("ok")
finally:
d.quit()
PY
test-fixtures/testpage.html should carry the sentinel element the extension mutates:
<!DOCTYPE html>
<html>
<head><title>Extension test page</title></head>
<body><h1 id="target">Test target</h1></body>
</html>
Treat the script as a template. It stops on the first failing assertion, binds to a run-unique port, cleans up its own temp directory on EXIT, INT and TERM, and never touches the host's global keychain or user profile. It is not a benchmark and it does not prove the extension is installed — that is what step 1 of the worksheet is for. Do not assume Selenium's Firefox-only install_addon call works with the Safari driver.
Safari Web Extension testing needs a real Safari on real macOS; the iOS Simulator does not surface the Settings > Extensions screens this flow exercises. Two Figment tiers are relevant.
Managed Build Mac is $249/month and bundles one M-series Mac with CI setup, backups, monitoring and 30 minutes of monthly support if you want someone to wire this worksheet into a scheduled job. Both hardware tiers collect requests until supply is confirmed, so confirm availability in the quote before committing a release date. Request a configuration at /mac-hosting/start.
Entry prices vary by hardware class; a $29 virtual Intel VM is not equivalent to a dedicated M-series Mac mini.
If your buying criterion is strictly the cheapest listed dedicated M4 with 16 GB today, CloudMac's annual number is lower than twelve months at Figment's $95 monthly headline. The $95 Figment entry is an unspecified M-series configuration priced in the quote, not a verified M4/16 GB match. If your criterion is managed CI work on a dedicated Mac, Figment's Managed Build Mac is the specific package the three listed alternatives do not price on their public plan pages.
Ready to run this flow on a real Mac? Browse Mac hosting or request a configuration.