diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ea58397 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + matrix: + php-version: [ '7.4', '8.1', '8.3' ] + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: pcov + + - run: composer update --prefer-dist --no-progress + + - run: vendor/bin/phpunit --coverage-text diff --git a/.gitignore b/.gitignore index 5469669..a74cbe5 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,9 @@ wp-config.php # # Note: If you wish to whitelist themes, # uncomment the next line -#/wp-content/themes \ No newline at end of file +#/wp-content/themes + +# Composer / PHPUnit +/vendor/ +/composer.lock +.phpunit.result.cache diff --git a/README.md b/README.md index dc62893..e53d36b 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,14 @@ Paigaldus 2. Aktiveeri WordPressi adminis (Plugins → DoneProject Plugin). 3. Lisa lehele/ postitusele shortcode (näide eespool). +Testid + +Unit-testid kasutavad PHPUnitit ja WordPressi funktsioonide asendajaid (`tests/wp-stubs.php`), seega WordPressi installatsiooni pole vaja. + +1. `composer install` +2. `composer test` (või `vendor/bin/phpunit`) +3. Kaetuse aruanne: `php -d pcov.enabled=1 vendor/bin/phpunit --coverage-text` (nõuab pcov või Xdebug laiendust) + Piirangud ja tähelepanekud - Pages ja category: vaikimisi ei kasuta WordPress pages taksonoomiat "category"; kui kasutad post_type="page" ja sinu lehed ei ole category-ga seotud, shortcode ei tagasta tulemusi — kasuta post tüüpi või registreeri category ka lehtedele. diff --git a/composer.json b/composer.json index 0834318..7f149d8 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,19 @@ }, "autoload": { "psr-4": { - "DoneProject\\\\": "src/" + "DoneProject\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "DoneProject\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit", + "test:coverage": "phpunit --coverage-text" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..46a1497 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + tests + + + + + src + + + diff --git a/tests/AdminTest.php b/tests/AdminTest.php new file mode 100644 index 0000000..627b034 --- /dev/null +++ b/tests/AdminTest.php @@ -0,0 +1,106 @@ +pluginStub() ); + + $hooks = array(); + foreach ( WPStub::callsTo( 'add_action' ) as $call ) { + $hooks[ $call[0] ] = $call[1]; + } + + $this->assertSame( array( $admin, 'register_settings' ), $hooks['admin_init'] ); + $this->assertSame( array( $admin, 'add_menu' ), $hooks['admin_menu'] ); + } + + public function testAddMenuRegistersOptionsPage(): void { + $admin = new Admin( $this->pluginStub() ); + + $admin->add_menu(); + + $this->assertSame( + array( 'DoneProject Settings', 'DoneProject', 'manage_options', 'doneproject-plugin', array( $admin, 'settings_page' ) ), + WPStub::firstCall( 'add_options_page' ) + ); + } + + public function testRegisterSettingsRegistersOptionSectionAndField(): void { + $admin = new Admin( $this->pluginStub() ); + + $admin->register_settings(); + + $setting = WPStub::firstCall( 'register_setting' ); + $this->assertSame( 'doneproject-plugin_group', $setting[0] ); + $this->assertSame( 'doneproject-plugin_greeting', $setting[1] ); + $this->assertSame( + array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'Tere' ), + $setting[2] + ); + + $section = WPStub::firstCall( 'add_settings_section' ); + $this->assertSame( 'doneproject-plugin_section', $section[0] ); + $this->assertSame( 'doneproject-plugin', $section[3] ); + + $field = WPStub::firstCall( 'add_settings_field' ); + $this->assertSame( 'doneproject-plugin_greeting_field', $field[0] ); + $this->assertSame( array( $admin, 'field_html' ), $field[2] ); + $this->assertSame( 'doneproject-plugin', $field[3] ); + $this->assertSame( 'doneproject-plugin_section', $field[4] ); + } + + public function testFieldHtmlRendersStoredValue(): void { + WPStub::$options['doneproject-plugin_greeting'] = 'Hei'; + $admin = new Admin( $this->pluginStub() ); + + ob_start(); + $admin->field_html(); + $html = ob_get_clean(); + + $this->assertSame( + '', + $html + ); + } + + public function testFieldHtmlRendersDefaultAndEscapesValue(): void { + WPStub::$options['doneproject-plugin_greeting'] = 'Tere "sõber" & '; + $admin = new Admin( $this->pluginStub() ); + + ob_start(); + $admin->field_html(); + $html = ob_get_clean(); + + $this->assertStringContainsString( 'value="Tere "s', $html ); + $this->assertStringNotContainsString( '', $html ); + } + + public function testSettingsPageRendersFormForPermittedUser(): void { + $admin = new Admin( $this->pluginStub() ); + + ob_start(); + $admin->settings_page(); + $html = ob_get_clean(); + + $this->assertStringContainsString( 'DoneProject seaded', $html ); + $this->assertStringContainsString( '
', $html ); + $this->assertSame( array( 'doneproject-plugin_group' ), WPStub::firstCall( 'settings_fields' ) ); + $this->assertSame( array( 'doneproject-plugin' ), WPStub::firstCall( 'do_settings_sections' ) ); + $this->assertTrue( WPStub::called( 'submit_button' ) ); + } + + public function testSettingsPageRendersNothingWithoutCapability(): void { + WPStub::$current_user_can = false; + $admin = new Admin( $this->pluginStub() ); + + ob_start(); + $admin->settings_page(); + $html = ob_get_clean(); + + $this->assertSame( '', $html ); + $this->assertFalse( WPStub::called( 'settings_fields' ) ); + $this->assertSame( array( 'manage_options' ), WPStub::firstCall( 'current_user_can' ) ); + } +} diff --git a/tests/FrontendTest.php b/tests/FrontendTest.php new file mode 100644 index 0000000..a193349 --- /dev/null +++ b/tests/FrontendTest.php @@ -0,0 +1,306 @@ +basedir = sys_get_temp_dir() . '/doneproject-tests-' . uniqid(); + mkdir( $this->basedir . '/2024/01', 0777, true ); + WPStub::$upload_dir = array( 'basedir' => $this->basedir, 'baseurl' => $this->baseurl ); + } + + protected function tearDown(): void { + $this->removeDir( $this->basedir ); + parent::tearDown(); + } + + private function removeDir( string $dir ): void { + if ( ! is_dir( $dir ) ) { + return; + } + foreach ( scandir( $dir ) as $entry ) { + if ( '.' === $entry || '..' === $entry ) { + continue; + } + $path = $dir . '/' . $entry; + is_dir( $path ) ? $this->removeDir( $path ) : unlink( $path ); + } + rmdir( $dir ); + } + + private function frontend(): Frontend { + return new Frontend( $this->pluginStub() ); + } + + private function localImage( string $relative = '/2024/01/photo.jpg' ): string { + file_put_contents( $this->basedir . $relative, 'image-bytes' ); + return $this->baseurl . $relative; + } + + public function testConstructorRegistersShortcodesAndAssetHook(): void { + $frontend = $this->frontend(); + + $shortcodes = array(); + foreach ( WPStub::callsTo( 'add_shortcode' ) as $call ) { + $shortcodes[ $call[0] ] = $call[1]; + } + + $this->assertSame( array( $frontend, 'shortcode' ), $shortcodes['doneproject'] ); + $this->assertSame( array( $frontend, 'pages_list_shortcode' ), $shortcodes['doneproject_pages'] ); + $this->assertSame( array( $frontend, 'pages_list_shortcode' ), $shortcodes['doneproject_list'] ); + $this->assertSame( + array( 'wp_enqueue_scripts', array( $frontend, 'enqueue_assets' ), 10 ), + WPStub::firstCall( 'add_action' ) + ); + } + + public function testEnqueueAssetsRegistersStyleAndScriptWithPluginVersion(): void { + $this->frontend()->enqueue_assets(); + + $style = WPStub::firstCall( 'wp_register_style' ); + $this->assertSame( 'doneproject-style', $style[0] ); + $this->assertStringEndsWith( 'assets/css/style.css', $style[1] ); + $this->assertSame( '0.1.0', $style[3] ); + + $script = WPStub::firstCall( 'wp_register_script' ); + $this->assertSame( 'doneproject-script', $script[0] ); + $this->assertStringEndsWith( 'assets/js/script.js', $script[1] ); + $this->assertSame( '0.1.0', $script[3] ); + $this->assertTrue( $script[4] ); + } + + public function testShortcodeUsesDefaultNameAndGreeting(): void { + $html = $this->frontend()->shortcode( array() ); + + $this->assertSame( '
Tere, maailm!
', $html ); + } + + public function testShortcodeUsesGivenNameAndStoredGreeting(): void { + WPStub::$options['doneproject-plugin_greeting'] = 'Hei'; + + $html = $this->frontend()->shortcode( array( 'name' => 'Tallinn' ) ); + + $this->assertSame( '
Hei, Tallinn!
', $html ); + } + + public function testShortcodeEscapesNameAndGreeting(): void { + WPStub::$options['doneproject-plugin_greeting'] = 'Tere'; + + $html = $this->frontend()->shortcode( array( 'name' => '' ) ); + + $this->assertStringNotContainsString( '