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
3 changes: 2 additions & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"wp-content/plugins/unsupported-elasticsearch-version.php": "./tests/e2e/src/wordpress-files/test-plugins/unsupported-elasticsearch-version.php",
"wp-content/plugins/multiple-required-features.php": "./tests/e2e/src/wordpress-files/test-plugins/multiple-required-features.php",
"wp-content/uploads/content-example.xml": "./tests/e2e/src/wordpress-files/test-docs/content-example.xml",
"wp-content/plugins/echo-shortcode.php": "./tests/e2e/src/wordpress-files/test-plugins/echo-shortcode.php"
"wp-content/plugins/echo-shortcode.php": "./tests/e2e/src/wordpress-files/test-plugins/echo-shortcode.php",
"wp-content/plugins/simulate-setting-dependency.php": "./tests/e2e/src/wordpress-files/test-plugins/simulate-setting-dependency.php"
}
}
}
Expand Down
22 changes: 19 additions & 3 deletions assets/js/features/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,29 @@ export const FeatureSettingsProvider = ({
children,
defaultSettings,
epioLogoUrl,
features,
features: initialFeatures,
indexMeta,
syncedSettings,
}) => {
const [isBusy, setIsBusy] = useState(false);
const [isSyncing, setIsSyncing] = useState(!!indexMeta);
const [settings, setSettings] = useState({ ...defaultSettings });
const [savedSettings, setSavedSettings] = useState({ ...syncedSettings });
const [features, setFeatures] = useState(initialFeatures);

/**
* Refetch the authoritative state snapshot from the REST API.
*/
const refresh = useCallback(async () => {
const response = await apiFetch({
method: 'GET',
url: apiUrl,
});

setFeatures(response.features);
setSavedSettings(response.settings);
setSettings(response.settingsDraft ?? response.settings);
}, [apiUrl]);

/**
* Get a feature's data by its slug.
Expand Down Expand Up @@ -158,7 +173,7 @@ export const FeatureSettingsProvider = ({
try {
setIsBusy(true);

const response = await apiFetch({
await apiFetch({
body: JSON.stringify(settings),
headers: {
'Content-Type': 'application/json',
Expand All @@ -167,7 +182,7 @@ export const FeatureSettingsProvider = ({
url: apiUrl,
});

setSavedSettings(response.data);
await refresh();
} finally {
setIsBusy(false);
}
Expand All @@ -184,6 +199,7 @@ export const FeatureSettingsProvider = ({
isSyncing,
setIsSyncing,
isSyncRequired,
refresh,
resetSettings,
saveSettings,
savedSettings,
Expand Down
9 changes: 9 additions & 0 deletions includes/classes/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,15 @@ public function get_settings_schema() {
return apply_filters( 'ep_feature_settings_schema', $settings_schema, $this->slug, $this );
}

/**
* Reset the cached settings schema so it is rebuilt on next access.
*
* @since 5.3.3
*/
public function reset_settings_schema() {
$this->settings_schema = [];
}

/**
* Default implementation of `set_settings_schema` based on the `default_settings` attribute
*
Expand Down
53 changes: 48 additions & 5 deletions includes/classes/REST/Features.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,17 @@ public function register_routes() {
'elasticpress/v1',
'features',
[
'args' => $this->get_args(),
'callback' => [ $this, 'update_settings' ],
'methods' => 'PUT',
'permission_callback' => [ $this, 'check_permission' ],
[
'callback' => [ $this, 'get_features' ],
'methods' => 'GET',
'permission_callback' => [ $this, 'check_permission' ],
],
[
'args' => $this->get_args(),
'callback' => [ $this, 'update_settings' ],
'methods' => 'PUT',
'permission_callback' => [ $this, 'check_permission' ],
],
]
);
}
Expand Down Expand Up @@ -221,8 +228,44 @@ public function update_settings( \WP_REST_Request $request ) {
}

return [
'data' => $current_settings,
'success' => true,
];
}

/**
* Return the current features payload along with persisted settings.
*
* @since 5.3.0
* @return array
*/
public function get_features() {
$store = FeaturesStore::factory();

$settings = $store->get_feature_settings();
$settings_draft = $store->get_feature_settings_draft();

return [
'features' => $this->get_features_payload(),
'settings' => is_array( $settings ) ? $settings : [],
'settingsDraft' => is_array( $settings_draft ) ? $settings_draft : null,
'success' => true,
];
}

/**
* Build the serialized features payload.
*
* @since 5.3.0
* @return array
*/
protected function get_features_payload() {
$features_objects = FeaturesStore::factory()->registered_features;

foreach ( $features_objects as $feature ) {
$feature->reset_settings_schema();
}

$features_data = array_map( fn( $feature ) => $feature->get_json(), $features_objects );
return array_values( $features_data );
}
}
114 changes: 114 additions & 0 deletions tests/e2e/src/specs/feature-interface.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
goToAdminPage,
wpCli,
maybeDisableFeature,
maybeEnableFeature,
setDefaultFeatureSettings,
deactivatePlugin,
} from '../utils.js';
Expand Down Expand Up @@ -176,4 +177,117 @@ test.describe('Features Interface', { tag: '@group1' }, () => {
loggedInPage.getByRole('checkbox', { name: 'Trigger Google Analytics' }),
).not.toBeDisabled();
});

