From ef6b0d1798a7bb9cba9507e1641a1a24e9321e46 Mon Sep 17 00:00:00 2001 From: Dominik Mengelt Date: Wed, 4 Feb 2026 15:21:10 +0100 Subject: [PATCH 1/3] test(react): add React 19 compatibility test to ensure components don't access element.ref --- .../GooglePayButton.react19.test.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/button-react/GooglePayButton.react19.test.tsx diff --git a/src/button-react/GooglePayButton.react19.test.tsx b/src/button-react/GooglePayButton.react19.test.tsx new file mode 100644 index 0000000..90c2082 --- /dev/null +++ b/src/button-react/GooglePayButton.react19.test.tsx @@ -0,0 +1,48 @@ +import GooglePayButton from './GooglePayButton'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import defaults from '../lib/__setup__/defaults'; + +describe('React 19 compatibility', () => { + it('does not access element.ref when mounting (simulates React 19)', () => { + const div = document.createElement('div'); + + // Save original createElement + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalCreateElement: any = React.createElement; + + // Monkeypatch React.createElement to wrap returned element in a Proxy that + // throws if code attempts to read the `ref` property (React 19 behavior). + // eslint-disable-next-line @typescript-eslint/no-explicit-any + React.createElement = function patchedCreateElement(...args: any[]) { + const el = originalCreateElement(...args); + return new Proxy(el, { + get(target, prop) { + if (prop === 'ref') { + // Allow React internals and render pipeline to read `ref` (they do this + // during reconciliation). Only throw if the access does not originate + // from React internals — this simulates component/library code + // incorrectly accessing `element.ref`. + const stack = new Error().stack || ''; + if (!/(?:react(?:-|\/)dom|react(?:-|\/)cjs|react(?:-|\/)umd)/i.test(stack)) { + throw new Error('Accessing element.ref is not allowed (simulating React 19)'); + } + // If stack indicates React internals, allow the access. + return (target as any)[prop]; + } + return (target as any)[prop]; + }, + }); + } as unknown as typeof React.createElement; + + // If mounting the component tries to read `element.ref`, the Proxy will throw + expect(() => { + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); + }).not.toThrow(); + + // restore + // eslint-disable-next-line @typescript-eslint/no-explicit-any + React.createElement = originalCreateElement; + }); +}); From e9e8311fc01f61145ac96ef1518637dc45dc1259 Mon Sep 17 00:00:00 2001 From: Dominik Mengelt Date: Wed, 4 Feb 2026 15:30:24 +0100 Subject: [PATCH 2/3] test(react): use createRoot API to avoid React 18 render/unmount deprecation warnings --- src/button-react/GooglePayButton.react19.test.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/button-react/GooglePayButton.react19.test.tsx b/src/button-react/GooglePayButton.react19.test.tsx index 90c2082..b2d217a 100644 --- a/src/button-react/GooglePayButton.react19.test.tsx +++ b/src/button-react/GooglePayButton.react19.test.tsx @@ -1,6 +1,6 @@ import GooglePayButton from './GooglePayButton'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import defaults from '../lib/__setup__/defaults'; describe('React 19 compatibility', () => { @@ -37,8 +37,9 @@ describe('React 19 compatibility', () => { // If mounting the component tries to read `element.ref`, the Proxy will throw expect(() => { - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + const root = createRoot(div); + root.render(); + root.unmount(); }).not.toThrow(); // restore From edbcdb3358dc64ea1fc830d584c363cc6fe8580f Mon Sep 17 00:00:00 2001 From: Dominik Mengelt Date: Wed, 4 Feb 2026 15:37:43 +0100 Subject: [PATCH 3/3] test(react): support environments without react-dom/client by falling back to legacy render API --- .../GooglePayButton.react19.test.tsx | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/button-react/GooglePayButton.react19.test.tsx b/src/button-react/GooglePayButton.react19.test.tsx index b2d217a..8d84ffe 100644 --- a/src/button-react/GooglePayButton.react19.test.tsx +++ b/src/button-react/GooglePayButton.react19.test.tsx @@ -1,6 +1,19 @@ import GooglePayButton from './GooglePayButton'; import React from 'react'; -import { createRoot } from 'react-dom/client'; +import ReactDOM from 'react-dom'; + +// Dynamically load React 18's createRoot API if available so this test works +// both with React 16 (CI) and React 18 (local environments). +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const createRootModule: any = (() => { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('react-dom/client'); + } catch (e) { + return null; + } +})(); + import defaults from '../lib/__setup__/defaults'; describe('React 19 compatibility', () => { @@ -37,9 +50,19 @@ describe('React 19 compatibility', () => { // If mounting the component tries to read `element.ref`, the Proxy will throw expect(() => { - const root = createRoot(div); - root.render(); - root.unmount(); + const createRootFn = createRootModule && createRootModule.createRoot; + if (createRootFn) { + const root = createRootFn(div); + root.render(); + root.unmount(); + } else { + // For older React versions used in CI (e.g., React 16), fall back to + // the legacy render/unmount APIs. + // eslint-disable-next-line react/no-deprecated + ReactDOM.render(, div); + // eslint-disable-next-line react/no-deprecated + ReactDOM.unmountComponentAtNode(div); + } }).not.toThrow(); // restore