From 72a7cda7b2e96f0b13b3ad1e48ff46e73f99bb37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 15 Jan 2026 16:48:17 +0100 Subject: [PATCH 1/6] basic impl --- src/__tests__/render-hook.test.tsx | 79 +++++++++++++++++++ src/__tests__/render.test.tsx | 49 +++++++++++- .../__tests__/validate-options.test.ts | 39 +++++++++ src/helpers/validate-options.ts | 15 ++++ src/render-hook.tsx | 5 +- src/render.tsx | 4 +- src/user-event/setup/__tests__/setup.test.ts | 51 ++++++++++++ src/user-event/setup/setup.ts | 6 +- 8 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/helpers/__tests__/validate-options.test.ts create mode 100644 src/helpers/validate-options.ts create mode 100644 src/user-event/setup/__tests__/setup.test.ts diff --git a/src/__tests__/render-hook.test.tsx b/src/__tests__/render-hook.test.tsx index ca1f3f828..ea0d27d7f 100644 --- a/src/__tests__/render-hook.test.tsx +++ b/src/__tests__/render-hook.test.tsx @@ -3,13 +3,20 @@ import * as React from 'react'; import { Text } from 'react-native'; import { act, renderHook } from '..'; +import { _console } from '../helpers/logger'; import { excludeConsoleMessage } from '../test-utils/console'; // eslint-disable-next-line no-console const originalConsoleError = console.error; + +beforeEach(() => { + jest.spyOn(_console, 'warn').mockImplementation(() => {}); +}); + afterEach(() => { // eslint-disable-next-line no-console console.error = originalConsoleError; + jest.restoreAllMocks(); }); function useSuspendingHook(promise: Promise) { @@ -289,3 +296,75 @@ test('handles custom hooks with complex logic', async () => { }); expect(result.current.count).toBe(4); }); + +test('does not warn when no options are passed', async () => { + function useTestHook() { + return React.useState(0); + } + + await renderHook(useTestHook); + + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('does not warn when only valid options are passed', async () => { + const Context = React.createContext('default'); + + function useTestHook() { + return React.useContext(Context); + } + + function Wrapper({ children }: { children: ReactNode }) { + return {children}; + } + + await renderHook(useTestHook, { wrapper: Wrapper, initialProps: undefined }); + + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('warns for unknown options but still passes valid options to render', async () => { + const Context = React.createContext('default'); + + function useTestHook() { + return React.useContext(Context); + } + + function Wrapper({ children }: { children: ReactNode }) { + return {children}; + } + + const { result } = await renderHook(useTestHook, { + wrapper: Wrapper, + unknownOption: 'value', + } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(result.current).toEqual('provided'); +}); + +test('warns when unknown option is passed', async () => { + function useTestHook() { + return React.useState(0); + } + + await renderHook(useTestHook, { unknownOption: 'value' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( + 'Unknown option(s) passed to renderHook: unknownOption', + ); +}); + +test('warns when multiple unknown options are passed', async () => { + function useTestHook() { + return React.useState(0); + } + + await renderHook(useTestHook, { unknown1: 'value1', unknown2: 'value2' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( + 'Unknown option(s) passed to renderHook: unknown1, unknown2', + ); +}); diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 08dc4c6b0..a1850659e 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Text, View } from 'react-native'; import { render, screen } from '..'; -import { logger } from '../helpers/logger'; +import { _console, logger } from '../helpers/logger'; test('renders a simple component', async () => { const TestComponent = () => ( @@ -17,6 +17,53 @@ test('renders a simple component', async () => { expect(screen.getByText('Hello World')).toBeOnTheScreen(); }); +beforeEach(() => { + jest.spyOn(_console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('does not warn when no options are passed', async () => { + const TestComponent = () => Test; + + await render(); + + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('does not warn when only valid options are passed', async () => { + const TestComponent = () => Test; + const Wrapper = ({ children }: { children: React.ReactNode }) => {children}; + + await render(, { wrapper: Wrapper, createNodeMock: jest.fn() }); + + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('warns when unknown option is passed', async () => { + const TestComponent = () => Test; + + await render(, { unknownOption: 'value' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( + 'Unknown option(s) passed to render: unknownOption', + ); +}); + +test('warns when multiple unknown options are passed', async () => { + const TestComponent = () => Test; + + await render(, { unknown1: 'value1', unknown2: 'value2' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( + 'Unknown option(s) passed to render: unknown1, unknown2', + ); +}); + describe('render options', () => { test('renders component with wrapper option', async () => { const TestComponent = () => Inner Content; diff --git a/src/helpers/__tests__/validate-options.test.ts b/src/helpers/__tests__/validate-options.test.ts new file mode 100644 index 000000000..9ba789276 --- /dev/null +++ b/src/helpers/__tests__/validate-options.test.ts @@ -0,0 +1,39 @@ +import { _console } from '../logger'; +import { validateOptions } from '../validate-options'; + +beforeEach(() => { + jest.spyOn(_console, 'warn').mockImplementation(() => {}); +}); + +test('does not warn when rest object is empty', () => { + validateOptions('testFunction', {}); + + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('warns when unknown option is passed', () => { + validateOptions('testFunction', { unknownOption: 'value' }); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(` + " ▲ Unknown option(s) passed to testFunction: unknownOption + " + `); +}); + +test('warns when multiple unknown options are passed', () => { + validateOptions('testFunction', { option1: 'value1', option2: 'value2', option3: 'value3' }); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(` + " ▲ Unknown option(s) passed to testFunction: option1, option2, option3 + " + `); +}); + +test('warns with correct function name', () => { + validateOptions('render', { invalid: true }); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain('render'); +}); diff --git a/src/helpers/validate-options.ts b/src/helpers/validate-options.ts new file mode 100644 index 000000000..60485275a --- /dev/null +++ b/src/helpers/validate-options.ts @@ -0,0 +1,15 @@ +import { logger } from './logger'; + +/** + * Validates that no unknown options are passed to a function. + * Logs a warning if unknown options are found. + * + * @param functionName - The name of the function being called (for error messages) + * @param restOptions - The rest object from destructuring that contains unknown options + */ +export function validateOptions(functionName: string, restOptions: Record): void { + const unknownKeys = Object.keys(restOptions); + if (unknownKeys.length > 0) { + logger.warn(`Unknown option(s) passed to ${functionName}: ${unknownKeys.join(', ')}`); + } +} diff --git a/src/render-hook.tsx b/src/render-hook.tsx index 564530c35..2cd81362c 100644 --- a/src/render-hook.tsx +++ b/src/render-hook.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import { validateOptions } from './helpers/validate-options'; import { render } from './render'; import type { RefObject } from './types'; @@ -38,7 +39,9 @@ export async function renderHook( return null; } - const { initialProps, ...renderOptions } = options ?? {}; + const { initialProps, wrapper, ...rest } = options ?? {}; + validateOptions('renderHook', rest); + const renderOptions = wrapper ? { wrapper } : {}; const { rerender: rerenderComponent, unmount } = await render( // @ts-expect-error since option can be undefined, initialProps can be undefined when it shouldn't be , diff --git a/src/render.tsx b/src/render.tsx index a98d519a0..ea4bf24b4 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -13,6 +13,7 @@ import { getConfig } from './config'; import type { DebugOptions } from './helpers/debug'; import { debug } from './helpers/debug'; import { HOST_TEXT_NAMES } from './helpers/host-component-names'; +import { validateOptions } from './helpers/validate-options'; import { setRenderResult } from './screen'; import { getQueriesForElement } from './within'; @@ -34,7 +35,8 @@ export type RenderResult = Awaited>; * to assert on the output. */ export async function render(element: React.ReactElement, options: RenderOptions = {}) { - const { wrapper: Wrapper, createNodeMock } = options || {}; + const { wrapper: Wrapper, createNodeMock, ...rest } = options || {}; + validateOptions('render', rest); const rendererOptions: RootOptions = { textComponentTypes: HOST_TEXT_NAMES, diff --git a/src/user-event/setup/__tests__/setup.test.ts b/src/user-event/setup/__tests__/setup.test.ts new file mode 100644 index 000000000..72e2ea5a8 --- /dev/null +++ b/src/user-event/setup/__tests__/setup.test.ts @@ -0,0 +1,51 @@ +import { _console } from '../../../helpers/logger'; +import { setup } from '../setup'; + +beforeEach(() => { + jest.spyOn(_console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('creates instance when no options are passed', () => { + const instance = setup(); + + expect(instance).toBeDefined(); + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('creates instance with valid options', () => { + const instance = setup({ delay: 100, advanceTimers: jest.fn() }); + + expect(instance).toBeDefined(); + expect(instance.config.delay).toBe(100); + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('creates instance correctly when unknown options are passed', () => { + const instance = setup({ delay: 50, unknownOption: 'value' } as any); + + expect(instance).toBeDefined(); + expect(instance.config.delay).toBe(50); + expect(_console.warn).toHaveBeenCalledTimes(1); +}); + +test('warns when unknown option is passed', () => { + setup({ unknownOption: 'value' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( + 'Unknown option(s) passed to userEvent.setup: unknownOption', + ); +}); + +test('warns when multiple unknown options are passed', () => { + setup({ delay: 100, unknown1: 'value1', unknown2: 'value2' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( + 'Unknown option(s) passed to userEvent.setup: unknown1, unknown2', + ); +}); diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index dfec333fc..79f9bfcc8 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -1,6 +1,7 @@ import type { HostElement } from 'test-renderer'; import { jestFakeTimersAreEnabled } from '../../helpers/timers'; +import { validateOptions } from '../../helpers/validate-options'; import { wrapAsync } from '../../helpers/wrap-async'; import { clear } from '../clear'; import { paste } from '../paste'; @@ -74,9 +75,12 @@ export interface UserEventConfig { } function createConfig(options?: UserEventSetupOptions): UserEventConfig { + const { delay, advanceTimers, ...rest } = options ?? {}; + validateOptions('userEvent.setup', rest); return { ...defaultOptions, - ...options, + ...(delay !== undefined && { delay }), + ...(advanceTimers !== undefined && { advanceTimers }), }; } From f8b703f040221f3aadf86ed7672269d3c62db0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 15 Jan 2026 16:56:23 +0100 Subject: [PATCH 2/6] callsite --- .../__tests__/validate-options.test.ts | 41 ++++++++++++------- src/helpers/validate-options.ts | 18 +++++++- src/render-hook.tsx | 2 +- src/render.tsx | 2 +- src/user-event/setup/setup.ts | 13 ++++-- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/helpers/__tests__/validate-options.test.ts b/src/helpers/__tests__/validate-options.test.ts index 9ba789276..ae9efa1e5 100644 --- a/src/helpers/__tests__/validate-options.test.ts +++ b/src/helpers/__tests__/validate-options.test.ts @@ -1,39 +1,52 @@ import { _console } from '../logger'; import { validateOptions } from '../validate-options'; +function testFunction() { + // Test function for callsite +} + beforeEach(() => { jest.spyOn(_console, 'warn').mockImplementation(() => {}); }); test('does not warn when rest object is empty', () => { - validateOptions('testFunction', {}); + validateOptions('testFunction', {}, testFunction); expect(_console.warn).not.toHaveBeenCalled(); }); test('warns when unknown option is passed', () => { - validateOptions('testFunction', { unknownOption: 'value' }); + validateOptions('testFunction', { unknownOption: 'value' }, testFunction); expect(_console.warn).toHaveBeenCalledTimes(1); - expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(` - " ▲ Unknown option(s) passed to testFunction: unknownOption - " - `); + const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; + expect(warningMessage).toContain('Unknown option(s) passed to testFunction: unknownOption'); + expect(warningMessage).toContain('validate-options.test.ts'); }); test('warns when multiple unknown options are passed', () => { - validateOptions('testFunction', { option1: 'value1', option2: 'value2', option3: 'value3' }); + validateOptions( + 'testFunction', + { option1: 'value1', option2: 'value2', option3: 'value3' }, + testFunction, + ); expect(_console.warn).toHaveBeenCalledTimes(1); - expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(` - " ▲ Unknown option(s) passed to testFunction: option1, option2, option3 - " - `); + const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; + expect(warningMessage).toContain( + 'Unknown option(s) passed to testFunction: option1, option2, option3', + ); + expect(warningMessage).toContain('validate-options.test.ts'); }); -test('warns with correct function name', () => { - validateOptions('render', { invalid: true }); +test('warns with correct function name and includes stack trace', () => { + function render() { + // Test function + } + validateOptions('render', { invalid: true }, render); expect(_console.warn).toHaveBeenCalledTimes(1); - expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain('render'); + const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; + expect(warningMessage).toContain('render'); + expect(warningMessage).toContain('validate-options.test.ts'); }); diff --git a/src/helpers/validate-options.ts b/src/helpers/validate-options.ts index 60485275a..5de46f7f6 100644 --- a/src/helpers/validate-options.ts +++ b/src/helpers/validate-options.ts @@ -1,3 +1,4 @@ +import { ErrorWithStack } from './errors'; import { logger } from './logger'; /** @@ -6,10 +7,23 @@ import { logger } from './logger'; * * @param functionName - The name of the function being called (for error messages) * @param restOptions - The rest object from destructuring that contains unknown options + * @param _callsite - The function where the validation is called from (unused, kept for API compatibility) */ -export function validateOptions(functionName: string, restOptions: Record): void { +export function validateOptions( + functionName: string, + restOptions: Record, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + _callsite: Function, +): void { const unknownKeys = Object.keys(restOptions); if (unknownKeys.length > 0) { - logger.warn(`Unknown option(s) passed to ${functionName}: ${unknownKeys.join(', ')}`); + // Pass validateOptions as callsite so the stack trace shows where the function (e.g., render) was called from + const stackTraceError = new ErrorWithStack('STACK_TRACE_ERROR', validateOptions); + const stackTrace = stackTraceError.stack + ? `\n\n${stackTraceError.stack.split('\n').slice(1).join('\n')}` + : ''; + logger.warn( + `Unknown option(s) passed to ${functionName}: ${unknownKeys.join(', ')}${stackTrace}`, + ); } } diff --git a/src/render-hook.tsx b/src/render-hook.tsx index 2cd81362c..8a7928b03 100644 --- a/src/render-hook.tsx +++ b/src/render-hook.tsx @@ -40,7 +40,7 @@ export async function renderHook( } const { initialProps, wrapper, ...rest } = options ?? {}; - validateOptions('renderHook', rest); + validateOptions('renderHook', rest, renderHook); const renderOptions = wrapper ? { wrapper } : {}; const { rerender: rerenderComponent, unmount } = await render( // @ts-expect-error since option can be undefined, initialProps can be undefined when it shouldn't be diff --git a/src/render.tsx b/src/render.tsx index ea4bf24b4..6bd777e56 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -36,7 +36,7 @@ export type RenderResult = Awaited>; */ export async function render(element: React.ReactElement, options: RenderOptions = {}) { const { wrapper: Wrapper, createNodeMock, ...rest } = options || {}; - validateOptions('render', rest); + validateOptions('render', rest, render); const rendererOptions: RootOptions = { textComponentTypes: HOST_TEXT_NAMES, diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index 79f9bfcc8..e48b931b3 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -58,7 +58,7 @@ const defaultOptions: Required = { * @returns UserEvent instance */ export function setup(options?: UserEventSetupOptions) { - const config = createConfig(options); + const config = createConfig(options, setup); const instance = createInstance(config); return instance; } @@ -74,9 +74,14 @@ export interface UserEventConfig { advanceTimers: (delay: number) => Promise | void; } -function createConfig(options?: UserEventSetupOptions): UserEventConfig { - const { delay, advanceTimers, ...rest } = options ?? {}; - validateOptions('userEvent.setup', rest); +function createConfig( + options: UserEventSetupOptions = {}, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + callsite: Function, +): UserEventConfig { + const { delay, advanceTimers, ...rest } = options; + validateOptions('userEvent.setup', rest, callsite); + return { ...defaultOptions, ...(delay !== undefined && { delay }), From b2a79d4a0aae445e57582e59876178e96d5949be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 15 Jan 2026 16:58:54 +0100 Subject: [PATCH 3/6] configure --- src/__tests__/config.test.ts | 48 ++++++++++++++++++++++++++++++++++++ src/config.ts | 23 +++++++++++------ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index fa18b9be8..c346af841 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -1,7 +1,13 @@ import { configure, getConfig, resetToDefaults } from '../config'; +import { _console } from '../helpers/logger'; beforeEach(() => { resetToDefaults(); + jest.spyOn(_console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); }); test('getConfig() returns existing configuration', () => { @@ -46,3 +52,45 @@ test('configure handles alias option defaultHidden', () => { configure({ defaultHidden: true }); expect(getConfig().defaultIncludeHiddenElements).toEqual(true); }); + +test('does not warn when no options are passed', () => { + configure({}); + + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('does not warn when only valid options are passed', () => { + configure({ + asyncUtilTimeout: 2000, + defaultIncludeHiddenElements: true, + defaultDebugOptions: { message: 'test' }, + defaultHidden: false, + }); + + expect(_console.warn).not.toHaveBeenCalled(); +}); + +test('warns when unknown option is passed', () => { + configure({ unknownOption: 'value' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; + expect(warningMessage).toContain('Unknown option(s) passed to configure: unknownOption'); + expect(warningMessage).toContain('config.test.ts'); +}); + +test('warns when multiple unknown options are passed', () => { + configure({ asyncUtilTimeout: 1000, unknown1: 'value1', unknown2: 'value2' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; + expect(warningMessage).toContain('Unknown option(s) passed to configure: unknown1, unknown2'); + expect(warningMessage).toContain('config.test.ts'); +}); + +test('still configures correctly when unknown options are passed', () => { + configure({ asyncUtilTimeout: 3000, unknownOption: 'value' } as any); + + expect(_console.warn).toHaveBeenCalledTimes(1); + expect(getConfig().asyncUtilTimeout).toBe(3000); +}); diff --git a/src/config.ts b/src/config.ts index 121e33bc4..b6becd284 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,5 @@ import type { DebugOptions } from './helpers/debug'; +import { validateOptions } from './helpers/validate-options'; /** * Global configuration options for React Native Testing Library. @@ -31,17 +32,25 @@ let config = { ...defaultConfig }; * Configure global options for React Native Testing Library. */ export function configure(options: Partial) { - const { defaultHidden, ...restOptions } = options; + const { + defaultHidden, + asyncUtilTimeout, + defaultIncludeHiddenElements, + defaultDebugOptions, + ...rest + } = options; + + validateOptions('configure', rest, configure); - const defaultIncludeHiddenElements = - restOptions.defaultIncludeHiddenElements ?? - defaultHidden ?? - config.defaultIncludeHiddenElements; + const resolvedDefaultIncludeHiddenElements = + defaultIncludeHiddenElements ?? defaultHidden ?? config.defaultIncludeHiddenElements; config = { ...config, - ...restOptions, - defaultIncludeHiddenElements, + ...(asyncUtilTimeout !== undefined && { asyncUtilTimeout }), + ...(defaultIncludeHiddenElements !== undefined && { defaultIncludeHiddenElements }), + ...(defaultDebugOptions !== undefined && { defaultDebugOptions }), + defaultIncludeHiddenElements: resolvedDefaultIncludeHiddenElements, }; } From feeae76c677d512a98afeecc267cd6cd213c273f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 15 Jan 2026 17:02:42 +0100 Subject: [PATCH 4/6] tweak tests --- src/__tests__/config.test.ts | 16 ---------- src/__tests__/render-hook.test.tsx | 33 -------------------- src/__tests__/render.test.tsx | 11 ------- src/user-event/setup/__tests__/setup.test.ts | 17 ---------- 4 files changed, 77 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index c346af841..6949f3a6a 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -78,19 +78,3 @@ test('warns when unknown option is passed', () => { expect(warningMessage).toContain('Unknown option(s) passed to configure: unknownOption'); expect(warningMessage).toContain('config.test.ts'); }); - -test('warns when multiple unknown options are passed', () => { - configure({ asyncUtilTimeout: 1000, unknown1: 'value1', unknown2: 'value2' } as any); - - expect(_console.warn).toHaveBeenCalledTimes(1); - const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; - expect(warningMessage).toContain('Unknown option(s) passed to configure: unknown1, unknown2'); - expect(warningMessage).toContain('config.test.ts'); -}); - -test('still configures correctly when unknown options are passed', () => { - configure({ asyncUtilTimeout: 3000, unknownOption: 'value' } as any); - - expect(_console.warn).toHaveBeenCalledTimes(1); - expect(getConfig().asyncUtilTimeout).toBe(3000); -}); diff --git a/src/__tests__/render-hook.test.tsx b/src/__tests__/render-hook.test.tsx index ea0d27d7f..6a926cc13 100644 --- a/src/__tests__/render-hook.test.tsx +++ b/src/__tests__/render-hook.test.tsx @@ -323,26 +323,6 @@ test('does not warn when only valid options are passed', async () => { expect(_console.warn).not.toHaveBeenCalled(); }); -test('warns for unknown options but still passes valid options to render', async () => { - const Context = React.createContext('default'); - - function useTestHook() { - return React.useContext(Context); - } - - function Wrapper({ children }: { children: ReactNode }) { - return {children}; - } - - const { result } = await renderHook(useTestHook, { - wrapper: Wrapper, - unknownOption: 'value', - } as any); - - expect(_console.warn).toHaveBeenCalledTimes(1); - expect(result.current).toEqual('provided'); -}); - test('warns when unknown option is passed', async () => { function useTestHook() { return React.useState(0); @@ -355,16 +335,3 @@ test('warns when unknown option is passed', async () => { 'Unknown option(s) passed to renderHook: unknownOption', ); }); - -test('warns when multiple unknown options are passed', async () => { - function useTestHook() { - return React.useState(0); - } - - await renderHook(useTestHook, { unknown1: 'value1', unknown2: 'value2' } as any); - - expect(_console.warn).toHaveBeenCalledTimes(1); - expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( - 'Unknown option(s) passed to renderHook: unknown1, unknown2', - ); -}); diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index a1850659e..6587f7d3e 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -53,17 +53,6 @@ test('warns when unknown option is passed', async () => { ); }); -test('warns when multiple unknown options are passed', async () => { - const TestComponent = () => Test; - - await render(, { unknown1: 'value1', unknown2: 'value2' } as any); - - expect(_console.warn).toHaveBeenCalledTimes(1); - expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( - 'Unknown option(s) passed to render: unknown1, unknown2', - ); -}); - describe('render options', () => { test('renders component with wrapper option', async () => { const TestComponent = () => Inner Content; diff --git a/src/user-event/setup/__tests__/setup.test.ts b/src/user-event/setup/__tests__/setup.test.ts index 72e2ea5a8..1bbfa38a1 100644 --- a/src/user-event/setup/__tests__/setup.test.ts +++ b/src/user-event/setup/__tests__/setup.test.ts @@ -24,14 +24,6 @@ test('creates instance with valid options', () => { expect(_console.warn).not.toHaveBeenCalled(); }); -test('creates instance correctly when unknown options are passed', () => { - const instance = setup({ delay: 50, unknownOption: 'value' } as any); - - expect(instance).toBeDefined(); - expect(instance.config.delay).toBe(50); - expect(_console.warn).toHaveBeenCalledTimes(1); -}); - test('warns when unknown option is passed', () => { setup({ unknownOption: 'value' } as any); @@ -40,12 +32,3 @@ test('warns when unknown option is passed', () => { 'Unknown option(s) passed to userEvent.setup: unknownOption', ); }); - -test('warns when multiple unknown options are passed', () => { - setup({ delay: 100, unknown1: 'value1', unknown2: 'value2' } as any); - - expect(_console.warn).toHaveBeenCalledTimes(1); - expect(jest.mocked(_console.warn).mock.calls[0][0]).toContain( - 'Unknown option(s) passed to userEvent.setup: unknown1, unknown2', - ); -}); From bb51212defce05b8b5820aba7c98dacda8e06226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 15 Jan 2026 17:19:59 +0100 Subject: [PATCH 5/6] tweaks --- .../__tests__/validate-options.test.ts | 22 ++++++++++++------- src/helpers/validate-options.ts | 16 ++++++++------ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/helpers/__tests__/validate-options.test.ts b/src/helpers/__tests__/validate-options.test.ts index ae9efa1e5..8fd26f160 100644 --- a/src/helpers/__tests__/validate-options.test.ts +++ b/src/helpers/__tests__/validate-options.test.ts @@ -16,7 +16,10 @@ test('does not warn when rest object is empty', () => { }); test('warns when unknown option is passed', () => { - validateOptions('testFunction', { unknownOption: 'value' }, testFunction); + function testFunctionWithCall() { + validateOptions('testFunction', { unknownOption: 'value' }, testFunctionWithCall); + } + testFunctionWithCall(); expect(_console.warn).toHaveBeenCalledTimes(1); const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; @@ -25,11 +28,14 @@ test('warns when unknown option is passed', () => { }); test('warns when multiple unknown options are passed', () => { - validateOptions( - 'testFunction', - { option1: 'value1', option2: 'value2', option3: 'value3' }, - testFunction, - ); + function testFunctionWithCall() { + validateOptions( + 'testFunction', + { option1: 'value1', option2: 'value2', option3: 'value3' }, + testFunctionWithCall, + ); + } + testFunctionWithCall(); expect(_console.warn).toHaveBeenCalledTimes(1); const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; @@ -41,9 +47,9 @@ test('warns when multiple unknown options are passed', () => { test('warns with correct function name and includes stack trace', () => { function render() { - // Test function + validateOptions('render', { invalid: true }, render); } - validateOptions('render', { invalid: true }, render); + render(); expect(_console.warn).toHaveBeenCalledTimes(1); const warningMessage = jest.mocked(_console.warn).mock.calls[0][0]; diff --git a/src/helpers/validate-options.ts b/src/helpers/validate-options.ts index 5de46f7f6..2748d7311 100644 --- a/src/helpers/validate-options.ts +++ b/src/helpers/validate-options.ts @@ -7,21 +7,23 @@ import { logger } from './logger'; * * @param functionName - The name of the function being called (for error messages) * @param restOptions - The rest object from destructuring that contains unknown options - * @param _callsite - The function where the validation is called from (unused, kept for API compatibility) + * @param callsite - The function where the validation is called from (e.g., render, renderHook) */ export function validateOptions( functionName: string, restOptions: Record, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - _callsite: Function, + callsite: Function, ): void { const unknownKeys = Object.keys(restOptions); if (unknownKeys.length > 0) { - // Pass validateOptions as callsite so the stack trace shows where the function (e.g., render) was called from - const stackTraceError = new ErrorWithStack('STACK_TRACE_ERROR', validateOptions); - const stackTrace = stackTraceError.stack - ? `\n\n${stackTraceError.stack.split('\n').slice(1).join('\n')}` - : ''; + // Pass the callsite function (e.g., render) to remove it from stack + // This leaves only where the user called the function from (e.g., test file) + const stackTraceError = new ErrorWithStack('STACK_TRACE_ERROR', callsite); + const stackLines = stackTraceError.stack ? stackTraceError.stack.split('\n') : []; + // Skip the first line (Error: STACK_TRACE_ERROR) to show the actual call sites + // The remaining lines show where the user called the function from + const stackTrace = stackLines.length > 1 ? `\n\n${stackLines.slice(1).join('\n')}` : ''; logger.warn( `Unknown option(s) passed to ${functionName}: ${unknownKeys.join(', ')}${stackTrace}`, ); From 2fa65d72aeaf937b13d1b7a52cc19bea4f991401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 15 Jan 2026 17:45:20 +0100 Subject: [PATCH 6/6] . --- src/config.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/config.ts b/src/config.ts index b6becd284..b910b40d8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -33,10 +33,10 @@ let config = { ...defaultConfig }; */ export function configure(options: Partial) { const { - defaultHidden, asyncUtilTimeout, - defaultIncludeHiddenElements, defaultDebugOptions, + defaultHidden, + defaultIncludeHiddenElements, ...rest } = options; @@ -47,9 +47,8 @@ export function configure(options: Partial) { config = { ...config, - ...(asyncUtilTimeout !== undefined && { asyncUtilTimeout }), - ...(defaultIncludeHiddenElements !== undefined && { defaultIncludeHiddenElements }), - ...(defaultDebugOptions !== undefined && { defaultDebugOptions }), + asyncUtilTimeout: asyncUtilTimeout ?? config.asyncUtilTimeout, + defaultDebugOptions, defaultIncludeHiddenElements: resolvedDefaultIncludeHiddenElements, }; }