From 93739ad46e7f9768c7d7358e6e645be5126acf11 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Sun, 25 Oct 2026 08:27:52 -0400 Subject: [PATCH] UI testing: a static check for the grid-gate class, a dev seeder, and the grid pass recorded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things, all from the same morning's findings. 1. paradigm-scan grows a FIFTH check, `datatable_gate` (blocker). A view whose registered JS calls tigerDataTable() while its owning controller never sets useDataTables is the TIGER-390 class: HTTP 200, empty table, zero console errors, because the grid script's bail-out guard is doing its job. Invisible to PHPUnit (nothing renders) and to the browser probe's "no console errors" detector, so it has to be caught statically. Mutation-verified twice — removing the flag from system/SitesController and then newsletter/AdminController each produces exactly one finding naming the file and the cause; restoring it goes quiet. Scope stated in the code: the flag is looked for anywhere in the controller, not inside the specific action, so a finding means the controller never sets it at all — the case that actually shipped, and zero false positives. That check also made the scan 20x SLOWER (29s), because reading a view's registered JS meant a `find` shell-out per asset per view — and hasAjaxHandler was already doing the same. Both now share one basename->path index built once: 29s -> 1.4s, faster than before the check existed. 2. tests/ui/bin/seed-dev.php — the probe org owned no content, so EVERY grid looked exactly like the three broken ones and separating them took a SQL query a scenario cannot run. Seeds one page, one article, one menu, one inactive snippet and one subscriber, all labelled `UI Probe`, addresses on the reserved .invalid domain. Dry run unless --apply; idempotent. Its guard went through three honest failures worth recording. First version read config.json relative to __DIR__, found none when run from a copied path, and so "passed" with an empty never_target — a check that could only ever succeed. Second version matched hosts as substrings, which would refuse tiger-dev.webtigers.com for containing webtigers.com. Third discovery killed the whole approach: an install cannot tell you which site it is — on tiger-dev APPLICATION_ENV is literally "production", tiger.site.url is unset, and gethostname() returns an EC2 internal name. So the operator NAMES the target with --base and never_target refuses the forbidden ones, which is the contract the two probes already use. Five branches each verified on dev: no --base refuses, tiger.webtigers.com refuses, www.webtigers.com refuses, a missing never_target refuses, and the dev base proceeds. 3. scenarios/admin-grids.json — the grid pass recorded in the same intent-level shape as auth.json, five scenarios with `verified` lines carrying what the real run saw. UI-GRID-EMPTY-TRUTH is the one to keep: an empty grid and a broken grid are the same picture, so never record a PASS on an empty grid without establishing why it is empty. That discipline is what surfaced TIGER-391 — /media's honest 0 rows next to /cms/page's 25 is what exposed the missing org filter. Scan: 108 views, 0 findings on all five checks. Unit suite: 1200 tests, 18318 assertions, green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013vdyTeoDrGE4hKxt53TsE8 --- tests/ui/README.md | 23 +++++ tests/ui/bin/paradigm-scan.php | 93 +++++++++++++++++-- tests/ui/bin/seed-dev.php | 139 ++++++++++++++++++++++++++++ tests/ui/scenarios/admin-grids.json | 91 ++++++++++++++++++ 4 files changed, 340 insertions(+), 6 deletions(-) create mode 100644 tests/ui/bin/seed-dev.php create mode 100644 tests/ui/scenarios/admin-grids.json diff --git a/tests/ui/README.md b/tests/ui/README.md index 67c1d408..80e38334 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -8,6 +8,10 @@ AI-driven functional testing for the Tiger UI. Three layers, each answering a di | `php tests/ui/bin/auth-probe.php` | does the AJAX/JSON contract behave? | seconds, no browser | | the `tiger-ui-probe` skill + Playwright MCP | does it WORK for a user? | a real browser | +Browser work is recorded as **scenarios** under `scenarios/` — `auth.json` (sign in, failed sign +in, reset, sign out) and `admin-grids.json` (every admin data grid actually draws). Each scenario is +intent-level with no selectors, and carries a `verified` line naming what the last real run saw. + The static scan is exhaustive but can only see code; the browser proves live behaviour but only on pages someone wrote a scenario for. Run both — that gap is where the TigerImage studio bugs lived (three defects that all passed a green PHPUnit suite because nothing ever rendered). @@ -23,6 +27,25 @@ scenarios use `@example.invalid` identifiers so a run can never trip a real acco Copy `credentials.local.json.dist` to `credentials.local.json` (gitignored) and fill in a **dev** test account. Without it the authenticated scenarios are skipped; everything anonymous still runs. +## Seed first, or a grid assertion means nothing + +``` +sudo -u apache php tests/ui/bin/seed-dev.php \ + --base=https://tiger-dev.webtigers.com --email=uiprobe-super@tiger.test --apply +``` + +An **empty grid and a broken grid are the same picture** — that is how three grids shipped serving +HTTP 200 with no rows and no console error (TIGER-390). If the probe account's org owns no content, +every grid looks like those three and the only way to tell them apart is a SQL query, which a +scenario cannot run. The seeder gives that org one page, one article, one menu, one (inactive) +snippet and one subscriber, all labelled `UI Probe` and on the reserved `.invalid` domain. + +It is a **dry run** unless you pass `--apply`, it is idempotent, and it refuses to run without +`--base`; a `--base` named in `never_target` is refused, and so is a missing `never_target` list. +That guard is explicit on purpose: an install cannot tell you which site it is — on tiger-dev, +`APPLICATION_ENV` is literally `production`, `tiger.site.url` is unset and `gethostname()` returns +an EC2 internal name — so the operator names the target and the list refuses the forbidden ones. + ## Exit codes Both probes exit non-zero on a real failure, so either drops into a script when you want that. diff --git a/tests/ui/bin/paradigm-scan.php b/tests/ui/bin/paradigm-scan.php index 44ba9ea9..2a3a5156 100644 --- a/tests/ui/bin/paradigm-scan.php +++ b/tests/ui/bin/paradigm-scan.php @@ -24,6 +24,7 @@ 'native_submit' => ['Form has no AJAX handler (native submit)','review'], 'rendered_rows' => ['Table rows server-rendered, not from /api','bug'], 'inline_script' => ['