diff --git a/packages/snaps-sdk/src/internals/structs.test.ts b/packages/snaps-sdk/src/internals/structs.test.ts index e5942b7744..7a714eccc7 100644 --- a/packages/snaps-sdk/src/internals/structs.test.ts +++ b/packages/snaps-sdk/src/internals/structs.test.ts @@ -8,6 +8,7 @@ import { any, } from '@metamask/superstruct'; +import { nullUnion } from './jsx'; import { enumValue, literal, @@ -18,6 +19,7 @@ import { import type { BoxElement } from '../jsx'; import { Footer, Icon, Text, Button, Box } from '../jsx'; import { + BoldStruct, BoxStruct, FieldStruct, FooterStruct, @@ -86,6 +88,33 @@ describe('typedUnion', () => { ); }); + it('validates when nested in a union including primitives', () => { + const stringUnion = nullUnion([ + string(), + typedUnion([TextStruct, BoldStruct]), + ]); + + // @ts-expect-error Invalid props. + const result = validate(Text({}), stringUnion); + + expect(result[0]?.message).toBe( + 'Expected the value to satisfy a union of `string | union`, but received: [object Object]', + ); + }); + + it('validates when nested in a union including non-typed objects', () => { + const nonTypedObjectUnion = nullUnion([ + typedUnion([TextStruct, BoldStruct]), + object({ type: literal('bar') }), + ]); + + const result = validate({ type: 'abc' }, nonTypedObjectUnion); + + expect(result[0]?.message).toBe( + 'Expected the value to satisfy a union of `union | object`, but received: [object Object]', + ); + }); + it('validates refined elements', () => { const refinedUnionStruct = typedUnion([BoxStruct, FooterStruct]); const result = validate( diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index 23c636cc59..e4ccf4a36e 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -615,11 +615,15 @@ export const FieldStruct: Describe = element('Field', { */ export const BoldStruct: Describe = element('Bold', { children: children([ - string(), - // eslint-disable-next-line @typescript-eslint/no-use-before-define - lazy(() => ItalicStruct) as unknown as Struct< - SnapElement - >, + selectiveUnion((value) => { + if (typeof value === 'string') { + return string(); + } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return ItalicStruct as unknown as Struct< + SnapElement + >; + }), ]), }); @@ -628,10 +632,13 @@ export const BoldStruct: Describe = element('Bold', { */ export const ItalicStruct: Describe = element('Italic', { children: children([ - string(), - lazy(() => BoldStruct) as unknown as Struct< - SnapElement - >, + selectiveUnion((value) => { + if (typeof value === 'string') { + return string(); + } + + return BoldStruct as unknown as Struct>; + }), ]), }); @@ -774,11 +781,18 @@ export const HeadingStruct: Describe = element('Heading', { export const LinkStruct: Describe = element('Link', { href: string(), children: children([ - FormattingStruct, - string(), - IconStruct, - ImageStruct, - AddressStruct, + selectiveUnion((value) => { + if (typeof value === 'string') { + return string(); + } + + return typedUnion([ + FormattingStruct, + IconStruct, + ImageStruct, + AddressStruct, + ]); + }), ]), }); @@ -896,13 +910,15 @@ export const TooltipStruct: Describe = element('Tooltip', { */ export const BannerStruct: Describe = element('Banner', { children: children([ - TextStruct, - LinkStruct, - IconStruct, - ButtonStruct, - BoldStruct, - ItalicStruct, - SkeletonStruct, + typedUnion([ + TextStruct, + LinkStruct, + IconStruct, + ButtonStruct, + BoldStruct, + ItalicStruct, + SkeletonStruct, + ]), ]), title: string(), severity: union([