diff --git a/packages/components/src/placeholder/README.md b/packages/components/src/placeholder/README.md
index bc59b593b463a..f260ab15378f0 100644
--- a/packages/components/src/placeholder/README.md
+++ b/packages/components/src/placeholder/README.md
@@ -39,7 +39,7 @@ Changes placeholder children layout from flex-row to flex-column.
Title of the placeholder.
-- Required: Yes
+- Required: No
### `notices`: `ReactNode`
diff --git a/packages/components/src/placeholder/test/index.js b/packages/components/src/placeholder/test/index.js
deleted file mode 100644
index e2a6090764cb2..0000000000000
--- a/packages/components/src/placeholder/test/index.js
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * External dependencies
- */
-import { shallow } from 'enzyme';
-
-/**
- * WordPress dependencies
- */
-import { more } from '@wordpress/icons';
-import { useResizeObserver } from '@wordpress/compose';
-
-/**
- * Internal dependencies
- */
-import Placeholder from '../';
-
-jest.mock( '@wordpress/compose', () => {
- return {
- ...jest.requireActual( '@wordpress/compose' ),
- useResizeObserver: jest.fn( () => [] ),
- };
-} );
-
-describe( 'Placeholder', () => {
- beforeEach( () => {
- useResizeObserver.mockReturnValue( [
-
,
- { width: 320 },
- ] );
- } );
-
- describe( 'basic rendering', () => {
- it( 'should by default render label section and fieldset.', () => {
- const placeholder = shallow( );
- const placeholderLabel = placeholder.find(
- '.components-placeholder__label'
- );
- const placeholderInstructions = placeholder.find(
- '.components-placeholder__instructions'
- );
- const placeholderFieldset = placeholder.find(
- '.components-placeholder__fieldset'
- );
-
- expect( placeholder.hasClass( 'components-placeholder' ) ).toBe(
- true
- );
- // Test for empty label.
- expect( placeholderLabel.exists() ).toBe( true );
- expect( placeholderLabel.find( 'Dashicon' ).exists() ).toBe(
- false
- );
- // Test for non existant instructions.
- expect( placeholderInstructions.exists() ).toBe( false );
- // Test for empty fieldset.
- expect( placeholderFieldset.exists() ).toBe( true );
- } );
-
- it( 'should render an Icon in the label section', () => {
- const placeholder = shallow( );
- const placeholderLabel = placeholder.find(
- '.components-placeholder__label'
- );
-
- expect( placeholderLabel.exists() ).toBe( true );
- expect( placeholderLabel.find( 'Icon' ).exists() ).toBe( true );
- } );
-
- it( 'should render a label section', () => {
- const label = 'WordPress';
- const placeholder = shallow( );
- const placeholderLabel = placeholder.find(
- '.components-placeholder__label'
- );
- const child = placeholderLabel.childAt( 1 );
-
- expect( child.text() ).toBe( label );
- } );
-
- it( 'should display an instructions element', () => {
- const element = Instructions
;
- const placeholder = shallow(
-
- );
- const placeholderInstructions = placeholder.find(
- '.components-placeholder__instructions'
- );
- const child = placeholderInstructions.childAt( 0 );
-
- expect( placeholderInstructions.exists() ).toBe( true );
- expect( child.matchesElement( element ) ).toBe( true );
- } );
-
- it( 'should display a fieldset from the children property', () => {
- const element = Fieldset
;
- const placeholder = shallow( );
- const placeholderFieldset = placeholder.find(
- 'fieldset.components-placeholder__fieldset'
- );
- const child = placeholderFieldset.childAt( 0 );
-
- expect( placeholderFieldset.exists() ).toBe( true );
- expect( child.matchesElement( element ) ).toBe( true );
- } );
-
- it( 'should display a legend if instructions are passed', () => {
- const element = Fieldset
;
- const instructions = 'Choose an option.';
- const placeholder = shallow(
-
- );
- const placeholderLegend = placeholder.find(
- 'legend.components-placeholder__instructions'
- );
-
- expect( placeholderLegend.exists() ).toBe( true );
- expect( placeholderLegend.text() ).toEqual( instructions );
- } );
-
- it( 'should add an additional className to the top container', () => {
- const placeholder = shallow(
-
- );
- expect( placeholder.hasClass( 'wp-placeholder' ) ).toBe( true );
- } );
-
- it( 'should add additional props to the top level container', () => {
- const placeholder = shallow( );
- expect( placeholder.prop( 'test' ) ).toBe( 'test' );
- } );
- } );
-
- describe( 'resize aware', () => {
- it( 'should not assign modifier class in first-pass `null` width from `useResizeObserver`', () => {
- useResizeObserver.mockReturnValue( [
- ,
- { width: 480 },
- ] );
-
- const placeholder = shallow( );
-
- expect( placeholder.hasClass( 'is-large' ) ).toBe( true );
- expect( placeholder.hasClass( 'is-medium' ) ).toBe( false );
- expect( placeholder.hasClass( 'is-small' ) ).toBe( false );
- } );
-
- it( 'should assign modifier class', () => {
- useResizeObserver.mockReturnValue( [
- ,
- { width: null },
- ] );
-
- const placeholder = shallow( );
-
- expect( placeholder.hasClass( 'is-large' ) ).toBe( false );
- expect( placeholder.hasClass( 'is-medium' ) ).toBe( false );
- expect( placeholder.hasClass( 'is-small' ) ).toBe( false );
- } );
- } );
-} );
diff --git a/packages/components/src/placeholder/test/index.tsx b/packages/components/src/placeholder/test/index.tsx
new file mode 100644
index 0000000000000..9e47d4d016a7f
--- /dev/null
+++ b/packages/components/src/placeholder/test/index.tsx
@@ -0,0 +1,174 @@
+/**
+ * External dependencies
+ */
+import { render, screen, within } from '@testing-library/react';
+
+/**
+ * WordPress dependencies
+ */
+import { useResizeObserver } from '@wordpress/compose';
+import { SVG, Path } from '@wordpress/primitives';
+
+/**
+ * Internal dependencies
+ */
+import BasePlaceholder from '../';
+import type { WordPressComponentProps } from '../../ui/context';
+import type { PlaceholderProps } from '../types';
+
+jest.mock( '@wordpress/compose', () => {
+ return {
+ ...jest.requireActual( '@wordpress/compose' ),
+ useResizeObserver: jest.fn( () => [] ),
+ };
+} );
+
+/**
+ * Test icon that can be queried by `getByTestId`
+ */
+const testIcon = (
+
+);
+
+const Placeholder = (
+ props: Omit<
+ WordPressComponentProps< PlaceholderProps< unknown >, 'div', false >,
+ 'ref'
+ >
+) => ;
+
+const getPlaceholder = () => screen.getByTestId( 'placeholder' );
+
+describe( 'Placeholder', () => {
+ beforeEach( () => {
+ // @ts-ignore
+ useResizeObserver.mockReturnValue( [
+ ,
+ { width: 320 },
+ ] );
+ } );
+
+ describe( 'basic rendering', () => {
+ it( 'should by default render label section and fieldset.', () => {
+ render( );
+ const placeholder = getPlaceholder();
+
+ expect( placeholder ).toHaveClass( 'components-placeholder' );
+
+ // Test for empty label. When the label is empty, the only way to
+ // query the div is with `querySelector`.
+ const label = placeholder.querySelector(
+ '.components-placeholder__label'
+ );
+ expect( label ).toBeInTheDocument();
+ expect( label ).toBeEmptyDOMElement();
+
+ // Test for non existent instructions. When the instructions is
+ // empty, the only way to query the div is with `querySelector`.
+ const placeholderInstructions = placeholder.querySelector(
+ '.components-placeholder__instructions'
+ );
+ expect( placeholderInstructions ).not.toBeInTheDocument();
+
+ // Test for empty fieldset.
+ const placeholderFieldset =
+ within( placeholder ).getByRole( 'group' );
+ expect( placeholderFieldset ).toBeInTheDocument();
+ expect( placeholderFieldset ).toBeEmptyDOMElement();
+ } );
+
+ it( 'should render an Icon in the label section', () => {
+ render( );
+
+ const placeholder = getPlaceholder();
+ const icon = within( placeholder ).getByTestId( 'icon' );
+ expect( icon.parentNode ).toHaveClass(
+ 'components-placeholder__label'
+ );
+ expect( icon ).toBeInTheDocument();
+ } );
+
+ it( 'should render a label section', () => {
+ const label = 'WordPress';
+ render( );
+ const placeholderLabel = screen.getByText( label );
+
+ expect( placeholderLabel ).toHaveClass(
+ 'components-placeholder__label'
+ );
+ expect( placeholderLabel ).toBeInTheDocument();
+ } );
+
+ it( 'should display a fieldset from the children property', () => {
+ const content = 'Fieldset';
+ render( { content } );
+ const placeholderFieldset = screen.getByRole( 'group' );
+
+ expect( placeholderFieldset ).toBeInTheDocument();
+ expect( placeholderFieldset ).toHaveTextContent( content );
+ } );
+
+ it( 'should display a legend if instructions are passed', () => {
+ const instructions = 'Choose an option.';
+ render(
+
+ Fieldset
+
+ );
+ const captionedFieldset = screen.getByRole( 'group', {
+ name: instructions,
+ } );
+
+ expect( captionedFieldset ).toBeInTheDocument();
+ } );
+
+ it( 'should add an additional className to the top container', () => {
+ render( );
+ const placeholder = getPlaceholder();
+
+ expect( placeholder ).toHaveClass( 'components-placeholder' );
+ expect( placeholder ).toHaveClass( 'wp-placeholder' );
+ } );
+
+ it( 'should add additional props to the top level container', () => {
+ render( );
+ const placeholder = getPlaceholder();
+
+ expect( placeholder ).toHaveAttribute( 'data-test', 'test' );
+ } );
+ } );
+
+ describe( 'resize aware', () => {
+ it( 'should not assign modifier class in first-pass `null` width from `useResizeObserver`', () => {
+ // @ts-ignore
+ useResizeObserver.mockReturnValue( [
+ ,
+ { width: 480 },
+ ] );
+
+ render( );
+ const placeholder = getPlaceholder();
+
+ expect( placeholder ).toHaveClass( 'is-large' );
+ expect( placeholder ).not.toHaveClass( 'is-medium' );
+ expect( placeholder ).not.toHaveClass( 'is-small' );
+ } );
+
+ it( 'should assign modifier class', () => {
+ // @ts-ignore
+ useResizeObserver.mockReturnValue( [
+ ,
+ { width: null },
+ ] );
+
+ render( );
+ const placeholder = getPlaceholder();
+
+ expect( placeholder ).not.toHaveClass( 'is-large' );
+ expect( placeholder ).not.toHaveClass( 'is-medium' );
+ expect( placeholder ).not.toHaveClass( 'is-small' );
+ } );
+ } );
+} );