• 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 useSelect may encounter because of network request time or any other reasons. The goal is to implement the component to take into account the isResolving and hasStartedResolution functions 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 useSelect mock to return the value in getEditedPostAttribute with some delay and set the necessary flag values for isResolving and hasStartedResolution to 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 useSelect from @wordpress/data package 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 useSelect hook 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 LastUpdateAtInfo component, it gives me the ID correctly:

    console.log( select( editorStore ).getCurrentPostId() );  // Logs 1

    But the meta object is undefined for some reason

    const postMeta = select( editorStore ).getEditedPostAttribute( 'meta' );  // undefined

    I 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.

Viewing 2 replies - 1 through 2 (of 2 total)
  • Thread Starter ImageChakrapani Gautam

    (@cherrygot)

    *Correction:

    The error received during mocking the post is this

    The entity being edited (postType, undefined) does not have a loaded config.

    And it happens because of editPost, not savePost. Ex:

    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 ).editPost({
    meta: post.postMeta
    });
    };

    Hi @cherrygot,
    If you are getting undefined when trying to fetch metadata using getEditedPostAttribute, it can be because meta fields are not registered properly for the Post Type in PHP. Make sure that show_in_rest is true for the Post Type.

    For this:

    The entity being edited (postType, undefined) does not have a loaded config.

    I believe what is happening is your code ran before the Post Type could be registered. It is weird because post is a default Post Type and should have worked out of the box.

    Maybe you can try out something like this done in core-data test.

    • This reply was modified 10 months, 3 weeks ago by ImageJaydeep Das.
Viewing 2 replies - 1 through 2 (of 2 total)

The topic ‘How to write tests for Gutenberg via mocking useSelect or via a dummy post?’ is closed to new replies.