diff --git a/src/Api/AwaitableWebpage.php b/src/Api/AwaitableWebpage.php index 71ac96b1..6255fa35 100644 --- a/src/Api/AwaitableWebpage.php +++ b/src/Api/AwaitableWebpage.php @@ -6,6 +6,7 @@ use Pest\Browser\Exceptions\BrowserExpectationFailedException; use Pest\Browser\Execution; +use Pest\Browser\Page as BrowserPage; use Pest\Browser\Playwright\Page; use Pest\Browser\Playwright\Playwright; use Pest\Browser\ServerManager; @@ -24,7 +25,7 @@ */ public function __construct( private Page $page, - private string $initialUrl, + private string|BrowserPage $initialUrl, private array $nonAwaitableMethods = [ 'assertScreenshotMatches', 'assertNoAccessibilityIssues', diff --git a/src/Api/Concerns/InteractsWithToolbar.php b/src/Api/Concerns/InteractsWithToolbar.php index bf102d9b..32554259 100644 --- a/src/Api/Concerns/InteractsWithToolbar.php +++ b/src/Api/Concerns/InteractsWithToolbar.php @@ -5,6 +5,7 @@ namespace Pest\Browser\Api\Concerns; use Pest\Browser\Api\Webpage; +use Pest\Browser\Page as BrowserPage; use Pest\Browser\Support\ComputeUrl; /** @@ -27,8 +28,10 @@ public function refresh(): self * * @param array $options */ - public function navigate(string $url, array $options = []): self + public function navigate(string|BrowserPage $url, array $options = []): self { + $this->page->setShorthandElements($url); + $url = ComputeUrl::from($url); $this->page->goto($url, $options); diff --git a/src/Api/Concerns/MakesConsoleAssertions.php b/src/Api/Concerns/MakesConsoleAssertions.php index 0ac7a8f2..46e58ea0 100644 --- a/src/Api/Concerns/MakesConsoleAssertions.php +++ b/src/Api/Concerns/MakesConsoleAssertions.php @@ -39,7 +39,7 @@ public function assertNoBrokenImages(): Webpage expect($brokenImages)->toBeEmpty(sprintf( 'Expected no broken images on the page initially with the url [%s], but found %s: %s', - $this->initialUrl, + $this->initialUrl(), count($brokenImages), implode(', ', $brokenImages), )); @@ -64,7 +64,7 @@ public function assertNoConsoleLogs(): Webpage expect($consoleLogs)->toBeEmpty(sprintf( 'Expected no console logs on the page initially with the url [%s], but found %s: %s', - $this->initialUrl, + $this->initialUrl(), count($consoleLogs), implode(', ', array_map(fn (array $log) => $log['message'], $consoleLogs)), )); @@ -81,7 +81,7 @@ public function assertNoJavaScriptErrors(): Webpage expect($javaScriptErrors)->toBeEmpty(sprintf( 'Expected no JavaScript errors on the page initially with the url [%s], but found %s: %s', - $this->initialUrl, + $this->initialUrl(), count($javaScriptErrors), implode(', ', array_map(fn (array $log) => $log['message'], $javaScriptErrors)), )); diff --git a/src/Api/Concerns/MakesElementAssertions.php b/src/Api/Concerns/MakesElementAssertions.php index 4cf6ddb2..add6e222 100644 --- a/src/Api/Concerns/MakesElementAssertions.php +++ b/src/Api/Concerns/MakesElementAssertions.php @@ -21,7 +21,7 @@ public function assertTitle(string|int|float $title): Webpage { $title = (string) $title; - expect($this->page->title())->toBe($title, "Expected page title to be '[{$title}]' but found '[{$this->page->title()}]' on the page initially with the url [{$this->initialUrl}]."); + expect($this->page->title())->toBe($title, "Expected page title to be '[{$title}]' but found '[{$this->page->title()}]' on the page initially with the url [{$this->initialUrl()}]."); return $this; } @@ -34,7 +34,7 @@ public function assertTitleContains(string|int|float $title): Webpage $title = (string) $title; $pageTitle = $this->page->title(); - $message = "Expected page title to contain '[{$title}]' but found '[{$pageTitle}]' on the page initially with the url [{$this->initialUrl}]."; + $message = "Expected page title to contain '[{$title}]' but found '[{$pageTitle}]' on the page initially with the url [{$this->initialUrl()}]."; expect(str_contains($pageTitle, $title))->toBeTrue($message); return $this; @@ -58,7 +58,7 @@ public function assertSee(string|int|float $text): Webpage } throw new ExpectationFailedException( - "Expected to see text [{$text}] on the page initially with the url [{$this->initialUrl}], but it was not found or not visible.", + "Expected to see text [{$text}] on the page initially with the url [{$this->initialUrl()}], but it was not found or not visible.", ); } @@ -74,7 +74,7 @@ public function assertDontSee(string|int|float $text): Webpage foreach ($locator->all() as $element) { if ($element->isVisible()) { throw new ExpectationFailedException( - "Expected not to see text [{$text}] on the page initially with the url [{$this->initialUrl}], but it was found.", + "Expected not to see text [{$text}] on the page initially with the url [{$this->initialUrl()}], but it was found.", ); } } @@ -93,7 +93,7 @@ public function assertSeeIn(string $selector, string|int|float $text): Webpage $locator = $this->guessLocator($selector); - expect($locator->getByText($text)->isVisible())->toBeTrue("Expected to see text [{$text}] within element [{$selector}] on the page initially with the url [{$this->initialUrl}], but it was not found or not visible."); + expect($locator->getByText($text)->isVisible())->toBeTrue("Expected to see text [{$text}] within element [{$selector}] on the page initially with the url [{$this->initialUrl()}], but it was not found or not visible."); return $this; } @@ -107,7 +107,7 @@ public function assertDontSeeIn(string $selector, string|int|float $text): Webpa $locator = $this->guessLocator($selector); - expect($locator->getByText($text)->count())->toBe(0, "Expected not to see text [{$text}] within element [{$selector}] on the page initially with the url [{$this->initialUrl}], but it was found."); + expect($locator->getByText($text)->count())->toBe(0, "Expected not to see text [{$text}] within element [{$selector}] on the page initially with the url [{$this->initialUrl()}], but it was found."); return $this; } @@ -119,7 +119,7 @@ public function assertSeeAnythingIn(string $selector): Webpage { $text = $this->guessLocator($selector)->textContent(); - expect($text)->not->toBeEmpty("Expected element [{$selector}] to contain some text on the page initially with the url [{$this->initialUrl}], but it was empty."); + expect($text)->not->toBeEmpty("Expected element [{$selector}] to contain some text on the page initially with the url [{$this->initialUrl()}], but it was empty."); return $this; } @@ -131,7 +131,7 @@ public function assertSeeNothingIn(string $selector): Webpage { $text = $this->guessLocator($selector)->textContent(); - expect($text)->toBeEmpty("Expected element [{$selector}] to be empty on the page initially with the url [{$this->initialUrl}], but it contained text: [{$text}]."); + expect($text)->toBeEmpty("Expected element [{$selector}] to be empty on the page initially with the url [{$this->initialUrl()}], but it contained text: [{$text}]."); return $this; } @@ -142,7 +142,7 @@ public function assertSeeNothingIn(string $selector): Webpage public function assertCount(string $selector, int $expected): Webpage { $count = $this->guessLocator($selector)->count(); - expect($count)->toBe($expected, "Expected to find {$expected} elements matching [{$selector}] on the page initially with the url [{$this->initialUrl}], but found {$count}."); + expect($count)->toBe($expected, "Expected to find {$expected} elements matching [{$selector}] on the page initially with the url [{$this->initialUrl()}], but found {$count}."); return $this; } @@ -179,7 +179,7 @@ public function assertScript(string $expression, mixed $expected = true): Webpag $resultStr = gettype($result); } - expect($result)->toBe($expected, "Expected JavaScript expression [{$expression}] to evaluate to {$expectedStr} on the page initially with the url [{$this->initialUrl}], but got {$resultStr}."); + expect($result)->toBe($expected, "Expected JavaScript expression [{$expression}] to evaluate to {$expectedStr} on the page initially with the url [{$this->initialUrl()}], but got {$resultStr}."); return $this; } @@ -190,7 +190,7 @@ public function assertScript(string $expression, mixed $expected = true): Webpag public function assertSourceHas(string $code): Webpage { $content = $this->page->content(); - $message = "Expected page source to contain [{$code}] on the page initially with the url [{$this->initialUrl}], but it was not found."; + $message = "Expected page source to contain [{$code}] on the page initially with the url [{$this->initialUrl()}], but it was not found."; expect(str_contains($content, $code))->toBeTrue($message); return $this; @@ -202,7 +202,7 @@ public function assertSourceHas(string $code): Webpage public function assertSourceMissing(string $code): Webpage { $content = $this->page->content(); - $message = "Expected page source not to contain [{$code}] on the page initially with the url [{$this->initialUrl}], but it was found."; + $message = "Expected page source not to contain [{$code}] on the page initially with the url [{$this->initialUrl()}], but it was found."; expect(str_contains($content, $code))->toBeFalse($message); return $this; @@ -215,7 +215,7 @@ public function assertSeeLink(string $link): Webpage { $locator = $this->guessLocator($link); - expect($locator->isVisible())->toBeTrue("Expected to see link with text [{$link}] on the page initially with the url [{$this->initialUrl}], but it was not found or not visible."); + expect($locator->isVisible())->toBeTrue("Expected to see link with text [{$link}] on the page initially with the url [{$this->initialUrl()}], but it was not found or not visible."); return $this; } @@ -227,7 +227,7 @@ public function assertDontSeeLink(string $link): Webpage { $locator = $this->guessLocator($link); - expect($locator->count())->toBe(0, "Expected not to see link with text [{$link}] on the page initially with the url [{$this->initialUrl}], but it was found."); + expect($locator->count())->toBe(0, "Expected not to see link with text [{$link}] on the page initially with the url [{$this->initialUrl()}], but it was found."); return $this; } @@ -240,7 +240,7 @@ public function assertChecked(string $field, string|int|float|null $value = null $value = $value !== null ? (string) $value : null; $valueDescription = $value !== null ? " with value [{$value}]" : ''; - expect($this->guessLocator($field, $value)->isChecked())->toBeTrue("Expected checkbox [{$field}]{$valueDescription} to be checked on the page initially with the url [{$this->initialUrl}], but it was not."); + expect($this->guessLocator($field, $value)->isChecked())->toBeTrue("Expected checkbox [{$field}]{$valueDescription} to be checked on the page initially with the url [{$this->initialUrl()}], but it was not."); return $this; } @@ -253,7 +253,7 @@ public function assertNotChecked(string $field, string|int|float|null $value = n $value = $value !== null ? (string) $value : null; $valueDescription = $value !== null ? " with value [{$value}]" : ''; - expect($this->guessLocator($field, $value)->isChecked())->toBeFalse("Expected checkbox [{$field}]{$valueDescription} not to be checked on the page initially with the url [{$this->initialUrl}], but it was."); + expect($this->guessLocator($field, $value)->isChecked())->toBeFalse("Expected checkbox [{$field}]{$valueDescription} not to be checked on the page initially with the url [{$this->initialUrl()}], but it was."); return $this; } @@ -278,7 +278,7 @@ public function assertIndeterminate(string $field, string|int|float|null $value "); $valueDescription = $value !== null ? " with value [{$value}]" : ''; - expect($isIndeterminate)->toBeTrue("Expected checkbox [{$field}]{$valueDescription} to be in indeterminate state on the page initially with the url [{$this->initialUrl}], but it was not."); + expect($isIndeterminate)->toBeTrue("Expected checkbox [{$field}]{$valueDescription} to be in indeterminate state on the page initially with the url [{$this->initialUrl()}], but it was not."); return $this; } @@ -290,7 +290,7 @@ public function assertRadioSelected(string $field, string|int|float $value): Web { $value = (string) $value; - expect($this->guessLocator($field, $value)->isChecked())->toBeTrue("Expected radio button [{$field}] with value [{$value}] to be selected on the page initially with the url [{$this->initialUrl}], but it was not."); + expect($this->guessLocator($field, $value)->isChecked())->toBeTrue("Expected radio button [{$field}] with value [{$value}] to be selected on the page initially with the url [{$this->initialUrl()}], but it was not."); return $this; } @@ -303,7 +303,7 @@ public function assertRadioNotSelected(string $field, string|int|float|null $val $value = $value !== null ? (string) $value : null; if ($value !== null) { - expect($this->guessLocator($field, $value)->isChecked())->toBeFalse("Expected radio button [{$field}] with value [{$value}] not to be selected on the page initially with the url [{$this->initialUrl}], but it was."); + expect($this->guessLocator($field, $value)->isChecked())->toBeFalse("Expected radio button [{$field}] with value [{$value}] not to be selected on the page initially with the url [{$this->initialUrl()}], but it was."); return $this; } @@ -321,7 +321,7 @@ public function assertRadioNotSelected(string $field, string|int|float|null $val } } - expect($anyChecked)->toBeFalse("Expected no radio buttons in group [{$field}] to be selected on the page initially with the url [{$this->initialUrl}], but at least one was selected."); + expect($anyChecked)->toBeFalse("Expected no radio buttons in group [{$field}] to be selected on the page initially with the url [{$this->initialUrl()}], but at least one was selected."); return $this; } @@ -336,7 +336,7 @@ public function assertSelected(string $field, string|int|float $value): Webpage $locator = $this->guessLocator($field); $actual = $locator->inputValue(); - expect($actual)->toBe($value, "Expected dropdown [{$field}] to have value [{$value}] selected on the page initially with the url [{$this->initialUrl}], but found [{$actual}]."); + expect($actual)->toBe($value, "Expected dropdown [{$field}] to have value [{$value}] selected on the page initially with the url [{$this->initialUrl()}], but found [{$actual}]."); return $this; } @@ -351,7 +351,7 @@ public function assertNotSelected(string $field, string|int|float $value): Webpa $locator = $this->guessLocator($field); $actual = $locator->inputValue(); - expect($actual)->not->toBe($value, "Expected dropdown [{$field}] not to have value [{$value}] selected on the page initially with the url [{$this->initialUrl}], but it was."); + expect($actual)->not->toBe($value, "Expected dropdown [{$field}] not to have value [{$value}] selected on the page initially with the url [{$this->initialUrl()}], but it was."); return $this; } @@ -365,7 +365,7 @@ public function assertValue(string $field, string|int|float $value): Webpage $locator = $this->guessLocator($field); - expect($actual = $locator->inputValue())->toBe($value, "Expected element [{$field}] to have value [{$value}] on the page initially with the url [{$this->initialUrl}], but found [{$actual}]."); + expect($actual = $locator->inputValue())->toBe($value, "Expected element [{$field}] to have value [{$value}] on the page initially with the url [{$this->initialUrl()}], but found [{$actual}]."); return $this; } @@ -378,7 +378,7 @@ public function assertValueIsNot(string $selector, string|int|float $value): Web $value = (string) $value; $actual = $this->guessLocator($selector)->inputValue(); - expect($actual)->not->toBe($value, "Expected element [{$selector}] not to have value [{$value}] on the page initially with the url [{$this->initialUrl}], but it did."); + expect($actual)->not->toBe($value, "Expected element [{$selector}] not to have value [{$value}] on the page initially with the url [{$this->initialUrl()}], but it did."); return $this; } @@ -391,7 +391,7 @@ public function assertAttribute(string $selector, string $attribute, string|int| $value = (string) $value; $actual = $this->guessLocator($selector)->getAttribute($attribute); - expect($actual)->toBe($value, "Expected element [{$selector}] to have attribute [{$attribute}] with value [{$value}] on the page initially with the url [{$this->initialUrl}], but found [{$actual}]."); + expect($actual)->toBe($value, "Expected element [{$selector}] to have attribute [{$attribute}] with value [{$value}] on the page initially with the url [{$this->initialUrl()}], but found [{$actual}]."); return $this; } @@ -402,7 +402,7 @@ public function assertAttribute(string $selector, string $attribute, string|int| public function assertAttributeMissing(string $selector, string $attribute): Webpage { $actual = $this->guessLocator($selector)->getAttribute($attribute); - expect($actual)->toBeNull("Expected element [{$selector}] not to have attribute [{$attribute}] on the page initially with the url [{$this->initialUrl}], but it had value [{$actual}]."); + expect($actual)->toBeNull("Expected element [{$selector}] not to have attribute [{$attribute}] on the page initially with the url [{$this->initialUrl()}], but it had value [{$actual}]."); return $this; } @@ -416,9 +416,9 @@ public function assertAttributeContains(string $selector, string $attribute, str $attributeValue = $this->guessLocator($selector)->getAttribute($attribute); - expect($attributeValue)->not->toBeNull("Expected element [{$selector}] to have attribute [{$attribute}] on the page initially with the url [{$this->initialUrl}], but it was not found."); + expect($attributeValue)->not->toBeNull("Expected element [{$selector}] to have attribute [{$attribute}] on the page initially with the url [{$this->initialUrl()}], but it was not found."); - $message = "Expected attribute [{$attribute}] of element [{$selector}] to contain [{$value}] on the page initially with the url [{$this->initialUrl}], but found [{$attributeValue}]."; + $message = "Expected attribute [{$attribute}] of element [{$selector}] to contain [{$value}] on the page initially with the url [{$this->initialUrl()}], but found [{$attributeValue}]."; expect(str_contains((string) $attributeValue, $value))->toBeTrue($message); return $this; @@ -437,7 +437,7 @@ public function assertAttributeDoesntContain(string $selector, string $attribute return $this; } - $message = "Expected attribute [{$attribute}] of element [{$selector}] not to contain [{$value}] on the page initially with the url [{$this->initialUrl}], but found [{$attributeValue}]."; + $message = "Expected attribute [{$attribute}] of element [{$selector}] not to contain [{$value}] on the page initially with the url [{$this->initialUrl()}], but found [{$attributeValue}]."; expect(str_contains($attributeValue, $value))->toBeFalse($message); return $this; @@ -470,7 +470,7 @@ public function assertVisible(string $selector): Webpage { $locator = $this->guessLocator($selector); - expect($locator->isVisible())->toBeTrue("Expected element [{$selector}] to be visible on the page initially with the url [{$this->initialUrl}], but it was not."); + expect($locator->isVisible())->toBeTrue("Expected element [{$selector}] to be visible on the page initially with the url [{$this->initialUrl()}], but it was not."); return $this; } @@ -481,7 +481,7 @@ public function assertVisible(string $selector): Webpage public function assertPresent(string $selector): Webpage { $count = $this->guessLocator($selector)->count(); - expect($count)->toBeGreaterThan(0, "Expected element [{$selector}] to be present in the DOM on the page initially with the url [{$this->initialUrl}], but it was not found."); + expect($count)->toBeGreaterThan(0, "Expected element [{$selector}] to be present in the DOM on the page initially with the url [{$this->initialUrl()}], but it was not found."); return $this; } @@ -492,7 +492,7 @@ public function assertPresent(string $selector): Webpage public function assertNotPresent(string $selector): Webpage { $count = $this->guessLocator($selector)->count(); - expect($count)->toBe(0, "Expected element [{$selector}] not to be present in the DOM on the page initially with the url [{$this->initialUrl}], but it was found."); + expect($count)->toBe(0, "Expected element [{$selector}] not to be present in the DOM on the page initially with the url [{$this->initialUrl()}], but it was found."); return $this; } @@ -504,7 +504,7 @@ public function assertMissing(string $selector): Webpage { $locator = $this->guessLocator($selector); - expect($locator->isVisible())->toBeFalse("Expected element [{$selector}] not to be visible on the page initially with the url [{$this->initialUrl}], but it was."); + expect($locator->isVisible())->toBeFalse("Expected element [{$selector}] not to be visible on the page initially with the url [{$this->initialUrl()}], but it was."); return $this; } @@ -514,7 +514,7 @@ public function assertMissing(string $selector): Webpage */ public function assertEnabled(string $field): Webpage { - expect($this->guessLocator($field)->isEnabled())->toBeTrue("Expected field [{$field}] to be enabled on the page initially with the url [{$this->initialUrl}], but it was disabled."); + expect($this->guessLocator($field)->isEnabled())->toBeTrue("Expected field [{$field}] to be enabled on the page initially with the url [{$this->initialUrl()}], but it was disabled."); return $this; } @@ -524,7 +524,7 @@ public function assertEnabled(string $field): Webpage */ public function assertDisabled(string $field): Webpage { - expect($this->guessLocator($field)->isDisabled())->toBeTrue("Expected field [{$field}] to be disabled on the page initially with the url [{$this->initialUrl}], but it was enabled."); + expect($this->guessLocator($field)->isDisabled())->toBeTrue("Expected field [{$field}] to be disabled on the page initially with the url [{$this->initialUrl()}], but it was enabled."); return $this; } @@ -536,7 +536,7 @@ public function assertButtonEnabled(string $button): Webpage { $selector = $this->guessLocator($button); - expect($selector->isEnabled())->toBeTrue("Expected button [{$button}] to be enabled on the page initially with the url [{$this->initialUrl}], but it was disabled."); + expect($selector->isEnabled())->toBeTrue("Expected button [{$button}] to be enabled on the page initially with the url [{$this->initialUrl()}], but it was disabled."); return $this; } @@ -548,7 +548,7 @@ public function assertButtonDisabled(string $button): Webpage { $selector = $this->guessLocator($button); - expect($selector->isDisabled())->toBeTrue("Expected button [{$button}] to be disabled on the page initially with the url [{$this->initialUrl}], but it was enabled."); + expect($selector->isDisabled())->toBeTrue("Expected button [{$button}] to be disabled on the page initially with the url [{$this->initialUrl()}], but it was enabled."); return $this; } diff --git a/src/Api/From.php b/src/Api/From.php index 792c1139..185e9099 100644 --- a/src/Api/From.php +++ b/src/Api/From.php @@ -8,6 +8,7 @@ use Pest\Browser\Enums\BrowserType; use Pest\Browser\Enums\Cities; use Pest\Browser\Enums\Device; +use Pest\Browser\Page as BrowserPage; /** * @mixin PendingAwaitablePage @@ -22,7 +23,7 @@ public function __construct( private BrowserType $browserType, private Device $device, - private string $url, + private string|BrowserPage $url, private array $options, ) { // diff --git a/src/Api/On.php b/src/Api/On.php index baec658f..d8958513 100644 --- a/src/Api/On.php +++ b/src/Api/On.php @@ -6,6 +6,7 @@ use Pest\Browser\Enums\BrowserType; use Pest\Browser\Enums\Device; +use Pest\Browser\Page as BrowserPage; /** * @mixin PendingAwaitablePage @@ -20,7 +21,7 @@ public function __construct( private BrowserType $browserType, private Device $device, - private string $url, + private string|BrowserPage $url, private array $options, ) { // diff --git a/src/Api/PendingAwaitablePage.php b/src/Api/PendingAwaitablePage.php index f6f09806..88ca79c1 100644 --- a/src/Api/PendingAwaitablePage.php +++ b/src/Api/PendingAwaitablePage.php @@ -9,6 +9,7 @@ use Pest\Browser\Enums\Cities; use Pest\Browser\Enums\ColorScheme; use Pest\Browser\Enums\Device; +use Pest\Browser\Page as BrowserPage; use Pest\Browser\Playwright\InitScript; use Pest\Browser\Playwright\Playwright; use Pest\Browser\Support\ComputeUrl; @@ -31,7 +32,7 @@ final class PendingAwaitablePage public function __construct( private readonly BrowserType $browserType, private readonly Device $device, - private readonly string $url, + private readonly string|BrowserPage $url, private readonly array $options, ) { // @@ -177,8 +178,8 @@ private function createAwaitablePage(): AwaitableWebpage $url = ComputeUrl::from($this->url); return new AwaitableWebpage( - $context->newPage()->goto($url, $this->options), - $url, + $context->newPage()->setShorthandElements($this->url)->goto($url, $this->options), + $this->url, ); } } diff --git a/src/Api/Webpage.php b/src/Api/Webpage.php index 37271685..7d9bff81 100644 --- a/src/Api/Webpage.php +++ b/src/Api/Webpage.php @@ -4,7 +4,9 @@ namespace Pest\Browser\Api; +use BadMethodCallException; use Pest\Browser\Execution; +use Pest\Browser\Page as BrowserPage; use Pest\Browser\Playwright\Locator; use Pest\Browser\Playwright\Page; use Pest\Browser\Support\GuessLocator; @@ -31,11 +33,32 @@ final class Webpage */ public function __construct( private readonly Page $page, - private readonly string $initialUrl, + private readonly string|BrowserPage $initialUrl, ) { // } + /** + * Dynamically call a method on the browser. + * + * @param array $arguments + * + * @throws BadMethodCallException + */ + public function __call(string $method, array $arguments): self + { + if ($this->initialUrl instanceof BrowserPage && method_exists($this->initialUrl, $method)) { + array_unshift($arguments, $this); + + // @phpstan-ignore-next-line method.dynamicName + $this->initialUrl->{$method}(...$arguments); + + return $this; + } + + throw new BadMethodCallException("Call to undefined method [{$method}]."); + } + /** * Dumps the current page's content and stops the execution. */ @@ -72,6 +95,18 @@ public function url(): string return $this->page->url(); } + /** + * Gets the page's initial URL. + */ + public function initialUrl(): string + { + if ($this->initialUrl instanceof BrowserPage) { + return $this->initialUrl->url(); + } + + return $this->initialUrl; + } + /** * Submits the first form found on the page. */ @@ -100,6 +135,8 @@ public function value(string $selector): string public function within(string $selector, callable $callback): self { + $selector = $this->resolveShorthandSelector($selector); + $previousScope = $this->currentScope; $this->currentScope = $previousScope !== null ? $previousScope.' >> '.$selector : $selector; @@ -118,6 +155,20 @@ public function within(string $selector, callable $callback): self */ private function guessLocator(string $selector, ?string $value = null): Locator { + $selector = $this->resolveShorthandSelector($selector); + return (new GuessLocator($this->page, $this->currentScope))->for($selector, $value); } + + /** + * Resolve the shorthand selector for the given page. + */ + private function resolveShorthandSelector(string $selector): string + { + $shorthandElements = $this->page->shorthandElements(); + + return str_replace( + array_keys($shorthandElements), array_values($shorthandElements), $selector + ); + } } diff --git a/src/Browsable.php b/src/Browsable.php index 5ed8baed..e63fd641 100644 --- a/src/Browsable.php +++ b/src/Browsable.php @@ -34,14 +34,29 @@ public function __markAsBrowserTest(): void /** * Browse to the given URL. * - * @template TUrl of array|string + * @template TUrl of array|string|Page * * @param TUrl $url * @param array $options - * @return (TUrl is array ? ArrayablePendingAwaitablePage : PendingAwaitablePage) + * @return (TUrl is array ? ArrayablePendingAwaitablePage : PendingAwaitablePage) */ - public function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage + public function visit(array|string|Page $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage { + if ($url instanceof Page) { + $options = [ + ...$this->pageTimezone($url), + ...$this->pageLocale($url), + ...$options, + ]; + + return new PendingAwaitablePage( + $url->browserType(), + $url->device(), + $url, + $options, + ); + } + if (is_string($url)) { return new PendingAwaitablePage( Playwright::defaultBrowserType(), @@ -52,12 +67,39 @@ public function visit(array|string $url, array $options = []): ArrayablePendingA } return new ArrayablePendingAwaitablePage( - array_map(fn (string $singleUrl): PendingAwaitablePage => new PendingAwaitablePage( - Playwright::defaultBrowserType(), - Device::DESKTOP, - $singleUrl, - $options, - ), $url), + array_map(fn (string|Page $singleUrl): PendingAwaitablePage => $this->visit($singleUrl, $options), $url), ); } + + /** + * Get the locale from page. + * + * @return array{locale?: string} + */ + private function pageLocale(Page $page): array + { + $locale = $page->locale(); + + if ($locale === '') { + return []; + } + + return ['locale' => $locale]; + } + + /** + * Get the timezone from page. + * + * @return array{timezoneId?: string} + */ + private function pageTimezone(Page $page): array + { + $timezone = $page->timezone(); + + if ($timezone === '') { + return []; + } + + return ['timezoneId' => $timezone]; + } } diff --git a/src/Page.php b/src/Page.php new file mode 100644 index 00000000..ee05c123 --- /dev/null +++ b/src/Page.php @@ -0,0 +1,15 @@ + + */ + public static function siteElements(): array + { + return []; + } + + /** + * Get the timezone for the page. + */ + public function timezone(): string + { + return ''; + } + + /** + * Get the locale for the page. + */ + public function locale(): string + { + return ''; + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements(): array + { + return []; + } + + /** + * Get the device for the page. + */ + public function device(): Device + { + return Device::DESKTOP; + } + + /** + * Get the browser type for the page. + */ + public function browserType(): BrowserType + { + return Playwright::defaultBrowserType(); + } +} diff --git a/src/Playwright/Page.php b/src/Playwright/Page.php index d97f3db3..7fd9d9d7 100644 --- a/src/Playwright/Page.php +++ b/src/Playwright/Page.php @@ -6,6 +6,7 @@ use Generator; use Pest\Browser\Execution; +use Pest\Browser\Page as BrowserPage; use Pest\Browser\Support\ImageDiffView; use Pest\Browser\Support\JavaScriptSerializer; use Pest\Browser\Support\Screenshot; @@ -22,6 +23,13 @@ final class Page { use Concerns\InteractsWithPlaywright; + /** + * Set the elements the resolver should use as shortcuts. + * + * @var array + */ + private array $shorthandElements = []; + /** * Whether the page has been closed. */ @@ -591,6 +599,33 @@ public function isClosed(): bool return $this->closed; } + /** + * Set the page elements the resolver should use as shortcuts. + */ + public function setShorthandElements(string|BrowserPage $url): self + { + if ($url instanceof BrowserPage) { + $elements = array_merge($url::siteElements(), $url->elements()); + + /** @var array $sortedElements */ + $sortedElements = collect($elements)->sortByDesc(fn (string $element, string $key): int => mb_strlen($key))->toArray(); + + $this->shorthandElements = $sortedElements; + } + + return $this; + } + + /** + * Get the shorthand elements of page. + * + * @return array + */ + public function shorthandElements(): array + { + return $this->shorthandElements; + } + /** * Screenshots the page and returns the binary data. */ diff --git a/src/Support/ComputeUrl.php b/src/Support/ComputeUrl.php index 5364e0b0..b58394e1 100644 --- a/src/Support/ComputeUrl.php +++ b/src/Support/ComputeUrl.php @@ -4,6 +4,7 @@ namespace Pest\Browser\Support; +use Pest\Browser\Page as BrowserPage; use Pest\Browser\ServerManager; /** @@ -14,8 +15,12 @@ /** * Computes the URL based on the given string. */ - public static function from(string $url): string + public static function from(string|BrowserPage $url): string { + if ($url instanceof BrowserPage) { + $url = $url->url(); + } + return match (true) { str_starts_with($url, 'http://') || str_starts_with($url, 'https://') => $url, ! str_starts_with($url, '/') => 'https://'.$url, diff --git a/tests/Browser/Visit/Page/ChainMethods.php b/tests/Browser/Visit/Page/ChainMethods.php new file mode 100644 index 00000000..4280b650 --- /dev/null +++ b/tests/Browser/Visit/Page/ChainMethods.php @@ -0,0 +1,42 @@ + ' + + + + '); + + $page = visit(new ChainedPage); + + $page->enterEmail('nuno@pest.com') + ->assertSeeIn('#header', 'Pest') + ->descriptionDisabled(); +}); + +final class ChainedPage extends Page +{ + public function url(): string + { + return '/'; + } + + public function enterEmail(Webpage $page, string $email): self + { + $page->type('email', $email); + + return $this; + } + + public function descriptionDisabled(Webpage $page): self + { + $page->assertDisabled('description'); + + return $this; + } +} diff --git a/tests/Browser/Visit/Page/MultiplePages.php b/tests/Browser/Visit/Page/MultiplePages.php new file mode 100644 index 00000000..de6083f7 --- /dev/null +++ b/tests/Browser/Visit/Page/MultiplePages.php @@ -0,0 +1,59 @@ + '
+

Page 1

+
'); + + Route::get('/page2', fn (): string => '
+

Page 2

+
'); + + Route::get('/page3', fn (): string => '
+

Page 3

+
'); + + $pages = visit(['/page1', new Page2, new Page3]); + + $pages->assertSee('Page'); +}); + +it('may array destructure multiple URLs and object pages', function (): void { + Route::get('/page1', fn (): string => '
+

Page 1

+
'); + + Route::get('/page2', fn (): string => '
+

Page 2

+
'); + + Route::get('/page3', fn (): string => '
+

Page 3

+
'); + + [$page1, $page2, $page3] = visit(['/page1', new Page2, new Page3]); + + $page1->assertSee('Page 1'); + $page2->assertSee('Page 2'); + $page3->assertSee('Page 3'); +}); + +final class Page2 extends Page +{ + public function url(): string + { + return '/page2'; + } +} + +final class Page3 extends Page +{ + public function url(): string + { + return '/page3'; + } +} diff --git a/tests/Browser/Visit/Page/ShorthandElements.php b/tests/Browser/Visit/Page/ShorthandElements.php new file mode 100644 index 00000000..c6db2c98 --- /dev/null +++ b/tests/Browser/Visit/Page/ShorthandElements.php @@ -0,0 +1,80 @@ + ' +
    +
  • Page 1
  • +
+ + + + '); + Route::get('/page-2', fn (): string => ' +
    +
  • Page 2
  • +
+ '); + + $page = visit(new ShorthandPage1); + + $page->assertSeeIn('@breadcrumb', 'Page 1') + ->type('@name-input', 'nuno maduro') + ->type('@email-input', 'nuno@pest.com') + ->assertDisabled('@description-textarea'); + + expect($page->value('#full-name'))->toBe('nuno maduro'); + expect($page->value('#email'))->toBe('nuno@pest.com'); + + $page->navigate(new ShorthandPage2); + $page->assertSeeIn('@breadcrumb', 'Page 2'); +}); + +it('may fail when asserting wrong shorthand selector using object page', function (): void { + Route::get('/page-1', fn (): string => '')->name('page'); + + $page = visit(new ShorthandPage1); + + $page->assertSeeIn('@wrong-shorthand-selector', 'Pest'); +})->throws(ExpectationFailedException::class); + +abstract class ShorthandBasePage extends Page +{ + #[Override] + final public static function siteElements(): array + { + return [ + '@breadcrumb' => '.custom-breadcrumb', + ]; + } +} + +final class ShorthandPage1 extends ShorthandBasePage +{ + public function url(): string + { + return '/page-1'; + } + + #[Override] + public function elements(): array + { + return [ + '@name-input' => 'fullname', + '@email-input' => '#email', + '@description-textarea' => 'description', + ]; + } +} + +final class ShorthandPage2 extends ShorthandBasePage +{ + public function url(): string + { + return '/page-2'; + } +} diff --git a/tests/Browser/Visit/Page/SinglePage.php b/tests/Browser/Visit/Page/SinglePage.php new file mode 100644 index 00000000..8506f73f --- /dev/null +++ b/tests/Browser/Visit/Page/SinglePage.php @@ -0,0 +1,59 @@ + ' + + + +

Locale/Timezone Test

+

Locale and timezone are set in browser context only.

+ + + '); + + $page = visit(new HomePage); + + $locale = $page->script('navigator.language'); + expect($locale)->toBe('fr-FR'); + + $timezone = $page->script('Intl.DateTimeFormat().resolvedOptions().timeZone'); + expect($timezone)->toBe('Europe/Paris'); +}); + +it('may visit external URLs with object page', function (): void { + $page = visit(new ExternalPage); + + $page->assertSee('Example Domain'); +}); + +final class ExternalPage extends Page +{ + public function url(): string + { + return 'https://example.com'; + } +} + +final class HomePage extends Page +{ + public function url(): string + { + return '/'; + } + + #[Override] + public function timezone(): string + { + return 'Europe/Paris'; + } + + #[Override] + public function locale(): string + { + return 'fr-FR'; + } +} diff --git a/tests/Browser/Webpage/Page/NavigateTest.php b/tests/Browser/Webpage/Page/NavigateTest.php new file mode 100644 index 00000000..4eeeb074 --- /dev/null +++ b/tests/Browser/Webpage/Page/NavigateTest.php @@ -0,0 +1,24 @@ + 'page 1'); + Route::get('/page-b', fn (): string => 'page 2'); + + $page = visit('/page-a'); + $page->assertSee('page 1'); + + $page->navigate(new PageB); + $page->assertSee('page 2'); +}); + +final class PageB extends Page +{ + public function url(): string + { + return '/page-b'; + } +} diff --git a/tests/Browser/Webpage/Page/WithinTest.php b/tests/Browser/Webpage/Page/WithinTest.php new file mode 100644 index 00000000..a15a5c2e --- /dev/null +++ b/tests/Browser/Webpage/Page/WithinTest.php @@ -0,0 +1,95 @@ + ' +
+ + + + + + + +
+ '); + + $page = visit(new ScopedPage); + + $page->within('@form', function ($page): void { + $page->type('@email-field', 'user@example.com') + ->type('@password-field', 'secret') + ->select('@role-field', 'admin') + ->check('@remember-field') + ->radio('@theme-field', 'dark') + ->press('Login'); + }); + + expect($page->value('#login-form input[name="email"]'))->toBe('user@example.com'); + expect($page->value('#login-form input[name="password"]'))->toBe('secret'); + expect($page->value('#login-form select[name="role"]'))->toBe('admin'); +}); + +it('works with nested scopes from object page', function (): void { + Route::get('/', fn (): string => ' +
+
+ +

Nested Text

+
+
+ '); + + $page = visit(new NestedScopesPage); + + $page->within('@outer-layer', function ($page): void { + $page->within('@inner-layer', function ($innerBrowser): void { + $innerBrowser->assertSee('Nested Text'); + $innerBrowser->click('@inner-btn')->assertSee('Inner button clicked'); + }); + }); +}); + +final class NestedScopesPage extends Page +{ + public function url(): string + { + return '/'; + } + + public function elements(): array + { + return [ + '@outer-layer' => '#outer', + '@inner-layer' => '.inner', + '@inner-btn' => '#inner-button', + ]; + } +} + +final class ScopedPage extends Page +{ + public function url(): string + { + return '/'; + } + + public function elements(): array + { + return [ + '@form' => '#login-form', + '@email-field' => 'email', + '@password-field' => 'password', + '@role-field' => 'role', + '@remember-field' => 'remember', + '@theme-field' => 'theme', + '@submit-btn' => '#submit-button', + ]; + } +}