[GH-ISSUE #3395] [Bug] Connected browser scenarios ignore the requested navigator language #1263

Closed
opened 2026-10-16 21:03:31 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3395

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

capture-browser-scenario in connect mode applies the requested formatting locale but leaves navigator.language at the external browser's original language. With environment.locale: "fr-FR", the local page captured navigator.language === "en-US" and an Intl formatting locale of fr-FR. The same fixture and locale in launch mode captured fr-FR for both. Applications that select their language through navigator.language therefore run in a different language from the scenario's declared deterministic environment.

Connect mode accepted the request and completed the action. It reported disconnected-external, and the independent caller confirmed that the external page and browser remained alive afterward. Launch mode reported terminated-owned-process. The attached capture's ordinary pre-attach event gap was explicit; no limitation identified the ignored page-language setting.

Reproduction

  1. Prepare a clean source checkout at the reproduced baseline and install its locked dependencies and compiled runtime:
git clone https://github.com/morluto/rea.git rea-locale-repro
cd rea-locale-repro
git checkout --detach 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. Use an already-installed Chrome-family executable and set CHROME to its absolute path. For the standard Google Chrome installation on macOS:
export CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
  1. Run the following from the checkout root. It creates every input file inline, starts a local fixture and an isolated caller-owned browser, finds the exact page target, calls the public capture CLI in connect mode, closes that browser, and repeats through launch mode as a control. The caller-owned browser's initial language must differ from fr-FR; the observed initial language was en-US.
: "${CHROME:?Set CHROME to an installed Chrome-family executable}"
case_dir="$(mktemp -d)"
mkdir -p "$case_dir/home" "$case_dir/temp"
cat > "$case_dir/repro.cjs" <<'JS'
const { chromium } = require(require('node:path').join(process.cwd(), 'node_modules/playwright-core'));
const { createServer } = require('node:http');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
const { readFile, writeFile } = require('node:fs/promises');
const { join } = require('node:path');
const run = promisify(execFile);
const html = '<!doctype html><title>Locale fixture</title><body><script>' +
  'document.body.dataset.navigatorLocale = navigator.language;' +
  'document.body.dataset.intlLocale = new Intl.DateTimeFormat().resolvedOptions().locale;' +
  '</script></body>';
async function capture(name, scenario) {
  const input = join(__dirname, `${name}.json`);
  await writeFile(input, JSON.stringify(scenario));
  const result = await run(process.execPath,
    ['scripts/rea.mjs', 'capture-browser-scenario', input, '--json'],
    { cwd: process.cwd(), env: process.env, timeout: 120000, maxBuffer: 1048576 });
  const value = JSON.parse(result.stdout).normalized_result;
  console.log(name, value.steps.at(-1).artifacts.dom.value.text);
  console.log(name, 'cleanup:', value.browser.cleanup);
}
(async () => {
  const server = createServer((request, response) => {
    response.setHeader('Content-Type', 'text/html');
    response.end(html);
  });
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  let context;
  try {
    const url = `http://127.0.0.1:${server.address().port}/`;
    const profile = join(__dirname, 'profile');
    context = await chromium.launchPersistentContext(profile, {
      executablePath: process.env.CHROME, headless: true,
      args: ['--remote-debugging-port=0'], env: process.env
    });
    const page = context.pages()[0];
    await page.goto(url);
    const initialLanguage = await page.evaluate(() => navigator.language);
    console.log('initial language:', initialLanguage);
    if (initialLanguage === 'fr-FR') throw new Error('Use a caller browser whose initial language differs from fr-FR.');
    const port = (await readFile(join(profile, 'DevToolsActivePort'), 'utf8')).split('\n')[0];
    const session = await context.newCDPSession(page);
    const { targetInfo } = await session.send('Target.getTargetInfo');
    await session.detach();
    const scenario = {
      browser: { mode: 'connect', cdp_endpoint: `http://127.0.0.1:${port}`, target_id: targetInfo.targetId },
      start_url: { url }, environment: { locale: 'fr-FR' },
      actions: [{ step_id: 'settle', action: 'wait_for_timeout', duration_ms: 100 }],
      capture: { at_end: ['dom', 'url'] }
    };
    await capture('connect', scenario);
    console.log('external browser preserved:', !page.isClosed() && context.browser().isConnected());
    await context.close();
    context = undefined;
    scenario.browser = { mode: 'launch', executable_path: process.env.CHROME };
    await capture('launch', scenario);
  } finally {
    if (context) await context.close();
    await new Promise(resolve => server.close(resolve));
  }
})().catch(error => { console.error(error); process.exitCode = 1; });
JS
env HOME="$case_dir/home" TMPDIR="$case_dir/temp" node "$case_dir/repro.cjs"
rm -rf "$case_dir"
  1. Compare the captured DOM fields. Connect mode returns data-navigator-locale="en-US" alongside data-intl-locale="fr-FR"; the launch control returns fr-FR for both fields. Check the printed cleanup states and the independent external-browser preservation check.

Expected behavior

The requested deterministic locale should apply to page language selection and formatting in both supported browser modes. For this fixture, a scenario selecting fr-FR should capture both data-navigator-locale="fr-FR" and data-intl-locale="fr-FR". If connect mode cannot supply the same locale semantics, it should identify that environment limitation instead of silently retaining the caller browser's original language.

docs/browser-scenario-contract.md:3 specifies deterministic browser settings for the shared launch/connect scenario contract. src/domain/browserScenarioValues.ts:93 accepts environment.locale without a connect-mode exception, and src/domain/browserScenarioValues.ts:124 describes the environment as deterministic browser settings. The locked Playwright locale contract at node_modules/playwright-core/types/types.d.ts:19318 explicitly includes navigator.language, request language, and formatting.

Impact and scope

P2, supported. A supported attached scenario can inspect application content under the external browser's original language while recording a different requested locale. This affects applications that choose localized behavior through navigator language; Intl-only formatting follows the requested setting in the observed case.

Workaround status is available. The same fixture and requested locale worked in launch mode. That route is practical when the investigation can use a fresh profile; this verification does not establish a workaround that preserves an existing profile's state.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0, macOS 27.0.1 arm64, locked playwright-core 1.58.2, and Google Chrome 155.0.8059.40. The runtime was already built for the observed reproduction.

Target and artifact context

A local HTTP fixture and a caller-owned browser with a disposable profile, initial language en-US, a loopback CDP endpoint, and an exact selected page target. The scenario captures DOM and URL artifacts after a 100 ms wait. The launch control uses the same fixture and requested locale through an owned browser.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The public connect-mode capture and launch-mode control reproduced the discrepancy. The observed DOM excerpts and cleanup states were:

Connect DOM:
<body data-navigator-locale="en-US" data-intl-locale="fr-FR">
Connect cleanup: disconnected-external
Independent caller check: external page and browser remained alive

Launch DOM:
<body data-navigator-locale="fr-FR" data-intl-locale="fr-FR">
Launch cleanup: terminated-owned-process

The evidence comes from executed public CLI captures. Local verification reproduced the behavior, and the reproduction includes the complete fixture and both browser modes.

Typed outcome or recovery expected

Repeat the attached capture from a browser whose original language differs from the requested locale. The page must observe the requested language as well as the requested formatting locale, or receive an explicit unsupported environment outcome. Keep the launch control passing. Disconnecting must preserve the external browser and release any owned locale overrides.

Analysis or suspected cause

Source-derived reasoning: src/browser/PlaywrightScenarioBrowser.ts:317 configures the attached environment with Emulation.setLocaleOverride, while src/browser/PlaywrightScenarioBrowser.ts:494 passes locale to the launch context. The observed DOM fields distinguish page language from formatting locale. The difference between the attached CDP override and the launch-context locale is consistent with the observed mismatch; the captures establish the failed contract without establishing additional locale effects such as request headers.

Additional context

  • Related completed issue #1320 concerns early CDP detach losing locale, timezone, and device-scale overrides; this reproduction retains the Intl locale while ignoring navigator language.
  • Related completed issue #1497 concerns endpoint spellings bypassing the attached-browser connection lock, rather than locale semantics.
  • Related completed issue #2166 concerns attached service-worker allow policy, rather than navigator language.
  • Related open issue #2956 concerns service-worker block enforcement and cleanup after navigation, rather than locale semantics.
  • Related open issue #3126 concerns remediation for service-worker rejection, rather than an accepted request silently retaining the original language.
Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3395 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior `capture-browser-scenario` in connect mode applies the requested formatting locale but leaves `navigator.language` at the external browser's original language. With `environment.locale: "fr-FR"`, the local page captured `navigator.language === "en-US"` and an Intl formatting locale of `fr-FR`. The same fixture and locale in launch mode captured `fr-FR` for both. Applications that select their language through `navigator.language` therefore run in a different language from the scenario's declared deterministic environment. Connect mode accepted the request and completed the action. It reported `disconnected-external`, and the independent caller confirmed that the external page and browser remained alive afterward. Launch mode reported `terminated-owned-process`. The attached capture's ordinary pre-attach event gap was explicit; no limitation identified the ignored page-language setting. ### Reproduction 1. Prepare a clean source checkout at the reproduced baseline and install its locked dependencies and compiled runtime: ```bash git clone https://github.com/morluto/rea.git rea-locale-repro cd rea-locale-repro git checkout --detach 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. Use an already-installed Chrome-family executable and set `CHROME` to its absolute path. For the standard Google Chrome installation on macOS: ```bash export CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ``` 3. Run the following from the checkout root. It creates every input file inline, starts a local fixture and an isolated caller-owned browser, finds the exact page target, calls the public capture CLI in connect mode, closes that browser, and repeats through launch mode as a control. The caller-owned browser's initial language must differ from `fr-FR`; the observed initial language was `en-US`. ```bash : "${CHROME:?Set CHROME to an installed Chrome-family executable}" case_dir="$(mktemp -d)" mkdir -p "$case_dir/home" "$case_dir/temp" cat > "$case_dir/repro.cjs" <<'JS' const { chromium } = require(require('node:path').join(process.cwd(), 'node_modules/playwright-core')); const { createServer } = require('node:http'); const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const { readFile, writeFile } = require('node:fs/promises'); const { join } = require('node:path'); const run = promisify(execFile); const html = '<!doctype html><title>Locale fixture</title><body><script>' + 'document.body.dataset.navigatorLocale = navigator.language;' + 'document.body.dataset.intlLocale = new Intl.DateTimeFormat().resolvedOptions().locale;' + '</script></body>'; async function capture(name, scenario) { const input = join(__dirname, `${name}.json`); await writeFile(input, JSON.stringify(scenario)); const result = await run(process.execPath, ['scripts/rea.mjs', 'capture-browser-scenario', input, '--json'], { cwd: process.cwd(), env: process.env, timeout: 120000, maxBuffer: 1048576 }); const value = JSON.parse(result.stdout).normalized_result; console.log(name, value.steps.at(-1).artifacts.dom.value.text); console.log(name, 'cleanup:', value.browser.cleanup); } (async () => { const server = createServer((request, response) => { response.setHeader('Content-Type', 'text/html'); response.end(html); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); let context; try { const url = `http://127.0.0.1:${server.address().port}/`; const profile = join(__dirname, 'profile'); context = await chromium.launchPersistentContext(profile, { executablePath: process.env.CHROME, headless: true, args: ['--remote-debugging-port=0'], env: process.env }); const page = context.pages()[0]; await page.goto(url); const initialLanguage = await page.evaluate(() => navigator.language); console.log('initial language:', initialLanguage); if (initialLanguage === 'fr-FR') throw new Error('Use a caller browser whose initial language differs from fr-FR.'); const port = (await readFile(join(profile, 'DevToolsActivePort'), 'utf8')).split('\n')[0]; const session = await context.newCDPSession(page); const { targetInfo } = await session.send('Target.getTargetInfo'); await session.detach(); const scenario = { browser: { mode: 'connect', cdp_endpoint: `http://127.0.0.1:${port}`, target_id: targetInfo.targetId }, start_url: { url }, environment: { locale: 'fr-FR' }, actions: [{ step_id: 'settle', action: 'wait_for_timeout', duration_ms: 100 }], capture: { at_end: ['dom', 'url'] } }; await capture('connect', scenario); console.log('external browser preserved:', !page.isClosed() && context.browser().isConnected()); await context.close(); context = undefined; scenario.browser = { mode: 'launch', executable_path: process.env.CHROME }; await capture('launch', scenario); } finally { if (context) await context.close(); await new Promise(resolve => server.close(resolve)); } })().catch(error => { console.error(error); process.exitCode = 1; }); JS env HOME="$case_dir/home" TMPDIR="$case_dir/temp" node "$case_dir/repro.cjs" rm -rf "$case_dir" ``` 4. Compare the captured DOM fields. Connect mode returns `data-navigator-locale="en-US"` alongside `data-intl-locale="fr-FR"`; the launch control returns `fr-FR` for both fields. Check the printed cleanup states and the independent external-browser preservation check. ### Expected behavior The requested deterministic locale should apply to page language selection and formatting in both supported browser modes. For this fixture, a scenario selecting `fr-FR` should capture both `data-navigator-locale="fr-FR"` and `data-intl-locale="fr-FR"`. If connect mode cannot supply the same locale semantics, it should identify that environment limitation instead of silently retaining the caller browser's original language. `docs/browser-scenario-contract.md:3` specifies deterministic browser settings for the shared launch/connect scenario contract. `src/domain/browserScenarioValues.ts:93` accepts `environment.locale` without a connect-mode exception, and `src/domain/browserScenarioValues.ts:124` describes the environment as deterministic browser settings. The locked Playwright locale contract at `node_modules/playwright-core/types/types.d.ts:19318` explicitly includes `navigator.language`, request language, and formatting. ### Impact and scope P2, supported. A supported attached scenario can inspect application content under the external browser's original language while recording a different requested locale. This affects applications that choose localized behavior through navigator language; Intl-only formatting follows the requested setting in the observed case. Workaround status is available. The same fixture and requested locale worked in launch mode. That route is practical when the investigation can use a fresh profile; this verification does not establish a workaround that preserves an existing profile's state. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0, macOS 27.0.1 arm64, locked playwright-core 1.58.2, and Google Chrome 155.0.8059.40. The runtime was already built for the observed reproduction. ### Target and artifact context A local HTTP fixture and a caller-owned browser with a disposable profile, initial language `en-US`, a loopback CDP endpoint, and an exact selected page target. The scenario captures DOM and URL artifacts after a 100 ms wait. The launch control uses the same fixture and requested locale through an owned browser. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The public connect-mode capture and launch-mode control reproduced the discrepancy. The observed DOM excerpts and cleanup states were: ```text Connect DOM: <body data-navigator-locale="en-US" data-intl-locale="fr-FR"> Connect cleanup: disconnected-external Independent caller check: external page and browser remained alive Launch DOM: <body data-navigator-locale="fr-FR" data-intl-locale="fr-FR"> Launch cleanup: terminated-owned-process ``` The evidence comes from executed public CLI captures. Local verification reproduced the behavior, and the reproduction includes the complete fixture and both browser modes. ### Typed outcome or recovery expected Repeat the attached capture from a browser whose original language differs from the requested locale. The page must observe the requested language as well as the requested formatting locale, or receive an explicit unsupported environment outcome. Keep the launch control passing. Disconnecting must preserve the external browser and release any owned locale overrides. ### Analysis or suspected cause Source-derived reasoning: `src/browser/PlaywrightScenarioBrowser.ts:317` configures the attached environment with `Emulation.setLocaleOverride`, while `src/browser/PlaywrightScenarioBrowser.ts:494` passes `locale` to the launch context. The observed DOM fields distinguish page language from formatting locale. The difference between the attached CDP override and the launch-context locale is consistent with the observed mismatch; the captures establish the failed contract without establishing additional locale effects such as request headers. ### Additional context - Related completed issue [#1320](https://github.com/morluto/rea/issues/1320) concerns early CDP detach losing locale, timezone, and device-scale overrides; this reproduction retains the Intl locale while ignoring navigator language. - Related completed issue [#1497](https://github.com/morluto/rea/issues/1497) concerns endpoint spellings bypassing the attached-browser connection lock, rather than locale semantics. - Related completed issue [#2166](https://github.com/morluto/rea/issues/2166) concerns attached service-worker allow policy, rather than navigator language. - Related open issue [#2956](https://github.com/morluto/rea/issues/2956) concerns service-worker block enforcement and cleanup after navigation, rather than locale semantics. - Related open issue [#3126](https://github.com/morluto/rea/issues/3126) concerns remediation for service-worker rejection, rather than an accepted request silently retaining the original language.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1263
No description provided.