diff --git a/.github/workflows/pre-merge-test.yaml b/.github/workflows/pre-merge-test.yaml new file mode 100644 index 0000000..afd42f6 --- /dev/null +++ b/.github/workflows/pre-merge-test.yaml @@ -0,0 +1,32 @@ + +# Workflow for running smoke tests for pull requests etc. + +name: Pre merge tests + +on: + pull_request: + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v2 + with: + node-version: '16' + + - name: Setup PHP with composer v2 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: composer:v2 + + - name: Install composer dependencies. + run: composer install + + - name: Run unit tests + run: composer test diff --git a/.gitignore b/.gitignore index b29496c..b91a192 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,7 @@ dist # track these files, if they exist !.gitignore !.editorconfig +!.github + +# PHPUnit +.phpunit.result.cache \ No newline at end of file diff --git a/README.md b/README.md index 54045f8..a59b76e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Built With](#built-with) - [Prerequisites](#prerequisites) - [Installation](#installation) + - [Tests](#tests) - [Roadmap](#roadmap) - [Contributing](#contributing) - [License](#license) @@ -65,13 +66,31 @@ nano setup.json ```sh php setup.php ``` -4. Install and build NPM packages +4. Install composer packages +```sh +composer install +``` +5. Install and build NPM packages ```sh npm install && npm run build ``` -5. Install composer packages +6. Build assets ```sh -composer install +npm run build +``` +### Tests + +1. Install composer packages +```sh +composer insall +``` +2. Run unit tests +``` +composer test +``` +3. For code coverage +``` +composer coverage ``` ## Roadmap diff --git a/build.php b/build.php index 7c58993..cd691dd 100644 --- a/build.php +++ b/build.php @@ -7,10 +7,11 @@ // Any command needed to run and build plugin assets when newly cheched out of repo. $buildCommands = [ - 'npm install --no-progress', + 'npm ci --no-progress', 'npx browserslist@latest --update-db', 'npm run build', - 'composer install --prefer-dist --no-progress' + 'composer install --prefer-dist --no-progress --no-dev', + 'composer dump-autoload --no-dev --classmap-authoritative' ]; // Files and directories not suitable for prod to be removed. @@ -24,7 +25,10 @@ 'webpack.config.js', 'node_modules', 'package-lock.json', - 'package.json' + 'package.json', + 'patchwork.json', + 'phpunit.xml', + 'source/tests' ]; $dirName = basename(dirname(__FILE__)); diff --git a/composer.json b/composer.json index b1b35f5..12cd990 100644 --- a/composer.json +++ b/composer.json @@ -3,18 +3,25 @@ "description": "{{BPREPLACEDESCRIPTION}}", "type": "wordpress-plugin", "license": "MIT", + "scripts": { + "test": "./vendor/bin/phpunit --testdox", + "coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --testdox", + "minimal": "./vendor/bin/phpunit" + }, "authors": [ { "name": "{{BPREPLACEAUTHOR}}", "email": "{{BPREPLACEAUTHOREMAIL}}" } ], + "autoload": { + "psr-4": {"{{BPREPLACENAMESPACE}}\\": "source/php/"} + }, "minimum-stability": "stable", "require": {}, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/{{BPREPLACEGITHUB}}/modularity-{{BPREPLACESLUG}}.git" - } - ] + "require-dev": { + "brain/monkey": "^2.6", + "codedungeon/phpunit-result-printer": "^0.31.0", + "phpunit/phpunit": "^9.5" + } } diff --git a/modularity-boilerplate.php b/modularity-boilerplate.php index fe23924..1952715 100755 --- a/modularity-boilerplate.php +++ b/modularity-boilerplate.php @@ -22,19 +22,15 @@ define('{{BPREPLACECAPSCONSTANT}}_URL', plugins_url('', __FILE__)); define('{{BPREPLACECAPSCONSTANT}}_TEMPLATE_PATH', {{BPREPLACECAPSCONSTANT}}_PATH . 'templates/'); define('{{BPREPLACECAPSCONSTANT}}_VIEW_PATH', {{BPREPLACECAPSCONSTANT}}_PATH . 'views/'); -define('{{BPREPLACECAPSCONSTANT}}_MODULE_VIEW_PATH', plugin_dir_path(__FILE__) . 'source/php/Module/views'); +define('{{BPREPLACECAPSCONSTANT}}_MODULE_VIEW_PATH', {{BPREPLACECAPSCONSTANT}}_PATH . 'source/php/Module/views'); define('{{BPREPLACECAPSCONSTANT}}_MODULE_PATH', {{BPREPLACECAPSCONSTANT}}_PATH . 'source/php/Module/'); load_plugin_textdomain('modularity-{{BPREPLACESLUG}}', false, plugin_basename(dirname(__FILE__)) . '/languages'); -require_once {{BPREPLACECAPSCONSTANT}}_PATH . 'source/php/Vendor/Psr4ClassLoader.php'; require_once {{BPREPLACECAPSCONSTANT}}_PATH . 'Public.php'; -// Instantiate and register the autoloader -$loader = new {{BPREPLACENAMESPACE}}\Vendor\Psr4ClassLoader(); -$loader->addPrefix('{{BPREPLACENAMESPACE}}', {{BPREPLACECAPSCONSTANT}}_PATH); -$loader->addPrefix('{{BPREPLACENAMESPACE}}', {{BPREPLACECAPSCONSTANT}}_PATH . 'source/php/'); -$loader->register(); +// Register the autoloader +require __DIR__ . '/vendor/autoload.php'; // Acf auto import and export $acfExportManager = new \AcfExportManager\AcfExportManager(); diff --git a/patchwork.json b/patchwork.json new file mode 100644 index 0000000..686abd3 --- /dev/null +++ b/patchwork.json @@ -0,0 +1,7 @@ +{ + "redefinable-internals": [ + "file_exists", + "file_get_contents", + "function_exists" + ] +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..6b97f68 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ + + + + + + ./source/php + + + + + + + + + ./source/tests/php + + + \ No newline at end of file diff --git a/setup.json b/setup.json index 390d4f6..eb94964 100644 --- a/setup.json +++ b/setup.json @@ -7,5 +7,5 @@ "capsConstant": "MODULARITY_BOILERPLATE", "author": "Firstname Lastname @ Company", "authoremail": "firstname.lastname@company.com", - "github": "githubname" + "github": "helsingborg-stad" } \ No newline at end of file diff --git a/source/php/Helper/CacheBust.php b/source/php/Helper/CacheBust.php index 5613c37..a9d5251 100755 --- a/source/php/Helper/CacheBust.php +++ b/source/php/Helper/CacheBust.php @@ -7,12 +7,21 @@ class CacheBust /** * Returns the revved/cache-busted file name of an asset. * @param string $name Asset name (array key) from rev-mainfest.json - * @param boolean $returnName Returns $name if set to true while in dev mode * @return string filename of the asset (including directory above) */ - public static function name($name, $returnName = true) + public function name($name) { - $revManifest = self::getRevManifest(); + $jsonPath = {{BPREPLACECAPSCONSTANT}}_PATH . apply_filters( + '{{BPREPLACENAMESPACE}}/Helper/CacheBust/RevManifestPath', + 'dist/manifest.json' + ); + + $revManifest = []; + if (file_exists($jsonPath)) { + $revManifest = json_decode(file_get_contents($jsonPath), true); + } elseif ($this->isDebug()) { + echo '
Error: Assets not built. Go to ' . {{BPREPLACECAPSCONSTANT}}_PATH . ' and run gulp. See ' . {{BPREPLACECAPSCONSTANT}}_PATH . 'README.md for more info.
'; + } if (!isset($revManifest[$name])) { return; @@ -22,20 +31,10 @@ public static function name($name, $returnName = true) } /** - * Decode assets json to array - * @return array containg assets filenames + * Check if debug mode, Remove constant dependency in tests. */ - public static function getRevManifest() + public function isDebug() { - $jsonPath = {{BPREPLACECAPSCONSTANT}}_PATH . apply_filters( - '{{BPREPLACENAMESPACE}}/Helper/CacheBust/RevManifestPath', - 'dist/manifest.json' - ); - - if (file_exists($jsonPath)) { - return json_decode(file_get_contents($jsonPath), true); - } elseif (WP_DEBUG) { - echo '
Error: Assets not built. Go to ' . {{BPREPLACECAPSCONSTANT}}_PATH . ' and run gulp. See ' . {{BPREPLACECAPSCONSTANT}}_PATH . 'README.md for more info.
'; - } + return defined('WP_DEBUG') && WP_DEBUG; } } diff --git a/source/php/Module/Boilerplate.php b/source/php/Module/Boilerplate.php index b96f7dc..d87fc59 100644 --- a/source/php/Module/Boilerplate.php +++ b/source/php/Module/Boilerplate.php @@ -2,8 +2,6 @@ namespace {{BPREPLACENAMESPACE}}\Module; -use {{BPREPLACENAMESPACE}}\Helper\CacheBust; - /** * Class {{BPREPLACESLUGCAMELCASE}} * @package {{BPREPLACESLUGCAMELCASE}}\Module @@ -18,6 +16,8 @@ public function init() $this->nameSingular = __("{{BPREPLACESLUGCAMELCASE}}", 'modularity-{{BPREPLACESLUG}}'); $this->namePlural = __("{{BPREPLACESLUGCAMELCASE}}", 'modularity-{{BPREPLACESLUG}}'); $this->description = __("{{BPREPLACEDESCRIPTION}}", 'modularity-{{BPREPLACESLUG}}'); + + $this->cacheBust = new \{{BPREPLACENAMESPACE}}\Helper\CacheBust(); } /** @@ -26,12 +26,10 @@ public function init() */ public function data(): array { - $data = array(); - //Append field config - $data = array_merge($data, (array) \Modularity\Helper\FormatObject::camelCase( + $data = (array) \Modularity\Helper\FormatObject::camelCase( get_fields($this->ID) - )); + ); //Translations $data['lang'] = (object) array( @@ -62,7 +60,7 @@ public function style() //Register custom css wp_register_style( 'modularity-{{BPREPLACESLUG}}', - {{BPREPLACECAPSCONSTANT}}_URL . '/dist/' . CacheBust::name('css/modularity-{{BPREPLACESLUG}}.css'), + {{BPREPLACECAPSCONSTANT}}_URL . '/dist/' . $this->cacheBust->name('css/modularity-{{BPREPLACESLUG}}.css'), null, '1.0.0' ); @@ -80,7 +78,7 @@ public function script() //Register custom css wp_register_script( 'modularity-{{BPREPLACESLUG}}', - {{BPREPLACECAPSCONSTANT}}_URL . '/dist/' . CacheBust::name('js/modularity-{{BPREPLACESLUG}}.js'), + {{BPREPLACECAPSCONSTANT}}_URL . '/dist/' . $this->cacheBust->name('js/modularity-{{BPREPLACESLUG}}.js'), null, '1.0.0' ); diff --git a/source/php/Vendor/Psr4ClassLoader.php b/source/php/Vendor/Psr4ClassLoader.php deleted file mode 100755 index 8e89f31..0000000 --- a/source/php/Vendor/Psr4ClassLoader.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -class Psr4ClassLoader -{ - /** - * @var array - */ - private $prefixes = array(); - - /** - * @param string $prefix - * @param string $baseDir - */ - public function addPrefix($prefix, $baseDir) - { - $prefix = trim($prefix, '\\').'\\'; - $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; - $this->prefixes[] = array($prefix, $baseDir); - } - - /** - * @param string $class - * - * @return string|null - */ - public function findFile($class) - { - $class = ltrim($class, '\\'); - foreach ($this->prefixes as $current) { - list($currentPrefix, $currentBaseDir) = $current; - if (0 === strpos($class, $currentPrefix)) { - $classWithoutPrefix = substr($class, strlen($currentPrefix)); - $file = $currentBaseDir.str_replace('\\', DIRECTORY_SEPARATOR, $classWithoutPrefix).'.php'; - - if (file_exists($file)) { - return $file; - } - } - } - } - - /** - * @param string $class - * - * @return bool - */ - public function loadClass($class) - { - $file = $this->findFile($class); - if (null !== $file) { - require $file; - - return true; - } - - return false; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Removes this instance from the registered autoloaders. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } -} diff --git a/source/tests/includes/PluginTestCase.php b/source/tests/includes/PluginTestCase.php new file mode 100644 index 0000000..baf5d4a --- /dev/null +++ b/source/tests/includes/PluginTestCase.php @@ -0,0 +1,42 @@ +returnArg(1); + Monkey\Functions\when('_e') + ->returnArg(1); + Monkey\Functions\when('_n') + ->returnArg(1); + } + + /** + * Teardown which calls \WP_Mock tearDown + * + * @return void + */ + public function tearDown(): void + { + Monkey\tearDown(); + parent::tearDown(); + } +} diff --git a/source/tests/includes/bootstrap.php b/source/tests/includes/bootstrap.php new file mode 100644 index 0000000..f30de68 --- /dev/null +++ b/source/tests/includes/bootstrap.php @@ -0,0 +1,19 @@ +addPsr4('{{BPREPLACENAMESPACE}}\\Test\\', __DIR__ . '/../php/'); + +require_once __DIR__ . '/PluginTestCase.php'; diff --git a/source/tests/php/Admin/SettingsTest.php b/source/tests/php/Admin/SettingsTest.php new file mode 100644 index 0000000..0ac77ea --- /dev/null +++ b/source/tests/php/Admin/SettingsTest.php @@ -0,0 +1,35 @@ +registerSettings()')); + } + + public function testRegisterSettings() + { + Functions\expect('acf_add_options_sub_page')->once()->with( + array( + 'page_title' => __("{{BPREPLACENAME}}", 'modularity-{{BPREPLACESLUG}}'), + 'menu_title' => __("{{BPREPLACENAME}} Settings", 'modularity-{{BPREPLACESLUG}}'), + 'menu_slug' => 'modularity-{{BPREPLACESLUG}}-settings', + 'parent_slug' => 'options-general.php', + 'capability' => 'manage_options' + ) + ); + + $settings = new Settings(); + + $settings->registerSettings(); + } +} diff --git a/source/tests/php/AppTest.php b/source/tests/php/AppTest.php new file mode 100644 index 0000000..0ab578c --- /dev/null +++ b/source/tests/php/AppTest.php @@ -0,0 +1,70 @@ +createPartialMock( + AppTest::class, + ['modularity_register_module'] + ); + parent::setUp(); + } + + public function testAddHooks() + { + new App(); + + self::assertNotFalse(has_action('plugins_loaded', '{{BPREPLACENAMESPACE}}\App->registerModule()')); + self::assertNotFalse(has_filter('Municipio/blade/view_paths', '{{BPREPLACENAMESPACE}}\App->addViewPaths()')); + } + + public function testAddViewPaths() + { + $path = '/test'; + Functions\when('is_child_theme')->justReturn(false); + $app = new App(); + $viewPaths = $app->addViewPaths([$path]); + $this->assertSame(end($viewPaths), $path); + } + + public function testAddViewPathsInChildTheme() + { + $path = '/test'; + Functions\when('is_child_theme')->justReturn(true); + $app = new App(); + $viewPaths = $app->addViewPaths([$path]); + $this->assertSame($viewPaths[0], $path); + } + + public function testRegisterModule() + { + Functions\when('function_exists')->justReturn(true); + + self::$functions->expects($this->once())->method('modularity_register_module'); + $app = new App(); + $app->registerModule(); + } + + // Helper function for mocking global function(Must break coding standard). + public function modularity_register_module() + { + return; + } +} + +// Helper function for mocking global function. +function modularity_register_module() +{ + return AppTest::$functions->modularity_register_module(); +} diff --git a/source/tests/php/Helper/CacheBustTest.php b/source/tests/php/Helper/CacheBustTest.php new file mode 100644 index 0000000..146050a --- /dev/null +++ b/source/tests/php/Helper/CacheBustTest.php @@ -0,0 +1,35 @@ +justReturn(false); + + $cacheBust = Mockery::mock('{{BPREPLACENAMESPACE}}\Helper\CacheBust')->makePartial(); + $cacheBust->shouldReceive('isDebug')->andReturn(true); + + $realfile = $cacheBust->name('nofile'); + + // Just expect some output. + $this->expectOutputRegex('/^.+$/'); + $this->assertSame($realfile, null); + } + + public function testReturnRealFileWhenFoundInManifest() + { + Functions\when('file_exists')->justReturn(true); + Functions\when('file_get_contents')->justReturn('{"file.js": "realfile.js"}'); + + $cacheBust = new CacheBust(); + $realfile = $cacheBust->name('file.js'); + + $this->assertSame($realfile, 'realfile.js'); + } +} diff --git a/source/tests/php/Module/BoilerplateTest.php b/source/tests/php/Module/BoilerplateTest.php new file mode 100644 index 0000000..fe2f514 --- /dev/null +++ b/source/tests/php/Module/BoilerplateTest.php @@ -0,0 +1,96 @@ +init(); + + $this->assertSame( + ${{BPREPLACESLUG}}->nameSingular, + __("{{BPREPLACESLUGCAMELCASE}}", 'modularity-{{BPREPLACESLUG}}') + ); + $this->assertSame( + ${{BPREPLACESLUG}}->namePlural, + __("{{BPREPLACESLUGCAMELCASE}}", 'modularity-{{BPREPLACESLUG}}') + ); + $this->assertSame( + ${{BPREPLACESLUG}}->description, + __("{{BPREPLACEDESCRIPTION}}", 'modularity-{{BPREPLACESLUG}}') + ); + } + + public function testData() + { + $testData = [ + 'testconfig' => 'testconfig', + 'lang' => (object) [ + 'info' => __( + "Hey! This is your new {{BPREPLACENAME}} module. Let's get going.", + 'modularity-{{BPREPLACESLUG}}' + ) + ] + ]; + + // Mock parent class that is loaded outside of plugin. + Mockery::mock('\Modularity\Module'); + $formatObject = Mockery::mock('overload:\Modularity\Helper\FormatObject'); + $formatObject->shouldReceive('camelCase')->once()->andReturn($testData); + + + + Functions\when('get_fields')->justReturn(false); + + ${{BPREPLACESLUG}} = new {{BPREPLACESLUGCAMELCASE}}(); + ${{BPREPLACESLUG}}->ID = 1; + + + + $data = ${{BPREPLACESLUG}}->data(); + + $this->assertSame($data['testconfig'], 'testconfig'); + $this->assertSame((array) $data['lang'], (array) $testData['lang']); + } + + public function testTemplate() + { + // Mock parent class that is loaded outside of plugin. + Mockery::mock('\Modularity\Module'); + ${{BPREPLACESLUG}} = new {{BPREPLACESLUGCAMELCASE}}(); + $this->assertSame(${{BPREPLACESLUG}}->template(), '{{BPREPLACESLUG}}.blade.php'); + } + + public function testStyle() + { + Functions\expect('wp_register_style')->once(); + Functions\expect('wp_enqueue_style')->once()->with('modularity-{{BPREPLACESLUG}}'); + + // Mock parent class that is loaded outside of plugin. + Mockery::mock('\Modularity\Module'); + ${{BPREPLACESLUG}} = new {{BPREPLACESLUGCAMELCASE}}(); + ${{BPREPLACESLUG}}->init(); + ${{BPREPLACESLUG}}->style(); + } + + public function testScript() + { + Functions\expect('wp_register_script')->once(); + Functions\expect('wp_enqueue_script')->once()->with('modularity-{{BPREPLACESLUG}}'); + + // Mock parent class that is loaded outside of plugin. + Mockery::mock('\Modularity\Module'); + + ${{BPREPLACESLUG}} = new {{BPREPLACESLUGCAMELCASE}}(); + ${{BPREPLACESLUG}}->init(); + ${{BPREPLACESLUG}}->script(); + } +}