Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ coverage.xml
# Playwright
node_modules/
/tests/Browser/Screenshots
/tests/Browser/StorageState

# MacOS
.DS_Store
1 change: 1 addition & 0 deletions src/Api/AwaitableWebpage.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function __construct(
private array $nonAwaitableMethods = [
'assertScreenshotMatches',
'assertNoAccessibilityIssues',
'saveStorageState',
],
) {
//
Expand Down
25 changes: 24 additions & 1 deletion src/Api/PendingAwaitablePage.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Pest\Browser\Playwright\InitScript;
use Pest\Browser\Playwright\Playwright;
use Pest\Browser\Support\ComputeUrl;
use Pest\Browser\Support\StorageState;

/**
* @mixin Webpage|AwaitableWebpage
Expand Down Expand Up @@ -154,6 +155,19 @@ public function geolocation(float $latitude, float $longitude): self
]);
}

/**
* Loads a previously saved storage state (cookies and localStorage) into the browser context.
*
* This allows tests to skip login flows by reusing authenticated state saved with saveStorageState().
*/
public function withStorageState(string $name): self
{
return new self($this->browserType, $this->device, $this->url, [
'storageState' => StorageState::path($name),
...$this->options,
]);
}

/**
* Creates the webpage instance.
*/
Expand All @@ -170,6 +184,13 @@ private function createAwaitablePage(): AwaitableWebpage
*/
private function buildAwaitablePage(array $options): AwaitableWebpage
{
if (isset($options['storageState']) && is_string($options['storageState'])) {
$options['storageState'] = json_decode(
(string) file_get_contents($options['storageState']),
true,
);
}

$browser = Playwright::browser($this->browserType)->launch();

$context = $browser->newContext([
Expand All @@ -184,8 +205,10 @@ private function buildAwaitablePage(array $options): AwaitableWebpage

$url = ComputeUrl::from($this->url);

$gotoOptions = array_diff_key($options, array_flip(['storageState', 'host']));

return new AwaitableWebpage(
$context->newPage()->goto($url, $options),
$context->newPage()->goto($url, $gotoOptions),
$url,
);
}
Expand Down
13 changes: 13 additions & 0 deletions src/Api/Webpage.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ public function value(string $selector): string
return $this->guessLocator($selector)->inputValue();
}

/**
* Saves the current browser context's storage state (cookies and localStorage) to a file.
*
* The file is saved to tests/Browser/StorageState/<name>.json and can be loaded in
* subsequent tests via withStorageState() to avoid repetitive login flows.
*/
public function saveStorageState(?string $name = null): self
{
$this->page->saveStorageState($name);

return $this;
}

/**
* Gets the locator for the given selector.
*/
Expand Down
16 changes: 16 additions & 0 deletions src/Playwright/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,20 @@ public function addInitScript(string $script): self

return $this;
}

/**
* Returns the current storage state (cookies and localStorage) as a JSON string.
*/
public function storageState(): string
{
$response = $this->sendMessage('storageState');

foreach ($response as $message) {
if (isset($message['result'])) {
return (string) json_encode($message['result']);
}
}

return '{"cookies":[],"origins":[]}';
}
}
14 changes: 14 additions & 0 deletions src/Playwright/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Pest\Browser\Support\Screenshot;
use Pest\Browser\Support\Selector;
use Pest\Browser\Support\Shell;
use Pest\Browser\Support\StorageState;
use Pest\TestSuite;
use PHPUnit\Framework\ExpectationFailedException;
use RuntimeException;
Expand Down Expand Up @@ -438,6 +439,19 @@ public function screenshot(bool $fullPage = true, ?string $filename = null): ?st
return Screenshot::save($binary, $filename);
}

/**
* Saves the current browser context's storage state (cookies and localStorage) to a file.
*
* The file is stored under tests/Browser/StorageState/ and can be loaded in subsequent
* tests via withStorageState() to skip repetitive login flows.
*/
public function saveStorageState(?string $name = null): string
{
$json = $this->context->storageState();

return StorageState::save($json, $name);
}

/**
* Make screenshot of a specific element.
*/
Expand Down
49 changes: 49 additions & 0 deletions src/Support/StorageState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Pest\Browser\Support;

use Pest\TestSuite;

/**
* @internal
*/
final class StorageState
{
/**
* Return the path to the storage state directory.
*/
public static function dir(): string
{
return TestSuite::getInstance()->rootPath
.'/tests/Browser/StorageState';
}

/**
* Return the full path for a storage state file.
*/
public static function path(string $name): string
{
return self::dir().DIRECTORY_SEPARATOR.mb_ltrim($name, '/').'.json';
}

/**
* Save a storage state JSON string to the filesystem.
*/
public static function save(string $json, ?string $name = null): string
{
if ($name === null) {
// @phpstan-ignore-next-line
$name = str_replace('__pest_evaluable_', '', test()->name());
}

if (is_dir(self::dir()) === false) {
mkdir(self::dir(), 0755, true);
}

file_put_contents(self::path($name), $json);

return $name;
}
}
45 changes: 45 additions & 0 deletions tests/Unit/Support/StorageStateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

use Pest\Browser\Support\StorageState;

it('places storage state files under tests/Browser/StorageState', function (): void {
StorageState::save('{"cookies":[],"origins":[]}', 'auth');

expect(file_exists(StorageState::path('auth')))->toBeTrue();

@unlink(StorageState::path('auth'));
});

it('returns the name used to save the state', function (): void {
$name = StorageState::save('{"cookies":[],"origins":[]}', 'my-session');

expect($name)->toBe('my-session');

@unlink(StorageState::path('my-session'));
});

it('builds the correct file path from a name', function (): void {
$path = StorageState::path('auth');

expect($path)->toEndWith('tests/Browser/StorageState/auth.json');
});

it('strips a leading slash from the name', function (): void {
$path = StorageState::path('/auth');

expect($path)->not->toContain('//');
expect($path)->toEndWith('tests/Browser/StorageState/auth.json');
});

it('overwrites an existing state file on re-save', function (): void {
StorageState::save('{"cookies":[],"origins":[]}', 'overwrite-test');
StorageState::save('{"cookies":[{"name":"session"}],"origins":[]}', 'overwrite-test');

$contents = file_get_contents(StorageState::path('overwrite-test'));

expect($contents)->toContain('session');

@unlink(StorageState::path('overwrite-test'));
});