NAME

PAGI::Test::Client - Test client for PAGI applications

SYNOPSIS

use PAGI::Test::Client;

my $client = PAGI::Test::Client->new(app => $app);

# Simple GET
my $res = $client->get('/');
is $res->status, 200;
is $res->text, 'Hello World';

# GET with query parameters
my $res = $client->get('/search', query => { q => 'perl' });

# POST with JSON body
my $res = $client->post('/api/users', json => { name => 'John' });

# POST with form data
my $res = $client->post('/login', form => { user => 'admin' });

# Custom headers
my $res = $client->get('/api', headers => { Authorization => 'Bearer xyz' });

# Multiple values for same header/query/form field
my $res = $client->get('/search',
    query   => { tag => ['perl', 'async'] },       # ?tag=perl&tag=async
    headers => { Accept => ['text/html', 'application/json'] },
);

# Arrayref of pairs for explicit ordering
my $res = $client->get('/api',
    headers => [['X-Custom', 'first'], ['X-Custom', 'second']],
);

# Multi-value form (checkboxes, multi-select)
my $res = $client->post('/survey',
    form => { colors => ['red', 'blue', 'green'] },
);

# Session cookies persist across requests
$client->post('/login', form => { user => 'admin', pass => 'secret' });
my $res = $client->get('/dashboard');  # authenticated!

DESCRIPTION

PAGI::Test::Client allows you to test PAGI applications without starting a real server. It invokes your app directly by constructing the PAGI protocol messages ($scope, $receive, $send), making tests fast and simple.

This is inspired by Starlette's TestClient but adapted for Perl and PAGI's specific features like first-class SSE support.

CONSTRUCTOR

new

my $client = PAGI::Test::Client->new(
    app      => $app,           # Required: PAGI app coderef
    headers  => { ... },        # Optional: default headers
    lifespan => 1,              # Optional: enable lifespan (default: 0)
);

Options

app (required)

The PAGI application coderef to test.

headers

Default headers to include in every request. Supports multiple formats:

# Simple hash (single values)
headers => { 'X-API-Key' => 'secret' }

# Hash with arrayref values (multiple values per header)
headers => { Accept => ['application/json', 'text/html'] }

# Arrayref of pairs (explicit ordering)
headers => [['Accept', 'application/json'], ['Accept', 'text/html']]

Request-specific headers with the same name will replace (not append to) these default headers.

lifespan

If true, the client will send lifespan.startup when started and lifespan.shutdown when stopped. Default is false (most tests don't need it).

HTTP METHODS

All HTTP methods return a PAGI::Test::Response object.

get

my $res = $client->get($path, %options);

post

my $res = $client->post($path, %options);

put

my $res = $client->put($path, %options);

patch

my $res = $client->patch($path, %options);

delete

my $res = $client->delete($path, %options);
my $res = $client->head($path, %options);

options

my $res = $client->options($path, %options);

Request Options

headers => { ... } or [ [...], [...] ]

Additional headers for this request. Supports multiple formats:

# Simple hash
headers => { Authorization => 'Bearer xyz' }

# Multiple values (arrayref in hash)
headers => { Accept => ['application/json', 'text/html'] }

# Arrayref of pairs (preserves order)
headers => [['X-Custom', 'first'], ['X-Custom', 'second']]

Request headers with the same name as client default headers will replace the defaults (not append).

query => { ... } or [ [...], [...] ]

Query string parameters. Supports multiple formats:

# Simple hash
query => { q => 'perl' }

# Multiple values
query => { tag => ['perl', 'async'] }  # ?tag=perl&tag=async

# Arrayref of pairs
query => [['tag', 'perl'], ['tag', 'async']]

Note: Query params are appended to any existing query string in the path. To avoid duplicates, put all params either in the path or in the query option, not both with the same key.

json => { ... }

JSON request body. Automatically sets Content-Type to application/json.

form => { ... } or [ [...], [...] ]

Form-encoded request body. Sets Content-Type to application/x-www-form-urlencoded. Supports multiple formats:

# Simple hash
form => { user => 'admin', pass => 'secret' }

# Multiple values (checkboxes, multi-select)
form => { colors => ['red', 'blue', 'green'] }

# Arrayref of pairs
form => [['color', 'red'], ['color', 'blue']]
body => $bytes

Raw request body bytes.

SESSION METHODS

cookies

my $hashref = $client->cookies;

Returns all current session cookies.

my $value = $client->cookie('session_id');

Returns a specific cookie value.

$client->set_cookie('theme', 'dark');

Manually sets a cookie.

clear_cookies

$client->clear_cookies;

Clears all session cookies.

WEBSOCKET

websocket

# Callback style (auto-close)
$client->websocket('/ws', sub {
    my ($ws) = @_;
    $ws->send_text('hello');
    is $ws->receive_text, 'echo: hello';
});

# Explicit style
my $ws = $client->websocket('/ws');
$ws->send_text('hello');
is $ws->receive_text, 'echo: hello';
$ws->close;

# With options
my $ws = $client->websocket('/ws',
    headers      => { Authorization => 'Bearer xyz' },
    subprotocols => ['chat', 'json'],
);

# Options with callback
$client->websocket('/ws', headers => { 'X-Token' => 'abc' }, sub {
    my ($ws) = @_;
    # ...
});

See PAGI::Test::WebSocket for the WebSocket connection API.

SSE (Server-Sent Events)

sse

# Callback style (auto-close)
$client->sse('/events', sub {
    my ($sse) = @_;
    my $event = $sse->receive_event;
    is $event->{data}, 'connected';
});

# Explicit style
my $sse = $client->sse('/events');
my $event = $sse->receive_event;
$sse->close;

# With headers (e.g., for reconnection)
my $sse = $client->sse('/events',
    headers => { 'Last-Event-ID' => '42' },
);

# Options with callback
$client->sse('/events', headers => { Authorization => 'Bearer xyz' }, sub {
    my ($sse) = @_;
    # ...
});

See PAGI::Test::SSE for the SSE connection API.

LIFESPAN

start

$client->start;

Triggers lifespan.startup. Only needed if lifespan = 1> was passed to the constructor.

stop

$client->stop;

Triggers lifespan.shutdown.

state

my $state = $client->state;

Returns the shared state hashref from lifespan.

run

PAGI::Test::Client->run($app, sub {
    my ($client) = @_;
    # ... tests ...
});

Class method that creates a client with lifespan enabled, calls start, runs your callback, then calls stop. Exceptions propagate.

SEE ALSO

PAGI::Test::Response, PAGI::Test::WebSocket, PAGI::Test::SSE