How to write tests for Gutenberg via mocking useSelect or via a dummy post?
-
Hi community,
I am running into a strange situation that has been bugging me for last 3 days and I can’t seem to figure out the right approach yet. I write here with some hopes to get some other viewpoints on the subject.
Background
Recently, we have been trying to follow Test Driven Development techniques at our workplace. While working with PHPUnit has been a piece of cake, I’m having trouble with writing tests for Gutenberg-based features that I’m working.
Consider the following feature, I am trying to create a field that allows users to choose the “Last Updated At” date for an article. It is going to be a datetime picker when fully implemented, but right now, the focus is just to fetch the data from backend and show the date on UI. Here’s how it is implemented:
import { __, _x } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import React, { useEffect, useState } from 'react';
import { store as editorStore } from '@wordpress/editor';
import { getSettings, getDate, gmdateI18n, dateI18n } from '@wordpress/date';
import { Button, __experimentalHStack as HStack } from '@wordpress/components';
// TODO: Implement a proper, reusable PostPanelRow component instead of HStack.
export function LastUpdatedAtInfo() {
const label = useLastUpdatedAtLabel();
return <HStack>
<div className="editor-post-panel__row-label">
{ __( 'Last updated at', 'articles' ) }
</div>
<div className="editor-post-panel__row-control">
<Button
size="compact"
variant="tertiary"
>
{ label }
</Button>
</div>
</HStack>;
}
function useLastUpdatedAtLabel() {
const [ label, setLabel ] = useState(
_x( 'Not set', 'The text to display is not set yet', 'articles' ),
);
const date = useSelect( ( select ) => {
const postMeta = select( editorStore ).getEditedPostAttribute( 'meta' );
const metaValue = postMeta?.[ window.Articles.FIELD_LAST_UPDATED_AT ];
if (
! metaValue ||
[ '0000-00-00 00:00:00', '1970-01-01 00:00:00' ].includes( metaValue ) ||
! getDate( metaValue )
) {
return null;
}
// The post meta is a GMT datetime in mysql format.
const dateString = metaValue.replace( ' ', 'T' ) + 'Z';
return getDate( dateString );
}, [] );
useEffect( () => {
if ( ! date ) {
return;
}
const formattedDate = dateI18n( 'M j, Y g:i\xa0a', date );
const timezone = getSettings().timezone.abbr;
setLabel( formattedDate + ' ' + timezone );
}, [ date ] );
return label;
}And for the tests, I started by mocking the useSelect like this:
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import { store as editorStore } from '@wordpress/editor';
import { getSettings, setSettings } from '@wordpress/date';
import { StoreDescriptor, useSelect } from '@wordpress/data';
import {
LastUpdatedAtInfo,
} from '../path/to/post-status-info/last-updated-at';
jest.mock( '@wordpress/data', () => {
const originalModule = jest.requireActual( '@wordpress/data' );
return {
...originalModule,
useSelect: jest.fn(),
};
} );
const useSelectMockImpl = ( metaValue: string ) => {
( useSelect as jest.Mock ).mockImplementation( ( selector, _d = [] ) => {
const select = ( store: StoreDescriptor ) => {
if ( store.name !== editorStore.name ) {
return {};
}
return {
getEditedPostAttribute: ( attribute: string ) => {
if ( attribute !== 'meta' ) {
return {};
}
return {
[ window.Articles.FIELD_LAST_UPDATED_AT ]: metaValue,
};
},
};
};
return selector( select );
} );
};
describe( '"Last updated at" field should', () => {
beforeAll( () => {
window.Articles = {
FIELD_LAST_UPDATED_AT: 'uml-updation_date',
};
} );
it( 'transform a GMT datetime to site timezone before displaying', () => {
useSelectMockImpl( '2025-05-08 12:07:36' );
const settings = getSettings();
setSettings( {
...settings,
timezone: {
string: 'Asia/Kolkata',
offset: '5.5',
abbr: 'IST',
offsetFormatted: '+05:30',
},
} );
const { container } = render( <LastUpdatedAtInfo /> );
setSettings( settings );
const button = container.querySelector( 'button' );
expect( button ).toBeInTheDocument();
expect( button ).toHaveTextContent( 'May 8, 2025 5:37 pm IST' );
} );
} );The tests so far are working great, no issues!
Problem
The problems arise when I start to think about testing the delay that
useSelectmay encounter because of network request time or any other reasons. The goal is to implement the component to take into account theisResolvingandhasStartedResolutionfunctions from editorStore into account to display the label. If the data is not resolved yet, show the label “Loading…” or something like that.In order for me to do so, I want my
useSelectmock to return the value ingetEditedPostAttributewith some delay and set the necessary flag values forisResolvingandhasStartedResolutionto work. Here’s the implementation I came up with:const useSelectMockImpl = ( metaValue: string, delay = 0 ) => {
let shouldResolve = false;
let hasResolutionStarted = false;
( useSelect as jest.Mock ).mockImplementation( ( selector, _d = [] ) => {
const select = ( store: StoreDescriptor ) => {
if ( store.name !== editorStore.name ) {
return {};
}
return {
getEditedPostAttribute: ( attribute: string ) => {
if ( attribute !== 'meta' ) {
return {};
}
if ( ! hasResolutionStarted ) {
hasResolutionStarted = true;
setTimeout( () => {
shouldResolve = true;
}, delay );
}
return shouldResolve
? ( {
[ window.Articles.FIELD_LAST_UPDATED_AT ]: metaValue,
} )
: null;
},
isResolving: ( selectorName: string, args: any[] ) => {
return selectorName === 'getEditedPostAttribute' &&
args[ 0 ] === 'meta' &&
hasResolutionStarted &&
! shouldResolve;
},
hasStartedResolution: ( selectorName: string, args: any[] ) => {
return selectorName === 'getEditedPostAttribute' &&
args[ 0 ] === 'meta' &&
hasResolutionStarted;
},
};
};
return selector( select );
} );
};And this leads to existing test to fail. Do understand that the default delay is set to 0 so it should be instantaneous. But I guess there will be some sort of execution delay so it can’t be truly 0.
But that is not the problem. The problem is that original
useSelectfrom@wordpress/datapackage has a tendancy to keep trying to load the value till it’s not loaded. It feels like some sort of subscription mechanism that keeps an eye on the value when it returns. What I am not able to mock is this behaviour of subscribing and then that forcing the re-render of the component.Since I don’t understand how to subscription can be mocked, I am running out ideas on how to attack this problem. I feel lazy right now to implement the entire subscription system in the mock as I’m not confident yet to go in that uncharted territory.
On the other hand, I thought, what if I mock the current state variables to make the original
@wordpress/data useSelecthook to feel like that we are on post edit page. How can I mock a post in Gutenberg? The logic here being that I will not have to mock the subscription mechanism myself and we could simply use the in-built one.So I started looking for some references about it and found this registry testing code on Github from Gutenberg codebase. https://github.com/WordPress/gutenberg/blob/de9d8bf200d7a2b9c6bdfc4108c46147da5124bc/packages/editor/src/store/test/actions.js#L67
Here’s how my mock a post function looked like:
const mockPost = ( postMeta: any ) => {
const post = {
id: 1,
type: 'post',
status: 'publish',
meta: postMeta,
};
dispatch( coreStore ).receiveEntityRecords( 'postType', 'post', post );
dispatch( editorStore ).setupEditor( post, {} );
};When I use this, and test the current post ID in the original
LastUpdateAtInfocomponent, it gives me the ID correctly:console.log( select( editorStore ).getCurrentPostId() ); // Logs 1But the meta object is undefined for some reason
const postMeta = select( editorStore ).getEditedPostAttribute( 'meta' ); // undefinedI have no idea what I am doing wrong when mocking a post for meta to not work. But again, this is uncharted territory for me so I am looking for some advice.
One thing that I did notice, maybe it helps, when saving the post during mocking, I got the error that there is not valid configuration defined for (postType, undefined). Here’s what the updated post mock was used:
const mockPost = ( postMeta: any ) => {
const post = {
id: 1,
type: 'post',
status: 'publish',
meta: postMeta,
};
dispatch( coreStore ).receiveEntityRecords( 'postType', 'post', post );
dispatch( editorStore ).setupEditor( post, {} );
dispatch( editorStore ).savePost();
};This leads me to thinking that maybe the postType post entity is yet not registered? I don’t know, I’m just shooting in the dark.
Conclusion
So with that background in mind, I would like to know your ideas, thoughts, comments on how to approach mocking useSelect OR a dummy post for testing. In fact, I think, we should try to have solutions for both cases as someone else may benefit from one or the other.
The topic ‘How to write tests for Gutenberg via mocking useSelect or via a dummy post?’ is closed to new replies.