Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev": "NODE_ENV=development wp-scripts build",
"format:js": "wp-scripts format src -- --config=../../../../.prettierrc.js",
"lint:css": "wp-scripts lint-style 'style.css' 'src/**/*.scss'",
"lint:js": "wp-scripts lint-js src",
"lint:js": "wp-scripts lint-js src --max-warnings=0",
"start": "wp-scripts start",
"test:unit": "wp-scripts test-unit-js"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,21 @@ const interfaceLabels = {
};

function Editor( { onError, postId } ) {
const { isInserterOpen, isListViewOpen, post, sidebarIsOpened, settings } = useSelect( ( select ) => {
const { isInserterOpened, isListViewOpened, getSettings } = select( patternStore );
const { getEntityRecord } = select( coreStore );
const { isInserterOpen, isListViewOpen, post, sidebarIsOpened, settings } = useSelect(
( select ) => {
const { isInserterOpened, isListViewOpened, getSettings } = select( patternStore );
const { getEntityRecord } = select( coreStore );

return {
isInserterOpen: isInserterOpened(),
isListViewOpen: isListViewOpened(),
post: getEntityRecord( 'postType', POST_TYPE, postId ),
sidebarIsOpened: !! select( interfaceStore ).getActiveComplementaryArea( STORE_NAME ),
settings: getSettings(),
};
}, [] );
return {
isInserterOpen: isInserterOpened(),
isListViewOpen: isListViewOpened(),
post: getEntityRecord( 'postType', POST_TYPE, postId ),
sidebarIsOpened: !! select( interfaceStore ).getActiveComplementaryArea( STORE_NAME ),
settings: getSettings(),
};
},
[ postId ]
);
const { setIsInserterOpened } = useDispatch( patternStore );
const { createInfoNotice } = useDispatch( noticesStore );
const [ isEntitiesSavedStatesOpen, setIsEntitiesSavedStatesOpen ] = useState( false );
Expand All @@ -73,7 +76,7 @@ function Editor( { onError, postId } ) {
isDismissible: false,
} );
}
}, [] );
}, [ createInfoNotice ] );

