From ec70782337acb566bdbc4206f912df48c0a5fa0d Mon Sep 17 00:00:00 2001 From: jwbrandon Date: Wed, 15 Jul 2026 15:01:25 -0400 Subject: [PATCH 1/5] fix(types): widen config types to match runtime passthrough The d.ts enumerated a closed set of config keys, so TypeScript rejected values the runtime passes through verbatim: custom keys in the config prop (e.g. _customAdServerData), and whitelisted top-level props the types never declared (advertising, floating, related, ...). It also drifted from the player (autostart only allowed 'viewable'). JWPlayerConfig is now Record so it can't fall out of sync with the player's config surface. Input positions (config, advertising, setup) also accept plain object, since interface-typed values lack the implicit index signature Record assignability requires. Declared props keep their strict types. --- src/jwplayer-react.d.ts | 100 ++++++++-------------------------------- test/types/consumer.tsx | 57 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 81 deletions(-) diff --git a/src/jwplayer-react.d.ts b/src/jwplayer-react.d.ts index fb96c66..bedb1b6 100644 --- a/src/jwplayer-react.d.ts +++ b/src/jwplayer-react.d.ts @@ -1,86 +1,13 @@ import * as React from 'react'; /** - * Example: {"2500":"High","1000":"Medium"} + * Player setup options, passed through verbatim to `jwplayer().setup()`. + * The player's config surface evolves independently of this package, so keys + * are intentionally left open rather than enumerated here. See + * https://docs.jwplayer.com/players/reference/player-configuration-reference + * for the supported options. */ -type QualityLabels = Record; - -type Stretching = 'uniform' | 'exactfit' | 'fill' | 'none'; - -/** - * Width in pixels or percentage - */ -type Width = number | string; - -interface AppearanceConfig { - aspectratio?: string; - controls?: boolean; - displaydescription?: boolean; - displayHeading?: boolean; - displayPlaybackLabel?: boolean; - displaytitle?: boolean; - height?: number; - horizontalVolumeSlider?: boolean; - nextUpDisplay?: boolean; - qualityLabels?: QualityLabels; - renderCaptionsNatively?: boolean; - stretching?: Stretching; - width?: Width; -} - -type AutoStart = 'viewable'; - -/** - * A positive value is an offset from the start of the video. - * A negative value is an offset from the end of the video. - * This property can be defined either as a number (-10) or a percentage as a string ("-2%") - */ -type NextUpOffset = string | number; - -interface BehaviorConfig { - aboutlink?: string; - abouttext?: string; - allowFullscreen?: boolean; - autostart?: AutoStart; - defaultBandwidthEstimate?: number; - generateSEOMetadata?: boolean; - liveSyncDuration?: number; - mute?: boolean; - nextupoffset?: NextUpOffset; - pipIcon?: string; - playbackRateControls?: boolean; - playbackRates?: number[]; - playlistIndex?: number; - repeat?: boolean; -} - -type MediaType = - | 'aac' | 'f4a' | 'f4v' | 'hls' | 'm3u' | 'm4v' | 'mov' | 'mp3' - | 'mp4' | 'mpeg' | 'oga' | 'ogg' | 'ogv' | 'vorbis' | 'webm'; - -interface MediaConfig { - file?: string; - description?: string; - image?: string; - mediaid?: string; - playlist?: string | object[]; - title?: string; - type?: MediaType; -} - -type Preload = 'metadata' | 'auto' | 'none'; - -interface RenderAndLoadingConfig { - base?: string; - flashplayer?: string; - hlsjsdefault?: boolean; - liveTimeout?: number; - loadAndParseHlsMetadata?: boolean; - preload?: Preload; -} - -export type JWPlayerConfig = - AppearanceConfig & BehaviorConfig & MediaConfig & RenderAndLoadingConfig; +export type JWPlayerConfig = Record; export type EventCallback = (...args: unknown[]) => void; @@ -94,7 +21,7 @@ export interface JWPlayerApi { once(event: string, callback: EventCallback): JWPlayerApi; off(event?: string, callback?: EventCallback): JWPlayerApi; remove(): void; - setup(config: Record): JWPlayerApi; + setup(config: JWPlayerConfig | object): JWPlayerApi; [member: string]: unknown; } @@ -115,7 +42,18 @@ export interface JWPlayerProps extends JWPlayerConfig { id?: string; /** Required unless a jwplayer library script is already loaded on the page */ library?: string; - config?: JWPlayerConfig; + /** + * The `| object` arm accepts interface-typed configs, which lack the + * implicit index signature TypeScript requires for Record assignability. + */ + config?: JWPlayerConfig | object; + /** + * Config merging is shallow, so a top-level advertising prop replaces the + * entire advertising block — both `config.advertising` and the player + * library/dashboard defaults. Include the full ad config here, not just + * the keys being changed. + */ + advertising?: JWPlayerConfig | object; /** * on props subscribe to player events; onAll fires for every event. * once props subscribe to the first firing only. diff --git a/test/types/consumer.tsx b/test/types/consumer.tsx index fe15456..d430388 100644 --- a/test/types/consumer.tsx +++ b/test/types/consumer.tsx @@ -6,6 +6,7 @@ */ import * as React from 'react'; import JWPlayer, { + JWPlayerConfig, JWPlayerInstance, JWPlayerProps, MountCallbackArguments, @@ -43,6 +44,62 @@ export const withInlineProps = ( /> ); +// generateConfig() spreads `config` verbatim into setup(), so custom keys the +// player config reference doesn't document must typecheck, including as an +// inline literal (excess-property checking would otherwise reject them). +export const withCustomConfigData = ( + +); + +// All whitelisted player config options are usable as top-level props (per the +// README), including ones with no dedicated declaration and custom ad keys. +export const withTopLevelConfigProps = ( + +); + +// Interface types get no implicit index signature, so `config` and +// `advertising` need their `| object` arm to accept interface-typed values. +interface AppPlayerConfig { + file: string; + autostart: boolean; +} +declare const appConfig: AppPlayerConfig; +export const withInterfaceTypedConfig = ( + { player.setup(appConfig); }} + /> +); + +// Declared props keep their strict types despite the open index signature. +// @ts-expect-error library must be a string +export const withBadLibrary = ; +// @ts-expect-error config must be an object +export const withBadConfig = ; +// @ts-expect-error advertising must be an object +export const withBadAdvertising = ; +// @ts-expect-error on props must be functions +export const withBadEventProp = ; + +export const customConfig: JWPlayerConfig = { + playlist: 'https://cdn.jwplayer.com/v2/media/abcd1234', + _customAdServerData: { segment: 'sports' }, +}; + // A ref resolves to the mounted instance, exposing the player API directly. const playerRef = React.createRef(); From 4f3bd8982ad66bafb10dd99aeaf90ebdc9029f79 Mon Sep 17 00:00:00 2001 From: jwbrandon Date: Wed, 15 Jul 2026 15:06:20 -0400 Subject: [PATCH 2/5] docs(types): link official config references in doc comments --- src/jwplayer-react.d.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/jwplayer-react.d.ts b/src/jwplayer-react.d.ts index bedb1b6..fd6edde 100644 --- a/src/jwplayer-react.d.ts +++ b/src/jwplayer-react.d.ts @@ -3,9 +3,9 @@ import * as React from 'react'; /** * Player setup options, passed through verbatim to `jwplayer().setup()`. * The player's config surface evolves independently of this package, so keys - * are intentionally left open rather than enumerated here. See - * https://docs.jwplayer.com/players/reference/player-configuration-reference - * for the supported options. + * are intentionally left open rather than enumerated here. See the official + * configuration reference for the supported options and their shapes: + * https://docs.jwplayer.com/players/reference/setup-options */ export type JWPlayerConfig = Record; @@ -51,7 +51,8 @@ export interface JWPlayerProps extends JWPlayerConfig { * Config merging is shallow, so a top-level advertising prop replaces the * entire advertising block — both `config.advertising` and the player * library/dashboard defaults. Include the full ad config here, not just - * the keys being changed. + * the keys being changed. See the official reference for its shape: + * https://docs.jwplayer.com/players/reference/advertising-config-ref */ advertising?: JWPlayerConfig | object; /** From 6b87a1dec5cbc8e1cb2abcdff655e0d7d4ac5207 Mon Sep 17 00:00:00 2001 From: jwbrandon Date: Wed, 15 Jul 2026 15:10:34 -0400 Subject: [PATCH 3/5] docs(types): link config section index from JWPlayerConfig --- src/jwplayer-react.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/jwplayer-react.d.ts b/src/jwplayer-react.d.ts index fd6edde..502158c 100644 --- a/src/jwplayer-react.d.ts +++ b/src/jwplayer-react.d.ts @@ -4,8 +4,11 @@ import * as React from 'react'; * Player setup options, passed through verbatim to `jwplayer().setup()`. * The player's config surface evolves independently of this package, so keys * are intentionally left open rather than enumerated here. See the official - * configuration reference for the supported options and their shapes: + * references for the supported options and their shapes — top-level options: * https://docs.jwplayer.com/players/reference/setup-options + * and every nested config section (advertising, captions, casting, drm, + * floating, playlists, related, sharing, skin, ...): + * https://docs.jwplayer.com/players/reference/configuration-options-getting-started */ export type JWPlayerConfig = Record; From 4671657a12d197244e6a17d1569b22e97427d25f Mon Sep 17 00:00:00 2001 From: jwbrandon Date: Wed, 15 Jul 2026 15:41:10 -0400 Subject: [PATCH 4/5] fix(types): reject top-level props the runtime whitelist drops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widening JWPlayerConfig gave JWPlayerProps an open index signature, so any top-level prop typechecked — including custom keys and typos the runtime whitelist silently drops, recreating the silent-vanishing confusion the config widening was meant to fix. Props now extend an enumerated mirror of the config-props.js whitelist (values still unknown, so no player-shape drift) and a unit test keeps the two lists in sync. React consumes the key prop before the component sees it, so key is omitted and only works via config. --- src/config-props.js | 2 + src/jwplayer-react.d.ts | 101 ++++++++++++++++++++++++++++++++++- test/types-whitelist.test.js | 20 +++++++ test/types/consumer.tsx | 10 +++- 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 test/types-whitelist.test.js diff --git a/src/config-props.js b/src/config-props.js index 19fc1a4..5182737 100644 --- a/src/config-props.js +++ b/src/config-props.js @@ -1,3 +1,5 @@ +// Mirrored by WhitelistedConfigKey in jwplayer-react.d.ts; a unit test keeps +// the two in sync. module.exports = new Set([ 'hlsjsProgressive', '__abSendDomainToFeeds', diff --git a/src/jwplayer-react.d.ts b/src/jwplayer-react.d.ts index 502158c..f8b5c24 100644 --- a/src/jwplayer-react.d.ts +++ b/src/jwplayer-react.d.ts @@ -39,7 +39,106 @@ export interface UnmountCallbackArguments { player: JWPlayerApi | null; } -export interface JWPlayerProps extends JWPlayerConfig { +/** + * The config options usable directly as props, mirroring the runtime + * whitelist in src/config-props.js (a unit test keeps the two in sync). + * The component silently drops top-level props outside that whitelist, so + * the props type stays closed to surface that at compile time — custom setup + * keys must ride in `config` instead. + * + * `key` is whitelisted at runtime but omitted here: React reserves the `key` + * prop and never forwards it, so a player license key only works via + * `config={{ key: ... }}`. + */ +type WhitelistedConfigKey = + | 'hlsjsProgressive' + | '__abSendDomainToFeeds' + | '_abZoomThumbnail' + | 'advertising' + | 'aboutlink' + | 'abouttext' + | 'aestoken' + | 'allowFullscreen' + | 'analytics' + | 'androidhls' + | 'aspectratio' + | 'autoPause' + | 'autostart' + | 'base' + | 'captions' + | 'cast' + | 'controls' + | 'defaultBandwidthEstimate' + | 'description' + | 'displaydescription' + | 'displayHeading' + | 'displayPlaybackLabel' + | 'displaytitle' + | 'drm' + | 'duration' + | 'enableDefaultCaptions' + | 'events' + | 'file' + | 'forceLocalizationDefaults' + | 'fwassetid' + | 'floating' + | 'ga' + | 'generateSEOMetadata' + | 'height' + | 'hlsjsConfig' + | 'hlsjsdefault' + | 'horizontalVolumeSlider' + | 'image' + | 'intl' + | 'listbar' + | 'liveSyncDuration' + | 'liveTimeout' + | 'localization' + | 'logo' + | 'mediaid' + | 'mute' + | 'nextUpDisplay' + | 'nextupoffset' + | 'pad' + | 'ph' + | 'pid' + | 'pipIcon' + | 'playbackRateControls' + | 'playbackRates' + | 'playlist' + | 'playlistIndex' + | 'plugins' + | 'preload' + | 'qualityLabel' + | 'qualityLabels' + | 'recommendations' + | 'related' + | 'renderCaptionsNatively' + | 'repeat' + | 'safarihlsjs' + | 'sdkplatform' + | 'selectedBitrate' + | 'setTimeEvents' + | 'skin' + | 'sharing' + | 'sources' + | 'stagevideo' + | 'streamtype' + | 'stretching' + | 'title' + | 'tracks' + | 'type' + | 'variations' + | 'volume' + | 'width' + | 'withCredentials' + | 'doNotTrack' + | 'doNotTrackCookies' + | 'images'; + +export type JWPlayerConfigProps = Partial>; + +export interface JWPlayerProps extends JWPlayerConfigProps { didMountCallback?: (args: MountCallbackArguments) => void; willUnmountCallback?: (args: UnmountCallbackArguments) => void; id?: string; diff --git a/test/types-whitelist.test.js b/test/types-whitelist.test.js new file mode 100644 index 0000000..7870862 --- /dev/null +++ b/test/types-whitelist.test.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const path = require('path'); +const configProps = require('../src/config-props'); + +describe('jwplayer-react.d.ts config prop whitelist', () => { + it('matches the runtime whitelist in config-props.js', () => { + const dts = fs.readFileSync(path.join(__dirname, '../src/jwplayer-react.d.ts'), 'utf8'); + const union = dts.match(/type WhitelistedConfigKey =([\s\S]*?);/); + + expect(union).not.toBeNull(); + + const declaredKeys = Array.from(union[1].matchAll(/'([^']+)'/g), (match) => match[1]); + + // React reserves the `key` prop and never forwards it to the component, + // so it is deliberately absent from the declared props. + const runtimeKeys = [...configProps].filter((key) => key !== 'key'); + + expect([...declaredKeys].sort()).toEqual(runtimeKeys.sort()); + }); +}); diff --git a/test/types/consumer.tsx b/test/types/consumer.tsx index d430388..82c5383 100644 --- a/test/types/consumer.tsx +++ b/test/types/consumer.tsx @@ -85,7 +85,15 @@ export const withInterfaceTypedConfig = ( /> ); -// Declared props keep their strict types despite the open index signature. +// The runtime silently drops top-level props outside the config-props.js +// whitelist, so the props type rejects them — custom keys must ride in +// `config` — and catches typo'd prop names. +// @ts-expect-error non-whitelisted top-level props are rejected +export const withCustomTopLevelProp = ; +// @ts-expect-error typo'd prop names are rejected +export const withTypoProp = ; + +// Declared props keep their strict types. // @ts-expect-error library must be a string export const withBadLibrary = ; // @ts-expect-error config must be an object From 3386f7d49fa4fb3bbf12aaa149484f0009182495 Mon Sep 17 00:00:00 2001 From: jwbrandon Date: Wed, 15 Jul 2026 16:02:07 -0400 Subject: [PATCH 5/5] feat(config): forward all non-component props to player setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config-props.js whitelist meant every new player option needed a package release before it worked as a top-level prop, and anything not yet listed was silently dropped — the confusion behind the custom ad data thread. generateConfig now forwards every prop except the component's own API (library, config, id, children, mount callbacks, and on*/once* event handlers), so the player decides which keys are meaningful and the wrapper can't drift out of sync. The props type reopens accordingly (extends JWPlayerConfig), and the whitelist mirror plus its sync test are gone. --- src/config-props.js | 89 ------------------------------ src/jwplayer-react.d.ts | 103 ++--------------------------------- src/util.js | 19 ++++++- test/jwplayer-react.test.js | 24 +++++++- test/types-whitelist.test.js | 20 ------- test/types/consumer.tsx | 21 +++---- 6 files changed, 55 insertions(+), 221 deletions(-) delete mode 100644 src/config-props.js delete mode 100644 test/types-whitelist.test.js diff --git a/src/config-props.js b/src/config-props.js deleted file mode 100644 index 5182737..0000000 --- a/src/config-props.js +++ /dev/null @@ -1,89 +0,0 @@ -// Mirrored by WhitelistedConfigKey in jwplayer-react.d.ts; a unit test keeps -// the two in sync. -module.exports = new Set([ - 'hlsjsProgressive', - '__abSendDomainToFeeds', - '_abZoomThumbnail', - 'advertising', - 'aboutlink', - 'abouttext', - 'aestoken', - 'allowFullscreen', - 'analytics', - 'androidhls', - 'aspectratio', - 'autoPause', - 'autostart', - 'base', - 'captions', - 'cast', - 'controls', - 'defaultBandwidthEstimate', - 'description', - 'displaydescription', - 'displayHeading', - 'displayPlaybackLabel', - 'displaytitle', - 'drm', - 'duration', - 'enableDefaultCaptions', - 'events', - 'file', - 'forceLocalizationDefaults', - 'fwassetid', - 'floating', - 'ga', - 'generateSEOMetadata', - 'height', - 'hlsjsConfig', - 'hlsjsdefault', - 'horizontalVolumeSlider', - 'image', - 'intl', - 'key', - 'listbar', - 'liveSyncDuration', - 'liveTimeout', - 'localization', - 'logo', - 'mediaid', - 'mute', - 'nextUpDisplay', - 'nextupoffset', - 'pad', - 'ph', - 'pid', - 'pipIcon', - 'playbackRateControls', - 'playbackRates', - 'playlist', - 'playlistIndex', - 'plugins', - 'preload', - 'qualityLabel', - 'qualityLabels', - 'recommendations', - 'related', - 'renderCaptionsNatively', - 'repeat', - 'safarihlsjs', - 'sdkplatform', - 'selectedBitrate', - 'setTimeEvents', - 'skin', - 'sharing', - 'sources', - 'stagevideo', - 'streamtype', - 'stretching', - 'title', - 'tracks', - 'type', - 'variations', - 'volume', - 'width', - 'withCredentials', - 'doNotTrack', - 'doNotTrackCookies', - 'images', -]); diff --git a/src/jwplayer-react.d.ts b/src/jwplayer-react.d.ts index f8b5c24..651fd1e 100644 --- a/src/jwplayer-react.d.ts +++ b/src/jwplayer-react.d.ts @@ -40,105 +40,14 @@ export interface UnmountCallbackArguments { } /** - * The config options usable directly as props, mirroring the runtime - * whitelist in src/config-props.js (a unit test keeps the two in sync). - * The component silently drops top-level props outside that whitelist, so - * the props type stays closed to surface that at compile time — custom setup - * keys must ride in `config` instead. + * Any prop that is not part of the component's own API (the declared props + * below and on/once handlers) is forwarded verbatim into + * `jwplayer().setup()`, so all player config options work as top-level props. * - * `key` is whitelisted at runtime but omitted here: React reserves the `key` - * prop and never forwards it, so a player license key only works via - * `config={{ key: ... }}`. + * Exception: React reserves the `key` prop and never forwards it, so a player + * license key only works via `config={{ key: ... }}`. */ -type WhitelistedConfigKey = - | 'hlsjsProgressive' - | '__abSendDomainToFeeds' - | '_abZoomThumbnail' - | 'advertising' - | 'aboutlink' - | 'abouttext' - | 'aestoken' - | 'allowFullscreen' - | 'analytics' - | 'androidhls' - | 'aspectratio' - | 'autoPause' - | 'autostart' - | 'base' - | 'captions' - | 'cast' - | 'controls' - | 'defaultBandwidthEstimate' - | 'description' - | 'displaydescription' - | 'displayHeading' - | 'displayPlaybackLabel' - | 'displaytitle' - | 'drm' - | 'duration' - | 'enableDefaultCaptions' - | 'events' - | 'file' - | 'forceLocalizationDefaults' - | 'fwassetid' - | 'floating' - | 'ga' - | 'generateSEOMetadata' - | 'height' - | 'hlsjsConfig' - | 'hlsjsdefault' - | 'horizontalVolumeSlider' - | 'image' - | 'intl' - | 'listbar' - | 'liveSyncDuration' - | 'liveTimeout' - | 'localization' - | 'logo' - | 'mediaid' - | 'mute' - | 'nextUpDisplay' - | 'nextupoffset' - | 'pad' - | 'ph' - | 'pid' - | 'pipIcon' - | 'playbackRateControls' - | 'playbackRates' - | 'playlist' - | 'playlistIndex' - | 'plugins' - | 'preload' - | 'qualityLabel' - | 'qualityLabels' - | 'recommendations' - | 'related' - | 'renderCaptionsNatively' - | 'repeat' - | 'safarihlsjs' - | 'sdkplatform' - | 'selectedBitrate' - | 'setTimeEvents' - | 'skin' - | 'sharing' - | 'sources' - | 'stagevideo' - | 'streamtype' - | 'stretching' - | 'title' - | 'tracks' - | 'type' - | 'variations' - | 'volume' - | 'width' - | 'withCredentials' - | 'doNotTrack' - | 'doNotTrackCookies' - | 'images'; - -export type JWPlayerConfigProps = Partial>; - -export interface JWPlayerProps extends JWPlayerConfigProps { +export interface JWPlayerProps extends JWPlayerConfig { didMountCallback?: (args: MountCallbackArguments) => void; willUnmountCallback?: (args: UnmountCallbackArguments) => void; id?: string; diff --git a/src/util.js b/src/util.js index b42a016..f552a5d 100644 --- a/src/util.js +++ b/src/util.js @@ -1,4 +1,16 @@ -import configProps from './config-props'; +import { ON_REGEX } from './const'; + +// Props that configure this component rather than the player. Everything else +// is forwarded verbatim into setup(), so the player — not this wrapper — +// decides which config keys are meaningful. +const componentProps = new Set([ + 'children', + 'config', + 'didMountCallback', + 'id', + 'library', + 'willUnmountCallback', +]); let idIndex = -1; export function generateUniqueId() { @@ -55,7 +67,10 @@ export function generateConfig(props) { const config = {}; Object.keys(props).forEach((key) => { - if (configProps.has(key)) config[key] = props[key]; + if (componentProps.has(key)) return; + // on* event handler props (once* also matches ON_REGEX) are not config + if (key.match(ON_REGEX)) return; + config[key] = props[key]; }); return { ...props.config, ...config, isReactComponent: true }; diff --git a/test/jwplayer-react.test.js b/test/jwplayer-react.test.js index e90795a..a78c314 100644 --- a/test/jwplayer-react.test.js +++ b/test/jwplayer-react.test.js @@ -91,10 +91,11 @@ describe('methods', () => { }); describe('generateConfig', () => { - it('generates a setup config from props without assigning unsupported properties', async () => { - const { instance } = await createMountedComponent({ unsupportedProperty: 3, floating: {}, width: 500 }); + it('forwards all non-component props into the setup config, including custom keys', async () => { + const { instance } = await createMountedComponent({ _customAdServerData: { segment: 'sports' }, floating: {}, width: 500 }); const setupConfig = window.jwplayer(instance.id).setup.mock.calls[0][0]; const expectedSetupConfig = { + _customAdServerData: { segment: 'sports' }, floating: {}, isReactComponent: true, playlist: 'https://cdn.jwplayer.com/v2/media/1g8jjku3', @@ -103,11 +104,28 @@ describe('methods', () => { expect(setupConfig).toEqual(expectedSetupConfig); }); + it('excludes component props and event handlers from the setup config', async () => { + const { instance } = await createMountedComponent({ + didMountCallback: noop, + willUnmountCallback: noop, + onPlay: noop, + onceReady: noop, + width: 500 + }); + const setupConfig = window.jwplayer(instance.id).setup.mock.calls[0][0]; + expect(setupConfig).toEqual({ + isReactComponent: true, + playlist: 'https://cdn.jwplayer.com/v2/media/1g8jjku3', + width: 500 + }); + }); + it('Props overwrite matching base config properties', async () => { const baseConfig = { width: 400, height: 300 }; - const { instance } = await createMountedComponent({ config: baseConfig, unsupportedProperty: 3, floating: {}, width: 500 }); + const { instance } = await createMountedComponent({ config: baseConfig, _customAdServerData: { segment: 'sports' }, floating: {}, width: 500 }); const setupConfig = window.jwplayer(instance.id).setup.mock.calls[0][0]; const expectedSetupConfig = { + _customAdServerData: { segment: 'sports' }, floating: {}, isReactComponent: true, playlist: 'https://cdn.jwplayer.com/v2/media/1g8jjku3', diff --git a/test/types-whitelist.test.js b/test/types-whitelist.test.js deleted file mode 100644 index 7870862..0000000 --- a/test/types-whitelist.test.js +++ /dev/null @@ -1,20 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const configProps = require('../src/config-props'); - -describe('jwplayer-react.d.ts config prop whitelist', () => { - it('matches the runtime whitelist in config-props.js', () => { - const dts = fs.readFileSync(path.join(__dirname, '../src/jwplayer-react.d.ts'), 'utf8'); - const union = dts.match(/type WhitelistedConfigKey =([\s\S]*?);/); - - expect(union).not.toBeNull(); - - const declaredKeys = Array.from(union[1].matchAll(/'([^']+)'/g), (match) => match[1]); - - // React reserves the `key` prop and never forwards it to the component, - // so it is deliberately absent from the declared props. - const runtimeKeys = [...configProps].filter((key) => key !== 'key'); - - expect([...declaredKeys].sort()).toEqual(runtimeKeys.sort()); - }); -}); diff --git a/test/types/consumer.tsx b/test/types/consumer.tsx index 82c5383..cea23aa 100644 --- a/test/types/consumer.tsx +++ b/test/types/consumer.tsx @@ -55,8 +55,9 @@ export const withCustomConfigData = ( /> ); -// All whitelisted player config options are usable as top-level props (per the -// README), including ones with no dedicated declaration and custom ad keys. +// Any non-component prop is forwarded into setup(), so all player config +// options — and custom keys like _customAdServerData — work as top-level +// props with no dedicated declaration. export const withTopLevelConfigProps = ( ); -// The runtime silently drops top-level props outside the config-props.js -// whitelist, so the props type rejects them — custom keys must ride in -// `config` — and catches typo'd prop names. -// @ts-expect-error non-whitelisted top-level props are rejected -export const withCustomTopLevelProp = ; -// @ts-expect-error typo'd prop names are rejected -export const withTypoProp = ; +export const withCustomTopLevelProp = ( + +); -// Declared props keep their strict types. +// Declared props keep their strict types despite the open index signature. // @ts-expect-error library must be a string export const withBadLibrary = ; // @ts-expect-error config must be an object