From 43c38e27b421f3126ed5d07672187afacbcbd5a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 12:19:12 +0000 Subject: [PATCH 1/4] test(e2e): trial Playwright with the render-regression spec What changed: - Add @playwright/test 1.63.0, matching the installed playwright package, and playwright.config.js. The config starts Hugo with the same flags as the Cypress runner (testing environment, port 1315, --noHTTPCache, --disableFastRender), reuses a running server locally, keeps traces for failed tests, and defines chromium, firefox, and webkit projects. PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH points chromium at a preinstalled browser. - Port cypress/e2e/content/render-regression.cy.js to playwright/e2e/render-regression.spec.js with the same 12 checks. - Add yarn test:e2e:playwright (Chromium) and a playwright-render job in pr-render-check.yml that runs beside cypress-render on layout, asset, and Playwright changes and uploads traces on failure. - Ignore test-results/ and playwright-report/. - Remove measure.mjs and vb.mjs, unreferenced one-off Playwright scripts at the repository root. Why: - Cypress needs a separate app download that sandboxed agent environments block, while Playwright runs from the npm package with preinstalled or CDN browsers. Running both on the same spec during a trial compares reliability and speed before deciding on a migration. Impact: - PRs that change layouts, assets, or Playwright files run both frameworks. The Cypress job is unchanged. Verification: - yarn test:e2e:playwright with the preinstalled Chromium: 12 passed in 1.2 minutes, including Hugo startup. - Reverting the #7173 fix (.gd display: inline-block) failed the two tests that guard it; the other two diff tests passed. - Firefox and WebKit projects were not run; their browsers aren't installed in this environment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018NiFuxdSqYvtMpAhNfWBbM --- .github/workflows/pr-render-check.yml | 50 ++++++ .gitignore | 2 + measure.mjs | 25 --- package.json | 2 + playwright.config.js | 40 +++++ playwright/e2e/render-regression.spec.js | 196 +++++++++++++++++++++++ vb.mjs | 17 -- yarn.lock | 9 +- 8 files changed, 298 insertions(+), 43 deletions(-) delete mode 100644 measure.mjs create mode 100644 playwright.config.js create mode 100644 playwright/e2e/render-regression.spec.js delete mode 100644 vb.mjs diff --git a/.github/workflows/pr-render-check.yml b/.github/workflows/pr-render-check.yml index 2429e58f5c..42cb7143e4 100644 --- a/.github/workflows/pr-render-check.yml +++ b/.github/workflows/pr-render-check.yml @@ -118,3 +118,53 @@ jobs: --spec "cypress/e2e/content/render-regression.cy.js" \ --spec "cypress/e2e/content/canonical.cy.js" \ --no-mapping + + playwright-render: + name: Playwright render-regression spec (trial) + runs-on: ubuntu-latest + # Runs beside cypress-render during the Playwright trial so both + # frameworks check the same pages on the same PRs. + if: | + github.event_name == 'pull_request' && + (contains(github.event.pull_request.labels.*.name, 'render-regression') || + github.event.pull_request.draft == false) + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Detect layout/asset changes + id: detect + run: | + CHANGED=$(git diff --name-only \ + "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}") + + if echo "$CHANGED" | grep -qE '^(layouts/|assets/|playwright/|playwright\.config\.js$|\.github/workflows/pr-render-check\.yml$)'; then + echo "should-run=true" >> $GITHUB_OUTPUT + else + echo "should-run=false" >> $GITHUB_OUTPUT + echo "â„šī¸ No layout/asset/Playwright file changed — skipping Playwright" + fi + + - name: Setup docs environment + if: steps.detect.outputs.should-run == 'true' + env: + CYPRESS_INSTALL_BINARY: 0 + uses: ./.github/actions/setup-docs-env + + - name: Install Chromium for Playwright + if: steps.detect.outputs.should-run == 'true' + run: npx playwright install --with-deps chromium + + - name: Run render-regression spec + if: steps.detect.outputs.should-run == 'true' + run: yarn test:e2e:playwright + + - name: Upload Playwright traces + if: failure() && steps.detect.outputs.should-run == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: playwright-traces + path: test-results/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index f8080ad9ad..bd4a067d41 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,8 @@ package-lock.json /cypress/downloads/* /cypress/screenshots/* /cypress/videos/* +/test-results/ +/playwright-report/ .lycheecache test-results.xml /influxdb3cli-build-scripts/content diff --git a/measure.mjs b/measure.mjs deleted file mode 100644 index 6a140a9773..0000000000 --- a/measure.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import { chromium } from 'playwright'; -const url = process.argv[2], out = process.argv[3]; -const b = await chromium.launch({ executablePath: '/opt/pw-browsers/chromium' }); -const p = await b.newPage({ viewport: { width: 1280, height: 1400 } }); -await p.goto(url, { waitUntil: 'networkidle' }); -await p.waitForSelector('.mermaid[data-processed="true"]', { timeout: 20000 }); -await p.waitForTimeout(800); -const m = await p.evaluate(() => { - const px = el => el ? getComputedStyle(el).fontSize : null; - const c = document.querySelector('.mermaid'); - const svg = c.querySelector('svg'); - const r = svg.getBoundingClientRect(); - const edge = [...c.querySelectorAll('.edgeLabel')].filter(e=>e.textContent.trim()); - const node = [...c.querySelectorAll('.nodeLabel')]; - return { - containerWidth: Math.round(c.getBoundingClientRect().width), - svgWidth: Math.round(r.width), svgHeight: Math.round(r.height), - svgMaxWidth: svg.style.maxWidth || getComputedStyle(svg).maxWidth, - edgeSizes: [...new Set(edge.map(px))], - nodeSizes: [...new Set(node.map(px))], - }; -}); -console.log(JSON.stringify(m)); -await (await p.$('.mermaid')).screenshot({ path: out }); -await b.close(); diff --git a/package.json b/package.json index 1c7e4231ef..09fbad72c3 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@evilmartians/lefthook": "^1.7.1", + "@playwright/test": "1.63.0", "@types/js-yaml": "^4.0.9", "autoprefixer": ">=10.2.5", "cypress": "^15.17.0", @@ -139,6 +140,7 @@ "lint-codeblocks": "node scripts/ci/lint-codeblocks.mjs", "lint-codeblocks:pretty": "scripts/ci/lint-codeblocks-pretty.sh", "test:e2e": "node cypress/support/run-e2e-specs.js", + "test:e2e:playwright": "playwright test --project=chromium", "test:render-regression": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/render-regression.cy.js\" --no-mapping", "test:render-artifacts": ".ci/scripts/check-render-artifacts.sh public", "test:no-test-fixtures": ".ci/scripts/check-no-test-fixtures.sh public", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000000..313b4a0132 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,40 @@ +import process from 'node:process'; +import { defineConfig, devices } from '@playwright/test'; + +const hugoPort = 1315; +const hugoServerCommand = [ + 'npx hugo server --environment testing', + `--port ${hugoPort} --noHTTPCache --disableFastRender`, +].join(' '); +const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; + +export default defineConfig({ + testDir: 'playwright/e2e', + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + reporter: process.env.CI ? [['github'], ['list']] : 'list', + expect: { timeout: 10000 }, + use: { + baseURL: `http://localhost:${hugoPort}`, + trace: 'retain-on-failure', + }, + webServer: { + command: hugoServerCommand, + url: `http://localhost:${hugoPort}/`, + reuseExistingServer: !process.env.CI, + timeout: 300000, + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + launchOptions: chromiumExecutablePath + ? { executablePath: chromiumExecutablePath } + : {}, + }, + }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + ], +}); diff --git a/playwright/e2e/render-regression.spec.js b/playwright/e2e/render-regression.spec.js new file mode 100644 index 0000000000..99e4265a11 --- /dev/null +++ b/playwright/e2e/render-regression.spec.js @@ -0,0 +1,196 @@ +import { expect, test } from '@playwright/test'; + +const escapedChromaFragments = [ + '
', + '
 `pre code.language-${language}`)
