diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 173dd3b007ec9a..a27e802b7462ab 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -4,15 +4,16 @@
### Bug Fix
-- `FontSizePicker`: Ensure that fluid font size presets appear correctly in the UI controls ([#44791](https://github.com/WordPress/gutenberg/pull/44791))
+- `FontSizePicker`: Ensure that fluid font size presets appear correctly in the UI controls ([#44791](https://github.com/WordPress/gutenberg/pull/44791)).
### Documentation
-- `VisuallyHidden`: Add some notes on best practices around stacking contexts when using this component ([#44867](https://github.com/WordPress/gutenberg/pull/44867))
+- `VisuallyHidden`: Add some notes on best practices around stacking contexts when using this component ([#44867](https://github.com/WordPress/gutenberg/pull/44867)).
### Internal
- `Modal`: Convert to TypeScript ([#42949](https://github.com/WordPress/gutenberg/pull/42949)).
+- `Navigator`: refactor unit tests to TypeScript and to `user-event` ([#44970](https://github.com/WordPress/gutenberg/pull/44970)).
- `withFilters`: Refactor away from `_.without()` ([#44980](https://github.com/WordPress/gutenberg/pull/44980/)).
## 21.2.0 (2022-10-05)
diff --git a/packages/components/src/navigator/test/index.js b/packages/components/src/navigator/test/index.js
deleted file mode 100644
index 7374ede686e815..00000000000000
--- a/packages/components/src/navigator/test/index.js
+++ /dev/null
@@ -1,472 +0,0 @@
-/**
- * External dependencies
- */
-import { render, screen, fireEvent } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-/**
- * WordPress dependencies
- */
-import { useState } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import {
- NavigatorProvider,
- NavigatorScreen,
- NavigatorButton,
- NavigatorBackButton,
-} from '../';
-
-jest.mock( 'framer-motion', () => {
- const actual = jest.requireActual( 'framer-motion' );
- return {
- __esModule: true,
- ...actual,
- AnimatePresence: ( { children } ) =>
{ children }
,
- motion: {
- ...actual.motion,
- div: require( 'react' ).forwardRef( ( { children }, ref ) => (
- { children }
- ) ),
- },
- };
-} );
-
-const INVALID_HTML_ATTRIBUTE = {
- raw: ' "\'><=invalid_path',
- escaped: " "'><=invalid_path",
-};
-
-const PATHS = {
- HOME: '/',
- CHILD: '/child',
- NESTED: '/child/nested',
- INVALID_HTML_ATTRIBUTE: INVALID_HTML_ATTRIBUTE.raw,
- NOT_FOUND: '/not-found',
-};
-
-function CustomNavigatorButton( { path, onClick, ...props } ) {
- return (
- {
- // Used to spy on the values passed to `navigator.goTo`.
- onClick?.( { type: 'goTo', path } );
- } }
- path={ path }
- { ...props }
- />
- );
-}
-
-function CustomNavigatorButtonWithFocusRestoration( {
- path,
- onClick,
- ...props
-} ) {
- return (
- {
- // Used to spy on the values passed to `navigator.goTo`.
- onClick?.( { type: 'goTo', path } );
- } }
- path={ path }
- { ...props }
- />
- );
-}
-
-function CustomNavigatorBackButton( { onClick, ...props } ) {
- return (
- {
- // Used to spy on the values passed to `navigator.goBack`.
- onClick?.( { type: 'goBack' } );
- } }
- { ...props }
- />
- );
-}
-
-const MyNavigation = ( {
- initialPath = PATHS.HOME,
- onNavigatorButtonClick,
-} ) => {
- const [ inputValue, setInputValue ] = useState( '' );
- return (
-
-
- This is the home screen.
-
- Navigate to non-existing screen.
-
-
- Navigate to child screen.
-
-
- Navigate to screen with an invalid HTML value as a path.
-
-
-
-
- This is the child screen.
-
- Navigate to nested screen.
-
-
- Go back
-
-
-
- {
- setInputValue( e.target.value );
- } }
- value={ inputValue }
- />
-
-
-
- This is the nested screen.
-
- Go back
-
-
-
-
- This is the screen with an invalid HTML value as a path.
-
- Go back
-
-
-
- { /* A `NavigatorScreen` with `path={ PATHS.NOT_FOUND }` is purposefully not included. */ }
-
- );
-};
-
-const getNavigationScreenByText = ( text, { throwIfNotFound = true } = {} ) => {
- const fnName = throwIfNotFound ? 'getByText' : 'queryByText';
- return screen[ fnName ]( text );
-};
-const getHomeScreen = ( { throwIfNotFound } = {} ) =>
- getNavigationScreenByText( 'This is the home screen.', {
- throwIfNotFound,
- } );
-const getChildScreen = ( { throwIfNotFound } = {} ) =>
- getNavigationScreenByText( 'This is the child screen.', {
- throwIfNotFound,
- } );
-const getNestedScreen = ( { throwIfNotFound } = {} ) =>
- getNavigationScreenByText( 'This is the nested screen.', {
- throwIfNotFound,
- } );
-const getInvalidHTMLPathScreen = ( { throwIfNotFound } = {} ) =>
- getNavigationScreenByText(
- 'This is the screen with an invalid HTML value as a path.',
- {
- throwIfNotFound,
- }
- );
-
-const getNavigationButtonByText = ( text, { throwIfNotFound = true } = {} ) => {
- const fnName = throwIfNotFound ? 'getByRole' : 'queryByRole';
- return screen[ fnName ]( 'button', { name: text } );
-};
-const getToNonExistingScreenButton = ( { throwIfNotFound } = {} ) =>
- getNavigationButtonByText( 'Navigate to non-existing screen.', {
- throwIfNotFound,
- } );
-const getToChildScreenButton = ( { throwIfNotFound } = {} ) =>
- getNavigationButtonByText( 'Navigate to child screen.', {
- throwIfNotFound,
- } );
-const getToNestedScreenButton = ( { throwIfNotFound } = {} ) =>
- getNavigationButtonByText( 'Navigate to nested screen.', {
- throwIfNotFound,
- } );
-const getToInvalidHTMLPathScreenButton = ( { throwIfNotFound } = {} ) =>
- getNavigationButtonByText(
- 'Navigate to screen with an invalid HTML value as a path.',
- {
- throwIfNotFound,
- }
- );
-const getBackButton = ( { throwIfNotFound } = {} ) =>
- getNavigationButtonByText( 'Go back', {
- throwIfNotFound,
- } );
-
-describe( 'Navigator', () => {
- const originalGetClientRects = window.Element.prototype.getClientRects;
-
- // `getClientRects` needs to be mocked so that `isVisible` from the `@wordpress/dom`
- // `focusable` module can pass, in a JSDOM env where the DOM elements have no width/height.
- const mockedGetClientRects = jest.fn( () => [
- {
- x: 0,
- y: 0,
- width: 100,
- height: 100,
- },
- ] );
-
- beforeAll( () => {
- window.Element.prototype.getClientRects =
- jest.fn( mockedGetClientRects );
- } );
-
- afterAll( () => {
- window.Element.prototype.getClientRects = originalGetClientRects;
- } );
-
- it( 'should render', () => {
- render( );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect(
- getChildScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect(
- getNestedScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- } );
-
- it( 'should show a different screen on the first render depending on the value of `initialPath`', () => {
- render( );
-
- expect(
- getHomeScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect( getChildScreen() ).toBeInTheDocument();
- expect(
- getNestedScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- } );
-
- it( 'should ignore changes to `initialPath` after the first render', () => {
- const { rerender } = render( );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect(
- getChildScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect(
- getNestedScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
-
- rerender( );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect(
- getChildScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect(
- getNestedScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- } );
-
- it( 'should not rended anything if the `initialPath` does not match any available screen', () => {
- render( );
-
- expect(
- getHomeScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect(
- getChildScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect(
- getNestedScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- } );
-
- it( 'should navigate across screens', () => {
- const spy = jest.fn();
-
- render( );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect( getToChildScreenButton() ).toBeInTheDocument();
-
- // Navigate to child screen.
- fireEvent.click( getToChildScreenButton() );
-
- expect( getChildScreen() ).toBeInTheDocument();
- expect( getBackButton() ).toBeInTheDocument();
-
- // Navigate back to home screen.
- fireEvent.click( getBackButton() );
- expect( getHomeScreen() ).toBeInTheDocument();
- expect( getToChildScreenButton() ).toBeInTheDocument();
-
- // Navigate again to child screen.
- fireEvent.click( getToChildScreenButton() );
-
- expect( getChildScreen() ).toBeInTheDocument();
- expect( getToNestedScreenButton() ).toBeInTheDocument();
-
- // Navigate to nested screen.
- fireEvent.click( getToNestedScreenButton() );
-
- expect( getNestedScreen() ).toBeInTheDocument();
- expect( getBackButton() ).toBeInTheDocument();
-
- // Navigate back to child screen.
- fireEvent.click( getBackButton() );
-
- expect( getChildScreen() ).toBeInTheDocument();
- expect( getToNestedScreenButton() ).toBeInTheDocument();
-
- // Navigate back to home screen.
- fireEvent.click( getBackButton() );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect( getToChildScreenButton() ).toBeInTheDocument();
-
- // Check the values passed to `navigator.goTo()`.
- expect( spy ).toHaveBeenCalledTimes( 6 );
- expect( spy ).toHaveBeenNthCalledWith( 1, {
- path: PATHS.CHILD,
- type: 'goTo',
- } );
- expect( spy ).toHaveBeenNthCalledWith( 2, {
- type: 'goBack',
- } );
- expect( spy ).toHaveBeenNthCalledWith( 3, {
- path: PATHS.CHILD,
- type: 'goTo',
- } );
- expect( spy ).toHaveBeenNthCalledWith( 4, {
- path: PATHS.NESTED,
- type: 'goTo',
- } );
- expect( spy ).toHaveBeenNthCalledWith( 5, {
- type: 'goBack',
- } );
- expect( spy ).toHaveBeenNthCalledWith( 6, {
- type: 'goBack',
- } );
- } );
-
- it( 'should not rended anything if the path does not match any available screen', () => {
- const spy = jest.fn();
-
- render( );
-
- expect( getToNonExistingScreenButton() ).toBeInTheDocument();
-
- // Attempt to navigate to non-existing screen. No screens get rendered.
- fireEvent.click( getToNonExistingScreenButton() );
-
- expect(
- getHomeScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect(
- getChildScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
- expect(
- getNestedScreen( { throwIfNotFound: false } )
- ).not.toBeInTheDocument();
-
- // Check the values passed to `navigator.goTo()`.
- expect( spy ).toHaveBeenCalledTimes( 1 );
- expect( spy ).toHaveBeenNthCalledWith( 1, {
- path: PATHS.NOT_FOUND,
- type: 'goTo',
- } );
- } );
-
- it( 'should restore focus correctly', () => {
- render( );
-
- expect( getHomeScreen() ).toBeInTheDocument();
-
- // Navigate to child screen.
- fireEvent.click( getToChildScreenButton() );
-
- expect( getChildScreen() ).toBeInTheDocument();
-
- // Navigate to nested screen.
- fireEvent.click( getToNestedScreenButton() );
-
- expect( getNestedScreen() ).toBeInTheDocument();
-
- // Navigate back to child screen, check that focus was correctly restored.
- fireEvent.click( getBackButton() );
-
- expect( getChildScreen() ).toBeInTheDocument();
- expect( getToNestedScreenButton() ).toHaveFocus();
-
- // Navigate back to home screen, check that focus was correctly restored.
- fireEvent.click( getBackButton() );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect( getToChildScreenButton() ).toHaveFocus();
- } );
-
- it( 'should escape the value of the `path` prop', () => {
- render( );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect( getToInvalidHTMLPathScreenButton() ).toBeInTheDocument();
-
- // The following line tests the implementation details, but it's necessary
- // as this would be otherwise transparent to the user.
- expect( getToInvalidHTMLPathScreenButton() ).toHaveAttribute(
- 'id',
- INVALID_HTML_ATTRIBUTE.escaped
- );
-
- // Navigate to screen with an invalid HTML value for its `path`.
- fireEvent.click( getToInvalidHTMLPathScreenButton() );
-
- expect( getInvalidHTMLPathScreen() ).toBeInTheDocument();
- expect( getBackButton() ).toBeInTheDocument();
-
- // Navigate back to home screen, check that the focus restoration selector
- // worked correctly despite the escaping.
- fireEvent.click( getBackButton() );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect( getToInvalidHTMLPathScreenButton() ).toHaveFocus();
- } );
-
- it( 'should keep focus on the element that is being interacted with, while re-rendering', async () => {
- const user = userEvent.setup( {
- advanceTimers: jest.advanceTimersByTime,
- } );
-
- render( );
-
- expect( getHomeScreen() ).toBeInTheDocument();
- expect( getToChildScreenButton() ).toBeInTheDocument();
-
- // Navigate to child screen.
- await user.click( getToChildScreenButton() );
-
- expect( getChildScreen() ).toBeInTheDocument();
- expect( getBackButton() ).toBeInTheDocument();
- expect( getToNestedScreenButton() ).toHaveFocus();
-
- // Interact with the input, the focus should stay on the input element.
- const input = screen.getByLabelText( 'This is a test input' );
- await user.type( input, 'd' );
- expect( input ).toHaveFocus();
- } );
-} );
diff --git a/packages/components/src/navigator/test/index.tsx b/packages/components/src/navigator/test/index.tsx
new file mode 100644
index 00000000000000..ffdaf0e2c19678
--- /dev/null
+++ b/packages/components/src/navigator/test/index.tsx
@@ -0,0 +1,470 @@
+/**
+ * External dependencies
+ */
+import type { ReactNode, ForwardedRef, ComponentPropsWithoutRef } from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import {
+ NavigatorProvider,
+ NavigatorScreen,
+ NavigatorButton,
+ NavigatorBackButton,
+} from '..';
+
+jest.mock( 'framer-motion', () => {
+ const actual = jest.requireActual( 'framer-motion' );
+ return {
+ __esModule: true,
+ ...actual,
+ AnimatePresence:
+ ( { children }: { children?: ReactNode } ) =>
+ () =>
+ { children }
,
+ motion: {
+ ...actual.motion,
+ div: require( 'react' ).forwardRef(
+ (
+ { children }: { children?: ReactNode },
+ ref: ForwardedRef< HTMLDivElement >
+ ) => { children }
+ ),
+ },
+ };
+} );
+
+const INVALID_HTML_ATTRIBUTE = {
+ raw: ' "\'><=invalid_path',
+ escaped: " "'><=invalid_path",
+};
+
+const PATHS = {
+ HOME: '/',
+ CHILD: '/child',
+ NESTED: '/child/nested',
+ INVALID_HTML_ATTRIBUTE: INVALID_HTML_ATTRIBUTE.raw,
+ NOT_FOUND: '/not-found',
+};
+
+const SCREEN_TEXT = {
+ home: 'This is the home screen.',
+ child: 'This is the child screen.',
+ nested: 'This is the nested screen.',
+ invalidHtmlPath: 'This is the screen with an invalid HTML value as a path.',
+};
+
+const BUTTON_TEXT = {
+ toNonExistingScreen: 'Navigate to non-existing screen.',
+ toChildScreen: 'Navigate to child screen.',
+ toNestedScreen: 'Navigate to nested screen.',
+ toInvalidHtmlPathScreen:
+ 'Navigate to screen with an invalid HTML value as a path.',
+ back: 'Go back',
+};
+
+type CustomTestOnClickHandler = (
+ args:
+ | {
+ type: 'goTo';
+ path: string;
+ }
+ | { type: 'goBack' }
+) => void;
+
+function CustomNavigatorButton( {
+ path,
+ onClick,
+ ...props
+}: Omit< ComponentPropsWithoutRef< typeof NavigatorButton >, 'onClick' > & {
+ onClick?: CustomTestOnClickHandler;
+} ) {
+ return (
+ {
+ // Used to spy on the values passed to `navigator.goTo`.
+ onClick?.( { type: 'goTo', path } );
+ } }
+ path={ path }
+ { ...props }
+ />
+ );
+}
+
+function CustomNavigatorButtonWithFocusRestoration( {
+ path,
+ onClick,
+ ...props
+}: Omit< ComponentPropsWithoutRef< typeof NavigatorButton >, 'onClick' > & {
+ onClick?: CustomTestOnClickHandler;
+} ) {
+ return (
+ {
+ // Used to spy on the values passed to `navigator.goTo`.
+ onClick?.( { type: 'goTo', path } );
+ } }
+ path={ path }
+ { ...props }
+ />
+ );
+}
+
+function CustomNavigatorBackButton( {
+ onClick,
+ ...props
+}: Omit< ComponentPropsWithoutRef< typeof NavigatorBackButton >, 'onClick' > & {
+ onClick?: CustomTestOnClickHandler;
+} ) {
+ return (
+ {
+ // Used to spy on the values passed to `navigator.goBack`.
+ onClick?.( { type: 'goBack' } );
+ } }
+ { ...props }
+ />
+ );
+}
+
+const MyNavigation = ( {
+ initialPath = PATHS.HOME,
+ onNavigatorButtonClick,
+}: {
+ initialPath?: string;
+ onNavigatorButtonClick?: CustomTestOnClickHandler;
+} ) => {
+ const [ inputValue, setInputValue ] = useState( '' );
+ return (
+
+
+ { SCREEN_TEXT.home }
+
+ { BUTTON_TEXT.toNonExistingScreen }
+
+
+ { BUTTON_TEXT.toChildScreen }
+
+
+ { BUTTON_TEXT.toInvalidHtmlPathScreen }
+
+
+
+
+ { SCREEN_TEXT.child }
+
+ { BUTTON_TEXT.toNestedScreen }
+
+
+ { BUTTON_TEXT.back }
+
+
+
+ {
+ setInputValue( e.target.value );
+ } }
+ value={ inputValue }
+ />
+
+
+
+ { SCREEN_TEXT.nested }
+
+ { BUTTON_TEXT.back }
+
+
+
+
+ { SCREEN_TEXT.invalidHtmlPath }
+
+ { BUTTON_TEXT.back }
+
+
+
+ { /* A `NavigatorScreen` with `path={ PATHS.NOT_FOUND }` is purposefully not included. */ }
+
+ );
+};
+
+const getScreen = ( screenKey: keyof typeof SCREEN_TEXT ) =>
+ screen.getByText( SCREEN_TEXT[ screenKey ] );
+const queryScreen = ( screenKey: keyof typeof SCREEN_TEXT ) =>
+ screen.queryByText( SCREEN_TEXT[ screenKey ] );
+const getNavigationButton = ( buttonKey: keyof typeof BUTTON_TEXT ) =>
+ screen.getByRole( 'button', { name: BUTTON_TEXT[ buttonKey ] } );
+
+describe( 'Navigator', () => {
+ const originalGetClientRects = window.Element.prototype.getClientRects;
+
+ // `getClientRects` needs to be mocked so that `isVisible` from the `@wordpress/dom`
+ // `focusable` module can pass, in a JSDOM env where the DOM elements have no width/height.
+ const mockedGetClientRects = jest.fn( () => [
+ {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ },
+ ] );
+
+ beforeAll( () => {
+ // @ts-expect-error There's no need for an exact mock, this is just needed
+ // for the tests to pass (see `mockedGetClientRects` inline comments).
+ window.Element.prototype.getClientRects =
+ jest.fn( mockedGetClientRects );
+ } );
+
+ afterAll( () => {
+ window.Element.prototype.getClientRects = originalGetClientRects;
+ } );
+
+ it( 'should render', () => {
+ render( );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( queryScreen( 'child' ) ).not.toBeInTheDocument();
+ expect( queryScreen( 'nested' ) ).not.toBeInTheDocument();
+ } );
+
+ it( 'should show a different screen on the first render depending on the value of `initialPath`', () => {
+ render( );
+
+ expect( queryScreen( 'home' ) ).not.toBeInTheDocument();
+ expect( getScreen( 'child' ) ).toBeInTheDocument();
+ expect( queryScreen( 'nested' ) ).not.toBeInTheDocument();
+ } );
+
+ it( 'should ignore changes to `initialPath` after the first render', () => {
+ const { rerender } = render( );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( queryScreen( 'child' ) ).not.toBeInTheDocument();
+ expect( queryScreen( 'nested' ) ).not.toBeInTheDocument();
+
+ rerender( );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( queryScreen( 'child' ) ).not.toBeInTheDocument();
+ expect( queryScreen( 'nested' ) ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not rended anything if the `initialPath` does not match any available screen', () => {
+ render( );
+
+ expect( queryScreen( 'home' ) ).not.toBeInTheDocument();
+ expect( queryScreen( 'child' ) ).not.toBeInTheDocument();
+ expect( queryScreen( 'nested' ) ).not.toBeInTheDocument();
+ } );
+
+ it( 'should navigate across screens', async () => {
+ const spy = jest.fn();
+
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+
+ render( );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toChildScreen' ) ).toBeInTheDocument();
+
+ // Navigate to child screen.
+ await user.click( getNavigationButton( 'toChildScreen' ) );
+
+ expect( getScreen( 'child' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'back' ) ).toBeInTheDocument();
+
+ // Navigate back to home screen.
+ await user.click( getNavigationButton( 'back' ) );
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toChildScreen' ) ).toBeInTheDocument();
+
+ // Navigate again to child screen.
+ await user.click( getNavigationButton( 'toChildScreen' ) );
+
+ expect( getScreen( 'child' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toNestedScreen' ) ).toBeInTheDocument();
+
+ // Navigate to nested screen.
+ await user.click( getNavigationButton( 'toNestedScreen' ) );
+
+ expect( getScreen( 'nested' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'back' ) ).toBeInTheDocument();
+
+ // Navigate back to child screen.
+ await user.click( getNavigationButton( 'back' ) );
+
+ expect( getScreen( 'child' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toNestedScreen' ) ).toBeInTheDocument();
+
+ // Navigate back to home screen.
+ await user.click( getNavigationButton( 'back' ) );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toChildScreen' ) ).toBeInTheDocument();
+
+ // Check the values passed to `navigator.goTo()`.
+ expect( spy ).toHaveBeenCalledTimes( 6 );
+ expect( spy ).toHaveBeenNthCalledWith( 1, {
+ path: PATHS.CHILD,
+ type: 'goTo',
+ } );
+ expect( spy ).toHaveBeenNthCalledWith( 2, {
+ type: 'goBack',
+ } );
+ expect( spy ).toHaveBeenNthCalledWith( 3, {
+ path: PATHS.CHILD,
+ type: 'goTo',
+ } );
+ expect( spy ).toHaveBeenNthCalledWith( 4, {
+ path: PATHS.NESTED,
+ type: 'goTo',
+ } );
+ expect( spy ).toHaveBeenNthCalledWith( 5, {
+ type: 'goBack',
+ } );
+ expect( spy ).toHaveBeenNthCalledWith( 6, {
+ type: 'goBack',
+ } );
+ } );
+
+ it( 'should not rended anything if the path does not match any available screen', async () => {
+ const spy = jest.fn();
+
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+
+ render( );
+
+ expect(
+ getNavigationButton( 'toNonExistingScreen' )
+ ).toBeInTheDocument();
+
+ // Attempt to navigate to non-existing screen. No screens get rendered.
+ await user.click( getNavigationButton( 'toNonExistingScreen' ) );
+
+ expect( queryScreen( 'home' ) ).not.toBeInTheDocument();
+ expect( queryScreen( 'child' ) ).not.toBeInTheDocument();
+ expect( queryScreen( 'nested' ) ).not.toBeInTheDocument();
+
+ // Check the values passed to `navigator.goTo()`.
+ expect( spy ).toHaveBeenCalledTimes( 1 );
+ expect( spy ).toHaveBeenNthCalledWith( 1, {
+ path: PATHS.NOT_FOUND,
+ type: 'goTo',
+ } );
+ } );
+
+ it( 'should restore focus correctly', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+
+ render( );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+
+ // Navigate to child screen.
+ await user.click( getNavigationButton( 'toChildScreen' ) );
+
+ expect( getScreen( 'child' ) ).toBeInTheDocument();
+
+ // Navigate to nested screen.
+ await user.click( getNavigationButton( 'toNestedScreen' ) );
+
+ expect( getScreen( 'nested' ) ).toBeInTheDocument();
+
+ // Navigate back to child screen, check that focus was correctly restored.
+ await user.click( getNavigationButton( 'back' ) );
+
+ expect( getScreen( 'child' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toNestedScreen' ) ).toHaveFocus();
+
+ // Navigate back to home screen, check that focus was correctly restored.
+ await user.click( getNavigationButton( 'back' ) );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toChildScreen' ) ).toHaveFocus();
+ } );
+
+ it( 'should escape the value of the `path` prop', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+
+ render( );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect(
+ getNavigationButton( 'toInvalidHtmlPathScreen' )
+ ).toBeInTheDocument();
+
+ // The following line tests the implementation details, but it's necessary
+ // as this would be otherwise transparent to the user.
+ expect(
+ getNavigationButton( 'toInvalidHtmlPathScreen' )
+ ).toHaveAttribute( 'id', INVALID_HTML_ATTRIBUTE.escaped );
+
+ // Navigate to screen with an invalid HTML value for its `path`.
+ await user.click( getNavigationButton( 'toInvalidHtmlPathScreen' ) );
+
+ expect( getScreen( 'invalidHtmlPath' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'back' ) ).toBeInTheDocument();
+
+ // Navigate back to home screen, check that the focus restoration selector
+ // worked correctly despite the escaping.
+ await user.click( getNavigationButton( 'back' ) );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect(
+ getNavigationButton( 'toInvalidHtmlPathScreen' )
+ ).toHaveFocus();
+ } );
+
+ it( 'should keep focus on the element that is being interacted with, while re-rendering', async () => {
+ const user = userEvent.setup( {
+ advanceTimers: jest.advanceTimersByTime,
+ } );
+
+ render( );
+
+ expect( getScreen( 'home' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toChildScreen' ) ).toBeInTheDocument();
+
+ // Navigate to child screen.
+ await user.click( getNavigationButton( 'toChildScreen' ) );
+
+ expect( getScreen( 'child' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'back' ) ).toBeInTheDocument();
+ expect( getNavigationButton( 'toNestedScreen' ) ).toHaveFocus();
+
+ // Interact with the input, the focus should stay on the input element.
+ const input = screen.getByLabelText( 'This is a test input' );
+ await user.type( input, 'd' );
+ expect( input ).toHaveFocus();
+ } );
+} );
diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js
index eefc7e3420405d..c52a3b8ad46587 100644
--- a/packages/edit-post/src/store/reducer.js
+++ b/packages/edit-post/src/store/reducer.js
@@ -153,7 +153,7 @@ export function listViewPanel( state = false, action ) {
}
/**
- * Reducer tracking whether the inserter is open.
+ * Reducer tracking whether template editing is on or off.
*
* @param {boolean} state
* @param {Object} action