|
| 1 | +# Testing puter.js |
| 2 | + |
| 3 | +This document covers the automated end-to-end test suite for `puter.js` UI APIs (`setMenubar`, `contextMenu`, etc.). The suite uses Playwright to drive a real Puter desktop and exercise the APIs the way real users hit them — including layout interaction bugs that pure-JS unit tests can't see. |
| 4 | + |
| 5 | +The suite started as a regression net and is structured to grow incrementally as more `puter.ui` methods are covered. |
| 6 | + |
| 7 | +## Quick start (first run on a new machine) |
| 8 | + |
| 9 | +1. **Start Puter desktop** (monorepo root): |
| 10 | + ```sh |
| 11 | + cd /path/to/puter |
| 12 | + npm start |
| 13 | + ``` |
| 14 | + On first launch this prints a credentials block: |
| 15 | + ``` |
| 16 | + ************************************************************ |
| 17 | + * Your default login credentials are: |
| 18 | + * Username: admin |
| 19 | + * Password: <hex string> |
| 20 | + ************************************************************ |
| 21 | + ``` |
| 22 | + Copy the password — it's stable across restarts. |
| 23 | + |
| 24 | +2. **Start the puter-js dev server** (serves `dist/puter.dev.js` and the test fixture on `localhost:8080`): |
| 25 | + ```sh |
| 26 | + cd /path/to/puter/src/puter-js |
| 27 | + npm start |
| 28 | + ``` |
| 29 | + Wait for webpack's "compiled successfully". |
| 30 | + |
| 31 | +3. **Install Playwright browsers** (one-time): |
| 32 | + ```sh |
| 33 | + cd /path/to/puter/src/puter-js |
| 34 | + npm install |
| 35 | + npm run playwright:install |
| 36 | + ``` |
| 37 | + |
| 38 | +4. **Set the admin password** (one-time, see [Setting the password](#setting-the-password) below for the recommended `.env` approach). |
| 39 | + |
| 40 | +5. **Run the tests**: |
| 41 | + ```sh |
| 42 | + cd /path/to/puter/src/puter-js |
| 43 | + npm run test:e2e |
| 44 | + ``` |
| 45 | + |
| 46 | +## Setting the password |
| 47 | + |
| 48 | +The admin password (from step 1) is the only credential the test suite needs. **Never hardcode it.** Two ways to provide it, pick one: |
| 49 | + |
| 50 | +### Option A: `.env` file (recommended) |
| 51 | + |
| 52 | +The `tests/e2e/.env` file is gitignored, so credentials never leave your machine. |
| 53 | + |
| 54 | +```sh |
| 55 | +cd /path/to/puter/src/puter-js |
| 56 | +cp tests/e2e/.env.example tests/e2e/.env |
| 57 | +``` |
| 58 | + |
| 59 | +Edit `tests/e2e/.env` and set the password you copied from step 1: |
| 60 | + |
| 61 | +``` |
| 62 | +PUTER_ADMIN_PASSWORD=<your password> |
| 63 | +``` |
| 64 | + |
| 65 | +That's it. All `npm run test:e2e*` commands pick it up automatically via `globalSetup`. |
| 66 | + |
| 67 | +### Option B: shell environment variable |
| 68 | + |
| 69 | +```sh |
| 70 | +export PUTER_ADMIN_PASSWORD=<your password> |
| 71 | +npm run test:e2e |
| 72 | +``` |
| 73 | + |
| 74 | +Add the `export` to your shell rc (`~/.zshrc`, `~/.bashrc`) to persist across sessions. Shell env always wins over `.env`. |
| 75 | + |
| 76 | +### When the password changes |
| 77 | + |
| 78 | +The admin password is stored in Puter's KV under `tmp_password` and survives restarts, so it changes rarely (typically only if you wipe the local Puter database). When it does change, update your `.env` and force a fresh login: |
| 79 | + |
| 80 | +```sh |
| 81 | +PUTER_TEST_RESET_AUTH=1 npm run test:e2e |
| 82 | +``` |
| 83 | + |
| 84 | +## Running tests |
| 85 | + |
| 86 | +All commands run from `src/puter-js`: |
| 87 | + |
| 88 | +| Command | What it does | |
| 89 | +| --- | --- | |
| 90 | +| `npm run test:e2e` | Headless run, both projects (`chromium` + `mobile-chromium`). | |
| 91 | +| `npm run test:e2e:headed` | Same, but watches the browser drive Puter desktop — useful for debugging selectors. | |
| 92 | +| `npm run test:e2e:ui` | Playwright UI mode (timeline scrubber, retry single test, picker for selectors). | |
| 93 | +| `npm run test:e2e:record` | Records video, trace, and screenshots for **every** test (passing or failing). Files land in `test-results/<spec>/`. | |
| 94 | +| `npm run test:e2e:report` | Opens the HTML report from the last run. | |
| 95 | +| `npx playwright test --project=mobile-chromium` | Run only the mobile project (the contextMenu z-index regression test lives here). | |
| 96 | + |
| 97 | +By default, video and trace are saved **only on failure**. `test:e2e:record` enables them for everything. |
| 98 | + |
| 99 | +## How it works |
| 100 | + |
| 101 | +``` |
| 102 | +┌─────────────────────────────────────────────────────────────┐ |
| 103 | +│ Playwright (chromium / mobile-chromium) │ |
| 104 | +│ ↓ navigates to │ |
| 105 | +│ http://puter.localhost:4100/app/puter-js-testing-<uuid> │ |
| 106 | +│ │ |
| 107 | +│ ┌─── Puter desktop (renders menus, contextMenus) ──────┐ │ |
| 108 | +│ │ ┌─── App iframe (the fixture) ──────────────────┐ │ │ |
| 109 | +│ │ │ loads dist/puter.dev.js │ │ │ |
| 110 | +│ │ │ calls puter.ui.setMenubar({...}) │ │ │ |
| 111 | +│ │ │ buttons trigger puter.ui.contextMenu({...}) │ │ │ |
| 112 | +│ │ │ logs interactions to <div id="log"> │ │ │ |
| 113 | +│ │ └───────────────────────────────────────────────┘ │ │ |
| 114 | +│ └──────────────────────────────────────────────────────┘ │ |
| 115 | +└─────────────────────────────────────────────────────────────┘ |
| 116 | +``` |
| 117 | + |
| 118 | +### Authentication (once per run, cached for 24h) |
| 119 | + |
| 120 | +`globalSetup` runs at the start of the test run: |
| 121 | + |
| 122 | +1. Direct `POST /login` from Node with the admin credentials → JWT token. |
| 123 | +2. Probes `window.api_origin` from Puter (server-templated) so we can pass it through. |
| 124 | +3. Opens chromium once and navigates to `puter.localhost:4100/?auth_token=<token>&api_origin=<origin>`. Puter's `initgui` auth_token handler runs the full auth setup: `puter.setAuthToken`, `puter.setAPIOrigin`, `/session/sync-cookie`, `update_auth_data`. |
| 125 | +4. Saves cookies + localStorage to `tests/e2e/.auth/state.json` (gitignored, cached for 24h). |
| 126 | + |
| 127 | +Every test context loads with that `storageState` → already signed in as admin → no per-test `/login`, no `/signup`, no rate limits. |
| 128 | + |
| 129 | +### Per-test flow |
| 130 | + |
| 131 | +For each test: |
| 132 | + |
| 133 | +1. **Test's `page` fixture** opens with the cached storageState — already authenticated. |
| 134 | +2. **`registerTestApp(page)`** verifies the fixture URL is reachable, navigates to `puter.localhost:4100/`, waits for the SDK to settle, and calls `puter.apps.create('puter-js-testing-<uuid>', '<fixture URL>')`. |
| 135 | +3. **`gotoTestApp(page, name)`** navigates to `puter.localhost:4100/app/<name>`. Puter desktop's `/app/<name>` handler calls `puter.apps.get(name)` → `launch_app(...)` → opens a window with an iframe at the fixture URL (with `?puter.app_instance_id=...` so puter.js detects `env=app`). Waits for `body.ready` inside the iframe. |
| 136 | +4. **Test body**: clicks fixture buttons inside the iframe → fixture calls `puter.ui.setMenubar(...)` / `puter.ui.contextMenu(...)`. puter.js postMessages to Puter desktop → desktop renders `.window-menubar` / `.context-menu` in the **parent** DOM. Playwright asserts on those parent-frame elements and clicks items. Each click fires the item's `action` callback **back inside the iframe** (via Puter's RPC hydration), which appends a known string to `<div id="log">`. Assertions check that the right log entry appeared. |
| 137 | +5. **`deleteTestApp(page, name)`** in `finally` — best-effort cleanup of the ephemeral app. |
| 138 | + |
| 139 | +### Two viewports, one regression test |
| 140 | + |
| 141 | +Both projects (`chromium` for desktop, `mobile-chromium` for `iPhone 13`) run the same specs. The mobile contextMenu test is the named regression for commit `aa5e398e`'s z-index bug — if the dismiss-overlay regresses to sit above the menu, the tap never reaches the item, the log stays empty, the test fails. |
| 142 | + |
| 143 | +## File layout |
| 144 | + |
| 145 | +``` |
| 146 | +src/puter-js/ |
| 147 | +├── playwright.config.js # projects, globalSetup, recording flags |
| 148 | +├── TESTING.md # this file |
| 149 | +└── tests/e2e/ |
| 150 | + ├── README.md # short pointer, defers here |
| 151 | + ├── .env # YOUR password (gitignored) |
| 152 | + ├── .env.example # committed template, no secrets |
| 153 | + ├── .auth/ # cached auth state (gitignored) |
| 154 | + ├── globalSetup.js # signs in once, saves storageState |
| 155 | + ├── helpers/ |
| 156 | + │ └── testApp.js # registerTestApp / gotoTestApp / deleteTestApp / waitForPuterReady |
| 157 | + ├── fixtures/ |
| 158 | + │ └── menubar-contextmenu.html # the Puter app under test |
| 159 | + └── specs/ |
| 160 | + ├── menubar.spec.js |
| 161 | + └── contextMenu.spec.js |
| 162 | +``` |
| 163 | + |
| 164 | +The legacy `test/` (singular) folder is the manual browser harness for `puter.ai` / `puter.fs` / `puter.kv` / `puter.txt2speech` and stays as-is — different purpose, different audience. |
| 165 | + |
| 166 | +## Adding new tests |
| 167 | + |
| 168 | +The harness is set up to scale linearly. To cover another `puter.ui` method (e.g. `alert`, `prompt`, `notify`, `showOpenFilePicker`): |
| 169 | + |
| 170 | +1. Add a button to `tests/e2e/fixtures/menubar-contextmenu.html` (or create a new fixture file) that calls the method with a known spec and logs the response into `<div id="log">`. |
| 171 | +2. Add a spec under `tests/e2e/specs/` following the same shape as `menubar.spec.js`: |
| 172 | + - `registerTestApp` → `gotoTestApp` → click fixture button → assert on Puter desktop's rendered DOM in the parent frame → click items / verify response → assert log entry appeared. |
| 173 | +3. If the method has different desktop/mobile rendering, that's automatically exercised by both projects. |
| 174 | + |
| 175 | +For methods that don't render UI (like `disableMenuItem`, `setMenuItemChecked`), assert on the resulting state changes in the menubar DOM instead of on log entries. |
| 176 | + |
| 177 | +## Configuration reference |
| 178 | + |
| 179 | +All env vars are optional unless marked required. |
| 180 | + |
| 181 | +| Variable | Default | Purpose | |
| 182 | +| --- | --- | --- | |
| 183 | +| `PUTER_ADMIN_PASSWORD` | _(required)_ | Local Puter admin password (from `npm start`'s startup banner). Stable across restarts. | |
| 184 | +| `PUTER_ADMIN_USERNAME` | `admin` | Local Puter admin username. | |
| 185 | +| `PUTER_TEST_ORIGIN` | `http://puter.localhost:4100` | The Puter desktop origin Playwright drives. | |
| 186 | +| `PUTER_TEST_FIXTURE_ORIGIN` | `http://localhost:8080` | Where the fixture HTML is served. The Puter app's `indexURL` is set to `${PUTER_TEST_FIXTURE_ORIGIN}/tests/e2e/fixtures/menubar-contextmenu.html`. | |
| 187 | +| `PUTER_TEST_RECORD` | unset | When `1`, records video for every test (default: only on failure). | |
| 188 | +| `PUTER_TEST_RESET_AUTH` | unset | When `1`, ignores the cached auth in `tests/e2e/.auth/state.json` and forces a fresh `/login`. Use after a password change or if Puter rejects the cached token. | |
| 189 | + |
| 190 | +## Troubleshooting |
| 191 | + |
| 192 | +| Symptom | Cause | Fix | |
| 193 | +| --- | --- | --- | |
| 194 | +| `PUTER_ADMIN_PASSWORD is not set` | Env var or `.env` missing. | Follow [Setting the password](#setting-the-password). | |
| 195 | +| `POST /login → HTTP 400: Username not found` | Local Puter hasn't created the admin user yet. | Run `npm start` from monorepo root and wait for the credentials banner. | |
| 196 | +| `POST /login → HTTP 400: Invalid password.` | Password in your `.env` is wrong. | Re-check the banner, update `.env`, then `PUTER_TEST_RESET_AUTH=1 npm run test:e2e`. | |
| 197 | +| `POST /login → HTTP 403: Forbidden` | Puter's CSRF check rejected the request. | Should not happen — globalSetup sets the `Origin` header. If it does, confirm `PUTER_TEST_ORIGIN` matches Puter's actual origin. | |
| 198 | +| `Fixture URL is unreachable` | puter-js dev server isn't running. | From `src/puter-js`: `npm start`, wait for "compiled successfully". | |
| 199 | +| `puter.apps.create failed: Unauthorized` with `APIOrigin: "https://api.puter.com"` | The cached `storageState` is from before the api_origin fix. | `rm -rf tests/e2e/.auth && npm run test:e2e`. | |
| 200 | +| Test launches a window but iframe stays blank | puter-js dev server stopped mid-run, or fixture path is wrong. | Restart `npm start` in `src/puter-js` and re-run. | |
| 201 | +| `body.ready` timeout in `gotoTestApp` | Puter desktop didn't open the app window. Usually means `puter.apps.get(name)` 404'd. | Check the test trace for `puter.apps.create` errors above this one. | |
| 202 | +| Mobile contextMenu test fails on `expect(log entry).toBeVisible()` | The actual bug we're guarding against — overlay z-index regression on mobile. | Fix the GUI's `.context-menu-sheet-backdrop` z-index (see commit `aa5e398e`). | |
| 203 | + |
| 204 | +## CI |
| 205 | + |
| 206 | +Not wired up yet. Local runs depend on Puter being started via `npm start` (which auto-creates the admin user on first launch). CI will need a different bootstrap path — TBD when we get there. |
0 commit comments