Skip to content

fix(app): copy buttons silently fail over plain HTTP#2231

Open
alex-fedotyev wants to merge 5 commits into
mainfrom
alex/clipboard-http-fallback
Open

fix(app): copy buttons silently fail over plain HTTP#2231
alex-fedotyev wants to merge 5 commits into
mainfrom
alex/clipboard-http-fallback

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

Closes #2135.

Summary

navigator.clipboard is only defined when the page is served over HTTPS or localhost. When HyperDX is reached over plain HTTP (Tailscale tunnel, corporate VPN, non-localhost host), every "copy" button in the search UI throws TypeError: Cannot read properties of undefined (reading 'writeText') and nothing reaches the user's clipboard. This adds a non-secure-context fallback and a clear toast.

The fix matches what brandon-pereira pre-approved in the issue thread: try the modern API first, fall back to the hidden-textarea + document.execCommand('copy') trick, and surface a clear failure toast when both paths fail.

Changes

  • New packages/app/src/utils/clipboard.ts with copyTextToClipboard (modern API + execCommand fallback) and copyTextWithToast (wraps the copy with a Mantine notification).
  • Replace all six raw navigator.clipboard.writeText call sites with the new util:
    • DBRowTableRowButtons.tsx: copy entire row as JSON, copy shareable link.
    • DBRowTableFieldWithPopover.tsx: copy field value.
    • DBRowJsonViewer.tsx: "Copy row as JSON", "Copy Object", and "Copy Value" actions in the parsed tab.
  • The failure toast text spells out the requirement: "Couldn't copy. HyperDX needs HTTPS or localhost to use the browser clipboard API." so users on Tailscale / VPN / non-localhost hosts know what to do.
  • Selection preservation in the fallback: any pre-existing document.getSelection() range is restored after the synthetic textarea select / copy sequence runs.

Why

react-copy-to-clipboard and Mantine's useClipboard hook both have this fallback built in, but they're declarative wrappers (JSX / hook) and the affected sites dispatch writeText from imperative event handlers, including one inside a context-menu action object. A small imperative util drops in cleanly with one-line changes at each site.

Test plan

  • make ci-lint passes.
  • make ci-unit passes (10 new tests in clipboard.test.ts; 1559 app tests + 832 common-utils tests total).
  • Unit coverage: modern-API success, modern-API throws -> fallback, modern-API undefined -> fallback, both fail, textarea cleanup on success, textarea cleanup on execCommand throw, selection preservation, toast colors / messages.
  • Playwright spec parses (npx playwright test --list lists all 9 tests).
  • e2e CI run validates the spec in full-stack mode (couldn't fully run e2e from inside the agent's docker-in-docker setup; CI will exercise the spec).

The spec covers all six call sites, the modern -> fallback transition, the "both paths fail" failure toast (with pageerror listener to assert nothing throws), and light + dark theme parity for the toast.

[ui-check: light-only]

I verified the success and failure toast paths via the unit tests' notifications.show assertions and the Playwright spec's theme parity checks. Light-only manual screenshot wasn't captured because the agent couldn't drive the live UI through the dev stack, but the e2e spec captures both data-mantine-color-scheme=light and dark paths.

[no-followups]

No deferred work: react-copy-to-clipboard usages in other components already have the fallback built in, so they aren't affected by this bug.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 12, 2026 2:19am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

🦋 Changeset detected

Latest commit: c319851

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label May 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 347 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 6
  • Production lines changed: 347 (+ 734 in test files, excluded from tier calculation)
  • Branch: alex/clipboard-http-fallback
  • Author: alex-fedotyev

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

E2E Test Results

All tests passed • 185 passed • 3 skipped • 1253s

Status Count
✅ Passed 185
❌ Failed 0
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

