Content Guidelines: Add frontend test coverage#76676
Content Guidelines: Add frontend test coverage#76676im3dabasia wants to merge 5 commits intoWordPress:trunkfrom
Conversation
|
@im3dabasia, Can I work on |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
@andrewserong, @aagam-shah would appreciate your review on this PR when you get a chance |
There was a problem hiding this comment.
Pull request overview
Adds Jest unit/integration-style tests to improve frontend coverage for the Content Guidelines feature, targeting store behavior and import/export and revision-history flows (per #76390 / tracking #76384).
Changes:
- Added store unit tests covering selectors, action creators, and basic reducer immutability checks.
- Added CRUD-oriented store tests validating category + block guideline updates and hydration from REST responses.
- Added API import/export tests covering valid imports, partial imports, invalid input handling, rollback on save failures, and export JSON structure.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| routes/content-guidelines/tests/store.test.ts | Unit tests for store selectors/actions/reducer edge cases. |
| routes/content-guidelines/tests/guidelines-crud.test.ts | Store CRUD-style tests validating read/write/update “flows” via reducer+selectors. |
| routes/content-guidelines/tests/api-import-export.test.ts | Tests for importContentGuidelines / exportContentGuidelines behavior and rollback/export structure. |
| routes/content-guidelines/tests/revision-history.test.ts | Intended revision history coverage, but currently mocks the API module and doesn’t exercise the component logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Mock dependencies | ||
| jest.mock( '@wordpress/data' ); | ||
| jest.mock( '@wordpress/notices' ); | ||
| jest.mock( '../api' ); | ||
|
|
There was a problem hiding this comment.
jest.mock( '../api' ) turns the API functions into mocks, but the tests then call fetchContentGuidelinesRevisions / restoreContentGuidelinesRevision directly. This makes the suite assert only that the mocked functions were called with the same args the test passed in, rather than exercising the real revision-history flow (the RevisionHistory component’s useEffect/handleRestore logic). Consider rendering components/revision-history.tsx with React Testing Library (mocking useSelect/useDispatch and the API module), or alternatively remove the API mock and unit-test the real api.ts functions by mocking @wordpress/api-fetch.
| test( 'should handle fetch error and dispatch error notice', async () => { | ||
| mockSelect.getId.mockReturnValue( 123 ); | ||
|
|
||
| mockFetchContentGuidelinesRevisions.mockRejectedValue( | ||
| new Error( 'API Error' ) | ||
| ); | ||
|
|
||
| const noticesDispatch = ( dispatch as jest.Mock )( noticesStore ); | ||
|
|
||
| await fetchContentGuidelinesRevisions( { | ||
| guidelinesId: 123, | ||
| perPage: 100, | ||
| } ).catch( () => { | ||
| noticesDispatch.createErrorNotice( | ||
| 'Could not load revision history. Please try again.', | ||
| { type: 'snackbar' } | ||
| ); | ||
| } ); | ||
|
|
||
| expect( mockDispatch.createErrorNotice ).toHaveBeenCalledWith( | ||
| 'Could not load revision history. Please try again.', | ||
| { type: 'snackbar' } | ||
| ); |
There was a problem hiding this comment.
Several tests manually call noticesDispatch.createErrorNotice(...) / createSuccessNotice(...) inside the test itself (e.g. in .catch blocks) instead of asserting that production code dispatched these notices. This can pass even if the app never shows notices. Adjust the tests to trigger the actual code paths (render the RevisionHistory component and interact with it, or call a real implementation function) and assert the notice dispatches happen as a result.
| test( 'should handle fetch error and dispatch error notice', async () => { | |
| mockSelect.getId.mockReturnValue( 123 ); | |
| mockFetchContentGuidelinesRevisions.mockRejectedValue( | |
| new Error( 'API Error' ) | |
| ); | |
| const noticesDispatch = ( dispatch as jest.Mock )( noticesStore ); | |
| await fetchContentGuidelinesRevisions( { | |
| guidelinesId: 123, | |
| perPage: 100, | |
| } ).catch( () => { | |
| noticesDispatch.createErrorNotice( | |
| 'Could not load revision history. Please try again.', | |
| { type: 'snackbar' } | |
| ); | |
| } ); | |
| expect( mockDispatch.createErrorNotice ).toHaveBeenCalledWith( | |
| 'Could not load revision history. Please try again.', | |
| { type: 'snackbar' } | |
| ); | |
| test( 'should reject when fetching revisions fails', async () => { | |
| mockSelect.getId.mockReturnValue( 123 ); | |
| mockFetchContentGuidelinesRevisions.mockRejectedValue( | |
| new Error( 'API Error' ) | |
| ); | |
| await expect( | |
| fetchContentGuidelinesRevisions( { | |
| guidelinesId: 123, | |
| perPage: 100, | |
| } ) | |
| ).rejects.toThrow( 'API Error' ); |
| let state = createInitialState(); | ||
| state.categories = { | ||
| site: 'Site', | ||
| copy: 'Copy', | ||
| images: 'Images', | ||
| additional: 'Additional', | ||
| blocks: {}, |
There was a problem hiding this comment.
createInitialState() returns the store's DEFAULT_STATE object by reference (via reducer(undefined, ...)). This test then mutates state.categories directly, which can leak state across tests within this file and also hides potential immutability issues. Prefer constructing a new state object (e.g. shallow-clone createInitialState() and override categories) instead of mutating the returned state in-place.
| let state = createInitialState(); | |
| state.categories = { | |
| site: 'Site', | |
| copy: 'Copy', | |
| images: 'Images', | |
| additional: 'Additional', | |
| blocks: {}, | |
| let state = { | |
| ...createInitialState(), | |
| categories: { | |
| site: 'Site', | |
| copy: 'Copy', | |
| images: 'Images', | |
| additional: 'Additional', | |
| blocks: {}, | |
| }, |
aswasif007
left a comment
There was a problem hiding this comment.
Thanks for working on this!
Mostly good, but I've requested some improvements.
| await expect( importContentGuidelines( file ) ).rejects.toThrow( | ||
| 'Save failed' | ||
| ); | ||
|
|
||
| // Verify rollback was called | ||
| expect( mockDispatch.setGuideline ).toHaveBeenCalledWith( | ||
| 'site', | ||
| 'Original site' | ||
| ); | ||
| expect( mockDispatch.setGuideline ).toHaveBeenCalledWith( | ||
| 'copy', | ||
| 'Original copy' | ||
| ); | ||
| } ); |
There was a problem hiding this comment.
Rollback tests can pass even if rollback is incomplete or out of order.
That's because The rollback assertions here only check that original values were called at least once, not that they are the final state after failure. If code regresses (e.g., writes original values and then overwrites again), these tests could still pass.
| // Verify rollback for block guidelines | ||
| expect( mockDispatch.setBlockGuideline ).toHaveBeenCalledWith( | ||
| 'core/paragraph', | ||
| 'Original paragraph' | ||
| ); | ||
| expect( mockDispatch.setBlockGuideline ).toHaveBeenCalledWith( | ||
| 'core/heading', | ||
| 'Original heading' | ||
| ); | ||
| } ); |
There was a problem hiding this comment.
The implementation explicitly clears current blocks before restoring previous ones; this is important when import introduces new block keys. The test does not assert that clear step happened, so a regression in that path would be undetected
| expect( mockDispatch.setGuideline ).toHaveBeenCalledWith( | ||
| 'site', | ||
| 'Site guidelines' | ||
| ); | ||
| expect( mockDispatch.setGuideline ).toHaveBeenCalledWith( | ||
| 'copy', | ||
| 'Copy guidelines' | ||
| ); |
There was a problem hiding this comment.
Let's also assert the presence of the images and additional guidelines.
| const result = await fetchContentGuidelinesRevisions( { | ||
| guidelinesId, | ||
| perPage: 100, | ||
| } ); | ||
|
|
||
| expect( mockFetchContentGuidelinesRevisions ).toHaveBeenCalledWith( | ||
| { | ||
| guidelinesId: 123, | ||
| perPage: 100, | ||
| } | ||
| ); |
There was a problem hiding this comment.
I think we are calling the mocked function then asserting the test's own invocation.
| test( 'should handle fetch error and dispatch error notice', async () => { | ||
| mockSelect.getId.mockReturnValue( 123 ); | ||
|
|
||
| mockFetchContentGuidelinesRevisions.mockRejectedValue( | ||
| new Error( 'API Error' ) | ||
| ); | ||
|
|
||
| const noticesDispatch = ( dispatch as jest.Mock )( noticesStore ); | ||
|
|
||
| await fetchContentGuidelinesRevisions( { | ||
| guidelinesId: 123, | ||
| perPage: 100, | ||
| } ).catch( () => { | ||
| noticesDispatch.createErrorNotice( | ||
| 'Could not load revision history. Please try again.', | ||
| { type: 'snackbar' } | ||
| ); | ||
| } ); | ||
|
|
||
| expect( mockDispatch.createErrorNotice ).toHaveBeenCalledWith( | ||
| 'Could not load revision history. Please try again.', | ||
| { type: 'snackbar' } | ||
| ); |
| await restoreContentGuidelinesRevision( | ||
| guidelinesId, | ||
| initial.revisions[ 0 ].id | ||
| ); | ||
|
|
||
| expect( mockRestoreContentGuidelinesRevision ).toHaveBeenCalledWith( | ||
| 123, | ||
| 1 | ||
| ); |
There was a problem hiding this comment.
same as above - we are calling a mocked function, then asserting that invocation.
| test( 'creates action with response payload', () => { | ||
| const response: RestGuidelinesResponse = { | ||
| id: 5, | ||
| status: 'draft', | ||
| guideline_categories: { | ||
| site: { guidelines: 'Site' }, | ||
| copy: { guidelines: 'Copy' }, | ||
| images: { guidelines: 'Images' }, | ||
| additional: { guidelines: 'Additional' }, | ||
| }, | ||
| }; | ||
|
|
||
| const action = actions.setFromResponse( response ); | ||
|
|
||
| expect( action.type ).toBe( 'SET_FROM_RESPONSE' ); | ||
| expect( action.response ).toBe( response ); |
There was a problem hiding this comment.
The file validates the action shape, but not reducer output after parsing API responses, which is where most complexity lives (blocks handling, fallback defaults, nullable fields). A bug in response parsing could pass this suite.
89e668b to
ab56bac
Compare
Work in Progress
What?
Closes #76390
Parent issue: #76384
Adds the tests mentioned in the issue.
Use of AI Tools
Claude