├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock └── src ├── Controllers └── PlaywrightController.php ├── PlaywrightBoilerplateCommand.php ├── PlaywrightServiceProvider.php ├── routes └── playwright.php └── stubs ├── laravel-examples.spec.ts └── laravel-helpers.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Yoann Frommelt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel + Playwright Integration 2 | 3 | This package provides the necessary boilerplate to quickly begin testing your Laravel applications using Playwright. 4 | 5 | ## Installation 6 | 7 | If you haven't already installed [Playwright](https://playwright.dev/docs/intro); that's your first step. 8 | 9 | ```bash 10 | yarn create playwright 11 | ``` 12 | 13 | Now you're ready to install this package through Composer. Pull it in as a development-only dependency. 14 | 15 | ```bash 16 | composer require web-id/laravel-playwright --dev 17 | ``` 18 | 19 | Finally, run the `playwright:boilerplate` command to copy over the initial boilerplate files for your Playwright tests. 20 | 21 | ```bash 22 | php artisan playwright:boilerplate 23 | ``` 24 | 25 | That's it! You're ready to go. We've provided an `laravel-examples.spec.ts` spec for you to play around with it. Let's run it now: 26 | 27 | ``` 28 | yarn playwright test 29 | ``` 30 | 31 | 32 | ## Credits 33 | 34 | - [Yoann Frommelt](https://www.linkedin.com/in/yoannfrommelt/) 35 | - [Jeffrey Way](https://twitter.com/jeffrey_way) for the amazing inspiration 36 | 37 | ## License 38 | 39 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-id/laravel-playwright", 3 | "description": "Laravel Playwright Boilerplate", 4 | "keywords": [ 5 | "laravel", 6 | "playwtight", 7 | "end-to-end" 8 | ], 9 | "type": "library", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Yoann Frommelt", 14 | "email": "yoann@web-id.fr" 15 | } 16 | ], 17 | "homepage": "https://github.com/web-id-fr/laravel-playwright", 18 | "autoload": { 19 | "psr-4": { 20 | "WebId\\LaravelPlaywright\\": "src/" 21 | } 22 | }, 23 | "minimum-stability": "dev", 24 | "require": { 25 | "php": "^8.0", 26 | "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0" 27 | }, 28 | "config": { 29 | "sort-packages": true 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "WebId\\LaravelPlaywright\\PlaywrightServiceProvider" 35 | ] 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "a8e43b0b8184f5887015a19d049e2f47", 8 | "packages": [ 9 | { 10 | "name": "doctrine/inflector", 11 | "version": "2.1.x-dev", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/doctrine/inflector.git", 15 | "reference": "bf265da9f831e46e646c7a01b59ee8a33f8c0365" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/doctrine/inflector/zipball/bf265da9f831e46e646c7a01b59ee8a33f8c0365", 20 | "reference": "bf265da9f831e46e646c7a01b59ee8a33f8c0365", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^7.2 || ^8.0" 25 | }, 26 | "require-dev": { 27 | "doctrine/coding-standard": "^11.0", 28 | "phpstan/phpstan": "^1.8", 29 | "phpstan/phpstan-phpunit": "^1.1", 30 | "phpstan/phpstan-strict-rules": "^1.3", 31 | "phpunit/phpunit": "^8.5 || ^9.5", 32 | "vimeo/psalm": "^4.25 || ^5.4" 33 | }, 34 | "type": "library", 35 | "autoload": { 36 | "psr-4": { 37 | "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" 38 | } 39 | }, 40 | "notification-url": "https://packagist.org/downloads/", 41 | "license": [ 42 | "MIT" 43 | ], 44 | "authors": [ 45 | { 46 | "name": "Guilherme Blanco", 47 | "email": "guilhermeblanco@gmail.com" 48 | }, 49 | { 50 | "name": "Roman Borschel", 51 | "email": "roman@code-factory.org" 52 | }, 53 | { 54 | "name": "Benjamin Eberlei", 55 | "email": "kontakt@beberlei.de" 56 | }, 57 | { 58 | "name": "Jonathan Wage", 59 | "email": "jonwage@gmail.com" 60 | }, 61 | { 62 | "name": "Johannes Schmitt", 63 | "email": "schmittjoh@gmail.com" 64 | } 65 | ], 66 | "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", 67 | "homepage": "https://www.doctrine-project.org/projects/inflector.html", 68 | "keywords": [ 69 | "inflection", 70 | "inflector", 71 | "lowercase", 72 | "manipulation", 73 | "php", 74 | "plural", 75 | "singular", 76 | "strings", 77 | "uppercase", 78 | "words" 79 | ], 80 | "support": { 81 | "issues": "https://github.com/doctrine/inflector/issues", 82 | "source": "https://github.com/doctrine/inflector/tree/2.1.x" 83 | }, 84 | "funding": [ 85 | { 86 | "url": "https://www.doctrine-project.org/sponsorship.html", 87 | "type": "custom" 88 | }, 89 | { 90 | "url": "https://www.patreon.com/phpdoctrine", 91 | "type": "patreon" 92 | }, 93 | { 94 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", 95 | "type": "tidelift" 96 | } 97 | ], 98 | "time": "2023-01-15T14:40:19+00:00" 99 | }, 100 | { 101 | "name": "illuminate/collections", 102 | "version": "9.x-dev", 103 | "source": { 104 | "type": "git", 105 | "url": "https://github.com/illuminate/collections.git", 106 | "reference": "d5182350914d6cdee3dd19e07864de26f4c3b741" 107 | }, 108 | "dist": { 109 | "type": "zip", 110 | "url": "https://api.github.com/repos/illuminate/collections/zipball/d5182350914d6cdee3dd19e07864de26f4c3b741", 111 | "reference": "d5182350914d6cdee3dd19e07864de26f4c3b741", 112 | "shasum": "" 113 | }, 114 | "require": { 115 | "illuminate/conditionable": "^9.0", 116 | "illuminate/contracts": "^9.0", 117 | "illuminate/macroable": "^9.0", 118 | "php": "^8.0.2" 119 | }, 120 | "suggest": { 121 | "symfony/var-dumper": "Required to use the dump method (^6.0)." 122 | }, 123 | "type": "library", 124 | "extra": { 125 | "branch-alias": { 126 | "dev-master": "9.x-dev" 127 | } 128 | }, 129 | "autoload": { 130 | "files": [ 131 | "helpers.php" 132 | ], 133 | "psr-4": { 134 | "Illuminate\\Support\\": "" 135 | } 136 | }, 137 | "notification-url": "https://packagist.org/downloads/", 138 | "license": [ 139 | "MIT" 140 | ], 141 | "authors": [ 142 | { 143 | "name": "Taylor Otwell", 144 | "email": "taylor@laravel.com" 145 | } 146 | ], 147 | "description": "The Illuminate Collections package.", 148 | "homepage": "https://laravel.com", 149 | "support": { 150 | "issues": "https://github.com/laravel/framework/issues", 151 | "source": "https://github.com/laravel/framework" 152 | }, 153 | "time": "2023-02-09T08:37:58+00:00" 154 | }, 155 | { 156 | "name": "illuminate/conditionable", 157 | "version": "9.x-dev", 158 | "source": { 159 | "type": "git", 160 | "url": "https://github.com/illuminate/conditionable.git", 161 | "reference": "bea24daa0fa84b7e7b0d5b84f62c71b7e2dc3364" 162 | }, 163 | "dist": { 164 | "type": "zip", 165 | "url": "https://api.github.com/repos/illuminate/conditionable/zipball/bea24daa0fa84b7e7b0d5b84f62c71b7e2dc3364", 166 | "reference": "bea24daa0fa84b7e7b0d5b84f62c71b7e2dc3364", 167 | "shasum": "" 168 | }, 169 | "require": { 170 | "php": "^8.0.2" 171 | }, 172 | "type": "library", 173 | "extra": { 174 | "branch-alias": { 175 | "dev-master": "9.x-dev" 176 | } 177 | }, 178 | "autoload": { 179 | "psr-4": { 180 | "Illuminate\\Support\\": "" 181 | } 182 | }, 183 | "notification-url": "https://packagist.org/downloads/", 184 | "license": [ 185 | "MIT" 186 | ], 187 | "authors": [ 188 | { 189 | "name": "Taylor Otwell", 190 | "email": "taylor@laravel.com" 191 | } 192 | ], 193 | "description": "The Illuminate Conditionable package.", 194 | "homepage": "https://laravel.com", 195 | "support": { 196 | "issues": "https://github.com/laravel/framework/issues", 197 | "source": "https://github.com/laravel/framework" 198 | }, 199 | "time": "2023-02-01T21:42:32+00:00" 200 | }, 201 | { 202 | "name": "illuminate/contracts", 203 | "version": "9.x-dev", 204 | "source": { 205 | "type": "git", 206 | "url": "https://github.com/illuminate/contracts.git", 207 | "reference": "44f65d723b13823baa02ff69751a5948bde60c22" 208 | }, 209 | "dist": { 210 | "type": "zip", 211 | "url": "https://api.github.com/repos/illuminate/contracts/zipball/44f65d723b13823baa02ff69751a5948bde60c22", 212 | "reference": "44f65d723b13823baa02ff69751a5948bde60c22", 213 | "shasum": "" 214 | }, 215 | "require": { 216 | "php": "^8.0.2", 217 | "psr/container": "^1.1.1|^2.0.1", 218 | "psr/simple-cache": "^1.0|^2.0|^3.0" 219 | }, 220 | "type": "library", 221 | "extra": { 222 | "branch-alias": { 223 | "dev-master": "9.x-dev" 224 | } 225 | }, 226 | "autoload": { 227 | "psr-4": { 228 | "Illuminate\\Contracts\\": "" 229 | } 230 | }, 231 | "notification-url": "https://packagist.org/downloads/", 232 | "license": [ 233 | "MIT" 234 | ], 235 | "authors": [ 236 | { 237 | "name": "Taylor Otwell", 238 | "email": "taylor@laravel.com" 239 | } 240 | ], 241 | "description": "The Illuminate Contracts package.", 242 | "homepage": "https://laravel.com", 243 | "support": { 244 | "issues": "https://github.com/laravel/framework/issues", 245 | "source": "https://github.com/laravel/framework" 246 | }, 247 | "time": "2023-02-08T14:36:30+00:00" 248 | }, 249 | { 250 | "name": "illuminate/macroable", 251 | "version": "9.x-dev", 252 | "source": { 253 | "type": "git", 254 | "url": "https://github.com/illuminate/macroable.git", 255 | "reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a" 256 | }, 257 | "dist": { 258 | "type": "zip", 259 | "url": "https://api.github.com/repos/illuminate/macroable/zipball/e3bfaf6401742a9c6abca61b9b10e998e5b6449a", 260 | "reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a", 261 | "shasum": "" 262 | }, 263 | "require": { 264 | "php": "^8.0.2" 265 | }, 266 | "type": "library", 267 | "extra": { 268 | "branch-alias": { 269 | "dev-master": "9.x-dev" 270 | } 271 | }, 272 | "autoload": { 273 | "psr-4": { 274 | "Illuminate\\Support\\": "" 275 | } 276 | }, 277 | "notification-url": "https://packagist.org/downloads/", 278 | "license": [ 279 | "MIT" 280 | ], 281 | "authors": [ 282 | { 283 | "name": "Taylor Otwell", 284 | "email": "taylor@laravel.com" 285 | } 286 | ], 287 | "description": "The Illuminate Macroable package.", 288 | "homepage": "https://laravel.com", 289 | "support": { 290 | "issues": "https://github.com/laravel/framework/issues", 291 | "source": "https://github.com/laravel/framework" 292 | }, 293 | "time": "2022-08-09T13:29:29+00:00" 294 | }, 295 | { 296 | "name": "illuminate/support", 297 | "version": "9.x-dev", 298 | "source": { 299 | "type": "git", 300 | "url": "https://github.com/illuminate/support.git", 301 | "reference": "3954928becd80579425d0105229ab3ec91b2afb7" 302 | }, 303 | "dist": { 304 | "type": "zip", 305 | "url": "https://api.github.com/repos/illuminate/support/zipball/3954928becd80579425d0105229ab3ec91b2afb7", 306 | "reference": "3954928becd80579425d0105229ab3ec91b2afb7", 307 | "shasum": "" 308 | }, 309 | "require": { 310 | "doctrine/inflector": "^2.0", 311 | "ext-ctype": "*", 312 | "ext-filter": "*", 313 | "ext-mbstring": "*", 314 | "illuminate/collections": "^9.0", 315 | "illuminate/conditionable": "^9.0", 316 | "illuminate/contracts": "^9.0", 317 | "illuminate/macroable": "^9.0", 318 | "nesbot/carbon": "^2.62.1", 319 | "php": "^8.0.2", 320 | "voku/portable-ascii": "^2.0" 321 | }, 322 | "conflict": { 323 | "tightenco/collect": "<5.5.33" 324 | }, 325 | "suggest": { 326 | "illuminate/filesystem": "Required to use the composer class (^9.0).", 327 | "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).", 328 | "ramsey/uuid": "Required to use Str::uuid() (^4.7).", 329 | "symfony/process": "Required to use the composer class (^6.0).", 330 | "symfony/uid": "Required to use Str::ulid() (^6.0).", 331 | "symfony/var-dumper": "Required to use the dd function (^6.0).", 332 | "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." 333 | }, 334 | "type": "library", 335 | "extra": { 336 | "branch-alias": { 337 | "dev-master": "9.x-dev" 338 | } 339 | }, 340 | "autoload": { 341 | "files": [ 342 | "helpers.php" 343 | ], 344 | "psr-4": { 345 | "Illuminate\\Support\\": "" 346 | } 347 | }, 348 | "notification-url": "https://packagist.org/downloads/", 349 | "license": [ 350 | "MIT" 351 | ], 352 | "authors": [ 353 | { 354 | "name": "Taylor Otwell", 355 | "email": "taylor@laravel.com" 356 | } 357 | ], 358 | "description": "The Illuminate Support package.", 359 | "homepage": "https://laravel.com", 360 | "support": { 361 | "issues": "https://github.com/laravel/framework/issues", 362 | "source": "https://github.com/laravel/framework" 363 | }, 364 | "time": "2023-02-08T14:37:20+00:00" 365 | }, 366 | { 367 | "name": "nesbot/carbon", 368 | "version": "dev-master", 369 | "source": { 370 | "type": "git", 371 | "url": "https://github.com/briannesbitt/Carbon.git", 372 | "reference": "496712849902241f04902033b0441b269effe001" 373 | }, 374 | "dist": { 375 | "type": "zip", 376 | "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/496712849902241f04902033b0441b269effe001", 377 | "reference": "496712849902241f04902033b0441b269effe001", 378 | "shasum": "" 379 | }, 380 | "require": { 381 | "ext-json": "*", 382 | "php": "^7.1.8 || ^8.0", 383 | "symfony/polyfill-mbstring": "^1.0", 384 | "symfony/polyfill-php80": "^1.16", 385 | "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" 386 | }, 387 | "require-dev": { 388 | "doctrine/dbal": "^2.0 || ^3.1.4", 389 | "doctrine/orm": "^2.7", 390 | "friendsofphp/php-cs-fixer": "^3.0", 391 | "kylekatarnls/multi-tester": "^2.0", 392 | "ondrejmirtes/better-reflection": "*", 393 | "phpmd/phpmd": "^2.9", 394 | "phpstan/extension-installer": "^1.0", 395 | "phpstan/phpstan": "^0.12.99 || ^1.7.14", 396 | "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", 397 | "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", 398 | "squizlabs/php_codesniffer": "^3.4" 399 | }, 400 | "default-branch": true, 401 | "bin": [ 402 | "bin/carbon" 403 | ], 404 | "type": "library", 405 | "extra": { 406 | "branch-alias": { 407 | "dev-3.x": "3.x-dev", 408 | "dev-master": "2.x-dev" 409 | }, 410 | "laravel": { 411 | "providers": [ 412 | "Carbon\\Laravel\\ServiceProvider" 413 | ] 414 | }, 415 | "phpstan": { 416 | "includes": [ 417 | "extension.neon" 418 | ] 419 | } 420 | }, 421 | "autoload": { 422 | "psr-4": { 423 | "Carbon\\": "src/Carbon/" 424 | } 425 | }, 426 | "notification-url": "https://packagist.org/downloads/", 427 | "license": [ 428 | "MIT" 429 | ], 430 | "authors": [ 431 | { 432 | "name": "Brian Nesbitt", 433 | "email": "brian@nesbot.com", 434 | "homepage": "https://markido.com" 435 | }, 436 | { 437 | "name": "kylekatarnls", 438 | "homepage": "https://github.com/kylekatarnls" 439 | } 440 | ], 441 | "description": "An API extension for DateTime that supports 281 different languages.", 442 | "homepage": "https://carbon.nesbot.com", 443 | "keywords": [ 444 | "date", 445 | "datetime", 446 | "time" 447 | ], 448 | "support": { 449 | "docs": "https://carbon.nesbot.com/docs", 450 | "issues": "https://github.com/briannesbitt/Carbon/issues", 451 | "source": "https://github.com/briannesbitt/Carbon" 452 | }, 453 | "funding": [ 454 | { 455 | "url": "https://github.com/sponsors/kylekatarnls", 456 | "type": "github" 457 | }, 458 | { 459 | "url": "https://opencollective.com/Carbon#sponsor", 460 | "type": "opencollective" 461 | }, 462 | { 463 | "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", 464 | "type": "tidelift" 465 | } 466 | ], 467 | "time": "2023-01-29T18:53:47+00:00" 468 | }, 469 | { 470 | "name": "psr/container", 471 | "version": "dev-master", 472 | "source": { 473 | "type": "git", 474 | "url": "https://github.com/php-fig/container.git", 475 | "reference": "90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5" 476 | }, 477 | "dist": { 478 | "type": "zip", 479 | "url": "https://api.github.com/repos/php-fig/container/zipball/90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5", 480 | "reference": "90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5", 481 | "shasum": "" 482 | }, 483 | "require": { 484 | "php": ">=7.4.0" 485 | }, 486 | "default-branch": true, 487 | "type": "library", 488 | "extra": { 489 | "branch-alias": { 490 | "dev-master": "2.0.x-dev" 491 | } 492 | }, 493 | "autoload": { 494 | "psr-4": { 495 | "Psr\\Container\\": "src/" 496 | } 497 | }, 498 | "notification-url": "https://packagist.org/downloads/", 499 | "license": [ 500 | "MIT" 501 | ], 502 | "authors": [ 503 | { 504 | "name": "PHP-FIG", 505 | "homepage": "https://www.php-fig.org/" 506 | } 507 | ], 508 | "description": "Common Container Interface (PHP FIG PSR-11)", 509 | "homepage": "https://github.com/php-fig/container", 510 | "keywords": [ 511 | "PSR-11", 512 | "container", 513 | "container-interface", 514 | "container-interop", 515 | "psr" 516 | ], 517 | "support": { 518 | "issues": "https://github.com/php-fig/container/issues", 519 | "source": "https://github.com/php-fig/container/tree/master" 520 | }, 521 | "time": "2022-07-19T17:36:59+00:00" 522 | }, 523 | { 524 | "name": "psr/simple-cache", 525 | "version": "dev-master", 526 | "source": { 527 | "type": "git", 528 | "url": "https://github.com/php-fig/simple-cache.git", 529 | "reference": "2d280c2aaa23a120f35d55cfde8581954a8e77fa" 530 | }, 531 | "dist": { 532 | "type": "zip", 533 | "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/2d280c2aaa23a120f35d55cfde8581954a8e77fa", 534 | "reference": "2d280c2aaa23a120f35d55cfde8581954a8e77fa", 535 | "shasum": "" 536 | }, 537 | "require": { 538 | "php": ">=8.0.0" 539 | }, 540 | "default-branch": true, 541 | "type": "library", 542 | "extra": { 543 | "branch-alias": { 544 | "dev-master": "3.0.x-dev" 545 | } 546 | }, 547 | "autoload": { 548 | "psr-4": { 549 | "Psr\\SimpleCache\\": "src/" 550 | } 551 | }, 552 | "notification-url": "https://packagist.org/downloads/", 553 | "license": [ 554 | "MIT" 555 | ], 556 | "authors": [ 557 | { 558 | "name": "PHP-FIG", 559 | "homepage": "https://www.php-fig.org/" 560 | } 561 | ], 562 | "description": "Common interfaces for simple caching", 563 | "keywords": [ 564 | "cache", 565 | "caching", 566 | "psr", 567 | "psr-16", 568 | "simple-cache" 569 | ], 570 | "support": { 571 | "source": "https://github.com/php-fig/simple-cache/tree/master" 572 | }, 573 | "time": "2022-04-08T16:41:45+00:00" 574 | }, 575 | { 576 | "name": "symfony/polyfill-mbstring", 577 | "version": "dev-main", 578 | "source": { 579 | "type": "git", 580 | "url": "https://github.com/symfony/polyfill-mbstring.git", 581 | "reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25" 582 | }, 583 | "dist": { 584 | "type": "zip", 585 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f9c7affe77a00ae32ca127ca6833d034e6d33f25", 586 | "reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25", 587 | "shasum": "" 588 | }, 589 | "require": { 590 | "php": ">=7.1" 591 | }, 592 | "provide": { 593 | "ext-mbstring": "*" 594 | }, 595 | "suggest": { 596 | "ext-mbstring": "For best performance" 597 | }, 598 | "default-branch": true, 599 | "type": "library", 600 | "extra": { 601 | "branch-alias": { 602 | "dev-main": "1.28-dev" 603 | }, 604 | "thanks": { 605 | "name": "symfony/polyfill", 606 | "url": "https://github.com/symfony/polyfill" 607 | } 608 | }, 609 | "autoload": { 610 | "files": [ 611 | "bootstrap.php" 612 | ], 613 | "psr-4": { 614 | "Symfony\\Polyfill\\Mbstring\\": "" 615 | } 616 | }, 617 | "notification-url": "https://packagist.org/downloads/", 618 | "license": [ 619 | "MIT" 620 | ], 621 | "authors": [ 622 | { 623 | "name": "Nicolas Grekas", 624 | "email": "p@tchwork.com" 625 | }, 626 | { 627 | "name": "Symfony Community", 628 | "homepage": "https://symfony.com/contributors" 629 | } 630 | ], 631 | "description": "Symfony polyfill for the Mbstring extension", 632 | "homepage": "https://symfony.com", 633 | "keywords": [ 634 | "compatibility", 635 | "mbstring", 636 | "polyfill", 637 | "portable", 638 | "shim" 639 | ], 640 | "support": { 641 | "source": "https://github.com/symfony/polyfill-mbstring/tree/main" 642 | }, 643 | "funding": [ 644 | { 645 | "url": "https://symfony.com/sponsor", 646 | "type": "custom" 647 | }, 648 | { 649 | "url": "https://github.com/fabpot", 650 | "type": "github" 651 | }, 652 | { 653 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 654 | "type": "tidelift" 655 | } 656 | ], 657 | "time": "2023-01-30T17:25:47+00:00" 658 | }, 659 | { 660 | "name": "symfony/polyfill-php80", 661 | "version": "dev-main", 662 | "source": { 663 | "type": "git", 664 | "url": "https://github.com/symfony/polyfill-php80.git", 665 | "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" 666 | }, 667 | "dist": { 668 | "type": "zip", 669 | "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", 670 | "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", 671 | "shasum": "" 672 | }, 673 | "require": { 674 | "php": ">=7.1" 675 | }, 676 | "default-branch": true, 677 | "type": "library", 678 | "extra": { 679 | "branch-alias": { 680 | "dev-main": "1.28-dev" 681 | }, 682 | "thanks": { 683 | "name": "symfony/polyfill", 684 | "url": "https://github.com/symfony/polyfill" 685 | } 686 | }, 687 | "autoload": { 688 | "files": [ 689 | "bootstrap.php" 690 | ], 691 | "psr-4": { 692 | "Symfony\\Polyfill\\Php80\\": "" 693 | }, 694 | "classmap": [ 695 | "Resources/stubs" 696 | ] 697 | }, 698 | "notification-url": "https://packagist.org/downloads/", 699 | "license": [ 700 | "MIT" 701 | ], 702 | "authors": [ 703 | { 704 | "name": "Ion Bazan", 705 | "email": "ion.bazan@gmail.com" 706 | }, 707 | { 708 | "name": "Nicolas Grekas", 709 | "email": "p@tchwork.com" 710 | }, 711 | { 712 | "name": "Symfony Community", 713 | "homepage": "https://symfony.com/contributors" 714 | } 715 | ], 716 | "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", 717 | "homepage": "https://symfony.com", 718 | "keywords": [ 719 | "compatibility", 720 | "polyfill", 721 | "portable", 722 | "shim" 723 | ], 724 | "support": { 725 | "source": "https://github.com/symfony/polyfill-php80/tree/main" 726 | }, 727 | "funding": [ 728 | { 729 | "url": "https://symfony.com/sponsor", 730 | "type": "custom" 731 | }, 732 | { 733 | "url": "https://github.com/fabpot", 734 | "type": "github" 735 | }, 736 | { 737 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 738 | "type": "tidelift" 739 | } 740 | ], 741 | "time": "2023-01-26T09:26:14+00:00" 742 | }, 743 | { 744 | "name": "symfony/translation", 745 | "version": "6.3.x-dev", 746 | "source": { 747 | "type": "git", 748 | "url": "https://github.com/symfony/translation.git", 749 | "reference": "3bb4f55af1296081299faa201fc8cfba05198417" 750 | }, 751 | "dist": { 752 | "type": "zip", 753 | "url": "https://api.github.com/repos/symfony/translation/zipball/3bb4f55af1296081299faa201fc8cfba05198417", 754 | "reference": "3bb4f55af1296081299faa201fc8cfba05198417", 755 | "shasum": "" 756 | }, 757 | "require": { 758 | "php": ">=8.1", 759 | "symfony/polyfill-mbstring": "~1.0", 760 | "symfony/translation-contracts": "^2.5|^3.0" 761 | }, 762 | "conflict": { 763 | "symfony/config": "<5.4", 764 | "symfony/console": "<5.4", 765 | "symfony/dependency-injection": "<5.4", 766 | "symfony/http-client-contracts": "<2.5", 767 | "symfony/http-kernel": "<5.4", 768 | "symfony/service-contracts": "<2.5", 769 | "symfony/twig-bundle": "<5.4", 770 | "symfony/yaml": "<5.4" 771 | }, 772 | "provide": { 773 | "symfony/translation-implementation": "2.3|3.0" 774 | }, 775 | "require-dev": { 776 | "nikic/php-parser": "^4.13", 777 | "psr/log": "^1|^2|^3", 778 | "symfony/config": "^5.4|^6.0", 779 | "symfony/console": "^5.4|^6.0", 780 | "symfony/dependency-injection": "^5.4|^6.0", 781 | "symfony/finder": "^5.4|^6.0", 782 | "symfony/http-client-contracts": "^2.5|^3.0", 783 | "symfony/http-kernel": "^5.4|^6.0", 784 | "symfony/intl": "^5.4|^6.0", 785 | "symfony/polyfill-intl-icu": "^1.21", 786 | "symfony/routing": "^5.4|^6.0", 787 | "symfony/service-contracts": "^2.5|^3", 788 | "symfony/yaml": "^5.4|^6.0" 789 | }, 790 | "suggest": { 791 | "nikic/php-parser": "To use PhpAstExtractor", 792 | "psr/log-implementation": "To use logging capability in translator", 793 | "symfony/config": "", 794 | "symfony/yaml": "" 795 | }, 796 | "type": "library", 797 | "autoload": { 798 | "files": [ 799 | "Resources/functions.php" 800 | ], 801 | "psr-4": { 802 | "Symfony\\Component\\Translation\\": "" 803 | }, 804 | "exclude-from-classmap": [ 805 | "/Tests/" 806 | ] 807 | }, 808 | "notification-url": "https://packagist.org/downloads/", 809 | "license": [ 810 | "MIT" 811 | ], 812 | "authors": [ 813 | { 814 | "name": "Fabien Potencier", 815 | "email": "fabien@symfony.com" 816 | }, 817 | { 818 | "name": "Symfony Community", 819 | "homepage": "https://symfony.com/contributors" 820 | } 821 | ], 822 | "description": "Provides tools to internationalize your application", 823 | "homepage": "https://symfony.com", 824 | "support": { 825 | "source": "https://github.com/symfony/translation/tree/6.3" 826 | }, 827 | "funding": [ 828 | { 829 | "url": "https://symfony.com/sponsor", 830 | "type": "custom" 831 | }, 832 | { 833 | "url": "https://github.com/fabpot", 834 | "type": "github" 835 | }, 836 | { 837 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 838 | "type": "tidelift" 839 | } 840 | ], 841 | "time": "2023-02-07T09:49:56+00:00" 842 | }, 843 | { 844 | "name": "symfony/translation-contracts", 845 | "version": "dev-main", 846 | "source": { 847 | "type": "git", 848 | "url": "https://github.com/symfony/translation-contracts.git", 849 | "reference": "68cce71402305a015f8c1589bfada1280dc64fe7" 850 | }, 851 | "dist": { 852 | "type": "zip", 853 | "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/68cce71402305a015f8c1589bfada1280dc64fe7", 854 | "reference": "68cce71402305a015f8c1589bfada1280dc64fe7", 855 | "shasum": "" 856 | }, 857 | "require": { 858 | "php": ">=8.1" 859 | }, 860 | "suggest": { 861 | "symfony/translation-implementation": "" 862 | }, 863 | "default-branch": true, 864 | "type": "library", 865 | "extra": { 866 | "branch-alias": { 867 | "dev-main": "3.3-dev" 868 | }, 869 | "thanks": { 870 | "name": "symfony/contracts", 871 | "url": "https://github.com/symfony/contracts" 872 | } 873 | }, 874 | "autoload": { 875 | "psr-4": { 876 | "Symfony\\Contracts\\Translation\\": "" 877 | }, 878 | "exclude-from-classmap": [ 879 | "/Test/" 880 | ] 881 | }, 882 | "notification-url": "https://packagist.org/downloads/", 883 | "license": [ 884 | "MIT" 885 | ], 886 | "authors": [ 887 | { 888 | "name": "Nicolas Grekas", 889 | "email": "p@tchwork.com" 890 | }, 891 | { 892 | "name": "Symfony Community", 893 | "homepage": "https://symfony.com/contributors" 894 | } 895 | ], 896 | "description": "Generic abstractions related to translation", 897 | "homepage": "https://symfony.com", 898 | "keywords": [ 899 | "abstractions", 900 | "contracts", 901 | "decoupling", 902 | "interfaces", 903 | "interoperability", 904 | "standards" 905 | ], 906 | "support": { 907 | "source": "https://github.com/symfony/translation-contracts/tree/v3.2.0" 908 | }, 909 | "funding": [ 910 | { 911 | "url": "https://symfony.com/sponsor", 912 | "type": "custom" 913 | }, 914 | { 915 | "url": "https://github.com/fabpot", 916 | "type": "github" 917 | }, 918 | { 919 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 920 | "type": "tidelift" 921 | } 922 | ], 923 | "time": "2022-11-25T10:21:52+00:00" 924 | }, 925 | { 926 | "name": "voku/portable-ascii", 927 | "version": "2.0.1", 928 | "source": { 929 | "type": "git", 930 | "url": "https://github.com/voku/portable-ascii.git", 931 | "reference": "b56450eed252f6801410d810c8e1727224ae0743" 932 | }, 933 | "dist": { 934 | "type": "zip", 935 | "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b56450eed252f6801410d810c8e1727224ae0743", 936 | "reference": "b56450eed252f6801410d810c8e1727224ae0743", 937 | "shasum": "" 938 | }, 939 | "require": { 940 | "php": ">=7.0.0" 941 | }, 942 | "require-dev": { 943 | "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" 944 | }, 945 | "suggest": { 946 | "ext-intl": "Use Intl for transliterator_transliterate() support" 947 | }, 948 | "type": "library", 949 | "autoload": { 950 | "psr-4": { 951 | "voku\\": "src/voku/" 952 | } 953 | }, 954 | "notification-url": "https://packagist.org/downloads/", 955 | "license": [ 956 | "MIT" 957 | ], 958 | "authors": [ 959 | { 960 | "name": "Lars Moelleken", 961 | "homepage": "http://www.moelleken.org/" 962 | } 963 | ], 964 | "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", 965 | "homepage": "https://github.com/voku/portable-ascii", 966 | "keywords": [ 967 | "ascii", 968 | "clean", 969 | "php" 970 | ], 971 | "support": { 972 | "issues": "https://github.com/voku/portable-ascii/issues", 973 | "source": "https://github.com/voku/portable-ascii/tree/2.0.1" 974 | }, 975 | "funding": [ 976 | { 977 | "url": "https://www.paypal.me/moelleken", 978 | "type": "custom" 979 | }, 980 | { 981 | "url": "https://github.com/voku", 982 | "type": "github" 983 | }, 984 | { 985 | "url": "https://opencollective.com/portable-ascii", 986 | "type": "open_collective" 987 | }, 988 | { 989 | "url": "https://www.patreon.com/voku", 990 | "type": "patreon" 991 | }, 992 | { 993 | "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", 994 | "type": "tidelift" 995 | } 996 | ], 997 | "time": "2022-03-08T17:03:00+00:00" 998 | } 999 | ], 1000 | "packages-dev": [], 1001 | "aliases": [], 1002 | "minimum-stability": "dev", 1003 | "stability-flags": [], 1004 | "prefer-stable": false, 1005 | "prefer-lowest": false, 1006 | "platform": { 1007 | "php": "^8.0" 1008 | }, 1009 | "platform-dev": [], 1010 | "plugin-api-version": "2.3.0" 1011 | } 1012 | -------------------------------------------------------------------------------- /src/Controllers/PlaywrightController.php: -------------------------------------------------------------------------------- 1 | getRoutes()) 16 | ->map(function (\Illuminate\Routing\Route $route) { 17 | return [ 18 | 'name' => $route->getName(), 19 | 'domain' => $route->getDomain(), 20 | 'action' => $route->getActionName(), 21 | 'uri' => $route->uri(), 22 | 'method' => $route->methods(), 23 | ]; 24 | }) 25 | ->keyBy('name'); 26 | } 27 | 28 | public function login(Request $request) 29 | { 30 | $attributes = $request->input('attributes', []); 31 | 32 | if (empty($attributes)) { 33 | $user = $this->factoryBuilder( 34 | $this->userClassName(), 35 | $request->input('state', []) 36 | )->create(); 37 | } else { 38 | $user = app($this->userClassName()) 39 | ->newQuery() 40 | ->where($attributes) 41 | ->first(); 42 | 43 | if (!$user) { 44 | $user = $this->factoryBuilder( 45 | $this->userClassName(), 46 | $request->input('state', []) 47 | )->create($attributes); 48 | } 49 | } 50 | 51 | $user->load($request->input('load', [])); 52 | 53 | return tap($user, function ($user) { 54 | auth()->login($user); 55 | 56 | $user->setHidden([])->setVisible([]); 57 | }); 58 | } 59 | 60 | public function currentUser() 61 | { 62 | return auth()->user()?->setHidden([])->setVisible([]); 63 | } 64 | 65 | public function logout() 66 | { 67 | auth()->logout(); 68 | } 69 | 70 | public function factory(Request $request) 71 | { 72 | return $this->factoryBuilder( 73 | $request->input('model'), 74 | $request->input('state', []) 75 | ) 76 | ->count(intval($request->input('count', 1))) 77 | ->create($request->input('attributes')) 78 | ->each(fn($model) => $model->setHidden([])->setVisible([])) 79 | ->load($request->input('load', [])) 80 | ->pipe(function ($collection) { 81 | return $collection->count() > 1 82 | ? $collection 83 | : $collection->first(); 84 | }); 85 | } 86 | 87 | public function artisan(Request $request) 88 | { 89 | Artisan::call( 90 | $request->input('command'), 91 | $request->input('parameters', []) 92 | ); 93 | } 94 | 95 | public function csrfToken() 96 | { 97 | return response()->json(csrf_token()); 98 | } 99 | 100 | public function runPhp(Request $request) 101 | { 102 | $code = $request->input('command'); 103 | 104 | if ($code[-1] !== ';') { 105 | $code .= ';'; 106 | } 107 | 108 | if (!Str::contains($code, 'return')) { 109 | $code = 'return ' . $code; 110 | } 111 | 112 | return response()->json([ 113 | 'result' => eval($code), 114 | ]); 115 | } 116 | 117 | protected function userClassName() 118 | { 119 | return config('auth.providers.users.model'); 120 | } 121 | 122 | protected function factoryBuilder($model, $states = []) 123 | { 124 | $factory = $model::factory(); 125 | 126 | $states = Arr::wrap($states); 127 | 128 | foreach ($states as $state => $attributes) { 129 | if (is_int($state)) { 130 | $state = $attributes; 131 | $attributes = []; 132 | } 133 | 134 | $attributes = Arr::wrap($attributes); 135 | 136 | $factory = $factory->{$state}(...$attributes); 137 | } 138 | 139 | return $factory; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/PlaywrightBoilerplateCommand.php: -------------------------------------------------------------------------------- 1 | isPlaywrightInstalled()) { 40 | $this->requirePlaywrightInstall(); 41 | 42 | return; 43 | } 44 | 45 | $playwrightPaths = [ 46 | 'e2e' => (int)$this->files->exists(base_path('e2e')), 47 | 'tests/e2e' => (int)$this->files->exists(base_path('tests/e2e')), 48 | 'playwright' => (int)$this->files->exists(base_path('playwright')), 49 | 'tests/playwright' => (int)$this->files->exists(base_path('tests/playwright')), 50 | ]; 51 | $playwrightPathDefault = array_flip($playwrightPaths)[true] ?? 'e2e'; 52 | 53 | $this->playwrightPath = trim( 54 | strtolower($this->ask('Where should we put the playwright directory? It should be the same directory you choose in the playwright installation wizard', $playwrightPathDefault)), 55 | '/' 56 | ); 57 | 58 | $this->copyStubs(); 59 | } 60 | 61 | /** 62 | * Copy the stubs from this package to the user's playwright folder. 63 | */ 64 | protected function copyStubs(): void 65 | { 66 | $this->files->copyDirectory(__DIR__ . '/stubs', $this->playwrightPath()); 67 | 68 | $this->status('Writing', $this->playwrightPath('laravel-helpers.ts', false)); 69 | $this->status('Writing', $this->playwrightPath('laravel-examples.spec.ts', false)); 70 | 71 | $this->line(''); 72 | } 73 | 74 | /** 75 | * Get the user-requested path to the Playwright directory. 76 | */ 77 | protected function playwrightPath(string $path = '', bool $absolute = true): string 78 | { 79 | $playwrightPath = $absolute ? base_path($this->playwrightPath) : $this->playwrightPath; 80 | 81 | return $playwrightPath . ($path ? DIRECTORY_SEPARATOR . $path : ''); 82 | } 83 | 84 | /** 85 | * Report the status of a file to the user. 86 | */ 87 | protected function status(string $type, string $file) 88 | { 89 | $this->line("{$type} {$file}"); 90 | } 91 | 92 | /** 93 | * Require that the user first install playwright through npm. 94 | */ 95 | protected function requirePlaywrightInstall() 96 | { 97 | $this->warn( 98 | <<<'EOT' 99 | 100 | Playwright not found. Please install it through npm and try again. 101 | 102 | npm init playwright@latest 103 | yarn create playwright 104 | 105 | EOT 106 | ); 107 | } 108 | 109 | /** 110 | * Check if Playwright is added to the package.json file. 111 | */ 112 | protected function isPlaywrightInstalled(): bool 113 | { 114 | $package = json_decode($this->files->get(base_path('package.json')), true); 115 | 116 | return Arr::get($package, 'devDependencies.@playwright/test') || Arr::get($package, 'dependencies.@playwright/test'); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/PlaywrightServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->environment('production')) { 13 | return; 14 | } 15 | 16 | $this->addRoutes(); 17 | 18 | if ($this->app->runningInConsole()) { 19 | $this->publishes([ 20 | __DIR__.'/routes/playwright.php' => base_path('routes/playwright.php'), 21 | ]); 22 | 23 | $this->commands([ 24 | PlaywrightBoilerplateCommand::class, 25 | ]); 26 | } 27 | } 28 | 29 | protected function addRoutes() 30 | { 31 | Route::namespace('') 32 | ->middleware('web') 33 | ->group(__DIR__.'/routes/playwright.php'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/routes/playwright.php: -------------------------------------------------------------------------------- 1 | name('playwright.factory'); 7 | Route::post('/__playwright__/login', [PlaywrightController::class, 'login'])->name('playwright.login'); 8 | Route::post('/__playwright__/logout', [PlaywrightController::class, 'logout'])->name('playwright.logout'); 9 | Route::post('/__playwright__/artisan', [PlaywrightController::class, 'artisan'])->name('playwright.artisan'); 10 | Route::post('/__playwright__/run-php', [PlaywrightController::class, 'runPhp'])->name('playwright.run-php'); 11 | Route::get('/__playwright__/csrf_token', [PlaywrightController::class, 'csrfToken'])->name('playwright.csrf-token'); 12 | Route::post('/__playwright__/routes', [PlaywrightController::class, 'routes'])->name('playwright.routes'); 13 | Route::post('/__playwright__/current-user', [PlaywrightController::class, 'currentUser'])->name('playwright.current-user'); 14 | -------------------------------------------------------------------------------- /src/stubs/laravel-examples.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import { create, currentUser, login, logout, php, refreshDatabase } from './laravel-helpers' 3 | 4 | test('Can create a new user and log them in', async ({ page }) => { 5 | const user1 = await login({ page }) 6 | expect(user1.name).toBeDefined() 7 | const user2 = await login({ page, attributes: { email: 'yoann@web-id.fr' } }) 8 | expect(user2.name).toEqual('Yoann') 9 | const user3 = await login({ 10 | page, 11 | attributes: { email: 'new@user.fr', name: 'New user' }, 12 | }) 13 | expect(user3.email).toEqual('new@user.fr') 14 | }) 15 | 16 | test('Can logout the current user', async ({ page }) => { 17 | await login({ page }) 18 | const userBefore = await currentUser({ page }) 19 | expect(userBefore.name).toBeDefined() 20 | await logout({ page }) 21 | const userAfter = await currentUser({ page }) 22 | expect(userAfter).toBeNull() 23 | }) 24 | 25 | test.only('Can execute arbitrary PHP', async ({ page }) => { 26 | await refreshDatabase({ page, parameters: { '--seed': true } }) 27 | const sum = await php({ page, command: '2+2' }) 28 | expect(sum).toEqual(4) 29 | const userCount = await php({ page, command: 'App\\Models\\User::count()' }) 30 | expect(userCount).toEqual(1) 31 | await page.pause() 32 | await create({ page, model: 'App\\Models\\User', count: 2 }) 33 | const userCountCreated = await php({ 34 | page, 35 | command: 'App\\Models\\User::count()', 36 | }) 37 | expect(userCountCreated).toEqual(3) 38 | }) 39 | -------------------------------------------------------------------------------- /src/stubs/laravel-helpers.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test' 2 | 3 | export type CsrfTokenProps = { 4 | page: Page 5 | } 6 | 7 | export async function csrfToken({ page }: CsrfTokenProps) { 8 | const response = await page.request.get('/__playwright__/csrf_token', { headers: { Accept: 'application/json' } }) 9 | return await response.json() 10 | } 11 | 12 | export type LoginProps = { 13 | page: Page 14 | attributes?: object 15 | } 16 | 17 | /** 18 | * Create a new user and log them in. 19 | * 20 | * @example login({page}) 21 | * login({page, attributes: {email: 'yoann@web-id.fr'}}) 22 | * login({page, attributes: {email: 'new@user.fr', name: 'New user'}}) 23 | */ 24 | export async function login({ page, attributes }: LoginProps) { 25 | const token = await csrfToken({ page }) 26 | 27 | const response = await page.request.post('/__playwright__/login', { 28 | headers: { Accept: 'application/json' }, 29 | data: { 30 | _token: token, 31 | attributes, 32 | }, 33 | }) 34 | return await response.json() 35 | } 36 | 37 | export type CurrentUserProps = { 38 | page: Page 39 | } 40 | 41 | /** 42 | * Fetch the currently authenticated user object. 43 | * 44 | * @example currentUser({page}) 45 | */ 46 | export async function currentUser({ page }: CurrentUserProps) { 47 | const token = await csrfToken({ page }) 48 | const response = await page.request.post('/__playwright__/current-user', { 49 | headers: { Accept: 'application/json' }, 50 | data: { _token: token }, 51 | }) 52 | const body = await response.text() 53 | if (!body) { 54 | console.log('No authenticated user found.') 55 | return null 56 | } 57 | return await response.json() 58 | } 59 | 60 | export type LogoutProps = { 61 | page: Page 62 | } 63 | 64 | /** 65 | * Logout the current user. 66 | * 67 | * @example logout({page}) 68 | */ 69 | export async function logout({ page }: LogoutProps) { 70 | const token = await csrfToken({ page }) 71 | const response = await page.request.post('/__playwright__/logout', { 72 | headers: { Accept: 'application/json' }, 73 | data: { _token: token }, 74 | }) 75 | console.log(await response.text()) 76 | return await response.text() 77 | } 78 | 79 | export type CreateProps = { 80 | page: Page 81 | model: string 82 | count?: number 83 | attributes?: object 84 | load?: string[] 85 | state?: string[] 86 | } 87 | 88 | /** 89 | * Create a new Eloquent factory. 90 | * 91 | * @param {String} model 92 | * @param {Number|null} times 93 | * @param {Object} attributes 94 | * 95 | * @example create({page, model: 'App\\Models\\User'}); 96 | * create({page, model: 'App\\Models\\User', count: 2}); 97 | * create({page, model: 'App\\Models\\User', attributes: { active: false }}); 98 | * create({page, model: 'App\\Models\\User', count: 2, attributes: { active: false }}); 99 | * create({page, model: 'App\\Models\\User', count: 2, attributes: { active: false }, load: ['profile']}); 100 | * create({page, model: 'App\\Models\\User', count: 2, attributes: { active: false }, load: ['profile'], state: ['guest']}); 101 | * create({page, model: 'App\\Models\\User', attributes: { active: false }, load: ['profile']); 102 | * create({page, model: 'App\\Models\\User', attributes: { active: false }, load: ['profile'], ['guest']); 103 | * create({page, model: 'App\\Models\\User', load: ['profile']}); 104 | * create({page, model: 'App\\Models\\User', load: ['profile'], state: ['guest']}); 105 | */ 106 | export async function create({ page, model, count = 1, attributes = {}, load = [], state = [] }: CreateProps) { 107 | const token = await csrfToken({ page }) 108 | const response = await page.request.post('/__playwright__/factory', { 109 | headers: { Accept: 'application/json' }, 110 | data: { _token: token, model, count, attributes, load, state }, 111 | }) 112 | return await response.json() 113 | } 114 | 115 | export type ArtisanProps = { 116 | page: Page 117 | command: string 118 | parameters: object 119 | } 120 | 121 | /** 122 | * Trigger an Artisan command. 123 | * 124 | * @example artisan({page, command: 'cache:clear'}); 125 | */ 126 | export async function artisan({ page, command, parameters = {} }: ArtisanProps) { 127 | const token = await csrfToken({ page }) 128 | console.log( 129 | [ 130 | command, 131 | Object.entries(parameters) 132 | .map(([k, v]) => `${k}="${v}"`) 133 | .join(' '), 134 | ].join(' ') 135 | ) 136 | return await page.request.post('/__playwright__/artisan', { 137 | headers: { Accept: 'application/json' }, 138 | data: { _token: token, command, parameters }, 139 | }) 140 | } 141 | 142 | export type RefreshDatabaseProps = { 143 | page: Page 144 | parameters?: object 145 | } 146 | 147 | /** 148 | * Refresh the database state. 149 | ** 150 | * @example refreshDatabase({page}); 151 | * refreshDatabase({page, parameters: {'--drop-views': true}}); 152 | */ 153 | export async function refreshDatabase({ page, parameters = {} }: RefreshDatabaseProps) { 154 | return await artisan({ page, command: 'migrate:fresh', parameters }) 155 | } 156 | 157 | export type SeedProps = { 158 | page: Page 159 | seederClass?: string 160 | } 161 | 162 | /** 163 | * Seed the database. 164 | * 165 | * @example seed({page}); 166 | * seed({page, seederClass: 'PlansTableSeeder'}); 167 | */ 168 | export async function seed({ page, seederClass = '' }: SeedProps) { 169 | const parameters = {} 170 | 171 | if (seederClass) { 172 | parameters['--class'] = seederClass 173 | } 174 | return await artisan({ page, command: 'db:seed', parameters }) 175 | } 176 | 177 | export type PhpProps = { 178 | page: Page 179 | command: string 180 | } 181 | 182 | /** 183 | * Execute arbitrary PHP. 184 | * 185 | * 186 | * @example php({page, command: '2 + 2'}) 187 | * php({page, command: 'App\\Model\\User::count()'}) 188 | */ 189 | export async function php({ page, command }: PhpProps) { 190 | const token = await csrfToken({ page }) 191 | const response = await page.request.post('/__playwright__/run-php', { 192 | headers: { Accept: 'application/json' }, 193 | data: { _token: token, command }, 194 | }) 195 | const json = await response.json() 196 | return json.result ?? json 197 | } 198 | --------------------------------------------------------------------------------