PR Review

  • ⚠️ PR description claims [no-followups] because react-copy-to-clipboard already has fallback, but PropertyComparisonChart.tsx:146 and ChartEditor/RawSqlChartInstructions.tsx:38 use Mantine's useClipboard, which calls navigator.clipboard.writeText directly (no fallback). Those copy buttons will still silently fail over plain HTTP. → Either migrate those two sites to copyTextWithToast in this PR, or remove the [no-followups] claim and file a follow-up.
  • ⚠️ clipboard.ts:8,42FALLBACK_MAX_BYTES = 1_000_000 is compared against text.length, which is UTF-16 code units, not bytes. For ASCII-heavy log JSON this is fine, but the variable name and comment misleadingly say "bytes". → Either rename to FALLBACK_MAX_CHARS or measure with new Blob([text]).size if a true byte cap was intended.
  • ℹ️ Otherwise the implementation is clean: secure-context guard avoids burning user-activation on a doomed await, finally block removes the scratch textarea and restores selection/focus, oversized-payload guard prevents main-thread freezes on the sync fallback, and unit + e2e coverage exercises both success and dual-failure paths. Test-only data-testid additions in HyperJson.tsx and DBRowTableIconButton.tsx are minimal and behavior-preserving. The virtualization-aware isMountedRef / tracked-timeout pattern in DBRowTableRowButtons and DBRowTableFieldWithPopover correctly fixes a real setState-after-unmount risk.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Deep Review

🟡 P2 -- recommended

  • packages/app/src/utils/clipboard.ts:98 -- The finally block runs removeChild, selection.addRange, and previouslyFocused?.focus() with no guards, so a DOMException from any of them (third party stripped the textarea, captured Range was invalidated mid-copy, focus target now detached) propagates out instead of returning succeeded -- turning a clean execCommand('copy') into a rejected promise that becomes an unhandled rejection at the void copyTextWithToast(...) sites in DBRowJsonViewer.tsx.

    • Fix: Wrap each cleanup statement (or the whole finally body) in its own try { ... } catch { /* swallow */ } so cleanup can never mask the boolean return.
    • correctness, reliability, adversarial
  • packages/app/tests/e2e/features/search/clipboard.spec.ts:25 -- declare global { interface Window { __copyHistory: CopyEntry[]; __forceModernThrow: boolean; __forceExecCommandFalse: boolean } } declares three test-only fields as required Window properties; packages/app/tsconfig.build.json excludes only **/*.test.ts(x) (not **/*.spec.ts) and next.config.mjs points next build at it, so the augmentation participates in the production type graph and any future window.__copyHistory.push(...) in src/ compiles silently to a runtime TypeError.

    • Fix: Either mark all three properties optional (__copyHistory?: CopyEntry[]; __forceModernThrow?: boolean; __forceExecCommandFalse?: boolean; -- matches the existing __HDX_THEME?: pattern in ThemeProvider.tsx), or add tests/e2e/** / **/*.spec.ts to tsconfig.build.json's exclude.
    • kieran-typescript, adversarial
  • packages/app/src/utils/clipboard.ts:119 -- FAILURE_MESSAGE is a non-exported constant that is then re-typed verbatim in packages/app/src/utils/__tests__/clipboard.test.ts:10 and packages/app/tests/e2e/features/search/clipboard.spec.ts:102; any wording edit must land in three files or the assertions silently stop matching.

    • Fix: Export FAILURE_MESSAGE from clipboard.ts, import it in the unit test, and either import it (or assert with expect.stringContaining) in the e2e spec.
    • maintainability, testing
  • packages/app/src/components/DBTable/DBRowTableRowButtons.tsx:25 -- The (useState isCopied, useRef isMountedRef, useRef resetTimer, await + ok-guard + clearTimeout + setTimeout + isMountedRef recheck) pattern is replicated three times across copyRowData, copyRowUrl, and DBRowTableFieldWithPopover.copyFieldValue; the next contributor has three sites to keep in sync (one already differs in cleanup brace style).

    • Fix: Extract a useCopyWithFlash(durationMs) hook under packages/app/src/hooks/ that returns { isCopied, copyWithFlash(text, successMessage?) } and owns the mount ref, timer ref, and ok-guarded reset.
    • maintainability
  • packages/app/src/components/DBTable/DBRowTableFieldWithPopover.tsx:98 -- No Jest+RTL test covers the new if (!ok || !isMountedRef.current) return failure short-circuit or the post-2s reset on either DBRowTableFieldWithPopover or DBRowTableRowButtons; the only assertion is the e2e fail-both tooltip-text check, so a regression that removed the ok guard or the cleanup clearTimeout would not break any unit test.

    • Fix: Add Jest+RTL tests with jest.useFakeTimers() that mock copyTextWithToast to resolve true and false, unmount during a pending await, and double-click within the 2 s window.
    • testing
