diff --git a/routes/connectors-home/test/ai-plugin-callout.tsx b/routes/connectors-home/test/ai-plugin-callout.tsx new file mode 100644 index 00000000000000..6a239f43a6250b --- /dev/null +++ b/routes/connectors-home/test/ai-plugin-callout.tsx @@ -0,0 +1,201 @@ +/** + * External dependencies + */ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { AiPluginCallout } from '../ai-plugin-callout'; + +jest.mock( '@wordpress/a11y', () => ( { + speak: jest.fn(), +} ) ); + +jest.mock( '@wordpress/components', () => { + const { createElement, forwardRef } = + jest.requireActual( '@wordpress/element' ); + + return { + Button: forwardRef( + ( + { + href, + children, + onClick, + disabled, + }: { + href?: string; + children: unknown; + onClick?: () => void; + disabled?: boolean; + }, + ref + ) => + href + ? createElement( 'a', { ref, href }, children ) + : createElement( + 'button', + { ref, type: 'button', onClick, disabled }, + children + ) + ), + ExternalLink: ( { + href, + children, + }: { + href: string; + children: unknown; + } ) => createElement( 'a', { href }, children ), + }; +} ); + +jest.mock( '@wordpress/core-data', () => ( { + store: 'core', +} ) ); + +jest.mock( '@wordpress/data', () => ( { + useDispatch: jest.fn(), + useSelect: jest.fn(), + createSelector: jest.fn( ( fn ) => fn ), + createRegistrySelector: jest.fn( ( fn ) => fn ), + createReduxStore: jest.fn( () => ( {} ) ), + combineReducers: jest.fn( ( reducers ) => ( state = {}, action ) => { + const newState: Record< string, unknown > = {}; + + Object.keys( reducers ).forEach( ( key ) => { + newState[ key ] = reducers[ key ]( + ( state as Record< string, unknown > )[ key ], + action + ); + } ); + + return newState; + } ), + register: jest.fn(), +} ) ); + +jest.mock( '../default-connectors', () => ( { + getConnectorData: jest.fn( () => ( { + openai: { + type: 'ai_provider', + authentication: { + method: 'api_key', + settingName: 'connectors_ai_openai_api_key', + isConnected: false, + }, + }, + } ) ), +} ) ); + +jest.mock( '../wp-logo-decoration', () => ( { + WpLogoDecoration: () => null, +} ) ); + +type StoreState = { + canCreate: boolean; + hasFinishedResolution: boolean; + plugin?: { plugin: string; status: string }; + siteSettings?: Record< string, string >; +}; + +const mockSaveEntityRecord = jest.fn(); +const mockCreateSuccessNotice = jest.fn(); +const mockCreateErrorNotice = jest.fn(); + +describe( 'AiPluginCallout', () => { + let storeState: StoreState; + let selectorStore: { + canUser: jest.Mock; + getEntityRecord: jest.Mock; + hasFinishedResolution: jest.Mock; + }; + + beforeEach( () => { + storeState = { + canCreate: true, + hasFinishedResolution: true, + plugin: { + plugin: 'ai/ai', + status: 'active', + }, + siteSettings: {}, + }; + + selectorStore = { + canUser: jest.fn( () => storeState.canCreate ), + getEntityRecord: jest.fn( + ( kind: string, name: string, id?: string ) => { + if ( kind === 'root' && name === 'site' ) { + return storeState.siteSettings; + } + + if ( + kind === 'root' && + name === 'plugin' && + id === 'ai/ai' + ) { + return storeState.plugin; + } + + return undefined; + } + ), + hasFinishedResolution: jest.fn( + () => storeState.hasFinishedResolution + ), + }; + + ( useSelect as jest.Mock ).mockImplementation( + ( mapSelect: ( select: () => typeof selectorStore ) => unknown ) => + mapSelect( () => selectorStore ) + ); + + mockSaveEntityRecord.mockReset(); + mockSaveEntityRecord.mockResolvedValue( undefined ); + mockCreateSuccessNotice.mockReset(); + mockCreateErrorNotice.mockReset(); + ( useDispatch as jest.Mock ).mockReturnValue( { + saveEntityRecord: mockSaveEntityRecord, + createSuccessNotice: mockCreateSuccessNotice, + createErrorNotice: mockCreateErrorNotice, + } ); + } ); + + it( 'links to the renamed AI settings page when the plugin is active without a connected provider', () => { + render( ); + + expect( + screen.getByRole( 'link', { + name: 'Control features in the AI plugin', + } ) + ).toHaveAttribute( 'href', 'options-general.php?page=ai-wp-admin' ); + } ); + + it( 'installs the AI plugin using the plugin slug', async () => { + const user = userEvent.setup(); + + storeState.plugin = undefined; + + render( ); + + await user.click( + screen.getByRole( 'button', { name: 'Install the AI plugin' } ) + ); + + await waitFor( () => { + expect( mockSaveEntityRecord ).toHaveBeenCalledWith( + 'root', + 'plugin', + { slug: 'ai', status: 'active' }, + { throwOnError: true } + ); + } ); + } ); +} );