Skip to content

Commit 36c9c47

Browse files
authored
Merge pull request #3073 from HeyPuter/puter-js-tests
Puter JS Web Component update
2 parents 90d01b8 + 189dae2 commit 36c9c47

14 files changed

Lines changed: 900 additions & 16 deletions

File tree

package-lock.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/puter-js/TESTING.md

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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.

src/puter-js/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,19 @@
3131
"start-webpack": "webpack --stats=errors-only && webpack --output-filename puter.dev.js --watch --devtool source-map --stats=errors-only",
3232
"start": "concurrently \"npm run start-server\" \"npm run start-webpack\"",
3333
"test": "npx http-server --cors -c-1 -o /test",
34+
"test:e2e": "playwright test",
35+
"test:e2e:headed": "playwright test --headed",
36+
"test:e2e:ui": "playwright test --ui",
37+
"test:e2e:record": "PUTER_TEST_RECORD=1 playwright test",
38+
"test:e2e:report": "playwright show-report",
39+
"playwright:install": "playwright install chromium",
3440
"build": "webpack && { echo \"// Copyright 2024-present Puter Technologies Inc. All rights reserved.\"; echo \"// Generated on $(date '+%Y-%m-%d %H:%M')\n\"; cat ./dist/puter.js; } > temp && mv temp ./dist/puter.js",
3541
"prepublishOnly": "npm run build && mv dist/puter.js dist/puter.cjs && npm version patch"
3642
},
3743
"author": "Puter Technologies Inc.",
3844
"license": "Apache-2.0",
3945
"devDependencies": {
46+
"@playwright/test": "^1.49.0",
4047
"concurrently": "^8.2.2",
4148
"http-server": "^14.1.1",
4249
"webpack-cli": "^5.1.4"

src/puter-js/playwright.config.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
const PUTER_ORIGIN = process.env.PUTER_TEST_ORIGIN || 'http://puter.localhost:4100';
7+
const FIXTURE_ORIGIN = process.env.PUTER_TEST_FIXTURE_ORIGIN || 'http://localhost:8080';
8+
const RECORD_ALL = !!process.env.PUTER_TEST_RECORD;
9+
const STORAGE_STATE_PATH = path.join(__dirname, 'tests', 'e2e', '.auth', 'state.json');
10+
11+
export default defineConfig({
12+
testDir: './tests/e2e/specs',
13+
fullyParallel: false,
14+
workers: 1,
15+
forbidOnly: !!process.env.CI,
16+
retries: 0,
17+
reporter: 'list',
18+
timeout: 120_000,
19+
expect: { timeout: 15_000 },
20+
globalSetup: './tests/e2e/globalSetup.js',
21+
use: {
22+
baseURL: PUTER_ORIGIN,
23+
storageState: STORAGE_STATE_PATH,
24+
trace: 'retain-on-failure',
25+
video: RECORD_ALL ? 'on' : 'retain-on-failure',
26+
screenshot: 'only-on-failure',
27+
},
28+
projects: [
29+
{
30+
name: 'chromium',
31+
use: { ...devices['Desktop Chrome'] },
32+
},
33+
{
34+
name: 'mobile-chromium',
35+
use: { ...devices['iPhone 13'] },
36+
},
37+
],
38+
metadata: {
39+
puterOrigin: PUTER_ORIGIN,
40+
fixtureOrigin: FIXTURE_ORIGIN,
41+
},
42+
});

0 commit comments

Comments
 (0)