Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Safari Extension Permission Model Test Flow

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.

Why a Scripted Install-And-Grant Is Harder Than It Looks

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).

Manifest and Container 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.

Manual Acceptance Worksheet

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.

#StepExpected resultEvidence to capture
1Open Safari > Settings > Extensions"My Test Extension" appears in the left-hand listScreenshot of the Extensions pane
2Tick the checkbox next to the extensionCheckbox becomes enabled; toolbar icon appearsScreenshot showing enabled state
3Open the per-site permission popup for the extensionOptions include Ask, Deny, Allow for listed sites and Allow on Every WebsiteScreenshot of popup
4Choose Allow on Every Website (or Allow for localhost only)Setting persists after closing SettingsScreenshot of updated selection
5Open http://localhost:3000/testpage.htmlToolbar icon stays enabled; no permission prompt reappearsScreenshot of Safari window
6Inspect #target in Web Inspectordata-extension-active="yes" is setCopy of outer HTML
7Quit and relaunch Safari; revisit the URLEnabled state and host grant survive restartScreenshot

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/

Automating the Page-Side Check With safaridriver

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.

Which Figment Mac Fits This Job

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.

  • M-series Mac from $95/month — Apple silicon suitable for current Xcode and current Safari. Exact chip, RAM, disk and macOS version are confirmed in the quote; this tier is request-based. Recommended for a Safari extension flow against the current Safari release.
  • Virtual Intel macOS VM at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk; browser-console desktop access; Sequoia planned) — a real macOS environment that runs compatible Intel Xcode 26 or earlier. It can host this flow if your container app and the installed Safari still support Intel. It is not an Apple silicon machine and will not run Xcode 27 or arm64-only tooling. Inbound public SSH is not implemented on this plan; commands run in the desktop terminal, and outbound CI agents can call out to their own services.

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.

Price Check Against Named Alternatives

Entry prices vary by hardware class; a $29 virtual Intel VM is not equivalent to a dedicated M-series Mac mini.

  • CloudMac (https://cloudmac.org/pricing) lists a dedicated Mac mini M4 (16 GB / 256 GB) at $99.99/month, and https://cloudmac.org/ advertises $999/year — an effective $83.25/month, roughly $141 less than twelve months at Figment's $95 M-series headline. CloudMac's 512 GB and 1 TB storage rows are listed as fully reserved at time of writing; the 256 GB row is marked reservable.
  • MacStadium (https://macstadium.com/pricing) lists M2.S at $109, M4.S (16 GB / 256 GB) at $149 and M4.M (24 GB / 512 GB) at $249 per month, with support and a published 99.99% uptime target. Annual terms are available via sales; the public page does not quote the annual dollar figure.
  • Mac Mini Vault (https://www.macminivault.com/dedicated-mac-mini/) lists M1 at $84.99 and M2 at $94.99, both shown Out of Stock, plus M6 and M5 Pro rows marked Notify me in both regions. Compare listed prices and ask about availability before quoting a start date.

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.


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms