package web import ( "bufio" "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "runtime" "strings" "sync" "testing" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/installer" "gitgud.io/mike/mpv-manager/pkg/platform" "github.com/stretchr/testify/require" ) // TestBrowserFixture is a development-only stdin-controlled server for real // browser tests. It has no fixture HTTP endpoints and cannot ship in the app. func TestBrowserFixture(t *testing.T) { if os.Getenv("MPV_MANAGER_BROWSER_FIXTURE") != "1" { t.Skip("started by npm run test:browser") } root := t.TempDir() for _, key := range []string{"HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "XDG_CONFIG_HOME"} { t.Setenv(key, root) } cfg, err := config.Reload() require.NoError(t, err) p := &platform.Platform{OSType: platform.OSType(runtime.GOOS), Arch: runtime.GOARCH, GPUInfo: &platform.GPUInfo{}} inst := installer.NewInstaller(installer.ReleaseInfo{}, root) server := NewServer(p, cfg, inst, nil) var refreshMu sync.Mutex refreshVersion := "9.9.9" refreshFails := false originalFetch := fetchReleaseInfo fetchReleaseInfo = func(ctx context.Context) (*installer.ReleaseInfo, error) { refreshMu.Lock() defer refreshMu.Unlock() if err := ctx.Err(); err != nil { return nil, err } if refreshFails { return nil, fmt.Errorf("fixture signature rejected") } return &installer.ReleaseInfo{Version: refreshVersion}, nil } t.Cleanup(func() { fetchReleaseInfo = originalFetch }) address, serving, err := server.Start("127.0.0.1:0") require.NoError(t, err) defer func() { require.NoError(t, server.Shutdown()); require.NoError(t, <-serving) }() respond := func(data any) { encoded, err := json.Marshal(data) require.NoError(t, err) fmt.Printf("BROWSER_FIXTURE %s\n", encoded) } respond(map[string]any{"url": "http://" + address}) type command struct { ID int `json:"id"` Version string `json:"version"` Fail bool `json:"fail"` Action string `json:"action"` JobID string `json:"jobId"` Lines int `json:"lines"` Content string `json:"content"` JobType string `json:"jobType"` AppID string `json:"appId"` } workers := map[string]chan string{} scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { var cmd command require.NoError(t, json.Unmarshal(scanner.Bytes(), &cmd)) reply := map[string]any{"id": cmd.ID} switch cmd.Action { case "windows-app-cards": var html bytes.Buffer windows := &platform.Platform{OSType: platform.Windows, Arch: "amd64"} for _, id := range []string{"default-mpv", "custom-mpv"} { app := config.InstalledApp{ID: id, AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: `C:\QA\` + id, Managed: true} group := AppGroupInfo{App: app, AppName: "MPV", InstallMethod: app.InstallMethod, Managed: true} require.NoError(t, server.templates.ExecuteTemplate(&html, "app-card", createAppCardData(group, windows))) } reply["html"] = html.String() case "release": refreshMu.Lock() refreshVersion, refreshFails = cmd.Version, cmd.Fail refreshMu.Unlock() case "config": path := config.GetConfigPath() require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) require.NoError(t, os.WriteFile(path, []byte(cmd.Content), 0600)) case "read-config": content, err := os.ReadFile(config.GetConfigPath()) require.NoError(t, err) reply["content"] = string(content) case "detected-app": app := config.InstalledApp{ID: "browser-adoption-fixture", AppName: "Adoption fixture", InstallMethod: "fixture", InstallPath: root, Managed: false, AppType: "app"} require.NoError(t, config.AddInstalledApp(app)) for _, saved := range config.GetInstalledApps() { if saved.AppName == app.AppName && saved.InstallPath == app.InstallPath { reply["appId"] = saved.ID } } require.NotEmpty(t, reply["appId"]) case "create": jobType := cmd.JobType if jobType == "" { jobType = "install" } appID := cmd.AppID job, ok := server.jobManager.CreateJobWithResources(jobType, "fixture", "Browser fixture", resourceInstallerMutation) require.True(t, ok) result := make(chan string, 1) workers[job.ID] = result require.True(t, server.jobManager.StartWorker(job.ID, func() { select { case action := <-result: server.jobManager.AddOutput(job.ID, "worker terminal output") if action == "fail" { server.jobManager.SetError(job.ID, fmt.Errorf("fixture failed"), "Expected browser QA failure") } else { if jobType == "adopt" { require.NoError(t, config.UpdateInstalledAppManagedByID(appID, true, "none")) } server.jobManager.CompleteJob(job.ID) } case <-job.Context().Done(): server.jobManager.AddOutput(job.ID, "cancellation cleanup complete") server.jobManager.SetError(job.ID, job.Context().Err(), "Fixture worker joined") } })) server.jobManager.UpdateProgress(job.ID, 25, "Staging fixture") reply["jobId"] = job.ID case "clients": server.jobManager.clientsMu.RLock() reply["clients"] = len(server.jobManager.clients) server.jobManager.clientsMu.RUnlock() case "modernz": opts := filepath.Join(config.GetMPVConfigPath(), "script-opts") require.NoError(t, os.MkdirAll(opts, 0700)) require.NoError(t, os.WriteFile(filepath.Join(opts, "modernz.conf"), []byte("vidscale=auto\n"), 0600)) require.NoError(t, config.Update(func(c *config.Config) error { c.UIConfigMigrations = nil; return nil })) scripts := filepath.Join(config.GetMPVConfigPath(), "scripts") require.NoError(t, os.MkdirAll(scripts, 0755)) require.NoError(t, os.WriteFile(filepath.Join(scripts, "modernz.lua"), []byte("-- QA ModernZ presence fixture"), 0600)) case "output": require.GreaterOrEqual(t, cmd.Lines, 0) require.LessOrEqual(t, cmd.Lines, 10000) for index := 0; index < cmd.Lines; index++ { server.jobManager.AddOutput(cmd.JobID, fmt.Sprintf("line %05d %s", index, strings.Repeat("x", 1024))) } server.jobManager.AddOutput(cmd.JobID, "LATEST OUTPUT SENTINEL") case "complete", "fail": result, ok := workers[cmd.JobID] require.True(t, ok) result <- cmd.Action delete(workers, cmd.JobID) case "cancel": require.NoError(t, server.jobManager.CancelJob(cmd.JobID)) case "stop": respond(reply) return default: t.Fatalf("unknown fixture action %q", cmd.Action) } respond(reply) } require.NoError(t, scanner.Err()) }