From 47a8410e39720f3756ba131a0648b5cd05d96d26 Mon Sep 17 00:00:00 2001 From: nbogie Date: Sat, 30 May 2026 12:16:03 +0100 Subject: [PATCH 01/11] install and init playwright for browser-based integration tests --- .gitignore | 10 ++++- package-lock.json | 83 +++++++++++++++++++++++++++++++++++---- package.json | 6 ++- playwright.config.js | 81 ++++++++++++++++++++++++++++++++++++++ tests/about-playwright.md | 29 ++++++++++++++ tests/example.spec.js | 19 +++++++++ 6 files changed, 217 insertions(+), 11 deletions(-) create mode 100644 playwright.config.js create mode 100644 tests/about-playwright.md create mode 100644 tests/example.spec.js diff --git a/.gitignore b/.gitignore index 2ee8937..f31527d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,12 @@ dist/ out/ .DS_Store /src/.DS_Store -.vscode \ No newline at end of file +.vscode +local.code-workspace + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/package-lock.json b/package-lock.json index be94487..58ce7bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,12 @@ "devDependencies": { "@babel/core": "^7.24.5", "@babel/preset-env": "^7.24.5", + "@playwright/test": "^1.60.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", + "@types/node": "^25.9.1", "babel-loader": "^9.1.3", "rollup": "^2.79.1", "rollup-plugin-ignore": "^1.0.10", @@ -1807,6 +1809,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-babel": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", @@ -1958,12 +1976,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", - "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/resolve": { @@ -4347,6 +4366,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5288,10 +5354,11 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", diff --git a/package.json b/package.json index ed9bb58..29d564d 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "0.3.0", "description": "p5.sound is a minimal wrapper for Tone.js designed to extend the musical and audio capabilities of the p5.js core library.", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "rollup -c" + "build": "rollup -c", + "test:integration": "npx playwright test" }, "keywords": [ "p5.js", @@ -26,10 +26,12 @@ "devDependencies": { "@babel/core": "^7.24.5", "@babel/preset-env": "^7.24.5", + "@playwright/test": "^1.60.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", + "@types/node": "^25.9.1", "babel-loader": "^9.1.3", "rollup": "^2.79.1", "rollup-plugin-ignore": "^1.0.10", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..3967e58 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,81 @@ +// @ts-check +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); + diff --git a/tests/about-playwright.md b/tests/about-playwright.md new file mode 100644 index 0000000..a344c9e --- /dev/null +++ b/tests/about-playwright.md @@ -0,0 +1,29 @@ +Visit https://playwright.dev/docs/intro for more information. ✨ + +You can run several commands: + + npx playwright test + Runs the end-to-end tests. + + npx playwright test --ui + Starts the interactive UI mode. + + npx playwright test --project=chromium + Runs the tests only on Desktop Chrome. + + npx playwright test example + Runs the tests in a specific file. + + npx playwright test --debug + Runs the tests in debug mode. + + npx playwright codegen + Auto generate tests with Codegen. + +We suggest that you begin by typing: + + npx playwright test + +And check out the following files: + - ./tests/example.spec.js - Example end-to-end test + - ./playwright.config.js - Playwright Test configuration diff --git a/tests/example.spec.js b/tests/example.spec.js new file mode 100644 index 0000000..26ed206 --- /dev/null +++ b/tests/example.spec.js @@ -0,0 +1,19 @@ +// @ts-check +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); From f46f078b22147184a1f799a15d7dbafd5c80d2c1 Mon Sep 17 00:00:00 2001 From: nbogie Date: Sat, 30 May 2026 12:16:40 +0100 Subject: [PATCH 02/11] Add browser-based tests of examples on web editor for firefox and chromium --- package.json | 4 +- playwright.config.js | 9 +- tests/{ => integration}/about-playwright.md | 20 +- tests/integration/about-these-tests.md | 32 ++ tests/{ => integration}/example.spec.js | 0 .../test-examples-on-web-editor.spec.js | 347 ++++++++++++++++++ 6 files changed, 397 insertions(+), 15 deletions(-) rename tests/{ => integration}/about-playwright.md (56%) create mode 100644 tests/integration/about-these-tests.md rename tests/{ => integration}/example.spec.js (100%) create mode 100644 tests/integration/test-examples-on-web-editor.spec.js diff --git a/package.json b/package.json index 29d564d..2253fcf 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "p5.sound is a minimal wrapper for Tone.js designed to extend the musical and audio capabilities of the p5.js core library.", "scripts": { "build": "rollup -c", - "test:integration": "npx playwright test" + "test:integration:chromium-only": "npx playwright test test-examples-on-web-editor --project=chromium", + "test:integration": "npx playwright test test-examples-on-web-editor", + "test:integration:ui": "npx playwright test test-examples-on-web-editor --ui" }, "keywords": [ "p5.js", diff --git a/playwright.config.js b/playwright.config.js index 3967e58..0616b33 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -45,10 +45,11 @@ export default defineConfig({ use: { ...devices['Desktop Firefox'] }, }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + //We haven't investigated setting up mic+camera permissions and auto-start of audio context on safari + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, /* Test against mobile viewports. */ // { diff --git a/tests/about-playwright.md b/tests/integration/about-playwright.md similarity index 56% rename from tests/about-playwright.md rename to tests/integration/about-playwright.md index a344c9e..32b51ff 100644 --- a/tests/about-playwright.md +++ b/tests/integration/about-playwright.md @@ -2,28 +2,28 @@ Visit https://playwright.dev/docs/intro for more information. ✨ You can run several commands: - npx playwright test + `npx playwright test` Runs the end-to-end tests. - npx playwright test --ui + `npx playwright test --ui` Starts the interactive UI mode. - npx playwright test --project=chromium + `npx playwright test --project=chromium` Runs the tests only on Desktop Chrome. - npx playwright test example + `npx playwright test example` Runs the tests in a specific file. - npx playwright test --debug + `npx playwright test --debug` Runs the tests in debug mode. - npx playwright codegen + `npx playwright codegen` Auto generate tests with Codegen. We suggest that you begin by typing: - +```bash npx playwright test - +``` And check out the following files: - - ./tests/example.spec.js - Example end-to-end test - - ./playwright.config.js - Playwright Test configuration + - `./tests/integration/example.spec.js` - Example end-to-end test + - `./playwright.config.js` - Playwright Test configuration diff --git a/tests/integration/about-these-tests.md b/tests/integration/about-these-tests.md new file mode 100644 index 0000000..2ac3cd8 --- /dev/null +++ b/tests/integration/about-these-tests.md @@ -0,0 +1,32 @@ +# About these tests + +See also: [about-playwright.md](./about-playwright.md) + +`tests/integration/test-examples-on-web-editor.spec.js` is a Playwright spec that smoke-tests a list of p5.sound example sketch on the web editor looking for console errors. + +**Where are the sketches?:** +The sketches are tested in place in the web editor. +At the moment that is where the example sketches live and where users will find them, so that's the best place to test them. +It _would_ be easier to test them if they were in this repo. + +**What does each test do?:** Each test loads the sketch → dismisses the cookie banner → presses Play → waits for the canvas → clicks inside it → lets it run → stops it → asserts **zero** console errors / uncaught exceptions. +Mic/camera are pre-granted at the context level (native prompts can't be clicked by Playwright), and `--autoplay-policy=no-user-gesture-required` lets audio start. These settings are made via a chromium-only mechanism. + +**Difficulties in testing on the web editor:** +- The preview is **two iframes deep** (`iframe[title="sketch preview"]` → blob: child → `#defaultCanvas0`). +- There's a **race**: the editor ships sketch code to the preview sandbox over `postMessage`; pressing Play too early means the code never arrives and no canvas renders. A short settle wait before Play fixes it (this is what your original `waitForTimeout(10000)` was hacking around). +- p5 marks the canvas `data-hidden="true"` during setup, so the test waits for `attached` (not `visible`) and forces the click — otherwise the strict visibility wait would mask the real signal. + +**Result (stable across parallel and serial runs): 18 pass, 4 fail** — and the 4 failures are *real bugs in the examples*, which is exactly the point: + +| Sketch | Error | +|---|---| +| 002-Amplitude-VisualizingLoudness | `loadSound is not defined`, `sound is not defined`, `...reading 'pixels'` | +| 004-OscillatorAmplitudeLFOmodulation | `Failed to execute 'connect' on 'AudioNode': Overload resolution failed.` | +| 006-DelayTime-Envelope_b | same `AudioNode connect` error | +| 010-PitchShifterOnSampleEnded | `loadSound is not defined` | + +The `connect`/`loadSound` failures look like they could be genuine p5.sound API regressions worth investigating separately. + +Two notes: +- This test suite is **Chromium-only** (`test.skip` on other browsers) because the mic/camera permissions and autoplay flag are Chromium-specific. Run with `npx playwright test test-examples-on-web-editor --project=chromium`. diff --git a/tests/example.spec.js b/tests/integration/example.spec.js similarity index 100% rename from tests/example.spec.js rename to tests/integration/example.spec.js diff --git a/tests/integration/test-examples-on-web-editor.spec.js b/tests/integration/test-examples-on-web-editor.spec.js new file mode 100644 index 0000000..536068e --- /dev/null +++ b/tests/integration/test-examples-on-web-editor.spec.js @@ -0,0 +1,347 @@ +//@ts-check +import { test as testOriginal, expect } from "@playwright/test"; + +/** + * Smoke-tests every sketch in the p5.sound examples collection on the p5 web + * editor, asserting that running each one produces no console errors or uncaught + * exceptions. + * + * The sketch list below was extracted once (on 2026-05-30) from the collection: + * https://editor.p5js.org/thomasjohnmartinez/collections/Dp0zGclVL + * The test runs against this literal list rather than re-scraping the collection + * at runtime, so it is deterministic and doesn't depend on the collection page's + * markup. Re-extract and update SKETCHES when the collection changes. + * + * Assumptions: + * * all examples will render a canvas (it will be clicked) + * * no example needs browser permissions beyond camera and microphone + * + * Notes on driving the editor reliably: + * - The preview runs in a cross-origin sandbox (preview.p5js.org). The editor + * ships the sketch code to that sandbox over postMessage; if we press Play + * before that channel is established the code never arrives and no canvas ever + * renders. We therefore wait for the editor to settle before pressing Play + * (see SETTLE_BEFORE_PLAY_MS). + * - The canvas ends up two iframes deep: iframe[title="sketch preview"] (the + * preview.p5js.org frame) → a blob: child iframe → #defaultCanvas0. + */ + +/** + * @typedef {import("@playwright/test").Page} Page + * @typedef {import("@playwright/test").Locator} Locator + * @typedef {import("@playwright/test").ConsoleMessage} ConsoleMessage + */ + +/** + * A single captured problem (console error or uncaught page exception). + * @typedef {Object} CapturedError + * @property {"console" | "pageerror"} kind + * @property {string} text + * @property {string} url The page URL at the time the problem was captured. + */ + +/** + * One sketch in the collection. + * @typedef {Object} Sketch + * @property {string} name Human-readable name, used as the test title. + * @property {string} url Direct URL to the sketch on the p5 web editor. + */ + +/** + * The launch/context setup one browser needs to run audio + getUserMedia sketches + * headlessly without prompts or gesture gating. + * @typedef {Object} BrowserSetup + * @property {string[]} permissions Context permissions to grant up front. + * @property {import("@playwright/test").LaunchOptions} launchOptions + */ + +/** @type {Sketch[]} */ +const SKETCHES = [ + { name: "001-Oscillator-FrequencyAmplitude", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/z-KkeTrcu" }, + { name: "002-Amplitude-VisualizingLoudness", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/Wlcnc6WCD" }, + { name: "003-Microphone-Effects", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/5NV6gUkWM" }, + { name: "004-OscillatorAmplitudeLFOmodulation", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/9bsyBm86Q" }, + { name: "005-Oscillator-Reverb", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/eMQrmFczQ" }, + { name: "006-DelayTime-Envelope", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/Dk95S298f" }, + { name: "006-DelayTime-Envelope_b", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/2ay47nReh" }, + { name: "006-EnvelopeAndfilter", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/buaI5fkJC" }, + { name: "007-Envelope-Attack-Release", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/jx8TmJAST" }, + { name: "008-FFT-WaveForm-Visualize", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/GKLghF22G" }, + { name: "008_b-FFT-WaveForm-VisualizeSoundFile", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/dQFLbAwch" }, + { name: "009-NoiseTypes", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/KSE9hEBCu" }, + { name: "010-PitchShifterOnSampleEnded", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/kq0zqgdmL" }, + { name: "011-ReverbDecayTime", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/6tyyCCbEg" }, + { name: "012-SoundFileSetPath", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/eQQsm5apX" }, + { name: "013-MultiSamplePlaybackWithAmplitudeAnalysis", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/lYJ5w-tbL" }, + { name: "014-3DSoundSource", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/vEvHsr3c-" }, + { name: "015-SoundFile3DScale", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/hvhRcqrqi" }, + { name: "016-String-Synthesis", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/1erR4NUQd" }, + { name: "016-String-Synthesis_b", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/n_owBAPTN" }, + { name: "018-Oscillator-Delay", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/aGXHwoPVm" }, + { name: "p5-to-tone", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/W0_fe403s" }, +]; + +/** + * Console-error texts we deliberately ignore — these are artifacts of running + * sketches in an automated browser, not real sketch/library failures: + * + * - 404s: the p5 editor prefetches URLs without a trailing slash, producing a + * benign 404. + * - AudioContext autoplay: even with autoplay enabled (see BROWSER_SETUP), a + * sketch that starts audio before our canvas-click gesture can still log this + * once; it reflects the headless environment, not the sketch. + * @type {RegExp[]} + */ +const IGNORED_CONSOLE_PATTERNS = [ + /Failed to load resource: the server responded with a status of 404/, + /The AudioContext was not allowed to start/, +]; + +/** + * How long to let the editor settle after load before pressing Play, so the + * editor↔preview-sandbox postMessage channel is established first. + */ +const SETTLE_BEFORE_PLAY_MS = 3000; + +/** How long to let the sketch run (and potentially throw) after the canvas appears. */ +const SKETCH_RUN_MS = 3000; + +/** Max time (ms) to wait for the play button to become visible. */ +const MAX_WAIT_FOR_PLAY_BUTTON_VISIBLE_MS = 15_000; +/** Max time (ms) to wait for the canvas to be in attached state. */ +const MAX_WAIT_FOR_ATTACHED_CANVAS = 20_000; +/** Max time (ms) to wait for the canvas to become visible before clicking it. */ +const MAX_WAIT_FOR_VISIBLE_CANVAS_MS = 5_000; +/** Max time (ms) for a single canvas click attempt. */ +const CANVAS_CLICK_TIMEOUT_MS = 5_000; + +/** + * Configs for per-browser setup. The keys are the browsers this spec runs on; a browser not + * listed here (e.g. webkit) is skipped. + * Each browser needs a different mechanism to: + * (a) satisfy granting getUserMedia (cameraµphone access) without a prompt and + * (b) let audio start without a user gesture. + * @type {Record} + */ +const BROWSER_SETUP = { + chromium: { + // Grant mic/camera so getUserMedia() resolves without a native prompt. + permissions: ["microphone", "camera"], + // --autoplay-policy lets audio start without a user gesture. + launchOptions: { args: ["--autoplay-policy=no-user-gesture-required"] }, + }, + firefox: { + // Firefox rejects the "microphone"/"camera" permission names; instead we + // feed a fake device and disable the prompt (see firefoxUserPrefs below). + permissions: [], + launchOptions: { + firefoxUserPrefs: { + // Fake media device + no prompt, in place of granting permissions. + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true, + // Allow autoplay (incl. WebAudio), in place of the autoplay launch flag. + "media.autoplay.default": 0, + "media.autoplay.blocking_policy": 0, + "media.autoplay.block-webaudio": false, + }, + }, + }, +}; + +//Modify the test function to automatically apply permissions and launchOptions appropriate to the +// specific browser being ued. +// Apply only the current browser's setup. +// * launchOptions is worker-scoped (the browser launches once per worker); +// * permissions is a per-test context option. +const test = testOriginal.extend({ + launchOptions: [ + async ({ browserName, launchOptions }, use) => { + await use({ ...launchOptions, ...(BROWSER_SETUP[browserName]?.launchOptions ?? {}) }); + }, + { scope: "worker" }, + ], + permissions: async ({ browserName }, use) => { + await use(BROWSER_SETUP[browserName]?.permissions ?? []); + }, +}); + +// Test setup for ALL browser types. +// A wide viewport keeps the editor's preview pane from collapsing +test.use({ viewport: { width: 1400, height: 900 } }); + +//Note: This for loop doesn't RUN the tests, rather it DECLARES them. +// In this way, for example, the developer can later decide in the UI test runner which tests to run and against which browser. +for (const sketch of SKETCHES) { + + // Declare one test for the sketch in question. + // The test has a title and a function which will be called if the test is run. + // Here we use our MODIFIED test() function which will automatically set up the + // browser launchOptions and permissions. + test(`${sketch.name} runs with no console errors`, async ({ page, browserName }) => { + // Skip browsers we have no headless audio/mic setup for (see BROWSER_SETUP). + test.skip(!BROWSER_SETUP[browserName], `No headless audio/mic setup for ${browserName}`); + + //the collection which will store any console errors encountered + const errors = trackErrors(page); + + //Visit the sketch in the editor + await page.goto(sketch.url, { waitUntil: "domcontentloaded" }); + + await dismissCookieBannerIfPresent(page); + + // Pressing play... + // The web editor's play button appears before the code has been sent to the iframe, and + // pressing it too early will do nothing (no canvas and no helpful "too early" error) + // So for now we wait giving time for the iframe setup to be done. This is brittle (and slows the tests). + // Better would be if the web editor disabled the button until it was ready for use. + // (see SETTLE_BEFORE_PLAY_MS). + await page.locator("#play-sketch").waitFor({ state: "visible", timeout: MAX_WAIT_FOR_PLAY_BUTTON_VISIBLE_MS }); + await page.waitForTimeout(SETTLE_BEFORE_PLAY_MS); + await page.locator("#play-sketch").click(); + + // Wait for the sketch's canvas to be created. We wait for "attached" rather + // than "visible": p5 marks the canvas data-hidden="true" while setup/preload + // runs, so requiring strict visibility here would time out before we get to + // the real check (console errors). If the canvas never attaches the sketch + // failed to start — fail now, but include any console error already captured + // (often the cause, e.g. a throw in setup() that aborted before createCanvas). + const canvas = getSketchCanvas(page); + const canvasAttached = await canvas + .waitFor({ state: "attached", timeout: MAX_WAIT_FOR_ATTACHED_CANVAS }) + .then(() => true) + .catch(() => false); + if (!canvasAttached) { + errors.push({ kind: "pageerror", text: "Sketch never rendered a canvas (preview stayed empty)", url: page.url() }); + } else { + await clickCanvasOnceVisible(page, canvas, errors); + + // Let the sketch run for a moment so runtime errors have a chance to surface. + await page.waitForTimeout(SKETCH_RUN_MS); + + // Should silence a noisy sketch that's being individually tested (and expose errors during resource release) + await stopSketch(page); + } + // In all cases, if we encountered errors in the console (and/or the canvas didn't attach) then + // the test should fail and report them. + expect(errors.length, formatErrors(sketch, errors)).toBe(0); + }); +} + +/** + * Attaches console + pageerror listeners and returns the live array they push + * into. Ignored patterns are filtered out at capture time. + * @param {Page} page + * @returns {CapturedError[]} + */ +function trackErrors(page) { + /** @type {CapturedError[]} */ + const errors = []; + + page.on("console", (/** @type {ConsoleMessage} */ msg) => { + if (msg.type() !== "error") return; + const text = msg.text(); + if (IGNORED_CONSOLE_PATTERNS.some((re) => re.test(text))) return; + errors.push({ kind: "console", text, url: page.url() }); + }); + + page.on("pageerror", (/** @type {Error} */ err) => { + errors.push({ kind: "pageerror", text: err.message, url: page.url() }); + }); + + return errors; +} + +/** + * Dismisses the editor's cookie-consent banner if it is showing. The banner only + * appears on the first visit per browser context, so its absence is fine. + * @param {Page} page + * @returns {Promise} + */ +async function dismissCookieBannerIfPresent(page) { + const allow = page.getByRole("button", { name: "Allow Essential" }); + if (await allow.isVisible().catch(() => false)) { + await allow.click(); + } +} + + +/** + * Clicks inside the sketch canvas once it is visible, recording any problem into + * `errors`. Various examples generate or modulate sound on mouse clicks, so this + * exercises that wiring and reveals basic bugs. + * + * We deliberately do NOT force the click: the canvas must actually become visible + * for a real click to land. If it never does, the test is suspect (we may be + * silently not exercising the sketch at all), so we record an error rather than + * mask it. + * @param {Page} page + * @param {Locator} canvas Locator for the sketch's #defaultCanvas0. + * @param {CapturedError[]} errors Sink for any failure encountered here. + * @returns {Promise} + */ +async function clickCanvasOnceVisible(page, canvas, errors) { + const canvasVisible = await canvas + .waitFor({ state: "visible", timeout: MAX_WAIT_FOR_VISIBLE_CANVAS_MS }) + .then(() => true) + .catch(() => false); + + if (!canvasVisible) { + errors.push({ kind: "pageerror", text: "Expected to click the canvas but it never became visible", url: page.url() }); + return; + } + + try { + await canvas.click({ timeout: CANVAS_CLICK_TIMEOUT_MS }); + } catch (e) { + errors.push({ kind: "pageerror", text: `Canvas click failed: ${firstLine(e)}`, url: page.url() }); + } +} + +/** + * Presses the editor's stop button. Best-effort: stopping is just cleanup, so a + * missing/disabled Stop button shouldn't fail the test (the error assertion is + * the real check). + * TODO: a failed stop button click should probably error. + * @param {Page} page + * @returns {Promise} + */ +async function stopSketch(page) { + await page.getByRole("button", { name: "Stop sketch" }).click({ timeout: 5_000 }).catch(() => {}); +} + +/** + * Locates the running sketch's default p5 canvas. The preview is nested two + * iframes deep: the outer iframe[title="sketch preview"] is the preview.p5js.org + * frame, which embeds a blob: child iframe that finally holds our #defaultCanvas0. + * @param {Page} page + * @returns {Locator} + */ +function getSketchCanvas(page) { + return page + .locator('iframe[title="sketch preview"]') + .contentFrame() + .locator("iframe") + .contentFrame() + .locator("#defaultCanvas0"); +} + +/** + * The first line of an Error's message (drops the stack), for compact reporting. + * @param {unknown} err + * @returns {string} + */ +function firstLine(err) { + return String(err instanceof Error ? err.message : err).split("\n")[0]; +} + +/** + * Builds a readable assertion message listing every captured error. + * @param {Sketch} sketch + * @param {CapturedError[]} errors + * @returns {string} + */ +function formatErrors(sketch, errors) { + if (errors.length === 0) return `No console errors for ${sketch.name}`; + const lines = errors.map((e) => ` [${e.kind}] ${e.text} (at ${e.url})`); + return `Console errors while running ${sketch.name}:\n${lines.join("\n")}`; +} From c6b593fb337094d68b1c0c3980f2fd1fceca5be2 Mon Sep 17 00:00:00 2001 From: nbogie Date: Sun, 31 May 2026 15:57:42 +0100 Subject: [PATCH 03/11] add a (disabled) github CI workflow for playwright integration testing (generated by playwright) --- .github/workflows/playwright.yml.DISABLED | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/playwright.yml.DISABLED diff --git a/.github/workflows/playwright.yml.DISABLED b/.github/workflows/playwright.yml.DISABLED new file mode 100644 index 0000000..adf086b --- /dev/null +++ b/.github/workflows/playwright.yml.DISABLED @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 20 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 From 4c0bbd832a52a56211bd0abcabe56f0463b10b3b Mon Sep 17 00:00:00 2001 From: nbogie Date: Sun, 31 May 2026 16:09:39 +0100 Subject: [PATCH 04/11] update about-these-tests.md --- tests/integration/about-these-tests.md | 46 ++++++++++++++------------ 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/integration/about-these-tests.md b/tests/integration/about-these-tests.md index 2ac3cd8..6bf499d 100644 --- a/tests/integration/about-these-tests.md +++ b/tests/integration/about-these-tests.md @@ -2,31 +2,35 @@ See also: [about-playwright.md](./about-playwright.md) -`tests/integration/test-examples-on-web-editor.spec.js` is a Playwright spec that smoke-tests a list of p5.sound example sketch on the web editor looking for console errors. +`tests/integration/test-examples-on-web-editor.spec.js` is a Playwright spec that smoke-tests a list of p5.sound example sketches on the p5 web editor, looking for console errors and uncaught exceptions, against (currently) chromium and firefox browsers. -**Where are the sketches?:** -The sketches are tested in place in the web editor. -At the moment that is where the example sketches live and where users will find them, so that's the best place to test them. -It _would_ be easier to test them if they were in this repo. +## Where are the sketches? -**What does each test do?:** Each test loads the sketch → dismisses the cookie banner → presses Play → waits for the canvas → clicks inside it → lets it run → stops it → asserts **zero** console errors / uncaught exceptions. -Mic/camera are pre-granted at the context level (native prompts can't be clicked by Playwright), and `--autoplay-policy=no-user-gesture-required` lets audio start. These settings are made via a chromium-only mechanism. +The sketches are tested in place in the web editor. At the moment that is where the example sketches live and where users will find them, so that's the best place to test them. It _would_ be easier to test them if they were in this repo. -**Difficulties in testing on the web editor:** -- The preview is **two iframes deep** (`iframe[title="sketch preview"]` → blob: child → `#defaultCanvas0`). -- There's a **race**: the editor ships sketch code to the preview sandbox over `postMessage`; pressing Play too early means the code never arrives and no canvas renders. A short settle wait before Play fixes it (this is what your original `waitForTimeout(10000)` was hacking around). -- p5 marks the canvas `data-hidden="true"` during setup, so the test waits for `attached` (not `visible`) and forces the click — otherwise the strict visibility wait would mask the real signal. +The list of sketch URLs is a literal array (`SKETCHES`) in the spec, extracted once from the collection. The test does not re-scrape the collection at runtime, so it is deterministic. Re-extract and update the list when the collection changes. -**Result (stable across parallel and serial runs): 18 pass, 4 fail** — and the 4 failures are *real bugs in the examples*, which is exactly the point: +## What does each test do? -| Sketch | Error | -|---|---| -| 002-Amplitude-VisualizingLoudness | `loadSound is not defined`, `sound is not defined`, `...reading 'pixels'` | -| 004-OscillatorAmplitudeLFOmodulation | `Failed to execute 'connect' on 'AudioNode': Overload resolution failed.` | -| 006-DelayTime-Envelope_b | same `AudioNode connect` error | -| 010-PitchShifterOnSampleEnded | `loadSound is not defined` | +Each test loads the sketch → dismisses the cookie banner → presses Play → waits for the canvas to attach → clicks inside it → lets it run → stops it → asserts **zero** console errors / uncaught exceptions. -The `connect`/`loadSound` failures look like they could be genuine p5.sound API regressions worth investigating separately. +## Browsers -Two notes: -- This test suite is **Chromium-only** (`test.skip` on other browsers) because the mic/camera permissions and autoplay flag are Chromium-specific. Run with `npx playwright test test-examples-on-web-editor --project=chromium`. +The suite is cross-browser. Each browser needs a *different* mechanism to (a) allow `getUserMedia` (camera/mic) without a prompt and (b) let audio start without a user gesture, so these are kept as separate, self-contained configs in `BROWSER_SETUP`: +- **Chromium** — grants `microphone`/`camera` permissions and passes `--autoplay-policy=no-user-gesture-required`. +- **Firefox** — those permission names aren't accepted, so instead it uses `firefoxUserPrefs` (fake media device + disabled prompt, plus autoplay prefs). +- **WebKit** — not in `BROWSER_SETUP`, so it is skipped (no equivalent headless audio/mic mechanism). + +Run all (supported) browsers with `npm run test:integration`, or just Chromium with `npm run test:integration:chromium-only`. (Native permission prompts are browser chrome and can't be clicked by Playwright, which is why the prompt is bypassed at the config level rather than clicked.) + +Note: under headless **Firefox** the sketches are **audible** (audio routes to the real output device); headless **Chromium** is silent (null audio backend). This is expected, not a bug. + +## Difficulties in testing on the web editor + +- The preview is **two iframes deep** (`iframe[title="sketch preview"]` → a `blob:` child iframe → `#defaultCanvas0`). +- There's a **race**: the editor ships the sketch code to the preview sandbox over `postMessage`; pressing Play before that channel is established means the code never arrives and no canvas renders. A short fixed settle wait before Play (`SETTLE_BEFORE_PLAY_MS`) is a brittle-but-readable workaround. (A better fix would be for the editor to disable Play until it's ready.) +- p5 marks the canvas `data-hidden="true"` during setup/preload, so the test waits for the canvas to be **`attached`** (not `visible`) before deciding the sketch started. +- The click itself, however, requires the canvas to become **`visible`** — and we deliberately do **not** force it. If the canvas attaches but never becomes clickable, or never attaches at all, the test fails with an explicit message rather than silently skipping the interaction: + - `Expected to click the canvas but it never became visible` + - `Sketch never rendered a canvas (preview stayed empty)` +- Because the tests run against the live editor and live example code, the pass/fail set reflects the current state of those sketches and that external platform, not this repo. \ No newline at end of file From 705566a7ad2f11993ab3356deeede0f6b1f325c1 Mon Sep 17 00:00:00 2001 From: nbogie Date: Sun, 31 May 2026 21:01:45 +0100 Subject: [PATCH 05/11] don't give a pass to 404s or failed to start audio context --- .../test-examples-on-web-editor.spec.js | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/integration/test-examples-on-web-editor.spec.js b/tests/integration/test-examples-on-web-editor.spec.js index 536068e..6b5ad44 100644 --- a/tests/integration/test-examples-on-web-editor.spec.js +++ b/tests/integration/test-examples-on-web-editor.spec.js @@ -81,22 +81,6 @@ const SKETCHES = [ { name: "p5-to-tone", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/W0_fe403s" }, ]; -/** - * Console-error texts we deliberately ignore — these are artifacts of running - * sketches in an automated browser, not real sketch/library failures: - * - * - 404s: the p5 editor prefetches URLs without a trailing slash, producing a - * benign 404. - * - AudioContext autoplay: even with autoplay enabled (see BROWSER_SETUP), a - * sketch that starts audio before our canvas-click gesture can still log this - * once; it reflects the headless environment, not the sketch. - * @type {RegExp[]} - */ -const IGNORED_CONSOLE_PATTERNS = [ - /Failed to load resource: the server responded with a status of 404/, - /The AudioContext was not allowed to start/, -]; - /** * How long to let the editor settle after load before pressing Play, so the * editor↔preview-sandbox postMessage channel is established first. @@ -229,7 +213,8 @@ for (const sketch of SKETCHES) { /** * Attaches console + pageerror listeners and returns the live array they push - * into. Ignored patterns are filtered out at capture time. + * into. We record *every* console error and uncaught exceptionObserved healthy + * sketches produce no console errors at all. * @param {Page} page * @returns {CapturedError[]} */ @@ -239,9 +224,7 @@ function trackErrors(page) { page.on("console", (/** @type {ConsoleMessage} */ msg) => { if (msg.type() !== "error") return; - const text = msg.text(); - if (IGNORED_CONSOLE_PATTERNS.some((re) => re.test(text))) return; - errors.push({ kind: "console", text, url: page.url() }); + errors.push({ kind: "console", text: msg.text(), url: page.url() }); }); page.on("pageerror", (/** @type {Error} */ err) => { From 9fc33d83776fb5cec2a7d3a79cd0b04f8c122f8d Mon Sep 17 00:00:00 2001 From: nbogie Date: Sun, 31 May 2026 21:03:40 +0100 Subject: [PATCH 06/11] tidy first para in about-these-tests.md --- tests/integration/about-these-tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/about-these-tests.md b/tests/integration/about-these-tests.md index 6bf499d..d39bc7b 100644 --- a/tests/integration/about-these-tests.md +++ b/tests/integration/about-these-tests.md @@ -2,7 +2,7 @@ See also: [about-playwright.md](./about-playwright.md) -`tests/integration/test-examples-on-web-editor.spec.js` is a Playwright spec that smoke-tests a list of p5.sound example sketches on the p5 web editor, looking for console errors and uncaught exceptions, against (currently) chromium and firefox browsers. +`tests/integration/test-examples-on-web-editor.spec.js` is a Playwright spec that smoke-tests a list of p5.sound example sketches on the p5 web editor, looking for console errors and uncaught exceptions. It currently runs against Chromium and Firefox (see [Browsers](#browsers)). ## Where are the sketches? From 47922290896a8f96b1f60e5f98d4312dc2d841e8 Mon Sep 17 00:00:00 2001 From: nbogie Date: Sun, 31 May 2026 21:09:05 +0100 Subject: [PATCH 07/11] silence the browser-based test run under firefox --- tests/integration/about-these-tests.md | 4 +--- tests/integration/test-examples-on-web-editor.spec.js | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/integration/about-these-tests.md b/tests/integration/about-these-tests.md index d39bc7b..835e728 100644 --- a/tests/integration/about-these-tests.md +++ b/tests/integration/about-these-tests.md @@ -23,8 +23,6 @@ The suite is cross-browser. Each browser needs a *different* mechanism to (a) al Run all (supported) browsers with `npm run test:integration`, or just Chromium with `npm run test:integration:chromium-only`. (Native permission prompts are browser chrome and can't be clicked by Playwright, which is why the prompt is bypassed at the config level rather than clicked.) -Note: under headless **Firefox** the sketches are **audible** (audio routes to the real output device); headless **Chromium** is silent (null audio backend). This is expected, not a bug. - ## Difficulties in testing on the web editor - The preview is **two iframes deep** (`iframe[title="sketch preview"]` → a `blob:` child iframe → `#defaultCanvas0`). @@ -33,4 +31,4 @@ Note: under headless **Firefox** the sketches are **audible** (audio routes to t - The click itself, however, requires the canvas to become **`visible`** — and we deliberately do **not** force it. If the canvas attaches but never becomes clickable, or never attaches at all, the test fails with an explicit message rather than silently skipping the interaction: - `Expected to click the canvas but it never became visible` - `Sketch never rendered a canvas (preview stayed empty)` -- Because the tests run against the live editor and live example code, the pass/fail set reflects the current state of those sketches and that external platform, not this repo. \ No newline at end of file +- Because the tests run against the live editor and live example code, the pass/fail set reflects the current state of those sketches and that external platform, not only the p5.sound library they're linked against. \ No newline at end of file diff --git a/tests/integration/test-examples-on-web-editor.spec.js b/tests/integration/test-examples-on-web-editor.spec.js index 6b5ad44..10749d2 100644 --- a/tests/integration/test-examples-on-web-editor.spec.js +++ b/tests/integration/test-examples-on-web-editor.spec.js @@ -127,6 +127,10 @@ const BROWSER_SETUP = { "media.autoplay.default": 0, "media.autoplay.blocking_policy": 0, "media.autoplay.block-webaudio": false, + // Mute output: headless Firefox routes audio to the real device (unlike + // headless Chromium's null sink), so the test run is otherwise audible. + // Web Audio still runs, so errors still surface — we just silence it. + "media.volume_scale": "0.0", }, }, }, From d670a981b8bbd4cd171938b62793fbb17f4fc257 Mon Sep 17 00:00:00 2001 From: nbogie Date: Thu, 11 Jun 2026 13:54:24 +0100 Subject: [PATCH 08/11] Add integration test to run against all local examples/. (Needs http-server dev dependency to serve the examples) No functional change to original test that runs against web-editor examples --- package-lock.json | 314 +++++++++++++++++- package.json | 10 +- playwright.config.js | 74 +++-- tests/integration/about-playwright.md | 29 -- tests/integration/about-these-tests.md | 60 +++- tests/integration/lib/browser-setup.js | 79 +++++ tests/integration/test-examples-local.spec.js | 173 ++++++++++ .../test-examples-on-web-editor.spec.js | 66 +--- 8 files changed, 649 insertions(+), 156 deletions(-) delete mode 100644 tests/integration/about-playwright.md create mode 100644 tests/integration/lib/browser-setup.js create mode 100644 tests/integration/test-examples-local.spec.js diff --git a/package-lock.json b/package-lock.json index 58ce7bc..84491ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@types/node": "^25.9.1", "babel-loader": "^9.1.3", + "http-server": "^14.1.1", "rollup": "^2.79.1", "rollup-plugin-ignore": "^1.0.10", "rollup-plugin-terser": "^7.0.2", @@ -2486,6 +2487,26 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -2910,6 +2931,16 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2947,12 +2978,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3198,6 +3230,13 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -3435,6 +3474,27 @@ "flat": "cli.js" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -3678,6 +3738,16 @@ "node": ">=0.8.0" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hoek": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", @@ -3688,6 +3758,19 @@ "node": ">=0.8.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3703,6 +3786,115 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-server/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/http-server/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/http-server/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/http-server/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-server/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/http-signature": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", @@ -4127,6 +4319,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -4137,10 +4339,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", @@ -4227,6 +4429,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -4413,6 +4625,27 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/portfinder/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4618,6 +4851,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4857,6 +5097,13 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4910,11 +5157,6 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -5400,6 +5642,18 @@ "node": ">=4" } }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5447,6 +5701,13 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5600,6 +5861,33 @@ "node": ">=10.13.0" } }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 2253fcf..01d90c3 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,12 @@ "description": "p5.sound is a minimal wrapper for Tone.js designed to extend the musical and audio capabilities of the p5.js core library.", "scripts": { "build": "rollup -c", - "test:integration:chromium-only": "npx playwright test test-examples-on-web-editor --project=chromium", - "test:integration": "npx playwright test test-examples-on-web-editor", - "test:integration:ui": "npx playwright test test-examples-on-web-editor --ui" + "test:integration": "npm run build && npx playwright test", + "test:integration:web": "npx playwright test test-examples-on-web-editor", + "test:integration:web:chromium": "npx playwright test test-examples-on-web-editor --project=web-chromium", + "test:integration:local": "npm run build && npx playwright test test-examples-local", + "test:integration:local:chromium": "npm run build && npx playwright test test-examples-local --project=local-chromium", + "test:integration:ui": "npm run build && npx playwright test --ui" }, "keywords": [ "p5.js", @@ -35,6 +38,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@types/node": "^25.9.1", "babel-loader": "^9.1.3", + "http-server": "^14.1.1", "rollup": "^2.79.1", "rollup-plugin-ignore": "^1.0.10", "rollup-plugin-terser": "^7.0.2", diff --git a/playwright.config.js b/playwright.config.js index 0616b33..a7f7b99 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -33,50 +33,56 @@ export default defineConfig({ trace: 'on-first-retry', }, - /* Configure projects for major browsers */ + /* + * Two example smoke suites, each run on the browsers we have headless + * audio/mic setup for (chromium & firefox; webkit is not investigated): + * + * web-* -> test-examples-on-web-editor.spec.js + * tests the published examples collection on editor.p5js.org (what users see) + * local-* -> test-examples-local.spec.js + * tests the repo's examples/ against the freshly built dist/, + * served by the webServer below (deterministic, offline) + * + * `testMatch` binds each spec to its projects so the two never cross over. + * The local-* projects set baseURL to the local http-server. + */ projects: [ { - name: 'chromium', + name: 'web-chromium', + testMatch: 'test-examples-on-web-editor.spec.js', use: { ...devices['Desktop Chrome'] }, }, - { - name: 'firefox', + name: 'web-firefox', + testMatch: 'test-examples-on-web-editor.spec.js', use: { ...devices['Desktop Firefox'] }, }, + { + name: 'local-chromium', + testMatch: 'test-examples-local.spec.js', + use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5050/' }, + }, + { + name: 'local-firefox', + testMatch: 'test-examples-local.spec.js', + use: { ...devices['Desktop Firefox'], baseURL: 'http://localhost:5050/' }, + }, //We haven't investigated setting up mic+camera permissions and auto-start of audio context on safari - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, + // webkit (Desktop Safari) is intentionally omitted. ], - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://localhost:3000', - // reuseExistingServer: !process.env.CI, - // }, + /* + * Static server for the local-examples suite. Serves the repo root so that + * each example's index.html and its ../../dist/p5.sound.js resolve. -c-1 + * disables caching so a fresh `npm run build` is always picked up. + * Harmless (just idles) when only the web-* projects run. + */ + webServer: { + command: 'npx http-server . -p 5050 -c-1 --silent', + url: 'http://localhost:5050/examples/', + reuseExistingServer: !process.env.CI, + timeout: 30_000, + }, }); diff --git a/tests/integration/about-playwright.md b/tests/integration/about-playwright.md deleted file mode 100644 index 32b51ff..0000000 --- a/tests/integration/about-playwright.md +++ /dev/null @@ -1,29 +0,0 @@ -Visit https://playwright.dev/docs/intro for more information. ✨ - -You can run several commands: - - `npx playwright test` - Runs the end-to-end tests. - - `npx playwright test --ui` - Starts the interactive UI mode. - - `npx playwright test --project=chromium` - Runs the tests only on Desktop Chrome. - - `npx playwright test example` - Runs the tests in a specific file. - - `npx playwright test --debug` - Runs the tests in debug mode. - - `npx playwright codegen` - Auto generate tests with Codegen. - -We suggest that you begin by typing: -```bash - npx playwright test -``` -And check out the following files: - - `./tests/integration/example.spec.js` - Example end-to-end test - - `./playwright.config.js` - Playwright Test configuration diff --git a/tests/integration/about-these-tests.md b/tests/integration/about-these-tests.md index 835e728..934934b 100644 --- a/tests/integration/about-these-tests.md +++ b/tests/integration/about-these-tests.md @@ -1,34 +1,64 @@ # About these tests -See also: [about-playwright.md](./about-playwright.md) +There are **two** Playwright smoke-test suites. Both run each p5.sound example sketch, do a trivial interaction, and assert **zero** console errors / uncaught exceptions. They differ only in *where the sketches come from*: -`tests/integration/test-examples-on-web-editor.spec.js` is a Playwright spec that smoke-tests a list of p5.sound example sketches on the p5 web editor, looking for console errors and uncaught exceptions. It currently runs against Chromium and Firefox (see [Browsers](#browsers)). +| Suite | Spec | Runs the sketches… | Good for | +| --- | --- | --- | --- | +| **web editor** | `test-examples-on-web-editor.spec.js` | in place on editor.p5js.org | testing what users actually see; catches platform/CDN drift | +| **local** | `test-examples-local.spec.js` | from this repo's `examples/`, against the freshly built `dist/` | deterministic, offline, tests *your* library changes | -## Where are the sketches? +Neither is "the real one" - they answer different questions. The web suite can drift if the published collection changes; the local suite can drift if `examples/` falls behind the collection. Keeping the two in sync is currently a manual job. + +The two specs are intentionally kept **separate and simple** rather than sharing a procedure: run locally there are no iframes, no Play button, no settle race, no cookie banner and no Stop button, so the local spec is much shorter. The only shared code is the per-browser audio/mic setup in [`lib/browser-setup.js`](./lib/browser-setup.js) - identical, non-obvious, and easy to get subtly wrong, so it lives in one place. -The sketches are tested in place in the web editor. At the moment that is where the example sketches live and where users will find them, so that's the best place to test them. It _would_ be easier to test them if they were in this repo. +## Where are the sketches? -The list of sketch URLs is a literal array (`SKETCHES`) in the spec, extracted once from the collection. The test does not re-scrape the collection at runtime, so it is deterministic. Re-extract and update the list when the collection changes. +- **web editor suite:** the list of sketch URLs is a literal array (`SKETCHES`) in the spec, extracted once from the [collection](https://editor.p5js.org/thomasjohnmartinez/collections/Dp0zGclVL). The test does not re-scrape at runtime, so it is deterministic. Re-extract and update the list when the collection changes. +- **local suite:** the list is discovered at load time by scanning `examples/*/` for directories containing an `index.html`. Each example's `index.html` loads `../../dist/p5.sound.js`, so the local suite tests **the library you just built** - run `npm run build` first (all relevant `test:integration:*` npm scripts do this for you). ## What does each test do? -Each test loads the sketch → dismisses the cookie banner → presses Play → waits for the canvas to attach → clicks inside it → lets it run → stops it → asserts **zero** console errors / uncaught exceptions. +Same essential procedure, different mechanics: + +- **web editor:** load the sketch → dismiss the cookie banner → press Play → wait for the canvas to attach → click inside canvas → let it run → stop it → assert zero errors. +- **local:** load the served `index.html` (the sketch auto-runs) → wait for the canvas to attach → click inside canvas → let it run → assert zero errors. ## Browsers -The suite is cross-browser. Each browser needs a *different* mechanism to (a) allow `getUserMedia` (camera/mic) without a prompt and (b) let audio start without a user gesture, so these are kept as separate, self-contained configs in `BROWSER_SETUP`: -- **Chromium** — grants `microphone`/`camera` permissions and passes `--autoplay-policy=no-user-gesture-required`. -- **Firefox** — those permission names aren't accepted, so instead it uses `firefoxUserPrefs` (fake media device + disabled prompt, plus autoplay prefs). -- **WebKit** — not in `BROWSER_SETUP`, so it is skipped (no equivalent headless audio/mic mechanism). +Both suites are cross-browser. Each browser needs a *different* mechanism to (a) allow `getUserMedia` (camera/mic access) without a prompt and (b) let audio start without a user gesture, so these are kept as separate, self-contained configs in `BROWSER_SETUP` (in `lib/browser-setup.js`): + +- **Chromium** - grants `microphone`/`camera` permissions and passes `--autoplay-policy=no-user-gesture-required`. +- **Firefox** - those permission names aren't accepted, so instead it uses `firefoxUserPrefs` (fake media device + disabled prompt, plus autoplay prefs). It also mutes output (`media.volume_scale: "0.0"`): headless Firefox routes audio to the real device, so the run would otherwise be audible. +- **WebKit** - not in `BROWSER_SETUP`, so it is skipped (no equivalent headless audio/mic mechanism - needs research). + +(Native permission prompts are browser chrome and can't be clicked by Playwright, which is why the prompt is bypassed at the config level rather than clicked.) + +## Test "projects" and how to run them -Run all (supported) browsers with `npm run test:integration`, or just Chromium with `npm run test:integration:chromium-only`. (Native permission prompts are browser chrome and can't be clicked by Playwright, which is why the prompt is bypassed at the config level rather than clicked.) +`playwright.config.js` defines four "projects" - a matrix of the two suites × {chromium, firefox} - bound to their spec via `testMatch`: `web-chromium`, `web-firefox`, `local-chromium`, `local-firefox`. The local projects point `baseURL` at a local `http-server` (a `webServer` in the config) that serves the repo root so `examples//` and its `../../dist/p5.sound.js` resolve. + +| Command | Runs | +| --- | --- | +| `npm run test:integration` | builds `dist/`, then everything (both suites, both browsers) | +| `npm run test:integration:web` | web-editor suite (both browsers) | +| `npm run test:integration:web:chromium` | web-editor suite, Chromium only | +| `npm run test:integration:local` | builds `dist/`, then the local suite (both browsers) | +| `npm run test:integration:local:chromium` | builds `dist/`, then local suite, Chromium only | +| `npm run test:integration:ui` | opens the Playwright UI runner | + +## What counts as a failure? + +Any console `error` or uncaught exception while the sketch loads, is clicked, and runs. Nothing is filtered - a 404 for a missing asset or a failed AudioContext start is a real failure, not noise. The interaction is also asserted: if the canvas attaches but never becomes clickable, or never attaches at all, the test fails with an explicit message rather than silently skipping the interaction: + +- `Expected to click the canvas but it never became visible` +- `Sketch never rendered a canvas (...)` ## Difficulties in testing on the web editor +These apply to the **web editor** suite only - the local suite sidesteps all of them: + - The preview is **two iframes deep** (`iframe[title="sketch preview"]` → a `blob:` child iframe → `#defaultCanvas0`). - There's a **race**: the editor ships the sketch code to the preview sandbox over `postMessage`; pressing Play before that channel is established means the code never arrives and no canvas renders. A short fixed settle wait before Play (`SETTLE_BEFORE_PLAY_MS`) is a brittle-but-readable workaround. (A better fix would be for the editor to disable Play until it's ready.) - p5 marks the canvas `data-hidden="true"` during setup/preload, so the test waits for the canvas to be **`attached`** (not `visible`) before deciding the sketch started. -- The click itself, however, requires the canvas to become **`visible`** — and we deliberately do **not** force it. If the canvas attaches but never becomes clickable, or never attaches at all, the test fails with an explicit message rather than silently skipping the interaction: - - `Expected to click the canvas but it never became visible` - - `Sketch never rendered a canvas (preview stayed empty)` -- Because the tests run against the live editor and live example code, the pass/fail set reflects the current state of those sketches and that external platform, not only the p5.sound library they're linked against. \ No newline at end of file + +Locally, the only comparable wrinkle is that p5 v2 numbers the default canvas inconsistently (`defaultCanvas1` for 2D sketches, `defaultCanvas0` for WEBGL ones), so the local spec matches the canvas by its `p5Canvas` class rather than by id. diff --git a/tests/integration/lib/browser-setup.js b/tests/integration/lib/browser-setup.js new file mode 100644 index 0000000..39d954b --- /dev/null +++ b/tests/integration/lib/browser-setup.js @@ -0,0 +1,79 @@ +//@ts-check +import { test as testOriginal, expect } from "@playwright/test"; + +/** + * Per-browser launch/context setup shared by BOTH example smoke suites + * (the web-editor suite and the local-examples suite). + * + * This is the ONLY code shared between those two suites. It lives here on + * purpose: the two test bodies are otherwise deliberately separate and simple, + * but this block is identical, non-obvious, and hard to get right + * (a wrong Firefox pref makes audio fail or the run audible without any error), + * so we don't duplicate it. + */ + +/** + * The launch/context setup one browser needs to run audio + getUserMedia sketches + * headlessly without prompts or gesture gating. + * @typedef {Object} BrowserSetup + * @property {string[]} permissions Context permissions to grant up front. + * @property {import("@playwright/test").LaunchOptions} launchOptions + */ + +/** + * Configs for per-browser setup. The keys are the browsers the suites run on; a + * browser not listed here (e.g. webkit) is skipped. + * Each browser needs a different mechanism to: + * (a) satisfy granting getUserMedia (camera & microphone access) without a prompt and + * (b) let audio start without a user gesture. + * @type {Record} + */ +export const BROWSER_SETUP = { + chromium: { + // Grant mic/camera so getUserMedia() resolves without a native prompt. + permissions: ["microphone", "camera"], + // --autoplay-policy lets audio start without a user gesture. + launchOptions: { args: ["--autoplay-policy=no-user-gesture-required"] }, + }, + firefox: { + // Firefox rejects the "microphone"/"camera" permission names; instead we + // feed a fake device and disable the prompt (see firefoxUserPrefs below). + permissions: [], + launchOptions: { + firefoxUserPrefs: { + // Fake media device + no prompt, in place of granting permissions. + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true, + // Allow autoplay (incl. WebAudio), in place of the autoplay launch flag. + "media.autoplay.default": 0, + "media.autoplay.blocking_policy": 0, + "media.autoplay.block-webaudio": false, + // Mute output: headless Firefox routes audio to the real device (unlike + // headless Chromium's null sink), so the test run is otherwise audible. + // Web Audio still runs, so errors still surface — we just silence it. + "media.volume_scale": "0.0", + }, + }, + }, +}; + +/** + * An overridden `test` function that automatically applies the current browser's setup: + * - launchOptions is worker-scoped (the browser launches once per worker); + * - permissions is a per-test context option. + * Use this in place of the plain @playwright/test `test`. Browsers absent from + * BROWSER_SETUP get empty defaults (callers should test.skip them). + */ +export const test = testOriginal.extend({ + launchOptions: [ + async ({ browserName, launchOptions }, use) => { + await use({ ...launchOptions, ...(BROWSER_SETUP[browserName]?.launchOptions ?? {}) }); + }, + { scope: "worker" }, + ], + permissions: async ({ browserName }, use) => { + await use(BROWSER_SETUP[browserName]?.permissions ?? []); + }, +}); + +export { expect }; diff --git a/tests/integration/test-examples-local.spec.js b/tests/integration/test-examples-local.spec.js new file mode 100644 index 0000000..5324285 --- /dev/null +++ b/tests/integration/test-examples-local.spec.js @@ -0,0 +1,173 @@ +//@ts-check +import { readdirSync, existsSync } from "node:fs"; +import path from "node:path"; +import { test, expect, BROWSER_SETUP } from "./lib/browser-setup.js"; + +/** + * Smoke-tests every example under ../../examples/ by serving it locally and + * checking that running it produces no console errors or uncaught exceptions. + * + * This is the local-source counterpart to test-examples-on-web-editor.spec.js. + * The web-editor suite tests what users see on editor.p5js.org; this one tests + * the examples as they live in the repo, running against the freshly built + * dist/p5.sound.js (each example's index.html loads ../../dist/p5.sound.js). + * + * The two suites are intentionally kept separate and simple rather than sharing + * a procedure: running locally there are NO iframes, no Play button, no + * editor/sandbox settle race, no cookie banner and no Stop button, so this file + * is much shorter than its web-editor sibling. The only shared code is the + * per-browser audio/mic setup in ./lib/browser-setup.js. + * + * Serving: playwright.config.js starts an http-server at the repo root and the + * local-* projects set baseURL to it, so we navigate to "examples//". + * + */ + +/** + * @typedef {import("@playwright/test").Page} Page + * @typedef {import("@playwright/test").Locator} Locator + * @typedef {import("@playwright/test").ConsoleMessage} ConsoleMessage + */ + +/** + * A single captured problem (console error or uncaught page exception). + * @typedef {Object} CapturedError + * @property {"console" | "pageerror"} kind + * @property {string} text + * @property {string} url The page URL at the time the problem was captured. + */ + +/** How long to let the sketch run (and potentially throw) after the canvas appears. */ +const SKETCH_RUN_MS = 3000; +/** Max time (ms) to wait for the canvas to be in attached state. */ +const MAX_WAIT_FOR_ATTACHED_CANVAS_MS = 15_000; +/** Max time (ms) to wait for the canvas to become visible before clicking it. */ +const MAX_WAIT_FOR_VISIBLE_CANVAS_MS = 5_000; +/** Max time (ms) for a single canvas click attempt. */ +const CANVAS_CLICK_TIMEOUT_MS = 5_000; + +/** + * Absolute path to the examples directory. Playwright runs with the repo root + * (the directory holding playwright.config.js) as the working directory. + */ +const EXAMPLES_DIR = path.resolve("examples"); + +/** + * Every example directory that has an index.html, discovered at load time. + * @type {string[]} + */ +const EXAMPLE_NAMES = readdirSync(EXAMPLES_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .filter((name) => existsSync(path.join(EXAMPLES_DIR, name, "index.html"))) + .sort(); + +// A wide viewport, to match the web-editor suite. +test.use({ viewport: { width: 1400, height: 900 } }); + +for (const name of EXAMPLE_NAMES) { + test(`${name} runs with no console errors`, async ({ page, browserName }) => { + // Skip browsers we have no headless audio/mic setup for (see BROWSER_SETUP). + test.skip(!BROWSER_SETUP[browserName], `No headless audio/mic setup for ${browserName}`); + + const errors = trackErrors(page); + + // Load the example. baseURL (set by the local-* project) points at the http-server serving the repo root. + await page.goto(`examples/${name}/`, { waitUntil: "load" }); + + // The canvas is right on the page here (no iframes). We match it by p5's + // "p5Canvas" class rather than #defaultCanvas0: p5 v2 numbers the default + // canvas inconsistently when run locally (defaultCanvas1 for 2D sketches, + // defaultCanvas0 for WEBGL ones?), but every example sketch seems to produce exactly one + // p5Canvas. Wait for "attached" rather than "visible": p5 marks the canvas + // data-hidden="true" while setup/preload runs. If it never attaches, the + // sketch failed to start. + const canvas = page.locator("canvas.p5Canvas"); + const canvasAttached = await canvas + .waitFor({ state: "attached", timeout: MAX_WAIT_FOR_ATTACHED_CANVAS_MS }) + .then(() => true) + .catch(() => false); + + if (!canvasAttached) { + errors.push({ kind: "pageerror", text: "Sketch never rendered a canvas (page stayed empty)", url: page.url() }); + } else { + await clickCanvasOnceVisible(page, canvas, errors); + // Let the sketch run for a moment so runtime errors have a chance to surface. + await page.waitForTimeout(SKETCH_RUN_MS); + } + + expect(errors.length, formatErrors(name, errors)).toBe(0); + }); +} + +/** + * Attaches console + pageerror listeners and returns the live array they push + * into. Records every console error and uncaught exception; healthy sketches + * produce none. + * @param {Page} page + * @returns {CapturedError[]} + */ +function trackErrors(page) { + /** @type {CapturedError[]} */ + const errors = []; + + page.on("console", (/** @type {ConsoleMessage} */ msg) => { + if (msg.type() !== "error") return; + errors.push({ kind: "console", text: msg.text(), url: page.url() }); + }); + + page.on("pageerror", (/** @type {Error} */ err) => { + errors.push({ kind: "pageerror", text: err.message, url: page.url() }); + }); + + return errors; +} + +/** + * Clicks inside the sketch canvas once it is visible, recording any problem into + * `errors`. Many examples generate or modulate sound on click, so this exercises + * that wiring. We do NOT force the click: if the canvas never becomes visible the + * test is suspect, so we record an error rather than mask it. + * @param {Page} page + * @param {Locator} canvas Locator for the sketch's #defaultCanvas0. + * @param {CapturedError[]} errors Sink for any failure encountered here. + * @returns {Promise} + */ +async function clickCanvasOnceVisible(page, canvas, errors) { + const canvasVisible = await canvas + .waitFor({ state: "visible", timeout: MAX_WAIT_FOR_VISIBLE_CANVAS_MS }) + .then(() => true) + .catch(() => false); + + if (!canvasVisible) { + errors.push({ kind: "pageerror", text: "Expected to click the canvas but it never became visible", url: page.url() }); + return; + } + + try { + await canvas.click({ timeout: CANVAS_CLICK_TIMEOUT_MS }); + } catch (e) { + errors.push({ kind: "pageerror", text: `Canvas click failed: ${firstLine(e)}`, url: page.url() }); + } +} + +/** + * The first line of an Error's message (drops the stack), for compact reporting. + * @param {unknown} err + * @returns {string} + */ +function firstLine(err) { + return String(err instanceof Error ? err.message : err).split("\n")[0]; +} + +/** + * Builds a readable assertion message listing every captured error. + * @param {string} name + * @param {CapturedError[]} errors + * @returns {string} + */ +function formatErrors(name, errors) { + if (errors.length === 0) return `No console errors for ${name}`; + const lines = errors.map((e) => ` [${e.kind}] ${e.text} (at ${e.url})`); + return `Console errors while running ${name}:\n${lines.join("\n")}`; +} diff --git a/tests/integration/test-examples-on-web-editor.spec.js b/tests/integration/test-examples-on-web-editor.spec.js index 10749d2..f445dff 100644 --- a/tests/integration/test-examples-on-web-editor.spec.js +++ b/tests/integration/test-examples-on-web-editor.spec.js @@ -1,5 +1,5 @@ //@ts-check -import { test as testOriginal, expect } from "@playwright/test"; +import { test, expect, BROWSER_SETUP } from "./lib/browser-setup.js"; /** * Smoke-tests every sketch in the p5.sound examples collection on the p5 web @@ -47,14 +47,6 @@ import { test as testOriginal, expect } from "@playwright/test"; * @property {string} url Direct URL to the sketch on the p5 web editor. */ -/** - * The launch/context setup one browser needs to run audio + getUserMedia sketches - * headlessly without prompts or gesture gating. - * @typedef {Object} BrowserSetup - * @property {string[]} permissions Context permissions to grant up front. - * @property {import("@playwright/test").LaunchOptions} launchOptions - */ - /** @type {Sketch[]} */ const SKETCHES = [ { name: "001-Oscillator-FrequencyAmplitude", url: "https://editor.p5js.org/thomasjohnmartinez/sketches/z-KkeTrcu" }, @@ -99,59 +91,9 @@ const MAX_WAIT_FOR_VISIBLE_CANVAS_MS = 5_000; /** Max time (ms) for a single canvas click attempt. */ const CANVAS_CLICK_TIMEOUT_MS = 5_000; -/** - * Configs for per-browser setup. The keys are the browsers this spec runs on; a browser not - * listed here (e.g. webkit) is skipped. - * Each browser needs a different mechanism to: - * (a) satisfy granting getUserMedia (cameraµphone access) without a prompt and - * (b) let audio start without a user gesture. - * @type {Record} - */ -const BROWSER_SETUP = { - chromium: { - // Grant mic/camera so getUserMedia() resolves without a native prompt. - permissions: ["microphone", "camera"], - // --autoplay-policy lets audio start without a user gesture. - launchOptions: { args: ["--autoplay-policy=no-user-gesture-required"] }, - }, - firefox: { - // Firefox rejects the "microphone"/"camera" permission names; instead we - // feed a fake device and disable the prompt (see firefoxUserPrefs below). - permissions: [], - launchOptions: { - firefoxUserPrefs: { - // Fake media device + no prompt, in place of granting permissions. - "media.navigator.streams.fake": true, - "media.navigator.permission.disabled": true, - // Allow autoplay (incl. WebAudio), in place of the autoplay launch flag. - "media.autoplay.default": 0, - "media.autoplay.blocking_policy": 0, - "media.autoplay.block-webaudio": false, - // Mute output: headless Firefox routes audio to the real device (unlike - // headless Chromium's null sink), so the test run is otherwise audible. - // Web Audio still runs, so errors still surface — we just silence it. - "media.volume_scale": "0.0", - }, - }, - }, -}; - -//Modify the test function to automatically apply permissions and launchOptions appropriate to the -// specific browser being ued. -// Apply only the current browser's setup. -// * launchOptions is worker-scoped (the browser launches once per worker); -// * permissions is a per-test context option. -const test = testOriginal.extend({ - launchOptions: [ - async ({ browserName, launchOptions }, use) => { - await use({ ...launchOptions, ...(BROWSER_SETUP[browserName]?.launchOptions ?? {}) }); - }, - { scope: "worker" }, - ], - permissions: async ({ browserName }, use) => { - await use(BROWSER_SETUP[browserName]?.permissions ?? []); - }, -}); +// The important overridden `test` function (with per-browser audio/mic setup), `expect`, and +// BROWSER_SETUP come from ./lib/browser-setup.js — the only code shared with the local-examples +// suite. BROWSER_SETUP is also used below to skip browsers we have no setup for. // Test setup for ALL browser types. // A wide viewport keeps the editor's preview pane from collapsing From b91070023ff7e8a651a116b0cdf73573c7251086 Mon Sep 17 00:00:00 2001 From: nbogie Date: Thu, 11 Jun 2026 17:26:03 +0100 Subject: [PATCH 09/11] add a section about first-time setup --- tests/integration/about-these-tests.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/integration/about-these-tests.md b/tests/integration/about-these-tests.md index 934934b..e27a755 100644 --- a/tests/integration/about-these-tests.md +++ b/tests/integration/about-these-tests.md @@ -11,6 +11,24 @@ Neither is "the real one" - they answer different questions. The web suite can d The two specs are intentionally kept **separate and simple** rather than sharing a procedure: run locally there are no iframes, no Play button, no settle race, no cookie banner and no Stop button, so the local spec is much shorter. The only shared code is the per-browser audio/mic setup in [`lib/browser-setup.js`](./lib/browser-setup.js) - identical, non-obvious, and easy to get subtly wrong, so it lives in one place. +## Setup (first time) + +```bash +npm install # installs packages, including @playwright/test and http-server +npx playwright install # downloads the browser binaries Playwright drives +``` + +Or if you really need to save disk space, you might be able to change that second command to +```bash +npx playwright install chromium firefox +``` + +These are two separate steps because they install different things: `npm install` populates `node_modules` whereas `npx playwright install` downloads the browser binaries separately into a machine-global cache (`~/Library/Caches/ms-playwright` on macOS). If you've previously run `npx playwright install` for any other Playwright project it may not need to do anything. + +Note `npx playwright install` installs the various browsers Playwright ships (Chromium, Firefox, WebKit, FFmpeg) even though we don't currently make use of them all. (On macOS 14 you may see a "frozen WebKit" warning - that's harmless here, since the suite currently skips WebKit.) + +The local examples also need a built `dist/`. The relevant `test:integration:*` npm scripts first run `npm run build` for you so that you're always testing the latest local code on your branch. + ## Where are the sketches? - **web editor suite:** the list of sketch URLs is a literal array (`SKETCHES`) in the spec, extracted once from the [collection](https://editor.p5js.org/thomasjohnmartinez/collections/Dp0zGclVL). The test does not re-scrape at runtime, so it is deterministic. Re-extract and update the list when the collection changes. From 93ae8ac9023260a598ab7ca33dc39c569a0b67aa Mon Sep 17 00:00:00 2001 From: nbogie Date: Mon, 15 Jun 2026 00:10:48 +0100 Subject: [PATCH 10/11] Add a section on testing the examples. Also introduce the local examples/ --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 89b6f5e..3561191 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ p5.sound.js extends the musical and sonic capabilities of [p5.js](https://p5js.o ## Examples -- p5.sound example on p5.js editor [here](https://editor.p5js.org/thomasjohnmartinez/collections/Dp0zGclVL) +- A set of p5.sound examples are in this repo at [examples/](examples/) +- The original examples can be found on the p5.js web editor [here](https://editor.p5js.org/thomasjohnmartinez/collections/Dp0zGclVL). Note that these may differ from the above set. - Legacy p5.js Sound Tutorial by Dan Shiffman on [YouTube](https://www.youtube.com/playlist?list=PLRqwX-V7Uu6aFcVjlDAkkGIixw70s7jpW) ## Documentation @@ -76,3 +77,32 @@ building reference pages (optional) ``` npx yuidoc . ``` + +## Testing the examples + +The library is configured to use [Playwright](https://playwright.dev/) to automatically test the [local](./examples) and [web-editor-hosted](https://editor.p5js.org/thomasjohnmartinez/collections/Dp0zGclVL) sets of p5.sound.js examples by automatically controlling a browser (firefox or chromium). + +If you haven't used Playwright on your system before, you'll have to run the following command _once_ to allow it to download the browsers it uses: + +### Setting up playwright +```bash +npx playwright install +``` + +### Starting the tests +1. Launch playwright's test-runner UI: +```bash +npm run test:integration:ui +``` + +2. Choose example set(s) and browser(s) +From the GUI, click "projects" and choose which examples ("web-" and/or "local-") and which browsers ("chromium" and/or "firefox") you wish to test. + +3. Run the tests! +click the green play button at the top of the list of tests. + +If a test fails, you can inspect its console log, the test actions, and even screenshots of what it looked like while it was running. + +There are also various other ways to run the tests automatically without any interaction. + +For more information, read [tests/integration/about-these-tests.md](tests/integration/about-these-tests.md) \ No newline at end of file From 6dcf90f32be17a00bedf582ecee111a1b8e57142 Mon Sep 17 00:00:00 2001 From: nbogie Date: Mon, 15 Jun 2026 01:56:18 +0100 Subject: [PATCH 11/11] add a note about loadSound vs mousePressed race when testing --- tests/integration/about-these-tests.md | 32 +++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/integration/about-these-tests.md b/tests/integration/about-these-tests.md index e27a755..0b7579e 100644 --- a/tests/integration/about-these-tests.md +++ b/tests/integration/about-these-tests.md @@ -79,4 +79,34 @@ These apply to the **web editor** suite only - the local suite sidesteps all of - There's a **race**: the editor ships the sketch code to the preview sandbox over `postMessage`; pressing Play before that channel is established means the code never arrives and no canvas renders. A short fixed settle wait before Play (`SETTLE_BEFORE_PLAY_MS`) is a brittle-but-readable workaround. (A better fix would be for the editor to disable Play until it's ready.) - p5 marks the canvas `data-hidden="true"` during setup/preload, so the test waits for the canvas to be **`attached`** (not `visible`) before deciding the sketch started. -Locally, the only comparable wrinkle is that p5 v2 numbers the default canvas inconsistently (`defaultCanvas1` for 2D sketches, `defaultCanvas0` for WEBGL ones), so the local spec matches the canvas by its `p5Canvas` class rather than by id. +## Other testing "gotchas" +p5 v2 numbers the default canvas inconsistently (`defaultCanvas1` for 2D sketches, `defaultCanvas0` for WEBGL ones), so the local spec matches the canvas by its `p5Canvas` class rather than by id. + +As the tests click the canvas very soon after it is marked attached, there's a chance that the following pattern in examples will hit on an undefined mySound because loadSound hasn't completed before the mouse click: +```js +let mySound; +async function setup(){ + createCanvas(400,400); + mySound = await loadSound(someURL); +} + +function mousePressed(){ + //mySound could still be undefined + mySound.play(); +} +``` +Either check if mySound is defined in mousePressed, or register the mousePressed event handler synchronously in line with awaiting loadSound: + +```js +let mySound; +async function setup(){ + const cnv = createCanvas(400,400); + mySound = await loadSound(someURL); + //only register the mouse handler afer the sound has finished loading! + cnv.mousePressed(playSound); +} + +function playSound(){ + mySound.play(); +} +```