diff --git a/.gitignore b/.gitignore index a38d132..c4198a3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ tests-examples/ build-script.sh integration-coverage.xml unit-coverage.xml +.idea/ diff --git a/includes/Flag.php b/includes/Flag.php index d4042f3..2830dbb 100644 --- a/includes/Flag.php +++ b/includes/Flag.php @@ -23,7 +23,7 @@ class Flag { * * @var string $option_name */ - public static $option_name = 'codeb_feature_flags'; + public static string $option_name = 'codeb_feature_flags'; /** diff --git a/jest-setup.js b/jest-setup.js index 7b0828b..f18a755 100644 --- a/jest-setup.js +++ b/jest-setup.js @@ -1 +1,13 @@ import '@testing-library/jest-dom'; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => {}, + }), +}); diff --git a/package.json b/package.json index 0fdf35f..6cc7d5d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint:js:fix": "wp-scripts lint-js --fix", "prepare": "husky", "start": "wp-scripts start", - "test:e2e": "npx playwright test --reporter=list", + "test:e2e": "wp-scripts test-playwright", "test:js": "wp-scripts test-unit-js", "test:watch": "wp-scripts test-unit-js --watch", "version:major": "node ./scripts/version major", @@ -25,6 +25,7 @@ "php:multisite": "wp-env run tests-wordpress --env-cwd=wp-content/plugins/feature-flags composer test:multisite" }, "dependencies": { + "@testing-library/user-event": "^14.5.2", "@wordpress/api-fetch": "^6.48.0", "@wordpress/components": "^27.0.0", "@wordpress/data": "^9.22.0", diff --git a/plugin.php b/plugin.php index 6b174a6..23f6d49 100644 --- a/plugin.php +++ b/plugin.php @@ -39,7 +39,7 @@ include_once __DIR__ . '/vendor/autoload.php'; } -// Enqueure scripts, styles in settings page. +// Enqueue scripts, styles in settings page. add_action( 'admin_enqueue_scripts', static function ( string $page ): void { @@ -156,7 +156,7 @@ static function ( $links ) { /** * Uninstall method for the plugin. - * + * * @return void */ function codeb_feature_flags_uninstall(): void { diff --git a/src/components/Flags.tsx b/src/components/Flags.tsx index 1cddc14..54f5794 100644 --- a/src/components/Flags.tsx +++ b/src/components/Flags.tsx @@ -8,7 +8,7 @@ import Header from './Header'; import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; -const Layout = (): JSX.Element => { +const Flags = () => { const [flags, setFlags] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -64,7 +64,7 @@ const Layout = (): JSX.Element => { return prevFlags.filter((f) => f.name !== ''); }); - remoteApi(flags); + await remoteApi(flags); setIsSaving(false); }; @@ -74,7 +74,7 @@ const Layout = (): JSX.Element => { const latestFlags = flags.filter((f) => f.id !== flagId); setFlags(latestFlags); - remoteApi(latestFlags); + await remoteApi(latestFlags); setIsSaving(false); }; @@ -120,4 +120,4 @@ const Layout = (): JSX.Element => { ); }; -export default Layout; +export default Flags; diff --git a/src/components/__tests__/FlagRow.test.js b/src/components/__tests__/FlagRow.test.js new file mode 100644 index 0000000..43a064c --- /dev/null +++ b/src/components/__tests__/FlagRow.test.js @@ -0,0 +1,42 @@ +import { render, fireEvent } from '@testing-library/react'; +import FlagRow from '../FlagRow'; + +test('FlagRow Function Test', async () => { + const item = { + id: 1, + name: 'Test Flag', + enabled: false, + }; + const flags = [{ ...item }]; + const setFlags = jest.fn(); + const setDisableSave = jest.fn(); + const handleSave = jest.fn(); + const handleDeleteFlag = jest.fn(); + + const { getByRole, getByLabelText } = render( + + ); + + // Test flipping the toggle + fireEvent.click(getByRole('checkbox')); + expect(setFlags).toHaveBeenCalledTimes(1); + expect(handleSave).toHaveBeenCalledTimes(1); + + // Test editing the flag name + const input = getByRole('textbox'); + fireEvent.change(input, { target: { value: 'New Flag' } }); + expect(setFlags).toHaveBeenCalledTimes(2); + + const sdkButton = getByLabelText('Click to see SDK setting'); + expect(sdkButton).toBeInTheDocument(); + + const deleteButton = getByLabelText('Delete Flag'); + expect(deleteButton).toBeInTheDocument(); +}); diff --git a/src/components/__tests__/SubmitControls.test.js b/src/components/__tests__/SubmitControls.test.js new file mode 100644 index 0000000..0c542ce --- /dev/null +++ b/src/components/__tests__/SubmitControls.test.js @@ -0,0 +1,78 @@ +import { render, screen, fireEvent } from '@testing-library/react'; + +import SubmitControls from '../SubmitControls'; + +describe('SubmitControls component', () => { + let mockHandleSave, mockSetFlags; + + beforeEach(() => { + mockHandleSave = jest.fn(); + mockSetFlags = jest.fn(); + }); + + it('renders component with all header fields', () => { + render( + + ); + + const addFlagButton = screen.getByText('Add Flag'); + const saveButton = screen.queryByText('Save'); + const cancelButton = screen.queryByText('Cancel'); + + // Click on Add Flag button + fireEvent.click(addFlagButton); + + // Assert setFlags is called + expect(mockSetFlags).toHaveBeenCalled(); + + // Assert Save & Cancel buttons do not exist when `lastFlag` is 0 + expect(saveButton).toBeNull(); + expect(cancelButton).toBeNull(); + }); + + test('save button interaction', () => { + render( + + ); + + const saveButton = screen.getByText('Save'); + + // Click on Save button + fireEvent.click(saveButton); + + // Assert handleSave is called + expect(mockHandleSave).toHaveBeenCalled(); + }); + + test('display saving text', () => { + render( + + ); + + const savingButton = screen.getByText('Saving'); + + // Assert Save button is now displaying "Saving" + expect(savingButton).toBeTruthy(); + }); +}); diff --git a/src/components/snippets/JsSnippet.tsx b/src/components/snippets/JsSnippet.tsx index 1e222b9..bcf88fe 100644 --- a/src/components/snippets/JsSnippet.tsx +++ b/src/components/snippets/JsSnippet.tsx @@ -3,7 +3,7 @@ import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import Clipboard from '../common/Clipboard'; -export default function ({ flag }: { flag: string }): JSX.Element { +const JsSnippet = ({ flag }: { flag: string }) => { const jsSnippet = useMemo(() => { return `import domReady from '@wordpress/dom-ready'; domReady(function () { @@ -22,4 +22,6 @@ domReady(function () { ); -} +}; + +export default JsSnippet; diff --git a/src/components/snippets/PhpSnippet.tsx b/src/components/snippets/PhpSnippet.tsx index 96c4c79..f2d6228 100644 --- a/src/components/snippets/PhpSnippet.tsx +++ b/src/components/snippets/PhpSnippet.tsx @@ -3,7 +3,7 @@ import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import Clipboard from '../common/Clipboard'; -export default function ({ flag }: { flag: string }): JSX.Element { +const PhpSnippet = ({ flag }: { flag: string }) => { const phpSnippet = useMemo(() => { return `use CodeB\\FeatureFlags\\Flag; if ( class_exists( '\\CodeB\\FeatureFlags\\Flag' ) && Flag::is_enabled( '${flag}' ) ) { @@ -17,4 +17,6 @@ if ( class_exists( '\\CodeB\\FeatureFlags\\Flag' ) && Flag::is_enabled( '${flag} ); -} +}; + +export default PhpSnippet; diff --git a/src/components/snippets/Snippet.tsx b/src/components/snippets/Snippet.tsx index 25bb9a9..c0995ec 100644 --- a/src/components/snippets/Snippet.tsx +++ b/src/components/snippets/Snippet.tsx @@ -1,5 +1,5 @@ import SyntaxHighlighter from 'react-syntax-highlighter'; -import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/hljs'; +import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/hljs'; const Snippet = ({ data, diff --git a/src/components/snippets/__tests__/JsSnippet.test.js b/src/components/snippets/__tests__/JsSnippet.test.js new file mode 100644 index 0000000..5c86d6a --- /dev/null +++ b/src/components/snippets/__tests__/JsSnippet.test.js @@ -0,0 +1,32 @@ +import JsSnippet from '../JsSnippet'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +describe('JsSnippet component', () => { + test('should render JsSnippet correctly with passed flag prop', async () => { + render(); + const result = screen.getByText(/JavaScript Snippet/i); + expect(result).toBeInTheDocument(); + }); + + test('should render the correct JavaScript snippet with the passed flag', async () => { + render(); + const snip = screen.getByText(/.codebFeatureFlags.isEnabled/i); + expect(snip).toBeInTheDocument(); + }); + + test('should update the JavaScript snippet when the flag prop changes', async () => { + const { rerender, asFragment } = render(); + let snip = screen.getByText(/testFlag1'/i); + expect(snip).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + + rerender(); + snip = screen.queryByText(/'testFlag1'/i); + expect(snip).toBeNull(); + + snip = screen.getByText(/'testFlag2'/i); + expect(snip).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/snippets/__tests__/PhpSnippet.test.js b/src/components/snippets/__tests__/PhpSnippet.test.js new file mode 100644 index 0000000..d556cb1 --- /dev/null +++ b/src/components/snippets/__tests__/PhpSnippet.test.js @@ -0,0 +1,14 @@ +import PhpSnippet from '../PhpSnippet'; +import { render } from '@testing-library/react'; + +describe('PhpSnippet component', () => { + it('it should render the correct PHP snippet', () => { + const { getByText } = render(); + expect(getByText(/testFlag/i)).toBeInTheDocument(); + }); + + it('matches snapshot', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/snippets/__tests__/TsSupport.test.js b/src/components/snippets/__tests__/TsSupport.test.js new file mode 100644 index 0000000..3a141e0 --- /dev/null +++ b/src/components/snippets/__tests__/TsSupport.test.js @@ -0,0 +1,9 @@ +import { render } from '@testing-library/react'; +import TsSupport from '../TsSupport'; + +describe('Typescript snippet component', () => { + test('renders without any error', async () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/snippets/__tests__/__snapshots__/JsSnippet.test.js.snap b/src/components/snippets/__tests__/__snapshots__/JsSnippet.test.js.snap new file mode 100644 index 0000000..ce80ef7 --- /dev/null +++ b/src/components/snippets/__tests__/__snapshots__/JsSnippet.test.js.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JsSnippet component should update the JavaScript snippet when the flag prop changes 1`] = ` + +
+

+ JavaScript Snippet +

+ +
+      
+        
+          import
+        
+        
+           domReady 
+        
+        
+          from
+        
+        
+           
+        
+        
+          '@wordpress/dom-ready'
+        
+        
+          ;
+
+        
+        
+          domReady(
+        
+        
+          function
+        
+        
+           (
+        
+        
+          ) 
+        
+        
+          {
+
+        
+        
+          	
+        
+        
+          if
+        
+        
+           (
+
+        
+        
+          		
+        
+        
+          typeof
+        
+        
+           
+        
+        
+          window
+        
+        
+          ?.codebFeatureFlags !== 
+        
+        
+          'undefined'
+        
+        
+           &&
+
+        
+        
+          		
+        
+        
+          window
+        
+        
+          .codebFeatureFlags.isEnabled(
+        
+        
+          'testFlag1'
+        
+        
+          )
+
+        
+        	) {
+
+        
+          		
+        
+        
+          // js code goes here...
+        
+        
+          
+
+        
+        	}
+});
+      
+    
+
+
+`; + +exports[`JsSnippet component should update the JavaScript snippet when the flag prop changes 2`] = ` + +
+

+ JavaScript Snippet +

+ +
+      
+        
+          import
+        
+        
+           domReady 
+        
+        
+          from
+        
+        
+           
+        
+        
+          '@wordpress/dom-ready'
+        
+        
+          ;
+
+        
+        
+          domReady(
+        
+        
+          function
+        
+        
+           (
+        
+        
+          ) 
+        
+        
+          {
+
+        
+        
+          	
+        
+        
+          if
+        
+        
+           (
+
+        
+        
+          		
+        
+        
+          typeof
+        
+        
+           
+        
+        
+          window
+        
+        
+          ?.codebFeatureFlags !== 
+        
+        
+          'undefined'
+        
+        
+           &&
+
+        
+        
+          		
+        
+        
+          window
+        
+        
+          .codebFeatureFlags.isEnabled(
+        
+        
+          'testFlag2'
+        
+        
+          )
+
+        
+        	) {
+
+        
+          		
+        
+        
+          // js code goes here...
+        
+        
+          
+
+        
+        	}
+});
+      
+    
+
+
+`; diff --git a/src/components/snippets/__tests__/__snapshots__/PhpSnippet.test.js.snap b/src/components/snippets/__tests__/__snapshots__/PhpSnippet.test.js.snap new file mode 100644 index 0000000..f1b4eca --- /dev/null +++ b/src/components/snippets/__tests__/__snapshots__/PhpSnippet.test.js.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PhpSnippet component matches snapshot 1`] = ` + +
+

+ PHP Snippet +

+ +
+      
+        
+          use
+        
+        
+           
+        
+        
+          CodeB
+        
+        
+          \\
+        
+        
+          FeatureFlags
+        
+        
+          \\
+        
+        
+          Flag
+        
+        
+          ;
+
+        
+        
+        
+          if
+        
+        
+           ( class_exists( 
+        
+        
+          '\\CodeB\\FeatureFlags\\Flag'
+        
+        
+           ) && Flag::is_enabled( 
+        
+        
+          'testFlag'
+        
+        
+           ) ) {
+
+        
+        
+          	
+        
+        
+          // php code goes here...
+        
+        
+          
+
+        
+        }
+      
+    
+
+
+`; diff --git a/src/components/snippets/__tests__/__snapshots__/TsSupport.test.js.snap b/src/components/snippets/__tests__/__snapshots__/TsSupport.test.js.snap new file mode 100644 index 0000000..f4ae727 --- /dev/null +++ b/src/components/snippets/__tests__/__snapshots__/TsSupport.test.js.snap @@ -0,0 +1,186 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Typescript snippet component renders without any error 1`] = ` + +
+

+ TypeScript support +

+

+ Create a file named + + flags.d.ts + + at the entrypoint of TypeScript code for the plugin/theme ( usually + + src + + directory ) and add the following declaration. +

+ +
+      
+        
+          declare
+        
+        
+           
+        
+        
+          namespace
+        
+        
+           codebFeatureFlags {
+
+        
+        
+          	
+        
+        
+          export
+        
+        
+           
+        
+        
+          interface
+        
+        
+           FeatureFlagProps {
+
+        
+        
+                  
+        
+        
+          isEnabled
+        
+        
+          : 
+        
+        
+          (
+        
+        
+          flag: 
+        
+        
+          string
+        
+        
+          ) =>
+        
+        
+           
+        
+        
+          boolean
+        
+        
+          ;
+
+        
+            }
+}
+
+        
+        
+          declare
+        
+        
+           
+        
+        
+          global
+        
+        
+           {
+
+        
+        
+              
+        
+        
+          interface
+        
+        
+           Window {
+
+        
+        
+                  
+        
+        
+          codebFeatureFlags
+        
+        
+          : codebFeatureFlags.FeatureFlagProps;
+
+        
+            }
+}
+
+        
+        
+          export
+        
+        
+           {};
+        
+      
+    
+
+
+`; diff --git a/tests/e2e/visual-comparison.spec.ts-snapshots/sdk-modal-chromium-darwin.png b/tests/e2e/visual-comparison.spec.ts-snapshots/sdk-modal-chromium-darwin.png index 6177ea7..0790503 100644 Binary files a/tests/e2e/visual-comparison.spec.ts-snapshots/sdk-modal-chromium-darwin.png and b/tests/e2e/visual-comparison.spec.ts-snapshots/sdk-modal-chromium-darwin.png differ diff --git a/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-darwin.png b/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-darwin.png index 3c2ff74..c5af052 100644 Binary files a/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-darwin.png and b/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-darwin.png differ diff --git a/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-linux.png b/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-linux.png index 7e02d26..c1540a7 100644 Binary files a/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-linux.png and b/tests/e2e/visual-comparison.spec.ts-snapshots/some-flags-chromium-linux.png differ diff --git a/yarn.lock b/yarn.lock index de92667..035e4e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1942,6 +1942,11 @@ "@testing-library/dom" "^9.0.0" "@types/react-dom" "^18.0.0" +"@testing-library/user-event@^14.5.2": + version "14.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" + integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"