Skip to content

Commit 3e89d97

Browse files
ciampomirka
authored andcommitted
Tests: Cover Menu scroll lock and LinkControl protocols in browsers (WordPress#83991)
Co-authored-by: ciampo <mciampini@git.wordpress.org> Co-authored-by: mirka <0mirka00@git.wordpress.org>
1 parent ee5f974 commit 3e89d97

4 files changed

Lines changed: 153 additions & 41 deletions

File tree

‎packages/block-editor/src/components/link-control/test/index.jsdom.test.jsx‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3402,12 +3402,6 @@ describe( 'URL validation', () => {
34023402
} )
34033403
);
34043404
} );
3405-
3406-
// Note: mailto: and tel: protocol URLs are handled by the validation logic
3407-
// (they skip URL constructor validation if they have a valid protocol),
3408-
// but testing them in the jsdom environment is problematic as the native
3409-
// URL constructor behavior may differ. These URLs are covered by the
3410-
// isURLLike validation which checks for valid protocols.
34113405
} );
34123406

34133407
describe( 'inputValue prop', () => {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { page, userEvent } from 'vitest/browser';
3+
import { render } from 'vitest-browser-react';
4+
import { SlotFillProvider } from '@wordpress/components';
5+
import { createElement } from '@wordpress/element';
6+
import { useDispatch, useSelect } from '@wordpress/data';
7+
import LinkControl from '../';
8+
9+
vi.mock( import( '@wordpress/data' ), { spy: true } );
10+
11+
beforeEach( () => {
12+
vi.mocked( useDispatch ).mockReturnValue( {
13+
saveEntityRecords: vi.fn(),
14+
} );
15+
vi.mocked( useSelect ).mockReturnValue( {
16+
fetchSearchSuggestions: () => Promise.resolve( [] ),
17+
} );
18+
} );
19+
20+
describe( 'LinkControl protocol validation', () => {
21+
describe.each( [ 'Apply', 'Enter' ] )( 'submission with %s', ( submit ) => {
22+
it.each( [ 'mailto:hello@wordpress.org', 'tel:123456789' ] )(
23+
'should submit %s without changing its protocol',
24+
async ( url ) => {
25+
const user = userEvent.setup();
26+
const onChange = vi.fn();
27+
await render(
28+
createElement(
29+
SlotFillProvider,
30+
null,
31+
createElement( LinkControl, {
32+
value: { url: 'https://wordpress.org' },
33+
forceIsEditingLink: true,
34+
onChange,
35+
} )
36+
)
37+
);
38+
39+
const input = page.getByRole( 'combobox' );
40+
await user.clear( input );
41+
await user.type( input, url );
42+
await expect
43+
.element( page.getByRole( 'option' ) )
44+
.toBeVisible();
45+
46+
if ( submit === 'Apply' ) {
47+
await user.click(
48+
page.getByRole( 'button', { name: 'Apply' } )
49+
);
50+
} else {
51+
await user.keyboard( '{Enter}' );
52+
}
53+
54+
await expect
55+
.poll( () => onChange.mock.calls )
56+
.toEqual( [ [ expect.objectContaining( { url } ) ] ] );
57+
}
58+
);
59+
} );
60+
} );

‎packages/components/src/menu/test/index.jsdom.test.tsx‎

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,6 @@ const waitForClosedMenu = () =>
2121
expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument()
2222
);
2323

24-
const openMenu = async ( user: ReturnType< typeof userEvent.setup > ) => {
25-
await user.click( screen.getByRole( 'button', { name: 'Open dropdown' } ) );
26-
await waitForFocusedMenu();
27-
};
28-
29-
// TODO: Add browser coverage for the computed overflow once component stories
30-
// use interaction tests. Jest mocks stylesheet imports, so this only verifies
31-
// the handoff between Ariakit's inline lock and Modal's body class.
32-
const isBodyScrollLocked = () =>
33-
document.body.style.overflow === 'hidden' ||
34-
document.body.classList.contains( 'modal-open' );
35-
3624
const resetTypeahead = () => {
3725
act( () => vi.advanceTimersByTime( 500 ) );
3826
};
@@ -253,29 +241,6 @@ describe( 'Menu', () => {
253241
await waitFor( () => expect( trigger ).toHaveFocus() );
254242
} );
255243

256-
it( 'should keep body scroll locked while handing off to a modal', async () => {
257-
render( <MenuWithModal /> );
258-
259-
expect( isBodyScrollLocked() ).toBe( false );
260-
await openMenu( user );
261-
expect( isBodyScrollLocked() ).toBe( true );
262-
263-
await user.click(
264-
screen.getByRole( 'menuitem', { name: 'Open modal' } )
265-
);
266-
await waitForClosedMenu();
267-
expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
268-
expect( isBodyScrollLocked() ).toBe( true );
269-
270-
await user.click(
271-
screen.getByRole( 'button', { name: 'Close modal' } )
272-
);
273-
await waitFor( () =>
274-
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument()
275-
);
276-
await waitFor( () => expect( isBodyScrollLocked() ).toBe( false ) );
277-
} );
278-
279244
it( 'should stay open when `hideOnClick` is `false`', async () => {
280245
render(
281246
<Menu defaultOpen>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { page, userEvent } from 'vitest/browser';
3+
import { render } from 'vitest-browser-react';
4+
import { useState } from '@wordpress/element';
5+
import { Menu } from '..';
6+
import Modal from '../../modal';
7+
// Load the body scroll-lock rule that WordPress enqueues for Modal.
8+
// eslint-disable-next-line @wordpress/no-non-module-stylesheet-imports
9+
import '../../modal/style.scss';
10+
11+
const MenuWithModal = () => {
12+
const [ isModalOpen, setIsModalOpen ] = useState( false );
13+
14+
return (
15+
<>
16+
<Menu>
17+
<Menu.TriggerButton>Open dropdown</Menu.TriggerButton>
18+
<Menu.Popover>
19+
<Menu.Item onClick={ () => setIsModalOpen( true ) }>
20+
Open modal
21+
</Menu.Item>
22+
</Menu.Popover>
23+
</Menu>
24+
{ isModalOpen && (
25+
<Modal
26+
title="Modal"
27+
onRequestClose={ () => setIsModalOpen( false ) }
28+
>
29+
<button onClick={ () => setIsModalOpen( false ) }>
30+
Close modal
31+
</button>
32+
</Modal>
33+
) }
34+
</>
35+
);
36+
};
37+
38+
describe( 'Menu scroll lock', () => {
39+
it.each( [ 'pointer', 'keyboard' ] )(
40+
'should keep body scroll locked when opening a modal with the %s and restore it after closing',
41+
async ( interaction ) => {
42+
const user = userEvent.setup();
43+
const initialOverflow = getComputedStyle( document.body ).overflow;
44+
expect( initialOverflow ).not.toBe( 'hidden' );
45+
await render( <MenuWithModal /> );
46+
47+
if ( interaction === 'pointer' ) {
48+
await user.click(
49+
page.getByRole( 'button', { name: 'Open dropdown' } )
50+
);
51+
} else {
52+
await user.tab();
53+
await user.keyboard( '{ArrowDown}' );
54+
}
55+
56+
await expect.element( page.getByRole( 'menu' ) ).toBeVisible();
57+
await expect
58+
.poll( () => getComputedStyle( document.body ).overflow )
59+
.toBe( 'hidden' );
60+
61+
if ( interaction === 'pointer' ) {
62+
await user.click(
63+
page.getByRole( 'menuitem', { name: 'Open modal' } )
64+
);
65+
} else {
66+
await user.keyboard( '{Enter}' );
67+
}
68+
69+
await expect
70+
.element( page.getByRole( 'menu' ) )
71+
.not.toBeInTheDocument();
72+
await expect.element( page.getByRole( 'dialog' ) ).toBeVisible();
73+
expect( getComputedStyle( document.body ).overflow ).toBe(
74+
'hidden'
75+
);
76+
77+
if ( interaction === 'pointer' ) {
78+
await user.click(
79+
page.getByRole( 'button', { name: 'Close modal' } )
80+
);
81+
} else {
82+
await user.keyboard( '{Escape}' );
83+
}
84+
85+
await expect
86+
.element( page.getByRole( 'dialog' ) )
87+
.not.toBeInTheDocument();
88+
await expect
89+
.poll( () => getComputedStyle( document.body ).overflow )
90+
.toBe( initialOverflow );
91+
}
92+
);
93+
} );

0 commit comments

Comments
 (0)