output:
Wall time: 3.3965 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Database Testing Running Seeders\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n## Running Seeders\n\nIf you would like to use [database seeders](/docs/{{version}}/seeding) to populate your database during a feature test, you may invoke the `seed` method. By default, the `seed` method will execute the `DatabaseSeeder`, which should execute all of your other seeders. Alternatively, you pass a specific seeder class name to the `seed` method:\n\n```php tab=Pest\n<?php\n\nuse Database\\Seeders\\OrderStatusSeeder;\nuse Database\\Seeders\\TransactionStatusSeeder;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\n\npest()->use(RefreshDatabase::class);\n\ntest('orders can be created', function () {\n    // Run the DatabaseSeeder...\n    $this->seed();\n\n    // Run a specific seeder...\n    $this->seed(OrderStatusSeeder::class);\n\n    // ...\n\n    // Run an array of specific seeders...\n    $this->seed([\n        OrderStatusSeeder::class,\n        TransactionStatusSeeder::class,\n        // ...\n    ]);\n});\n```\n\n```php tab=PHPUnit\n<?php\n\nnamespace Tests\\Feature;\n\nuse Database\\Seeders\\OrderStatusSeeder;\nuse Database\\Seeders\\TransactionStatusSeeder;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    use RefreshDatabase;\n\n    /**\n     * Test creating a new order.\n     */\n    public function test_orders_can_be_created(): void\n    {\n        // Run the DatabaseSeeder...\n        $this->seed();\n\n        // Run a specific seeder...\n        $this->seed(OrderStatusSeeder::class);\n\n        // ...\n\n        // Run an array of specific seeders...\n        $this->seed([\n            OrderStatusSeeder::class,\n            TransactionStatusSeeder::class,\n            // ...\n        ]);\n    }\n}\n```\n\nAlternatively, you may instruct Laravel to automatically seed the database before each test that uses the `RefreshDatabase` trait. You may accomplish this by defining a `$seed` property on your base test class:\n\n```php\n<?php\n\nnamespace Tests;\n\nuse Illuminate\\Foundation\\Testing\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{\n    /**\n     * Indicates whether the default seeder should run before each test.\n     *\n     * @var bool\n     */\n    protected $seed = true;\n}\n```\n\nWhen the `$seed` property is `true`, the test will run the `Database\\Seeders\\DatabaseSeeder` class before each test that uses the `RefreshDatabase` trait. However, you may specify a specific seeder that should be executed by defining a `$seeder` property on your test class:\n\n```php\nuse Database\\Seeders\\OrderStatusSeeder;\n\n/**\n * Run a specific seeder before each test.\n *\n * @var string\n */\nprotected $seeder = OrderStatusSeeder::class;\n```\n\n---\n\n## Wire Transition Browser support\nWire Transition | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-transition\n\n## Browser support\n\nView Transitions are supported in Chrome 111+, Edge 111+, and Safari 18+. In browsers that don't support View Transitions, elements will appear and disappear without animation—the functionality still works, just without the visual transition.\n\n> [!warning] Firefox has limited support\n> Firefox 144+ supports basic view transitions, but does not support transition types.\n\n[View browser support on caniuse.com →](https://caniuse.com/view-transitions)\n\n---\n\n## Tia\nTia | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/tia.md\n\n---\ntitle: Test Impact Analysis (TIA)\ndescription: Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes.\n---\n\n# Test Impact Analysis (TIA)\n\n> **Heads up — please keep this between us.** Test Impact Analysis has shipped quietly in Pest 4 so a small group of teams can battle-test it on real suites before we put it on stage. The official, public launch is reserved for **Pest 5**.\n>\n> If you're reading this, you're getting an early look as a thank-you for being part of the Pest community. We'd love your feedback, bug reports, and edge cases — but we kindly ask that you **don't share this publicly** (no tweets, blog posts, livestreams, or conference demos) until the Pest 5 announcement. Internal use within your team is absolutely fine.\n>\n> Thanks for helping us get this right.\n\nTest Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes. The first time you run with `--tia`, Pest records a graph of which tests depend on which files. Every run after that, Pest looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.\n\nA typical Laravel suite that takes 15 seconds replays in under a second. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.\n\nTo get started, just add the `--tia` flag to any Pest invocation:\n\n```bash\n./vendor/bin/pest --parallel --tia\n```\n\nThe first run is the **baseline** — Pest enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. Expect a small overhead on this run only.\n\nEvery subsequent run is a **replay**. Pest compares your working tree against the baseline and re-runs only the tests affected by your changes:\n\n```\nTests:    774 passed (2658 assertions, 7 affected, 2 uncached, 765 replayed)\nDuration: 0.74s\n```\n\n`affected` is the set of tests Pest re-ran because their dependencies changed. `uncached` means Pest had to execute a test because no cached result existed yet. `replayed` is the set whose results were served from cache.\n\n## How Pest Decides What To Run\n\nFor each file you've changed, Pest looks for tests that depend on it:\n\n- **PHP source files** — your `app/` classes, controllers, models, helpers — are tracked through the coverage driver. A change to `app/Models/User.php` re-runs only the tests that touched `User`.\n- **Migrations** are intersected with the tables each test queried during the baseline. A column rename in `create_users_table.php` re-runs only the tests that queried the `users` table.\n- **Inertia pages** under `resources/js/Pages` re-run only the tests that server-side-rendered them.\n- **Shared JS components** under `resources/js/Components`, `Layouts`, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.\n- **Frontend runtime files** like `resources/js/App.jsx`, `resources/js/bootstrap.js`, `resources/js/echo.js`, and `resources/js/favicon.js` re-run tests that rendered Inertia components, because they can affect the whole client runtime.\n- **Blade templates** re-run only the tests that rendered them, including renders triggered by browser tests.\n- **Arch tests** re-run for project PHP source changes, because Arch expectations inspect files by namespace and path instead of executing those files.\n- **Browser assets** such as CSS, public build files, static public assets, and `public/hot` re-run browser tests only.\n- **Anything else** — config files, route files, fixture data, files outside the recorded graph — falls through to a broader pattern. Editing `config/app.php` re-runs every test, because Pest can't statically prove which tests depend on it.\n\nSome files change the shape of the graph itself rather than a single test result. Pest rebuilds the graph when structural inputs drift, including `composer.json`, `composer.lock`, `phpunit.xml`, `vite.config.*`, `package.json`, Node lockfiles, and `tsconfig` / `jsconfig` files. Environment files such as `.env`, `.env.testing`, and local variants drop cached results and re-execute the suite while keeping the graph.\n\n## Cosmetic Edits Don't Run Anything\n\nPest normalises file content before comparing, so cosmetic changes don't trigger any tests. PHP files have whitespace, line comments, and docblocks stripped before hashing. Blade strips `{{-- … --}}` comments. JS, TS, Vue, and Svelte files have line and block comments removed too.\n\nThe result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.\n\n## Built-in Environments\n\nPest ships with watch defaults for the most common PHP stacks, and applies them automatically when their packages are installed:\n\n- **PHP** — always-on baseline rules for `composer.json`, `composer.lock`, `phpunit.xml`, and similar structural inputs.\n- **Laravel** — `app/`, `routes/`, `config/`, `database/migrations/`, `resources/views/`, and friends.\n- **Symfony** — `config/`, `migrations/`, `src/Migrations/`, `templates/`, `translations/`, `config/doctrine/`, `assets/`, `webpack.config.js`, and `importmap.php`.\n- **Livewire** — `resources/views/livewire/`, `resources/views/components/`, `resources/views/pages/`, plus JS/TS under `resources/js/`.\n- **Inertia** — server-side-rendered pages under `resources/js/Pages` and the Vite module graph for `Components`, `Layouts`, and runtime entry files.\n- **Browser** — CSS, public build files, static public assets, and `public/hot` re-run browser tests only.\n\nYou don't have to configure anything to opt in to these — Pest detects each framework via Composer and merges the relevant rules. To extend or override them, see [Custom Watch Patterns](#custom-watch-patterns).\n\n## Modes\n\nPest supports a few flags alongside `--tia`:\n\n| Flag | Behaviour |\n|---|---|\n| `--tia` | Replay if a baseline graph exists, otherwise record. |\n| `--no-tia` | Disable TIA for this run, even if `pest()->tia()->always()` is configured. |\n| `--tia --fresh` | Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale. |\n| `--tia --refetch` | Force a CI baseline fetch even within the 24-hour cooldown after a previous failed fetch. |\n| `--tia --filtered` | Narrow PHPUnit to only the affected test files rather than loading all tests and replaying cached results for unaffected ones. |\n| `--tia --locally` | Equivalent to `pest()->tia()->always()->locally()` — run TIA automatically on local machines but skip on CI. |\n| `--tia --baselined` | Opt in to fetching the shared baseline from CI when no local graph exists or the local graph drifts. |\n| `--baseline` | Print the absolute path of this project's TIA storage directory and exit. Designed for CI uploads — see [Sharing The Baseline From CI](#sharing-the-baseline-from-ci). |\n\n### Environment Variables\n\nEach enabling flag has an environment variable equivalent, useful for CI matrices, container entry points, and shared developer configs:\n\n| Variable | Equivalent flag |\n|---|---|\n| `PEST_TIA=1` | `--tia` |\n| `PEST_TIA_FILTERED=1` | `--filtered` |\n| `PEST_TIA_LOCALLY=1` | `--locally` |\n| `PEST_TIA_BASELINED=1` | `--baselined` |\n\n## Sharing The Baseline From CI\n\nRecording the baseline locally takes minutes on large suites. Instead, you can have CI record it once per merge to `main`, and every developer downloads the result.\n\nBaseline fetching is **opt-in**. Enable it either with `--tia --baselined` on the command line, the `PEST_TIA_BASELINED=1` environment variable, or — preferred for teams — by calling `pest()->tia()->baselined()` in `tests/Pest.php`. Once enabled, when Pest detects no local graph (or the local graph is out of date) it uses GitHub's CLI to download the latest successful run of a `tia-baseline.yml` workflow's `pest-tia-baseline` artifact. Pest validates the fetched graph against your project state — if it matches, it's adopted. Otherwise it's discarded and a local rebuild proceeds.\n\nHere's a starter workflow you can drop into `.github/workflows/tia-baseline.yml`:\n\n```yaml\nname: TIA Baseline\non:\n  push: { branches: [main] }\n  schedule: [{ cron: '0 3 * * *' }]\n  workflow_dispatch:\njobs:\n  baseline:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with: { fetch-depth: 0 }\n      - uses: shivammathur/setup-php@v2\n        with: { php-version: '8.4', coverage: xdebug }\n      - run: composer install --no-interaction --prefer-dist\n\n      - name: Run tests\n        run: ./vendor/bin/pest --parallel --tia --coverage\n\n      - name: Resolve TIA baseline path\n        id: baseline\n        run: echo \"path=$(vendor/bin/pest --baseline)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Upload TIA baseline\n        uses: actions/upload-artifact@v4\n        with:\n          name: pest-tia-baseline\n          path: ${{ steps.baseline.outputs.path }}\n          include-hidden-files: true\n          retention-days: 30\n```\n\n`vendor/bin/pest --baseline` prints the absolute path to this project's TIA storage directory (typically `~/.pest/tia/<project-key>/`), which is exactly what `actions/upload-artifact` needs to bundle the recorded graph and coverage cache. `include-hidden-files: true` is required because the baseline lives under a dot-prefixed directory.\n\nAfter CI runs, every developer with `baselined()` enabled who runs `pest --tia` for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.\n\n## Storage\n\nPest stores its state at `~/.pest/tia/<project-key>/`, where the project key is derived from your normalised git remote URL — so `git@github.com:foo/bar.git` and `https://github.com/foo/bar` produce the same key. A non-git project falls back to a hash of the project's absolute path.\n\nSharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.\n\n## Configuration\n\nYou can configure TIA behaviour in `tests/Pest.php` via `pest()->tia()`:\n\n```php\npest()->tia()\n    ->always()     // run TIA on every invocation, no --tia flag needed\n    ->locally()    // restrict always() to local environments only\n    ->baselined()  // fetch the shared baseline from CI when no local graph exists\n    ->filtered();  // narrow PHPUnit to only affected test files\n```\n\n**`always()`** activates TIA for every `pest` run without requiring the `--tia` flag. Pair it with **`locally()`** to restrict that behaviour to local machines — on CI (detected via the `--ci` flag or the `CI` environment variable) TIA is skipped automatically. An explicit `--tia` on the command line always takes effect regardless, and `--no-tia` can disable it for a single run.\n\n**`filtered()`** enables filtered mode, equivalent to `--tia --filtered`. In this mode Pest narrows PHPUnit to only the affected test files rather than loading the full suite and replaying cached results for unaffected tests:\n\n```php\npest()->tia()->filtered();\n```\n\n**`baselined()`** opts in to fetching the shared TIA baseline from CI when no local graph exists or the local graph drifts. See [Sharing The Baseline From CI](#sharing-the-baseline-from-ci) for the recommended workflow:\n\n```php\npest()->tia()->baselined();\n```\n\n## Custom Watch Patterns\n\nIf your project has a directory layout that doesn't match the framework defaults, you can register custom watch patterns in `tests/Pest.php`:\n\n```php\npest()->tia()->watch([\n    'config/billing/**/*.php' => 'tests/Feature/Billing',\n    'public/build/**/*'       => 'tests/Browser',\n]);\n```\n\nEach glob maps to a test directory or an exact test file. Whenever a matching file changes, every test under that directory is invalidated. If a glob already exists in Pest's defaults, your target is merged with the existing targets instead of replacing them.\n\n---\n\nNow that you've learned how to use Test Impact Analysis to speed up your test suite, let's discuss how to integrate Pest with your continuous integration workflow: [Continuous Integration](/docs/continuous-integration)\n\n\n---\n\n## Testing Browser testing\nTesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing\n\n## Browser testing\n\nPest v4 includes first-party browser testing support powered by Playwright. This allows you to test your Livewire components in a real browser, interacting with them just like a user would.\n\n### Installing browser testing\n\nFirst, install the Pest browser plugin:\n\n```shell\ncomposer require pestphp/pest-plugin-browser --dev\n```\n\nNext, install Playwright via npm:\n\n```shell\nnpm install playwright@latest\nnpx playwright install\n```\n\nFor complete browser testing documentation, see the [Pest browser testing guide](https://pestphp.com/docs/browser-testing).\n\n### Writing browser tests\n\nInstead of using `Livewire::test()`, you can use `Livewire::visit()` to test your component in a real browser:\n\n```php\nit('can create a new post', function () {\n    Livewire::visit('post.create')\n        ->type('[wire\\:model=\"title\"]', 'My first post')\n        ->type('[wire\\:model=\"content\"]', 'This is the content')\n        ->press('Save')\n        ->assertSee('Post created successfully');\n});\n```\n\nBrowser tests are slower than unit tests but provide end-to-end confidence that your components work as expected in a real browser environment.\n\nFor a complete list of available browser testing assertions, see the [Pest browser testing assertions](https://pestphp.com/docs/browser-testing#content-available-assertions).\n\n> [!info] When to use browser tests\n> Use browser tests for critical user flows and complex interactions. For most component testing, the standard `Livewire::test()` approach is faster and sufficient.\n\n---\n\n## Database Testing Introduction Resetting the Database After Each Test\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n### Resetting the Database After Each Test\n\nBefore proceeding much further, let's discuss how to reset your database after each of your tests so that data from a previous test does not interfere with subsequent tests. Laravel's included `Illuminate\\Foundation\\Testing\\RefreshDatabase` trait will take care of this for you. Simply use the trait on your test class:\n\n```php tab=Pest\n<?php\n\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\n\npest()->use(RefreshDatabase::class);\n\ntest('basic example', function () {\n    $response = $this->get('/');\n\n    // ...\n});\n```\n\n```php tab=PHPUnit\n<?php\n\nnamespace Tests\\Feature;\n\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    use RefreshDatabase;\n\n    /**\n     * A basic functional test example.\n     */\n    public function test_basic_example(): void\n    {\n        $response = $this->get('/');\n\n        // ...\n    }\n}\n```\n\nThe `Illuminate\\Foundation\\Testing\\RefreshDatabase` trait does not migrate your database if your schema is up to date. Instead, it will only execute the test within a database transaction. Therefore, any records added to the database by test cases that do not use this trait may still exist in the database.\n\nIf you would like to totally reset the database, you may use the `Illuminate\\Foundation\\Testing\\DatabaseMigrations` or `Illuminate\\Foundation\\Testing\\DatabaseTruncation` traits instead. However, both of these options are significantly slower than the `RefreshDatabase` trait.\n\n---\n\n## Database Testing\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n# Database Testing\n\n- [Introduction](#introduction)\n    - [Resetting the Database After Each Test](#resetting-the-database-after-each-test)\n- [Model Factories](#model-factories)\n- [Running Seeders](#running-seeders)\n- [Available Assertions](#available-assertions)\n\n---\n\n## Browser Testing assertNotSelected\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertNotSelected\n\nThe `assertNotSelected` method asserts that the given dropdown does not have the given value selected:\n\n```php\n$page->assertNotSelected('country', 'UK');\n```\n\n---\n\n## Styles Browser support\nStyles | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/styles\n\n## Browser support\n\nScoped styles use [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting), which is supported in all modern browsers (Chrome 120+, Firefox 117+, Safari 17.2+). For older browser support, consider using a CSS preprocessor or the `@assets` directive with pre-compiled stylesheets.\n\n---\n\n## Installation Components not updating or errors in browser console\nInstallation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/installation\n\n## Components not updating or errors in browser console\n\n**Check the following:**\n- Ensure `@livewireStyles` is in the `<head>` of your layout\n- Ensure `@livewireScripts` is before `</body>` in your layout\n- Check your browser's developer console for JavaScript errors\n- Verify you're running a supported PHP version (8.1+) and Laravel version (10+)\n- Clear your application cache: `php artisan cache:clear`\n\nIf issues persist, check the [troubleshooting documentation](/docs/4.x/troubleshooting) for more detailed debugging steps.\n\n---\n\n## Browser Testing waitForKey\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### waitForKey\n\nThe `waitForKey` method opens the current page URL in the default web browser and waits for a key press:\n\n```php\n$page->waitForKey(); // Useful for debugging\n```\n\n## Debugging tests\n\n\nSometimes you may want to debug your browser tests. Pest provides a convenient way to do this by using the `--debug` option, which makes pest to open the browser window and pause the execution of the test when it fails. You can then inspect the page and see what went wrong.\n\n```bash\n./vendor/bin/pest --debug\n```\n\nOptionally, you can also use the `debug()` method in your test. It will limit execution to this test (like using [`only()`](filtering-tests.md#only)), pause the execution and open the browser window:\n\n```php\n$page->debug();\n```\n\n\n\nYou can also take a screenshot of the current page using the `screenshot()` method. This is useful for visual debugging:\n\nNOTE: If you don't pass the filename, it will use the test name as the filename.\n\n```php\n$page->screenshot();\n$page->screenshot(fullPage: true);\n$page->screenshot(filename: 'custom-name');\n```\n\n\nYou can also take a screenshot of a specific element using the `screenshotElement()` method:\n\n```php\n$page->screenshotElement('#my-element');\n```\n\n\nYou can also use the `tinker()` method to open a Tinker session in the context of the current page. This allows you to interact with the page using PHP code:\n\n```php\n$page->tinker();\n```\n\n\nAfter you can run your tests with the `--headed` option to open the browser window:\n\n```bash\n./vendor/bin/pest --headed\n```\n\nIf you wish to run the tests in a headed mode by default, you can set it in your `Pest.php` configuration file:\n\n```php\npest()->browser()->headed();\n```\n\n## Continuous Integration\n\nYou may refer to Pest's [Continuous Integration](https://pestphp.com/docs/continuous-integration) documentation for more information on how to run your browser tests in a CI environment.\n\nHowever, if you are using GitHub Actions, you need to add the following steps to your workflow file:\n\n```yaml\n    - uses: actions/setup-node@v4\n      with:\n        node-version: lts/*\n\n    - name: Install dependencies\n      run: npm ci\n\n    - name: Install Playwright Browsers\n      run: npx playwright install --with-deps\n```\n\n---\n\nNow, let's dive into architectural testing and how it can benefit your development process. By performing architectural testing, you can evaluate the overall design of your application and identify potential flaws before they become significant issues: [Arch Testing](/docs/arch-testing)\n\n---\n\n## Browser Testing assertSelected\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertSelected\n\nThe `assertSelected` method asserts that the given dropdown has the given value selected:\n\n```php\n$page->assertSelected('country', 'US');\n```\n\n---\n\n## Browser Testing\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n---\ntitle: Browser Testing\ndescription:\n---\n\n# Browser Testing\n\nBrowser testing is an essential part of modern web development, allowing you to ensure that your application works correctly across different browsers and devices. Pest provides a simple and elegant way to write browser tests. Here is an example of how to write a browser test using Pest:\n\n```php\nit('may welcome the user', function () {\n    $page = visit('/');\n\n    $page->assertSee('Welcome');\n});\n```\n\nThis is a basic example of a browser test that checks if the homepage contains the text \"Welcome\". However, Pest's browser testing capabilities go beyond this simple example. You can use various methods to interact with the page, such as clicking buttons, filling out forms, and navigating between pages.\n\nHere is an example of a more complex browser test, on Laravel, that checks if a user can sign in:\n\n```php\nit('may sign in the user', function () {\n    Event::fake();\n\n    User::factory()->create([ // assumes RefreshDatabase trait is used on Pest.php...\n        'email' => 'nuno@laravel.com',\n        'password' => 'password',\n    ]);\n\n    $page = visit('/')->on()->mobile()->firefox();\n\n    $page->click('Sign In')\n         ->assertUrlIs('/login')\n         ->assertSee('Sign In to Your Account')\n         ->fill('email', 'nuno@laravel.com')\n         ->fill('password', 'password')\n         ->click('Submit')\n         ->assertSee('Dashboard');\n\n    $this->assertAuthenticated();\n\n    Event::assertDispatched(UserLoggedIn::class);\n});\n```\n\nNote that, you are leveraging the full power of Laravel's testing capabilities, such as refresh database, event faking, and authentication assertions, while also actually doing browser testing.\n\n## Getting Started\n\nTo get started with browser testing in Pest, you need to install the Pest Browser plugin. You can do this by running the following command:\n\n```bash\ncomposer require pestphp/pest-plugin-browser --dev\n\nnpm install playwright@latest\nnpx playwright install\n```\n\nFinally, add `tests/Browser/Screenshots` to your `.gitignore` file to avoid committing screenshots taken during browser tests.\n\n### Running Browser Tests\n\nRunning browser tests is similar to running regular Pest tests:\n\n```bash\n./vendor/bin/pest\n```\n\nWe recommend running tests in parallel using the `--parallel` option to speed up the execution:\n\n```bash\n./vendor/bin/pest --parallel\n```\n\nFor debugging purposes, you can run the tests in a headed mode and pause the execution at the end of the failed test run:\n\n```bash\n./vendor/bin/pest --debug\n```\n\n### Navigation\n\nThe `visit()` method is used to navigate to a specific URL in your browser test. It provides various methods to interact with the page:\n\n```php\ntest('example', function () {\n    $page = visit('/');\n\n    $page->assertSee('Welcome');\n});\n```\n\n### Using Other Browsers\n\nBy default, the `visit()` method uses Chrome as the browser. However, if you want to use a different browser, you can specify it using the `--browser` option when running the tests:\n\n```bash\n./vendor/bin/pest --browser firefox\n./vendor/bin/pest --browser safari\n```\n\nIf you wish to use a different browser by default without specifying it in the command line, you can set it in your `Pest.php` configuration file:\n\n```php\npest()->browser()->inFirefox();\npest()->browser()->inSafari();\n```\n\n### Using Other Devices\n\nThe `visit()` method uses a desktop viewport. However, you can specify a mobile viewport using the `onMobile()` method. For example:\n\n```php\n$page = visit('/')->on()->mobile();\n```\n\nIf you wish to use a specific device, you can use the `on()` method and chain it with the `macbook14`, `iPhone14Pro`, etc:\n\n```php\n$page = visit('/')->on()->iPhone14Pro();\n```\n\n### Using Dark Mode\n\nPest enforces a light color scheme by default. However, you can specify a dark color scheme using the `inDarkMode()` method:\n\n```php\n$page = visit('/')->inDarkMode();\n```\n\n### Visiting Multiple Pages\n\nYou can visit multiple pages simultaneously by passing an array of URLs to the `visit()` method. This is useful for testing scenarios where you need to interact with multiple pages at once:\n\n```php\n$pages = visit(['/', '/about']);\n\n$pages->assertNoSmoke()\n    ->assertNoAccessibilityIssues()\n    ->assertNoConsoleLogs()\n    ->assertNoJavaScriptErrors();\n\n[$homePage, $aboutPage] = $pages;\n\n$homePage->assertSee('Welcome to our website');\n$aboutPage->assertSee('About Us');\n```\n\n### Navigation\n\nAfter visiting a page, you can navigate to other pages using the `navigate()` method. This method allows you to navigate to a different URL while keeping the current browser context:\n\n```php\n$page = visit('/');\n\n$page->navigate('/about')\n     ->assertSee('About Us');\n```\n\n### Locating Elements\n\nYou can locate elements in the DOM using text or CSS selectors. Pest provides a simple syntax for locating elements:\n\n```php\n// Clicks the first link with the text \"Login\"\n$page->click('Login');\n\n// Clicks the first element with the class \"btn-primary\"\n$page->click('.btn-primary');\n\n// Clicks the element with the data-test attribute \"login\"\n$page->click('@login');\n\n// Clicks the element with the ID \"submit-button\"\n$page->click('#submit-button');\n\n// etc...\n```\n\n### Configuring Timeouts\n\nSometimes, elements may take time to appear on the page. By default, Pest waits for `5` seconds before timing out. You can configure the default timeout for browser tests in your `Pest.php` configuration file:\n\n```php\npest()->browser()->timeout(10000);\n```\n\n### Configuring User Agent\n\nBy default, the User Agent will default to the Browser you're running for tests such as: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/133.0.6943.16 Safari/537.36` \n\nYou may wish to override the User Agent of the browser for all of your tests, you can configure this in the `Pest.php` configuration file:\n\n```php\npest()->browser()->userAgent('CustomUserAgent');\n```\n\n### Configuring Host\n\nBy default, the server will bind to `127.0.0.1` for all browser tests.\n\nYou may wish to override the host for subdomain applications. You can configure this in the `Pest.php` configuration file:\n\n```php\npest()->browser()->withHost('some-subdomain.localhost');\n```\n\n### Geolocation\n\nSometimes, you need to define where the browser believes it is physically on the earth.  This method takes a latitude and longitude and will set the `geolocation` permission in the browser and then make the coordinates available via Javascript's getCurrentPosition API:\n\n```php\n$page = visit('/')\n     ->geolocation(39.399872, -8.224454);\n```\n\n### Configuring Locale\n\nYou can set the locale for your test requests using the `withLocale` method. This is particularly useful for testing multilingual applications.\n\n```php\n$page = visit('/')->withLocale('fr-FR');\n\n$page->assertSee('Bienvenue');\n```\n### Configuring Timezone\n\nYou can set the timezone for your test requests using the `withTimezone` method. This is useful for testing date and time displays in different time zones.\n\n```php\n$page = visit('/')->withTimezone('America/New_York');\n\n$page->assertSee('EST');\n```\n\n### Configuring UserAgent\n\nYou can set the User-Agent header for your test requests using the `withUserAgent` method. This is useful for testing how your application responds to different types of clients, such as mobile browsers or bots.\n\n```php\n$page = visit('/')->withUserAgent('Googlebot');\n\n$page->assertSee('Welcome, bot!');\n```\n\n### Configuring Host\n\nYou can set the host for your test server using the `withHost` method. This is useful for testing subdomains or where different hosts serve different content.\n\n```php\n$page = visit('/dashboard')->withHost('some-subdomain.localhost');\n\n$page->assertSee('Welcome to Some Subdomain');\n```\n\n## Table of Contents\n\n### Available Assertions\n\n
\n\n[assertTitle](#assert-title)\n[assertTitleContains](#assert-title-contains)\n[assertSee](#assert-see)\n[assertDontSee](#assert-dont-see)\n[assertSeeIn](#assert-see-in)\n[assertDontSeeIn](#assert-dont-see-in)\n[assertSeeAnythingIn](#assert-see-anything-in)\n[assertSeeNothingIn](#assert-see-nothing-in)\n[assertCount](#assert-count)\n[assertScript](#assert-script)\n[assertSourceHas](#assert-source-has)\n[assertSourceMissing](#assert-source-missing)\n[assertSeeLink](#assert-see-link)\n[assertDontSeeLink](#assert-dont-see-link)\n[assertChecked](#assert-checked)\n[assertNotChecked](#assert-not-checked)\n[assertIndeterminate](#assert-indeterminate)\n[assertRadioSelected](#assert-radio-selected)\n[assertRadioNotSelected](#assert-radio-not-selected)\n[assertSelected](#assert-selected)\n[assertNotSelected](#assert-not-selected)\n[assertValue](#assert-value)\n[assertValueIsNot](#assert-value-is-not)\n[assertAttribute](#assert-attribute)\n[assertAttributeMissing](#assert-attribute-missing)\n[assertAttributeContains](#assert-attribute-contains)\n[assertAttributeDoesntContain](#assert-attribute-doesnt-contain)\n[assertAriaAttribute](#assert-aria-attribute)\n[assertDataAttribute](#assert-data-attribute)\n[assertVisible](#assert-visible)\n[assertPresent](#assert-present)\n[assertNotPresent](#assert-not-present)\n[assertMissing](#assert-missing)\n[assertEnabled](#assert-enabled)\n[assertDisabled](#assert-disabled)\n[assertButtonEnabled](#assert-button-enabled)\n[assertButtonDisabled](#assert-button-disabled)\n[assertUrlIs](#assert-url-is)\n[assertSchemeIs](#assert-scheme-is)\n[assertSchemeIsNot](#assert-scheme-is-not)\n[assertHostIs](#assert-host-is)\n[assertHostIsNot](#assert-host-is-not)\n[assertPortIs](#assert-port-is)\n[assertPortIsNot](#assert-port-is-not)\n[assertPathBeginsWith](#assert-path-begins-with)\n[assertPathEndsWith](#assert-path-ends-with)\n[assertPathContains](#assert-path-contains)\n[assertPathIs](#assert-path-is)\n[assertPathIsNot](#assert-path-is-not)\n[assertQueryStringHas](#assert-query-string-has)\n[assertQueryStringMissing](#assert-query-string-missing)\n[assertFragmentIs](#assert-fragment-is)\n[assertFragmentBeginsWith](#assert-fragment-begins-with)\n[assertFragmentIsNot](#assert-fragment-is-not)\n[assertNoSmoke](#assert-no-smoke)\n[assertNoConsoleLogs](#assert-no-console-logs)\n[assertNoJavaScriptErrors](#assert-no-javascript-errors)\n[assertNoAccessibilityIssues](#assert-no-accessibility-issues)\n[assertScreenshotMatches](#assert-screenshot-matches)\n\n
\n\n### Element Interactions\n\n
\n\n[click](#click)\n[text](#text)\n[attribute](#attribute)\n[keys](#keys)\n[withKeyDown](#withKeyDown)\n[type](#type)\n[typeSlowly](#type-slowly)\n[select](#select)\n[append](#append)\n[clear](#clear)\n[radio](#radio)\n[check](#check)\n[uncheck](#uncheck)\n[attach](#attach)\n[press](#press)\n[pressAndWaitFor](#press-and-wait-for)\n[drag](#drag)\n[hover](#hover)\n[submit](#submit)\n[value](#value)\n[withinIframe](#within-iframe)\n[resize](#resize)\n[script](#script)\n[content](#content)\n[url](#url)\n[wait](#wait)\n[waitForKey](#wait-for-key)\n\n
\n\n### Debugging tests\n\n
\n\n[debug](#debug)\n[screenshot](#screenshot)\n[screenshotElement](#screenshot-element)\n[tinker](#tinker)\n[headed](#headed)\n\n
\n\n## Element Assertions\n\n---\n\n## Attribute Url Browser history\nAttribute Url | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-url\n\n## Browser history\n\nBy default, Livewire uses `history.replaceState()` to modify the URL without adding browser history entries. To add history entries (making the back button restore previous query values), use `history`:\n\n```php\n#[Url(history: true)] // [tl! highlight]\npublic $search = '';\n```\n\nNow clicking the browser's back button will restore previous search values instead of navigating to the previous page.\n\n---\n\n## Browser Testing withinIframe\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n\n### withinIframe\n\nThe `withinIframe` method allows you to interact with elements inside an iframe:\n\n```php\nuse Pest\\Browser\\Api\\PendingAwaitablePage;\n\n$page->withinIframe('.iframe-container', function (PendingAwaitablePage $page) {\n    $page->type('frame-input', 'Hello iframe')\n        ->click('frame-button');\n});\n```\n\n---\n\n## Attribute On Listening to browser events\nAttribute On | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-on\n\n## Listening to browser events\n\nYou can also listen for browser events dispatched from JavaScript:\n\n```php\n#[On('user-logged-in')]\npublic function handleUserLogin()\n{\n    // Handle login...\n}\n```\n\n```javascript\n// From JavaScript\nwindow.dispatchEvent(new CustomEvent('user-logged-in'));\n```\n\n---\n\n## Browser Testing content\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### content\n\nThe `content` method gets the page's content:\n\n```php\n$html = $page->content();\n```\n\n---\n\n## Browser Testing assertDontSeeIn\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertDontSeeIn\n\nThe `assertDontSeeIn` method asserts that the given text is not present within the selector:\n\n```php\n$page->assertDontSeeIn('.error-container', 'Error occurred');\n```\n\n---\n\n## Pest V4 Is Here Now With Browser Testing\nPest V4 Is Here Now With Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest-v4-is-here-now-with-browser-testing.md\n\n---\ntitle: Pest v4 Is Here — Now with Browser Testing\ndescription: Today, we're thrilled to announce Pest v4 — our biggest release yet, featuring powerful new browser testing with parallel support and full Laravel integration.\n---\n\n> To get started with Pest v4's new features including browser testing, please refer to the upgrade guide: [Upgrade Guide →](/docs/upgrade-guide).\n\n- [Browser Testing](#content-pest-v4-is-here-now-with-browser-testing)\n- [Smoke Testing](#content-smoke-testing)\n- [Visual Regression Testing](#content-visual-regression-testing)\n- [Test Sharding](#content-test-sharding)\n- [Type Coverage Is Much Faster](#content-type-coverage-is-much-faster)\n- [Profanity Checking](#content-profanity-checking)\n- [On Top of PHPUnit 12](#content-on-top-of-phpunit-12)\n\n# Pest v4 Is Here — Now with Browser Testing\n\nToday, we're thrilled to announce the release of **Pest v4**, bringing the biggest testing upgrade yet: powerful **[Browser Testing](/docs/browser-testing)**. Pest's new browser testing features let you run elegant, maintainable browser tests — with first-class support for Laravel's testing API and the ability to run tests in parallel. For the first time, this is browser testing that feels as good as writing unit tests.\n\nHere is the creator of Pest, [Nuno Maduro](https://twitter.com/enunomaduro), demoing the new browser testing features in Pest v4 at Laracon US:\n\n[![Pest v4](/assets/pest4_play.png)](https://youtu.be/f5gAgwwwwOI?si=LtPpySZe3tf8qMjz&t=52)\n\nHere is an example of Browser Testing using [Laravel](https://laravel.com):\n\n```php\nit('may reset the password', function () {\n    // access any laravel testing helpers...\n    Notification::fake();\n\n    // access to the database — using the RefreshDatabase trait (even sqlite in memory...)\n    $this->actingAs(User::factory()->create());\n\n    $page = visit('/sign-in') // visit on a real browser...\n        ->on()->mobile() // or ->desktop(), ->tablet(), etc...\n        ->inDarkMode(); // or ->inLightMode()\n\n    $page->assertSee('Sign In')\n         ->click('Forgot Password?')\n         ->type('email', 'nuno@laravel.com')\n         ->press('Send Reset Link')\n         ->assertSee('We have emailed your password reset link!')\n         ->assertNoJavascriptErrors(); // or ->assertNoConsoleLogs()\n\n    Notification::assertSent(ResetPassword::class);\n});\n```\n\nWith Pest v4's browser testing, you can:\n- Seamlessly use **Laravel features** like `Event::fake()`, `assertAuthenticated()`, and model factories\n- Use `RefreshDatabase`, even with SQLite in-memory databases, to ensure a clean state for each test\n- Test on **multiple browsers** (Chrome, Firefox, Safari)\n- Test on **different devices** and viewports (like iPhone 14 Pro, tablets, or custom breakpoints)\n- Switch **color schemes** (light/dark mode)\n- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.)\n- Run **parallel browser tests** for dramatically faster suites\n- Take **screenshots** or pause tests for debugging\n- …all with the elegance of Pest syntax\n- **Playwright-based** — modern, fast, and reliable\n\nTo get started with browser testing in Pest, you need to install the Pest Browser Plugin:\n\n```bash\ncomposer require pestphp/pest-plugin-browser --dev\n\nnpm install playwright@latest\nnpx playwright install\n```\n\nAfter, you may use the `visit()` function anywhere. Finally, running this test is as simple as executing `./vendor/bin/pest` in your terminal. Pest will handle the rest, launching a browser, navigating to the page, and performing the actions you specified.\n\n## Smoke Testing\n\nSmoke testing your application in real browsers has never been easier. With Pest v4, you can literally visit all your application pages, and ensure they don't throw any JavaScript errors, and they don't log any console errors.\n\n```php\n$routes = ['/', '/about', '/contact'];\n\nvisit($routes)->assertNoSmoke();\n\n// assertNoSmoke() is a shorthand for:\n// - assertNoJavascriptErrors()\n// - assertNoConsoleLogs()\n```\n\n## Visual Regression Testing\n\nWant to ensure your pages look exactly as expected over time? Pest v4 introduces visual regression testing with the `assertScreenshotMatches()` assertion. This allows you to take screenshots of your pages and compare them against baseline images, ensuring that your UI remains consistent across changes.\n\n```php\n$pages = visit(['/', '/about', '/contact']);\n\n$pages->assertScreenshotMatches();\n```\n\nThis is just a glimpse of what Browser Testing in Pest v4 can do. Find out more about the new features below, and check out the [Browser Testing documentation](/docs/browser-testing) for a complete guide on how to get started.\n\n## Test Sharding\n\nPest v4 introduces **Test Sharding**, allowing you to split your test suite into smaller, manageable chunks. This is particularly useful for large applications (or when running browser tests) where running all tests at once can be time-consuming.\n\nThis feature is particularly useful on CI platforms, where on things like GitHub actions you can no longer scale vertical, but rather horizontally. This means you can run your tests in parallel across multiple machines, significantly speeding up your test suite execution.\n\nTo get started with Test Sharding, you can use the `--shard` option when running Pest:\n\n```bash\n# GitHub Workflow One\n./vendor/bin/pest --shard=1/4\n\n# GitHub Workflow Two\n./vendor/bin/pest --shard=2/4\n\n# GitHub Workflow Three\n./vendor/bin/pest --shard=3/4\n\n# GitHub Workflow Four\n./vendor/bin/pest --shard=4/4\n```\n\nYou may combine this with the `--parallel` option to run your tests in parallel, and this way trully maximize your test suite execution speed:\n\n```bash\n./vendor/bin/pest --shard=1/4 --parallel\n```\n\nFinally, to easily set up sharding your CI configuration, you just have to ensure each job in your CI configuration runs a different shard of your test suite. For example, in GitHub Actions, you can use the `matrix` strategy to define multiple jobs that run different shards:\n\n```yaml\nstrategy:\n  matrix:\n    shard: [1, 2, 3, 4]\n\nname: Tests (Shard ${{ matrix.shard }}/4)\n\nsteps:\n  - name: Run tests\n    run: pest --parallel --shard ${{ matrix.shard }}/4\n```\n\n## Type Coverage Is Much Faster\n\nRemember the days when you had to wait for your type coverage to run? Not anymore! Pest v4 introduces a new type coverage engine that is significantly faster than previous versions.\n\nType coverage is 2x faster on the first run and instant on subsequent runs. This means you can quickly check your type coverage without waiting for long periods, making your development workflow much more efficient.\n\nIn addition, Type Coverage now supports **Sharding**. This means you can run type coverage with the `--shard` option, just like you do with your tests.\n\n## Profanity Checking\n\nPest v4 introduces a new feature that allows you to check for profanity in your test code. This is particularly useful for maintaining a clean and professional codebase, especially in collaborative environments.\n\nYou can enable profanity checking by adding the `--profanity` option when running Pest:\n\n```bash\n./vendor/bin/pest --profanity\n```\n\nTo start using Pest's Profanity plugin, you need to require the plugin via Composer.\n\n```bash\ncomposer require pestphp/pest-plugin-profanity --dev\n```\n\nAfter requiring the plugin, you may utilize the `--profanity` option to generate a report of your profanity.\n\n```bash\n./vendor/bin/pest --profanity\n```\n\n\n\nIf any of your files contain profanity, they will be highlighted in red and displayed using their respective line\nnumbers and the profane word(s) that have been found.\n\nAs an example, `pr31(f*ck)` means that the word \"fuck\" was found on line 31.\n\nTo learn more about the Profanity plugin and how to configure it, check out the [Profanity documentation](/docs/profanity).\n\n### Skip Locally or On CI\n\nPest v4 introduces the ability to conditionally skip tests based on the environment. You can use `skipLocally()` to skip tests when running locally, or `skipOnCi` to skip tests when running on a CI server.\n\n```php\nit('does not run locally', function () {\n    // This test will be skipped when running locally\n})->skipLocally();\n\nit('does not run on CI', function () {\n    // This test will be skipped when running on a CI server\n})->skipOnCi();\n```\n\n### Miscellaneous Improvements\n\n- You may now use `skipLocally()` or `skipOnCi` to conditionally skip tests based on the environment.\n- The `not->toHaveSuspiciousCharacters()` arch expectation has been added to help you identify potential suspicious characters in your code. This arch expectation is now enabled by default on the `php` arch preset. This expectation requires the `intl` PHP extension.\n- The expectation `toBeSlug` has been added to help you validate that a string is a valid slug.\n\n### On Top of PHPUnit 12\n\nPest v4 is built on top of PHPUnit 12, which means you get all the latest features and improvements from PHPUnit. As such, be sure to check out the [PHPUnit 12 release announcement](https://phpunit.de/announcements/phpunit-12.html).\n\n### Thanks To You, Pest v4 Is Here!\n\nThere's never been a better time to dive into testing and start using Pest. If you're ready to get started with Pest v4 right away, check out our [installation guide](/docs/installation) for step-by-step instructions. And if you're currently using an earlier version of Pest, we've got you covered with detailed upgrade instructions in our [upgrade guide](/docs/upgrade-guide).\n\nThank you for your continued support and feedback. We can't wait to see what you build with Pest v4!\n\n---\n\nThank you for reading about Pest v4's new features! Want to get started with Pest? You can find the installation guide in the next section of the documentation: [Installation →](/docs/installation)\n\n\n\n_[Truncated]_"}]


cmd: rg -n "Classic Cotton|Premium Slim|draft|Sold out|backorder|inventory_policy|deny|continue|New Arrivals|T-Shirts|About|featured|featured products|option" database app resources/views/livewire/storefront -g '*.php' -g '*.blade.php'


cmd: sed -n '1185,1420p' specs/08-PLAYWRIGHT-E2E-PLAN.md


cmd: sed -n '1,180p' resources/views/livewire/storefront/home.blade.php && sed -n '1,180p' resources/views/livewire/storefront/collections/index.blade.php && sed -n '1,220p' resources/views/livewire/storefront/collections/show.blade.php && sed -n '1,180p' resources/views/livewire/storefront/products/show.blade.php


cmd: sed -n '1,220p' app/Livewire/Storefront/Products/Show.php && sed -n '1,180p' app/Livewire/Storefront/Search/Index.php


command: sed -n '1185,1420p' specs/08-PLAYWRIGHT-E2E-PLAN.md

type: read

aggregated_output:
## Suite 7: Storefront Browsing

**File:** `tests/Browser/Storefront/BrowsingTest.php`

**Purpose:** Verify all storefront pages render correctly as a guest visitor - home page, collection listings, product detail with variant options, search results, static pages, and content visibility rules (draft/stock).

### Storefront Page Map

```mermaid
flowchart LR
    Home["/"] --> Collections["/collections"]
    Home --> Product["/products/{handle}"]
    Collections --> CollectionDetail["/collections/{handle}"]
    CollectionDetail --> Product
    Home --> Search["/search?q=..."]
    Search --> Product
    Home --> Pages["/pages/{handle}"]
    Product --> Cart["/cart"]
```

---

### Test 7.1: Shows featured products on home page

**Preconditions:** Seeded products

**Steps:**
1. Navigate to `/`

**Expected Results:**
- Page displays "Acme Fashion"
- Page displays "Classic Cotton T-Shirt"
- Page displays "24.99"
- No JavaScript errors

---

### Test 7.2: Shows collection with product grid

**Preconditions:** Seeded collections and products

**Steps:**
1. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- Page displays "Classic Cotton T-Shirt"
- No JavaScript errors

---

### Test 7.3: Can navigate from collection to product

**Preconditions:** Seeded collections and products

**Steps:**
1. Navigate to `/collections/t-shirts`
2. Click "Classic Cotton T-Shirt"

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "24.99"
- Page displays "Add to cart"
- No JavaScript errors

---

### Test 7.4: Shows product detail with variant options

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "24.99"
- Page displays "Size"
- Page displays "Color"
- No JavaScript errors

---

### Test 7.5: Shows size and color option values

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays size options: "S", "M", "L", "XL"
- Page displays color options: "Black", "White", "Navy"
- No JavaScript errors

---

### Test 7.6: Updates price when variant changes on product with compare-at pricing

**Preconditions:** Product #2 with compare-at pricing

**Steps:**
1. Navigate to `/products/premium-slim-fit-jeans`
2. Select a variant that has a compare-at price (sale variant)

**Expected Results:**
- Page displays "Premium Slim Fit Jeans"
- Sale price is displayed
- Compare-at (original) price is shown with strikethrough styling
- No JavaScript errors

---

### Test 7.7: Shows search results for valid query

**Preconditions:** Seeded products

**Steps:**
1. Navigate to `/search?q=cotton`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt" in results
- No JavaScript errors

---

### Test 7.8: Shows no results message for invalid query

**Preconditions:** None

**Steps:**
1. Navigate to `/search?q=zznonexistentproductzz`

**Expected Results:**
- Page displays "No results"
- No JavaScript errors

---

### Test 7.9: Does not show draft products on storefront collections

**Preconditions:** Product #15 with draft status

**Steps:**
1. Navigate to `/collections`

**Expected Results:**
- Product #15 (draft) must NOT appear anywhere in the listing
- No JavaScript errors

---

### Test 7.10: Does not show draft products in search results

**Preconditions:** Product #15 with draft status

**Steps:**
1. Navigate to `/search?q=draft`

**Expected Results:**
- No draft products appear in search results
- No JavaScript errors

---

### Test 7.11: Shows out of stock messaging for deny-policy product

**Preconditions:** Product #17 (0 stock, inventory policy = deny)

**Steps:**
1. Navigate to `/products/limited-edition-sneakers`

**Expected Results:**
- Page displays "Sold out"
- "Add to cart" button is disabled or hidden
- No JavaScript errors

---

### Test 7.12: Shows backorder messaging for continue-policy product

**Preconditions:** Product #18 (0 stock, inventory policy = continue)

**Steps:**
1. Navigate to `/products/backorder-denim-jacket`

**Expected Results:**
- Page displays "Available on backorder"
- "Add to cart" button is still enabled
- No JavaScript errors

---

### Test 7.13: Shows new arrivals collection

**Preconditions:** Seeded collections

**Steps:**
1. Navigate to `/collections/new-arrivals`

**Expected Results:**
- Page displays "New Arrivals"
- No JavaScript errors

---

### Test 7.14: Shows static about page

**Preconditions:** Seeded pages

**Steps:**
1. Navigate to `/pages/about`

**Expected Results:**
- Page displays "About"
- No JavaScript errors

---

### Test 7.15: Navigates between pages using the main navigation

**Preconditions:** Seeded store with navigation

**Steps:**
1. Navigate to `/`
2. Click "T-Shirts" in the main navigation menu

**Expected Results:**
- Page navigates to the T-Shirts collection
- Page displays "T-Shirts"
- No JavaScript errors

**Total: 15 tests**

---

status: completed


command: sed -n '1,220p' app/Livewire/Storefront/Products/Show.php && sed -n '1,180p' app/Livewire/Storefront/Search/Index.php

Index.php

aggregated_output:
<?php

namespace App\Livewire\Storefront\Products;

use App\Enums\InventoryPolicy;
use App\Models\Customer;
use App\Models\Product;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Services\CartService;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;

class Show extends Component
{
    public string $handle;

    public int $storeId;

    /**
     * @var array<string, string>
     */
    public array $selectedOptions = [];

    public int $quantity = 1;

    public function mount(string $handle): void
    {
        $this->handle = $handle;
        $this->storeId = $this->store()->getKey();

        $variant = $this->product()->variants->firstWhere('is_default', true)
            ?? $this->product()->variants->first();

        if ($variant instanceof ProductVariant) {
            $this->selectedOptions = $variant->optionValues
                ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
                ->all();
        }
    }

    public function selectOption(string $optionName, string $value): void
    {
        $this->selectedOptions[$optionName] = $value;
        $this->quantity = 1;
    }

    public function increaseQuantity(): void
    {
        $variant = $this->selectedVariant();
        $max = $variant?->inventoryItem?->policy === InventoryPolicy::Deny
            ? max(1, $variant->inventoryItem->availableQuantity())
            : null;

        if ($max === null || $this->quantity < $max) {
            $this->quantity++;
        }
    }

    public function decreaseQuantity(): void
    {
        $this->quantity = max(1, $this->quantity - 1);
    }

    public function addToCart(): void
    {
        $store = $this->store();
        $variant = $this->selectedVariant();

        if (! $variant instanceof ProductVariant || ! $this->canAddToCart()) {
            return;
        }

        $customer = Auth::guard('customer')->user();
        $customer = $customer instanceof Customer ? $customer : null;

        app(CartService::class)->addLine(
            app(CartService::class)->getOrCreateForSession($store, $customer),
            $variant->getKey(),
            $this->quantity,
        );

        $this->dispatch('cart-updated');
        $this->dispatch('toast', type: 'success', message: __('Added to cart'));
    }

    public function store(): Store
    {
        if (isset($this->storeId)) {
            $store = Store::query()->findOrFail($this->storeId);
            app()->instance('current_store', $store);

            return $store;
        }

        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    public function product(): Product
    {
        $this->store();

        return Product::query()
            ->with(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections'])
            ->where('handle', $this->handle)
            ->where('status', 'active')
            ->whereNotNull('published_at')
            ->firstOrFail();
    }

    public function selectedVariant(): ?ProductVariant
    {
        $product = $this->product();

        if ($product->options->isEmpty()) {
            return $product->variants->first();
        }

        return $product->variants->first(function (ProductVariant $variant): bool {
            $options = $variant->optionValues
                ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
                ->all();

            return $options === $this->selectedOptions;
        });
    }

    public function canAddToCart(): bool
    {
        $variant = $this->selectedVariant();

        if (! $variant instanceof ProductVariant || ! $variant->isPurchasable()) {
            return false;
        }

        $inventory = $variant->inventoryItem;

        return $inventory?->policy === InventoryPolicy::Continue
            || ($inventory?->availableQuantity() ?? 0) >= $this->quantity;
    }

    /**
     * @return array{message: string, class: string}
     */
    public function stockState(): array
    {
        $variant = $this->selectedVariant();
        $inventory = $variant?->inventoryItem;

        if (! $inventory) {
            return ['message' => 'Unavailable', 'class' => 'text-red-700 dark:text-red-300'];
        }

        if ($inventory->policy === InventoryPolicy::Continue && $inventory->availableQuantity() <= 0) {
            return ['message' => 'Available on backorder', 'class' => 'text-blue-700 dark:text-blue-300'];
        }

        if ($inventory->availableQuantity() <= 0) {
            return ['message' => 'Out of stock', 'class' => 'text-red-700 dark:text-red-300'];
        }

        if ($inventory->availableQuantity() <= 10) {
            return ['message' => "Only {$inventory->availableQuantity()} left in stock", 'class' => 'text-amber-700 dark:text-amber-300'];
        }

        return ['message' => 'In stock', 'class' => 'text-green-700 dark:text-green-300'];
    }

    public function render(): mixed
    {
        $product = $this->product();

        return view('livewire.storefront.products.show', [
            'product' => $product,
            'selectedVariant' => $this->selectedVariant(),
            'stockState' => $this->stockState(),
        ])->layout('layouts.storefront', [
            'title' => $product->title,
        ]);
    }
}
<?php

namespace App\Livewire\Storefront\Search;

use App\Models\Product;
use App\Models\Store;
use App\Services\SearchService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    public string $q = '';

    public string $sort = 'relevance';

    public bool $inStock = false;

    public ?int $minPrice = null;

    public ?int $maxPrice = null;

    /**
     * @var array<int, string>
     */
    public array $types = [];

    /**
     * @var array<int, string>
     */
    public array $vendors = [];

    public function mount(): void
    {
        $this->q = (string) request('q', '');
    }

    public function updated(): void
    {
        $this->resetPage();
    }

    public function clearFilters(): void
    {
        $this->sort = 'relevance';
        $this->inStock = false;
        $this->minPrice = null;
        $this->maxPrice = null;
        $this->types = [];
        $this->vendors = [];
        $this->resetPage();
    }

    public function products(): LengthAwarePaginator
    {
        return app(SearchService::class)->search(
            $this->store(),
            $this->q,
            $this->filters(),
            12,
            $this->sort,
        );
    }

    public function productTypes(): Collection
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->store()->getKey())
            ->where('status', 'active')
            ->whereNotNull('published_at')
            ->whereNotNull('product_type')
            ->distinct()
            ->orderBy('product_type')
            ->pluck('product_type');
    }

    public function productVendors(): Collection
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->store()->getKey())
            ->where('status', 'active')
            ->whereNotNull('published_at')
            ->whereNotNull('vendor')
            ->distinct()
            ->orderBy('vendor')
            ->pluck('vendor');
    }

    public function render(): mixed
    {
        return view('livewire.storefront.search.index', [
            'products' => $this->products(),
            'productTypes' => $this->productTypes(),
            'productVendors' => $this->productVendors(),
        ])->layout('layouts.storefront', [
            'title' => __('Search results'),
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function filters(): array
    {
        return [
            'in_stock' => $this->inStock,
            'price_min' => $this->minPrice === null ? null : $this->minPrice * 100,
            'price_max' => $this->maxPrice === null ? null : $this->maxPrice * 100,
            'product_type' => $this->types,
            'vendor' => $this->vendors,
        ];
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }
}


command: rg -n "Classic Cotton|Premium Slim|draft|Sold out|backorder|inventory_policy|deny|continue|New Arrivals|T-Shirts|About|featured|featured products|option" database app resources/views/livewire/storefront -g '*.php' -g '*.blade.php'

type: search

cmd: rg -n 'Classic Cotton|Premium Slim|draft|Sold out|backorder|inventory_policy|deny|continue|New Arrivals|T-Shirts|About|featured|featured products|option' database app resources/views/livewire/storefront -g '*.php' -g '*.blade.php'

query: Classic Cotton|Premium Slim|draft|Sold out|backorder|inventory_policy|deny|continue|New Arrivals|T-Shirts|About|featured|featured products|option

path: database

aggregated_output:
resources/views/livewire/storefront/home.blade.php:20:                @foreach ($featuredProducts->take(4) as $product)
resources/views/livewire/storefront/home.blade.php:39:            @foreach ($featuredCollections as $collection)
resources/views/livewire/storefront/home.blade.php:60:            @foreach ($featuredProducts as $product)
resources/views/livewire/storefront/cart-drawer.blade.php:37:                                        {{ $line->variant->optionValues->map(fn ($value) => $value->option->name.': '.$value->value)->implode(' / ') ?: ($line->variant->sku ?: 'Default') }}
resources/views/livewire/storefront/checkout/show.blade.php:69:                            <flux:select.option value="DE">Germany</flux:select.option>
resources/views/livewire/storefront/checkout/show.blade.php:70:                            <flux:select.option value="AT">Austria</flux:select.option>
resources/views/livewire/storefront/checkout/show.blade.php:71:                            <flux:select.option value="CH">Switzerland</flux:select.option>
resources/views/livewire/storefront/checkout/show.blade.php:161:                                <flux:select.option value="credit_card">Credit card</flux:select.option>
resources/views/livewire/storefront/checkout/show.blade.php:162:                                <flux:select.option value="paypal">PayPal</flux:select.option>
resources/views/livewire/storefront/checkout/show.blade.php:163:                                <flux:select.option value="bank_transfer">Bank transfer</flux:select.option>
resources/views/livewire/storefront/collections/show.blade.php:57:                    <flux:select.option value="featured">Featured</flux:select.option>
resources/views/livewire/storefront/collections/show.blade.php:58:                    <flux:select.option value="price_asc">Price: Low to High</flux:select.option>
resources/views/livewire/storefront/collections/show.blade.php:59:                    <flux:select.option value="price_desc">Price: High to Low</flux:select.option>
resources/views/livewire/storefront/collections/show.blade.php:60:                    <flux:select.option value="newest">Newest</flux:select.option>
resources/views/livewire/storefront/search/index.blade.php:59:                    <flux:select.option value="relevance">Relevance</flux:select.option>
resources/views/livewire/storefront/search/index.blade.php:60:                    <flux:select.option value="price_asc">Price: Low to High</flux:select.option>
resources/views/livewire/storefront/search/index.blade.php:61:                    <flux:select.option value="price_desc">Price: High to Low</flux:select.option>
resources/views/livewire/storefront/search/index.blade.php:62:                    <flux:select.option value="newest">Newest</flux:select.option>
resources/views/livewire/storefront/products/show.blade.php:32:            @if ($product->options->isNotEmpty())
resources/views/livewire/storefront/products/show.blade.php:34:                    @foreach ($product->options as $option)
resources/views/livewire/storefront/products/show.blade.php:35:                        <fieldset class="space-y-3" wire:key="product-option-selector-{{ $option->getKey() }}">
resources/views/livewire/storefront/products/show.blade.php:36:                            <legend class="text-sm font-medium text-zinc-950 dark:text-white">{{ $option->name }}</legend>
resources/views/livewire/storefront/products/show.blade.php:39:                                @foreach ($option->values as $value)
resources/views/livewire/storefront/products/show.blade.php:40:                                    @php($selected = ($selectedOptions[$option->name] ?? null) === $value->value)
resources/views/livewire/storefront/products/show.blade.php:43:                                        wire:click="selectOption(@js($option->name), @js($value->value))"
resources/views/livewire/storefront/products/show.blade.php:70:                    {{ $this->canAddToCart() ? 'Add to cart' : 'Sold out' }}
app/Services/VariantMatrixService.php:20:            $product->load(['options.values', 'variants.optionValues']);
app/Services/VariantMatrixService.php:22:            $valueGroups = $product->options
app/Services/VariantMatrixService.php:24:                ->map(fn ($option) => $option->values->sortBy('position')->values())
app/Services/VariantMatrixService.php:36:                return $this->combinationKey($variant->optionValues->pluck('id')->all());
app/Services/VariantMatrixService.php:45:                    continue;
app/Services/VariantMatrixService.php:49:                $variant->optionValues()->sync($combination);
app/Services/VariantMatrixService.php:57:                $key = $this->combinationKey($variant->optionValues->pluck('id')->all());
app/Services/VariantMatrixService.php:60:                    continue;
app/Services/VariantMatrixService.php:74:        $variants = $product->variants()->with('optionValues')->get();
app/Services/VariantMatrixService.php:101: …30805 chars truncated…tion_values' => $variant->optionValues->map(fn ($value): array => [
app/Http/Resources/Admin/V1/ProductResource.php:64:                        'option_name' => $value->option?->name,
database/seeders/CollectionSeeder.php:18:                ['title' => 'New Arrivals', 'handle' => 'new-arrivals', 'description_html' => '
Discover the latest additions to our store.
'],
database/seeders/CollectionSeeder.php:19:                ['title' => 'T-Shirts', 'handle' => 't-shirts', 'description_html' => '
Premium cotton tees for every occasion.
'],
database/seeders/CollectionSeeder.php:24:                ['title' => 'Featured', 'handle' => 'featured', 'description_html' => '
Featured electronics for tenant isolation tests.
'],
app/Http/Resources/Storefront/V1/CartLineResource.php:34:                'options' => $this->variant->relationLoaded('optionValues')
app/Http/Resources/Storefront/V1/CartLineResource.php:35:                    ? $this->variant->optionValues->map(fn ($value): array => [
app/Http/Resources/Storefront/V1/CartLineResource.php:36:                        'name' => $value->option?->name,
app/Models/InventoryItem.php:36:        'policy' => 'deny',
app/Models/Theme.php:33:        'status' => 'draft',
app/Models/Product.php:37:        'status' => 'draft',
app/Models/Product.php:52:    public function options(): HasMany
database/migrations/2026_05_03_213902_create_collections_table.php:21:            $table->enum('status', ['draft', 'active', 'archived'])->default('active');
app/Models/ProductVariant.php:148:    public function optionValues(): BelongsToMany
app/Models/ProductVariant.php:150:        return $this->belongsToMany(ProductOptionValue::class, 'variant_option_values', 'variant_id', 'product_option_value_id');
app/Models/Page.php:32:        'status' => 'draft',
database/migrations/2026_05_03_213857_create_product_options_table.php:14:        Schema::create('product_options', function (Blueprint $table) {
database/migrations/2026_05_03_213857_create_product_options_table.php:30:        Schema::dropIfExists('product_options');
database/migrations/2026_05_03_223824_create_themes_table.php:19:            $table->enum('status', ['draft', 'published'])->default('draft');
database/migrations/2026_05_03_223841_create_pages_table.php:20:            $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
database/migrations/0001_01_01_000002_create_jobs_table.php:31:            $table->mediumText('options')->nullable();
database/migrations/2026_05_03_225427_create_discounts_table.php:26:            $table->enum('status', ['draft', 'active', 'expired', 'disabled'])->default('active');
app/Models/ProductOptionValue.php:22:        'product_option_id',
app/Models/ProductOptionValue.php:47:            $builder->whereHas('option.product', function (Builder $query) use ($store): void {
app/Models/ProductOptionValue.php:58:    public function option(): BelongsTo
app/Models/ProductOptionValue.php:60:        return $this->belongsTo(ProductOption::class, 'product_option_id');
app/Models/ProductOptionValue.php:68:        return $this->belongsToMany(ProductVariant::class, 'variant_option_values', 'product_option_value_id', 'variant_id');
database/migrations/2026_05_03_213900_create_variant_option_values_table.php:14:        Schema::create('variant_option_values', function (Blueprint $table) {
database/migrations/2026_05_03_213900_create_variant_option_values_table.php:16:            $table->foreignId('product_option_value_id')->constrained()->cascadeOnDelete();
database/migrations/2026_05_03_213900_create_variant_option_values_table.php:18:            $table->primary(['variant_id', 'product_option_value_id']);
database/migrations/2026_05_03_213900_create_variant_option_values_table.php:19:            $table->index('product_option_value_id');
database/migrations/2026_05_03_213900_create_variant_option_values_table.php:28:        Schema::dropIfExists('variant_option_values');
database/migrations/2026_05_03_213856_create_products_table.php:19:            $table->enum('status', ['draft', 'active', 'archived'])->default('draft');
database/migrations/2026_05_03_213858_create_product_option_values_table.php:14:        Schema::create('product_option_values', function (Blueprint $table) {
database/migrations/2026_05_03_213858_create_product_option_values_table.php:16:            $table->foreignId('product_option_id')->constrained()->cascadeOnDelete();
database/migrations/2026_05_03_213858_create_product_option_values_table.php:20:            $table->index('product_option_id');
database/migrations/2026_05_03_213858_create_product_option_values_table.php:21:            $table->unique(['product_option_id', 'position']);
database/migrations/2026_05_03_213858_create_product_option_values_table.php:30:        Schema::dropIfExists('product_option_values');
database/migrations/2026_05_03_213901_create_inventory_items_table.php:20:            $table->enum('policy', ['deny', 'continue'])->default('deny');


command: sed -n '1,180p' resources/views/livewire/storefront/home.blade.php && sed -n '1,180p' resources/views/livewire/storefront/collections/index.blade.php && sed -n '1,220p' resources/views/livewire/storefront/collections/show.blade.php && sed -n '1,180p' resources/views/livewire/storefront/products/show.blade.php

show.blade.php

aggregated_output:
    @php($hero = data_get($themeSettings, 'home.hero', []))

{{ data_get($hero, 'eyebrow') }}

{{ data_get($hero, 'heading', $store->name) }}

{{ data_get($hero, 'subheading') }}

                    <flux:button :href="data_get($hero, 'primary_url', '/collections')" wire:navigate variant="primary">{{ data_get($hero, 'primary_label', 'Shop') }}</flux:button>
                    <flux:button :href="data_get($hero, 'secondary_url', '/collections')" wire:navigate variant="ghost">{{ data_get($hero, 'secondary_label', 'View collections') }}</flux:button>

                @foreach ($featuredProducts->take(4) as $product)

getKey() }}">
                        <x-storefront.product-card :product="$product" />

                @endforeach

Featured collections

Curated paths through the current store catalog.

            All collections

            @foreach ($featuredCollections as $collection)
                handle) }}" class="group rounded-lg border border-zinc-200 bg-white p-5 transition hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700" wire:navigate wire:key="home-collection-{{ $collection->getKey() }}">

                        <flux:icon name="rectangle-stack" class="size-10" />

{{ $collection->title }}

{{ $collection->products_count }} products

            @endforeach

Featured products

Active products currently published for {{ $store->name }}.

            @foreach ($featuredProducts as $product)
                <x-storefront.product-card :product="$product" wire:key="home-product-{{ $product->getKey() }}" />
            @endforeach

    <x-storefront.breadcrumbs :items="[['label' => 'Collections']]" />

Collections

Browse curated groups of products from this store.

        @foreach ($collections as $collection)
            handle) }}" class="group rounded-lg border border-zinc-200 bg-white p-5 transition hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700" wire:navigate wire:key="collection-index-{{ $collection->getKey() }}">

{{ $tag }}
                    @endforeach

{{ $product->title }}

                @if ($selectedVariant)
                    <x-storefront.price :amount="$selectedVariant->price_amount" :compare-at="$selectedVariant->compare_at_amount" :currency="$selectedVariant->currency" class="text-2xl" aria-live="polite" />
                @endif

            @if ($product->options->isNotEmpty())

                    @foreach ($product->options as $option)
                        <fieldset class="space-y-3" wire:key="product-option-selector-{{ $option->getKey() }}">
                            <legend class="text-sm font-medium text-zinc-950 dark:text-white">{{ $option->name }}</legend>

                                @foreach ($option->values as $value)
                                    @php($selected = ($selectedOptions[$option->name] ?? null) === $value->value)
                                    <button
                                        type="button"
                                        wire:click="selectOption(@js($option->name), @js($value->value))"
                                        @class([
                                            'min-h-10 rounded-md border px-4 py-2 text-sm font-medium transition',
                                            'border-blue-700 bg-blue-50 text-blue-800 dark:border-blue-300 dark:bg-blue-950 dark:text-blue-100' => $selected,
                                            'border-zinc-200 text-zinc-700 hover:border-zinc-400 dark:border-zinc-700 dark:text-zinc-200 dark:hover:border-zinc-500' => ! $selected,
                                        ])
                                        aria-pressed="{{ $selected ? 'true' : 'false' }}"
                                    >
                                        {{ $value->value }}
                                    </button>
                                @endforeach

                        </fieldset>
                    @endforeach

            @endif

{{ $stockState['message'] }}

                    <button type="button" wire:click="decreaseQuantity" class="flex size-11 items-center justify-center border-r border-zinc-200 text-lg dark:border-zinc-700" aria-label="Decrease quantity">-</button>
                    <input wire:model.live="quantity" type="number" min="1" class="h-11 w-full border-0 bg-transparent text-center text-sm" aria-label="Quantity">
                    <button type="button" wire:click="increaseQuantity" class="flex size-11 items-center justify-center border-l border-zinc-200 text-lg dark:border-zinc-700" aria-label="Increase quantity">+</button>

                <flux:button wire:click="addToCart" variant="primary" class="w-full" :disabled="! $this->canAddToCart()">
                    {{ $this->canAddToCart() ? 'Add to cart' : 'Sold out' }}
                </flux:button>

            @if ($product->description_html)

                    {!! $product->description_html !!}

            @endif

Vendor

{{ $product->vendor ?: '-' }}

Product type

{{ $product->product_type ?: '-' }}

                @if ($selectedVariant)

SKU

{{ $selectedVariant->sku ?: '-' }}

Shipping

{{ $selectedVariant->requires_shipping ? 'Required' : 'Digital delivery' }}

                @endif