Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3402,12 +3402,6 @@ describe( 'URL validation', () => {
} )
);
} );

// Note: mailto: and tel: protocol URLs are handled by the validation logic
// (they skip URL constructor validation if they have a valid protocol),
// but testing them in the jsdom environment is problematic as the native
// URL constructor behavior may differ. These URLs are covered by the
// isURLLike validation which checks for valid protocols.
} );

describe( 'inputValue prop', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { SlotFillProvider } from '@wordpress/components';
import { createElement } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import LinkControl from '../';

vi.mock( import( '@wordpress/data' ), { spy: true } );

beforeEach( () => {
vi.mocked( useDispatch ).mockReturnValue( {
saveEntityRecords: vi.fn(),
} );
vi.mocked( useSelect ).mockReturnValue( {
fetchSearchSuggestions: () => Promise.resolve( [] ),
} );
} );

describe( 'LinkControl protocol validation', () => {
describe.each( [ 'Apply', 'Enter' ] )( 'submission with %s', ( submit ) => {
it.each( [ 'mailto:hello@wordpress.org', 'tel:123456789' ] )(
'should submit %s without changing its protocol',
async ( url ) => {
const user = userEvent.setup();
const onChange = vi.fn();
await render(
createElement(
SlotFillProvider,
null,
createElement( LinkControl, {
value: { url: 'https://wordpress.org' },
forceIsEditingLink: true,
onChange,
} )
)
);

const input = page.getByRole( 'combobox' );
await user.clear( input );
await user.type( input, url );
await expect
.element( page.getByRole( 'option' ) )
.toBeVisible();

if ( submit === 'Apply' ) {
await user.click(
page.getByRole( 'button', { name: 'Apply' } )
);
} else {
await user.keyboard( '{Enter}' );
}

await expect
.poll( () => onChange.mock.calls )
.toEqual( [ [ expect.objectContaining( { url } ) ] ] );
}
);
} );
} );
35 changes: 0 additions & 35 deletions packages/components/src/menu/test/index.jsdom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,6 @@ const waitForClosedMenu = () =>
expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument()
);

const openMenu = async ( user: ReturnType< typeof userEvent.setup > ) => {
await user.click( screen.getByRole( 'button', { name: 'Open dropdown' } ) );
await waitForFocusedMenu();
};

// TODO: Add browser coverage for the computed overflow once component stories
// use interaction tests. Jest mocks stylesheet imports, so this only verifies
// the handoff between Ariakit's inline lock and Modal's body class.
const isBodyScrollLocked = () =>
document.body.style.overflow === 'hidden' ||
document.body.classList.contains( 'modal-open' );

const resetTypeahead = () => {
act( () => vi.advanceTimersByTime( 500 ) );
};
Expand Down Expand Up @@ -253,29 +241,6 @@ describe( 'Menu', () => {
await waitFor( () => expect( trigger ).toHaveFocus() );
} );

it( 'should keep body scroll locked while handing off to a modal', async () => {
render( <MenuWithModal /> );

expect( isBodyScrollLocked() ).toBe( false );
await openMenu( user );
expect( isBodyScrollLocked() ).toBe( true );

await user.click(
screen.getByRole( 'menuitem', { name: 'Open modal' } )
);
await waitForClosedMenu();
expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
expect( isBodyScrollLocked() ).toBe( true );

await user.click(
screen.getByRole( 'button', { name: 'Close modal' } )
);
await waitFor( () =>
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument()
);
await waitFor( () => expect( isBodyScrollLocked() ).toBe( false ) );
} );

it( 'should stay open when `hideOnClick` is `false`', async () => {
render(
<Menu defaultOpen>
Expand Down
93 changes: 93 additions & 0 deletions packages/components/src/menu/test/scroll-lock.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, it } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { useState } from '@wordpress/element';
import { Menu } from '..';
import Modal from '../../modal';
// Load the body scroll-lock rule that WordPress enqueues for Modal.
// eslint-disable-next-line @wordpress/no-non-module-stylesheet-imports
import '../../modal/style.scss';
Comment on lines +8 to +9

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember you pinged me about these somewhere. We can exclude test files from this rule, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, I just wasan't sure if it was a good idea to exclude them systematically and wanted to make sure I heard your opinion first :)

I will tweak the rule.


const MenuWithModal = () => {
const [ isModalOpen, setIsModalOpen ] = useState( false );

return (
<>
<Menu>
<Menu.TriggerButton>Open dropdown</Menu.TriggerButton>
<Menu.Popover>
<Menu.Item onClick={ () => setIsModalOpen( true ) }>
Open modal
</Menu.Item>
</Menu.Popover>
</Menu>
{ isModalOpen && (
<Modal
title="Modal"
onRequestClose={ () => setIsModalOpen( false ) }
>
<button onClick={ () => setIsModalOpen( false ) }>
Close modal
</button>
</Modal>
) }
</>
);
};

describe( 'Menu scroll lock', () => {
it.each( [ 'pointer', 'keyboard' ] )(
'should keep body scroll locked when opening a modal with the %s and restore it after closing',
async ( interaction ) => {
const user = userEvent.setup();
const initialOverflow = getComputedStyle( document.body ).overflow;
expect( initialOverflow ).not.toBe( 'hidden' );
await render( <MenuWithModal /> );

if ( interaction === 'pointer' ) {
await user.click(
page.getByRole( 'button', { name: 'Open dropdown' } )
);
} else {
await user.tab();
await user.keyboard( '{ArrowDown}' );
}

await expect.element( page.getByRole( 'menu' ) ).toBeVisible();
await expect
.poll( () => getComputedStyle( document.body ).overflow )
.toBe( 'hidden' );

if ( interaction === 'pointer' ) {
await user.click(
page.getByRole( 'menuitem', { name: 'Open modal' } )
);
} else {
await user.keyboard( '{Enter}' );
}

await expect
.element( page.getByRole( 'menu' ) )
.not.toBeInTheDocument();
await expect.element( page.getByRole( 'dialog' ) ).toBeVisible();
expect( getComputedStyle( document.body ).overflow ).toBe(
'hidden'
);

if ( interaction === 'pointer' ) {
await user.click(
page.getByRole( 'button', { name: 'Close modal' } )
);
} else {
await user.keyboard( '{Escape}' );
}

await expect
.element( page.getByRole( 'dialog' ) )
.not.toBeInTheDocument();
await expect
.poll( () => getComputedStyle( document.body ).overflow )
.toBe( initialOverflow );
}
);
} );
Loading