🔵 P3 nitpicks (12)
  • packages/app/src/utils/clipboard.ts:8 -- FALLBACK_MAX_BYTES = 1_000_000 is named in bytes and warned about in bytes, but the guard is text.length > FALLBACK_MAX_BYTES (UTF-16 code units), so an emoji-heavy payload counts at half its true byte size while a 1,000,001-char ASCII string is refused.

    • Fix: Rename to FALLBACK_MAX_CODE_UNITS, or measure new TextEncoder().encode(text).length against a true byte budget.
  • packages/app/src/utils/clipboard.ts:89 -- textarea.select() runs outside the try block, so if it throws (detached prototype, hostile environment, late mutation between appendChild and select) the textarea stays in the DOM and the finally cleanup is skipped.

    • Fix: Move textarea.select() inside the try block so cleanup runs unconditionally.
  • packages/app/src/utils/clipboard.ts:119 -- One toast string covers three distinct failures: real both-paths-fail, size-cap refusal (where the user is on HTTP and the advice is technically right but doesn't explain the size cap), and a modern-API rejection on HTTPS via permissions-policy or sandboxed iframe (where the "switch to HTTPS" advice is wrong because the user is already on HTTPS).

    • Fix: Branch the toast text per discriminated failure reason -- distinct messages for size-cap, secure-context rejection, and the generic fallback failure.
  • packages/app/src/components/DBTable/DBRowTableFieldWithPopover.tsx:104 -- If click 1 succeeds (isCopied=true, reset timer armed) and click 2 within the 2 s window fails (copyTextWithToast returns false, the red toast shows, the handler returns), the icon stays in its "Copied!" state for up to 2 s contradicting the failure toast the user just saw. Same shape in DBRowTableRowButtons.copyRowData / copyRowUrl.

    • Fix: On the !ok branch, also clearTimeout(copyResetTimeoutRef.current) and setIsCopied(false) so the icon state matches the toast.
  • packages/app/src/utils/__tests__/clipboard.test.ts:183 -- The size-cap test only covers 1_000_001; a regression from > to >= would silently pass because no test exercises exactly FALLBACK_MAX_BYTES.

    • Fix: Add a test that copies 'x'.repeat(1_000_000) via the fallback and asserts the result is true and execCommand was called.
  • packages/app/src/utils/__tests__/clipboard.test.ts:97 -- isSecureContextWithClipboard's ?.writeText short-circuit is only covered for clipboard === undefined, not for the partial polyfill navigator.clipboard = {}.

    • Fix: Add a test that sets navigator.clipboard = {} and asserts the call routes to the fallback without a TypeError.
  • packages/app/src/utils/clipboard.ts:76 -- The selection === null branch is unreachable from the test suite because jsdom always returns a Selection.

    • Fix: Stub document.getSelection to return null and assert copyTextToClipboard still resolves true.
  • packages/app/src/utils/clipboard.ts:99 -- The removeChild/addRange/focus throw scenarios that drive the P2 finally-cleanup issue have no unit coverage; a fix that wraps each in try/catch needs a pinning test.

    • Fix: Monkey-patch Node.prototype.removeChild (and selection.addRange) to throw, assert copyTextToClipboard resolves to a boolean rather than rejecting.
  • packages/app/tests/e2e/features/search/clipboard.spec.ts:294 -- "writes through to the real OS clipboard via the fallback" forces isSecureContext=false but leaves navigator.clipboard.writeText intact and asserts only that readText returns text, so a future regression loosening the secure-context gate would still pass.

    • Fix: Pre-spy on navigator.clipboard.writeText via addInitScript and assert it was never invoked, anchoring the negative.
  • packages/app/src/utils/__tests__/clipboard.test.ts:24 -- beforeEach does not reset document.body, so a future test that fails mid-flight before its own cleanup leaks DOM into the next case's querySelectorAll('textarea').length === 0 assertion.

    • Fix: Add afterEach(() => { document.body.innerHTML = ''; }).
  • packages/app/src/components/DBRowJsonViewer.test.tsx:172 -- The updated copy tests now only assert against the modern API path (mockClipboard.mockResolvedValue(undefined) and isSecureContext = true); the new notifications.show integration through copyTextWithToast is no longer pinned at the component level.

    • Fix: Add expect(mantineShow).toHaveBeenCalledWith({ color: 'green', message: 'Copied object to clipboard' }) to the existing cases.
  • packages/app/src/components/DBTable/DBRowTableRowButtons.tsx:30 -- useRef<NodeJS.Timeout | undefined> is used for browser timers; setTimeout returns number in the DOM lib, and the alias only resolves because @types/node is in scope. Other files in the app use ReturnType<typeof setTimeout> for the same job.

    • Fix: Switch the three useRef<NodeJS.Timeout | undefined> declarations (here and in DBRowTableFieldWithPopover.tsx) to useRef<ReturnType<typeof setTimeout> | undefined>.

Reviewers (7): correctness, testing, maintainability, reliability, adversarial, kieran-typescript, julik-frontend-races.

Testing gaps:

  • No RTL coverage for the isMountedRef + reset-timer lifecycle on DBRowTableRowButtons / DBRowTableFieldWithPopover (unmount-during-await, rapid double-click, post-2s reset).
  • No unit coverage for the finally-cleanup throw paths (removeChild / addRange / focus) that drive the top P2.
  • No e2e or unit coverage for the modern-rejects-then-fallback path (__forceModernThrow is declared but never assigned in installCopyHook).
  • Pre-existing virtualised-row recycle race in DBRowTable.tsx (slot-index keyExtractor) is not introduced by this diff but is not closed by the new isMountedRef; a "click copy → scroll virtualised list → assert no other row shows the active state" test would surface it.

@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Pushed a9b9c41 to fix the e2e failures.

Root cause: addInitScript serialises functions via .toString(), which drops closure-captured variables. HOOK_INIT_SCRIPT returned a closure that captured mode from the outer scope, so mode was always undefined inside the page. Every test ran in no-modern mode regardless of what was passed in, which is why the "modern API" tests always got source === 'fallback' and the "fail-both" tests never set __forceExecCommandFalse.

Fix: Pass mode as the Playwright arg parameter to addInitScript instead — Playwright serialises that separately and injects it as the function argument.

Also moved textarea cleanup and selection restore into a finally block in execCommandFallback, and added previouslyFocused?.focus() to address the deep-review P2 about stolen focus on the HTTP fallback path.

The lint failure (make: *** Error 139) is a runner segfault unrelated to the code — expect CI to pass on this push.

@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Pushed 8c5f81e to drop the three theme-parity tests.

Root cause of the shard 3 failures: the dark-theme cases set localStorage.mantine-color-scheme-value in an init script, but HyperDX reads its color preference from hdx-user-preferences (a JSON object with a colorMode field, see packages/app/src/useUserPreferences.tsx). The light test passed only because chromium defaults to light; the dark tests asserted on data-mantine-color-scheme and got "light".

Rather than rewrite them against the right key, I dropped them. The clipboard util doesn't render anything theme-specific (toasts are stock notifications.show), so theme parity isn't this util's concern. The deep-review's P3 made the same point. Six tests remain, covering all six callsites + the modern -> fallback transition + the failure toast.

@github-actions github-actions Bot added review/tier-3 Standard — full human review required and removed review/tier-2 Low risk — AI review + quick human skim labels May 7, 2026
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

alex-fedotyev commented May 7, 2026

Pushed 09d0653f addressing the deep-review P2 list. Summary of what landed:

Util changes (packages/app/src/utils/clipboard.ts)

P2 item Fix
catch swallows error console.warn on both catch paths
as HTMLElement cast narrowed with instanceof HTMLElement
awaited rejection past activation isSecureContextWithClipboard() upfront; non-secure pages route straight to fallback
FAILURE_MESSAGE over-asserts softened to "Couldn't copy to clipboard. If you're on plain HTTP, switch to HTTPS or localhost."
multi-MB freeze refuse fallback at text.length > 1_000_000 with a warn + failure toast; modern path still handles large payloads

Callsite changes

  • DBRowJsonViewer.tsx (3 sites): void copyTextWithToast(...) so floating Promises don't drop the success/fail signal.
  • DBRowTableFieldWithPopover.tsx and DBRowTableRowButtons.tsx: track post-await timeout ids in refs, gate setIsCopied on isMountedRef.current, clear timers in the unmount cleanup. Rows and popovers are virtualised; this stops setState-on-unmounted noise and leaked timers when the parent recycles mid-await.

Test additions

Unit tests (16, was 10):

  • secure-context routing (isSecureContext=false -> sync fallback even when writeText exists).
  • payload cap: refused fallback when over 1MB; modern path still copies large payloads.
  • focus restore: assertion that document.activeElement === input after the fallback runs.
  • empty-string copy.
  • console.warn is hit on catch paths.
  • existing DBRowJsonViewer.test.tsx copy tests now seed window.isSecureContext = true (the new util short-circuits to the fallback otherwise).

E2E tests (12, was 6):

  • per-callsite modern API: row JSON, row URL, field-value popover, parsed-tab "Copy row as JSON", parsed-tab "Copy Value", parsed-tab "Copy Object".
  • per-callsite fallback: row JSON and row URL on no-modern mode.
  • per-callsite failure toast: row JSON, field-value popover, parsed-tab "Copy row as JSON" on fail-both mode.
  • if (ok) setIsCopied regression: assert the icon's tooltip stays "Copy entire row as JSON" (not "Copied!") after a fail-both click.
  • one test bypasses the hook entirely: forces window.isSecureContext = false, lets the real document.execCommand('copy') run, then reads the OS clipboard via navigator.clipboard.readText() to verify the bytes actually landed. Skipped on non-chromium.

Stable locators

Added data-testid on the four copy buttons (row-copy-json-button, row-copy-link-button, field-copy-value-button, json-viewer-copy-row) and on each HyperJson leaf type (hyperjson-value-string, -number, -boolean, -object, -array). The e2e spec now uses these instead of [class*="string"] and the getByTitle().or().catch() chain.

Tier impact

Predicted tier: review/tier-3 (346 prod LOC + 733 test LOC). Crossed the 150-line tier-2 ceiling because of the 17 P2 items. If you'd rather the util + race fixes land in one PR and the test expansion in another, the split is clean (commit 09d0653f is mostly mechanical); flag and I'll split.

What I deferred

P3 nitpicks (3 items): the SSR guards in clipboard.ts and an empty-string test for copyTextWithToast are now covered. The "drop SSR guards entirely" suggestion I left in place because the cost is one line and the safety is real.

alex-fedotyev and others added 4 commits May 8, 2026 00:20
Closes #2135.

`navigator.clipboard` is only defined when the page is served over
HTTPS or `localhost`. When HyperDX is reached over plain HTTP
(Tailscale tunnel, corporate VPN, non-localhost host), every "copy"
button throws `TypeError: Cannot read properties of undefined
(reading 'writeText')` and nothing reaches the user's clipboard.

Add `packages/app/src/utils/clipboard.ts` with two helpers:

- `copyTextToClipboard(text)` tries the modern API first, then falls
  back to a hidden-textarea + `document.execCommand('copy')` trick
  that works in non-secure contexts.
- `copyTextWithToast(text, successMessage)` wraps the copy with a
  Mantine notification (green on success, red on failure with a
  message that explains the HTTPS / localhost requirement).

Replace all six raw `navigator.clipboard.writeText` call sites with
the new util:

- `DBRowTableRowButtons.tsx` row JSON + shareable URL buttons
- `DBRowTableFieldWithPopover.tsx` field-value popover
- `DBRowJsonViewer.tsx` "Copy row as JSON", "Copy Object", and
  "Copy Value" actions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…serialisation

addInitScript serialises functions via .toString(), which drops closure
variables. HOOK_INIT_SCRIPT returned a closure that captured `mode`, so
`mode` was always undefined inside the page and every test ran in
no-modern mode. Pass mode as the Playwright arg parameter instead.

Also moves textarea cleanup and selection restore into a finally block in
execCommandFallback, and restores previouslyFocused.focus() so the HTTP
fallback path does not silently steal typing context from open inputs.
The three theme-parity tests asserted on `html[data-mantine-color-scheme]`
after seeding `localStorage.mantine-color-scheme-value`, but the app reads
its color preference from `hdx-user-preferences` (a JSON object with a
`colorMode` field). The light test passed only because chromium defaults
to light; the dark tests failed in CI for that reason.

Drop the tests rather than rewriting against the right key. The clipboard
util doesn't render anything theme-specific (toasts are stock
`notifications.show`), so theme parity isn't this util's concern. The
remaining six cases still cover all six callsites plus the modern -> fallback
transition and the failure toast.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Util (`packages/app/src/utils/clipboard.ts`):