+  .join(', ');
+
+const representativePages = [
+  {
+    path: '/influxdb3/core/reference/sample-data/',
+    exercises:
+      'placeholders inside code-tab-content + custom-timestamps wrapper',
+  },
+  {
+    path: '/influxdb3/enterprise/admin/backup-restore/',
+    exercises:
+      'placeholders inside tab-content wrapper (the page that regressed the most in #7079)',
+  },
+  {
+    path: '/influxdb3/cloud-dedicated/reference/sample-data/',
+    exercises:
+      'placeholders with DATABASE_(TOKEN|NAME) regex grouping inside custom-timestamps wrapper',
+  },
+  {
+    path: '/influxdb3/clustered/admin/users/add/',
+    exercises: 'placeholders + callouts in nested expand wrappers',
+  },
+  {
+    path: '/influxdb3/cloud-serverless/reference/sample-data/',
+    exercises:
+      'post-migration placeholders fence attribute with mixed legacy/new syntax',
+  },
+];
+
+async function expectHighlightedCodeBlock(page) {
+  await expect(page.locator(highlightedCodeBlock).first()).toBeAttached();
+}
+
+async function expectNoEscapedChromaFragments(page) {
+  const codeTexts = await page.locator('pre code').allTextContents();
+  for (const fragment of escapedChromaFragments) {
+    expect(
+      codeTexts.filter((text) => text.includes(fragment)),
+      `code blocks containing escaped chroma fragment "${fragment}"`
+    ).toEqual([]);
+  }
+}
+
+function sectionUnderHeading(page, headingId) {
+  return page.locator(
+    `xpath=//*[@id="${headingId}"]/following-sibling::*` +
+      `[not(self::h3)][preceding-sibling::h3[1][@id="${headingId}"]]`
+  );
+}
+
+function diffLines(page, headingId, lineClasses) {
+  return sectionUnderHeading(page, headingId).locator(
+    lineClasses.map((lineClass) => `pre.chroma .${lineClass}`).join(', ')
+  );
+}
+
+test.describe('Shortcode examples page', () => {
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/example/');
+  });
+
+  test('renders /example/ with no escaped chroma fragments (#7079)', async ({
+    page,
+  }) => {
+    await expectHighlightedCodeBlock(page);
+    await expectNoEscapedChromaFragments(page);
+  });
+
+  test('highlights line protocol field families without changing source text', async ({
+    page,
+  }) => {
+    const lineProtocol = page
+      .locator(
+        'xpath=//h3[contains(., "Line protocol fence")]/following-sibling::*'
+      )
+      .locator('code.language-lp')
+      .first();
+
+    await expect(lineProtocol).toContainText('a::b::c="quoted-value"');
+    await expect(
+      lineProtocol.locator('.nx', { hasText: 'cpu' }).first()
+    ).toBeAttached();
+    await expect(lineProtocol.locator('.lp-family-delimiter')).toHaveText([
+      '::',
+      '::',
+      '::',
+      '::',
+    ]);
+    await expect(
+      lineProtocol.locator('.na', { hasText: 'b::c' }).first()
+    ).toBeAttached();
+    await expect(
+      lineProtocol.locator('.s', { hasText: 'Unicode-value' }).first()
+    ).toBeAttached();
+  });
+
+  test('uses escaped plain text for malformed line protocol', async ({
+    page,
+  }) => {
+    const malformedFence = page
+      .locator('p', { hasText: 'The malformed fence falls back' })
+      .locator(
+        'xpath=following-sibling::*[1][contains(concat(" ", normalize-space(@class), " "), " highlight ")]'
+      )
+      .locator('code.language-lp');
+
+    await expect(malformedFence).toContainText('cpu field="unterminated');
+  });
+});
+
+test.describe('Representative product pages (#7079)', () => {
+  for (const { path, exercises } of representativePages) {
+    test(`${path} — ${exercises}`, async ({ page }) => {
+      await page.goto(path);
+      await expectHighlightedCodeBlock(page);
+      await expectNoEscapedChromaFragments(page);
+    });
+  }
+});
+
+test.describe('Diff fence highlighting (#7173)', () => {
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/example/');
+  });
+
+  test('renders each .gd line on its own row in the subtractive fixture', async ({
+    page,
+  }) => {
+    const deletedLines = diffLines(page, 'diff-fence--subtractive-only', [
+      'gd',
+    ]);
+    await expect(deletedLines).toHaveCount(3);
+    const rowTops = await deletedLines.evaluateAll((lines) =>
+      lines.map((line) => Math.round(line.getBoundingClientRect().top))
+    );
+    expect(new Set(rowTops).size).toBe(rowTops.length);
+  });
+
+  test('applies display:block to .gd and .gi', async ({ page }) => {
+    const changedLines = diffLines(
+      page,
+      'diff-fence--mixed-additions-and-deletions',
+      ['gd', 'gi']
+    );
+    const displays = await changedLines.evaluateAll((lines) =>
+      lines.map((line) => getComputedStyle(line).display)
+    );
+    expect(displays.length).toBeGreaterThanOrEqual(2);
+    expect(new Set(displays)).toEqual(new Set(['block']));
+  });
+
+  test('extends the highlight past the visible width on overflowing lines', async ({
+    page,
+  }) => {
+    const firstDeletedLine = diffLines(
+      page,
+      'diff-fence--subtractive-with-overflow',
+      ['gd']
+    ).first();
+    const { lineWidth, visibleWidth } = await firstDeletedLine.evaluate(
+      (line) => ({
+        lineWidth: line.offsetWidth,
+        visibleWidth: line.parentElement.clientWidth,
+      })
+    );
+    expect(lineWidth).toBeGreaterThan(visibleWidth);
+  });
+
+  test('does not tint context lines in the surrounding-context fixture', async ({
+    page,
+  }) => {
+    await expect(
+      diffLines(page, 'diff-fence--subtractive-with-surrounding-context', [
+        'gd',
+      ])
+    ).toHaveCount(2);
+  });
+});
diff --git a/vb.mjs b/vb.mjs
deleted file mode 100644
index 1f307a15bd..0000000000
--- a/vb.mjs
+++ /dev/null
@@ -1,17 +0,0 @@
-import { chromium } from 'playwright';
-const b = await chromium.launch({ executablePath: '/opt/pw-browsers/chromium' });
-const p = await b.newPage({ viewport: { width: 1280, height: 1400 } });
-await p.goto(process.argv[2], { waitUntil: 'networkidle' });
-await p.waitForSelector('.mermaid[data-processed="true"]');
-await p.waitForTimeout(600);
-console.log(await p.evaluate(() => {
-  const s = document.querySelector('.mermaid svg');
-  return JSON.stringify({
-    viewBox: s.getAttribute('viewBox'),
-    widthAttr: s.getAttribute('width'),
-    heightAttr: s.getAttribute('height'),
-    preserveAR: s.getAttribute('preserveAspectRatio'),
-    foreignObjects: s.querySelectorAll('foreignObject').length,
-  });
-}));
-await b.close();
diff --git a/yarn.lock b/yarn.lock
index d8cbdd9b93..2eb99bd7c4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -248,6 +248,13 @@
   dependencies:
     "@chevrotain/types" "~11.1.2"
 
