mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-10-23 05:32:05 +00:00
Add e2e test for maintenance author fix
This commit is contained in:
1 parent
08753d59ab
commit
f8f1b44795
5 files changed
+145
-7
No files matched your search
+32
-6
@@ -74,19 +74,45 @@ be fully covered by Go tests. For UI or client-JS changes, run the app (on :8090
|
||||
and check it in a browser **with the devtools console open** (CSP violations only
|
||||
show there). If you can't validate it, say so — don't claim it works.
|
||||
|
||||
**Browser tests (`mise run e2e`).** Regressions worth locking in get a `chromedp`
|
||||
test. These live in their own black-box package `internal/e2e/` (one
|
||||
`<feature>_test.go` per feature, sharing `harness_test.go`) behind a
|
||||
`//go:build browser` tag, so `go test ./...` never needs a browser — only
|
||||
`mise run e2e` runs them. That task starts a `chromedp/headless-shell` **Docker
|
||||
container** (no local Chrome install) and runs the `browser`-tagged suite against
|
||||
an in-process test server; the browser reaches the server via
|
||||
`host.docker.internal`, and the harness fails the test on any CSP violation. If
|
||||
the container isn't up, the tests **skip** (never fail). Reuse the `e2eServer`
|
||||
harness (`newE2EServer`, `login`, `newRepeater`, `startBrowser`,
|
||||
`setSessionCookie`) — it authenticates via the public `/session/callback` handoff
|
||||
and depends only on exported app APIs, so keep it that way (no reaching into
|
||||
package internals). Give elements a stable `data-testid`/class hook rather than
|
||||
selecting on Bootstrap layout classes. CI runs these non-gating
|
||||
(`.woodpecker/e2e.yaml`); `E2E_DEVTOOLS_URL`/`E2E_BROWSER_HOST` override the
|
||||
addresses there.
|
||||
|
||||
### 5. CI is the gate, not the first line
|
||||
Woodpecker (`.woodpecker/`): `test` + `lint` + `vuln` (govulncheck) must pass
|
||||
before `build`; `deploy` follows `build` on `main`. A reachable CVE blocks the
|
||||
build. Catch problems locally first; if CI catches something you didn't, close the
|
||||
local-testing gap.
|
||||
|
||||
### 6. Git hygiene — stage, don't commit
|
||||
### 6. Git hygiene — stage one batch at a time, don't commit
|
||||
This is a single-developer project with no PR workflow. **Stage your changes but do
|
||||
NOT commit or push** — the developer does the committing. Keep each staged set to
|
||||
one logical change (so it becomes one clean commit): stage exactly those files with
|
||||
explicit `git add <file> …` (never `-A`/`.`), and review `git diff --cached` before
|
||||
handing off. If a task spans two logical changes, finish and hand off the first
|
||||
(staged) before starting the second.
|
||||
NOT commit or push** — the developer commits from a git GUI. Stage exactly the
|
||||
files for one logical change with explicit `git add <file> …` (never `-A`/`.`).
|
||||
|
||||
**One batch at a time.** When a task spans multiple logical commits, do NOT stage
|
||||
everything at once — that co-mingles the batches in the single index and makes them
|
||||
impossible to commit separately. Instead:
|
||||
1. Stage exactly one batch, give a one-line description the developer can base a
|
||||
commit message on, and **stop** — hand off and wait.
|
||||
2. The developer reviews `git diff --cached` and commits that batch in their GUI.
|
||||
3. Continue only when the developer says so; then stage the next batch.
|
||||
|
||||
Order batches so each is independently green (builds/tests pass) once committed.
|
||||
Provide file-list `git add` commands only as reference — do not run the next
|
||||
batch's staging until told to.
|
||||
|
||||
## Performance
|
||||
The product is server-rendered over Postgres; perf work lives mostly in SQL and
|
||||
|
||||
@@ -20,3 +20,12 @@ run = "go run ./cmd/meshtender --seed"
|
||||
|
||||
[tasks.reset]
|
||||
run = "go run ./cmd/meshtender --reset"
|
||||
|
||||
[tasks.e2e]
|
||||
run = """
|
||||
docker run -d --rm --name mt-headless -p 9222:9222 \
|
||||
--add-host=host.docker.internal:host-gateway chromedp/headless-shell:latest >/dev/null
|
||||
trap 'docker stop mt-headless >/dev/null' EXIT
|
||||
until curl -sf http://127.0.0.1:9222/json/version >/dev/null; do sleep 0.5; done
|
||||
go test -tags browser ./internal/e2e/
|
||||
"""
|
||||
@@ -0,0 +1,45 @@
|
||||
# Browser (end-to-end) tests: drive a real headless Chrome against the app to
|
||||
# catch rendered-DOM and CSP regressions the Go tests can't. Runs the
|
||||
# `browser`-tagged suite in internal/e2e.
|
||||
#
|
||||
# This is its OWN workflow and deliberately NOT a `build` dependency: a flaky
|
||||
# browser must never block the release gate. Promote it to gating (add it to
|
||||
# build.yaml's depends_on) once it has proven stable.
|
||||
#
|
||||
# NOTE: the container<->step networking here is unverified against this
|
||||
# Woodpecker instance/backend. Two addresses must line up:
|
||||
# - E2E_DEVTOOLS_URL: this step reaching the browser service (by service name;
|
||||
# mirrors how the test step reaches `postgres`, which is known to work).
|
||||
# - E2E_BROWSER_HOST: the browser service reaching back to this step's
|
||||
# in-process test server. On the docker backend a step is reachable by its
|
||||
# name (`e2e`); on other backends this may need adjusting. The tests skip
|
||||
# cleanly if the DevTools endpoint can't be reached, so a misconfig here
|
||||
# yields skips, not false failures.
|
||||
when:
|
||||
- event: [push, pull_request]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
environment:
|
||||
POSTGRES_USER: meshtender
|
||||
POSTGRES_PASSWORD: meshtender
|
||||
POSTGRES_DB: meshtender_test
|
||||
headless:
|
||||
image: chromedp/headless-shell:latest
|
||||
|
||||
steps:
|
||||
e2e:
|
||||
image: golang:1.26.4
|
||||
environment:
|
||||
MESHTENDER_TEST_DATABASE_URL: postgres://meshtender:meshtender@postgres:5432/meshtender_test?sslmode=disable
|
||||
# This step -> the browser's DevTools endpoint (service by name).
|
||||
E2E_DEVTOOLS_URL: http://headless:9222
|
||||
# The browser service -> this step's in-process test server (step by name).
|
||||
E2E_BROWSER_HOST: e2e
|
||||
commands:
|
||||
- until bash -c 'echo > /dev/tcp/postgres/5432' 2>/dev/null; do sleep 1; done
|
||||
- until bash -c 'echo > /dev/tcp/headless/9222' 2>/dev/null; do sleep 1; done
|
||||
- go test -tags browser ./internal/e2e/
|
||||
|
||||
depends_on: []
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="text-secondary float-end">{{.PerformedAt.Format "Jan 2, 2006"}}</div>
|
||||
<div style="white-space: pre-wrap">{{.Note}}</div>
|
||||
<div class="text-secondary small mt-2 d-flex align-items-center gap-2">
|
||||
<span>{{if .AuthorName}}{{.AuthorName}}{{else}}(unknown){{end}}</span>
|
||||
<span class="maint-author" data-testid="maint-author">{{if .AuthorName}}{{.AuthorName}}{{else}}(unknown){{end}}</span>
|
||||
{{if $.IsOwner}}
|
||||
<form method="post" action="/repeaters/{{$.Repeater.PublicID}}/maintenance/delete" class="m-0 ms-auto" data-confirm="Delete this maintenance entry?">
|
||||
<input type="hidden" name="entry_id" value="{{.ID}}">
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//go:build browser
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cdplog "github.com/chromedp/cdproto/log"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
// TestE2EMaintenanceShowsCurrentAuthorName is the browser-level guarantee for
|
||||
// the stale-author-name bug: the maintenance history must render the author's
|
||||
// *current* display name, not the name captured when the entry was logged. It
|
||||
// renames the author after logging the entry, then asserts the rendered name
|
||||
// reflects the rename — which only holds with the ListMaintenance fix in place.
|
||||
// Also asserts the page runs clean under the strict CSP.
|
||||
func TestE2EMaintenanceShowsCurrentAuthorName(t *testing.T) {
|
||||
srv := newE2EServer(t)
|
||||
user, cookie := srv.login(t, "e2euser")
|
||||
rep := srv.newRepeater(t, user.ID, "Field Repeater")
|
||||
|
||||
// Log an entry snapshotting the name as it was (the username, since the
|
||||
// seeded user has no display name yet), exactly as the handler does.
|
||||
if err := srv.store.AddMaintenanceEntry(srv.ctx, rep.ID, user.ID, "e2euser", "replaced battery", time.Now()); err != nil {
|
||||
t.Fatalf("add maintenance entry: %v", err)
|
||||
}
|
||||
// Now rename the author. A correct read path shows the new name on the old
|
||||
// entry; the pre-fix snapshot read would still show "e2euser".
|
||||
const renamed = "Renamed In Browser"
|
||||
if err := srv.store.SetDisplayName(srv.ctx, user.ID, renamed); err != nil {
|
||||
t.Fatalf("set display name: %v", err)
|
||||
}
|
||||
|
||||
bctx, cancel, watch := startBrowser(t)
|
||||
defer cancel()
|
||||
|
||||
url := srv.browserURL + "/repeaters/" + rep.PublicID + "/maintenance"
|
||||
var authorText string
|
||||
if err := chromedp.Run(bctx,
|
||||
network.Enable(),
|
||||
cdplog.Enable(),
|
||||
setSessionCookie(cookie),
|
||||
chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(`.maint-author`, chromedp.ByQuery),
|
||||
chromedp.Text(`.maint-author`, &authorText, chromedp.ByQuery),
|
||||
); err != nil {
|
||||
t.Fatalf("browser run against %s: %v", url, err)
|
||||
}
|
||||
|
||||
if got := strings.TrimSpace(authorText); got != renamed {
|
||||
t.Fatalf("maintenance author rendered %q, want current display name %q (stale snapshot?)", got, renamed)
|
||||
}
|
||||
watch.assertClean(t)
|
||||
}
|
||||
Reference in new issue
Block a user