//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) }