1package mcp23import (4 "encoding/json"5 "errors"6 "fmt"7 "os"8 "os/exec"9 "path/filepath"10 "strings"11 "testing"1213 sdk "github.com/modelcontextprotocol/go-sdk/mcp"1415 "src.rest/cat/012/internal/sheet"16)1718// TestViewInBrowser draws the view as the OpenAI Apps SDK's hosts and19// MCP Apps hosts do, in Chromium through Playwright (testdata/view.mjs),20// and checks the grid, the pointer and formula bar, scrolling, the21// theme and the frame's height. It needs Node and the playwright22// package, found through PLAYWRIGHT_DIR (a folder whose node_modules23// holds it) or NODE_PATH, and is skipped without them.24// VIEW_SHOTS names a folder for screenshots of each case.25func TestViewInBrowser(t *testing.T) {26 if testing.Short() {27 t.Skip("-short")28 }29 nodePath := playwrightPath(t)30 w := sheet.NewBook()31 s := w.Sheet(0)32 s.Set(sheet.Addr{}, "Month")33 s.Set(sheet.Addr{Col: 1}, "Revenue")34 s.Set(sheet.Addr{Col: 2}, "Cost")35 s.Set(sheet.Addr{Col: 3}, "Profit")36 for r := 1; r <= 60; r++ {37 s.Set(sheet.Addr{Row: r}, fmt.Sprintf("Week %d", r))38 s.Set(sheet.Addr{Col: 1, Row: r}, fmt.Sprintf("$%d,%03d", 10+r%7, r*37%1000))39 s.Set(sheet.Addr{Col: 2, Row: r}, fmt.Sprintf("$%d,%03d", 6+r%3, r*53%1000))40 s.Set(sheet.Addr{Col: 3, Row: r}, fmt.Sprintf("=B%d-C%d", r+1, r+1))41 }42 path := filepath.Join(t.TempDir(), "weeks.012")43 writeBook(t, w, path)44 cs := connect(t, Options{Default: path})45 result := func(name string, args map[string]any) *sdk.CallToolResult {46 return call(t, cs, name, args, nil)47 }48 gridArgs := map[string]any{"ref": "A1:D61"}49 chartArgs := map[string]any{"data": "A1:C9", "type": "line", "title": "Revenue and cost"}50 cases := map[string]any{51 "page": viewPage(),52 "cases": map[string]any{53 "grid": map[string]any{"args": gridArgs, "result": result("read_range", gridArgs), "grid": map[string]string{"a1": "Month", "cell": "D2", "input": "=B2-C2", "below": "D3"}},54 "chart": map[string]any{"args": chartArgs, "result": result("create_chart", chartArgs)},55 },56 }57 data, err := json.Marshal(cases)58 if err != nil {59 t.Fatal(err)60 }61 file := filepath.Join(t.TempDir(), "cases.json")62 if err := os.WriteFile(file, data, 0o644); err != nil {63 t.Fatal(err)64 }65 args := []string{filepath.Join("testdata", "view.mjs"), file}66 if shots := os.Getenv("VIEW_SHOTS"); shots != "" {67 args = append(args, shots)68 }69 cmd := exec.Command("node", args...)70 cmd.Env = append(os.Environ(), "NODE_PATH="+nodePath)71 out, err := cmd.CombinedOutput()72 var exit *exec.ExitError73 switch {74 case errors.As(err, &exit) && exit.ExitCode() == 3:75 t.Skipf("no browser for Playwright: %s", out)76 case err != nil:77 t.Fatalf("%v\n%s", err, out)78 }79}8081// playwrightPath is the NODE_PATH that finds the playwright package,82// skipping the test when there's none.83func playwrightPath(t *testing.T) string {84 t.Helper()85 if _, err := exec.LookPath("node"); err != nil {86 t.Skip("no node")87 }88 var dirs []string89 if d := os.Getenv("PLAYWRIGHT_DIR"); d != "" {90 dirs = append(dirs, filepath.Join(d, "node_modules"), d)91 }92 if d := os.Getenv("NODE_PATH"); d != "" {93 dirs = append(dirs, d)94 }95 nodePath := strings.Join(dirs, string(os.PathListSeparator))96 cmd := exec.Command("node", "-e", "require('playwright')")97 cmd.Env = append(os.Environ(), "NODE_PATH="+nodePath)98 if err := cmd.Run(); err != nil {99 t.Skip("no playwright package: set PLAYWRIGHT_DIR to a folder whose node_modules holds it")100 }101 return nodePath102}