From f4bfb0488a96fc98f61562c27a4e81a219fca396 Mon Sep 17 00:00:00 2001 From: juno-goh Date: Fri, 2 Jan 2026 14:26:40 +0800 Subject: [PATCH] feat: add CSP nonce support for scrollbar measurements --- src/getScrollBarSize.tsx | 17 +++-- tests/getScrollBarSize.test.ts | 133 +++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 5 deletions(-) diff --git a/src/getScrollBarSize.tsx b/src/getScrollBarSize.tsx index 77092c06..0ed2f77f 100644 --- a/src/getScrollBarSize.tsx +++ b/src/getScrollBarSize.tsx @@ -10,7 +10,10 @@ type ExtendCSSStyleDeclaration = CSSStyleDeclaration & { let cached: ScrollBarSize; -function measureScrollbarSize(ele?: HTMLElement): ScrollBarSize { +function measureScrollbarSize( + ele?: HTMLElement, + nonce?: string, +): ScrollBarSize { const randomId = `rc-scrollbar-measure-${Math.random() .toString(36) .substring(7)}`; @@ -53,6 +56,7 @@ ${widthStyle} ${heightStyle} }`, randomId, + nonce ? { csp: { nonce } } : undefined, ); } catch (e) { // Can't wrap, just log error @@ -86,18 +90,21 @@ ${heightStyle} }; } -export default function getScrollBarSize(fresh?: boolean): number { +export default function getScrollBarSize( + fresh?: boolean, + nonce?: string, +): number { if (typeof document === 'undefined') { return 0; } if (fresh || cached === undefined) { - cached = measureScrollbarSize(); + cached = measureScrollbarSize(undefined, nonce); } return cached.width; } -export function getTargetScrollBarSize(target: HTMLElement) { +export function getTargetScrollBarSize(target: HTMLElement, nonce?: string) { if ( typeof document === 'undefined' || !target || @@ -106,5 +113,5 @@ export function getTargetScrollBarSize(target: HTMLElement) { return { width: 0, height: 0 }; } - return measureScrollbarSize(target); + return measureScrollbarSize(target, nonce); } diff --git a/tests/getScrollBarSize.test.ts b/tests/getScrollBarSize.test.ts index 9c03b945..535441d0 100644 --- a/tests/getScrollBarSize.test.ts +++ b/tests/getScrollBarSize.test.ts @@ -2,6 +2,7 @@ import getScrollBarSize, { getTargetScrollBarSize, } from '../src/getScrollBarSize'; import { spyElementPrototypes } from '../src/test/domHook'; +import * as dynamicCSS from '../src/Dom/dynamicCSS'; const DEFAULT_SIZE = 16; @@ -48,4 +49,136 @@ describe('getScrollBarSize', () => { }); }); }); + + describe('nonce support', () => { + let updateCSSSpy: jest.SpyInstance; + + beforeEach(() => { + updateCSSSpy = jest.spyOn(dynamicCSS, 'updateCSS'); + }); + + afterEach(() => { + updateCSSSpy.mockRestore(); + }); + + it('should pass nonce to updateCSS when provided in getScrollBarSize', () => { + const testNonce = 'test-nonce-123'; + + // We need to test with getTargetScrollBarSize since getScrollBarSize + // without an element doesn't trigger updateCSS + const mockElement = document.createElement('div'); + document.body.appendChild(mockElement); + + // Mock getComputedStyle to return webkit scrollbar dimensions + const originalGetComputedStyle = window.getComputedStyle; + window.getComputedStyle = jest + .fn() + .mockImplementation((element, pseudoElement) => { + if (pseudoElement === '::-webkit-scrollbar') { + return { + width: '10px', + height: '10px', + }; + } + return { + scrollbarColor: '', + scrollbarWidth: '', + overflow: 'scroll', + }; + }) as any; + + // This will trigger updateCSS with webkit styles + getTargetScrollBarSize(mockElement, testNonce); + + // Restore original + window.getComputedStyle = originalGetComputedStyle; + document.body.removeChild(mockElement); + + // Check if updateCSS was called with nonce + const updateCSSCalls = updateCSSSpy.mock.calls; + const callWithNonce = updateCSSCalls.find( + call => call[2]?.csp?.nonce === testNonce, + ); + + expect(callWithNonce).toBeDefined(); + }); + + it('should pass nonce to updateCSS when provided in getTargetScrollBarSize', () => { + const testNonce = 'target-nonce-456'; + const mockElement = document.createElement('div'); + document.body.appendChild(mockElement); + + // Mock getComputedStyle to return webkit scrollbar dimensions + const originalGetComputedStyle = window.getComputedStyle; + window.getComputedStyle = jest + .fn() + .mockImplementation((element, pseudoElement) => { + if (pseudoElement === '::-webkit-scrollbar') { + return { + width: '12px', + height: '12px', + }; + } + return { + scrollbarColor: '', + scrollbarWidth: '', + overflow: 'scroll', + }; + }) as any; + + getTargetScrollBarSize(mockElement, testNonce); + + // Restore original + window.getComputedStyle = originalGetComputedStyle; + document.body.removeChild(mockElement); + + // Check if updateCSS was called with nonce + const updateCSSCalls = updateCSSSpy.mock.calls; + const callWithNonce = updateCSSCalls.find( + call => call[2]?.csp?.nonce === testNonce, + ); + + expect(callWithNonce).toBeDefined(); + }); + + it('should not pass nonce to updateCSS when not provided', () => { + const mockElement = document.createElement('div'); + document.body.appendChild(mockElement); + + // Mock getComputedStyle to return webkit scrollbar dimensions + const originalGetComputedStyle = window.getComputedStyle; + window.getComputedStyle = jest + .fn() + .mockImplementation((element, pseudoElement) => { + if (pseudoElement === '::-webkit-scrollbar') { + return { + width: '8px', + height: '8px', + }; + } + return { + scrollbarColor: '', + scrollbarWidth: '', + overflow: 'scroll', + }; + }) as any; + + // Clear previous calls + updateCSSSpy.mockClear(); + + // Call without nonce + getTargetScrollBarSize(mockElement); + + // Restore original + window.getComputedStyle = originalGetComputedStyle; + document.body.removeChild(mockElement); + + // Check if updateCSS was called without nonce (undefined as third parameter) + const updateCSSCalls = updateCSSSpy.mock.calls; + if (updateCSSCalls.length > 0) { + const lastCall = updateCSSCalls[updateCSSCalls.length - 1]; + expect(lastCall[2]).toBeUndefined(); + } + }); + }); });