diff --git a/includes/Abstracts/Abstract_Feature.php b/includes/Abstracts/Abstract_Feature.php index fe52055c3..eb322425a 100644 --- a/includes/Abstracts/Abstract_Feature.php +++ b/includes/Abstracts/Abstract_Feature.php @@ -169,7 +169,7 @@ public function get_category(): string { * {@inheritDoc} */ final public function is_globally_enabled(): bool { - return (bool) get_option( Settings_Registration::GLOBAL_OPTION, false ); + return (bool) get_option( Settings_Registration::GLOBAL_OPTION, true ); } /** diff --git a/routes/ai-home/stage.tsx b/routes/ai-home/stage.tsx index fb51ef486..46b4f5813 100644 --- a/routes/ai-home/stage.tsx +++ b/routes/ai-home/stage.tsx @@ -2,16 +2,7 @@ * WordPress dependencies */ import { Page } from '@wordpress/admin-ui'; -import { - Button, - Card, - Icon, - Link, - Notice, - Popover, - Stack, - VisuallyHidden, -} from '@wordpress/ui'; +import { Button, Card, Link, Notice, Stack } from '@wordpress/ui'; import { DropdownMenu, MenuGroup, @@ -32,7 +23,6 @@ import { __, _n, sprintf } from '@wordpress/i18n'; import { check as checkIcon, download as downloadIcon, - info as infoIcon, tool as toolIcon, upload as uploadIcon, } from '@wordpress/icons'; @@ -96,8 +86,6 @@ interface PageData { } const FEATURE_SETTING_PATTERN = /^wpai_feature_(.+)_enabled$/; -const GLOBAL_FIELD_ID = 'wpai_features_enabled'; -const noop = () => {}; function isRecord( value: unknown ): value is Record< string, unknown > { return typeof value === 'object' && value !== null; @@ -270,40 +258,6 @@ const STABLE_FEATURE_DEFINITIONS: FeatureData[] = ( () => { return unique; } )(); -interface InfoTipProps { - content: string; -} - -function InfoTip( { content }: InfoTipProps ) { - const title = __( 'More information', 'ai' ); - - return ( - - - - - } - className="ai-settings-page__infotip-popover" - > - - }> - { title } - - - { content } - - - - ); -} - function buildToggleMessage( edits: Record< string, unknown >, featureDefinitions: FeatureData[] @@ -362,11 +316,6 @@ function buildToggleMessage( return __( 'Settings saved.', 'ai' ); } - if ( entry[ 0 ] === GLOBAL_FIELD_ID ) { - return entry[ 1 ] - ? __( 'AI enabled.', 'ai' ) - : __( 'AI disabled.', 'ai' ); - } const feature = featureDefinitions.find( ( f ) => f.settingName === entry[ 0 ] ); @@ -378,29 +327,14 @@ function buildToggleMessage( sprintf( __( '%s disabled.', 'ai' ), label ); } -function DisabledToggle( { field, data }: DataFormControlProps< AISettings > ) { - return ( - - ); -} - interface SectionActionsProps extends DataFormControlProps< AISettings > { experimentSettings: string[]; - globalEnabled: boolean; onBulkChange: ( edits: Record< string, boolean > ) => void; } function SectionActions( { experimentSettings, data, - globalEnabled, onBulkChange, }: SectionActionsProps ) { const allEnabled = useMemo( () => { @@ -453,7 +387,7 @@ function SectionActions( { variant="outline" size="compact" onClick={ handleEnableAll } - disabled={ ! globalEnabled || allEnabled } + disabled={ allEnabled } > { __( 'Enable all', 'ai' ) } @@ -461,7 +395,7 @@ function SectionActions( { variant="outline" size="compact" onClick={ handleDisableAll } - disabled={ ! globalEnabled || allDisabled } + disabled={ allDisabled } > { __( 'Disable all', 'ai' ) } @@ -661,16 +595,11 @@ function VisualCardToggle( { onChange, }: DataFormControlProps< AISettings > ) { const feature = VISUAL_CARD_FEATURES.get( field.id ); - const globalEnabled = !! data[ GLOBAL_FIELD_ID ]; const checked = !! field.getValue( { item: data } ); const isDeveloperMode = useDeveloperModeContext(); return ( - + { feature?.image && ( { onChange( { [ field.id ]: value } ) } - disabled={ ! globalEnabled } help={ field.description } /> - { globalEnabled && checked && isDeveloperMode && feature && ( + { checked && isDeveloperMode && feature && ( { - const settingKeys = new Set< string >( [ GLOBAL_FIELD_ID ] ); + const settingKeys = new Set< string >(); for ( const feature of featureDefinitions ) { settingKeys.add( feature.settingName ); @@ -797,12 +725,6 @@ function AISettingsPage() { return aiSettings; }, [ aiSettingKeys, editedRecord ] ); - const globalEnabled = Boolean( data[ GLOBAL_FIELD_ID ] ); - const globalToggleDescription = __( - 'Control whether AI is enabled for your site. When disabled, all features and experiments will be inactive regardless of their individual settings.', - 'ai' - ); - const handleChange = useCallback( async ( edits: Record< string, unknown > ) => { const keys = Object.keys( edits ); @@ -874,7 +796,6 @@ function AISettingsPage() { ), @@ -892,8 +813,6 @@ function AISettingsPage() { if ( VISUAL_CARD_FEATURES.has( feature.settingName ) ) { baseField.Edit = VisualCardToggle; - } else if ( ! globalEnabled ) { - baseField.Edit = DisabledToggle; } else if ( feature.settingsFields.length > 0 ) { baseField.Edit = FeatureToggleWithSettings; } else { @@ -912,7 +831,7 @@ function AISettingsPage() { } ); return [ ...sectionActionsFields, ...featureFields ]; - }, [ featureDefinitions, featureGroups, globalEnabled, handleChange ] ); + }, [ featureDefinitions, featureGroups, handleChange ] ); const form = useMemo< Form >( () => { const showcaseChildren: string[] = []; @@ -1019,19 +938,6 @@ function AISettingsPage() { ) } actions={ <> - - { - void handleChange( { - [ GLOBAL_FIELD_ID ]: checked, - } ); - } } - disabled={ isLoading } - /> - - { admin, page, } ) => { - // Start with AI disabled so we can confirm cancelling leaves it untouched. - await disableExperiments( admin, page ); + // Ensure Title Generation is disabled so we can confirm cancelling leaves it untouched. + await disableExperiment( admin, page, 'Title Generation' ); const filePath = writeTempExportFile( { - wpai_features_enabled: true, + 'wpai_feature_title-generation_enabled': true, } ); tempFiles.push( filePath ); @@ -194,20 +193,20 @@ test.describe( 'Settings import/export', () => { await dialog.getByRole( 'button', { name: 'Cancel' } ).click(); await expect( dialog ).not.toBeVisible(); - // Global AI toggle must remain disabled since the import was cancelled. - await expect( page.getByLabel( 'Enable AI' ) ).not.toBeChecked(); + // Title Generation toggle must remain disabled since the import was cancelled. + await expect( page.getByLabel( 'Title Generation' ) ).not.toBeChecked(); } ); test( 'Confirming the import applies settings without a page reload', async ( { admin, page, } ) => { - // Start with AI disabled so the imported value is a visible change. - await disableExperiments( admin, page ); - await expect( page.getByLabel( 'Enable AI' ) ).not.toBeChecked(); + // Ensure Title Generation is disabled so the imported value is a visible change. + await disableExperiment( admin, page, 'Title Generation' ); + await expect( page.getByLabel( 'Title Generation' ) ).not.toBeChecked(); const filePath = writeTempExportFile( { - wpai_features_enabled: true, + 'wpai_feature_title-generation_enabled': true, } ); tempFiles.push( filePath ); @@ -234,12 +233,12 @@ test.describe( 'Settings import/export', () => { // The toggle should reflect the imported value immediately, with no // manual page reload required (regression guard for the stale // core-data cache/infinite-spinner issue). - await expect( page.getByLabel( 'Enable AI' ) ).toBeChecked( { + await expect( page.getByLabel( 'Title Generation' ) ).toBeChecked( { timeout: 10000, } ); - // Cleanup. - await enableExperiments( admin, page ); + // Cleanup: restore Title Generation to disabled. + await disableExperiment( admin, page, 'Title Generation' ); } ); test( 'Rejects an invalid file and shows an error notice', async ( { diff --git a/tests/e2e/specs/admin/settings.spec.js b/tests/e2e/specs/admin/settings.spec.js index 00a33ce63..9afb47b88 100644 --- a/tests/e2e/specs/admin/settings.spec.js +++ b/tests/e2e/specs/admin/settings.spec.js @@ -9,7 +9,6 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); const { clearConnectors, seedCredentials, - disableExperiments, disableExperiment, enableExperiment, enableExperiments, @@ -101,27 +100,11 @@ test.describe( 'Plugin settings', () => { .click(); } ); - test( 'Can turn on Experiments', async ( { admin, page } ) => { - // Globally disable experiments. - await disableExperiments( admin, page ); - - // Ensure global AI setting is disabled. - await expect( page.getByLabel( 'Enable AI' ) ).not.toBeChecked(); - - // Ensure feature toggles are disabled when AI is disabled. - await expect( - page - .locator( - '#ai-wp-admin-app .components-form-toggle.is-disabled' - ) - .first() - ).toBeVisible(); - - // Globally turn on experiments. - await enableExperiments( admin, page ); - - // Ensure global AI setting is enabled. - await expect( page.getByLabel( 'Enable AI' ) ).toBeChecked(); + test( 'Settings page displays experiment sections', async ( { + admin, + page, + } ) => { + await visitSettingsPage( admin ); // Ensure we see the editor experiments section. await expect( @@ -140,13 +123,13 @@ test.describe( 'Plugin settings', () => { } ) => { // Use a fixed desktop viewport so the admin menu is at full width and // snackbar placement is deterministic. - await page.setViewportSize( { width: 1280, height: 800 } ); + await page.setViewportSize( { width: 1440, height: 800 } ); await visitSettingsPage( admin ); - // Toggle the global setting to trigger a snackbar. - const globalToggle = page.getByLabel( 'Enable AI' ); - await expect( globalToggle ).toBeVisible( { timeout: 10000 } ); - await globalToggle.click(); + // Toggle a feature setting to trigger a snackbar. + const featureToggle = page.getByLabel( 'Title Generation' ); + await expect( featureToggle ).toBeVisible( { timeout: 10000 } ); + await featureToggle.click(); const snackbar = page.getByTestId( 'snackbar' ).first(); await expect( snackbar ).toBeVisible(); @@ -162,6 +145,9 @@ test.describe( 'Plugin settings', () => { expect( snackBox.x + snackBox.width ).toBeLessThanOrEqual( contentBox.x ); + + // Restore toggle state. + await featureToggle.click(); } ); test( 'Inline settings retain pending edits when another toggle auto-saves', async ( { @@ -312,28 +298,6 @@ test.describe( 'Plugin settings', () => { } } ); - test( 'Cannot bulk manage experiments when global AI is disabled', async ( { - admin, - page, - } ) => { - // Disable global AI. - await disableExperiments( admin, page ); - - // Verify both buttons are disabled. - const enableAllButton = getEnableAllButton( - page, - EXPERIMENT_GROUPS.editor - ); - - const disableAllButton = getDisableAllButton( - page, - EXPERIMENT_GROUPS.editor - ); - - await expect( enableAllButton ).toBeDisabled(); - await expect( disableAllButton ).toBeDisabled(); - } ); - test( 'Each experiment group has its own bulk action buttons', async ( { admin, page, @@ -715,35 +679,30 @@ test.describe( 'Plugin settings', () => { admin, page, } ) => { - // Globally turn on experiments so the Image Generation feature can be enabled. + // Turn on experiments so the Image Generation feature can be enabled. await enableExperiments( admin, page ); // Enable the visual Image Generation feature card. await enableExperiment( admin, page, 'Image Generation and Editing' ); - // Turn on model selection while AI is globally enabled. + // Turn on model selection. await enableModelSelection( page ); - // Globally disable AI. The feature card remains checked, but inactive. - await disableExperiments( admin, page ); + // Disable the visual feature card. + await disableExperiment( admin, page, 'Image Generation and Editing' ); - const disabledImageGenerationCard = page.locator( - '.ai-showcase-card--disabled', - { - has: page.getByText( 'Image Generation and Editing' ), - } - ); + const imageGenerationCard = page.locator( '.ai-showcase-card', { + has: page.getByText( 'Image Generation and Editing' ), + } ); - await expect( disabledImageGenerationCard ).toBeVisible(); + await expect( imageGenerationCard ).toBeVisible(); // The disabled visual feature card should not expose active provider/model controls. await expect( - disabledImageGenerationCard.locator( '.ai-developer-mode-fields' ) + imageGenerationCard.locator( '.ai-developer-mode-fields' ) ).not.toBeVisible(); // Restore state. - await enableExperiments( admin, page ); await disableModelSelection( page ); - await disableExperiment( admin, page, 'Image Generation and Editing' ); } ); } ); diff --git a/tests/e2e/utils/helpers.ts b/tests/e2e/utils/helpers.ts index 43899193f..c01c7f615 100644 --- a/tests/e2e/utils/helpers.ts +++ b/tests/e2e/utils/helpers.ts @@ -163,47 +163,58 @@ export const clearConnector = async ( export const disableExperiments = async ( admin: Admin, page: Page ) => { await visitSettingsPage( admin ); - // Wait for page to fully load before finding the global toggle. - const globalToggle = page.getByLabel( 'Enable AI' ); - await expect( globalToggle ).toBeVisible( { timeout: 10000 } ); - await expect( globalToggle ).toBeEnabled( { timeout: 10000 } ); + const disableAllButtons = page.getByRole( 'button', { + name: 'Disable all', + } ); + await expect( disableAllButtons.first() ).toBeVisible( { timeout: 10000 } ); - // Nothing to do if experiments are already disabled. - if ( ! ( await globalToggle.isChecked() ) ) { - return; + const count = await disableAllButtons.count(); + for ( let i = 0; i < count; i++ ) { + const button = disableAllButtons.nth( i ); + if ( await button.isEnabled() ) { + const savePromise = page.waitForResponse( + ( response ) => + response.url().includes( '/wp/v2/settings' ) && + response.request().method() === 'POST' && + response.status() === 200 + ); + await button.click(); + await savePromise; + await expect( button ).toBeDisabled( { timeout: 10000 } ); + } + } + + const showcaseToggles = page.locator( + '.ai-showcase-card input[type="checkbox"]' + ); + const showcaseCount = await showcaseToggles.count(); + for ( let i = 0; i < showcaseCount; i++ ) { + const toggle = showcaseToggles.nth( i ); + if ( await toggle.isChecked() ) { + const savePromise = page.waitForResponse( + ( response ) => + response.url().includes( '/wp/v2/settings' ) && + response.request().method() === 'POST' && + response.status() === 200 + ); + await toggle.uncheck(); + await savePromise; + await expect( toggle ).not.toBeChecked( { timeout: 10000 } ); + } } - await globalToggle.uncheck(); - await expect( - page.locator( '.components-snackbar__content', { - hasText: 'AI disabled.', - } ) - ).toBeVisible(); }; /** - * Globally enables experiments. + * Ensures AI experiments are globally enabled. + * + * With the "Enable AI" header toggle removed, the plugin is enabled by default. * * @param admin The admin fixture from the test context. * @param page The page object. */ -export const enableExperiments = async ( admin: Admin, page: Page ) => { - await visitSettingsPage( admin ); - - // Wait for page to fully load before finding the global toggle. - const globalToggle = page.getByLabel( 'Enable AI' ); - await expect( globalToggle ).toBeVisible( { timeout: 10000 } ); - await expect( globalToggle ).toBeEnabled( { timeout: 10000 } ); - - // Nothing to do if experiments are already enabled. - if ( await globalToggle.isChecked() ) { - return; - } - await globalToggle.check(); - await expect( - page.locator( '.components-snackbar__content', { - hasText: 'AI enabled.', - } ) - ).toBeVisible(); +export const enableExperiments = async ( admin?: Admin, page?: Page ) => { + void admin; + void page; }; /**