From 7e44491819d7c540381f71d3358302f48a9f20f0 Mon Sep 17 00:00:00 2001 From: Mohan Raj Date: Sun, 25 Feb 2024 16:39:08 +0000 Subject: [PATCH 1/3] introduce max flag filter --- README.md | 21 +++++++++-- docker-compose.yml | 2 +- includes/Api/Flags.php | 19 +++++++++- src/components/Flags.tsx | 39 ++++++++++++++------ src/styles/settings.scss | 9 +++-- tests/integration/FlagsApiTest.php | 57 ++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0528d50..e028e26 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,26 @@ WordPress Feature flags plugin allow developers to configure features in plugins ## Hooks -### JS Filters +### PHP filters -##### mrFeatureFlags.newFlag.defaultStatus +#### `mr_feature_flags_max_allowed` + +Filter to define the maximum number of allowed flags. It is recommended to keep this to default value, which is 20. + +Example usage: + +```php +add_filter( + 'mr_feature_flags_max_allowed', + static function () { + return 10; + } +); +``` + +### JS filters + +##### `mrFeatureFlags.newFlag.defaultStatus` The filter controls whether the new flag is enabled by default or not. Default `true` diff --git a/docker-compose.yml b/docker-compose.yml index 155b23a..d50fea8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: depends_on: - mysql mysql: - image: mysql:8 + image: mysql:8.0 env_file: .env environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} diff --git a/includes/Api/Flags.php b/includes/Api/Flags.php index 6f92946..c8ad57d 100644 --- a/includes/Api/Flags.php +++ b/includes/Api/Flags.php @@ -29,6 +29,13 @@ class Flags { */ public static $option_name = 'mr_feature_flags'; + /** + * Maximum allowed flags. + * + * @var int $max_flags + */ + private static $max_flags = 20; + /** * Register feature flag endpoints. * @@ -92,6 +99,16 @@ public function post_flags( WP_REST_Request $request ) { $input_data = $request->get_json_params(); if ( is_array( $input_data['flags'] ) ) { + /** + * Filter to update max allowed feature flags. + */ + $max_allowed_flags = apply_filters( 'mr_feature_flags_max_allowed', self::$max_flags ); + if ( count( $input_data['flags'] ) > $max_allowed_flags ) { + // translators: %d is a placeholder for the maximum allowed flags. + $error_message = sprintf( __( 'Cannot add more than %d flags', 'mr-feature-flags' ), $max_allowed_flags ); + return new WP_Error( 'flag_limit_exceeded', $error_message, array( 'status' => 400 ) ); + } + update_option( self::$option_name, $input_data['flags'] ); return rest_ensure_response( array( @@ -101,7 +118,7 @@ public function post_flags( WP_REST_Request $request ) { ); } - return new WP_Error( 'invalid_input', 'Cannot update flags', array( 'status' => 400 ) ); + return new WP_Error( 'invalid_input', __( 'Cannot update flags', 'mr-feature-flags' ), array( 'status' => 400 ) ); } /** diff --git a/src/components/Flags.tsx b/src/components/Flags.tsx index b001237..e74ef03 100644 --- a/src/components/Flags.tsx +++ b/src/components/Flags.tsx @@ -6,7 +6,7 @@ import SubmitControls from './SubmitControls'; import { getFlags, updateFlags } from '../utils'; import Header from './Header'; import { __ } from '@wordpress/i18n'; -import { dispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; const Layout = (): JSX.Element => { const [flags, setFlags] = useState([]); @@ -14,6 +14,9 @@ const Layout = (): JSX.Element => { const [isSaving, setIsSaving] = useState(false); const [disableSave, setDisableSave] = useState(false); + const { createErrorNotice, createSuccessNotice } = + useDispatch('core/notices'); + useEffect(() => { const logFlags = async () => { const fetchedFlags = await getFlags(); @@ -26,15 +29,31 @@ const Layout = (): JSX.Element => { logFlags(); }, [setFlags, setIsLoading]); - const remoteApi = useCallback(async (input: Flag[]) => { - await updateFlags({ ...input }); - //@ts-ignore - dispatch('core/notices').createSuccessNotice('Saved successfully!', { - type: 'snackbar', - id: 'mr-feature-flags-snackbar', - icon: <>✅, - }); - }, []); + const remoteApi = useCallback( + async (input: Flag[]) => { + try { + const response = await updateFlags({ ...input }); + if ('status' in response && response.status === 200) { + createSuccessNotice( + __('Saved successfully!', 'mr-feature-flags'), + { + type: 'snackbar', + id: 'mr-feature-flags-snackbar', + icon: <>✅, + } + ); + } + } catch (error: unknown) { + //@ts-ignore + createErrorNotice(error?.message, { + type: 'snackbar', + id: 'mr-feature-flags-snackbar', + icon: <>❌, + }); + } + }, + [createErrorNotice, createSuccessNotice] + ); const lastFlag = flags?.at(-1)?.id || 0; diff --git a/src/styles/settings.scss b/src/styles/settings.scss index 48e9987..ae2ab99 100644 --- a/src/styles/settings.scss +++ b/src/styles/settings.scss @@ -10,9 +10,14 @@ bottom: 3.5rem; position: fixed; + div { + float: right; + right: 250px; + } + .components-snackbar__icon { - left: 18px; top: auto; + left: 18px; } } @@ -55,7 +60,7 @@ .mr-feature-flags-toggle { margin-top: 7px; margin-left: 40px; - min-width: 150px; + min-width: 130px !important; } .mr-feature-flags-sdk { diff --git a/tests/integration/FlagsApiTest.php b/tests/integration/FlagsApiTest.php index 92154b1..4180429 100644 --- a/tests/integration/FlagsApiTest.php +++ b/tests/integration/FlagsApiTest.php @@ -153,6 +153,63 @@ public function test_create_item_with_invalid_input_array() { } + public function test_create_item_with_default_max_allowed_filter() { + wp_set_current_user( self::$admin ); + $flags = [['id'=>1, 'name'=>'test', 'enabled'=>true], + ['id'=>2, 'name'=>'test2', 'enabled'=>false], + ['id'=>3, 'name'=>'test3', 'enabled'=>false], + ['id'=>4, 'name'=>'test4', 'enabled'=>false], + ['id'=>5, 'name'=>'test5', 'enabled'=>false], + ['id'=>6, 'name'=>'test6', 'enabled'=>false], + ['id' => 7, 'name' => 'test7', 'enabled' => true], + ['id' => 8, 'name' => 'test8', 'enabled' => false], + ['id' => 9, 'name' => 'test9', 'enabled' => false], + ['id' => 10, 'name' => 'test10', 'enabled' => false], + ['id' => 11, 'name' => 'test11', 'enabled' => false], + ['id' => 12, 'name' => 'test12', 'enabled' => false], + ['id' => 13, 'name' => 'test13', 'enabled' => false], + ['id' => 14, 'name' => 'test14', 'enabled' => false], + ['id' => 15, 'name' => 'test15', 'enabled' => false], + ['id' => 16, 'name' => 'test16', 'enabled' => false], + ['id' => 17, 'name' => 'test17', 'enabled' => false], + ['id' => 18, 'name' => 'test18', 'enabled' => false], + ['id' => 19, 'name' => 'test19', 'enabled' => false], + ['id' => 20, 'name' => 'test20', 'enabled' => false], + ['id' => 21, 'name' => 'test21', 'enabled' => false],]; + + $request = new WP_REST_Request( 'POST', self::$api_endpoint ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( ['flags' => $flags] ) ); + $response = rest_get_server()->dispatch( $request ); + $response_message = $response->get_data()['message']; + + $this->assertErrorResponse( 'flag_limit_exceeded', $response, 400 ); + $this->assertEquals('Maximum allowed flags are 20', $response_message); + + } + + public function test_create_item_with_custom_max_allowed_filter() { + wp_set_current_user( self::$admin ); + + // Mock the filter hook + $mocked_max_flags = 3; + add_filter('mr_feature_flags_max_allowed', function () use ($mocked_max_flags) { + return $mocked_max_flags; + }); + + $flags = [['id'=>1, 'name'=>'test', 'enabled'=>true], ['id'=>2, 'name'=>'test2', 'enabled'=>false], ['id'=>3, 'name'=>'test2', 'enabled'=>false], ['id'=>4, 'name'=>'test2', 'enabled'=>false]]; + + $request = new WP_REST_Request( 'POST', self::$api_endpoint ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( ['flags' => $flags] ) ); + $response = rest_get_server()->dispatch( $request ); + $response_message = $response->get_data()['message']; + + $this->assertErrorResponse( 'flag_limit_exceeded', $response, 400 ); + $this->assertEquals('Maximum allowed flags are 3', $response_message); + + } + public function test_create_item_without_input() { wp_set_current_user( self::$admin ); From 6fc9bf4de1c880c7aaadabbe151482e295bae9ef Mon Sep 17 00:00:00 2001 From: Mohan Raj Date: Sun, 25 Feb 2024 17:35:31 +0000 Subject: [PATCH 2/3] add clipboard check in e2e test --- playwright.config.ts | 1 + tests/e2e/feature-flags.spec.ts | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 1e1eb6d..8c9f182 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ use: { baseURL: process.env.WP_BASE_URL, trace: 'on-first-retry', + permissions: ['clipboard-read'], }, projects: [ diff --git a/tests/e2e/feature-flags.spec.ts b/tests/e2e/feature-flags.spec.ts index 8bf5270..ae9e749 100644 --- a/tests/e2e/feature-flags.spec.ts +++ b/tests/e2e/feature-flags.spec.ts @@ -45,12 +45,12 @@ test.describe('Feature flags', () => { //Create another flag with same name await page.getByRole('button', { name: 'Add Flag' }).click(); await page.getByRole('textbox').last().fill('test'); - expect(await page.getByText(ERROR_FLAG_EXISTS)).toBeVisible(); + expect(page.getByText(ERROR_FLAG_EXISTS)).toBeVisible(); expect(page.getByRole('button', { name: 'Save' })).toBeDisabled(); //update flag name to be unique and check text validation. await page.getByRole('textbox').last().fill('test 2'); - expect(await page.getByText(ERROR_FLAG_INVALID)).toBeVisible(); + expect(page.getByText(ERROR_FLAG_INVALID)).toBeVisible(); expect(page.getByRole('button', { name: 'Save' })).toBeDisabled(); @@ -72,6 +72,23 @@ test.describe('Feature flags', () => { expect( page.getByRole('heading', { name: 'SDK for feature flag: test' }) ).toBeVisible(); + + // Check PHP Snippet clipboard details + await page.getByLabel('Copy to clipboard').first().click(); + const phpClipboardText = await page.evaluate( + 'navigator.clipboard.readText()' + ); + expect(phpClipboardText).toContain("Flag::is_enabled( 'test' )"); + + // Check JS Snippet clipboard details + await page.getByLabel('Copy to clipboard').nth(1).click(); + const jsClipboardText: string = await page.evaluate( + 'navigator.clipboard.readText()' + ); + expect(jsClipboardText).toContain( + "window.mrFeatureFlags.isEnabled('test')" + ); + await page.locator('button[aria-label="Close"]').click(); await page From 4c3728418fb37f1f4e329b8389518f5b10e65b36 Mon Sep 17 00:00:00 2001 From: Mohan Raj Date: Tue, 27 Feb 2024 20:14:26 +0000 Subject: [PATCH 3/3] add beforeeach step test --- tests/e2e/feature-flags.spec.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/e2e/feature-flags.spec.ts b/tests/e2e/feature-flags.spec.ts index ae9e749..50626ef 100644 --- a/tests/e2e/feature-flags.spec.ts +++ b/tests/e2e/feature-flags.spec.ts @@ -5,7 +5,7 @@ import { ERROR_FLAG_EXISTS, ERROR_FLAG_INVALID } from '../../src/constants'; test.use({ storageState: process.env.WP_AUTH_STORAGE }); test.describe('Feature flags', () => { - test('Create and delete flags e2e scenarios', async ({ page, admin }) => { + test.beforeEach(async ({ page, admin }) => { await admin.visitAdminPage('/'); //Find the feature flags in side menu @@ -15,10 +15,11 @@ test.describe('Feature flags', () => { await expect( page.getByRole('heading', { name: 'Feature Flags settings' }) ).toBeVisible(); + }); + test('Create and delete flags e2e scenarios', async ({ page }) => { //Create new flag await page.getByRole('button', { name: 'Add Flag' }).click(); - // await expect(await page.getByRole('textbox').count()).toBe(4); await page.getByRole('textbox').last().fill('test'); await page.getByRole('button', { name: 'Save' }).click(); //Confirm save success @@ -26,7 +27,7 @@ test.describe('Feature flags', () => { await page.getByLabel('Dismiss this notice').innerText() ).toMatch(/Saved successfully!/); - //Toggle flag test + //Toggle feature flag await page .locator('id=mr-feature-flag-item') .last() @@ -51,17 +52,15 @@ test.describe('Feature flags', () => { //update flag name to be unique and check text validation. await page.getByRole('textbox').last().fill('test 2'); expect(page.getByText(ERROR_FLAG_INVALID)).toBeVisible(); - expect(page.getByRole('button', { name: 'Save' })).toBeDisabled(); + //Delete the flag await page .locator('id=mr-feature-flag-item') .last() .getByLabel('Delete Flag') .click(); - await page.getByRole('button', { name: 'Yes' }).click(); - //Confirm delete success expect( await page.getByLabel('Dismiss this notice').innerText() @@ -88,15 +87,15 @@ test.describe('Feature flags', () => { expect(jsClipboardText).toContain( "window.mrFeatureFlags.isEnabled('test')" ); - + //Close SDK modal await page.locator('button[aria-label="Close"]').click(); + //Delete the created flag await page .locator('id=mr-feature-flag-item') .last() .getByLabel('Delete Flag') .click(); - await page.getByRole('button', { name: 'Yes' }).click(); }); });