diff --git a/README.md b/README.md index 07c12bf..d110af9 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,7 @@ This pattern is useful in CI — run Rodney as a post-deploy check, an accessibi |---|---|---| | `RODNEY_HOME` | `~/.rodney` | Data directory for state and Chrome profile | | `ROD_CHROME_BIN` | `/usr/bin/google-chrome` | Path to Chrome/Chromium binary | +| `ROD_CHROME_ARGS` | (none) | Extra Chrome flags as space-separated args (for example `disable-features=HttpsUpgrades`) | | `ROD_TIMEOUT` | `30` | Default timeout in seconds for element queries | | `HTTPS_PROXY` / `HTTP_PROXY` | (none) | Authenticated proxy auto-detected on start | diff --git a/help.txt b/help.txt index 26d40b4..8cadde6 100644 --- a/help.txt +++ b/help.txt @@ -71,6 +71,9 @@ current directory, the local session is used; otherwise the global session. Use "rodney start --local" to create a directory-scoped session. Environment: + ROD_CHROME_BIN Path to Chrome/Chromium binary + ROD_CHROME_ARGS Extra Chrome flags (space-separated) + ROD_TIMEOUT Default element query timeout in seconds RODNEY_HOME Override data directory (default: ~/.rodney) Exit codes: diff --git a/main.go b/main.go index f2ba222..92706ff 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/launcher/flags" "github.com/go-rod/rod/lib/proto" ) @@ -78,12 +79,12 @@ func resolveStateDir(mode scopeMode, workingDir string) string { // State persisted between CLI invocations type State struct { - DebugURL string `json:"debug_url"` - ChromePID int `json:"chrome_pid"` - ActivePage int `json:"active_page"` // index into pages list - DataDir string `json:"data_dir"` - ProxyPID int `json:"proxy_pid,omitempty"` // PID of auth proxy helper - ProxyPort int `json:"proxy_port,omitempty"` // local port of auth proxy + DebugURL string `json:"debug_url"` + ChromePID int `json:"chrome_pid"` + ActivePage int `json:"active_page"` // index into pages list + DataDir string `json:"data_dir"` + ProxyPID int `json:"proxy_pid,omitempty"` // PID of auth proxy helper + ProxyPort int `json:"proxy_port,omitempty"` // local port of auth proxy } func stateDir() string { @@ -294,6 +295,55 @@ func init() { } } +type chromeArg struct { + name string + value string + hasValue bool +} + +func parseChromeArgs(raw string) ([]chromeArg, error) { + fields := strings.Fields(raw) + parsed := make([]chromeArg, 0, len(fields)) + + for _, field := range fields { + arg := strings.TrimLeft(field, "-") + if arg == "" { + return nil, fmt.Errorf("invalid flag %q", field) + } + + parts := strings.SplitN(arg, "=", 2) + if parts[0] == "" { + return nil, fmt.Errorf("missing flag name in %q", field) + } + + entry := chromeArg{name: parts[0]} + if len(parts) == 2 { + entry.value = parts[1] + entry.hasValue = true + } + parsed = append(parsed, entry) + } + + return parsed, nil +} + +func applyChromeArgs(l *launcher.Launcher, raw string) error { + args, err := parseChromeArgs(raw) + if err != nil { + return err + } + + for _, arg := range args { + if arg.hasValue { + l.Set(flags.Flag(arg.name), arg.value) + continue + } + l.Set(flags.Flag(arg.name)) + } + + return nil +} + // withPage loads state, connects, and returns the active page. // Caller should NOT close the browser (we just disconnect). func withPage() (*State, *rod.Browser, *rod.Page) { @@ -366,6 +416,12 @@ func cmdStart(args []string) { l = l.Bin(bin) } + if chromeArgs := os.Getenv("ROD_CHROME_ARGS"); chromeArgs != "" { + if err := applyChromeArgs(l, chromeArgs); err != nil { + fatal("invalid ROD_CHROME_ARGS: %v", err) + } + } + // Detect authenticated proxy and launch helper if needed var proxyPID, proxyPort int if server, user, pass, needed := detectProxy(); needed { @@ -1515,7 +1571,7 @@ func queryAXNodes(page *rod.Page, name, role string) ([]*proto.AccessibilityAXNo } result, err := proto.AccessibilityQueryAXTree{ - BackendNodeID: doc.Root.BackendNodeID, + BackendNodeID: doc.Root.BackendNodeID, AccessibleName: name, Role: role, }.Call(page) diff --git a/main_test.go b/main_test.go index 1c9d848..466f0f3 100644 --- a/main_test.go +++ b/main_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "reflect" "strings" "testing" "time" @@ -160,6 +161,39 @@ func navigateTo(t *testing.T, path string) *rod.Page { return page } +func TestParseChromeArgs_ParsesFlagsAndValues(t *testing.T) { + args, err := parseChromeArgs("disable-features=HttpsUpgrades --ignore-certificate-errors") + if err != nil { + t.Fatalf("parseChromeArgs returned error: %v", err) + } + + want := []chromeArg{ + {name: "disable-features", value: "HttpsUpgrades", hasValue: true}, + {name: "ignore-certificate-errors", hasValue: false}, + } + + if !reflect.DeepEqual(args, want) { + t.Fatalf("unexpected parse result\nwant: %#v\ngot: %#v", want, args) + } +} + +func TestParseChromeArgs_HandlesEmptyInput(t *testing.T) { + args, err := parseChromeArgs("") + if err != nil { + t.Fatalf("parseChromeArgs returned error: %v", err) + } + if len(args) != 0 { + t.Fatalf("expected empty args, got: %#v", args) + } +} + +func TestParseChromeArgs_RejectsInvalidFlag(t *testing.T) { + _, err := parseChromeArgs("--") + if err == nil { + t.Fatal("expected parseChromeArgs to fail for invalid flag") + } +} + // ===================== // ax-tree tests (RED) // =====================