diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2b113193..cce9aa34 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -29,14 +29,12 @@ jobs: event: ['${{ github.event_name }}'] shard: [1, 2, 3, 4] node: ['22', '24'] - wp: ['6.2', 'latest', 'trunk'] + wp: ['6.2', '6.3', '6.8', 'latest', 'trunk'] exclude: # On PRs: only test Node 22 + WP latest and trunk for fast feedback # On trunk: full matrix with node 24 and minimum WP version - event: 'pull_request' node: '24' - - event: 'pull_request' - wp: '6.2' env: WP_ENV_CORE: ${{ matrix.wp == 'trunk' && 'WordPress/WordPress' || (matrix.wp != 'latest' && format('WordPress/WordPress#{0}', matrix.wp) || null) }} diff --git a/assets/src/js/commands/admin-commands.js b/assets/src/js/commands/admin-commands.js index 9dcb6013..1dedcfc5 100644 --- a/assets/src/js/commands/admin-commands.js +++ b/assets/src/js/commands/admin-commands.js @@ -12,9 +12,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { createElement } from '@wordpress/element'; -import { Icon } from '@wordpress/components'; -import { dispatch } from '@wordpress/data'; +import { dispatch, select } from '@wordpress/data'; import { addQueryArgs } from '@wordpress/url'; import { layout, @@ -31,12 +29,12 @@ import { * Register admin commands for SCF */ const registerAdminCommands = () => { - if ( ! dispatch( 'core/commands' ) || ! window.acf?.data ) { + if ( ! select( 'core/commands') || ! dispatch( 'core/commands' ) ) { return; } + const registeredCommands = select( 'core/commands' ).getCommands(); const commandStore = dispatch( 'core/commands' ); - const adminUrl = window.acf?.data?.admin_url || ''; const viewCommands = [ { @@ -45,10 +43,6 @@ const registerAdminCommands = () => { url: 'edit.php', urlArgs: { post_type: 'acf-field-group' }, icon: layout, - description: __( - 'SCF: View and manage custom field groups', - 'secure-custom-fields' - ), keywords: [ 'acf', 'custom fields', @@ -62,10 +56,6 @@ const registerAdminCommands = () => { url: 'edit.php', urlArgs: { post_type: 'acf-post-type' }, icon: postList, - description: __( - 'SCF: Manage custom post types', - 'secure-custom-fields' - ), keywords: [ 'cpt', 'content types', 'manage post types' ], }, { @@ -74,10 +64,6 @@ const registerAdminCommands = () => { url: 'edit.php', urlArgs: { post_type: 'acf-taxonomy' }, icon: category, - description: __( - 'SCF: Manage custom taxonomies for organizing content', - 'secure-custom-fields' - ), keywords: [ 'categories', 'tags', 'terms', 'custom taxonomies' ], }, { @@ -86,10 +72,6 @@ const registerAdminCommands = () => { url: 'edit.php', urlArgs: { post_type: 'acf-ui-options-page' }, icon: settings, - description: __( - 'SCF: Manage custom options pages for global settings', - 'secure-custom-fields' - ), keywords: [ 'settings', 'global options', 'site options' ], }, { @@ -98,10 +80,6 @@ const registerAdminCommands = () => { url: 'admin.php', urlArgs: { page: 'acf-tools' }, icon: tool, - description: __( - 'SCF: Access SCF utility tools', - 'secure-custom-fields' - ), keywords: [ 'utilities', 'import export', 'json' ], }, { @@ -110,10 +88,6 @@ const registerAdminCommands = () => { url: 'admin.php', urlArgs: { page: 'acf-tools', tool: 'import' }, icon: upload, - description: __( - 'SCF: Import field groups, post types, taxonomies, and options pages', - 'secure-custom-fields' - ), keywords: [ 'upload', 'json', 'migration', 'transfer' ], }, { @@ -122,10 +96,6 @@ const registerAdminCommands = () => { url: 'admin.php', urlArgs: { page: 'acf-tools', tool: 'export' }, icon: download, - description: __( - 'SCF: Export field groups, post types, taxonomies, and options pages', - 'secure-custom-fields' - ), keywords: [ 'download', 'json', 'backup', 'migration' ], }, ]; @@ -138,10 +108,6 @@ const registerAdminCommands = () => { url: 'post-new.php', urlArgs: { post_type: 'acf-field-group' }, icon: plus, - description: __( - 'SCF: Create a new field group to organize custom fields', - 'secure-custom-fields' - ), keywords: [ 'add', 'new', @@ -156,10 +122,6 @@ const registerAdminCommands = () => { url: 'post-new.php', urlArgs: { post_type: 'acf-post-type' }, icon: plus, - description: __( - 'SCF: Create a new custom post type', - 'secure-custom-fields' - ), keywords: [ 'add', 'new', 'create', 'cpt', 'content type' ], }, { @@ -168,10 +130,6 @@ const registerAdminCommands = () => { url: 'post-new.php', urlArgs: { post_type: 'acf-taxonomy' }, icon: plus, - description: __( - 'SCF: Create a new custom taxonomy', - 'secure-custom-fields' - ), keywords: [ 'add', 'new', @@ -187,10 +145,6 @@ const registerAdminCommands = () => { url: 'post-new.php', urlArgs: { post_type: 'acf-ui-options-page' }, icon: plus, - description: __( - 'SCF: Create a new custom options page', - 'secure-custom-fields' - ), keywords: [ 'add', 'new', 'create', 'options', 'settings page' ], }, ]; @@ -199,27 +153,36 @@ const registerAdminCommands = () => { commandStore.registerCommand( { name: 'scf/' + command.name, label: command.label, - icon: createElement( Icon, { icon: command.icon } ), + icon: command.icon, context: 'admin', - description: command.description, keywords: command.keywords, callback: ( { close } ) => { - document.location = command.urlArgs - ? addQueryArgs( adminUrl + command.url, command.urlArgs ) - : adminUrl + command.url; + document.location = addQueryArgs( + command.url, + command.urlArgs + ); close(); }, } ); }; // WordPress 6.9+ adds Command Palette commands for all admin menu items. - const wpVersion = window.acf.data.wp_version; - const isWp69Plus = - wpVersion.localeCompare( '6.9', undefined, { numeric: true } ) >= 0; + // For older versions, we need to register them manually. The most reliable way to + // detect this is to check if the commands are already registered. + viewCommands.forEach( ( command ) => { + const commandUrl = addQueryArgs( command.url, command.urlArgs ); + // WordPress stores destination URLs in the command *name*, appended to + // the menu slug (which is also a relative URL), resulting in somewhat + // peculiar naming, e.g. + // edit.php?post_type=acf-field-group-edit.php?post_type=acf-ui-options-page + if ( registeredCommands.some( ( cmd ) => cmd.name.endsWith( commandUrl ) ) ) { + return; + } + registerCommand( command ); + } ); - if ( ! isWp69Plus ) { - viewCommands.forEach( registerCommand ); - } + // "Create New" commands are not automatically registered by WordPress, + // so we always register them. createCommands.forEach( registerCommand ); }; diff --git a/assets/src/js/commands/custom-post-type-commands.js b/assets/src/js/commands/custom-post-type-commands.js index 1240996b..8f4a611c 100644 --- a/assets/src/js/commands/custom-post-type-commands.js +++ b/assets/src/js/commands/custom-post-type-commands.js @@ -15,8 +15,6 @@ * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { createElement } from '@wordpress/element'; -import { Icon } from '@wordpress/components'; import { dispatch } from '@wordpress/data'; import { addQueryArgs } from '@wordpress/url'; import { page, plus, edit } from '@wordpress/icons'; @@ -34,7 +32,6 @@ const registerPostTypeCommands = () => { } const commandStore = dispatch( 'core/commands' ); - const adminUrl = window.acf.data.admin_url || ''; const postTypes = window.acf.data.customPostTypes; // WordPress 6.9+ adds Command Palette commands for all admin menu items. @@ -58,9 +55,8 @@ const registerPostTypeCommands = () => { commandStore.registerCommand( { name: `scf/cpt-${ postType.name }`, label: postType.all_items, - icon: createElement( Icon, { icon: page } ), + icon: page, context: 'admin', - description: postType.all_items, keywords: [ 'post type', 'content', @@ -69,7 +65,7 @@ const registerPostTypeCommands = () => { postType.label, ].filter( Boolean ), callback: ( { close } ) => { - document.location = addQueryArgs( adminUrl + 'edit.php', { + document.location = addQueryArgs( 'edit.php', { post_type: postType.name, } ); close(); @@ -80,9 +76,8 @@ const registerPostTypeCommands = () => { commandStore.registerCommand( { name: `scf/new-${ postType.name }`, label: postType.add_new_item, - icon: createElement( Icon, { icon: plus } ), + icon: plus, context: 'admin', - description: postType.add_new_item, keywords: [ 'add', 'new', @@ -93,7 +88,7 @@ const registerPostTypeCommands = () => { ], callback: ( { close } ) => { document.location = addQueryArgs( - adminUrl + 'post-new.php', + 'post-new.php', { post_type: postType.name, } @@ -111,13 +106,8 @@ const registerPostTypeCommands = () => { __( 'Edit post type: %s', 'secure-custom-fields' ), postType.label ), - icon: createElement( Icon, { icon: edit } ), + icon: edit, context: 'admin', - description: sprintf( - /* translators: %s: post type label */ - __( 'Edit the %s post type settings', 'secure-custom-fields' ), - postType.label - ), keywords: [ 'edit', 'modify', @@ -128,7 +118,7 @@ const registerPostTypeCommands = () => { postType.label, ], callback: ( { close } ) => { - document.location = addQueryArgs( adminUrl + 'post.php', { + document.location = addQueryArgs( 'post.php', { post: postType.id, action: 'edit', } ); diff --git a/tests/e2e/command-palette.spec.ts b/tests/e2e/command-palette.spec.ts new file mode 100644 index 00000000..e082786e --- /dev/null +++ b/tests/e2e/command-palette.spec.ts @@ -0,0 +1,91 @@ +/** + * Internal dependencies + */ +const { test, expect, wpVersionAtLeast } = require( './fixtures' ); + +test.describe( 'Command Palette', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'secure-custom-fields' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'secure-custom-fields' ); + } ); + + test( 'should register SCF create commands', async ( { page, admin } ) => { + await admin.visitAdminPage( 'index.php' ); + test.skip( + ! ( await wpVersionAtLeast( page, 6, 3 ) ), + 'Command Palette requires WordPress 6.3+' + ); + + // Open the command palette via keyboard shortcut. + await page.keyboard.press( 'ControlOrMeta+k' ); + + const input = page.getByRole( 'combobox', { + name: 'Search commands and settings', + } ); + await expect( input ).toBeVisible(); + + // Search for a create command — these are always registered by SCF. + await input.fill( 'Create New Field Group' ); + await expect( + page.getByRole( 'option', { name: /Create New Field Group/ } ) + ).toBeVisible(); + } ); + + test( 'should register SCF view commands without duplicates', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( 'index.php' ); + test.skip( + ! ( await wpVersionAtLeast( page, 6, 3 ) ), + 'Command Palette requires WordPress 6.3+' + ); + + await page.keyboard.press( 'ControlOrMeta+k' ); + + const input = page.getByRole( 'combobox', { + name: 'Search commands and settings', + } ); + + // Search for a view command that exists in both WP's auto-registered + // admin menu commands and SCF's view commands list. + await input.fill( 'Field Groups' ); + + const options = page.getByRole( 'option', { + name: /Field Groups/, + } ); + + // There should be exactly one match, not two (no duplicate). + await expect( options ).toHaveCount( 1 ); + } ); + + test( 'should navigate to field groups via command palette', async ( { + page, + admin, + } ) => { + await admin.visitAdminPage( 'index.php' ); + // Command Palette was introduced in WordPress 6.3 + test.skip( + ! ( await wpVersionAtLeast( page, 6, 3 ) ), + 'Command Palette requires WordPress 6.3+' + ); + + await page.keyboard.press( 'ControlOrMeta+k' ); + + const input = page.getByRole( 'combobox', { + name: 'Search commands and settings', + } ); + + await input.fill( 'Field Groups' ); + await page + .getByRole( 'option', { name: /Field Groups/ } ) + .click(); + + await expect( page ).toHaveURL( + /edit\.php\?post_type=acf-field-group/ + ); + } ); +} ); diff --git a/tests/e2e/fixtures.js b/tests/e2e/fixtures.js index 8674f328..7c93e076 100644 --- a/tests/e2e/fixtures.js +++ b/tests/e2e/fixtures.js @@ -177,13 +177,13 @@ const test = wpTest.extend( { async function wpVersionAtLeast( page, major, minor ) { return page.evaluate( ( [ maj, min ] ) => { - const branchClass = [ ...document.body.classList ].find( ( c ) => - c.startsWith( 'branch-' ) + const versionClass = [ ...document.body.classList ].find( ( c ) => + c.startsWith( 'version-' ) ); - if ( ! branchClass ) { + if ( ! versionClass ) { return true; } - const match = branchClass.match( /branch-(\d+)-(\d+)/ ); + const match = versionClass.match( /version-(\d+)-(\d+)/ ); if ( ! match ) { return true; } diff --git a/tests/js/commands/admin-commands.test.js b/tests/js/commands/admin-commands.test.js deleted file mode 100644 index 8e5d8d9f..00000000 --- a/tests/js/commands/admin-commands.test.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Unit tests for admin commands version-based registration - */ - -let mockCommandStore = null; - -jest.mock( '@wordpress/data', () => ( { - dispatch: jest.fn( () => mockCommandStore ), -} ) ); - -jest.mock( '@wordpress/i18n', () => ( { __: ( str ) => str } ) ); -jest.mock( '@wordpress/element', () => ( { createElement: jest.fn() } ) ); -jest.mock( '@wordpress/components', () => ( { Icon: 'Icon' } ) ); -jest.mock( '@wordpress/url', () => ( { addQueryArgs: jest.fn() } ) ); -jest.mock( '@wordpress/icons', () => ( { - layout: 'icon', - plus: 'icon', - postList: 'icon', - category: 'icon', - settings: 'icon', - tool: 'icon', - upload: 'icon', - download: 'icon', -} ) ); - -describe( 'Admin Commands', () => { - let mockRegisterCommand; - let capturedCallback; - - beforeEach( () => { - jest.clearAllMocks(); - jest.resetModules(); - mockRegisterCommand = jest.fn(); - mockCommandStore = { registerCommand: mockRegisterCommand }; - window.requestIdleCallback = jest.fn( ( cb ) => { - capturedCallback = cb; - } ); - } ); - - // 7 viewCommands + 4 createCommands = 11 on WP < 6.9 - // 4 createCommands only on WP 6.9+ (viewCommands skipped) - it.each( [ - [ '6.8', 11 ], - [ '6.8.1', 11 ], - [ '6.9', 4 ], - [ '6.9-beta1', 4 ], - [ '6.9.1', 4 ], - [ '7.0', 4 ], - ] )( 'WP %s registers %i commands', async ( wpVersion, expectedCount ) => { - window.acf = { - data: { - admin_url: 'http://example.com/wp-admin/', - wp_version: wpVersion, - }, - }; - - await import( '../../../assets/src/js/commands/admin-commands.js' ); - capturedCallback(); - - expect( mockRegisterCommand ).toHaveBeenCalledTimes( expectedCount ); - } ); - - it( 'WP 6.9+ skips view commands but keeps create commands', async () => { - window.acf = { - data: { - admin_url: 'http://example.com/wp-admin/', - wp_version: '6.9', - }, - }; - - await import( '../../../assets/src/js/commands/admin-commands.js' ); - capturedCallback(); - - const names = mockRegisterCommand.mock.calls.map( - ( c ) => c[ 0 ].name - ); - - // View commands should NOT be registered - expect( names ).not.toContain( 'scf/field-groups' ); - expect( names ).not.toContain( 'scf/post-types' ); - - // Create commands should be registered - expect( names ).toContain( 'scf/new-field-group' ); - expect( names ).toContain( 'scf/new-post-type' ); - } ); -} );