// Don't render the Editor until the settings are set and loaded
if ( ! settings?.siteUrl || ! post ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* WordPress dependencies
*/
import { createElement, createRoot } from '@wordpress/element';

/*
* `@wordpress/element` renders with the React copy it bundles, which is a
* different instance than the one at the repo root (kept for forward-compat
* work). Pull `act` from that same copy so the render and its hooks share a
* single React dispatcher, otherwise React throws an invalid-hook-call error.
*/
const { act } = jest.requireActual(
require.resolve( 'react', { paths: [ require.resolve( '@wordpress/element' ) ] } )
);

const mockGetEntityRecord = jest.fn();
// A stable reference: `useSelect` warns in dev if the mapping returns a new
// object for the same state, and the editor short-circuits when `siteUrl` is
// absent regardless.
const mockSettings = {};

/*
* Replace the stores with minimal stand-ins so `useSelect` (the unit under
* test) stays real while `@wordpress/core-data` and friends are not pulled in.
* `getSettings` returns no `siteUrl`, so the editor short-circuits to `null`
* before mounting its heavy render tree — but the `useSelect` mapping still
* runs, which is what reads `postId`.
*/
const mockMakeStore = ( name, selectors, actions = {} ) => {
const { createReduxStore, register } = jest.requireActual( '@wordpress/data' );
const store = createReduxStore( name, { reducer: ( state = {} ) => state, selectors, actions } );
register( store );
return store;
};

jest.mock( '../../../store', () => ( {
POST_TYPE: 'wporg-pattern',
store: mockMakeStore(
'test/editor-pattern',
{
isInserterOpened: () => false,
isListViewOpened: () => false,
getSettings: () => mockSettings,
},
{ setIsInserterOpened: () => ( { type: 'NOOP' } ) }
),
} ) );
jest.mock( '@wordpress/core-data', () => ( {
store: mockMakeStore( 'test/editor-core', {
getEntityRecord: ( state, kind, postType, id ) => mockGetEntityRecord( kind, postType, id ),
} ),
} ) );
jest.mock( '@wordpress/notices', () => ( {
store: mockMakeStore( 'test/editor-notices', {}, { createInfoNotice: () => ( { type: 'NOOP' } ) } ),
} ) );
Comment thread
obenland marked this conversation as resolved.
jest.mock( '@wordpress/interface', () => {
const { createElement: el } = jest.requireActual( '@wordpress/element' );
const Noop = () => null;
return {
store: mockMakeStore( 'test/editor-interface', { getActiveComplementaryArea: () => undefined } ),
ComplementaryArea: Object.assign( Noop, { Slot: Noop } ),
FullscreenMode: Noop,
InterfaceSkeleton: () => el( 'div', null ),
};
} );
Comment thread
obenland marked this conversation as resolved.

/* Stub the heavy components so importing the editor module stays cheap. */
const Noop = () => null;
jest.mock( '@wordpress/editor', () => ( {
EditorNotices: Noop,
EditorProvider: Noop,
EditorSnackbars: Noop,
ErrorBoundary: Noop,
UnsavedChangesWarning: Noop,
} ) );
jest.mock( '@wordpress/components', () => {
const { createElement: el } = jest.requireActual( '@wordpress/element' );
const N = () => null;
return {
Notice: N,
Popover: Object.assign( N, { Slot: N } ),
SlotFillProvider: ( { children } ) => el( 'div', null, children ),
};
} );
jest.mock( '@wordpress/block-editor', () => ( { BlockBreadcrumb: Noop } ) );
jest.mock( '@wordpress/keyboard-shortcuts', () => ( { ShortcutProvider: () => null } ) );
jest.mock( '../../block-editor', () => ( { __esModule: true, default: Noop } ) );
jest.mock( '../../header', () => ( { __esModule: true, default: Noop } ) );
jest.mock( '../../secondary-sidebar/inserter-sidebar', () => ( { __esModule: true, default: Noop } ) );
jest.mock( '../../secondary-sidebar/list-view-sidebar', () => ( { __esModule: true, default: Noop } ) );
jest.mock( '../../keyboard-shortcuts', () => ( { __esModule: true, default: Noop } ) );
jest.mock( '../../sidebar', () => ( { SidebarComplementaryAreaFills: Noop } ) );
jest.mock( '../../url-controller', () => ( { __esModule: true, default: Noop } ) );
jest.mock( '../../welcome-guide', () => ( { __esModule: true, default: Noop } ) );
jest.mock( '../save-sidebar', () => ( { __esModule: true, default: Noop } ) );

describe( 'Editor', () => {
let Editor;
let container;
let root;

beforeAll( () => {
global.IS_REACT_ACT_ENVIRONMENT = true;
global.wporgLocale = { id: 'en_US' };
Editor = require( '../index' ).default;
} );

Comment thread
obenland marked this conversation as resolved.
beforeEach( () => {
container = document.createElement( 'div' );
document.body.appendChild( container );
root = createRoot( container );
mockGetEntityRecord.mockReset();
} );

afterEach( () => {
act( () => root.unmount() );
container.remove();
} );

const render = ( postId ) => {
act( () => {
root.render( createElement( Editor, { postId, onError: () => {} } ) );
} );
};

it( 'looks up the post for the given postId', () => {
render( 1 );

expect( mockGetEntityRecord ).toHaveBeenCalledWith( 'postType', 'wporg-pattern', 1 );
} );

it( 'looks up the new post when postId changes', () => {
render( 1 );
mockGetEntityRecord.mockClear();

// Re-render with a different postId. Without `postId` in the `useSelect`
// dependency array, the memoized selector keeps reading the first id.
render( 2 );

expect( mockGetEntityRecord ).toHaveBeenCalledWith( 'postType', 'wporg-pattern', 2 );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ export default function FeatureToggle( { feature, label, info, messageActivated,
}
};

const isActive = useSelect( ( select ) => {
return select( patternStore ).isFeatureActive( feature );
}, [] );
const isActive = useSelect( ( select ) => select( patternStore ).isFeatureActive( feature ), [ feature ] );

const { toggleFeature } = useDispatch( patternStore );

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* WordPress dependencies
*/
import { createElement, createRoot } from '@wordpress/element';
import { dispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import { store as patternStore } from '../../../../store';

/*
* `@wordpress/element` renders with the React copy it bundles, which is a
* different instance than the one at the repo root (kept for forward-compat
* work). Pull `act` from that same copy so the render and its hooks share a
* single React dispatcher, otherwise React throws an invalid-hook-call error.
*/
const { act } = jest.requireActual(
require.resolve( 'react', { paths: [ require.resolve( '@wordpress/element' ) ] } )
);

/*
* Replace the real store with a minimal stand-in that mirrors `isFeatureActive`
* / `toggleFeature`. This keeps `useSelect` (the unit under test) real while
* avoiding the heavy `@wordpress/core-data` graph the real store pulls in.
*/
jest.mock( '../../../../store', () => {
const { createReduxStore, register } = jest.requireActual( '@wordpress/data' );
const store = createReduxStore( 'test/feature-toggle', {
reducer: ( state = {}, action ) =>
action.type === 'TOGGLE_FEATURE' ? { ...state, [ action.feature ]: ! state[ action.feature ] } : state,
actions: {
toggleFeature: ( feature ) => ( { type: 'TOGGLE_FEATURE', feature } ),
},
selectors: {
isFeatureActive: ( state, feature ) => !! state[ feature ],
},
} );
register( store );
return { store };
} );

/*
* The unit under test is the `useSelect` dependency array that derives the
* selected state, not the `MenuItem` chrome. Stub it so the markup exposes that
* state directly.
*/
jest.mock( '@wordpress/components', () => {
const { createElement: el } = jest.requireActual( '@wordpress/element' );
return {
MenuItem: ( { isSelected, children } ) =>
el( 'button', { type: 'button', 'aria-checked': String( !! isSelected ) }, children ),
};
} );

describe( 'FeatureToggle', () => {
let FeatureToggle;
let container;
let root;

beforeAll( () => {
global.IS_REACT_ACT_ENVIRONMENT = true;
FeatureToggle = require( '../index' ).default;
} );

beforeEach( () => {
container = document.createElement( 'div' );
document.body.appendChild( container );
root = createRoot( container );
} );

afterEach( () => {
act( () => root.unmount() );
container.remove();
} );

const render = ( feature ) => {
act( () => {
root.render( createElement( FeatureToggle, { feature, label: 'Toggle' } ) );
} );
};

const isActive = () => container.querySelector( 'button' ).getAttribute( 'aria-checked' );

it( 'reflects the active state of the feature it is given', () => {
dispatch( patternStore ).toggleFeature( 'reflects-active' );

render( 'reflects-active' );

expect( isActive() ).toBe( 'true' );
} );

it( 'reports an untouched feature as inactive', () => {
render( 'never-toggled' );

expect( isActive() ).toBe( 'false' );
} );

it( 'updates when the feature prop changes', () => {
dispatch( patternStore ).toggleFeature( 'prop-change-on' );

render( 'prop-change-on' );
expect( isActive() ).toBe( 'true' );

// Re-render with a different, inactive feature. Without `feature` in the
// `useSelect` dependency array, the stale closure keeps reporting the
// first feature's state.
render( 'prop-change-off' );
expect( isActive() ).toBe( 'false' );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function OpenverseGrid( { searchTerm, onClose, onSelect, multiple
// Set up a debounced search term, so we don't query constantly while someone is typing.
useEffect( () => {
setDebouncedSearchTerm( searchTerm );
}, [ searchTerm ] );
}, [ searchTerm, setDebouncedSearchTerm ] );

// When the search changes, reset back to page 1, and trigger a search.
useEffect( () => {
Expand Down Expand Up @@ -91,7 +91,7 @@ export default function OpenverseGrid( { searchTerm, onClose, onSelect, multiple
}

onClose();
}, [ selected, multiple ] );
}, [ selected, multiple, onSelect, onClose ] );

const onClick = useCallback(
( newValue ) => {
Expand Down
Loading
Loading