test.describe('Settings Schema Updates on Save', () => {
test.afterEach(async ({ loggedInPage }) => {
await deactivatePlugin(loggedInPage, 'simulate-setting-dependency', 'wpCli');
await setDefaultFeatureSettings();
});

test.afterAll(async () => {
await wpCli('plugin deactivate simulate-setting-dependency', true);
await setDefaultFeatureSettings();
});

test('Feature settings schema updates without page refresh when dependency changes', async ({
loggedInPage,
}) => {
await activatePlugin(loggedInPage, 'simulate-setting-dependency', 'wpCli');
await maybeEnableFeature('test_conditional_settings');

// Start with Post Search disabled so conditional options are hidden
await maybeDisableFeature('search');

// Navigate to the settings page
await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress');
await expect(loggedInPage.locator('.ep-settings-page form')).toBeVisible();
await loggedInPage.waitForFunction(() => {
return !!(window as any).epDashboard && !!(window as any).epDashboard.features;
});

// Navigate to the Test Conditional Settings panel
await loggedInPage.getByRole('button', { name: 'Core Search' }).click();
await loggedInPage.getByRole('button', { name: 'Test Conditional Settings' }).click();
await expect(
loggedInPage.locator('div[id*="test_conditional_settings-view"]'),
).toBeVisible();
await loggedInPage.waitForTimeout(500);

// Verify only the base options are visible (Post Search is not active)
await expect(loggedInPage.getByLabel('Basic')).toBeVisible();
await expect(loggedInPage.getByLabel('Standard')).toBeVisible();
await expect(
loggedInPage.getByLabel('Advanced (requires Post Search)'),
).not.toBeVisible();
await expect(
loggedInPage.getByLabel('Expert (requires Post Search)'),
).not.toBeVisible();

// Navigate to Post Search and enable it
await loggedInPage.getByRole('button', { name: 'Post Search' }).click();
await expect(loggedInPage.locator('div[id*="search-view"]')).toBeVisible();
await loggedInPage.waitForTimeout(500);
await loggedInPage.getByRole('checkbox', { name: 'Enable' }).setChecked(true);

// Save and wait for the save operation to complete.
const saveButton = loggedInPage.getByRole('button', { name: 'Save changes' });
const saveSnackbar = loggedInPage
.locator('.components-snackbar')
.filter({ hasText: 'Feature settings saved' });
const saveAndConfirm = async () => {
await saveButton.click();
await expect(saveSnackbar.last()).toBeVisible({ timeout: 10000 });
await saveSnackbar.evaluateAll((nodes) => {
nodes.forEach((node) => (node as HTMLElement).click());
});
await expect(saveSnackbar).toHaveCount(0, { timeout: 5000 });
};
await saveAndConfirm();

// Navigate back to the Test Conditional Settings (no page refresh)
await loggedInPage.getByRole('button', { name: 'Test Conditional Settings' }).click();
await expect(
loggedInPage.locator('div[id*="test_conditional_settings-view"]'),
).toBeVisible();
await loggedInPage.waitForTimeout(500);

// Verify the conditional options now appear
await expect(loggedInPage.getByLabel('Basic')).toBeVisible();
await expect(loggedInPage.getByLabel('Standard')).toBeVisible();
await expect(loggedInPage.getByLabel('Advanced (requires Post Search)')).toBeVisible();
await expect(loggedInPage.getByLabel('Expert (requires Post Search)')).toBeVisible();

// Select a conditional option that only exists while Post Search is active
await loggedInPage.getByLabel('Advanced (requires Post Search)').check();
await saveAndConfirm();

// Now disable Post Search and verify the options disappear
await loggedInPage.getByRole('button', { name: 'Post Search' }).click();
await expect(loggedInPage.locator('div[id*="search-view"]')).toBeVisible();
await loggedInPage.waitForTimeout(500);
await loggedInPage.getByRole('checkbox', { name: 'Enable' }).setChecked(false);
await saveAndConfirm();

// Navigate back to the Test Conditional Settings (no page refresh)
await loggedInPage.getByRole('button', { name: 'Test Conditional Settings' }).click();
await expect(
loggedInPage.locator('div[id*="test_conditional_settings-view"]'),
).toBeVisible();
await loggedInPage.waitForTimeout(500);

// Verify the conditional options are gone again (schema refresh)
await expect(loggedInPage.getByLabel('Basic')).toBeVisible();
await expect(loggedInPage.getByLabel('Standard')).toBeVisible();
await expect(
loggedInPage.getByLabel('Advanced (requires Post Search)'),
).not.toBeVisible();
await expect(
loggedInPage.getByLabel('Expert (requires Post Search)'),
).not.toBeVisible();

// Verify the previously-selected `advanced` value was normalized
await expect(loggedInPage.getByLabel('Basic')).toBeChecked();
await expect(loggedInPage.getByLabel('Standard')).not.toBeChecked();
});
});
});
Loading
Loading