diff --git a/.changeset/astro-buttons-accept-one.md b/.changeset/astro-buttons-accept-one.md new file mode 100644 index 00000000000..32a19550f64 --- /dev/null +++ b/.changeset/astro-buttons-accept-one.md @@ -0,0 +1,5 @@ +--- +'@clerk/astro': patch +--- + +Allow unstyled button components to accept a single React element passed as an array. Fixes a misleading "multiple children" error that could appear when a custom button child crossed a server/client boundary and arrived as a one-item array. diff --git a/packages/astro/src/react/__tests__/SignInButton.test.tsx b/packages/astro/src/react/__tests__/SignInButton.test.tsx new file mode 100644 index 00000000000..ac375a08e1f --- /dev/null +++ b/packages/astro/src/react/__tests__/SignInButton.test.tsx @@ -0,0 +1,100 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import React from 'react'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type * as UtilsModule from '../utils'; + +const mockRedirectToSignIn = vi.fn(); +const originalError = console.error; + +const mockClerk = { + redirectToSignIn: mockRedirectToSignIn, +} as any; + +vi.mock('../utils', async importActual => { + const actual = await importActual(); + return { + ...actual, + withClerk: (Component: any) => (props: any) => { + return ( + + ); + }, + }; +}); + +const { SignInButton } = await import('../SignInButton'); + +describe('', () => { + beforeAll(() => { + console.error = vi.fn(); + }); + + afterAll(() => { + console.error = originalError; + }); + + beforeEach(() => { + mockRedirectToSignIn.mockReset(); + }); + + afterEach(() => { + cleanup(); + }); + + it('renders the default button and calls clerk.redirectToSignIn when clicked', async () => { + render(); + const btn = screen.getByText('Sign in'); + await userEvent.click(btn); + expect(mockRedirectToSignIn).toHaveBeenCalled(); + }); + + it('renders passed button and calls both click handlers', async () => { + const handler = vi.fn(); + + render( + + + , + ); + + const btn = screen.getByText('custom button'); + await userEvent.click(btn); + + expect(handler).toHaveBeenCalled(); + expect(mockRedirectToSignIn).toHaveBeenCalled(); + }); + + it('accepts a single child passed as an array', async () => { + const handler = vi.fn(); + + render( + + {[ + , + ]} + , + ); + + const btn = screen.getByText('custom button'); + await userEvent.click(btn); + + expect(handler).toHaveBeenCalled(); + expect(mockRedirectToSignIn).toHaveBeenCalled(); + }); +}); diff --git a/packages/astro/src/react/utils.tsx b/packages/astro/src/react/utils.tsx index f1da84324b4..90a4ba41bc7 100644 --- a/packages/astro/src/react/utils.tsx +++ b/packages/astro/src/react/utils.tsx @@ -62,6 +62,12 @@ export const assertSingleChild = try { return React.Children.only(children); } catch { + const childArray = React.Children.toArray(children); + + if (childArray.length === 1 && React.isValidElement(childArray[0])) { + return childArray[0]; + } + return `You've passed multiple children components to <${name}/>. You can only pass a single child component or text.`; } };