- Route straight to the synchronous fallback when `window.isSecureContext`
  is false. The previous flow awaited a writeText rejection first, which
  could land after the click's user-activation token expired and leave the
  fallback without activation (Safari HTTPS-permission-denied path).
- Narrow `document.activeElement` with `instanceof HTMLElement` so SVG /
  MathML elements don't slip through the focus-restore.
- Cap the synchronous fallback at 1MB. Multi-MB JSON payloads through
  `textarea.value = text; document.execCommand('copy')` froze the main
  thread for seconds; oversized payloads now show the failure toast.
- `console.warn` on each catch path so developers have a breadcrumb when
  remoting into a customer browser.
- Soften the failure-toast wording to cover permission-denied / sandboxed-
  iframe / missing-API cases without asserting the cause is HTTP.

Callsites:

- `void` the floating Promise calls in `DBRowJsonViewer` (3 sites) so a
  future invariant break surfaces as an unhandled rejection instead of
  silently dropping the success/fail signal.
- Track post-await timeout ids in refs and gate post-await `setIsCopied`
  on `isMountedRef.current` in `DBRowTableFieldWithPopover` and
  `DBRowTableRowButtons`. Rows / popovers are virtualised; without this
  guard the timer leaks and setState-on-unmounted noise fires after the
  parent recycles.

