From 1d00b0c2eff3f6cb9c1bcf7cfe4e8436d9b8fb4a Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:03:08 +0000 Subject: [PATCH 1/2] Install regional locale data --- images/chromium-headful/Dockerfile | 4 ++++ images/chromium-headless/image/Dockerfile | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 452e0db5..604e0dea 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -200,7 +200,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap xdg-utils \ libvulkan1 \ fontconfig \ + locales \ + tzdata \ unzip && \ + locale-gen en_US.UTF-8 en_GB.UTF-8 en_CA.UTF-8 en_IE.UTF-8 en_SG.UTF-8 && \ apt-get clean && rm -rf /var/lib/apt/lists/* # Install fonts to match a realistic Ubuntu 22.04 desktop fingerprint. @@ -296,6 +299,7 @@ RUN set -eux; \ unzip -q /tmp/cd.zip -d /tmp; \ mv /opt/chrome-linux64 /opt/chrome-for-testing; \ mv /opt/chrome-for-testing/chrome /opt/chrome-for-testing/chromium; \ + for locale in en-CA en-IE en-SG; do cp /opt/chrome-for-testing/locales/en-GB.pak "/opt/chrome-for-testing/locales/${locale}.pak"; done; \ ln -sf /opt/chrome-for-testing/chromium /usr/bin/chromium; \ mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/chromedriver; \ chmod +x /usr/local/bin/chromedriver /opt/chrome-for-testing/chromium; \ diff --git a/images/chromium-headless/image/Dockerfile b/images/chromium-headless/image/Dockerfile index 920c988b..7f434eda 100644 --- a/images/chromium-headless/image/Dockerfile +++ b/images/chromium-headless/image/Dockerfile @@ -156,11 +156,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap xclip \ xdotool \ fontconfig \ + locales \ + tzdata \ fonts-noto-cjk \ fonts-noto-color-emoji \ fonts-noto-mono \ fonts-nanum \ supervisor; \ + locale-gen en_US.UTF-8 en_GB.UTF-8 en_CA.UTF-8 en_IE.UTF-8 en_SG.UTF-8; \ fc-cache -f # sqlite3 for debugging the cookies file; unzip for the chrome-for-testing archives below. @@ -178,6 +181,7 @@ RUN set -eux; \ unzip -q /tmp/cd.zip -d /tmp; \ mv /opt/chrome-linux64 /opt/chrome-for-testing; \ mv /opt/chrome-for-testing/chrome /opt/chrome-for-testing/chromium; \ + for locale in en-CA en-IE en-SG; do cp /opt/chrome-for-testing/locales/en-GB.pak "/opt/chrome-for-testing/locales/${locale}.pak"; done; \ ln -sf /opt/chrome-for-testing/chromium /usr/bin/chromium; \ mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/chromedriver; \ chmod +x /usr/local/bin/chromedriver /opt/chrome-for-testing/chromium; \ From 6486e5b62dea16d8d6d1354535f4bf1dde55cd7a Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:08:16 +0000 Subject: [PATCH 2/2] Test regional browser location settings --- images/chromium-headful/Dockerfile | 1 + images/chromium-headless/image/Dockerfile | 1 + server/e2e/e2e_browser_location_test.go | 125 ++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 server/e2e/e2e_browser_location_test.go diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 604e0dea..58dc89d3 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -291,6 +291,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap libxext6 libxfixes3 libxkbcommon0; # Install Chrome + ChromeDriver as a matched pair from chrome-for-testing so they cannot drift apart. +# Regional pack filenames preserve each locale identifier while reusing en-GB UI strings. ARG CHROME_VERSION=152.0.7977.42 RUN set -eux; \ curl -fsSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome.zip; \ diff --git a/images/chromium-headless/image/Dockerfile b/images/chromium-headless/image/Dockerfile index 7f434eda..ceb5012c 100644 --- a/images/chromium-headless/image/Dockerfile +++ b/images/chromium-headless/image/Dockerfile @@ -173,6 +173,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap apt-get --no-install-recommends -y install sqlite3 unzip; # Install Chrome + ChromeDriver as a matched pair from chrome-for-testing so they cannot drift apart. +# Regional pack filenames preserve each locale identifier while reusing en-GB UI strings. ARG CHROME_VERSION=152.0.7977.42 RUN set -eux; \ curl -fsSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome.zip; \ diff --git a/server/e2e/e2e_browser_location_test.go b/server/e2e/e2e_browser_location_test.go new file mode 100644 index 00000000..a0a0044a --- /dev/null +++ b/server/e2e/e2e_browser_location_test.go @@ -0,0 +1,125 @@ +package e2e + +import ( + "context" + "encoding/json" + "fmt" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type browserLocationProbe struct { + Language string `json:"language"` + Locale string `json:"locale"` + TimeZone string `json:"timeZone"` +} + +func TestRegionalBrowserLocation(t *testing.T) { + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + for _, test := range []struct { + name string + image string + }{ + {name: "headful", image: headfulImage}, + {name: "headless", image: headlessImage}, + } { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + c := NewTestContainer(t, test.image) + require.NoError(t, c.Start(ctx, ContainerConfig{Env: map[string]string{ + "TZ": "Asia/Singapore", + "LANG": "en_SG.UTF-8", + "LC_ALL": "en_SG.UTF-8", + "CHROMIUM_FLAGS": "--lang=en-SG --accept-lang=en-SG,en --remote-allow-origins=*", + }}), "failed to start container") + defer c.Stop(ctx) + + require.NoError(t, c.WaitReady(ctx), "api not ready") + require.NoError(t, c.WaitDevTools(ctx), "devtools not ready") + + osLocation, err := execCombinedOutput(ctx, c, "sh", []string{"-c", `printf '%s|%s' "$(locale charmap)" "$(date +%z)"`}) + require.NoError(t, err, "failed to inspect OS locale and timezone") + require.Equal(t, "UTF-8|+0800", osLocation) + + browserLocation, err := evaluateBrowserLocation(ctx, c.CDPURL()) + require.NoError(t, err) + require.Equal(t, browserLocationProbe{ + Language: "en-SG", + Locale: "en-SG", + TimeZone: "Asia/Singapore", + }, browserLocation) + }) + } +} + +func evaluateBrowserLocation(ctx context.Context, wsURL string) (browserLocationProbe, error) { + client, err := newCDPClient(ctx, wsURL) + if err != nil { + return browserLocationProbe{}, err + } + defer client.Close() + + targetRaw, err := client.Call(ctx, "Target.createTarget", map[string]any{"url": "about:blank"}, "") + if err != nil { + return browserLocationProbe{}, fmt.Errorf("Target.createTarget: %w", err) + } + targetID, err := decodeJSONStringField(targetRaw, "targetId") + if err != nil { + return browserLocationProbe{}, err + } + defer func() { + _, _ = client.Call(ctx, "Target.closeTarget", map[string]any{"targetId": targetID}, "") + }() + + attachRaw, err := client.Call(ctx, "Target.attachToTarget", map[string]any{ + "targetId": targetID, + "flatten": true, + }, "") + if err != nil { + return browserLocationProbe{}, fmt.Errorf("Target.attachToTarget: %w", err) + } + sessionID, err := decodeJSONStringField(attachRaw, "sessionId") + if err != nil { + return browserLocationProbe{}, err + } + + const expression = `JSON.stringify({ + language: navigator.language, + locale: Intl.DateTimeFormat().resolvedOptions().locale, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone +})` + evalRaw, err := client.Call(ctx, "Runtime.evaluate", map[string]any{ + "expression": expression, + "returnByValue": true, + }, sessionID) + if err != nil { + return browserLocationProbe{}, fmt.Errorf("Runtime.evaluate: %w", err) + } + + var evalEnvelope struct { + Result struct { + Value string `json:"value"` + } `json:"result"` + ExceptionDetails json.RawMessage `json:"exceptionDetails"` + } + if err := json.Unmarshal(evalRaw, &evalEnvelope); err != nil { + return browserLocationProbe{}, fmt.Errorf("decode Runtime.evaluate result: %w", err) + } + if len(evalEnvelope.ExceptionDetails) > 0 { + return browserLocationProbe{}, fmt.Errorf("browser location probe raised an exception: %s", evalEnvelope.ExceptionDetails) + } + + var result browserLocationProbe + if err := json.Unmarshal([]byte(evalEnvelope.Result.Value), &result); err != nil { + return browserLocationProbe{}, fmt.Errorf("decode browser location probe %q: %w", evalEnvelope.Result.Value, err) + } + return result, nil +}