+"@playwright/test@1.63.0":
+  version "1.63.0"
+  resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.63.0.tgz#808020c46e8b6b91f9d628cd0ebcbb32bdfd8b6c"
+  integrity sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==
+  dependencies:
+    playwright "1.63.0"
+
 "@puppeteer/browsers@2.13.2":
   version "2.13.2"
   resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.13.2.tgz#dcc8e7a4545d9680a8a72c53f132e80afc582284"
@@ -4237,7 +4244,7 @@ playwright-core@1.63.0:
   resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.63.0.tgz#e57665bc32846c213ac39a1e4d5bc6228e76b376"
   integrity sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==
 
-playwright@^1.58.1:
+playwright@1.63.0, playwright@^1.58.1:
   version "1.63.0"
   resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.63.0.tgz#99b56f9f69b1b70c44f00bf84b2fe52348ae2511"
   integrity sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==

From 7d98d17c0ed65d43eca91dd5d732edd4699f4390 Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 14 Oct 2026 11:00:40 +0000
Subject: [PATCH 2/4] chore(agents): regenerate content instruction adapters

What changed:
- Regenerate .claude/rules/content.md,
  .github/instructions/content.instructions.md, and content/AGENTS.md
  from .agents/instructions/content.md.

Why:
- #7877 updated the shared-content guidance in .agents/instructions but
  not the generated adapters, so yarn validate:agent-instructions fails
  on master and the commit hooks rewrite these files on every run.

