2026-04-23 03:06:49 +03:00
|
|
|
//go:build integration
|
|
|
|
|
// +build integration
|
|
|
|
|
|
|
|
|
|
package browser_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-04-30 23:32:39 +03:00
|
|
|
_ "embed"
|
2026-04-23 03:06:49 +03:00
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2026-04-30 23:32:39 +03:00
|
|
|
"strings"
|
2026-04-23 03:06:49 +03:00
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/go-rod/rod"
|
|
|
|
|
"github.com/karust/openserp/core"
|
|
|
|
|
browserprofile "github.com/karust/openserp/core/browser"
|
|
|
|
|
"github.com/karust/openserp/testutil"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-30 23:32:39 +03:00
|
|
|
//go:embed profile_surface_test.js
|
|
|
|
|
var profileSurfaceScript string
|
|
|
|
|
|
2026-04-23 03:06:49 +03:00
|
|
|
func TestProfileCoherence(t *testing.T) {
|
|
|
|
|
testutil.RequireIntegration(t)
|
|
|
|
|
|
|
|
|
|
fixture := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
_, _ = w.Write([]byte(`<!doctype html><html><head><meta charset="utf-8"><title>coherence</title></head><body>ok</body></html>`))
|
|
|
|
|
}))
|
|
|
|
|
defer fixture.Close()
|
|
|
|
|
|
|
|
|
|
browser, err := core.NewBrowser(core.BrowserOpts{
|
|
|
|
|
IsHeadless: true,
|
|
|
|
|
IsLeakless: false,
|
|
|
|
|
Timeout: 20 * time.Second,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("create browser: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
if closeErr := browser.Close(); closeErr != nil {
|
|
|
|
|
t.Fatalf("close browser: %v", closeErr)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
engine string
|
|
|
|
|
region string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "windows lane",
|
|
|
|
|
engine: "google",
|
|
|
|
|
region: "ru",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "mac lane",
|
|
|
|
|
engine: "bing",
|
|
|
|
|
region: "en-US",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
ctx := core.WithEngine(context.Background(), tc.engine)
|
|
|
|
|
ctx = core.WithProfileRegion(ctx, tc.region)
|
2026-04-30 23:32:39 +03:00
|
|
|
ctx = core.WithBrowserProfileUsage(ctx)
|
2026-04-23 03:06:49 +03:00
|
|
|
|
|
|
|
|
page, err := browser.Navigate(ctx, fixture.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("navigate fixture: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
if closeErr := core.ClosePageWithTimeout(context.Background(), page, time.Second); closeErr != nil {
|
|
|
|
|
t.Fatalf("close page: %v", closeErr)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
got, err := browserProfileSurface(page)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("collect profile surfaces: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 23:32:39 +03:00
|
|
|
expected := selectedProfileFromContext(t, ctx)
|
|
|
|
|
expected.UserAgent = expectedUserAgentForRuntime(expected.UserAgent, got.UserAgent)
|
2026-04-23 03:06:49 +03:00
|
|
|
if got.UserAgent != expected.UserAgent {
|
|
|
|
|
t.Fatalf("navigator.userAgent mismatch:\nexpected: %s\nactual: %s", expected.UserAgent, got.UserAgent)
|
|
|
|
|
}
|
|
|
|
|
if got.Platform != expected.Platform {
|
|
|
|
|
t.Fatalf("navigator.userAgentData.platform mismatch: expected %q got %q", expected.Platform, got.Platform)
|
|
|
|
|
}
|
|
|
|
|
if got.NavigatorPlatform != expectedNavigatorPlatform(expected.Platform) {
|
|
|
|
|
t.Fatalf("navigator.platform mismatch: expected %q got %q", expectedNavigatorPlatform(expected.Platform), got.NavigatorPlatform)
|
|
|
|
|
}
|
|
|
|
|
if got.Locale != expected.Locale {
|
|
|
|
|
t.Fatalf("Intl locale mismatch: expected %q got %q", expected.Locale, got.Locale)
|
|
|
|
|
}
|
|
|
|
|
if got.Timezone != expected.Timezone {
|
|
|
|
|
t.Fatalf("Intl timezone mismatch: expected %q got %q", expected.Timezone, got.Timezone)
|
|
|
|
|
}
|
|
|
|
|
if len(got.NavigatorLanguages) == 0 {
|
|
|
|
|
t.Fatal("navigator.languages is empty")
|
|
|
|
|
}
|
|
|
|
|
if got.NavigatorLanguages[0] != expected.NavigatorLangs[0] {
|
|
|
|
|
t.Fatalf("navigator.languages[0] mismatch: expected %q got %q", expected.NavigatorLangs[0], got.NavigatorLanguages[0])
|
|
|
|
|
}
|
|
|
|
|
if got.WebdriverType != "undefined" {
|
|
|
|
|
t.Fatalf("navigator.webdriver expected undefined, got %q", got.WebdriverType)
|
|
|
|
|
}
|
|
|
|
|
if got.WebdriverOwnPropPresent {
|
|
|
|
|
t.Fatal("navigator own property 'webdriver' should not be present")
|
|
|
|
|
}
|
|
|
|
|
if got.WorkerUserAgent != got.UserAgent {
|
|
|
|
|
t.Fatalf("worker userAgent mismatch: main %q worker %q", got.UserAgent, got.WorkerUserAgent)
|
|
|
|
|
}
|
|
|
|
|
if got.WorkerPlatform != got.NavigatorPlatform {
|
|
|
|
|
t.Fatalf("worker platform mismatch: main %q worker %q", got.NavigatorPlatform, got.WorkerPlatform)
|
|
|
|
|
}
|
|
|
|
|
if len(got.WorkerNavigatorLangs) == 0 {
|
|
|
|
|
t.Fatal("worker navigator.languages is empty")
|
|
|
|
|
}
|
|
|
|
|
if got.WorkerNavigatorLangs[0] != got.NavigatorLanguages[0] {
|
|
|
|
|
t.Fatalf("worker navigator.languages[0] mismatch: main %q worker %q", got.NavigatorLanguages[0], got.WorkerNavigatorLangs[0])
|
|
|
|
|
}
|
|
|
|
|
if got.WorkerTimezone != got.Timezone {
|
|
|
|
|
t.Fatalf("worker timezone mismatch: main %q worker %q", got.Timezone, got.WorkerTimezone)
|
|
|
|
|
}
|
2026-04-30 23:32:39 +03:00
|
|
|
if got.WorkerWebGLVendor != expected.WebGLVendor {
|
|
|
|
|
t.Fatalf("worker WebGL vendor mismatch: expected %q got %q", expected.WebGLVendor, got.WorkerWebGLVendor)
|
|
|
|
|
}
|
|
|
|
|
if got.WorkerWebGLRenderer != expected.WebGLRenderer {
|
|
|
|
|
t.Fatalf("worker WebGL renderer mismatch: expected %q got %q", expected.WebGLRenderer, got.WorkerWebGLRenderer)
|
|
|
|
|
}
|
|
|
|
|
if got.InnerHeight >= got.OuterHeight {
|
|
|
|
|
t.Fatalf("innerHeight should be smaller than outerHeight, got inner=%d outer=%d", got.InnerHeight, got.OuterHeight)
|
|
|
|
|
}
|
|
|
|
|
if got.OuterHeight > got.ScreenAvailHeight {
|
|
|
|
|
t.Fatalf("outerHeight should fit in screen.availHeight, got outer=%d avail=%d", got.OuterHeight, got.ScreenAvailHeight)
|
|
|
|
|
}
|
|
|
|
|
if got.ScreenAvailHeight >= got.ScreenHeight {
|
|
|
|
|
t.Fatalf("screen.availHeight should be smaller than screen.height, got avail=%d screen=%d", got.ScreenAvailHeight, got.ScreenHeight)
|
|
|
|
|
}
|
2026-04-23 03:06:49 +03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type profileSurface struct {
|
2026-04-30 23:32:39 +03:00
|
|
|
UserAgent string `json:"userAgent"`
|
|
|
|
|
Platform string `json:"platform"`
|
|
|
|
|
NavigatorPlatform string `json:"navigatorPlatform"`
|
|
|
|
|
NavigatorLanguages []string `json:"navigatorLanguages"`
|
|
|
|
|
Timezone string `json:"timezone"`
|
|
|
|
|
Locale string `json:"locale"`
|
|
|
|
|
WebdriverType string `json:"webdriverType"`
|
|
|
|
|
WebdriverOwnPropPresent bool `json:"webdriverOwnPropPresent"`
|
|
|
|
|
WorkerUserAgent string `json:"workerUserAgent"`
|
|
|
|
|
WorkerPlatform string `json:"workerPlatform"`
|
|
|
|
|
WorkerNavigatorLangs []string `json:"workerNavigatorLangs"`
|
|
|
|
|
WorkerTimezone string `json:"workerTimezone"`
|
|
|
|
|
WorkerWebGLVendor string `json:"workerWebGLVendor"`
|
|
|
|
|
WorkerWebGLRenderer string `json:"workerWebGLRenderer"`
|
|
|
|
|
InnerHeight int `json:"innerHeight"`
|
|
|
|
|
OuterHeight int `json:"outerHeight"`
|
|
|
|
|
ScreenHeight int `json:"screenHeight"`
|
|
|
|
|
ScreenAvailHeight int `json:"screenAvailHeight"`
|
2026-04-23 03:06:49 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func browserProfileSurface(page *rod.Page) (profileSurface, error) {
|
2026-04-30 23:32:39 +03:00
|
|
|
result, err := page.Eval(profileSurfaceScript)
|
2026-04-23 03:06:49 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return profileSurface{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var out profileSurface
|
|
|
|
|
if err := result.Value.Unmarshal(&out); err != nil {
|
|
|
|
|
return profileSurface{}, fmt.Errorf("decode eval payload: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func expectedNavigatorPlatform(platform string) string {
|
|
|
|
|
switch platform {
|
|
|
|
|
case "Windows":
|
|
|
|
|
return "Win32"
|
|
|
|
|
case "macOS":
|
|
|
|
|
return "MacIntel"
|
|
|
|
|
default:
|
|
|
|
|
return "Linux x86_64"
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-30 23:32:39 +03:00
|
|
|
|
|
|
|
|
func selectedProfileFromContext(t *testing.T, ctx context.Context) browserprofile.Profile {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
ids := core.BrowserProfileIDsFromContext(ctx)
|
|
|
|
|
if len(ids) == 0 {
|
|
|
|
|
t.Fatal("expected selected browser profile id")
|
|
|
|
|
}
|
|
|
|
|
profile, ok := browserprofile.ProfileByID(ids[0])
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("selected browser profile %q not found", ids[0])
|
|
|
|
|
}
|
|
|
|
|
return profile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func expectedUserAgentForRuntime(profileUserAgent, runtimeUserAgent string) string {
|
|
|
|
|
runtimeChrome := chromeToken(runtimeUserAgent)
|
|
|
|
|
if runtimeChrome == "" {
|
|
|
|
|
return profileUserAgent
|
|
|
|
|
}
|
|
|
|
|
profileChrome := chromeToken(profileUserAgent)
|
|
|
|
|
if profileChrome == "" {
|
|
|
|
|
return profileUserAgent
|
|
|
|
|
}
|
|
|
|
|
return strings.Replace(profileUserAgent, profileChrome, runtimeChrome, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func chromeToken(userAgent string) string {
|
|
|
|
|
const prefix = "Chrome/"
|
|
|
|
|
start := strings.Index(userAgent, prefix)
|
|
|
|
|
if start < 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
end := strings.IndexByte(userAgent[start:], ' ')
|
|
|
|
|
if end < 0 {
|
|
|
|
|
return userAgent[start:]
|
|
|
|
|
}
|
|
|
|
|
return userAgent[start : start+end]
|
|
|
|
|
}
|