test(e2e): framework E2E binary asli (pty + emulator layar) — 6 skenario hijau
CI / test (push) Successful in 10m50s
CI / test (push) Successful in 10m50s
This commit is contained in:
1 parent
0548de0db4
commit
3f3cc9e8bb
6 files changed
+493
-2
No files matched your search
+206
@@ -0,0 +1,206 @@
|
||||
// Package e2e — pengujian end-to-end binary tia-love yang sebenarnya.
|
||||
//
|
||||
// Binary hasil `go build` dijalankan di dalam pty, output-nya diumpankan ke
|
||||
// emulator layar mini (screen_test.go), lalu skenario mengirim tombol dan
|
||||
// memverifikasi isi LAYAR — bukan byte mentah.
|
||||
//
|
||||
// Jalankan: go test ./e2e/ -v -count=1
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
const (
|
||||
rows = 40
|
||||
cols = 120
|
||||
)
|
||||
|
||||
var binPath string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
tmp, err := os.MkdirTemp("", "tia-e2e-*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
binPath = filepath.Join(tmp, "tia-love")
|
||||
|
||||
build := exec.Command("go", "build", "-o", binPath, "./cmd/tia-love")
|
||||
build.Dir = ".." // root modul (test dijalankan dari e2e/)
|
||||
if out, err := build.CombinedOutput(); err != nil {
|
||||
_ = os.RemoveAll(tmp)
|
||||
panic("build binary e2e gagal: " + err.Error() + "\n" + string(out))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
_ = os.RemoveAll(tmp)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
type tui struct {
|
||||
t *testing.T
|
||||
cmd *exec.Cmd
|
||||
ptmx *os.File
|
||||
mu sync.Mutex
|
||||
scr *screen
|
||||
waited bool
|
||||
}
|
||||
|
||||
func startTUI(t *testing.T) *tui {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("e2e pty tidak didukung di Windows")
|
||||
}
|
||||
cmd := exec.Command(binPath)
|
||||
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: rows, Cols: cols})
|
||||
if err != nil {
|
||||
t.Fatalf("start pty: %v", err)
|
||||
}
|
||||
u := &tui{t: t, cmd: cmd, ptmx: ptmx, scr: newScreen(rows, cols)}
|
||||
go func() {
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := ptmx.Read(buf)
|
||||
if n > 0 {
|
||||
u.mu.Lock()
|
||||
u.scr.write(buf[:n])
|
||||
u.mu.Unlock()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
_ = ptmx.Close()
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
}
|
||||
if !u.waited {
|
||||
_, _ = cmd.Process.Wait()
|
||||
}
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *tui) send(s string) {
|
||||
u.t.Helper()
|
||||
if _, err := u.ptmx.Write([]byte(s)); err != nil {
|
||||
u.t.Fatalf("tulis ke pty: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *tui) screen() string {
|
||||
u.mu.Lock()
|
||||
defer u.mu.Unlock()
|
||||
return u.scr.String()
|
||||
}
|
||||
|
||||
func (u *tui) waitContains(substr string, d time.Duration) {
|
||||
u.t.Helper()
|
||||
deadline := time.Now().Add(d)
|
||||
for time.Now().Before(deadline) {
|
||||
if strings.Contains(u.screen(), substr) {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
u.t.Fatalf("layar tidak memuat %q dalam %v.\n--- snapshot layar ---\n%s", substr, d, u.screen())
|
||||
}
|
||||
|
||||
func (u *tui) waitExit(d time.Duration) int {
|
||||
u.t.Helper()
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- u.cmd.Wait() }()
|
||||
select {
|
||||
case err := <-done:
|
||||
u.waited = true
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return ee.ExitCode()
|
||||
}
|
||||
u.t.Fatalf("wait: %v", err)
|
||||
case <-time.After(d):
|
||||
u.t.Fatalf("proses tidak keluar dalam %v", d)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// --- Skenario ---
|
||||
|
||||
func TestVersionCommand(t *testing.T) {
|
||||
out, err := exec.Command(binPath, "version").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("`tia-love version` gagal: %v (%s)", err, out)
|
||||
}
|
||||
if !strings.Contains(string(out), "tia") {
|
||||
t.Fatalf("output version tak terduga: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownCommandExitCode(t *testing.T) {
|
||||
err := exec.Command(binPath, "perintah-hantu").Run()
|
||||
ee, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("mau ExitError, dapat %v", err)
|
||||
}
|
||||
if ee.ExitCode() != 2 {
|
||||
t.Fatalf("exit code %d, mau 2", ee.ExitCode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIBootAndQuit(t *testing.T) {
|
||||
u := startTUI(t)
|
||||
u.waitContains("tia — Terminal Is A My Way", 5*time.Second)
|
||||
u.waitContains("audio:", 5*time.Second)
|
||||
u.send("q")
|
||||
if code := u.waitExit(5 * time.Second); code != 0 {
|
||||
t.Fatalf("exit code %d, mau 0", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToastAppears(t *testing.T) {
|
||||
u := startTUI(t)
|
||||
u.waitContains("tia — Terminal Is A My Way", 5*time.Second)
|
||||
u.send("t")
|
||||
u.waitContains("SUKSES", 3*time.Second)
|
||||
u.send("q")
|
||||
u.waitExit(5 * time.Second)
|
||||
}
|
||||
|
||||
func TestReducedMotionToggle(t *testing.T) {
|
||||
u := startTUI(t)
|
||||
u.waitContains("tia — Terminal Is A My Way", 5*time.Second)
|
||||
u.send("a") // animasi dimatikan
|
||||
u.waitContains("animasi — fps", 3*time.Second)
|
||||
u.send("a") // nyalakan lagi — tidak boleh crash
|
||||
u.send("q")
|
||||
if code := u.waitExit(5 * time.Second); code != 0 {
|
||||
t.Fatalf("exit code %d, mau 0", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMouseAndNavigateKeys(t *testing.T) {
|
||||
u := startTUI(t)
|
||||
u.waitContains("belum ada klik", 5*time.Second)
|
||||
// klik kiri di tengah layar (SGR 1006: ESC [ < 0 ; x ; y M)
|
||||
u.send("\x1b[<0;60;10M\x1b[<0;60;10m")
|
||||
u.waitContains("klik #1", 3*time.Second)
|
||||
u.send("j")
|
||||
u.send("k")
|
||||
u.send("q")
|
||||
if code := u.waitExit(5 * time.Second); code != 0 {
|
||||
t.Fatalf("exit code %d, mau 0", code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user