From 2fa50d42957d78890d5d958532570321a8e30c77 Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Fri, 23 Oct 2026 14:36:29 +0200 Subject: [PATCH 1/4] test: add Pest browser tests for the feedback page Company rule since 2026-10-23: every repository has Pest browser tests (pestphp/pest-plugin-browser with Playwright). The existing Playwright suites in widget/ and e2e/ cover the widget; the feedback page behind signed links, the only HTML the API serves, was only checked as HTML strings. These tests drive it in Chromium, under its real CSP: - message, sender and screenshot render, the image actually loads - the technical tab shows console errors, failed requests and the environment, #technical opens it directly - the message card hides and shows, copy announces itself - a team link completes and reopens the entry and logs who did it, the sender's link has no status buttons - expired and tampered links and typed API URLs get an empty page The suite lives in tests/Browser with its own Browser testsuite and RefreshDatabase. `composer test` and `test:coverage` exclude it, `composer test:browser` runs it. playwright is pinned like the widget's @playwright/test. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 + CHANGELOG.md | 1 + README.md | 1 + composer.json | 6 +- composer.lock | 1574 +++++++++++++++++++++++++++- package-lock.json | 32 + package.json | 3 + phpunit.xml | 3 + tests/Browser/FeedbackPageTest.php | 145 +++ tests/Pest.php | 1 + 10 files changed, 1766 insertions(+), 3 deletions(-) create mode 100644 tests/Browser/FeedbackPageTest.php diff --git a/.gitignore b/.gitignore index e7338bb..e1f991c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ Thumbs.db /e2e/playwright-report /public/widget /resources/widget + +# Pest browser test failure screenshots +/tests/Browser/Screenshots/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2687e10..2ae930c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the API and the widget are documented here. The format fo - The widget is no longer published to npm: the API serves it at `/widget/v1/feedback.js` (one script tag, no npm, no CDN, updated with every deploy). `npm run build` compiles it into `resources/widget/v1`. The npm package `@codebar-ag/feedback-widget` 1.0.0 stays available but gets no updates. Removed: release workflow, type definitions, SRI file, publint and attw. - The widget only appears on pages the API accepts: it checks the project config first and stays hidden on a `403` (origin or IP not allowed). Refused `GET` requests now carry `Access-Control-Allow-Origin` so the widget can read the `403`; the preflight for sending stays refused. - Dev dependencies: vite 8.3.5, typescript-eslint 8.72.0. +- Pest browser tests (`tests/Browser`, Playwright Chromium) for the feedback page: tabs, message card, copy, completing and reopening, refused links. New dev dependencies: pestphp/pest-plugin-browser 5.1, playwright 1.64.1. `composer test` leaves them out, `composer test:browser` runs them. ### Fixed diff --git a/README.md b/README.md index c987446..6449ea2 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,7 @@ npm run build # builds the widget and copies it to resources npm --prefix widget run check # lint, types, Vitest, build, size budget npm --prefix widget run e2e # Playwright: Chromium, Firefox, WebKit, mobile npm --prefix widget run e2e:full # API and widget together +composer test:browser # Pest browser tests of the feedback page (npx playwright install chromium first) npm --prefix widget run dev # playground with a mock API on http://localhost:4173/fixtures/index.html ``` diff --git a/composer.json b/composer.json index 19e9e33..c3b6154 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.9", "pestphp/pest": "^5.0", + "pestphp/pest-plugin-browser": "^5.1", "pestphp/pest-plugin-laravel": "^5.0", "pestphp/pest-plugin-type-coverage": "^5.0" }, @@ -55,8 +56,9 @@ "lint": "./vendor/bin/pint --test", "format": "./vendor/bin/pint", "analyse": "./vendor/bin/phpstan analyse --memory-limit=1G --no-progress", - "test": "./vendor/bin/pest --parallel --compact", - "test:coverage": "./vendor/bin/pest --coverage --min=90", + "test": "./vendor/bin/pest --exclude-testsuite=Browser --parallel --compact", + "test:coverage": "./vendor/bin/pest --exclude-testsuite=Browser --coverage --min=90", + "test:browser": "./vendor/bin/pest tests/Browser", "check": [ "@lint", "@analyse", diff --git a/composer.lock b/composer.lock index e0339f3..201c23e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0deb113b18e34126f15b18f7c3608dcd", + "content-hash": "8548e40d846f9b3470b3fec67aaf259e", "packages": [ { "name": "brick/math", @@ -6744,6 +6744,1236 @@ } ], "packages-dev": [ + { + "name": "amphp/amp", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/73c38b323ff8d790abf0f76c56fcc892bda4111a", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-19T17:59:20+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/ec5948bfafac808f410406e18bc7b52a2d4629b7", + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-26T17:53:54+00:00" + }, + { + "name": "amphp/hpack", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/hpack.git", + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/hpack/zipball/291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "http2jp/hpack-test-case": "^1", + "nikic/php-fuzzer": "^0.0.11", + "phpunit/phpunit": "^7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "HTTP/2 HPack implementation.", + "homepage": "https://github.com/amphp/hpack", + "keywords": [ + "headers", + "hpack", + "http-2" + ], + "support": { + "issues": "https://github.com/amphp/hpack/issues", + "source": "https://github.com/amphp/hpack/tree/v3.2.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-03T19:28:59+00:00" + }, + { + "name": "amphp/http", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/http.git", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http/zipball/3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "shasum": "" + }, + "require": { + "amphp/hpack": "^3", + "amphp/parser": "^1.1", + "league/uri-components": "^2.4.2 | ^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "league/uri": "^6.8 | ^7.1", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.26.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/constants.php" + ], + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Basic HTTP primitives which can be shared by servers and clients.", + "support": { + "issues": "https://github.com/amphp/http/issues", + "source": "https://github.com/amphp/http/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-11-23T14:57:26+00:00" + }, + { + "name": "amphp/http-client", + "version": "v5.3.6", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-client.git", + "reference": "ca155026acafa74a612d776a97202d53077fee86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-client/zipball/ca155026acafa74a612d776a97202d53077fee86", + "reference": "ca155026acafa74a612d776a97202d53077fee86", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/hpack": "^3", + "amphp/http": "^2", + "amphp/pipeline": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "league/uri": "^7", + "league/uri-components": "^7", + "league/uri-interfaces": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2", + "revolt/event-loop": "^1" + }, + "conflict": { + "amphp/file": "<3 | >=5" + }, + "require-dev": { + "amphp/file": "^3 | ^4", + "amphp/http-server": "^3", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "ext-json": "*", + "kelunik/link-header-rfc5988": "^1", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "suggest": { + "amphp/file": "Required for file request bodies and HTTP archive logging", + "ext-json": "Required for logging HTTP archives", + "ext-zlib": "Allows using compression for response bodies." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\Http\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "An advanced async HTTP client library for PHP, enabling efficient, non-blocking, and concurrent requests and responses.", + "homepage": "https://amphp.org/http-client", + "keywords": [ + "async", + "client", + "concurrent", + "http", + "non-blocking", + "rest" + ], + "support": { + "issues": "https://github.com/amphp/http-client/issues", + "source": "https://github.com/amphp/http-client/tree/v5.3.6" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-15T23:29:38+00:00" + }, + { + "name": "amphp/http-server", + "version": "v3.4.7", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-server.git", + "reference": "aa96ca38c5f822a827236543d5417d0c2a782ef0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-server/zipball/aa96ca38c5f822a827236543d5417d0c2a782ef0", + "reference": "aa96ca38c5f822a827236543d5417d0c2a782ef0", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/hpack": "^3", + "amphp/http": "^2", + "amphp/pipeline": "^1", + "amphp/socket": "^2.1", + "amphp/sync": "^2.2", + "league/uri": "^7.1", + "league/uri-interfaces": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2", + "psr/log": "^1 | ^2 | ^3", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/http-client": "^5", + "amphp/log": "^2", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "league/uri-components": "^7.1", + "monolog/monolog": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "suggest": { + "ext-zlib": "Allows GZip compression of response bodies" + }, + "type": "library", + "autoload": { + "files": [ + "src/Driver/functions.php", + "src/Middleware/functions.php", + "src/functions.php" + ], + "psr-4": { + "Amp\\Http\\Server\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "A non-blocking HTTP application server for PHP based on Amp.", + "homepage": "https://github.com/amphp/http-server", + "keywords": [ + "amp", + "amphp", + "async", + "http", + "non-blocking", + "server" + ], + "support": { + "issues": "https://github.com/amphp/http-server/issues", + "source": "https://github.com/amphp/http-server/tree/v3.4.7" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-10-04T15:14:17+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.7" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-26T14:50:43+00:00" + }, + { + "name": "amphp/process", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "583959df17d00304ad7b0b32285373f985935643" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643", + "reference": "583959df17d00304ad7b0b32285373f985935643", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-31T15:11:55+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/b347be5aff6b2cc025208bb4d896607eb470c018", + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^7", + "league/uri-interfaces": "^7", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-08-22T18:13:20+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "amphp/websocket", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/amphp/websocket.git", + "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/websocket/zipball/963904b6a883c4b62d9222d1d9749814fac96a3b", + "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/socket": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "suggest": { + "ext-zlib": "Required for compression" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Websocket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + } + ], + "description": "Shared code for websocket servers and clients.", + "homepage": "https://github.com/amphp/websocket", + "keywords": [ + "amp", + "amphp", + "async", + "http", + "non-blocking", + "websocket" + ], + "support": { + "issues": "https://github.com/amphp/websocket/issues", + "source": "https://github.com/amphp/websocket/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-10-28T21:28:45+00:00" + }, + { + "name": "amphp/websocket-client", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/websocket-client.git", + "reference": "dc033fdce0af56295a23f63ac4f579b34d470d6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/websocket-client/zipball/dc033fdce0af56295a23f63ac4f579b34d470d6c", + "reference": "dc033fdce0af56295a23f63ac4f579b34d470d6c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2.1", + "amphp/http": "^2.1", + "amphp/http-client": "^5", + "amphp/socket": "^2.2", + "amphp/websocket": "^2", + "league/uri": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1|^2", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/http-server": "^3", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/websocket-server": "^3|^4", + "phpunit/phpunit": "^9", + "psalm/phar": "~5.26.1", + "psr/log": "^1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Websocket\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Async WebSocket client for PHP based on Amp.", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "http", + "non-blocking", + "websocket" + ], + "support": { + "issues": "https://github.com/amphp/websocket-client/issues", + "source": "https://github.com/amphp/websocket-client/tree/v2.0.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-08-24T17:25:34+00:00" + }, { "name": "brianium/paratest", "version": "v7.27.4", @@ -6837,6 +8067,50 @@ ], "time": "2026-10-22T06:15:12+00:00" }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.6", @@ -7288,6 +8562,64 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, { "name": "larastan/larastan", "version": "v3.13.0", @@ -7447,6 +8779,90 @@ }, "time": "2026-09-10T10:47:55+00:00" }, + { + "name": "league/uri-components", + "version": "7.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-components.git", + "reference": "4e37c1663f7d0daebbab6b3d947631b0835b3943" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/4e37c1663f7d0daebbab6b3d947631b0835b3943", + "reference": "4e37c1663f7d0daebbab6b3d947631b0835b3943", + "shasum": "" + }, + "require": { + "league/uri": "^7.9.0", + "php": "^8.1" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-mbstring": "to use the sorting algorithm of URLSearchParams", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI components manipulation library", + "homepage": "http://uri.thephpleague.com", + "keywords": [ + "authority", + "components", + "fragment", + "host", + "middleware", + "modifier", + "path", + "port", + "query", + "rfc3986", + "scheme", + "uri", + "url", + "userinfo" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-components/tree/7.9.0" + }, + "funding": [ + { + "url": "https://github.com/nyamsprod", + "type": "github" + } + ], + "time": "2026-10-16T15:19:33+00:00" + }, { "name": "mockery/mockery", "version": "1.6.15", @@ -8084,6 +9500,90 @@ ], "time": "2026-07-21T07:48:55+00:00" }, + { + "name": "pestphp/pest-plugin-browser", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-browser.git", + "reference": "3425026b6563ca936cbdfabb9ac979355fab74c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-browser/zipball/3425026b6563ca936cbdfabb9ac979355fab74c1", + "reference": "3425026b6563ca936cbdfabb9ac979355fab74c1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3.1.3", + "amphp/http-server": "^3.4.6", + "amphp/websocket-client": "^2.0.2", + "ext-sockets": "*", + "pestphp/pest": "^5.2.1", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", + "symfony/process": "^8.1.7" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "livewire/livewire": "^4.4.7", + "nunomaduro/collision": "^8.9.5", + "orchestra/testbench": "^11.3", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-laravel": "^5.0.1", + "pestphp/pest-plugin-type-coverage": "^5.0.2" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Browser\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Browser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Pest plugin to test browser interactions", + "keywords": [ + "browser", + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-browser/tree/v5.1.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-10-02T10:52:14+00:00" + }, { "name": "pestphp/pest-plugin-laravel", "version": "v5.0.1", @@ -9260,6 +10760,78 @@ ], "time": "2026-10-16T05:56:47+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.9", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "44061cf513e53c6200372fc935ac42271566295d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" + }, + "time": "2026-05-16T17:55:38+00:00" + }, { "name": "sebastian/cli-parser", "version": "5.0.1", diff --git a/package-lock.json b/package-lock.json index 8bb184e..b706b1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,41 @@ "name": "codebar-feedback", "hasInstallScript": true, "license": "MIT", + "devDependencies": { + "playwright": "1.64.1" + }, "engines": { "node": ">=26" } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } } } } diff --git a/package.json b/package.json index eab0cdd..eaa2e32 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "scripts": { "postinstall": "npm --prefix widget ci --audit false --fund false", "build": "npm --prefix widget run build" + }, + "devDependencies": { + "playwright": "1.64.1" } } diff --git a/phpunit.xml b/phpunit.xml index 810b32a..7feb556 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -14,6 +14,9 @@ tests/Arch + + tests/Browser + diff --git a/tests/Browser/FeedbackPageTest.php b/tests/Browser/FeedbackPageTest.php new file mode 100644 index 0000000..ac6b695 --- /dev/null +++ b/tests/Browser/FeedbackPageTest.php @@ -0,0 +1,145 @@ +project = FeedbackProject::factory()->create(['name' => 'Alpha', 'allowed_origins' => [TEST_ORIGIN]]); +}); + +/** Sends feedback the way the widget does, with screenshot and diagnostics. */ +function sentFeedback(FeedbackProject $project): FeedbackEntry +{ + postFeedback($project, [ + 'screenshot' => fakeJpeg(200, 400), + 'identity' => json_encode(['id' => 'u-1', 'name' => 'Anna Muster']), + 'console_log' => json_encode([['type' => 'error', 'message' => 'Boom', 'count' => 2, 'time' => 1]]), + 'network_log' => json_encode([['method' => 'POST', 'url' => 'https://api.beispiel.ch/save', 'status' => 500, 'duration' => 120, 'count' => 1, 'time' => 1]]), + ])->assertCreated(); + + return FeedbackEntry::query()->latest('id')->firstOrFail(); +} + +it('shows the message, the sender and the screenshot', function (): void { + $entry = sentFeedback($this->project); + + $page = visit(FeedbackLink::page($entry, now()->addHour())); + + $page->assertSee('Alpha') + ->assertSee($entry->id) + ->assertSee('open') + ->assertSee('Die Schaltfläche «Speichern» reagiert nicht.') + ->assertSee('Anna Muster') + ->assertSee('anna@beispiel.ch') + ->assertSee('1 console error') + ->assertVisible('img[alt^="Screenshot of"]') + ->assertNoJavaScriptErrors(); + + expect($page->script('() => document.querySelector(\'img[alt^="Screenshot of"]\').naturalWidth'))->toBe(200); +}); + +it('switches to the technical tab with console errors, failed requests and the environment', function (): void { + $entry = sentFeedback($this->project); + + $page = visit(FeedbackLink::page($entry, now()->addHour())); + + $page->assertDontSee('Failed requests') + ->click('#tab-technical') + ->assertSee('Failed requests') + ->assertSee('Boom') + ->assertSee('https://api.beispiel.ch/save') + ->assertSee('Environment') + ->assertSee('Europe/Zurich') + ->assertDontSee('Die Schaltfläche «Speichern» reagiert nicht.') + ->assertAttribute('#tab-technical', 'aria-selected', 'true') + ->assertNoJavaScriptErrors(); + + expect($page->script('() => location.hash'))->toBe('#technical'); + + $page->click('#tab-feedback') + ->assertSee('Die Schaltfläche «Speichern» reagiert nicht.') + ->assertDontSee('Failed requests'); +}); + +it('opens the technical tab directly from the #technical link', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour()).'#technical') + ->assertSee('Failed requests') + ->assertAttribute('#tab-technical', 'aria-selected', 'true') + ->assertNoJavaScriptErrors(); +}); + +it('hides and shows the message card', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour())) + ->click('#card-toggle') + ->assertAttribute('#card-toggle', 'aria-expanded', 'false') + ->assertAttribute('#card-toggle', 'aria-label', 'Show message') + ->click('#card-toggle') + ->assertAttribute('#card-toggle', 'aria-expanded', 'true') + ->assertNoJavaScriptErrors(); +}); + +it('copies the feedback ID and announces it', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour())) + ->click('[aria-label="Copy feedback ID"]') + ->assertSeeIn('#live', 'Copied to clipboard') + ->assertNoJavaScriptErrors(); +}); + +it('lets the team complete and reopen an entry from a team link', function (): void { + $entry = sentFeedback($this->project); + + $page = visit(FeedbackLink::page($entry, now()->addHour(), manage: true, by: 'Sebastian')); + + $page->press('Mark as completed') + ->assertSee('completed') + ->assertSee('Reopen') + ->assertDontSee('Mark as completed') + ->assertNoJavaScriptErrors(); + + expect($entry->fresh()->completed_at)->not->toBeNull() + ->and(Activity::query()->where('subject_id', $entry->id)->sole()->properties['by'])->toBe('Sebastian'); + + $page->press('Reopen') + ->assertSee('Mark as completed') + ->assertNoJavaScriptErrors(); + + expect($entry->fresh()->completed_at)->toBeNull(); +}); + +it('shows no status buttons on the read-only link the sender gets', function (): void { + $entry = sentFeedback($this->project); + + visit(FeedbackLink::page($entry, now()->addHour())) + ->assertSee('open') + ->assertDontSee('Mark as completed') + ->assertNoJavaScriptErrors(); +}); + +it('rejects expired and tampered links with an empty page', function (string $url): void { + $entry = sentFeedback($this->project); + $url = str_replace('{entry}', $entry->id, $url); + + $page = visit($url === 'expired' ? FeedbackLink::page($entry, now()->subMinute()) : $url); + + expect($page->script('() => document.body.innerText.trim()'))->toBe(''); + $page->assertDontSee('Alpha'); +})->with([ + 'expired' => ['expired'], + 'tampered' => ['/v1/feedback/{entry}?expires=4102444800&signature=forged'], +]); + +it('answers a typed API URL with an empty page', function (): void { + $page = visit("/v1/projects/{$this->project->public_key}/config"); + + expect($page->script('() => document.body.innerText.trim()'))->toBe(''); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 20fda49..f6d5a7e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,6 +11,7 @@ pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Feature'); pest()->extend(TestCase::class)->in('Unit'); +pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Browser'); const TEST_ORIGIN = 'https://www.beispiel.ch'; From 64cfffacecbaac865013ebb3b79541b4b4d1355e Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Fri, 23 Oct 2026 14:36:29 +0200 Subject: [PATCH 2/4] ci: run the Pest browser tests on GitHub-hosted runners New lane "pull-request | ci pest browser" on ubuntu-latest (public repository, so no self-hosted runners) with the PHP and Node setup of the pest lane and a PostgreSQL 17 service on a Docker-assigned port. It builds the widget, installs Playwright's Chromium and runs tests/Browser; on failure it uploads the logs and the failure screenshots. The pest, coverage and database matrix lanes run plain `pest`, which would now also pick up the Browser testsuite without a browser, so they exclude it. Their coverage comes from the same tests as before. Co-Authored-By: Claude Opus 5.5 --- .../workflows/pest_coverage_pull_request.yml | 4 +- .../pest_database_matrix_pull_request.yml | 2 +- .github/workflows/pest_pull_request.yml | 2 +- .../workflows/pest_pull_request_browser.yml | 119 ++++++++++++++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/pest_pull_request_browser.yml diff --git a/.github/workflows/pest_coverage_pull_request.yml b/.github/workflows/pest_coverage_pull_request.yml index f811b59..bcd8979 100644 --- a/.github/workflows/pest_coverage_pull_request.yml +++ b/.github/workflows/pest_coverage_pull_request.yml @@ -91,10 +91,10 @@ jobs: run: php -v - name: Execute Pest Coverage - run: XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=${{ env.PEST_MIN_COVERAGE }} + run: XDEBUG_MODE=coverage ./vendor/bin/pest --exclude-testsuite=Browser --coverage --min=${{ env.PEST_MIN_COVERAGE }} - name: Execute Pest Type Coverage - run: XDEBUG_MODE=coverage ./vendor/bin/pest --type-coverage --min=${{ env.PEST_MIN_TYPE_COVERAGE }} + run: XDEBUG_MODE=coverage ./vendor/bin/pest --exclude-testsuite=Browser --type-coverage --min=${{ env.PEST_MIN_TYPE_COVERAGE }} - name: Store Log Artifacts if: failure() diff --git a/.github/workflows/pest_database_matrix_pull_request.yml b/.github/workflows/pest_database_matrix_pull_request.yml index 6e33d9b..8361612 100644 --- a/.github/workflows/pest_database_matrix_pull_request.yml +++ b/.github/workflows/pest_database_matrix_pull_request.yml @@ -68,4 +68,4 @@ jobs: run: composer install --no-interaction --no-progress --prefer-dist - name: Pest - run: ./vendor/bin/pest + run: ./vendor/bin/pest --exclude-testsuite=Browser diff --git a/.github/workflows/pest_pull_request.yml b/.github/workflows/pest_pull_request.yml index 78a412a..a10d705 100644 --- a/.github/workflows/pest_pull_request.yml +++ b/.github/workflows/pest_pull_request.yml @@ -85,7 +85,7 @@ jobs: run: npm run build - name: Execute Pest Test - run: ./vendor/bin/pest + run: ./vendor/bin/pest --exclude-testsuite=Browser - name: Store Log Artifacts if: failure() diff --git a/.github/workflows/pest_pull_request_browser.yml b/.github/workflows/pest_pull_request_browser.yml new file mode 100644 index 0000000..7b29b3b --- /dev/null +++ b/.github/workflows/pest_pull_request_browser.yml @@ -0,0 +1,119 @@ +name: Pull Request | CI Pest Browser + +on: pull_request + +# A new push cancels the run it replaces. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + ci_pest_browser: + # Public repository: GitHub-hosted runners only, never the self-hosted group. + runs-on: ubuntu-latest + name: pull-request | ci pest browser + + services: + postgres: + image: postgres:17 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: laravel_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432 + + env: + PHP_VERSION: 8.5 + NODE_VERSION: 26 + + steps: + # CI always tests against PostgreSQL. These variables beat .env and the + # phpunit.xml defaults (in-memory SQLite). The service gets a + # Docker-assigned host port. + - name: Point the app at the Postgres service + run: | + { + echo "DB_CONNECTION=pgsql" + echo "DB_HOST=127.0.0.1" + echo "DB_PORT=${{ job.services.postgres.ports['5432'] }}" + echo "DB_DATABASE=laravel_test" + echo "DB_USERNAME=postgres" + echo "DB_PASSWORD=postgres" + echo "DB_URL=" + } >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup PHP Environment + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.PHP_VERSION }} + extensions: dom, curl, fileinfo, pgsql, pdo_pgsql, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + + - name: Prepare the .env file + run: cp .env.example .env + + - name: Cache Composer Dependencies + uses: actions/cache@v6 + with: + path: | + ~/.composer/cache/files + ~/.composer/cache/repo + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install Composer Dependencies + run: composer install --no-progress --prefer-dist --no-interaction --optimize-autoloader + + - name: Publish Laravel assets + run: php artisan vendor:publish --tag=laravel-assets --ansi --force + + - name: Setup PostgreSQL Database + run: | + until pg_isready -h localhost -p "$DB_PORT" -U postgres; do sleep 1; done + + - name: Generate App Key & Run Migrations + run: | + php artisan key:generate + php artisan migrate:fresh + + - name: Setup Node.js Environment + uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install NPM Dependencies + run: npm ci + + - name: Build Frontend Assets + run: npm run build + + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + + - name: Execute Pest Browser Tests + run: ./vendor/bin/pest tests/Browser + + - name: Store Log Artifacts + if: failure() + uses: actions/upload-artifact@v7 + with: + name: Store report artifacts + path: ./storage/logs + + - name: Store Screenshots + if: failure() + uses: actions/upload-artifact@v7 + with: + name: Browser Screenshots + path: ./tests/Browser/Screenshots + if-no-files-found: ignore From 3532c9dd7106774044bc1780329f3594ccaba786 Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Fri, 23 Oct 2026 16:09:43 +0200 Subject: [PATCH 3/4] fix(health): drop OptimizedAppCheck, Cloud containers never see deploy-time caches Laravel Cloud builds the config/route/event caches in the deploy command, but those caches never reach the running containers, so OptimizedAppCheck failed on every environment. Remove it from the health registration and from the tests that pin the registered checks. `php artisan optimize` in the deploy script stays. Co-Authored-By: Claude Opus 5.5 --- app/Providers/AppServiceProvider.php | 5 +---- tests/Feature/HealthCheckTest.php | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6ae236c..0511c7b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -25,7 +25,6 @@ use Spatie\Health\Checks\Checks\CacheCheck; use Spatie\Health\Checks\Checks\DebugModeCheck; use Spatie\Health\Checks\Checks\EnvironmentCheck; -use Spatie\Health\Checks\Checks\OptimizedAppCheck; use Spatie\Health\Checks\Checks\ScheduleCheck; use Spatie\Health\Facades\Health; use Spatie\SecurityAdvisoriesHealthCheck\SecurityAdvisoriesCheck; @@ -41,8 +40,7 @@ public function boot(): void /** * Checks for Oh Dear (OH_DEAR_HEALTH_CHECK_*) and `php artisan health:check`: - * the standard set agreed for all Laravel Cloud apps. OptimizedAppCheck - * needs `php artisan optimize` in the deploy command. + * the standard set agreed for all Laravel Cloud apps. */ private function configureHealthChecks(): void { @@ -52,7 +50,6 @@ private function configureHealthChecks(): void DatabaseCheck::new(), DatabaseConnectionCountCheck::new(), CacheCheck::new(), - OptimizedAppCheck::new(), ScheduleCheck::new()->heartbeatMaxAgeInMinutes(120), SecurityAdvisoriesCheck::new(), NightwatchIssuesCheck::new()->if(filled(config('default.nightwatch.api_token')) && filled(config('default.nightwatch.application_id'))), diff --git a/tests/Feature/HealthCheckTest.php b/tests/Feature/HealthCheckTest.php index be3c838..58758cd 100644 --- a/tests/Feature/HealthCheckTest.php +++ b/tests/Feature/HealthCheckTest.php @@ -35,7 +35,6 @@ 'SecurityAdvisoriesCheck', 'ScheduleCheck', 'CacheCheck', - 'OptimizedAppCheck', 'JobsCheck', 'FailedJobsCheck', 'PendingMigrationsCheck', From 532eb9c60951d855b3776c0cca2ee6870e019e66 Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Fri, 23 Oct 2026 16:13:54 +0200 Subject: [PATCH 4/4] fix(health): report the database connection count as info only DatabaseConnectionCountCheck no longer warns or fails; it reports "N connections" with the count in meta. On PostgreSQL it counts only connections to the current database (pg_stat_activity filtered by current_database()), because the Neon servers are shared between apps. Tests cover the pgsql count, a non-numeric result and the spatie fallback for other drivers. Co-Authored-By: Claude Opus 5.5 --- app/Checks/DatabaseConnectionCountCheck.php | 42 ++++++++--- .../DatabaseConnectionCountCheckTest.php | 74 +++++++++++++++++-- 2 files changed, 100 insertions(+), 16 deletions(-) diff --git a/app/Checks/DatabaseConnectionCountCheck.php b/app/Checks/DatabaseConnectionCountCheck.php index e669a02..32dfca6 100644 --- a/app/Checks/DatabaseConnectionCountCheck.php +++ b/app/Checks/DatabaseConnectionCountCheck.php @@ -4,32 +4,52 @@ namespace App\Checks; +use Illuminate\Database\ConnectionResolverInterface; +use Illuminate\Support\Str; use Spatie\Health\Checks\Checks\DatabaseConnectionCountCheck as SpatieDatabaseConnectionCountCheck; use Spatie\Health\Checks\Result; /** - * spatie's DatabaseConnectionCountCheck with the agreed thresholds (warn - * above 50, fail above 80), reporting ok for apps without a database and - * for SQLite, which has no connection count. + * Informational only: reports the number of open connections of the app's + * own database and never warns or fails. On PostgreSQL only connections to + * the current database count, because Neon servers are shared between apps. + * Apps without a database and SQLite report ok. */ class DatabaseConnectionCountCheck extends SpatieDatabaseConnectionCountCheck { - protected ?int $warningThreshold = 50; - - protected int $errorThreshold = 80; - public function run(): Result { if (! DatabaseAvailability::isAvailable()) { return Result::make()->shortSummary('no database')->ok(); } - $connection = $this->connectionName ?? $this->getDefaultConnectionName(); - - if (config("database.connections.{$connection}.driver") === 'sqlite') { + if (config("database.connections.{$this->connection()}.driver") === 'sqlite') { return Result::make()->shortSummary('sqlite')->ok(); } - return parent::run(); + $connectionCount = $this->getConnectionCount(); + + return Result::make() + ->ok() + ->meta(['connection_count' => $connectionCount]) + ->shortSummary($connectionCount.' '.Str::plural('connection', $connectionCount)); + } + + protected function getConnectionCount(): int + { + $connection = app(ConnectionResolverInterface::class)->connection($this->connection()); + + if ($connection->getDriverName() !== 'pgsql') { + return parent::getConnectionCount(); + } + + $count = $connection->scalar('select count(*) from pg_stat_activity where datname = current_database()'); + + return is_numeric($count) ? (int) $count : 0; + } + + private function connection(): string + { + return $this->connectionName ?? $this->getDefaultConnectionName(); } } diff --git a/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php b/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php index da0dd13..c20513a 100644 --- a/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php +++ b/tests/Unit/Checks/DatabaseConnectionCountCheckTest.php @@ -3,6 +3,9 @@ declare(strict_types=1); use App\Checks\DatabaseConnectionCountCheck; +use Illuminate\Database\Connection; +use Illuminate\Database\ConnectionResolverInterface; +use Illuminate\Database\MySqlConnection; use Spatie\Health\Enums\Status; function connectionCountCheckWith(int $connections): DatabaseConnectionCountCheck @@ -16,6 +19,19 @@ function connectionCountCheckWith(int $connections): DatabaseConnectionCountChec return $check; } +function bindConnection(string $class, string $driver, mixed $count): void +{ + $connection = Mockery::mock($class); + $connection->shouldReceive('getDriverName')->andReturn($driver); + $connection->shouldReceive('scalar')->andReturn($count); + $connection->shouldReceive('selectOne')->andReturn((object) ['COUNT(*)' => $count]); + + $resolver = Mockery::mock(ConnectionResolverInterface::class); + $resolver->shouldReceive('connection')->andReturn($connection); + + app()->instance(ConnectionResolverInterface::class, $resolver); +} + it('reports ok with "no database" when the app has none', function () { config(['health.uses_database' => false]); @@ -34,10 +50,58 @@ function connectionCountCheckWith(int $connections): DatabaseConnectionCountChec ->and($result->shortSummary)->toBe('sqlite'); })->group('unit', 'checks'); -it('applies the agreed thresholds', function (int $connections, Status $status) { - expect(connectionCountCheckWith($connections)->run()->status->equals($status))->toBeTrue(); +it('only reports the count and never warns or fails', function (int $connections, string $summary) { + $result = connectionCountCheckWith($connections)->run(); + + expect($result->status->equals(Status::ok()))->toBeTrue() + ->and($result->shortSummary)->toBe($summary) + ->and($result->meta)->toBe(['connection_count' => $connections]); })->with([ - 'ok' => [10, Status::ok()], - 'warning above 50' => [60, Status::warning()], - 'failed above 80' => [90, Status::failed()], + 'one' => [1, '1 connection'], + 'many' => [500, '500 connections'], ])->group('unit', 'checks'); + +it('counts only the connections of the current database on postgres', function () { + config(['database.connections.health_pgsql' => ['driver' => 'pgsql']]); + bindConnection(Connection::class, 'pgsql', 7); + + $check = new class extends DatabaseConnectionCountCheck + { + public function count(): int + { + return $this->getConnectionCount(); + } + }; + + expect($check->connectionName('health_pgsql')->count())->toBe(7); +})->group('unit', 'checks'); + +it('reports zero when postgres returns no number', function () { + config(['database.connections.health_pgsql' => ['driver' => 'pgsql']]); + bindConnection(Connection::class, 'pgsql', null); + + $check = new class extends DatabaseConnectionCountCheck + { + public function count(): int + { + return $this->getConnectionCount(); + } + }; + + expect($check->connectionName('health_pgsql')->count())->toBe(0); +})->group('unit', 'checks'); + +it('falls back to spatie for other drivers', function () { + config(['database.connections.health_mysql' => ['driver' => 'mysql']]); + bindConnection(MySqlConnection::class, 'mysql', 4); + + $check = new class extends DatabaseConnectionCountCheck + { + public function count(): int + { + return $this->getConnectionCount(); + } + }; + + expect($check->connectionName('health_mysql')->count())->toBe(4); +})->group('unit', 'checks');