diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 7c73a73..a916b73 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -19,14 +19,15 @@ jobs: runs-on: ubuntu-latest - # Define the matrix of different PHP, Laravel, and testbench versions + # Define the matrix of different PHP, Laravel, testbench versions, and shards strategy: # Fail the whole workflow if one of the jobs fails fail-fast: true matrix: - php: [ 8.2, 8.3, 8.4 ] + php: [ 8.3, 8.4 ] laravel: [ 11.*, 12.* ] dependency-version: [ prefer-stable ] + shard: [ 1, 2, 3, 4 ] include: # Laravel 12 uses Orchestra Testbench 10 - laravel: 12.* @@ -34,7 +35,7 @@ jobs: # Laravel 11 uses Orchestra Testbench 9 - laravel: 11.* testbench: 9.* - name: PHP ${{ matrix.php }} / L${{ matrix.laravel }} / ${{ matrix.dependency-version }} + name: PHP ${{ matrix.php }} / L${{ matrix.laravel }} / ${{ matrix.dependency-version }} / Shard ${{ matrix.shard }}/4 steps: @@ -46,6 +47,29 @@ jobs: with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v3 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-latest + restore-keys: | + playwright-${{ runner.os }}-latest + playwright-${{ runner.os }}- + + - name: Install and update Playwright + run: | + if [ ! -d ~/.cache/ms-playwright ]; then + npm install playwright@latest + npx playwright install --with-deps + fi + - name: Validate composer.json and composer.lock run: composer validate --strict @@ -61,19 +85,31 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug - # extensions: mbstring, gd, intl + extensions: mbstring, gd, intl, pcntl - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update - composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress --no-interaction + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update + composer require "orchestra/testbench:${{ matrix.testbench }}" --dev --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress --no-interaction --no-scripts + + - name: Setup testbench environment + run: | + cp workbench/.env.example workbench/.env + sed -i 's/APP_KEY=/APP_KEY=base64:ZQvPGC7uVADkjOgtGIIuCI8u3\/Pzu+VaRObIbHsgjCc=/' workbench/.env + sed -i 's/APP_ENV=local/APP_ENV=testing/' workbench/.env + grep "APP_KEY=base64:" workbench/.env + npm install + php vendor/bin/testbench vendor:publish --tag='filament-shield-config' + php vendor/bin/testbench filament:assets + php vendor/bin/testbench package:sync-skeleton - name: Run test suite - run: composer test -- --coverage-clover ./coverage.xml + run: vendor/bin/pest --shard=${{ matrix.shard }}/4 --parallel --coverage-clover ./coverage.xml - name: Upload coverage reports to Codecov # Make sure the Codecov action is only executed once - if: matrix.php == '8.2' && matrix.laravel == '12.*' && matrix.dependency-version == 'prefer-stable' + if: matrix.php == '8.3' && matrix.laravel == '12.*' && matrix.dependency-version == 'prefer-stable' && matrix.shard == 1 uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/composer.json b/composer.json index af04be2..37ee2e2 100644 --- a/composer.json +++ b/composer.json @@ -53,8 +53,10 @@ "require-dev": { "laravel/pint": "^1.21", "orchestra/testbench": "^9.9|^10.0", - "pestphp/pest": "^3.7", - "pestphp/pest-plugin-livewire": "^3.0" + "pestphp/pest": "^4.0", + "pestphp/pest-plugin-browser": "^4.0", + "pestphp/pest-plugin-laravel": "^4.0", + "pestphp/pest-plugin-livewire": "^4.0" }, "scripts": { "post-autoload-dump": [ @@ -64,7 +66,12 @@ "post-install-cmd": "@setup", "post-update-cmd": "@setup", "format": "vendor/bin/pint", - "test": "vendor/bin/testbench package:test", + "test": "vendor/bin/testbench package:test --parallel", + "test-shard": "vendor/bin/pest --shard=%SHARD%/%SHARDS%", + "test-shard-1": "vendor/bin/pest --shard=1/4", + "test-shard-2": "vendor/bin/pest --shard=2/4", + "test-shard-3": "vendor/bin/pest --shard=3/4", + "test-shard-4": "vendor/bin/pest --shard=4/4", "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", "prepare": "@php vendor/bin/testbench package:discover --ansi", "build": "@php vendor/bin/testbench workbench:build --ansi", diff --git a/package-lock.json b/package-lock.json index 0eae449..de0e573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,9 @@ "requires": true, "packages": { "": { + "dependencies": { + "playwright": "^1.56.1" + }, "devDependencies": { "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", @@ -633,6 +636,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1042,6 +1059,36 @@ "dev": true, "license": "ISC" }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index fc63ee2..fc2c7b3 100644 --- a/package.json +++ b/package.json @@ -8,5 +8,8 @@ "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", "husky": "^9.1.7" + }, + "dependencies": { + "playwright": "^1.56.1" } } diff --git a/tests/Browser/CountryResourceBrowserTest.php b/tests/Browser/CountryResourceBrowserTest.php new file mode 100644 index 0000000..ef760fa --- /dev/null +++ b/tests/Browser/CountryResourceBrowserTest.php @@ -0,0 +1,48 @@ +setUpSuperAdmin(); +}); + +test('can browse country resource page', function () { + $region = Region::factory()->create(['name' => 'Test Region']); + $country = Country::factory()->create([ + 'name' => 'Test Country', + 'id' => 'TC', + 'region_id' => $region->id, + ]); + + $this->visit(CountryResource::getUrl()) + ->assertSee('Countries') + ->assertSee('Test Country') + ->assertSee('TC') + ->assertSee('Test Region'); +}); + +test('can interact with country resource table', function () { + $region = Region::factory()->create(['name' => 'Browser Test Region']); + $country = Country::factory()->create([ + 'name' => 'Browser Test Country', + 'id' => 'BTC', + 'region_id' => $region->id, + ]); + + $this->visit(CountryResource::getUrl()) + ->assertSee('Browser Test Country') + ->assertSee('BTC') + ->assertSee('Browser Test Region') + ->assertSee('Countries'); +}); + +test('country resource page loads without JavaScript errors', function () { + $this->visit(CountryResource::getUrl()) + ->assertSee('Countries') + ->assertDontSee('Uncaught') + ->assertDontSee('ReferenceError') + ->assertDontSee('TypeError') + ->assertDontSee('SyntaxError'); +}); diff --git a/tests/Browser/VisualRegressionTest.php b/tests/Browser/VisualRegressionTest.php new file mode 100644 index 0000000..be670d4 --- /dev/null +++ b/tests/Browser/VisualRegressionTest.php @@ -0,0 +1,93 @@ +setUpSuperAdmin(); +}); + +describe('Visual Regression Tests', function () { + test('country resource page visual regression', function () { + $region = Region::factory()->create(['name' => 'Visual Test Region']); + $country = Country::factory()->create([ + 'name' => 'Visual Test Country', + 'id' => 'VT', + 'a3_id' => 'VTC', + 'num_code' => '999', + 'flag' => '🏳️', + 'region_id' => $region->id, + ]); + + $this->visit(CountryResource::getUrl()) + ->assertSee('Countries') + ->assertSee('Visual Test Country') + ->wait(1) + ->assertScreenshotMatches(); + }); + + test('currency resource page visual regression', function () { + $this->visit(CurrencyResource::getUrl()) + ->assertSee('Currencies') + ->wait(1) + ->assertScreenshotMatches(); + }); + + test('post resource page visual regression', function () { + $this->visit(PostResource::getUrl()) + ->assertSee('Posts') + ->wait(1) + ->assertScreenshotMatches(); + }); + + test('region resource page visual regression', function () { + $this->visit(RegionResource::getUrl()) + ->assertSee('Regions') + ->wait(1) + ->assertScreenshotMatches(); + }); + + test('country resource page with data visual regression', function () { + $region1 = Region::factory()->create(['name' => 'Europe']); + $region2 = Region::factory()->create(['name' => 'Asia']); + + Country::factory()->create([ + 'name' => 'Germany', + 'id' => 'DE', + 'a3_id' => 'DEU', + 'num_code' => '276', + 'flag' => 'πŸ‡©πŸ‡ͺ', + 'region_id' => $region1->id, + ]); + + Country::factory()->create([ + 'name' => 'Japan', + 'id' => 'JP', + 'a3_id' => 'JPN', + 'num_code' => '392', + 'flag' => 'πŸ‡―πŸ‡΅', + 'region_id' => $region2->id, + ]); + + Country::factory()->create([ + 'name' => 'United States', + 'id' => 'US', + 'a3_id' => 'USA', + 'num_code' => '840', + 'flag' => 'πŸ‡ΊπŸ‡Έ', + 'region_id' => $region1->id, + ]); + + $this->visit(CountryResource::getUrl()) + ->assertSee('Countries') + ->assertSee('Germany') + ->assertSee('Japan') + ->assertSee('United States') + ->wait(1) + ->assertScreenshotMatches(); + }); +}); diff --git a/tests/Feature/SmokeTest.php b/tests/Feature/SmokeTest.php new file mode 100644 index 0000000..00dda2f --- /dev/null +++ b/tests/Feature/SmokeTest.php @@ -0,0 +1,90 @@ +setUpSuperAdmin(); +}); + +describe('Smoke Tests', function () { + test('all resource URLs are accessible', function () { + $resources = [ + CountryResource::class, + CurrencyResource::class, + PostResource::class, + RegionResource::class, + ]; + + foreach ($resources as $resource) { + $this->get($resource::getUrl()) + ->assertSuccessful() + ->assertSee('Filament'); + } + }); + + test('country resource URLs are accessible', function () { + $this->get(CountryResource::getUrl()) + ->assertSuccessful() + ->assertSee('Countries') + ->assertSee('Filament'); + }); + + test('currency resource URLs are accessible', function () { + $this->get(CurrencyResource::getUrl()) + ->assertSuccessful() + ->assertSee('Currencies') + ->assertSee('Filament'); + }); + + test('post resource URLs are accessible', function () { + $this->get(PostResource::getUrl()) + ->assertSuccessful() + ->assertSee('Posts') + ->assertSee('Filament'); + }); + + test('region resource URLs are accessible', function () { + $this->get(RegionResource::getUrl()) + ->assertSuccessful() + ->assertSee('Regions') + ->assertSee('Filament'); + }); + + test('all resource URLs return valid HTML', function () { + $resources = [ + CountryResource::class, + CurrencyResource::class, + PostResource::class, + RegionResource::class, + ]; + + foreach ($resources as $resource) { + $response = $this->get($resource::getUrl()); + + $response->assertSuccessful(); + $response->assertHeader('content-type', 'text/html; charset=UTF-8'); + $response->assertSee('', false); + } + }); + + test('all resource URLs have no JavaScript errors', function () { + $resources = [ + CountryResource::class, + CurrencyResource::class, + PostResource::class, + RegionResource::class, + ]; + + foreach ($resources as $resource) { + $response = $this->get($resource::getUrl()); + + $response->assertSuccessful(); + $response->assertDontSee('Uncaught'); + $response->assertDontSee('ReferenceError'); + $response->assertDontSee('TypeError'); + } + }); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index a6a0767..68cd3be 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -40,7 +40,10 @@ protected function migrate(): self */ protected function setUpSuperAdmin(): self { - $this->superAdmin = User::factory()->create(); + $this->superAdmin = User::factory()->create([ + 'name' => 'Test Super Admin', + 'email' => 'test@example.com', + ]); // Assign super admin role and give all permissions $superAdminRole = \Spatie\Permission\Models\Role::where('name', 'super_admin')->first();