Skip to content

Commit bc0e1dc

Browse files
gmjuhaszmatticbot
authored andcommitted
Social: Add admin page unit tests (#41951)
* Add unit tests for the social admin page * Add structural unit tests to the admin page * changelog * Flip feature flag logic Committed via a GitHub action: https://github.com/Automattic/jetpack/actions/runs/13539999216 Upstream-Ref: Automattic/jetpack@8af3587
1 parent ec25255 commit bc0e1dc

File tree

5 files changed

+355
-0
lines changed

5 files changed

+355
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ This is an alpha version! The changes listed here are not final.
1212
### Security
1313
- Social: Moved Mastodon input form to start
1414

15+
### Added
16+
- Added unit tests for the admin page
17+
1518
### Changed
1619
- Social | Improve connect URL generation
1720

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// eslint-disable-next-line import/order -- This is a test file and we need to import the mocks first
2+
import { mockStore } from '../../../utils/test-mocks.js';
3+
import { isJetpackSelfHostedSite } from '@automattic/jetpack-script-data';
4+
import { render, screen } from '@testing-library/react';
5+
import { hasSocialPaidFeatures } from '../../../utils';
6+
import AdminPageHeader from '../page-header';
7+
8+
describe( 'AdminPageHeader', () => {
9+
beforeEach( () => {
10+
jest.clearAllMocks();
11+
mockStore();
12+
} );
13+
14+
it( 'should show license text when no paid features and is Jetpack site', () => {
15+
hasSocialPaidFeatures.mockReturnValue( false );
16+
isJetpackSelfHostedSite.mockReturnValue( true );
17+
18+
render( <AdminPageHeader /> );
19+
expect(
20+
screen.getByText( /Already have an existing plan or license key\?/i )
21+
).toBeInTheDocument();
22+
expect( screen.getByRole( 'link' ) ).toHaveAttribute(
23+
'href',
24+
'https://example.com/add-license'
25+
);
26+
} );
27+
28+
it( 'should not show license text when has paid features', () => {
29+
hasSocialPaidFeatures.mockReturnValue( true );
30+
isJetpackSelfHostedSite.mockReturnValue( true );
31+
32+
render( <AdminPageHeader /> );
33+
expect(
34+
screen.queryByText( /Already have an existing plan or license key\?/i )
35+
).not.toBeInTheDocument();
36+
} );
37+
38+
it( 'should not show license text when not a Jetpack site', () => {
39+
hasSocialPaidFeatures.mockReturnValue( false );
40+
isJetpackSelfHostedSite.mockReturnValue( false );
41+
42+
render( <AdminPageHeader /> );
43+
expect(
44+
screen.queryByText( /Already have an existing plan or license key\?/i )
45+
).not.toBeInTheDocument();
46+
} );
47+
} );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// eslint-disable-next-line import/order -- This is a test file and we need to import the mocks first
2+
import { mockStore } from '../../../utils/test-mocks';
3+
import { useConnection } from '@automattic/jetpack-connection';
4+
import { isJetpackSelfHostedSite, siteHasFeature } from '@automattic/jetpack-script-data';
5+
import { render, screen } from '@testing-library/react';
6+
import { SocialAdminPage } from '../';
7+
import { getSocialScriptData } from '../../../utils';
8+
9+
// Mock child components to simplify testing - We only test the SocialAdminPage component here
10+
jest.mock( '../connection-screen', () => () => <div data-testid="connection-screen" /> );
11+
jest.mock( '../header', () => () => <div data-testid="header" /> );
12+
jest.mock( '../info-section', () => () => <div data-testid="info-section" /> );
13+
jest.mock( '../page-header', () => () => <div data-testid="page-header" /> );
14+
jest.mock( '../pricing-page', () => ( { onDismiss } ) => (
15+
<button data-testid="pricing-page" onClick={ onDismiss } onKeyDown={ onDismiss } />
16+
) );
17+
jest.mock( '../support-section', () => () => <div data-testid="support-section" /> );
18+
jest.mock( '../toggles/social-image-generator-toggle', () => () => (
19+
<div data-testid="social-image-generator-toggle" />
20+
) );
21+
jest.mock( '../toggles/social-module-toggle', () => () => (
22+
<div data-testid="social-module-toggle" />
23+
) );
24+
jest.mock( '../toggles/social-notes-toggle', () => () => (
25+
<div data-testid="social-notes-toggle" />
26+
) );
27+
jest.mock( '../toggles/utm-toggle', () => () => <div data-testid="utm-toggle" /> );
28+
29+
describe( 'SocialAdminPage', () => {
30+
beforeEach( () => {
31+
jest.clearAllMocks();
32+
mockStore();
33+
useConnection.mockReturnValue( {
34+
isUserConnected: true,
35+
isRegistered: true,
36+
} );
37+
isJetpackSelfHostedSite.mockReturnValue( true );
38+
siteHasFeature.mockReturnValue( true );
39+
getSocialScriptData.mockReturnValue( {
40+
plugin_info: {
41+
social: { version: '1.0.0' },
42+
jetpack: { version: '1.0.0' },
43+
},
44+
} );
45+
} );
46+
47+
describe( 'Page rendering', () => {
48+
it( 'should render connection screen when not connected', () => {
49+
useConnection.mockReturnValue( {
50+
isUserConnected: false,
51+
isRegistered: false,
52+
} );
53+
54+
render( <SocialAdminPage /> );
55+
expect( screen.getByTestId( 'connection-screen' ) ).toBeInTheDocument();
56+
} );
57+
58+
it( 'should render main admin page when connected', () => {
59+
render( <SocialAdminPage /> );
60+
61+
expect( screen.getByTestId( 'page-header' ) ).toBeInTheDocument();
62+
expect( screen.getByTestId( 'header' ) ).toBeInTheDocument();
63+
expect( screen.getByTestId( 'info-section' ) ).toBeInTheDocument();
64+
expect( screen.getByTestId( 'support-section' ) ).toBeInTheDocument();
65+
} );
66+
67+
it( 'should render pricing page when showPricingPage is true and no paid features', () => {
68+
mockStore( {
69+
getSocialSettings: () => ( { showPricingPage: true } ),
70+
} );
71+
72+
render( <SocialAdminPage /> );
73+
expect( screen.getByTestId( 'pricing-page' ) ).toBeInTheDocument();
74+
} );
75+
} );
76+
77+
describe( 'Toggle visibility', () => {
78+
describe( 'UTM toggle', () => {
79+
it( 'should show when module is enabled', () => {
80+
render( <SocialAdminPage /> );
81+
expect( screen.getByTestId( 'utm-toggle' ) ).toBeInTheDocument();
82+
} );
83+
84+
it( 'should not show when module is disabled', () => {
85+
mockStore( {
86+
getSocialModuleSettings: () => ( { publicize: false } ),
87+
} );
88+
render( <SocialAdminPage /> );
89+
expect( screen.queryByTestId( 'utm-toggle' ) ).not.toBeInTheDocument();
90+
} );
91+
} );
92+
93+
describe( 'Social Notes toggle', () => {
94+
it( 'should show when plugin is active and module is enabled', () => {
95+
render( <SocialAdminPage /> );
96+
expect( screen.getByTestId( 'social-notes-toggle' ) ).toBeInTheDocument();
97+
} );
98+
99+
it( 'should not show when plugin is not active', () => {
100+
getSocialScriptData.mockReturnValue( {
101+
plugin_info: {
102+
social: { version: null },
103+
jetpack: { version: '1.0.0' },
104+
},
105+
} );
106+
render( <SocialAdminPage /> );
107+
expect( screen.queryByTestId( 'social-notes-toggle' ) ).not.toBeInTheDocument();
108+
} );
109+
110+
it( 'should not show when module is disabled', () => {
111+
mockStore( {
112+
getSocialModuleSettings: () => ( { publicize: false } ),
113+
} );
114+
render( <SocialAdminPage /> );
115+
expect( screen.queryByTestId( 'social-notes-toggle' ) ).not.toBeInTheDocument();
116+
} );
117+
} );
118+
119+
describe( 'Social Image Generator toggle', () => {
120+
it( 'should show when feature is available and module is enabled', () => {
121+
render( <SocialAdminPage /> );
122+
expect( screen.getByTestId( 'social-image-generator-toggle' ) ).toBeInTheDocument();
123+
} );
124+
125+
it( 'should not show when feature is not available', () => {
126+
siteHasFeature.mockReturnValue( false );
127+
render( <SocialAdminPage /> );
128+
expect( screen.queryByTestId( 'social-image-generator-toggle' ) ).not.toBeInTheDocument();
129+
} );
130+
131+
it( 'should not show when module is disabled', () => {
132+
mockStore( {
133+
getSocialModuleSettings: () => ( { publicize: false } ),
134+
} );
135+
render( <SocialAdminPage /> );
136+
expect( screen.queryByTestId( 'social-image-generator-toggle' ) ).not.toBeInTheDocument();
137+
} );
138+
} );
139+
} );
140+
} );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// eslint-disable-next-line import/order -- This is a test file and we need to import the mocks first
2+
import { mockStore } from '../../../utils/test-mocks';
3+
import { getScriptData } from '@automattic/jetpack-script-data';
4+
import { render, screen } from '@testing-library/react';
5+
import { useDispatch } from '@wordpress/data';
6+
import { getSocialScriptData, hasSocialPaidFeatures } from '../../../utils';
7+
import SocialModuleToggle from '../toggles/social-module-toggle';
8+
9+
jest.mock( '../../connection-management', () => () => <div data-testid="connection-management" /> );
10+
11+
describe( 'SocialModuleToggle', () => {
12+
const mockUpdateSettings = jest.fn();
13+
14+
beforeEach( () => {
15+
jest.clearAllMocks();
16+
mockStore();
17+
18+
// Mock dispatch to capture updateSocialModuleSettings calls
19+
useDispatch.mockReturnValue( {
20+
updateSocialModuleSettings: mockUpdateSettings,
21+
} );
22+
23+
// Default script data mocks
24+
getScriptData.mockReturnValue( {
25+
site: {
26+
wpcom: { blog_id: '123' },
27+
},
28+
} );
29+
30+
getSocialScriptData.mockReturnValue( {
31+
urls: { connectionsManagementPage: 'https://example.com/connections' },
32+
feature_flags: { useAdminUiV1: true },
33+
is_publicize_enabled: true,
34+
} );
35+
} );
36+
37+
it( 'should render connection management component by default', () => {
38+
render( <SocialModuleToggle /> );
39+
40+
expect( screen.getByTestId( 'connection-management' ) ).toBeInTheDocument();
41+
expect( screen.queryByText( /Manage social media connections/i ) ).not.toBeInTheDocument();
42+
} );
43+
44+
it( 'should render legacy UI when useAdminUiV1 is false', () => {
45+
getSocialScriptData.mockReturnValue( {
46+
urls: { connectionsManagementPage: 'https://example.com/connections' },
47+
feature_flags: { useAdminUiV1: false },
48+
is_publicize_enabled: true,
49+
} );
50+
51+
render( <SocialModuleToggle /> );
52+
53+
expect( screen.getByText( /Manage social media connections/i ) ).toBeInTheDocument();
54+
expect( screen.getByText( /Manage social media connections/i ) ).toBeEnabled();
55+
} );
56+
57+
it( 'should render with module disabled', () => {
58+
getSocialScriptData.mockReturnValue( {
59+
urls: { connectionsManagementPage: 'https://example.com/connections' },
60+
feature_flags: { useAdminUiV1: false },
61+
is_publicize_enabled: true,
62+
} );
63+
64+
mockStore( {
65+
getSocialModuleSettings: () => ( { publicize: false } ),
66+
} );
67+
68+
render( <SocialModuleToggle /> );
69+
70+
expect( screen.getByText( /Manage social media connections/i ) ).toBeInTheDocument();
71+
expect( screen.getByText( /Manage social media connections/i ) ).toBeDisabled();
72+
} );
73+
74+
it( 'should show upgrade trigger when no paid features', () => {
75+
render( <SocialModuleToggle /> );
76+
77+
expect( screen.getByText( /Unlock advanced sharing options/i ) ).toBeInTheDocument();
78+
} );
79+
80+
it( 'should not show upgrade trigger with paid features', () => {
81+
hasSocialPaidFeatures.mockReturnValue( true );
82+
render( <SocialModuleToggle /> );
83+
84+
expect( screen.queryByText( /Unlock advanced sharing options/i ) ).not.toBeInTheDocument();
85+
} );
86+
} );

src/utils/test-mocks.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { jest } from '@jest/globals';
2+
3+
// Dependencies mocks
4+
jest.mock( '@wordpress/data', () => ( {
5+
useSelect: jest.fn(),
6+
useDispatch: jest.fn(),
7+
createSelector: jest.fn(),
8+
createReduxStore: jest.fn(),
9+
register: jest.fn(),
10+
combineReducers: jest.fn( () => () => ( {} ) ),
11+
createRegistrySelector: jest.fn( fn => fn ),
12+
} ) );
13+
14+
jest.mock( '@automattic/jetpack-connection', () => ( {
15+
useConnection: jest.fn(),
16+
} ) );
17+
18+
jest.mock( '@automattic/jetpack-script-data', () => ( {
19+
getScriptData: jest.fn(),
20+
isWpcomPlatformSite: jest.fn(),
21+
isJetpackSelfHostedSite: jest.fn(),
22+
isSimpleSite: jest.fn(),
23+
siteHasFeature: jest.fn(),
24+
getMyJetpackUrl: jest.fn( () => 'https://example.com/add-license' ),
25+
} ) );
26+
27+
jest.mock( '@wordpress/editor', () => ( {
28+
store: 'core/editor',
29+
} ) );
30+
31+
jest.mock( '@wordpress/core-data', () => ( {
32+
store: 'core',
33+
} ) );
34+
35+
jest.mock( '@automattic/jetpack-components', () => ( {
36+
AdminPage: ( { children, header } ) => (
37+
<div>
38+
{ header }
39+
{ children }
40+
</div>
41+
),
42+
AdminSection: ( { children } ) => <div>{ children }</div>,
43+
AdminSectionHero: ( { children } ) => <div>{ children }</div>,
44+
Container: ( { children } ) => <div>{ children }</div>,
45+
Col: ( { children } ) => <div>{ children }</div>,
46+
GlobalNotices: () => null,
47+
Button: ( { children, disabled, onClick } ) => (
48+
<button disabled={ disabled } onClick={ onClick }>
49+
{ children }
50+
</button>
51+
),
52+
Text: ( { children } ) => <div>{ children }</div>,
53+
ContextualUpgradeTrigger: ( { description } ) => <div>{ description }</div>,
54+
getRedirectUrl: jest.fn(),
55+
useBreakpointMatch: jest.fn().mockReturnValue( [ false ] ),
56+
} ) );
57+
58+
jest.mock( './', () => ( {
59+
...jest.requireActual( './' ),
60+
getSocialScriptData: jest.fn( () => ( {
61+
plugin_info: {
62+
social: { version: '1.0.0' },
63+
jetpack: { version: '1.0.0' },
64+
},
65+
} ) ),
66+
hasSocialPaidFeatures: jest.fn( () => false ),
67+
} ) );
68+
69+
// Store
70+
const defaultStore = {
71+
getSocialModuleSettings: () => ( { publicize: true } ),
72+
getSocialSettings: () => ( { showPricingPage: false } ),
73+
isSavingSocialModuleSettings: () => false,
74+
};
75+
76+
export const mockStore = ( overrides = {} ) => {
77+
const store = { ...defaultStore, ...overrides };
78+
jest.requireMock( '@wordpress/data' ).useSelect.mockImplementation( fn => fn( () => store ) );
79+
};

0 commit comments

Comments
 (0)