template-monorepo/test/e2e/journey.go

87 lines
2.6 KiB
Go

//go:build e2e
package e2e
import (
"sync/atomic"
"testing"
)
// Journey is a k6-style user journey: an ordered sequence of HTTP steps that
// share local state (closures over the parent test). If any step fails the
// remaining steps are auto-skipped, mirroring k6 scenario "abort on fail"
// behaviour so logs are easy to read — you stop at the first broken step.
//
// Use j.Step(id, desc, fn) to add steps; the framework prints:
//
// ▶ [J-1] Tenant Owner 入職第一天
// ▶ [J-1.1] GET /me 看自己是誰
// ▶ [J-1.2] PATCH /me 更新 display_name
// ⊘ [J-1.3] skipped (journey aborted)
type Journey struct {
t *testing.T
id string
title string
aborted atomic.Bool
total int
ran int
failed bool
}
// NewJourney prints the journey banner and returns a builder.
// Call j.Run() at the end to print the final summary.
func NewJourney(t *testing.T, id, title string) *Journey {
t.Helper()
t.Logf("▶ [%s] %s", id, title)
return &Journey{t: t, id: id, title: title}
}
// Step adds a step. fn receives a sub-test t scoped to this step so testify
// require.* abort just this step (not the whole Test function), letting the
// framework print a clean "aborted" line for subsequent steps.
//
// Pre-existing failures in earlier steps short-circuit the step and emit a
// "⊘ skipped" line.
func (j *Journey) Step(id, desc string, fn func(t *testing.T)) {
j.total++
stepID := j.id + "." + id
name := stepID + " " + desc
j.t.Run(name, func(t *testing.T) {
if j.aborted.Load() {
t.Skipf("⊘ [%s] skipped — journey aborted at an earlier step", stepID)
return
}
t.Logf(" ▶ [%s] %s", stepID, desc)
j.ran++
// fn may call require.* which marks t failed and FailNow's. After it
// returns we inspect t.Failed() to decide whether to abort siblings.
fn(t)
if t.Failed() {
j.aborted.Store(true)
j.failed = true
t.Logf("✗ [%s] FAIL — aborting remaining steps", stepID)
}
})
}
// SkipStep marks a step as intentionally skipped (e.g. requires ZITADEL).
// The journey is NOT aborted; the next Step still runs.
func (j *Journey) SkipStep(id, desc, reason string) {
j.total++
stepID := j.id + "." + id
name := stepID + " " + desc
j.t.Run(name, func(t *testing.T) {
t.Skipf("⊘ [%s] %s — %s", stepID, desc, reason)
})
}
// Summary prints the final journey result. Always defer this right after
// NewJourney so it runs even on require.* abort.
func (j *Journey) Summary() {
status := "✔"
if j.failed {
status = "✗"
}
j.t.Logf("%s [%s] %s — %d/%d steps executed", status, j.id, j.title, j.ran, j.total)
}