Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions images/chromium-headful/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -288,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; \
Expand All @@ -296,6 +300,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; \
Expand Down
5 changes: 5 additions & 0 deletions images/chromium-headless/image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -170,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; \
Expand All @@ -178,6 +182,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; \
Expand Down
125 changes: 125 additions & 0 deletions server/e2e/e2e_browser_location_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading