From bea3b4d2c94754e12b0ada3c57a17806170d9a73 Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Wed, 10 Sep 2025 12:59:06 -0300 Subject: [PATCH 1/8] Brings `within` tests back --- tests/Browser/Webpage/WithinTest.php | 187 +++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 tests/Browser/Webpage/WithinTest.php diff --git a/tests/Browser/Webpage/WithinTest.php b/tests/Browser/Webpage/WithinTest.php new file mode 100644 index 00000000..8f821bf0 --- /dev/null +++ b/tests/Browser/Webpage/WithinTest.php @@ -0,0 +1,187 @@ + ' + +
+ + Content Link +
+ '); + + Route::get('/sidebar', fn (): string => 'Sidebar Page'); + Route::get('/content', fn (): string => 'Content Page'); + + $page = visit('/'); + + $page->within('#sidebar', function ($page): void { + $page->click('Sidebar Button'); + }) + ->within('#sidebar', function ($page): void { + $page->click('Sidebar Link'); + }) + ->within('#content', function ($page): void { + $page->assertDontSee('Sidebar Link'); + }) + ->assertUrlIs(url('/sidebar')) + ->assertSee('Sidebar Page'); +}); + +it('can type in input within a scoped selector', function (): void { + Route::get('/', fn (): string => ' +
+ + +
+
+ + +
+ '); + + $page = visit('/'); + + $page->within('#form1', function ($page): void { + $page->type('username', 'john_doe'); + }); + + $page->within('#form2', function ($page): void { + $page->type('username', 'john@example.com'); + }); + + expect($page->value('#form1 input[name="username"]'))->toBe('john_doe'); + expect($page->value('#form2 input[name="username"]'))->toBe('john@example.com'); +}); + +it('can assert text within a scoped selector', function (): void { + Route::get('/', fn (): string => ' + + + '); + + $page = visit('/'); + + $page->within('#header', function ($page): void { + $page->assertSee('Welcome'); + $page->assertSee('Header content'); + $page->assertDontSee('Footer content'); + }); + + $page->within('#footer', function ($page): void { + $page->assertSee('Contact'); + $page->assertSee('Footer content'); + $page->assertDontSee('Header content'); + }); +}); + +it('can use css selectors within scope', function (): void { + Route::get('/', fn (): string => ' +
+
+ +
+
+ +
+
+
+ +
+ '); + + $page = visit('/'); + + $page->within('.container', function ($page): void { + $page->click('.item:first-child .btn'); + }); + + $page->within('.container .item:first-child', function ($page): void { + $page->assertSee('Clicked'); + }); +}); + +it('works with data-test selectors within scope', function (): void { + Route::get('/', fn (): string => ' +
+ +
+
+ +
+ '); + + $page = visit('/'); + + $page->within('[data-testid="sidebar"]', function ($page): void { + $page->click('@action-btn')->assertSee('Sidebar Clicked'); + }); + $page->within('[data-testid="content"]', function ($page): void { + $page->assertSee('Content Action')->assertDontSee('Content Clicked'); + $page->click('@action-btn')->assertSee('Content Clicked'); + }); +}); + +it('works with nested scopes', function (): void { + Route::get('/', fn (): string => ' +
+
+ +

Nested Text

+
+
+ '); + + $page = visit('/'); + + $page->within('#outer', function ($page): void { + $page->within('.inner', function ($innerBrowser): void { + $innerBrowser->assertSee('Nested Text'); + $innerBrowser->click('#inner-button')->assertSee('Inner button clicked'); + }); + }); +}); + +it('handles form interactions within scope', function (): void { + Route::get('/', fn (): string => ' +
+ + + + + + + +
+ '); + + $page = visit('/'); + + $page->within('#login-form', function ($page): void { + $page->type('email', 'user@example.com') + ->type('password', 'secret') + ->select('role', 'admin') + ->check('remember') + ->radio('theme', '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'); +}); From 5217553466ea831d7740d67554f87662466c99bd Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Wed, 10 Sep 2025 12:59:58 -0300 Subject: [PATCH 2/8] Chane return type from `Webpage` to `self` --- src/Api/Concerns/InteractsWithElements.php | 2 +- src/Api/Concerns/MakesConsoleAssertions.php | 12 ++-- src/Api/Concerns/MakesElementAssertions.php | 76 ++++++++++----------- src/Api/Concerns/MakesUrlAssertions.php | 36 +++++----- 4 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/Api/Concerns/InteractsWithElements.php b/src/Api/Concerns/InteractsWithElements.php index 24566e7e..b77d7df0 100644 --- a/src/Api/Concerns/InteractsWithElements.php +++ b/src/Api/Concerns/InteractsWithElements.php @@ -100,7 +100,7 @@ public function hover(string $selector): self /** * Right-click the element matching the given selector. */ - public function rightClick(string $text): Webpage + public function rightClick(string $text): self { $this->guessLocator($text)->click([ 'button' => 'right', diff --git a/src/Api/Concerns/MakesConsoleAssertions.php b/src/Api/Concerns/MakesConsoleAssertions.php index 0ac7a8f2..36cad259 100644 --- a/src/Api/Concerns/MakesConsoleAssertions.php +++ b/src/Api/Concerns/MakesConsoleAssertions.php @@ -20,7 +20,7 @@ trait MakesConsoleAssertions /** * Asserts there are no console logs or JavaScript errors on the page. */ - public function assertNoSmoke(): Webpage + public function assertNoSmoke(): self { $this->assertNoConsoleLogs(); $this->assertNoJavaScriptErrors(); @@ -31,7 +31,7 @@ public function assertNoSmoke(): Webpage /** * Asserts there are no broken images on the page. */ - public function assertNoBrokenImages(): Webpage + public function assertNoBrokenImages(): self { $this->page->waitForLoadState('load'); @@ -50,7 +50,7 @@ public function assertNoBrokenImages(): Webpage /** * Asserts there are no missing images on the page. */ - public function assertNoMissingImages(): Webpage + public function assertNoMissingImages(): self { return $this->assertNoBrokenImages(); } @@ -58,7 +58,7 @@ public function assertNoMissingImages(): Webpage /** * Asserts there are no console logs on the page. */ - public function assertNoConsoleLogs(): Webpage + public function assertNoConsoleLogs(): self { $consoleLogs = $this->page->consoleLogs(); @@ -75,7 +75,7 @@ public function assertNoConsoleLogs(): Webpage /** * Asserts there are no JavaScript errors on the page. */ - public function assertNoJavaScriptErrors(): Webpage + public function assertNoJavaScriptErrors(): self { $javaScriptErrors = $this->page->javaScriptErrors(); @@ -92,7 +92,7 @@ public function assertNoJavaScriptErrors(): Webpage /** * Asserts the accessibility of the page. */ - public function assertNoAccessibilityIssues(int $level = 1): Webpage + public function assertNoAccessibilityIssues(int $level = 1): self { $this->page->waitForLoadState('networkidle'); $this->page->waitForFunction('document.readyState === "complete"'); diff --git a/src/Api/Concerns/MakesElementAssertions.php b/src/Api/Concerns/MakesElementAssertions.php index 0270add7..21ffec3c 100644 --- a/src/Api/Concerns/MakesElementAssertions.php +++ b/src/Api/Concerns/MakesElementAssertions.php @@ -16,7 +16,7 @@ trait MakesElementAssertions /** * Assert that the page title matches the given text. */ - public function assertTitle(string|int|float $title): Webpage + public function assertTitle(string|int|float $title): self { $title = (string) $title; @@ -28,7 +28,7 @@ public function assertTitle(string|int|float $title): Webpage /** * Assert that the page title contains the given text. */ - public function assertTitleContains(string|int|float $title): Webpage + public function assertTitleContains(string|int|float $title): self { $title = (string) $title; @@ -42,7 +42,7 @@ public function assertTitleContains(string|int|float $title): Webpage /** * Assert that the given text is present on the page. */ - public function assertSee(string|int|float $text): Webpage + public function assertSee(string|int|float $text): self { $text = (string) $text; @@ -66,7 +66,7 @@ public function assertSee(string|int|float $text): Webpage /** * Assert that the given text is not present on the page. */ - public function assertDontSee(string|int|float $text): Webpage + public function assertDontSee(string|int|float $text): self { $text = (string) $text; @@ -90,7 +90,7 @@ public function assertDontSee(string|int|float $text): Webpage /** * Assert that the given text is present within the selector. */ - public function assertSeeIn(string $selector, string|int|float $text): Webpage + public function assertSeeIn(string $selector, string|int|float $text): self { $text = (string) $text; @@ -104,7 +104,7 @@ public function assertSeeIn(string $selector, string|int|float $text): Webpage /** * Assert that the given text is not present within the selector. */ - public function assertDontSeeIn(string $selector, string|int|float $text): Webpage + public function assertDontSeeIn(string $selector, string|int|float $text): self { $text = (string) $text; @@ -118,7 +118,7 @@ public function assertDontSeeIn(string $selector, string|int|float $text): Webpa /** * Assert that any text is present within the selector. */ - public function assertSeeAnythingIn(string $selector): Webpage + public function assertSeeAnythingIn(string $selector): self { $text = $this->guessLocator($selector)->textContent(); @@ -130,7 +130,7 @@ public function assertSeeAnythingIn(string $selector): Webpage /** * Assert that no text is present within the selector. */ - public function assertSeeNothingIn(string $selector): Webpage + public function assertSeeNothingIn(string $selector): self { $text = $this->guessLocator($selector)->textContent(); @@ -142,7 +142,7 @@ public function assertSeeNothingIn(string $selector): Webpage /** * Assert that a given element is present a given amount of times. */ - public function assertCount(string $selector, int $expected): Webpage + public function assertCount(string $selector, int $expected): self { $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}."); @@ -153,7 +153,7 @@ public function assertCount(string $selector, int $expected): Webpage /** * Assert that the given JavaScript expression evaluates to the given value. */ - public function assertScript(string $expression, mixed $expected = true): Webpage + public function assertScript(string $expression, mixed $expected = true): self { if (! Str::contains($expression, ['===', '!==', '==', '!=', '>', '<', '>=', '<=', '&&', '||']) && ! Str::startsWith($expression, 'return ') && ! Str::startsWith($expression, 'function')) { $expression = "function() { return {$expression}; }"; @@ -190,7 +190,7 @@ public function assertScript(string $expression, mixed $expected = true): Webpag /** * Assert that the given source code is present on the page. */ - public function assertSourceHas(string $code): Webpage + public function assertSourceHas(string $code): self { $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."; @@ -202,7 +202,7 @@ public function assertSourceHas(string $code): Webpage /** * Assert that the given source code is not present on the page. */ - public function assertSourceMissing(string $code): Webpage + public function assertSourceMissing(string $code): self { $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."; @@ -214,7 +214,7 @@ public function assertSourceMissing(string $code): Webpage /** * Assert that the given link is present on the page. */ - public function assertSeeLink(string $link): Webpage + public function assertSeeLink(string $link): self { $locator = $this->guessLocator($link); @@ -226,7 +226,7 @@ public function assertSeeLink(string $link): Webpage /** * Assert that the given link is not present on the page. */ - public function assertDontSeeLink(string $link): Webpage + public function assertDontSeeLink(string $link): self { $locator = $this->guessLocator($link); @@ -238,7 +238,7 @@ public function assertDontSeeLink(string $link): Webpage /** * Assert that the given checkbox is checked. */ - public function assertChecked(string $field, string|int|float|null $value = null): Webpage + public function assertChecked(string $field, string|int|float|null $value = null): self { $value = $value !== null ? (string) $value : null; @@ -251,7 +251,7 @@ public function assertChecked(string $field, string|int|float|null $value = null /** * Assert that the given checkbox is not checked. */ - public function assertNotChecked(string $field, string|int|float|null $value = null): Webpage + public function assertNotChecked(string $field, string|int|float|null $value = null): self { $value = $value !== null ? (string) $value : null; @@ -264,7 +264,7 @@ public function assertNotChecked(string $field, string|int|float|null $value = n /** * Assert that the given checkbox is in an indeterminate state. */ - public function assertIndeterminate(string $field, string|int|float|null $value = null): Webpage + public function assertIndeterminate(string $field, string|int|float|null $value = null): self { $value = $value !== null ? (string) $value : null; @@ -289,7 +289,7 @@ public function assertIndeterminate(string $field, string|int|float|null $value /** * Assert that the given radio field is selected. */ - public function assertRadioSelected(string $field, string|int|float $value): Webpage + public function assertRadioSelected(string $field, string|int|float $value): self { $value = (string) $value; @@ -301,7 +301,7 @@ public function assertRadioSelected(string $field, string|int|float $value): Web /** * Assert that the given radio field is not selected. */ - public function assertRadioNotSelected(string $field, string|int|float|null $value = null): Webpage + public function assertRadioNotSelected(string $field, string|int|float|null $value = null): self { $value = $value !== null ? (string) $value : null; @@ -332,7 +332,7 @@ public function assertRadioNotSelected(string $field, string|int|float|null $val /** * Assert that the given dropdown has the given value selected. */ - public function assertSelected(string $field, string|int|float $value): Webpage + public function assertSelected(string $field, string|int|float $value): self { $value = (string) $value; @@ -347,7 +347,7 @@ public function assertSelected(string $field, string|int|float $value): Webpage /** * Assert that the given dropdown does not have the given value selected. */ - public function assertNotSelected(string $field, string|int|float $value): Webpage + public function assertNotSelected(string $field, string|int|float $value): self { $value = (string) $value; @@ -362,7 +362,7 @@ public function assertNotSelected(string $field, string|int|float $value): Webpa /** * Assert that the element matching the given selector has the given value. */ - public function assertValue(string $field, string|int|float $value): Webpage + public function assertValue(string $field, string|int|float $value): self { $value = (string) $value; @@ -376,7 +376,7 @@ public function assertValue(string $field, string|int|float $value): Webpage /** * Assert that the element matching the given selector does not have the given value. */ - public function assertValueIsNot(string $selector, string|int|float $value): Webpage + public function assertValueIsNot(string $selector, string|int|float $value): self { $value = (string) $value; @@ -389,7 +389,7 @@ public function assertValueIsNot(string $selector, string|int|float $value): Web /** * Assert that the element matching the given selector has the given value in the provided attribute. */ - public function assertAttribute(string $selector, string $attribute, string|int|float $value): Webpage + public function assertAttribute(string $selector, string $attribute, string|int|float $value): self { $value = (string) $value; @@ -402,7 +402,7 @@ public function assertAttribute(string $selector, string $attribute, string|int| /** * Assert that the element matching the given selector is missing the provided attribute. */ - public function assertAttributeMissing(string $selector, string $attribute): Webpage + public function assertAttributeMissing(string $selector, string $attribute): self { $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}]."); @@ -413,7 +413,7 @@ public function assertAttributeMissing(string $selector, string $attribute): Web /** * Assert that the element matching the given selector contains the given value in the provided attribute. */ - public function assertAttributeContains(string $selector, string $attribute, string|int|float $value): Webpage + public function assertAttributeContains(string $selector, string $attribute, string|int|float $value): self { $value = (string) $value; @@ -430,7 +430,7 @@ public function assertAttributeContains(string $selector, string $attribute, str /** * Assert that the element matching the given selector does not contain the given value in the provided attribute. */ - public function assertAttributeDoesntContain(string $selector, string $attribute, string|int|float $value): Webpage + public function assertAttributeDoesntContain(string $selector, string $attribute, string|int|float $value): self { $value = (string) $value; @@ -449,7 +449,7 @@ public function assertAttributeDoesntContain(string $selector, string $attribute /** * Assert that the element matching the given selector has the given value in the provided aria attribute. */ - public function assertAriaAttribute(string $selector, string $attribute, string|int|float $value): Webpage + public function assertAriaAttribute(string $selector, string $attribute, string|int|float $value): self { $value = (string) $value; @@ -459,7 +459,7 @@ public function assertAriaAttribute(string $selector, string $attribute, string| /** * Assert that the element matching the given selector has the given value in the provided data attribute. */ - public function assertDataAttribute(string $selector, string $attribute, string|int|float $value): Webpage + public function assertDataAttribute(string $selector, string $attribute, string|int|float $value): self { $value = (string) $value; @@ -469,7 +469,7 @@ public function assertDataAttribute(string $selector, string $attribute, string| /** * Assert that the element matching the given selector is visible. */ - public function assertVisible(string $selector): Webpage + public function assertVisible(string $selector): self { $locator = $this->guessLocator($selector); @@ -481,7 +481,7 @@ public function assertVisible(string $selector): Webpage /** * Assert that the element matching the given selector is present. */ - public function assertPresent(string $selector): Webpage + public function assertPresent(string $selector): self { $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."); @@ -492,7 +492,7 @@ public function assertPresent(string $selector): Webpage /** * Assert that the element matching the given selector is not present in the source. */ - public function assertNotPresent(string $selector): Webpage + public function assertNotPresent(string $selector): self { $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."); @@ -503,7 +503,7 @@ public function assertNotPresent(string $selector): Webpage /** * Assert that the element matching the given selector is not visible. */ - public function assertMissing(string $selector): Webpage + public function assertMissing(string $selector): self { $locator = $this->guessLocator($selector); @@ -515,7 +515,7 @@ public function assertMissing(string $selector): Webpage /** * Assert that the given field is enabled. */ - public function assertEnabled(string $field): Webpage + public function assertEnabled(string $field): self { 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."); @@ -525,7 +525,7 @@ public function assertEnabled(string $field): Webpage /** * Assert that the given field is disabled. */ - public function assertDisabled(string $field): Webpage + public function assertDisabled(string $field): self { 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."); @@ -535,7 +535,7 @@ public function assertDisabled(string $field): Webpage /** * Assert that the given button is enabled. */ - public function assertButtonEnabled(string $button): Webpage + public function assertButtonEnabled(string $button): self { $selector = $this->guessLocator($button); @@ -547,7 +547,7 @@ public function assertButtonEnabled(string $button): Webpage /** * Assert that the given button is disabled. */ - public function assertButtonDisabled(string $button): Webpage + public function assertButtonDisabled(string $button): self { $selector = $this->guessLocator($button); @@ -561,7 +561,7 @@ public function assertButtonDisabled(string $button): Webpage * * @deprecated Use `assertSee` instead. */ - public function waitForText(string|int|float $text): Webpage + public function waitForText(string|int|float $text): self { $text = (string) $text; diff --git a/src/Api/Concerns/MakesUrlAssertions.php b/src/Api/Concerns/MakesUrlAssertions.php index 564a9d4a..f28c5172 100644 --- a/src/Api/Concerns/MakesUrlAssertions.php +++ b/src/Api/Concerns/MakesUrlAssertions.php @@ -15,7 +15,7 @@ trait MakesUrlAssertions /** * Assert that the current URL (without the query string) matches the given string. */ - public function assertUrlIs(string $url): Webpage + public function assertUrlIs(string $url): self { $pattern = str_replace('\*', '.*', preg_quote($url, '/')); @@ -42,7 +42,7 @@ public function assertUrlIs(string $url): Webpage /** * Assert that the current URL scheme matches the given scheme. */ - public function assertSchemeIs(string $scheme): Webpage + public function assertSchemeIs(string $scheme): self { $pattern = str_replace('\*', '.*', preg_quote($scheme, '/')); @@ -57,7 +57,7 @@ public function assertSchemeIs(string $scheme): Webpage /** * Assert that the current URL scheme does not match the given scheme. */ - public function assertSchemeIsNot(string $scheme): Webpage + public function assertSchemeIsNot(string $scheme): self { $actual = parse_url($this->page->url(), PHP_URL_SCHEME) ?? ''; @@ -70,7 +70,7 @@ public function assertSchemeIsNot(string $scheme): Webpage /** * Assert that the current URL host matches the given host. */ - public function assertHostIs(string $host): Webpage + public function assertHostIs(string $host): self { $pattern = str_replace('\*', '.*', preg_quote($host, '/')); @@ -85,7 +85,7 @@ public function assertHostIs(string $host): Webpage /** * Assert that the current URL host does not match the given host. */ - public function assertHostIsNot(string $host): Webpage + public function assertHostIsNot(string $host): self { $actual = parse_url($this->page->url(), PHP_URL_HOST) ?? ''; @@ -98,7 +98,7 @@ public function assertHostIsNot(string $host): Webpage /** * Assert that the current URL port matches the given port. */ - public function assertPortIs(string $port): Webpage + public function assertPortIs(string $port): self { $pattern = str_replace('\*', '.*', preg_quote($port, '/')); @@ -120,7 +120,7 @@ public function assertPortIs(string $port): Webpage /** * Assert that the current URL port does not match the given port. */ - public function assertPortIsNot(string $port): Webpage + public function assertPortIsNot(string $port): self { $actual = (string) (parse_url($this->page->url(), PHP_URL_PORT) ?? '80'); @@ -133,7 +133,7 @@ public function assertPortIsNot(string $port): Webpage /** * Assert that the current URL path begins with the given path. */ - public function assertPathBeginsWith(string $path): Webpage + public function assertPathBeginsWith(string $path): self { /** @var non-empty-string $actualPath */ $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -152,7 +152,7 @@ public function assertPathBeginsWith(string $path): Webpage * * @param array $parameters */ - public function assertRoute(string $route, array $parameters = []): Webpage + public function assertRoute(string $route, array $parameters = []): self { if (function_exists('route') === false) { throw new RuntimeException('The [route] function is not available. Ensure you are using a framework that provides this function.'); @@ -164,7 +164,7 @@ public function assertRoute(string $route, array $parameters = []): Webpage /** * Assert that the current URL path ends with the given path. */ - public function assertPathEndsWith(string $path): Webpage + public function assertPathEndsWith(string $path): self { $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -180,7 +180,7 @@ public function assertPathEndsWith(string $path): Webpage /** * Assert that the current URL path contains the given path. */ - public function assertPathContains(string $path): Webpage + public function assertPathContains(string $path): self { $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -195,7 +195,7 @@ public function assertPathContains(string $path): Webpage /** * Assert that the current path matches the given path. */ - public function assertPathIs(string $path): Webpage + public function assertPathIs(string $path): self { $pattern = str_replace('\*', '.*', preg_quote($path, '/')); @@ -210,7 +210,7 @@ public function assertPathIs(string $path): Webpage /** * Assert that the current path does not match the given path. */ - public function assertPathIsNot(string $path): Webpage + public function assertPathIsNot(string $path): self { $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -223,7 +223,7 @@ public function assertPathIsNot(string $path): Webpage /** * Assert that the given query string parameter is present and has a given value. */ - public function assertQueryStringHas(string $name, ?string $value = null): Webpage + public function assertQueryStringHas(string $name, ?string $value = null): self { $output = $this->assertHasQueryStringParameter($name); @@ -242,7 +242,7 @@ public function assertQueryStringHas(string $name, ?string $value = null): Webpa /** * Assert that the given query string parameter is missing. */ - public function assertQueryStringMissing(string $name): Webpage + public function assertQueryStringMissing(string $name): self { $parsedUrl = parse_url($this->page->url()); @@ -263,7 +263,7 @@ public function assertQueryStringMissing(string $name): Webpage /** * Assert that the URL's current hash fragment matches the given fragment. */ - public function assertFragmentIs(string $fragment): Webpage + public function assertFragmentIs(string $fragment): self { $href = $this->page->evaluate('window.location.href'); @@ -286,7 +286,7 @@ public function assertFragmentIs(string $fragment): Webpage /** * Assert that the URL's current hash fragment begins with the given fragment. */ - public function assertFragmentBeginsWith(string $fragment): Webpage + public function assertFragmentBeginsWith(string $fragment): self { $href = $this->page->evaluate('window.location.href'); @@ -306,7 +306,7 @@ public function assertFragmentBeginsWith(string $fragment): Webpage /** * Assert that the URL's current hash fragment does not match the given fragment. */ - public function assertFragmentIsNot(string $fragment): Webpage + public function assertFragmentIsNot(string $fragment): self { $href = $this->page->evaluate('window.location.href'); From 91ae817262913ea6b7b9f8591bed732a80122e00 Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Wed, 10 Sep 2025 13:01:10 -0300 Subject: [PATCH 3/8] Extract text locator to its own method --- src/Api/Concerns/MakesElementAssertions.php | 8 ++------ src/Api/Webpage.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Api/Concerns/MakesElementAssertions.php b/src/Api/Concerns/MakesElementAssertions.php index 21ffec3c..3e8f5d47 100644 --- a/src/Api/Concerns/MakesElementAssertions.php +++ b/src/Api/Concerns/MakesElementAssertions.php @@ -46,9 +46,7 @@ public function assertSee(string|int|float $text): self { $text = (string) $text; - $locator = $this->page->unstrict( - fn () => $this->page->getByText($text), - ); + $locator = $this->getTextLocator($text); foreach ($locator->all() as $element) { if ($element->isVisible()) { @@ -70,9 +68,7 @@ public function assertDontSee(string|int|float $text): self { $text = (string) $text; - $locator = $this->page->unstrict( - fn () => $this->page->getByText($text), - ); + $locator = $this->getTextLocator($text); foreach ($locator->all() as $element) { if ($element->isVisible()) { diff --git a/src/Api/Webpage.php b/src/Api/Webpage.php index d42876a7..343d5e3c 100644 --- a/src/Api/Webpage.php +++ b/src/Api/Webpage.php @@ -102,4 +102,14 @@ private function guessLocator(string $selector, ?string $value = null): Locator { return (new GuessLocator($this->page))->for($selector, $value); } + + /** + * Gets the locator for the given text. + */ + private function getTextLocator(string $text): Locator + { + return $this->page->unstrict( + fn (): Locator => $this->page->getByText($text), + ); + } } From 9e9c4bfd5445961fe6392522734916ad01af8217 Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Wed, 10 Sep 2025 13:02:49 -0300 Subject: [PATCH 4/8] Adds optional `scope` parameter to the `GuessLocator` class --- src/Support/GuessLocator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Support/GuessLocator.php b/src/Support/GuessLocator.php index b9abb555..e133f351 100644 --- a/src/Support/GuessLocator.php +++ b/src/Support/GuessLocator.php @@ -18,6 +18,7 @@ */ public function __construct( private Page $page, + private ?string $scope = null, ) { // } From 2fcba6cffcf5e5efdd1fdb343d59661965b95b37 Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Wed, 10 Sep 2025 13:03:19 -0300 Subject: [PATCH 5/8] Apply scope to locators when needed --- src/Support/GuessLocator.php | 38 +++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Support/GuessLocator.php b/src/Support/GuessLocator.php index e133f351..f80ef857 100644 --- a/src/Support/GuessLocator.php +++ b/src/Support/GuessLocator.php @@ -33,16 +33,18 @@ public function for(string $selector, ?string $value = null): Locator $selector .= sprintf('[value=%s]', Selector::escapeForAttributeSelectorOrRegex($value, true)); } - return $this->page->locator($selector); + return $this->applyScopeToLocator($this->page->locator($selector)); } if (Selector::isDataTest($selector)) { $id = Selector::escapeForAttributeSelectorOrRegex(str_replace('@', '', $selector), true); - return $this->page->unstrict( - fn (): Locator => $this->page->locator( - "[data-testid=$id], [data-test=$id]", - ), + return $this->applyScopeToLocator( + $this->page->unstrict( + fn (): Locator => $this->page->locator( + "[data-testid=$id], [data-test=$id]", + ), + ) ); } @@ -53,8 +55,10 @@ public function for(string $selector, ?string $value = null): Locator $formattedSelector .= sprintf('[value=%s]', Selector::escapeForAttributeSelectorOrRegex($value, true)); } - $locator = $this->page->unstrict( - fn (): Locator => $this->page->locator($formattedSelector), + $locator = $this->applyScopeToLocator( + $this->page->unstrict( + fn (): Locator => $this->page->locator($formattedSelector), + ) ); if ($locator->count() > 0) { @@ -68,8 +72,24 @@ public function for(string $selector, ?string $value = null): Locator ); } - return $this->page->unstrict( - fn (): Locator => $this->page->getByText($selector, true), + return $this->applyScopeToLocator( + $this->page->unstrict( + fn (): Locator => $this->page->getByText($selector, true), + ) ); } + + /** + * Applies scope to a locator if scope is defined. + */ + private function applyScopeToLocator(Locator $locator): Locator + { + if ($this->scope === null) { + return $locator; + } + + $scopedParent = $this->page->locator($this->scope); + + return $scopedParent->locator($locator->selector()); + } } From a31c1284224490d2fa0523f43b2b2fab73d52dd1 Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Wed, 10 Sep 2025 13:03:49 -0300 Subject: [PATCH 6/8] Adds a `ScopedWebpage` to handle the `within` context --- src/Api/ScopedWebpage.php | 89 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/Api/ScopedWebpage.php diff --git a/src/Api/ScopedWebpage.php b/src/Api/ScopedWebpage.php new file mode 100644 index 00000000..cfe151f9 --- /dev/null +++ b/src/Api/ScopedWebpage.php @@ -0,0 +1,89 @@ +page->content()); + } + + /** + * Submits the first form found on the page. + */ + public function submit(): self + { + $this->guessLocator('[type="submit"]')->click(); + + return $this; + } + + /** + * Gets the page instance. + */ + public function value(string $selector): string + { + return $this->guessLocator($selector)->inputValue(); + } + + /** + * Limits the scope of subsequent interactions to within a specific element. + */ + public function within(string $selector, callable $callback): self + { + $nestedScope = $this->scope.' '.$selector; + $scopedWebpage = new self($this->page, $this->initialUrl, $nestedScope); + + $callback($scopedWebpage); + + return $this; + } + + /** + * Gets the locator for the given selector. + */ + private function guessLocator(string $selector, ?string $value = null): Locator + { + return (new GuessLocator($this->page, $this->scope))->for($selector, $value); + } + + /** + * Gets the locator for the given text. + */ + private function getTextLocator(string $text): Locator + { + return $this->page->unstrict( + fn (): Locator => $this->page->locator($this->scope)->getByText($text) + ); + } +} From 5b53a670b2a1f072d884d8b612182d2a4b1b52ed Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Wed, 10 Sep 2025 13:03:59 -0300 Subject: [PATCH 7/8] Adds `within` to `Webpage` --- src/Api/Webpage.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Api/Webpage.php b/src/Api/Webpage.php index 343d5e3c..485b43ae 100644 --- a/src/Api/Webpage.php +++ b/src/Api/Webpage.php @@ -95,6 +95,18 @@ public function value(string $selector): string return $this->guessLocator($selector)->inputValue(); } + /** + * Limits the scope of subsequent interactions to within a specific element. + */ + public function within(string $selector, callable $callback): self + { + $scopedWebpage = new ScopedWebpage($this->page, $this->initialUrl, $selector); + + $callback($scopedWebpage); + + return $this; + } + /** * Gets the locator for the given selector. */ From 40c26cde4f8154e49ef750afa0088b01d2e2ba01 Mon Sep 17 00:00:00 2001 From: Mateus Junges Date: Fri, 12 Sep 2025 19:43:19 -0300 Subject: [PATCH 8/8] Revert changes to `MakesUrlAssertions` --- src/Api/Concerns/MakesUrlAssertions.php | 36 ++++++++++++------------- src/Api/ScopedWebpage.php | 3 +-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Api/Concerns/MakesUrlAssertions.php b/src/Api/Concerns/MakesUrlAssertions.php index f28c5172..564a9d4a 100644 --- a/src/Api/Concerns/MakesUrlAssertions.php +++ b/src/Api/Concerns/MakesUrlAssertions.php @@ -15,7 +15,7 @@ trait MakesUrlAssertions /** * Assert that the current URL (without the query string) matches the given string. */ - public function assertUrlIs(string $url): self + public function assertUrlIs(string $url): Webpage { $pattern = str_replace('\*', '.*', preg_quote($url, '/')); @@ -42,7 +42,7 @@ public function assertUrlIs(string $url): self /** * Assert that the current URL scheme matches the given scheme. */ - public function assertSchemeIs(string $scheme): self + public function assertSchemeIs(string $scheme): Webpage { $pattern = str_replace('\*', '.*', preg_quote($scheme, '/')); @@ -57,7 +57,7 @@ public function assertSchemeIs(string $scheme): self /** * Assert that the current URL scheme does not match the given scheme. */ - public function assertSchemeIsNot(string $scheme): self + public function assertSchemeIsNot(string $scheme): Webpage { $actual = parse_url($this->page->url(), PHP_URL_SCHEME) ?? ''; @@ -70,7 +70,7 @@ public function assertSchemeIsNot(string $scheme): self /** * Assert that the current URL host matches the given host. */ - public function assertHostIs(string $host): self + public function assertHostIs(string $host): Webpage { $pattern = str_replace('\*', '.*', preg_quote($host, '/')); @@ -85,7 +85,7 @@ public function assertHostIs(string $host): self /** * Assert that the current URL host does not match the given host. */ - public function assertHostIsNot(string $host): self + public function assertHostIsNot(string $host): Webpage { $actual = parse_url($this->page->url(), PHP_URL_HOST) ?? ''; @@ -98,7 +98,7 @@ public function assertHostIsNot(string $host): self /** * Assert that the current URL port matches the given port. */ - public function assertPortIs(string $port): self + public function assertPortIs(string $port): Webpage { $pattern = str_replace('\*', '.*', preg_quote($port, '/')); @@ -120,7 +120,7 @@ public function assertPortIs(string $port): self /** * Assert that the current URL port does not match the given port. */ - public function assertPortIsNot(string $port): self + public function assertPortIsNot(string $port): Webpage { $actual = (string) (parse_url($this->page->url(), PHP_URL_PORT) ?? '80'); @@ -133,7 +133,7 @@ public function assertPortIsNot(string $port): self /** * Assert that the current URL path begins with the given path. */ - public function assertPathBeginsWith(string $path): self + public function assertPathBeginsWith(string $path): Webpage { /** @var non-empty-string $actualPath */ $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -152,7 +152,7 @@ public function assertPathBeginsWith(string $path): self * * @param array $parameters */ - public function assertRoute(string $route, array $parameters = []): self + public function assertRoute(string $route, array $parameters = []): Webpage { if (function_exists('route') === false) { throw new RuntimeException('The [route] function is not available. Ensure you are using a framework that provides this function.'); @@ -164,7 +164,7 @@ public function assertRoute(string $route, array $parameters = []): self /** * Assert that the current URL path ends with the given path. */ - public function assertPathEndsWith(string $path): self + public function assertPathEndsWith(string $path): Webpage { $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -180,7 +180,7 @@ public function assertPathEndsWith(string $path): self /** * Assert that the current URL path contains the given path. */ - public function assertPathContains(string $path): self + public function assertPathContains(string $path): Webpage { $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -195,7 +195,7 @@ public function assertPathContains(string $path): self /** * Assert that the current path matches the given path. */ - public function assertPathIs(string $path): self + public function assertPathIs(string $path): Webpage { $pattern = str_replace('\*', '.*', preg_quote($path, '/')); @@ -210,7 +210,7 @@ public function assertPathIs(string $path): self /** * Assert that the current path does not match the given path. */ - public function assertPathIsNot(string $path): self + public function assertPathIsNot(string $path): Webpage { $actualPath = parse_url($this->page->url(), PHP_URL_PATH) ?? ''; @@ -223,7 +223,7 @@ public function assertPathIsNot(string $path): self /** * Assert that the given query string parameter is present and has a given value. */ - public function assertQueryStringHas(string $name, ?string $value = null): self + public function assertQueryStringHas(string $name, ?string $value = null): Webpage { $output = $this->assertHasQueryStringParameter($name); @@ -242,7 +242,7 @@ public function assertQueryStringHas(string $name, ?string $value = null): self /** * Assert that the given query string parameter is missing. */ - public function assertQueryStringMissing(string $name): self + public function assertQueryStringMissing(string $name): Webpage { $parsedUrl = parse_url($this->page->url()); @@ -263,7 +263,7 @@ public function assertQueryStringMissing(string $name): self /** * Assert that the URL's current hash fragment matches the given fragment. */ - public function assertFragmentIs(string $fragment): self + public function assertFragmentIs(string $fragment): Webpage { $href = $this->page->evaluate('window.location.href'); @@ -286,7 +286,7 @@ public function assertFragmentIs(string $fragment): self /** * Assert that the URL's current hash fragment begins with the given fragment. */ - public function assertFragmentBeginsWith(string $fragment): self + public function assertFragmentBeginsWith(string $fragment): Webpage { $href = $this->page->evaluate('window.location.href'); @@ -306,7 +306,7 @@ public function assertFragmentBeginsWith(string $fragment): self /** * Assert that the URL's current hash fragment does not match the given fragment. */ - public function assertFragmentIsNot(string $fragment): self + public function assertFragmentIsNot(string $fragment): Webpage { $href = $this->page->evaluate('window.location.href'); diff --git a/src/Api/ScopedWebpage.php b/src/Api/ScopedWebpage.php index cfe151f9..a7ddf77c 100644 --- a/src/Api/ScopedWebpage.php +++ b/src/Api/ScopedWebpage.php @@ -19,8 +19,7 @@ Concerns\InteractsWithViewPort, Concerns\MakesConsoleAssertions, Concerns\MakesElementAssertions, - Concerns\MakesScreenshotAssertions, - Concerns\MakesUrlAssertions; + Concerns\MakesScreenshotAssertions; public function __construct( private Page $page,