Impact:
- Agents that read the adapters get the current shared-content preview
  guidance.

Verification:
- yarn validate:agent-instructions passes.

Co-Authored-By: Claude Opus 5.5 
Claude-Session: https://claude.ai/code/session_018NiFuxdSqYvtMpAhNfWBbM
---
 .claude/rules/content.md                     | 6 +++++-
 .github/instructions/content.instructions.md | 6 +++++-
 content/AGENTS.md                            | 6 +++++-
 3 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/.claude/rules/content.md b/.claude/rules/content.md
index a645636c75..c3f1bbb4d2 100644
--- a/.claude/rules/content.md
+++ b/.claude/rules/content.md
@@ -25,7 +25,11 @@ the `docs` CLI or direct editing.
   examples.
 - Do not hardcode production docs URLs when a relative link or `relref` works.
 - Shared files have no frontmatter. A stub's `source:` must begin `/shared/`.
-  Direct shared edits require touching every source stub; `docs edit` finds them.
+  Identify every consumer before editing shared content. When previewing changes
+  with a running `hugo server`, touch each consuming stub or restart the server
+  because the watcher doesn't rebuild consumers automatically after shared-source
+  changes. Fresh builds, including CI builds and checks, don't require touching
+  consumer stubs. Edit stub content only when its frontmatter needs to change.
 - Use resource-ownership language for import/write/load guidance and write
   recommendations in InfluxData's first-person plural voice.
 
