ArtisanPack UI
DocsPageSpeed InsightsTesting

Testing

PageSpeed Insights v1.0.0

Feature tests boot a full Testbench application through ; unit tests stay framework-free so they run without the container. Nothing in this package calls...

Running the package's own suite

composer install
composer test          # Pest
composer lint          # PHP-CS-Fixer (dry run) + PHPCS
composer fix           # PHP-CS-Fixer, applied
composer cs            # PHPCS only
composer cs:fix        # PHPCBF

npm install
npm test               # Vitest, against the React and Vue components
npm run test:watch
npm run type-check     # vue-tsc --noEmit

Feature tests boot a full Testbench application through Tests\TestCase; unit tests stay framework-free so they run without the container.

Nothing in this package calls the live PageSpeed API from a test. Runs are served from checked-in response fixtures via the psiFixture( $name ) helper.

Testing an application that uses the package

Never let a test reach Google

Fake the HTTP client. A run costs quota, takes 20–60 seconds, and gives a different answer every time — three separate reasons not to do it in a suite.

use Illuminate\Support\Facades\Http;

Http::fake( [
    'www.googleapis.com/pagespeedonline/*' => Http::response( [
        'lighthouseResult' => [
            'categories' => [
                'performance' => [ 'id' => 'performance', 'score' => 0.92 ],
            ],
            'audits' => [],
        ],
    ] ),
] );

For anything beyond a smoke test, prefer seeding result rows directly — see Factories. The parser's own behaviour is already covered by this package's suite; your application's tests are about what it does with a result, not about how the payload is read.

Give it a key, or assert that it has none

Most paths short-circuit without an API key, which is easy to mistake for a broken test:

config()->set( 'pagespeed-insights.driver', 'config' );
config()->set( 'pagespeed-insights.api_key', 'test-key' );

The config driver is the one to use in tests: it needs no migration and no encryption, and ApiKeyRepository is a plain container bind, so changing pagespeed-insights.driver mid-test takes effect on the next resolve.

To exercise the missing-key path instead, set api_key to null and assert on MissingApiKeyException, exit code 3 from pagespeed:test, or the no_api_key endpoint error.

Turn off what you are not testing

config()->set( 'pagespeed-insights.scheduling.enabled', false );
config()->set( 'pagespeed-insights.alerts.enabled', false );
config()->set( 'pagespeed-insights.rate_limit.per_minute', 0 );

Most configuration is read on resolve rather than at boot, so it can be changed from inside a test. Three keys cannot: routes.enabled, routes.prefix, and routes.middleware are read when the service provider boots. Set those in defineEnvironment() (Testbench) or in the test's config file.

The digest is a delayed job

alerts.digest buffers regressions and flushes them with a delayed job, and the sync queue driver ignores delays. On a sync queue every regression sends immediately.

Either set alerts.digest.enabled to false and assert on the notification directly, or use Queue::fake() and assert the flush job was dispatched.

Factories

Both models ship factories with states covering the cases worth asserting on:

use ArtisanPackUI\PageSpeedInsights\Models\PageSpeedResult;
use ArtisanPackUI\PageSpeedInsights\Models\PageSpeedUrl;

$url = PageSpeedUrl::factory()->create( [ 'url' => 'https://example.com/pricing' ] );

PageSpeedResult::factory()->for( $url, 'pageSpeedUrl' )->create();

PageSpeedUrlFactory

State Gives you
inactive() A paused URL — skipped by the scheduler and the staleness detector
neverTested() A null last_tested_at
due( $frequency ) A URL whose cadence has elapsed
notDue( $frequency ) One whose cadence has not
fromSitemap() source = sitemap
strategy( $strategy ) A URL tested on one form factor only

PageSpeedResultFactory

State Gives you
failed( $message ) A failed row with an error_message and no scores
poor() Scores below every band floor — useful for threshold alerts
degraded() A completed run carrying warnings, so wasDegraded() is true
withoutFieldData() A completed run with no CrUX data
withRawResponse( $payload ) A row with raw_response set, for retention tests
desktop() strategy = desktop

degraded() is the one to reach for when testing anything that treats a thin run differently — alert baselines, the score card's degraded state, the --json warnings block. See Configuration → Degraded runs.

Testing the components

Livewire

use ArtisanPackUI\PageSpeedInsights\Livewire\ScoreCard;
use Livewire\Livewire;

Livewire::test( ScoreCard::class, [ 'url' => 'https://example.com/pricing' ] )
    ->assertSet( 'state', ScoreCard::STATE_LOADED )
    ->assertSee( '92' );

Assert against the STATE_* constants rather than against rendered strings where you can: the states are the contract, and the wording is translatable.

$url and $strategy are #[Locked], so a test cannot set() them — pass them to mount().

React and Vue

Both sets take a fetchImpl prop, which is the supported injection point for a test double. There is no need to intercept window.fetch:

render(<ScoreCard url="https://example.com/pricing" fetchImpl={fakeFetch} />)

endpointBase is worth setting explicitly in tests too, so a suite does not depend on routes.prefix being the default.

Optional-dependency detection

Three support classes decide whether an optional package is installed, and each caches the answer for the process. Tests that need to pretend otherwise can override them:

use ArtisanPackUI\PageSpeedInsights\Support\CmsFrameworkInstalled;
use ArtisanPackUI\PageSpeedInsights\Support\LivewireInstalled;
use ArtisanPackUI\PageSpeedInsights\Support\UiComponentsInstalled;

UiComponentsInstalled::setForTesting( false );  // render the fallback markup
UiComponentsInstalled::reset();                 // back to real detection

Reset all three in tearDown() — the package's own Tests\TestCase does — or a later test inherits the pretence.

CI

pagespeed:test is the command built for a pipeline: it runs synchronously and exits with one code per failure class. See Commands → pagespeed:test and its exit codes.

- run: php artisan pagespeed:test "$DEPLOY_URL" --min-performance=90 --max-lcp-ms=2500 --json

Budget the categories you actually requested — a budget on an unrequested category is refused before the request goes out — and remember that a budgeted measurement which did not come back exits 4 rather than passing.