From 03352e596245ce68c3eb088e9855afb4ae82beb6 Mon Sep 17 00:00:00 2001 From: SAPHID Date: Sat, 24 Oct 2026 07:59:16 +0000 Subject: [PATCH] Clarify desktop installation and first headset connection --- CHANGELOG.md | 4 ++ README.md | 89 +++++++++++++++++++++++------------ site/README.md | 30 +++++++++++- site/public/css/site.css | 17 ++++++- site/public/index.html | 63 +++++++++++++++---------- site/public/js/site.js | 34 ++++++------- site/test/onboarding.test.mjs | 59 +++++++++++++++++++++++ 7 files changed, 223 insertions(+), 73 deletions(-) create mode 100644 site/test/onboarding.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 463cd24..bbdf3d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Clarify first-time desktop downloads, installation and opening on the website + and README, including CPU choices, unsigned-app warnings, pairing fallback + and the source-only iOS build. Keep section navigation available on mobile. + - Treat headset aliases and other CSV metadata as plain text in power and heat exports, so spreadsheet formula prefixes cannot run when a report is opened. diff --git a/README.md b/README.md index bcaad87..6f046c2 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ **Manage your Valve Steam Frame from your computer.**
See what the headset sees, install games and Android apps, move files and text across, and check battery and status, all over SSH. -[![Latest release](https://img.shields.io/github/v/release/saphid/steam-frame?label=release&color=1a9fff)](https://github.com/saphid/steam-frame/releases/latest) +[![Latest release](https://img.shields.io/github/v/release/saphid/frame-control?label=release&color=1a9fff)](https://github.com/saphid/frame-control/releases/latest) [![Platforms](https://img.shields.io/badge/macOS%20%7C%20Windows%20%7C%20Linux-2a475e?label=runs%20on)](#install) -[![Checks](https://img.shields.io/github/actions/workflow/status/saphid/steam-frame/checks.yml?branch=main&label=checks)](https://github.com/saphid/steam-frame/actions/workflows/checks.yml) +[![Checks](https://img.shields.io/github/actions/workflow/status/saphid/frame-control/checks.yml?branch=main&label=checks)](https://github.com/saphid/frame-control/actions/workflows/checks.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-66c0f4)](LICENSE) [**Website**](https://frame-control.pages.dev) · [**Download**](#install) · [Trailer](#trailer) · [Features](#features) · [Set up the headset](#set-up-the-headset) · [Feedback](#feedback) · [Docs](#going-further) @@ -19,7 +19,7 @@ See what the headset sees, install games and Android apps, move files and text a Frame Control's Games tab: installed games, sideloaded titles, and your Steam library with Frame ratings -Watch the Frame Control trailer +Watch the Frame Control trailer The trailer: 66 seconds, with sound. Downloads the MP4 from the trailer release. @@ -116,17 +116,30 @@ already ships (sideloading a game copies Valve's own devkit scripts to ## Install +**Install Frame Control on your computer, not on the headset.** The website is +for downloads and instructions; it does not connect to your Frame in a browser. +Choose the operating system and processor of the computer you will use. + | | Download | Needs | |---|---|---| -| **macOS** (Apple Silicon) | [Frame-Control-mac-arm64.dmg](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-mac-arm64.dmg) | Nothing extra | -| **Windows** 10 / 11 (x64) | [Frame-Control-Setup-x64.exe](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-Setup-x64.exe) · [portable .zip](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-win-x64.zip) | Nothing extra | -| **Linux** (x64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-x86_64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-amd64.deb) | `ssh` (most desktops have it) | -| **Linux** (arm64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.deb) | `ssh`, and `adb` for Android apps (`sudo apt install adb`) | +| **macOS** (Apple Silicon) | [Frame-Control-mac-arm64.dmg](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-mac-arm64.dmg) | Nothing extra | +| **Windows** 10 / 11 (x64) | [Frame-Control-Setup-x64.exe](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-Setup-x64.exe) · [portable .zip](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-win-x64.zip) | OpenSSH Client (normally installed) | +| **Linux** (x64) | [AppImage](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-x86_64.AppImage) · [.deb](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-amd64.deb) | `ssh` (most desktops have it) | +| **Linux** (arm64) | [AppImage](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-arm64.AppImage) · [.deb](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-arm64.deb) | `ssh`, and `adb` for Android apps (`sudo apt install adb`) | -**iPhone and iPad:** the same features from your phone, with nothing to install on -a computer. Build it from [`ios/`](ios) in Xcode; see [docs/iphone.md](docs/iphone.md). +**Which build?** On a Mac, check Apple menu → About This Mac: the download is +for Apple Silicon (M-series); no Intel Mac build is published. On Windows, +check Settings → System → About → System type: the release is x64, not a native +Windows ARM build. On Linux, `uname -m` reports `x86_64` (x64) or `aarch64` +(arm64). Use `.deb` on Debian/Ubuntu, or AppImage on other distributions. -The app brings its own Python and `adb`; SSH is built into macOS and Windows. +**iPhone and iPad:** no App Store, TestFlight or `.ipa` download is provided in +the current release. It runs from your phone without a computer after setup. +Build it from [`ios/`](ios) in Xcode on a Mac with your own signing team; see +[docs/iphone.md](docs/iphone.md). + +The app brings its own Python and `adb`. macOS includes SSH; Windows needs the +OpenSSH Client optional feature (normally installed). From 0.4 it updates itself: when a new version is published, a banner offers **Update and restart**. It sends anonymous usage statistics, which you can turn off. Sharing compatibility results and error details is opt-in. See @@ -139,7 +152,7 @@ distribution's. If you already have `adb`, the app uses yours. There's no paid Apple developer account behind it, so macOS says the app is damaged or can't be checked. Drag it to Applications, then clear the download -quarantine once: +quarantine once, only for a download you trust from the releases linked above: ```sh xattr -dr com.apple.quarantine "/Applications/Frame Control.app" @@ -168,26 +181,44 @@ If it complains about FUSE, install `libfuse2` (Ubuntu 24.04+: `libfuse2t64`), or run it with `--appimage-extract-and-run`. +## Open the app + +- **macOS:** open the `.dmg`, drag **Frame Control** to **Applications**, then + open it from Applications. For the unsigned-app warning, use the macOS steps + above. Allow local network access and control of Terminal when prompted. +- **Windows:** run the installer, then open **Frame Control** from Start. For + the portable download, extract the entire `.zip` first and run + `Frame Control.exe` inside it. See the SmartScreen note above. +- **Linux:** install the `.deb` with your package installer and launch + **Frame Control** from your applications menu; or make the AppImage executable + and run it as described above. Install the OpenSSH client if `ssh` is missing. + ## Set up the headset -You type one password on the headset, once. Everything else happens on your -computer. +Keep the Frame awake and connect it and your computer to the same trusted local +network. Guest Wi-Fi/client isolation can prevent discovery and connections. 1. **On the Frame:** Steam Settings → System → **Enable Developer Mode**, then - in the Developer section, **Set User Password**. Pick something short: - you'll type it once more on your computer and then never again. -2. **On your computer:** open Frame Control. It offers to **Set Up - Connection**, which finds the headset, creates an SSH key, and asks for that - password once in a terminal window. If it can't find the Frame, type the - IP address from the Frame's Quick Settings. - - Before asking for the password it tries Valve's SteamOS devkit pairing: in - the headset, open Steam Settings → Developer → **Pair new host** and approve - the request, and no password is needed. (The service and the pairing-mode - step are verified on a Frame; the approval itself isn't yet. See - [SSH](docs/ssh.md#password-free-pairing-steamos-devkit-service).) -3. That's it. The app now reaches the headset whenever it's awake and on the - same network. For anywhere else, see [Tailscale](docs/tailscale.md). + Developer → **Set User Password**. Choose a password that isn't easy to guess: + it also protects `sudo` and remote desktop, even after SSH keys are set up. +2. **On your computer:** open Frame Control and choose **Set Up Connection…**. + If you dismissed the first-run prompt, use **Frame → Set Up Connection…**. + Setup opens a terminal and tries to find the headset. If it can't, use its IP + address from Quick Settings or Steam Settings → Internet. Windows/Linux setup + prompts for it; macOS exits instead. On macOS, return to the app → + **Devices & connection → + Add a headset…**, set SSH alias to `frame` for + the default headset, fill in **Address**, then choose **Set Up…**. +3. **Pair or enter the password:** setup first tries Valve's devkit pairing. + Open Steam Settings → Developer → **Pair new host** in the headset and approve + only the request you just started. The service and pairing-mode step are + verified on a Frame; approval and key installation through that flow remain + unverified. If pairing fails or times out, setup falls back to asking for the + Developer Mode password in the computer's terminal. Password typing may show + no characters; press Enter when done. See [SSH details](docs/ssh.md#password-free-pairing-steamos-devkit-service). +4. After setup succeeds, return to Frame Control. It uses SSH keys for later + connections whenever the Frame is awake and reachable. If it stays offline, + check Developer Mode, the address and network isolation, then run setup again. + For connections away from home, see [Tailscale](docs/tailscale.md). **What it changes:** only what you click. Installs go to your user account on the Frame (`--user` Flatpaks, Lepton instances, Steam downloads, sideloaded @@ -252,8 +283,8 @@ Frame's software fits together, all checked against a real headset and labelled loopback `127.0.0.1:8080`) only through SSH tunnels. The compatibility database key (maintainer-only) is never written to the repo. - `steamos` has `sudo`, protected by the same Developer Mode password. Once - you've switched to key auth, a short password still protects `sudo` and - RDP, so pick one that isn't trivially guessable. + you've switched to key auth, that password still protects `sudo` and RDP, + so pick one that isn't trivially guessable. - Don't port-forward 22, 3389, or 5555–5599 from your router. For remote access, use Tailscale: `scripts/tailscale-on-frame.sh` (no sudo). In its userspace mode **every** Frame port is reachable from your tailnet, including Steam's DevTools diff --git a/site/README.md b/site/README.md index 1180f4e..b9a83de 100644 --- a/site/README.md +++ b/site/README.md @@ -5,7 +5,8 @@ The Frame Control website, , on Cloudflare Page - `public/`: static pages. `/` is the landing page, `/feedback/` the feedback form, `/privacy/` the privacy note. - `functions/api/feedback.js`: `POST /api/feedback`, which turns the form into a GitHub issue labelled `feedback`. - `lib/feedback.js`: validation and issue formatting, tested by `test/feedback.test.mjs`. -- `public/js/site.js`: settings, including the Ko-fi page name for the donate buttons. +- `public/js/site.js`: settings, including the Ko-fi page name for the donate buttons, OS hints and opening-help deep links. OS hints lead to instructions, never directly to an architecture-specific installer. +- `test/onboarding.test.mjs`: desktop/mobile browser hints and opening-help deep links. ## Feedback → GitHub issues @@ -33,3 +34,30 @@ npx wrangler pages deploy --branch main # production ``` Point `GITHUB_REPO` in `.dev.vars` at a scratch repo when testing locally so test issues don't land on the real tracker. + +## First-run verification + +Before changing download or setup instructions: + +- Compare every linked filename with `gh release view --repo saphid/frame-control --json tagName,assets`. + Browser OS hints cannot establish CPU architecture; keep every platform choice visible. +- Run `node --test site/test/*.test.mjs` from the repo root, plus + `node --check site/public/js/site.js` and `node --check site/public/js/feedback.js`. +- Serve `public/` locally (for example `python3 -m http.server 8793 --directory site/public` + from the repo root). Check 320px, 390px, 768px and desktop widths: section navigation, + the hero link, download cards, opening-help links, feedback and privacy pages. Check + horizontal overflow and that anchor targets clear the sticky header. Repeat with + JavaScript disabled: downloads and navigation must remain available. +- Check setup labels against `app/main.js`, `scripts/connect.sh`, `ui/frame_connect.py` + and the Devices page. macOS discovery failure exits; Windows/Linux prompt for an IP. + The app's Devices → Add a headset dialog can pass an explicit address on all platforms. +- Separate website checks from native installation and real-headset pairing evidence. + A successful page/link check does not verify an installer or headset approval flow. + +On 2026-10-24, the first-run update was checked against published v0.4.16 assets: +all seven linked desktop installers returned HTTP 200 after redirects, as did the +linked GitHub documentation and trailer. Local page/resource links and section +fragments were checked; navigation stayed visible without horizontal overflow at +320, 390, 768 and 1280 CSS pixels. Native macOS/Windows installs and real-headset +pairing were not exercised for this documentation change. No site deployment was +performed. diff --git a/site/public/css/site.css b/site/public/css/site.css index 284a1f0..0e51a61 100644 --- a/site/public/css/site.css +++ b/site/public/css/site.css @@ -38,8 +38,13 @@ code { font: 14px ui-monospace, SFMono-Regular, Menlo, monospace; background: rg .top nav a { color: var(--muted); font-size: 14.5px; font-weight: 500; } .top nav a:hover, .top nav a[aria-current] { color: var(--bright); } .top .end { margin-left: auto; display: flex; gap: 10px; align-items: center; } -@media (max-width: 880px) { .top nav { display: none; } } -@media (max-width: 480px) { .top .end .ghost { display: none; } } +@media (max-width: 1050px) { + html { scroll-padding-top: 150px; } + .top .wrap { height: auto; min-height: 64px; flex-wrap: wrap; gap: 8px 16px; padding-top: 12px; padding-bottom: 8px; } + .top nav { order: 3; width: 100%; margin-left: 0; flex-wrap: wrap; gap: 0 18px; } + .top nav a { padding: 8px 0; } +} +@media (max-width: 480px) { .top .end { display: none; } } /* ---- buttons ---- */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 9px; height: 46px; padding: 0 22px; border-radius: 10px; @@ -199,3 +204,11 @@ textarea { min-height: 180px; resize: vertical; } .done p { color: var(--muted); margin: 10px auto 24px; max-width: 460px; } .done .cta { margin-top: 0; } .page h1 { font-size: clamp(34px, 5vw, 48px); } + +/* Opening instructions remain readable on narrow screens. */ +.dl p, .setup-note { color: var(--muted); font-size: 15px; margin-top: 16px; } +.dl { min-width: 0; } +code { overflow-wrap: anywhere; } +pre { white-space: pre-wrap; overflow-wrap: anywhere; } +pre code { background: none; padding: 0; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } diff --git a/site/public/index.html b/site/public/index.html index ad221f3..ca79464 100644 --- a/site/public/index.html +++ b/site/public/index.html @@ -4,7 +4,7 @@ Frame Control: manage your Steam Frame from your computer - + @@ -41,13 +41,13 @@
- Free and open source · macOS · Windows · Linux · iPhone + Free and open source · macOS · Windows · Linux

Your Steam Frame, managed from your desk.

-

See what the headset sees, install games and Android apps, move files and text across, and keep an eye on battery and status. All over SSH, with nothing to install on the Frame.

+

See what the headset sees, install games and Android apps, move files and text across, and keep an eye on battery and status. Install Frame Control on your computer, then connect to your headset over SSH. This website provides downloads and instructions; it does not control the headset in your browser.

- Download Frame Control + Choose your computer’s download @@ -173,7 +173,7 @@
Frame Control on iPhone: headset view and device status. @@ -186,23 +186,24 @@
Setup -

One password, typed once

-

Everything else happens on your computer.

+

Connect your computer to the Frame

+

First install and open the desktop app. Keep the headset awake, on the same trusted network as your computer. Guest Wi-Fi or client isolation can block the connection.

-

Turn on Developer Mode

-

On the Frame: Steam Settings → System → Enable Developer Mode, then Set User Password in the Developer section.

+

Prepare the headset

+

On the Frame: Steam Settings → System → Enable Developer Mode, then Developer → Set User Password. Choose a password that isn't easy to guess; it also protects remote desktop and sudo.

-

Set up the connection

-

Open Frame Control and choose Set Up Connection. It finds the headset, makes an SSH key, and asks for that password once.

+

Run connection setup

+

In the desktop app, choose Set Up Connection…. If you skipped the first prompt, use Frame → Set Up Connection…. A terminal opens. If discovery fails, find the Frame's IP in Quick Settings or Steam Settings → Internet. Windows/Linux setup prompts for it. On macOS, return to the app → Devices & connection → + Add a headset…, use SSH alias frame for the default headset, enter the IP in Address and choose Set Up….

-

That's it

-

The app reaches the headset whenever it's awake and on the same network. For anywhere else, use Tailscale.

+

Pair, with a password fallback

+

Setup tries Valve's pairing first: open Developer → Pair new host in the headset and approve only your request. If it fails or times out, enter the Developer Mode password in the computer's terminal (typing may be invisible), then press Enter.

+

The devkit service and pairing-mode step were checked on a Frame; approval and key installation through that flow remain unverified. Pairing details. After setup succeeds, return to the app; future connections use SSH keys. Still offline? Check Developer Mode, the IP and network isolation, then rerun setup. For use away from home, see Tailscale.

@@ -211,32 +212,39 @@
Download

Get Frame Control

-

Free, with Python and adb built in. Release notes

+

Choose your computer’s OS and processor, not the headset’s. Python is bundled; Linux needs ssh, and arm64 Linux also needs your distribution’s adb for Android apps. Release notes

-
-

macOS

- Apple Silicon +
+

macOS

+ Apple Silicon (M-series) only · no Intel Mac build +

Check Apple menu → About This Mac for your chip. Open the DMG, drag Frame Control to Applications, then open it there. Allow local network access and Terminal control when asked.

+

The app isn't notarized. macOS opening instructions.

-
-

Windows

+
+

Windows

Windows 10 or 11, x64 +

Check Settings → System → About → System type. No native Windows ARM build is published. Run the installer, then open Frame Control from Start. The OpenSSH Client optional feature must be installed (it normally is). For portable use, extract the entire ZIP and run Frame Control.exe.

+

The installer isn't code-signed. SmartScreen instructions.

-
-

Linux

+
+

Linux

x64 or arm64 · needs ssh +

Run uname -m: x86_64 means x64; aarch64 means arm64. Install the matching .deb on Debian/Ubuntu and open Frame Control from your applications menu, or use AppImage on other distributions.

+

AppImage permissions and FUSE help. On arm64 Debian/Ubuntu, install adb with sudo apt install adb for Android apps.

@@ -249,15 +257,22 @@

Good questions

-
+
macOS says the app is damaged or can't be checked -

The app isn't notarized, because there's no paid Apple developer account behind it. Drag it to Applications, then clear the download quarantine once in Terminal:

+

The app isn't notarized, because there's no paid Apple developer account behind it. Only for a download you trust from the releases linked above: drag it to Applications, then clear the download quarantine once in Terminal:

xattr -dr com.apple.quarantine "/Applications/Frame Control.app"

-
+
Windows SmartScreen says it protected my PC

The installer isn't code-signed. Choose More info → Run anyway, or use the portable .zip: unzip it anywhere and run Frame Control.exe.

+
+ How do I open the Linux AppImage? +

In a terminal in your download folder, use the exact filename for your architecture. For x64:

+
chmod +x Frame-Control-linux-x86_64.AppImage
+./Frame-Control-linux-x86_64.AppImage
+

If FUSE is missing, install libfuse2 (Ubuntu 24.04+: libfuse2t64), or run the same AppImage with --appimage-extract-and-run. Install your distribution’s OpenSSH client if ssh is missing.

+
What does it change on my headset?

Only what you click. Installs go to your user account on the Frame, and nothing needs sudo except the power buttons. On your computer it adds a Host frame entry to ~/.ssh/config and two SSH keys.

diff --git a/site/public/js/site.js b/site/public/js/site.js index c1c338d..4cf3706 100644 --- a/site/public/js/site.js +++ b/site/public/js/site.js @@ -5,8 +5,6 @@ const SITE = { kofi: "alexsouthwell", }; -const RELEASE = `https://github.com/${SITE.repo}/releases/latest/download/`; - // Donate buttons. for (const el of document.querySelectorAll("[data-kofi]")) { if (SITE.kofi) el.href = `https://ko-fi.com/${SITE.kofi}`; @@ -15,37 +13,39 @@ for (const el of document.querySelectorAll("[data-kofi]")) { for (const el of document.querySelectorAll("[data-needs-kofi]")) el.hidden = !SITE.kofi; for (const el of document.querySelectorAll("[data-no-kofi]")) el.hidden = !!SITE.kofi; -// Best guess at the visitor's platform, for the hero button and the download cards. +// Browser hints identify an OS, not a reliable CPU architecture. Always show +// the install instructions and all choices before starting a download. function detectPlatform() { const ua = navigator.userAgent; const hint = navigator.userAgentData?.platform || navigator.platform || ""; - if (/iPhone|iPad|iPod/.test(ua) || (/Mac/.test(hint) && navigator.maxTouchPoints > 1)) return "ios"; - if (/Android/.test(ua)) return null; + if (/iPhone|iPad|iPod|Android/.test(ua) || (/Mac/.test(hint) && navigator.maxTouchPoints > 1)) return null; if (/Mac/.test(hint) || /Macintosh/.test(ua)) return "mac"; if (/Win/.test(hint) || /Windows/.test(ua)) return "windows"; - if (/Linux/.test(hint) || /Linux/.test(ua)) return /aarch64|arm64/i.test(ua + hint) ? "linux-arm" : "linux"; + if (/Linux/.test(hint) || /Linux/.test(ua)) return "linux"; return null; } -const PLATFORMS = { - mac: { name: "macOS", file: "Frame-Control-mac-arm64.dmg" }, - windows: { name: "Windows", file: "Frame-Control-Setup-x64.exe" }, - linux: { name: "Linux", file: "Frame-Control-linux-x86_64.AppImage" }, - "linux-arm": { name: "Linux (arm64)", file: "Frame-Control-linux-arm64.AppImage" }, -}; - const platform = detectPlatform(); const hero = document.getElementById("hero-download"); -if (hero && PLATFORMS[platform]) { - hero.href = RELEASE + PLATFORMS[platform].file; - hero.querySelector("[data-label]").textContent = `Download for ${PLATFORMS[platform].name}`; +if (hero && platform) { + hero.href = `#download-${platform}`; + const name = { mac: "macOS", windows: "Windows", linux: "Linux" }[platform]; + hero.querySelector("[data-label]").textContent = `Choose a ${name} download`; } -const card = platform && document.querySelector(`.dl[data-os="${platform.replace("-arm", "")}"]`); +const card = platform && document.querySelector(`.dl[data-os="${platform}"]`); if (card) { card.classList.add("mine"); card.querySelector(".tag").hidden = false; } +// Keep linked opening instructions visible, including direct FAQ deep links. +function revealInstructions() { + const target = document.getElementById(location.hash.slice(1)); + if (target?.tagName === "DETAILS") target.open = true; +} +window.addEventListener("hashchange", revealInstructions); +revealInstructions(); + // Latest version number, so the page never goes stale. Fails quietly. const versionEls = document.querySelectorAll("[data-version]"); if (versionEls.length) { diff --git a/site/test/onboarding.test.mjs b/site/test/onboarding.test.mjs new file mode 100644 index 0000000..4ec1321 --- /dev/null +++ b/site/test/onboarding.test.mjs @@ -0,0 +1,59 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; + +const script = readFileSync(new URL('../public/js/site.js', import.meta.url), 'utf8'); +const html = readFileSync(new URL('../public/index.html', import.meta.url), 'utf8'); + +function visit(navigator, hash = '') { + const label = { textContent: 'Choose your computer’s download' }; + const hero = { href: '#download', querySelector: () => label }; + const cards = Object.fromEntries(['mac', 'windows', 'linux'].map(os => [os, { + classes: [], classList: { add(name) { cards[os].classes.push(name); } }, + tag: { hidden: true }, querySelector() { return this.tag; }, + }])); + const details = { tagName: 'DETAILS', open: false }; + const listeners = {}; + const context = { + navigator, location: { hash }, window: { addEventListener: (name, fn) => { listeners[name] = fn; } }, + document: { + querySelectorAll: () => [], + getElementById: id => id === 'hero-download' ? hero : id === 'mac-opening' ? details : null, + querySelector: selector => cards[selector.match(/data-os="([^"]+)"/)[1]], + }, + }; + runInNewContext(script, context); + return { hero, cards, details, listeners, context }; +} + +for (const [name, navigator, expected] of [ + ['Intel Mac', { platform: 'MacIntel', userAgent: 'Macintosh' }, 'mac'], + ['Apple Silicon browser with Intel hint', { platform: 'MacIntel', userAgent: 'Macintosh', userAgentData: { platform: 'macOS' } }, 'mac'], + ['Windows', { platform: 'Win32', userAgent: 'Windows NT 10.0' }, 'windows'], + ['Linux x64', { platform: 'Linux x86_64', userAgent: 'Linux x86_64' }, 'linux'], + ['Linux ARM', { platform: 'Linux aarch64', userAgent: 'Linux aarch64' }, 'linux'], + ['iPhone', { platform: 'iPhone', userAgent: 'iPhone' }, null], + ['iPad desktop mode', { platform: 'MacIntel', userAgent: 'Macintosh', maxTouchPoints: 5 }, null], + ['Android', { platform: 'Linux armv8l', userAgent: 'Linux Android' }, null], + ['unknown', { userAgent: '' }, null], +]) { + test(`${name} opens choices instead of guessing an installer architecture`, () => { + const { hero, cards } = visit(navigator); + assert.equal(hero.href, expected ? `#download-${expected}` : '#download'); + assert.ok(html.includes(`id="${hero.href.slice(1)}"`)); + assert.deepEqual(Object.keys(cards).filter(os => !cards[os].tag.hidden), expected ? [expected] : []); + }); +} + +test('opening help expands for direct and in-page links; unknown fragments are safe', () => { + const result = visit({ userAgent: '' }, '#mac-opening'); + assert.equal(result.details.open, true); + result.details.open = false; + result.context.location.hash = '#unknown'; + result.listeners.hashchange(); + assert.equal(result.details.open, false); + result.context.location.hash = '#mac-opening'; + result.listeners.hashchange(); + assert.equal(result.details.open, true); +});