diff --git a/.github/instructions/content.instructions.md b/.github/instructions/content.instructions.md
index 302a0113a9..7439c2b467 100644
--- a/.github/instructions/content.instructions.md
+++ b/.github/instructions/content.instructions.md
@@ -24,7 +24,11 @@ the `docs` CLI or direct editing.
   examples.
 - Do not hardcode production docs URLs when a relative link or `relref` works.
 - Shared files have no frontmatter. A stub's `source:` must begin `/shared/`.
-  Direct shared edits require touching every source stub; `docs edit` finds them.
+  Identify every consumer before editing shared content. When previewing changes
+  with a running `hugo server`, touch each consuming stub or restart the server
+  because the watcher doesn't rebuild consumers automatically after shared-source
+  changes. Fresh builds, including CI builds and checks, don't require touching
+  consumer stubs. Edit stub content only when its frontmatter needs to change.
 - Use resource-ownership language for import/write/load guidance and write
   recommendations in InfluxData's first-person plural voice.
 
diff --git a/content/AGENTS.md b/content/AGENTS.md
index 6bb868ca05..e74e89a181 100644
--- a/content/AGENTS.md
+++ b/content/AGENTS.md
@@ -24,7 +24,11 @@ the `docs` CLI or direct editing.
   examples.
 - Do not hardcode production docs URLs when a relative link or `relref` works.
 - Shared files have no frontmatter. A stub's `source:` must begin `/shared/`.
-  Direct shared edits require touching every source stub; `docs edit` finds them.
+  Identify every consumer before editing shared content. When previewing changes
+  with a running `hugo server`, touch each consuming stub or restart the server
+  because the watcher doesn't rebuild consumers automatically after shared-source
+  changes. Fresh builds, including CI builds and checks, don't require touching
+  consumer stubs. Edit stub content only when its frontmatter needs to change.
 - Use resource-ownership language for import/write/load guidance and write
   recommendations in InfluxData's first-person plural voice.
 

