diff --git a/assets/src/block-validation/components/amp-document-status/test/amp-document-status-notification.js b/assets/src/block-validation/components/amp-document-status/test/amp-document-status-notification.js index a0aa33f7e76..65c647f19a5 100644 --- a/assets/src/block-validation/components/amp-document-status/test/amp-document-status-notification.js +++ b/assets/src/block-validation/components/amp-document-status/test/amp-document-status-notification.js @@ -9,6 +9,15 @@ import { beforeAll, describe, expect, it, jest } from '@jest/globals'; */ import { useDispatch, useSelect } from '@wordpress/data'; +jest.mock('@wordpress/edit-post', () => ({ + PluginSidebar: ({ children }) => children, + PluginSidebarMoreMenuItem: ({ children }) => children, +})); + +jest.mock('@wordpress/block-editor', () => ({ + BlockIcon: ({ icon }) => icon, +})); + /** * Internal dependencies */ @@ -16,10 +25,15 @@ import AMPDocumentStatusNotification from '../index'; import { useAMPDocumentToggle } from '../../../hooks/use-amp-document-toggle'; import { useErrorsFetchingStateChanges } from '../../../hooks/use-errors-fetching-state-changes'; -jest.mock('@wordpress/data/build/components/use-select', () => jest.fn()); -jest.mock('@wordpress/data/build/components/use-dispatch/use-dispatch', () => - jest.fn() -); +jest.mock('@wordpress/data', () => ({ + useSelect: jest.fn(), + useDispatch: jest.fn(), + combineReducers: jest.fn(), + createSelector: jest.fn(), + createReduxStore: jest.fn(), + register: jest.fn(), +})); + jest.mock('../../../hooks/use-amp-document-toggle', () => ({ useAMPDocumentToggle: jest.fn(), })); diff --git a/assets/src/block-validation/components/amp-validation-status/test/revalidate-notification.js b/assets/src/block-validation/components/amp-validation-status/test/revalidate-notification.js index 1bd45f9d52a..cab4b0e5ebe 100644 --- a/assets/src/block-validation/components/amp-validation-status/test/revalidate-notification.js +++ b/assets/src/block-validation/components/amp-validation-status/test/revalidate-notification.js @@ -22,10 +22,15 @@ import { useDispatch, useSelect } from '@wordpress/data'; import AMPRevalidateNotification from '../revalidate-notification'; import { useErrorsFetchingStateChanges } from '../../../hooks/use-errors-fetching-state-changes'; -jest.mock('@wordpress/data/build/components/use-select', () => jest.fn()); -jest.mock('@wordpress/data/build/components/use-dispatch/use-dispatch', () => - jest.fn() -); +jest.mock('@wordpress/data', () => ({ + useSelect: jest.fn(), + useDispatch: jest.fn(), + combineReducers: jest.fn(), + createSelector: jest.fn(), + createReduxStore: jest.fn(), + register: jest.fn(), +})); + jest.mock('../../../hooks/use-errors-fetching-state-changes', () => ({ useErrorsFetchingStateChanges: jest.fn(), })); diff --git a/assets/src/block-validation/components/amp-validation-status/test/status-notification.js b/assets/src/block-validation/components/amp-validation-status/test/status-notification.js index b3409fe4af0..09134f270b0 100644 --- a/assets/src/block-validation/components/amp-validation-status/test/status-notification.js +++ b/assets/src/block-validation/components/amp-validation-status/test/status-notification.js @@ -14,10 +14,14 @@ import { useDispatch, useSelect } from '@wordpress/data'; */ import AMPValidationStatusNotification from '../status-notification'; -jest.mock('@wordpress/data/build/components/use-select', () => jest.fn()); -jest.mock('@wordpress/data/build/components/use-dispatch/use-dispatch', () => - jest.fn() -); +jest.mock('@wordpress/data', () => ({ + useSelect: jest.fn(), + useDispatch: jest.fn(), + combineReducers: jest.fn(), + createSelector: jest.fn(), + createReduxStore: jest.fn(), + register: jest.fn(), +})); describe('AMPValidationStatusNotification', () => { const autosave = jest.fn(); diff --git a/assets/src/block-validation/hooks/test/use-amp-document-toggle.js b/assets/src/block-validation/hooks/test/use-amp-document-toggle.js index d8ff1439baa..00f5b91c54c 100644 --- a/assets/src/block-validation/hooks/test/use-amp-document-toggle.js +++ b/assets/src/block-validation/hooks/test/use-amp-document-toggle.js @@ -14,10 +14,10 @@ import { useDispatch, useSelect } from '@wordpress/data'; */ import { useAMPDocumentToggle } from '../use-amp-document-toggle'; -jest.mock('@wordpress/data/build/components/use-select', () => jest.fn()); -jest.mock('@wordpress/data/build/components/use-dispatch/use-dispatch', () => - jest.fn() -); +jest.mock('@wordpress/data', () => ({ + useSelect: jest.fn(), + useDispatch: jest.fn(), +})); describe('useAMPDocumentToggle', () => { const editPost = jest.fn(); diff --git a/assets/src/block-validation/hooks/test/use-errors-fetching-state-changes.js b/assets/src/block-validation/hooks/test/use-errors-fetching-state-changes.js index c342596787b..98d583f4804 100644 --- a/assets/src/block-validation/hooks/test/use-errors-fetching-state-changes.js +++ b/assets/src/block-validation/hooks/test/use-errors-fetching-state-changes.js @@ -14,7 +14,11 @@ import { useSelect } from '@wordpress/data'; */ import { useErrorsFetchingStateChanges } from '../use-errors-fetching-state-changes'; -jest.mock('@wordpress/data/build/components/use-select', () => jest.fn()); +jest.mock('@wordpress/data', () => ({ + useSelect: jest.fn(), + createReduxStore: jest.fn(), + register: jest.fn(), +})); describe('useErrorsFetchingStateChanges', () => { function ComponentContainingHook() { diff --git a/assets/src/block-validation/hooks/test/use-post-dirty-state-updates.js b/assets/src/block-validation/hooks/test/use-post-dirty-state-updates.js index 6268364f0a2..36d31175ecf 100644 --- a/assets/src/block-validation/hooks/test/use-post-dirty-state-updates.js +++ b/assets/src/block-validation/hooks/test/use-post-dirty-state-updates.js @@ -2,7 +2,14 @@ * External dependencies */ import { render, act } from '@testing-library/react'; -import { beforeAll, describe, expect, it, jest } from '@jest/globals'; +import { + beforeAll, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; /** * WordPress dependencies @@ -21,11 +28,42 @@ import { import { usePostDirtyStateChanges } from '../use-post-dirty-state-changes'; import { store as blockValidationStore } from '../../store'; -jest.mock('@wordpress/data/build/components/use-select', () => jest.fn()); -jest.mock('@wordpress/compose/build/hooks/use-debounce', () => (fn) => fn); +const mockState = { isPostDirty: false }; + +jest.mock('@wordpress/data', () => ({ + useSelect: jest.fn(), + useDispatch: jest.fn(() => ({})), + createReduxStore: jest.fn((key, options) => ({ key, ...options })), + register: jest.fn(), + select: jest.fn((storeName) => { + if (storeName?.key === 'amp/block-validation') { + return { getIsPostDirty: jest.fn(() => mockState.isPostDirty) }; + } + return { getIsPostDirty: jest.fn(() => false) }; + }), + dispatch: jest.fn((storeName) => { + if (storeName === 'test/use-post-dirty-state-updates') { + return { + change: jest.fn(() => { + mockState.isPostDirty = true; + }), + }; + } + return { change: jest.fn() }; + }), + subscribe: jest.fn(() => jest.fn()), +})); + +const { useDispatch } = jest.requireMock('@wordpress/data'); +jest.mock('@wordpress/compose', () => ({ + ...jest.requireActual('@wordpress/compose'), + useDebounce: (fn) => fn, +})); describe('usePostDirtyStateChanges', () => { const getEditedPostContent = jest.fn(); + const setIsPostDirty = jest.fn(); + const setMaybeIsPostDirty = jest.fn(); function ComponentContainingHook() { usePostDirtyStateChanges(); @@ -38,12 +76,26 @@ describe('usePostDirtyStateChanges', () => { } function setupUseSelect(overrides) { - useSelect.mockImplementation(() => ({ + const settings = { getEditedPostContent, isSavingOrPreviewingPost: false, isPostDirty: select(blockValidationStore).getIsPostDirty(), ...overrides, - })); + }; + + // If saving/previewing, clear dirty state + if (settings.isSavingOrPreviewingPost) { + mockState.isPostDirty = false; + } + + useSelect.mockImplementation(() => settings); + } + + function setupUseDispatch() { + useDispatch.mockReturnValue({ + setIsPostDirty, + setMaybeIsPostDirty, + }); } beforeAll(() => { @@ -57,6 +109,11 @@ describe('usePostDirtyStateChanges', () => { ); }); + beforeEach(() => { + mockState.isPostDirty = false; + setupUseDispatch(); + }); + it('sets dirty state when content changes and clears it after save', () => { // Initial render. getEditedPostContent.mockReturnValue('initial'); @@ -98,4 +155,81 @@ describe('usePostDirtyStateChanges', () => { expect(select(blockValidationStore).getIsPostDirty()).toBe(true); }); + + it('calls setIsPostDirty(true) when updated content differs from initial content', () => { + // Mock the subscribe function to capture the listener + let subscribedListener; + const mockSubscribe = jest.fn((listener) => { + subscribedListener = listener; + return jest.fn(); // return unsubscribe function + }); + + const { subscribe } = jest.requireMock('@wordpress/data'); + subscribe.mockImplementation(mockSubscribe); + + // Initial render with initial content + getEditedPostContent.mockReturnValue('initial content'); + setupUseSelect({ + isPostDirty: false, + isSavingOrPreviewingPost: false, + }); + + const { rerender } = render(); + + // Verify setIsPostDirty was not called initially + expect(setIsPostDirty).not.toHaveBeenCalledWith(true); + + // Change the content returned by getEditedPostContent + getEditedPostContent.mockReturnValue('modified content'); + + // Trigger the listener to update updatedContent state + act(() => { + subscribedListener(); + }); + + // Re-render to trigger the useEffect that checks content !== updatedContent + act(() => { + rerender(); + }); + + // Verify setIsPostDirty(true) was called when content changed + expect(setIsPostDirty).toHaveBeenCalledWith(true); + }); + + it('calls setUpdatedContent when listener is triggered', () => { + // Mock the subscribe function to capture the listener + let subscribedListener; + const mockSubscribe = jest.fn((listener) => { + subscribedListener = listener; + return jest.fn(); // return unsubscribe function + }); + + // Import and mock the subscribe function + const { subscribe } = jest.requireMock('@wordpress/data'); + subscribe.mockImplementation(mockSubscribe); + + // Initial render + getEditedPostContent.mockReturnValue('initial'); + setupUseSelect({ + isPostDirty: false, // Ensure post is not dirty so subscription happens + isSavingOrPreviewingPost: false, + }); + + render(); + + // Verify subscribe was called + expect(subscribe).toHaveBeenCalledWith(expect.any(Function)); + expect(subscribedListener).toBeDefined(); + + // Change the content that getEditedPostContent returns + getEditedPostContent.mockReturnValue('updated content from listener'); + + // Trigger the listener (simulating store change) + act(() => { + subscribedListener(); + }); + + // Verify getEditedPostContent was called during listener execution + expect(getEditedPostContent).toHaveBeenCalledWith(); + }); }); diff --git a/assets/src/block-validation/hooks/test/use-validation-error-state-updates.js b/assets/src/block-validation/hooks/test/use-validation-error-state-updates.js index 5af3748d9ef..725c1cdd4af 100644 --- a/assets/src/block-validation/hooks/test/use-validation-error-state-updates.js +++ b/assets/src/block-validation/hooks/test/use-validation-error-state-updates.js @@ -2,7 +2,7 @@ * External dependencies */ import { render, waitFor } from '@testing-library/react'; -import { describe, expect, it, jest } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; /** * WordPress dependencies @@ -19,7 +19,35 @@ import { import { store as blockValidationStore } from '../../store'; // This allows us to tweak the returned value on each test -jest.mock('@wordpress/data/build/components/use-select', () => jest.fn()); +const mockValidationState = { errors: [] }; + +jest.mock('@wordpress/data', () => ({ + useSelect: jest.fn(), + useDispatch: jest.fn(() => ({ + setIsFetchingErrors: jest.fn(), + setFetchingErrorsRequestErrorMessage: jest.fn(), + setReviewLink: jest.fn(), + setSupportLink: jest.fn(), + setValidationErrors: jest.fn((errors) => { + mockValidationState.errors = errors || []; + }), + })), + createReduxStore: jest.fn((key, options) => ({ key, ...options })), + register: jest.fn(), + select: jest.fn((storeName) => { + if (storeName?.key === 'amp/block-validation') { + return { + getIsPostDirty: jest.fn(() => false), + getValidationErrors: jest.fn(() => mockValidationState.errors), + }; + } + return { + getIsPostDirty: jest.fn(() => false), + getValidationErrors: jest.fn(() => []), + }; + }), + dispatch: jest.fn(() => ({ change: jest.fn() })), +})); jest.mock( '@wordpress/api-fetch', @@ -27,14 +55,21 @@ jest.mock( new Promise((resolve) => { resolve({ review_link: 'http://site.test/wp-admin', - results: - require('../../store/test/__data__/raw-validation-errors') - .rawValidationErrors, + results: Array(8) + .fill() + .map((_, i) => ({ + code: `mock_error_${i}`, + message: `Mock validation error ${i}`, + })), }); }) ); describe('useValidationErrorStateUpdates', () => { + beforeEach(() => { + mockValidationState.errors = []; + }); + function ComponentContainingHook() { useValidationErrorStateUpdates(); diff --git a/assets/src/block-validation/store/test/store.js b/assets/src/block-validation/store/test/store.js index 18c1927f997..e2464e86cc4 100644 --- a/assets/src/block-validation/store/test/store.js +++ b/assets/src/block-validation/store/test/store.js @@ -52,6 +52,14 @@ describe('Block validation data store', () => { 'http://example.com' ); + dispatch(blockValidationStore).setSupportLink( + 'http://support.example.com' + ); + + expect(select(blockValidationStore).getSupportLink()).toBe( + 'http://support.example.com' + ); + expect(select(blockValidationStore).getAMPCompatibilityBroken()).toBe( false ); @@ -83,5 +91,15 @@ describe('Block validation data store', () => { dispatch(blockValidationStore).setIsFetchingErrors(false); expect(select(blockValidationStore).getIsFetchingErrors()).toBe(false); + + expect(select(blockValidationStore).getIsPostDirty()).toBe(false); + + dispatch(blockValidationStore).setIsPostDirty(true); + + expect(select(blockValidationStore).getIsPostDirty()).toBe(true); + + dispatch(blockValidationStore).setIsPostDirty(false); + + expect(select(blockValidationStore).getIsPostDirty()).toBe(false); }); }); diff --git a/assets/src/components/amp-setting-toggle/test/__snapshots__/index.js.snap b/assets/src/components/amp-setting-toggle/test/__snapshots__/index.js.snap index 30b98416830..2cf398c85f6 100644 --- a/assets/src/components/amp-setting-toggle/test/__snapshots__/index.js.snap +++ b/assets/src/components/amp-setting-toggle/test/__snapshots__/index.js.snap @@ -5,13 +5,13 @@ exports[`AMPSettingToggle matches snapshots 1`] = ` className="amp-setting-toggle" >