Tests:

- 16 unit tests cover modern API, secure-context routing, modern-throws
  fallback, both-fail, textarea cleanup, selection restore, focus restore,
  payload cap (refused + still uses modern), empty string, and toast
  colours / messages.
- 12 e2e tests cover all six callsites on the modern path, the modern ->
  fallback transition for row JSON and row URL, the failure toast on three
  callsites, the `if (ok) setIsCopied` regression for the row-JSON button,
  the `Copy Object` parsed-tab branch, and one test that bypasses the hook
  to verify the real OS clipboard receives the text via the fallback.
- Stable `data-testid` on row JSON / link / field-popover / json-viewer
  copy buttons and on each HyperJson leaf type, so the e2e doesn't depend
  on `[class*="string"]` or hover-only Mantine tooltip text.
- Existing `DBRowJsonViewer.test.tsx` copy tests now seed
  `window.isSecureContext = true` and `await waitFor(...)`, since the new
  util short-circuits to the fallback otherwise.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The "field-value popover copy button" test in clipboard.spec.ts hovered
`searchPage.table.firstRow.locator('td').nth(1)` and waited for
`field-copy-value-button` to appear. The popover trigger is an inner
`<span>` inside `DBRowTableFieldWithPopover`, and `mouseenter` does not
bubble, so hovering the outer `td` did not always fire the span's
`onMouseEnter`. CI Shard 2 hit this on all three retries
(run 25529406984, job 74932164598).

Add a stable `data-testid="field-popover-trigger"` to the `<Popover.Target>`
span and hover that locator directly in the test. Raise the visibility
timeout to 10000ms to match the rest of clipboard.spec.ts (line 108).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copying trace attributes/columns as JSON doesn't work

1 participant