From caac1159452e895186cfb97d9ea3b5d05c65ce1f Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 14 Oct 2026 12:32:18 +0000
Subject: [PATCH 3/4] test(e2e): port the custom timestamps time zone tests to
 Playwright

What changed:
- Add playwright/e2e/custom-timestamps.spec.js, ported from the Cypress
  spec in #7819. It checks the sample data date picker on the Cloud
  Serverless get-started write page:
  - the selected date east (Asia/Tokyo) and west (America/Los_Angeles)
    of UTC, and the saved preference
  - the default local "yesterday" when the local and UTC dates differ,
    using page.clock
  - every home line protocol sample moves to the selected date
- Run only render-regression.spec.js in the playwright-render CI job
  until #7873 merges.

Why:
- #7819 is closed as superseded by #7873, but its tests cover #6424.
  Playwright sets the time zone per test with timezoneId instead of the
  Chrome DevTools Protocol override the Cypress version needed.

Impact:
- No CI change for this spec yet. Add it to the playwright-render job
  after #7873 merges.

Verification:
- On this branch (master's date picker): 3 of 4 tests fail, matching
  #7819's report. Tokyo shows 2025-03-14 for a 2025-03-15 selection,
  the default shows 2025-03-13 instead of 2025-03-14, and the line
  protocol keeps its 2024 timestamps. The Los Angeles control passes.
- On a local merge with #7873: all 16 Playwright tests pass.

Co-Authored-By: Claude Opus 5.5 
Claude-Session: https://claude.ai/code/session_018NiFuxdSqYvtMpAhNfWBbM
---
 .github/workflows/pr-render-check.yml    |  4 +-
 playwright/e2e/custom-timestamps.spec.js | 86 ++++++++++++++++++++++++
 2 files changed, 89 insertions(+), 1 deletion(-)
 create mode 100644 playwright/e2e/custom-timestamps.spec.js

diff --git a/.github/workflows/pr-render-check.yml b/.github/workflows/pr-render-check.yml
index 42cb7143e4..c762a466ac 100644
--- a/.github/workflows/pr-render-check.yml
+++ b/.github/workflows/pr-render-check.yml
@@ -157,9 +157,11 @@ jobs:
         if: steps.detect.outputs.should-run == 'true'
         run: npx playwright install --with-deps chromium
 
+      # custom-timestamps.spec.js checks date picker fixes from #7873.
+      # Add it here after #7873 merges; until then it fails on master.
       - name: Run render-regression spec
         if: steps.detect.outputs.should-run == 'true'
-        run: yarn test:e2e:playwright
+        run: yarn test:e2e:playwright playwright/e2e/render-regression.spec.js
 
       - name: Upload Playwright traces
         if: failure() && steps.detect.outputs.should-run == 'true'
diff --git a/playwright/e2e/custom-timestamps.spec.js b/playwright/e2e/custom-timestamps.spec.js
new file mode 100644
index 0000000000..4238761043
--- /dev/null
+++ b/playwright/e2e/custom-timestamps.spec.js
@@ -0,0 +1,86 @@
+import { expect, test } from '@playwright/test';
+
+const serverlessWritePage = '/influxdb3/cloud-serverless/get-started/write/';
+const preferencesKey = 'influxdata_docs_preferences';
+
+function firstSampleSeconds(date) {
+  return String(Date.parse(`${date}T08:00:00Z`) / 1000);
+}
+
+async function selectSampleDate(page, date) {
+  await page.locator('.custom-time-trigger a[data-action="open"]').click();
+  await page
+    .locator('#custom-date-selector')
+    .evaluate((picker, selected) => picker.datepicker.setDate(selected), date);
+  await page.locator('#submit-custom-date').click();
+}
+
+async function savedSampleDate(page) {
+  return page.evaluate(
+    (key) =>
+      JSON.parse(localStorage.getItem(key) || '{}').sample_get_started_date,
+    preferencesKey
+  );
+}
+
+async function expectSampleDate(page, date) {
+  const timeRange = page.locator('span.custom-timestamps').first();
+  await expect(timeRange).toContainText(`${date}T08:00:00Z`);
+  await expect(timeRange).toContainText(`${date}T20:00:00Z`);
+  expect(await savedSampleDate(page)).toBe(date);
+}
+
+test.describe('Custom timestamps date picker', () => {
+  test.describe('east of UTC', () => {
+    test.use({ timezoneId: 'Asia/Tokyo' });
+
+    test('uses the selected date (#6424)', async ({ page }) => {
+      await page.goto(serverlessWritePage);
+      await selectSampleDate(page, '2025-03-15');
+      await expectSampleDate(page, '2025-03-15');
+    });
+
+    test("defaults to the reader's local yesterday", async ({ page }) => {
+      await page.clock.setFixedTime(new Date('2025-03-14T21:00:00Z'));
+      await page.goto(serverlessWritePage);
+      await expectSampleDate(page, '2025-03-14');
+    });
+  });
+
+  test.describe('west of UTC', () => {
+    test.use({ timezoneId: 'America/Los_Angeles' });
+
+    test('uses the selected date', async ({ page }) => {
+      await page.goto(serverlessWritePage);
+      await selectSampleDate(page, '2025-03-15');
+      await expectSampleDate(page, '2025-03-15');
+    });
+  });
+
+  test.describe('UTC', () => {
+    test.use({ timezoneId: 'UTC' });
+
+    test('rewrites every line protocol sample to the selected date (#6424)', async ({
+      page,
+    }) => {
+      await page.goto(serverlessWritePage);
+      await selectSampleDate(page, '2025-03-15');
+
+      const sampleBlocks = page
+        .locator('.custom-timestamps pre')
+        .filter({ hasText: 'home,room=' });
+      await expect(sampleBlocks.first()).toContainText(
+        firstSampleSeconds('2025-03-15')
+      );
+      const sampleTexts = await sampleBlocks.allTextContents();
+      expect(sampleTexts.length).toBeGreaterThan(0);
+      expect(
+        sampleTexts.filter(
+          (text) =>
+            !text.includes(firstSampleSeconds('2025-03-15')) ||
+            text.includes(firstSampleSeconds('2022-01-01'))
+        )
+      ).toEqual([]);
+    });
+  });
+});

From 356c4791226a38bfa405d78a367407f7b8e782fa Mon Sep 17 00:00:00 2001
From: Claude 
Date: Sat, 17 Oct 2026 15:24:39 +0000
Subject: [PATCH 4/4] ci(test): run the custom-timestamps Playwright spec in CI

What changed:
The playwright-render job now runs custom-timestamps.spec.js beside
render-regression.spec.js.

Why:
The spec checks date picker fixes that reached master with #7873.

Impact:
PRs that touch layouts, assets, or Playwright files run four more
browser tests.

Verification:
After merging master, both specs pass locally in Chromium: 16 tests.

Co-Authored-By: Claude Opus 5.5 
Claude-Session: https://claude.ai/code/session_018NiFuxdSqYvtMpAhNfWBbM
---
 .github/workflows/pr-render-check.yml | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/pr-render-check.yml b/.github/workflows/pr-render-check.yml
index c762a466ac..4359381aa1 100644
--- a/.github/workflows/pr-render-check.yml
+++ b/.github/workflows/pr-render-check.yml
@@ -157,11 +157,12 @@ jobs:
         if: steps.detect.outputs.should-run == 'true'
         run: npx playwright install --with-deps chromium
 
-      # custom-timestamps.spec.js checks date picker fixes from #7873.
-      # Add it here after #7873 merges; until then it fails on master.
-      - name: Run render-regression spec
+      - name: Run render-regression and custom-timestamps specs
         if: steps.detect.outputs.should-run == 'true'
-        run: yarn test:e2e:playwright playwright/e2e/render-regression.spec.js
+        run: |
+          yarn test:e2e:playwright \
+            playwright/e2e/render-regression.spec.js \
+            playwright/e2e/custom-timestamps.spec.js
 
       - name: Upload Playwright traces
         if: failure() && steps.detect.outputs.should-run == 'true'