├── Assert.php ├── AssertableJsonString.php ├── Concerns ├── AssertsStatusCodes.php ├── RunsInParallel.php └── TestDatabases.php ├── Constraints ├── ArraySubset.php ├── CountInDatabase.php ├── HasInDatabase.php ├── NotSoftDeletedInDatabase.php ├── SeeInOrder.php └── SoftDeletedInDatabase.php ├── Exceptions └── InvalidArgumentException.php ├── Fluent ├── AssertableJson.php └── Concerns │ ├── Debugging.php │ ├── Has.php │ ├── Interaction.php │ └── Matching.php ├── LICENSE.md ├── LoggedExceptionCollection.php ├── ParallelConsoleOutput.php ├── ParallelRunner.php ├── ParallelTesting.php ├── ParallelTestingServiceProvider.php ├── PendingCommand.php ├── TestComponent.php ├── TestResponse.php ├── TestResponseAssert.php ├── TestView.php └── composer.json /Assert.php: -------------------------------------------------------------------------------- 1 | json = $jsonable; 41 | 42 | if ($jsonable instanceof JsonSerializable) { 43 | $this->decoded = $jsonable->jsonSerialize(); 44 | } elseif ($jsonable instanceof Jsonable) { 45 | $this->decoded = json_decode($jsonable->toJson(), true); 46 | } elseif (is_array($jsonable)) { 47 | $this->decoded = $jsonable; 48 | } else { 49 | $this->decoded = json_decode($jsonable, true); 50 | } 51 | } 52 | 53 | /** 54 | * Validate and return the decoded response JSON. 55 | * 56 | * @param string|null $key 57 | * @return mixed 58 | */ 59 | public function json($key = null) 60 | { 61 | return data_get($this->decoded, $key); 62 | } 63 | 64 | /** 65 | * Assert that the response JSON has the expected count of items at the given key. 66 | * 67 | * @param int $count 68 | * @param string|null $key 69 | * @return $this 70 | */ 71 | public function assertCount(int $count, $key = null) 72 | { 73 | if (! is_null($key)) { 74 | PHPUnit::assertCount( 75 | $count, data_get($this->decoded, $key), 76 | "Failed to assert that the response count matched the expected {$count}" 77 | ); 78 | 79 | return $this; 80 | } 81 | 82 | PHPUnit::assertCount($count, 83 | $this->decoded, 84 | "Failed to assert that the response count matched the expected {$count}" 85 | ); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Assert that the response has the exact given JSON. 92 | * 93 | * @param array $data 94 | * @return $this 95 | */ 96 | public function assertExact(array $data) 97 | { 98 | $actual = $this->reorderAssocKeys((array) $this->decoded); 99 | 100 | $expected = $this->reorderAssocKeys($data); 101 | 102 | PHPUnit::assertEquals( 103 | json_encode($expected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), 104 | json_encode($actual, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) 105 | ); 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Assert that the response has the similar JSON as given. 112 | * 113 | * @param array $data 114 | * @return $this 115 | */ 116 | public function assertSimilar(array $data) 117 | { 118 | $actual = json_encode( 119 | Arr::sortRecursive((array) $this->decoded), 120 | JSON_UNESCAPED_UNICODE 121 | ); 122 | 123 | PHPUnit::assertEquals(json_encode(Arr::sortRecursive($data), JSON_UNESCAPED_UNICODE), $actual); 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Assert that the response contains the given JSON fragment. 130 | * 131 | * @param array $data 132 | * @return $this 133 | */ 134 | public function assertFragment(array $data) 135 | { 136 | $actual = json_encode( 137 | Arr::sortRecursive((array) $this->decoded), 138 | JSON_UNESCAPED_UNICODE 139 | ); 140 | 141 | foreach (Arr::sortRecursive($data) as $key => $value) { 142 | $expected = $this->jsonSearchStrings($key, $value); 143 | 144 | PHPUnit::assertTrue( 145 | Str::contains($actual, $expected), 146 | 'Unable to find JSON fragment: '.PHP_EOL.PHP_EOL. 147 | '['.json_encode([$key => $value], JSON_UNESCAPED_UNICODE).']'.PHP_EOL.PHP_EOL. 148 | 'within'.PHP_EOL.PHP_EOL. 149 | "[{$actual}]." 150 | ); 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * Assert that the response does not contain the given JSON fragment. 158 | * 159 | * @param array $data 160 | * @param bool $exact 161 | * @return $this 162 | */ 163 | public function assertMissing(array $data, $exact = false) 164 | { 165 | if ($exact) { 166 | return $this->assertMissingExact($data); 167 | } 168 | 169 | $actual = json_encode( 170 | Arr::sortRecursive((array) $this->decoded), 171 | JSON_UNESCAPED_UNICODE 172 | ); 173 | 174 | foreach (Arr::sortRecursive($data) as $key => $value) { 175 | $unexpected = $this->jsonSearchStrings($key, $value); 176 | 177 | PHPUnit::assertFalse( 178 | Str::contains($actual, $unexpected), 179 | 'Found unexpected JSON fragment: '.PHP_EOL.PHP_EOL. 180 | '['.json_encode([$key => $value], JSON_UNESCAPED_UNICODE).']'.PHP_EOL.PHP_EOL. 181 | 'within'.PHP_EOL.PHP_EOL. 182 | "[{$actual}]." 183 | ); 184 | } 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * Assert that the response does not contain the exact JSON fragment. 191 | * 192 | * @param array $data 193 | * @return $this 194 | */ 195 | public function assertMissingExact(array $data) 196 | { 197 | $actual = json_encode( 198 | Arr::sortRecursive((array) $this->decoded), 199 | JSON_UNESCAPED_UNICODE 200 | ); 201 | 202 | foreach (Arr::sortRecursive($data) as $key => $value) { 203 | $unexpected = $this->jsonSearchStrings($key, $value); 204 | 205 | if (! Str::contains($actual, $unexpected)) { 206 | return $this; 207 | } 208 | } 209 | 210 | PHPUnit::fail( 211 | 'Found unexpected JSON fragment: '.PHP_EOL.PHP_EOL. 212 | '['.json_encode($data, JSON_UNESCAPED_UNICODE).']'.PHP_EOL.PHP_EOL. 213 | 'within'.PHP_EOL.PHP_EOL. 214 | "[{$actual}]." 215 | ); 216 | } 217 | 218 | /** 219 | * Assert that the response does not contain the given path. 220 | * 221 | * @param string $path 222 | * @return $this 223 | */ 224 | public function assertMissingPath($path) 225 | { 226 | PHPUnit::assertFalse(Arr::has($this->json(), $path)); 227 | 228 | return $this; 229 | } 230 | 231 | /** 232 | * Assert that the expected value and type exists at the given path in the response. 233 | * 234 | * @param string $path 235 | * @param mixed $expect 236 | * @return $this 237 | */ 238 | public function assertPath($path, $expect) 239 | { 240 | if ($expect instanceof Closure) { 241 | PHPUnit::assertTrue($expect($this->json($path))); 242 | } else { 243 | PHPUnit::assertSame(enum_value($expect), $this->json($path)); 244 | } 245 | 246 | return $this; 247 | } 248 | 249 | /** 250 | * Assert that the given path in the response contains all of the expected values without looking at the order. 251 | * 252 | * @param string $path 253 | * @param array $expect 254 | * @return $this 255 | */ 256 | public function assertPathCanonicalizing($path, $expect) 257 | { 258 | PHPUnit::assertEqualsCanonicalizing($expect, $this->json($path)); 259 | 260 | return $this; 261 | } 262 | 263 | /** 264 | * Assert that the response has a given JSON structure. 265 | * 266 | * @param array|null $structure 267 | * @param array|null $responseData 268 | * @param bool $exact 269 | * @return $this 270 | */ 271 | public function assertStructure(?array $structure = null, $responseData = null, bool $exact = false) 272 | { 273 | if (is_null($structure)) { 274 | return $this->assertSimilar($this->decoded); 275 | } 276 | 277 | if (! is_null($responseData)) { 278 | return (new static($responseData))->assertStructure($structure, null, $exact); 279 | } 280 | 281 | if ($exact) { 282 | PHPUnit::assertIsArray($this->decoded); 283 | 284 | $keys = (new Collection($structure))->map(fn ($value, $key) => is_array($value) ? $key : $value)->values(); 285 | 286 | if ($keys->all() !== ['*']) { 287 | PHPUnit::assertEquals($keys->sort()->values()->all(), (new Collection($this->decoded))->keys()->sort()->values()->all()); 288 | } 289 | } 290 | 291 | foreach ($structure as $key => $value) { 292 | if (is_array($value) && $key === '*') { 293 | PHPUnit::assertIsArray($this->decoded); 294 | 295 | foreach ($this->decoded as $responseDataItem) { 296 | $this->assertStructure($structure['*'], $responseDataItem, $exact); 297 | } 298 | } elseif (is_array($value)) { 299 | PHPUnit::assertArrayHasKey($key, $this->decoded); 300 | 301 | $this->assertStructure($structure[$key], $this->decoded[$key], $exact); 302 | } else { 303 | PHPUnit::assertArrayHasKey($value, $this->decoded); 304 | } 305 | } 306 | 307 | return $this; 308 | } 309 | 310 | /** 311 | * Assert that the response is a superset of the given JSON. 312 | * 313 | * @param array $data 314 | * @param bool $strict 315 | * @return $this 316 | */ 317 | public function assertSubset(array $data, $strict = false) 318 | { 319 | PHPUnit::assertArraySubset( 320 | $data, $this->decoded, $strict, $this->assertJsonMessage($data) 321 | ); 322 | 323 | return $this; 324 | } 325 | 326 | /** 327 | * Reorder associative array keys to make it easy to compare arrays. 328 | * 329 | * @param array $data 330 | * @return array 331 | */ 332 | protected function reorderAssocKeys(array $data) 333 | { 334 | $data = Arr::dot($data); 335 | ksort($data); 336 | 337 | $result = []; 338 | 339 | foreach ($data as $key => $value) { 340 | Arr::set($result, $key, $value); 341 | } 342 | 343 | return $result; 344 | } 345 | 346 | /** 347 | * Get the assertion message for assertJson. 348 | * 349 | * @param array $data 350 | * @return string 351 | */ 352 | protected function assertJsonMessage(array $data) 353 | { 354 | $expected = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 355 | 356 | $actual = json_encode($this->decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 357 | 358 | return 'Unable to find JSON: '.PHP_EOL.PHP_EOL. 359 | "[{$expected}]".PHP_EOL.PHP_EOL. 360 | 'within response JSON:'.PHP_EOL.PHP_EOL. 361 | "[{$actual}].".PHP_EOL.PHP_EOL; 362 | } 363 | 364 | /** 365 | * Get the strings we need to search for when examining the JSON. 366 | * 367 | * @param string $key 368 | * @param string $value 369 | * @return array 370 | */ 371 | protected function jsonSearchStrings($key, $value) 372 | { 373 | $needle = Str::substr(json_encode([$key => $value], JSON_UNESCAPED_UNICODE), 1, -1); 374 | 375 | return [ 376 | $needle.']', 377 | $needle.'}', 378 | $needle.',', 379 | ]; 380 | } 381 | 382 | /** 383 | * Get the total number of items in the underlying JSON array. 384 | * 385 | * @return int 386 | */ 387 | public function count(): int 388 | { 389 | return count($this->decoded); 390 | } 391 | 392 | /** 393 | * Determine whether an offset exists. 394 | * 395 | * @param mixed $offset 396 | * @return bool 397 | */ 398 | public function offsetExists($offset): bool 399 | { 400 | return isset($this->decoded[$offset]); 401 | } 402 | 403 | /** 404 | * Get the value at the given offset. 405 | * 406 | * @param string $offset 407 | * @return mixed 408 | */ 409 | public function offsetGet($offset): mixed 410 | { 411 | return $this->decoded[$offset]; 412 | } 413 | 414 | /** 415 | * Set the value at the given offset. 416 | * 417 | * @param string $offset 418 | * @param mixed $value 419 | * @return void 420 | */ 421 | public function offsetSet($offset, $value): void 422 | { 423 | $this->decoded[$offset] = $value; 424 | } 425 | 426 | /** 427 | * Unset the value at the given offset. 428 | * 429 | * @param string $offset 430 | * @return void 431 | */ 432 | public function offsetUnset($offset): void 433 | { 434 | unset($this->decoded[$offset]); 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /Concerns/AssertsStatusCodes.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 17 | } 18 | 19 | /** 20 | * Assert that the response has a 201 "Created" status code. 21 | * 22 | * @return $this 23 | */ 24 | public function assertCreated() 25 | { 26 | return $this->assertStatus(201); 27 | } 28 | 29 | /** 30 | * Assert that the response has a 202 "Accepted" status code. 31 | * 32 | * @return $this 33 | */ 34 | public function assertAccepted() 35 | { 36 | return $this->assertStatus(202); 37 | } 38 | 39 | /** 40 | * Assert that the response has the given status code and no content. 41 | * 42 | * @param int $status 43 | * @return $this 44 | */ 45 | public function assertNoContent($status = 204) 46 | { 47 | $this->assertStatus($status); 48 | 49 | PHPUnit::assertEmpty($this->getContent(), 'Response content is not empty.'); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Assert that the response has a 301 "Moved Permanently" status code. 56 | * 57 | * @return $this 58 | */ 59 | public function assertMovedPermanently() 60 | { 61 | return $this->assertStatus(301); 62 | } 63 | 64 | /** 65 | * Assert that the response has a 302 "Found" status code. 66 | * 67 | * @return $this 68 | */ 69 | public function assertFound() 70 | { 71 | return $this->assertStatus(302); 72 | } 73 | 74 | /** 75 | * Assert that the response has a 304 "Not Modified" status code. 76 | * 77 | * @return $this 78 | */ 79 | public function assertNotModified() 80 | { 81 | return $this->assertStatus(304); 82 | } 83 | 84 | /** 85 | * Assert that the response has a 307 "Temporary Redirect" status code. 86 | * 87 | * @return $this 88 | */ 89 | public function assertTemporaryRedirect() 90 | { 91 | return $this->assertStatus(307); 92 | } 93 | 94 | /** 95 | * Assert that the response has a 308 "Permanent Redirect" status code. 96 | * 97 | * @return $this 98 | */ 99 | public function assertPermanentRedirect() 100 | { 101 | return $this->assertStatus(308); 102 | } 103 | 104 | /** 105 | * Assert that the response has a 400 "Bad Request" status code. 106 | * 107 | * @return $this 108 | */ 109 | public function assertBadRequest() 110 | { 111 | return $this->assertStatus(400); 112 | } 113 | 114 | /** 115 | * Assert that the response has a 401 "Unauthorized" status code. 116 | * 117 | * @return $this 118 | */ 119 | public function assertUnauthorized() 120 | { 121 | return $this->assertStatus(401); 122 | } 123 | 124 | /** 125 | * Assert that the response has a 402 "Payment Required" status code. 126 | * 127 | * @return $this 128 | */ 129 | public function assertPaymentRequired() 130 | { 131 | return $this->assertStatus(402); 132 | } 133 | 134 | /** 135 | * Assert that the response has a 403 "Forbidden" status code. 136 | * 137 | * @return $this 138 | */ 139 | public function assertForbidden() 140 | { 141 | return $this->assertStatus(403); 142 | } 143 | 144 | /** 145 | * Assert that the response has a 404 "Not Found" status code. 146 | * 147 | * @return $this 148 | */ 149 | public function assertNotFound() 150 | { 151 | return $this->assertStatus(404); 152 | } 153 | 154 | /** 155 | * Assert that the response has a 405 "Method Not Allowed" status code. 156 | * 157 | * @return $this 158 | */ 159 | public function assertMethodNotAllowed() 160 | { 161 | return $this->assertStatus(405); 162 | } 163 | 164 | /** 165 | * Assert that the response has a 406 "Not Acceptable" status code. 166 | * 167 | * @return $this 168 | */ 169 | public function assertNotAcceptable() 170 | { 171 | return $this->assertStatus(406); 172 | } 173 | 174 | /** 175 | * Assert that the response has a 408 "Request Timeout" status code. 176 | * 177 | * @return $this 178 | */ 179 | public function assertRequestTimeout() 180 | { 181 | return $this->assertStatus(408); 182 | } 183 | 184 | /** 185 | * Assert that the response has a 409 "Conflict" status code. 186 | * 187 | * @return $this 188 | */ 189 | public function assertConflict() 190 | { 191 | return $this->assertStatus(409); 192 | } 193 | 194 | /** 195 | * Assert that the response has a 410 "Gone" status code. 196 | * 197 | * @return $this 198 | */ 199 | public function assertGone() 200 | { 201 | return $this->assertStatus(410); 202 | } 203 | 204 | /** 205 | * Assert that the response has a 415 "Unsupported Media Type" status code. 206 | * 207 | * @return $this 208 | */ 209 | public function assertUnsupportedMediaType() 210 | { 211 | return $this->assertStatus(415); 212 | } 213 | 214 | /** 215 | * Assert that the response has a 422 "Unprocessable Content" status code. 216 | * 217 | * @return $this 218 | */ 219 | public function assertUnprocessable() 220 | { 221 | return $this->assertStatus(422); 222 | } 223 | 224 | /** 225 | * Assert that the response has a 429 "Too Many Requests" status code. 226 | * 227 | * @return $this 228 | */ 229 | public function assertTooManyRequests() 230 | { 231 | return $this->assertStatus(429); 232 | } 233 | 234 | /** 235 | * Assert that the response has a 500 "Internal Server Error" status code. 236 | * 237 | * @return $this 238 | */ 239 | public function assertInternalServerError() 240 | { 241 | return $this->assertStatus(500); 242 | } 243 | 244 | /** 245 | * Assert that the response has a 503 "Service Unavailable" status code. 246 | * 247 | * @return $this 248 | */ 249 | public function assertServiceUnavailable() 250 | { 251 | return $this->assertStatus(503); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Concerns/RunsInParallel.php: -------------------------------------------------------------------------------- 1 | options = $options; 61 | 62 | if ($output instanceof ConsoleOutput) { 63 | $output = new ParallelConsoleOutput($output); 64 | } 65 | 66 | $runnerResolver = static::$runnerResolver ?: function ($options, OutputInterface $output) { 67 | $wrapperRunnerClass = class_exists(\ParaTest\WrapperRunner\WrapperRunner::class) 68 | ? \ParaTest\WrapperRunner\WrapperRunner::class 69 | : \ParaTest\Runners\PHPUnit\WrapperRunner::class; 70 | 71 | return new $wrapperRunnerClass($options, $output); 72 | }; 73 | 74 | $this->runner = $runnerResolver($options, $output); 75 | } 76 | 77 | /** 78 | * Set the application resolver callback. 79 | * 80 | * @param \Closure|null $resolver 81 | * @return void 82 | */ 83 | public static function resolveApplicationUsing($resolver) 84 | { 85 | static::$applicationResolver = $resolver; 86 | } 87 | 88 | /** 89 | * Set the runner resolver callback. 90 | * 91 | * @param \Closure|null $resolver 92 | * @return void 93 | */ 94 | public static function resolveRunnerUsing($resolver) 95 | { 96 | static::$runnerResolver = $resolver; 97 | } 98 | 99 | /** 100 | * Runs the test suite. 101 | * 102 | * @return int 103 | */ 104 | public function execute(): int 105 | { 106 | $configuration = $this->options instanceof \ParaTest\Options 107 | ? $this->options->configuration 108 | : $this->options->configuration(); 109 | 110 | (new PhpHandler())->handle($configuration->php()); 111 | 112 | $this->forEachProcess(function () { 113 | ParallelTesting::callSetUpProcessCallbacks(); 114 | }); 115 | 116 | try { 117 | $potentialExitCode = $this->runner->run(); 118 | } finally { 119 | $this->forEachProcess(function () { 120 | ParallelTesting::callTearDownProcessCallbacks(); 121 | }); 122 | } 123 | 124 | return $potentialExitCode ?? $this->getExitCode(); 125 | } 126 | 127 | /** 128 | * Returns the highest exit code encountered throughout the course of test execution. 129 | * 130 | * @return int 131 | */ 132 | public function getExitCode(): int 133 | { 134 | return $this->runner->getExitCode(); 135 | } 136 | 137 | /** 138 | * Apply the given callback for each process. 139 | * 140 | * @param callable $callback 141 | * @return void 142 | */ 143 | protected function forEachProcess($callback) 144 | { 145 | $processes = $this->options instanceof \ParaTest\Options 146 | ? $this->options->processes 147 | : $this->options->processes(); 148 | 149 | Collection::range(1, $processes)->each(function ($token) use ($callback) { 150 | tap($this->createApplication(), function ($app) use ($callback, $token) { 151 | ParallelTesting::resolveTokenUsing(fn () => $token); 152 | 153 | $callback($app); 154 | })->flush(); 155 | }); 156 | } 157 | 158 | /** 159 | * Creates the application. 160 | * 161 | * @return \Illuminate\Contracts\Foundation\Application 162 | * 163 | * @throws \RuntimeException 164 | */ 165 | protected function createApplication() 166 | { 167 | $applicationResolver = static::$applicationResolver ?: function () { 168 | if (trait_exists(\Tests\CreatesApplication::class)) { 169 | $applicationCreator = new class 170 | { 171 | use \Tests\CreatesApplication; 172 | }; 173 | 174 | return $applicationCreator->createApplication(); 175 | } elseif (file_exists($path = (Application::inferBasePath().'/bootstrap/app.php'))) { 176 | $app = require $path; 177 | 178 | $app->make(Kernel::class)->bootstrap(); 179 | 180 | return $app; 181 | } 182 | 183 | throw new RuntimeException('Parallel Runner unable to resolve application.'); 184 | }; 185 | 186 | return $applicationResolver(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Concerns/TestDatabases.php: -------------------------------------------------------------------------------- 1 | whenNotUsingInMemoryDatabase(function ($database) { 31 | if (ParallelTesting::option('recreate_databases')) { 32 | Schema::dropDatabaseIfExists( 33 | $this->testDatabase($database) 34 | ); 35 | } 36 | }); 37 | }); 38 | 39 | ParallelTesting::setUpTestCase(function ($testCase) { 40 | $uses = array_flip(class_uses_recursive(get_class($testCase))); 41 | 42 | $databaseTraits = [ 43 | Testing\DatabaseMigrations::class, 44 | Testing\DatabaseTransactions::class, 45 | Testing\DatabaseTruncation::class, 46 | Testing\RefreshDatabase::class, 47 | ]; 48 | 49 | if (Arr::hasAny($uses, $databaseTraits) && ! ParallelTesting::option('without_databases')) { 50 | $this->whenNotUsingInMemoryDatabase(function ($database) use ($uses) { 51 | [$testDatabase, $created] = $this->ensureTestDatabaseExists($database); 52 | 53 | $this->switchToDatabase($testDatabase); 54 | 55 | if (isset($uses[Testing\DatabaseTransactions::class])) { 56 | $this->ensureSchemaIsUpToDate(); 57 | } 58 | 59 | if ($created) { 60 | ParallelTesting::callSetUpTestDatabaseCallbacks($testDatabase); 61 | } 62 | }); 63 | } 64 | }); 65 | 66 | ParallelTesting::tearDownProcess(function () { 67 | $this->whenNotUsingInMemoryDatabase(function ($database) { 68 | if (ParallelTesting::option('drop_databases')) { 69 | Schema::dropDatabaseIfExists( 70 | $this->testDatabase($database) 71 | ); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | /** 78 | * Ensure a test database exists and returns its name. 79 | * 80 | * @param string $database 81 | * @return array 82 | */ 83 | protected function ensureTestDatabaseExists($database) 84 | { 85 | $testDatabase = $this->testDatabase($database); 86 | 87 | try { 88 | $this->usingDatabase($testDatabase, function () { 89 | Schema::hasTable('dummy'); 90 | }); 91 | } catch (QueryException) { 92 | $this->usingDatabase($database, function () use ($testDatabase) { 93 | Schema::dropDatabaseIfExists($testDatabase); 94 | Schema::createDatabase($testDatabase); 95 | }); 96 | 97 | return [$testDatabase, true]; 98 | } 99 | 100 | return [$testDatabase, false]; 101 | } 102 | 103 | /** 104 | * Ensure the current database test schema is up to date. 105 | * 106 | * @return void 107 | */ 108 | protected function ensureSchemaIsUpToDate() 109 | { 110 | if (! static::$schemaIsUpToDate) { 111 | Artisan::call('migrate'); 112 | 113 | static::$schemaIsUpToDate = true; 114 | } 115 | } 116 | 117 | /** 118 | * Runs the given callable using the given database. 119 | * 120 | * @param string $database 121 | * @param callable $callable 122 | * @return void 123 | */ 124 | protected function usingDatabase($database, $callable) 125 | { 126 | $original = DB::getConfig('database'); 127 | 128 | try { 129 | $this->switchToDatabase($database); 130 | $callable(); 131 | } finally { 132 | $this->switchToDatabase($original); 133 | } 134 | } 135 | 136 | /** 137 | * Apply the given callback when tests are not using in memory database. 138 | * 139 | * @param callable $callback 140 | * @return void 141 | */ 142 | protected function whenNotUsingInMemoryDatabase($callback) 143 | { 144 | if (ParallelTesting::option('without_databases')) { 145 | return; 146 | } 147 | 148 | $database = DB::getConfig('database'); 149 | 150 | if ($database !== ':memory:') { 151 | $callback($database); 152 | } 153 | } 154 | 155 | /** 156 | * Switch to the given database. 157 | * 158 | * @param string $database 159 | * @return void 160 | */ 161 | protected function switchToDatabase($database) 162 | { 163 | DB::purge(); 164 | 165 | $default = config('database.default'); 166 | 167 | $url = config("database.connections.{$default}.url"); 168 | 169 | if ($url) { 170 | config()->set( 171 | "database.connections.{$default}.url", 172 | preg_replace('/^(.*)(\/[\w-]*)(\??.*)$/', "$1/{$database}$3", $url), 173 | ); 174 | } else { 175 | config()->set( 176 | "database.connections.{$default}.database", 177 | $database, 178 | ); 179 | } 180 | } 181 | 182 | /** 183 | * Returns the test database name. 184 | * 185 | * @return string 186 | */ 187 | protected function testDatabase($database) 188 | { 189 | $token = ParallelTesting::token(); 190 | 191 | return "{$database}_test_{$token}"; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Constraints/ArraySubset.php: -------------------------------------------------------------------------------- 1 | strict = $strict; 32 | $this->subset = $subset; 33 | } 34 | 35 | /** 36 | * Evaluates the constraint for parameter $other. 37 | * 38 | * If $returnResult is set to false (the default), an exception is thrown 39 | * in case of a failure. null is returned otherwise. 40 | * 41 | * If $returnResult is true, the result of the evaluation is returned as 42 | * a boolean value instead: true in case of success, false in case of a 43 | * failure. 44 | * 45 | * @param mixed $other 46 | * @param string $description 47 | * @param bool $returnResult 48 | * @return bool|null 49 | * 50 | * @throws \PHPUnit\Framework\ExpectationFailedException 51 | * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException 52 | */ 53 | public function evaluate($other, string $description = '', bool $returnResult = false): ?bool 54 | { 55 | // type cast $other & $this->subset as an array to allow 56 | // support in standard array functions. 57 | $other = $this->toArray($other); 58 | $this->subset = $this->toArray($this->subset); 59 | 60 | $patched = array_replace_recursive($other, $this->subset); 61 | 62 | if ($this->strict) { 63 | $result = $other === $patched; 64 | } else { 65 | $result = $other == $patched; 66 | } 67 | 68 | if ($returnResult) { 69 | return $result; 70 | } 71 | 72 | if (! $result) { 73 | $f = new ComparisonFailure( 74 | $patched, 75 | $other, 76 | var_export($patched, true), 77 | var_export($other, true) 78 | ); 79 | 80 | $this->fail($other, $description, $f); 81 | } 82 | 83 | return null; 84 | } 85 | 86 | /** 87 | * Returns a string representation of the constraint. 88 | * 89 | * @return string 90 | * 91 | * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException 92 | */ 93 | public function toString(): string 94 | { 95 | return 'has the subset '.(new Exporter)->export($this->subset); 96 | } 97 | 98 | /** 99 | * Returns the description of the failure. 100 | * 101 | * The beginning of failure messages is "Failed asserting that" in most 102 | * cases. This method should return the second part of that sentence. 103 | * 104 | * @param mixed $other 105 | * @return string 106 | * 107 | * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException 108 | */ 109 | protected function failureDescription($other): string 110 | { 111 | return 'an array '.$this->toString(); 112 | } 113 | 114 | /** 115 | * Returns the description of the failure. 116 | * 117 | * The beginning of failure messages is "Failed asserting that" in most 118 | * cases. This method should return the second part of that sentence. 119 | * 120 | * @param iterable $other 121 | * @return array 122 | */ 123 | protected function toArray(iterable $other): array 124 | { 125 | if (is_array($other)) { 126 | return $other; 127 | } 128 | 129 | if ($other instanceof ArrayObject) { 130 | return $other->getArrayCopy(); 131 | } 132 | 133 | if ($other instanceof Traversable) { 134 | return iterator_to_array($other); 135 | } 136 | 137 | return (array) $other; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Constraints/CountInDatabase.php: -------------------------------------------------------------------------------- 1 | expectedCount = $expectedCount; 41 | 42 | $this->database = $database; 43 | } 44 | 45 | /** 46 | * Check if the expected and actual count are equal. 47 | * 48 | * @param string $table 49 | * @return bool 50 | */ 51 | public function matches($table): bool 52 | { 53 | $this->actualCount = $this->database->table($table)->count(); 54 | 55 | return $this->actualCount === $this->expectedCount; 56 | } 57 | 58 | /** 59 | * Get the description of the failure. 60 | * 61 | * @param string $table 62 | * @return string 63 | */ 64 | public function failureDescription($table): string 65 | { 66 | return sprintf( 67 | "table [%s] matches expected entries count of %s. Entries found: %s.\n", 68 | $table, $this->expectedCount, $this->actualCount 69 | ); 70 | } 71 | 72 | /** 73 | * Get a string representation of the object. 74 | * 75 | * @param int $options 76 | * @return string 77 | */ 78 | public function toString($options = 0): string 79 | { 80 | return (new ReflectionClass($this))->name; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Constraints/HasInDatabase.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected $data; 31 | 32 | /** 33 | * Create a new constraint instance. 34 | * 35 | * @param \Illuminate\Database\Connection $database 36 | * @param array $data 37 | */ 38 | public function __construct(Connection $database, array $data) 39 | { 40 | $this->data = $data; 41 | 42 | $this->database = $database; 43 | } 44 | 45 | /** 46 | * Check if the data is found in the given table. 47 | * 48 | * @param string $table 49 | * @return bool 50 | */ 51 | public function matches($table): bool 52 | { 53 | return $this->database->table($table) 54 | ->where($this->data) 55 | ->exists(); 56 | } 57 | 58 | /** 59 | * Get the description of the failure. 60 | * 61 | * @param string $table 62 | * @return string 63 | */ 64 | public function failureDescription($table): string 65 | { 66 | return sprintf( 67 | "a row in the table [%s] matches the attributes %s.\n\n%s", 68 | $table, $this->toString(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), $this->getAdditionalInfo($table) 69 | ); 70 | } 71 | 72 | /** 73 | * Get additional info about the records found in the database table. 74 | * 75 | * @param string $table 76 | * @return string 77 | */ 78 | protected function getAdditionalInfo($table) 79 | { 80 | $query = $this->database->table($table); 81 | 82 | $similarResults = $query->where( 83 | array_key_first($this->data), 84 | $this->data[array_key_first($this->data)] 85 | )->select(array_keys($this->data))->limit($this->show)->get(); 86 | 87 | if ($similarResults->isNotEmpty()) { 88 | $description = 'Found similar results: '.json_encode($similarResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 89 | } else { 90 | $query = $this->database->table($table); 91 | 92 | $results = $query->select(array_keys($this->data))->limit($this->show)->get(); 93 | 94 | if ($results->isEmpty()) { 95 | return 'The table is empty'; 96 | } 97 | 98 | $description = 'Found: '.json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 99 | } 100 | 101 | if ($query->count() > $this->show) { 102 | $description .= sprintf(' and %s others', $query->count() - $this->show); 103 | } 104 | 105 | return $description; 106 | } 107 | 108 | /** 109 | * Get a string representation of the object. 110 | * 111 | * @param int $options 112 | * @return string 113 | */ 114 | public function toString($options = 0): string 115 | { 116 | foreach ($this->data as $key => $data) { 117 | $output[$key] = $data instanceof Expression ? $data->getValue($this->database->getQueryGrammar()) : $data; 118 | } 119 | 120 | return json_encode($output ?? [], $options); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Constraints/NotSoftDeletedInDatabase.php: -------------------------------------------------------------------------------- 1 | database = $database; 48 | $this->data = $data; 49 | $this->deletedAtColumn = $deletedAtColumn; 50 | } 51 | 52 | /** 53 | * Check if the data is found in the given table. 54 | * 55 | * @param string $table 56 | * @return bool 57 | */ 58 | public function matches($table): bool 59 | { 60 | return $this->database->table($table) 61 | ->where($this->data) 62 | ->whereNull($this->deletedAtColumn) 63 | ->exists(); 64 | } 65 | 66 | /** 67 | * Get the description of the failure. 68 | * 69 | * @param string $table 70 | * @return string 71 | */ 72 | public function failureDescription($table): string 73 | { 74 | return sprintf( 75 | "any existing row in the table [%s] matches the attributes %s.\n\n%s", 76 | $table, $this->toString(), $this->getAdditionalInfo($table) 77 | ); 78 | } 79 | 80 | /** 81 | * Get additional info about the records found in the database table. 82 | * 83 | * @param string $table 84 | * @return string 85 | */ 86 | protected function getAdditionalInfo($table) 87 | { 88 | $query = $this->database->table($table); 89 | 90 | $results = $query->limit($this->show)->get(); 91 | 92 | if ($results->isEmpty()) { 93 | return 'The table is empty'; 94 | } 95 | 96 | $description = 'Found: '.json_encode($results, JSON_PRETTY_PRINT); 97 | 98 | if ($query->count() > $this->show) { 99 | $description .= sprintf(' and %s others', $query->count() - $this->show); 100 | } 101 | 102 | return $description; 103 | } 104 | 105 | /** 106 | * Get a string representation of the object. 107 | * 108 | * @return string 109 | */ 110 | public function toString(): string 111 | { 112 | return json_encode($this->data); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Constraints/SeeInOrder.php: -------------------------------------------------------------------------------- 1 | content = $content; 32 | } 33 | 34 | /** 35 | * Determine if the rule passes validation. 36 | * 37 | * @param array $values 38 | * @return bool 39 | */ 40 | public function matches($values): bool 41 | { 42 | $decodedContent = html_entity_decode($this->content, ENT_QUOTES, 'UTF-8'); 43 | 44 | $position = 0; 45 | 46 | foreach ($values as $value) { 47 | if (empty($value)) { 48 | continue; 49 | } 50 | 51 | $decodedValue = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); 52 | 53 | $valuePosition = mb_strpos($decodedContent, $decodedValue, $position); 54 | 55 | if ($valuePosition === false || $valuePosition < $position) { 56 | $this->failedValue = $value; 57 | 58 | return false; 59 | } 60 | 61 | $position = $valuePosition + mb_strlen($decodedValue); 62 | } 63 | 64 | return true; 65 | } 66 | 67 | /** 68 | * Get the description of the failure. 69 | * 70 | * @param array $values 71 | * @return string 72 | */ 73 | public function failureDescription($values): string 74 | { 75 | return sprintf( 76 | 'Failed asserting that \'%s\' contains "%s" in specified order.', 77 | $this->content, 78 | $this->failedValue 79 | ); 80 | } 81 | 82 | /** 83 | * Get a string representation of the object. 84 | * 85 | * @return string 86 | */ 87 | public function toString(): string 88 | { 89 | return (new ReflectionClass($this))->name; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Constraints/SoftDeletedInDatabase.php: -------------------------------------------------------------------------------- 1 | data = $data; 48 | 49 | $this->database = $database; 50 | 51 | $this->deletedAtColumn = $deletedAtColumn; 52 | } 53 | 54 | /** 55 | * Check if the data is found in the given table. 56 | * 57 | * @param string $table 58 | * @return bool 59 | */ 60 | public function matches($table): bool 61 | { 62 | return $this->database->table($table) 63 | ->where($this->data) 64 | ->whereNotNull($this->deletedAtColumn) 65 | ->exists(); 66 | } 67 | 68 | /** 69 | * Get the description of the failure. 70 | * 71 | * @param string $table 72 | * @return string 73 | */ 74 | public function failureDescription($table): string 75 | { 76 | return sprintf( 77 | "any soft deleted row in the table [%s] matches the attributes %s.\n\n%s", 78 | $table, $this->toString(), $this->getAdditionalInfo($table) 79 | ); 80 | } 81 | 82 | /** 83 | * Get additional info about the records found in the database table. 84 | * 85 | * @param string $table 86 | * @return string 87 | */ 88 | protected function getAdditionalInfo($table) 89 | { 90 | $query = $this->database->table($table); 91 | 92 | $results = $query->limit($this->show)->get(); 93 | 94 | if ($results->isEmpty()) { 95 | return 'The table is empty'; 96 | } 97 | 98 | $description = 'Found: '.json_encode($results, JSON_PRETTY_PRINT); 99 | 100 | if ($query->count() > $this->show) { 101 | $description .= sprintf(' and %s others', $query->count() - $this->show); 102 | } 103 | 104 | return $description; 105 | } 106 | 107 | /** 108 | * Get a string representation of the object. 109 | * 110 | * @return string 111 | */ 112 | public function toString(): string 113 | { 114 | return json_encode($this->data); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | path = $path; 47 | $this->props = $props; 48 | } 49 | 50 | /** 51 | * Compose the absolute "dot" path to the given key. 52 | * 53 | * @param string $key 54 | * @return string 55 | */ 56 | protected function dotPath(string $key = ''): string 57 | { 58 | if (is_null($this->path)) { 59 | return $key; 60 | } 61 | 62 | return rtrim(implode('.', [$this->path, $key]), '.'); 63 | } 64 | 65 | /** 66 | * Retrieve a prop within the current scope using "dot" notation. 67 | * 68 | * @param string|null $key 69 | * @return mixed 70 | */ 71 | protected function prop(?string $key = null) 72 | { 73 | return Arr::get($this->props, $key); 74 | } 75 | 76 | /** 77 | * Instantiate a new "scope" at the path of the given key. 78 | * 79 | * @param string $key 80 | * @param \Closure $callback 81 | * @return $this 82 | */ 83 | protected function scope(string $key, Closure $callback): self 84 | { 85 | $props = $this->prop($key); 86 | $path = $this->dotPath($key); 87 | 88 | PHPUnit::assertIsArray($props, sprintf('Property [%s] is not scopeable.', $path)); 89 | 90 | $scope = new static($props, $path); 91 | $callback($scope); 92 | $scope->interacted(); 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Instantiate a new "scope" on the first child element. 99 | * 100 | * @param \Closure $callback 101 | * @return $this 102 | */ 103 | public function first(Closure $callback): self 104 | { 105 | $props = $this->prop(); 106 | 107 | $path = $this->dotPath(); 108 | 109 | PHPUnit::assertNotEmpty($props, $path === '' 110 | ? 'Cannot scope directly onto the first element of the root level because it is empty.' 111 | : sprintf('Cannot scope directly onto the first element of property [%s] because it is empty.', $path) 112 | ); 113 | 114 | $key = array_keys($props)[0]; 115 | 116 | $this->interactsWith($key); 117 | 118 | return $this->scope($key, $callback); 119 | } 120 | 121 | /** 122 | * Instantiate a new "scope" on each child element. 123 | * 124 | * @param \Closure $callback 125 | * @return $this 126 | */ 127 | public function each(Closure $callback): self 128 | { 129 | $props = $this->prop(); 130 | 131 | $path = $this->dotPath(); 132 | 133 | PHPUnit::assertNotEmpty($props, $path === '' 134 | ? 'Cannot scope directly onto each element of the root level because it is empty.' 135 | : sprintf('Cannot scope directly onto each element of property [%s] because it is empty.', $path) 136 | ); 137 | 138 | foreach (array_keys($props) as $key) { 139 | $this->interactsWith($key); 140 | 141 | $this->scope($key, $callback); 142 | } 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Create a new instance from an array. 149 | * 150 | * @param array $data 151 | * @return static 152 | */ 153 | public static function fromArray(array $data): self 154 | { 155 | return new static($data); 156 | } 157 | 158 | /** 159 | * Create a new instance from an AssertableJsonString. 160 | * 161 | * @param \Illuminate\Testing\AssertableJsonString $json 162 | * @return static 163 | */ 164 | public static function fromAssertableJsonString(AssertableJsonString $json): self 165 | { 166 | return static::fromArray($json->json()); 167 | } 168 | 169 | /** 170 | * Get the instance as an array. 171 | * 172 | * @return array 173 | */ 174 | public function toArray() 175 | { 176 | return $this->props; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Fluent/Concerns/Debugging.php: -------------------------------------------------------------------------------- 1 | prop($prop)); 20 | 21 | return $this; 22 | } 23 | 24 | /** 25 | * Retrieve a prop within the current scope using "dot" notation. 26 | * 27 | * @param string|null $key 28 | * @return mixed 29 | */ 30 | abstract protected function prop(?string $key = null); 31 | } 32 | -------------------------------------------------------------------------------- /Fluent/Concerns/Has.php: -------------------------------------------------------------------------------- 1 | dotPath(); 22 | 23 | PHPUnit::assertCount( 24 | $key, 25 | $this->prop(), 26 | $path 27 | ? sprintf('Property [%s] does not have the expected size.', $path) 28 | : sprintf('Root level does not have the expected size.') 29 | ); 30 | 31 | return $this; 32 | } 33 | 34 | PHPUnit::assertCount( 35 | $length, 36 | $this->prop($key), 37 | sprintf('Property [%s] does not have the expected size.', $this->dotPath($key)) 38 | ); 39 | 40 | return $this; 41 | } 42 | 43 | /** 44 | * Assert that the prop size is between a given minimum and maximum. 45 | * 46 | * @param int|string $min 47 | * @param int|string $max 48 | * @return $this 49 | */ 50 | public function countBetween(int|string $min, int|string $max): self 51 | { 52 | $path = $this->dotPath(); 53 | 54 | $prop = $this->prop(); 55 | 56 | PHPUnit::assertGreaterThanOrEqual( 57 | $min, 58 | count($prop), 59 | $path 60 | ? sprintf('Property [%s] size is not greater than or equal to [%s].', $path, $min) 61 | : sprintf('Root level size is not greater than or equal to [%s].', $min) 62 | ); 63 | 64 | PHPUnit::assertLessThanOrEqual( 65 | $max, 66 | count($prop), 67 | $path 68 | ? sprintf('Property [%s] size is not less than or equal to [%s].', $path, $max) 69 | : sprintf('Root level size is not less than or equal to [%s].', $max) 70 | ); 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Ensure that the given prop exists. 77 | * 78 | * @param string|int $key 79 | * @param int|\Closure|null $length 80 | * @param \Closure|null $callback 81 | * @return $this 82 | */ 83 | public function has($key, $length = null, ?Closure $callback = null): self 84 | { 85 | $prop = $this->prop(); 86 | 87 | if (is_int($key) && is_null($length)) { 88 | return $this->count($key); 89 | } 90 | 91 | PHPUnit::assertTrue( 92 | Arr::has($prop, $key), 93 | sprintf('Property [%s] does not exist.', $this->dotPath($key)) 94 | ); 95 | 96 | $this->interactsWith($key); 97 | 98 | if (! is_null($callback)) { 99 | return $this->has($key, function (self $scope) use ($length, $callback) { 100 | return $scope 101 | ->tap(function (self $scope) use ($length) { 102 | if (! is_null($length)) { 103 | $scope->count($length); 104 | } 105 | }) 106 | ->first($callback) 107 | ->etc(); 108 | }); 109 | } 110 | 111 | if (is_callable($length)) { 112 | return $this->scope($key, $length); 113 | } 114 | 115 | if (! is_null($length)) { 116 | return $this->count($key, $length); 117 | } 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Assert that all of the given props exist. 124 | * 125 | * @param array|string $key 126 | * @return $this 127 | */ 128 | public function hasAll($key): self 129 | { 130 | $keys = is_array($key) ? $key : func_get_args(); 131 | 132 | foreach ($keys as $prop => $count) { 133 | if (is_int($prop)) { 134 | $this->has($count); 135 | } else { 136 | $this->has($prop, $count); 137 | } 138 | } 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Assert that at least one of the given props exists. 145 | * 146 | * @param array|string $key 147 | * @return $this 148 | */ 149 | public function hasAny($key): self 150 | { 151 | $keys = is_array($key) ? $key : func_get_args(); 152 | 153 | PHPUnit::assertTrue( 154 | Arr::hasAny($this->prop(), $keys), 155 | sprintf('None of properties [%s] exist.', implode(', ', $keys)) 156 | ); 157 | 158 | foreach ($keys as $key) { 159 | $this->interactsWith($key); 160 | } 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * Assert that none of the given props exist. 167 | * 168 | * @param array|string $key 169 | * @return $this 170 | */ 171 | public function missingAll($key): self 172 | { 173 | $keys = is_array($key) ? $key : func_get_args(); 174 | 175 | foreach ($keys as $prop) { 176 | $this->missing($prop); 177 | } 178 | 179 | return $this; 180 | } 181 | 182 | /** 183 | * Assert that the given prop does not exist. 184 | * 185 | * @param string $key 186 | * @return $this 187 | */ 188 | public function missing(string $key): self 189 | { 190 | PHPUnit::assertNotTrue( 191 | Arr::has($this->prop(), $key), 192 | sprintf('Property [%s] was found while it was expected to be missing.', $this->dotPath($key)) 193 | ); 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * Compose the absolute "dot" path to the given key. 200 | * 201 | * @param string $key 202 | * @return string 203 | */ 204 | abstract protected function dotPath(string $key = ''): string; 205 | 206 | /** 207 | * Marks the property as interacted. 208 | * 209 | * @param string $key 210 | * @return void 211 | */ 212 | abstract protected function interactsWith(string $key): void; 213 | 214 | /** 215 | * Retrieve a prop within the current scope using "dot" notation. 216 | * 217 | * @param string|null $key 218 | * @return mixed 219 | */ 220 | abstract protected function prop(?string $key = null); 221 | 222 | /** 223 | * Instantiate a new "scope" at the path of the given key. 224 | * 225 | * @param string $key 226 | * @param \Closure $callback 227 | * @return $this 228 | */ 229 | abstract protected function scope(string $key, Closure $callback); 230 | 231 | /** 232 | * Disables the interaction check. 233 | * 234 | * @return $this 235 | */ 236 | abstract public function etc(); 237 | 238 | /** 239 | * Instantiate a new "scope" on the first element. 240 | * 241 | * @param \Closure $callback 242 | * @return $this 243 | */ 244 | abstract public function first(Closure $callback); 245 | } 246 | -------------------------------------------------------------------------------- /Fluent/Concerns/Interaction.php: -------------------------------------------------------------------------------- 1 | interacted, true)) { 28 | $this->interacted[] = $prop; 29 | } 30 | } 31 | 32 | /** 33 | * Asserts that all properties have been interacted with. 34 | * 35 | * @return void 36 | */ 37 | public function interacted(): void 38 | { 39 | PHPUnit::assertSame( 40 | [], 41 | array_diff(array_keys($this->prop()), $this->interacted), 42 | $this->path 43 | ? sprintf('Unexpected properties were found in scope [%s].', $this->path) 44 | : 'Unexpected properties were found on the root level.' 45 | ); 46 | } 47 | 48 | /** 49 | * Disables the interaction check. 50 | * 51 | * @return $this 52 | */ 53 | public function etc(): self 54 | { 55 | $this->interacted = array_keys($this->prop()); 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Retrieve a prop within the current scope using "dot" notation. 62 | * 63 | * @param string|null $key 64 | * @return mixed 65 | */ 66 | abstract protected function prop(?string $key = null); 67 | } 68 | -------------------------------------------------------------------------------- /Fluent/Concerns/Matching.php: -------------------------------------------------------------------------------- 1 | has($key); 24 | 25 | $actual = $this->prop($key); 26 | 27 | if ($expected instanceof Closure) { 28 | PHPUnit::assertTrue( 29 | $expected(is_array($actual) ? new Collection($actual) : $actual), 30 | sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key)) 31 | ); 32 | 33 | return $this; 34 | } 35 | 36 | $expected = $expected instanceof Arrayable 37 | ? $expected->toArray() 38 | : enum_value($expected); 39 | 40 | $this->ensureSorted($expected); 41 | $this->ensureSorted($actual); 42 | 43 | PHPUnit::assertSame( 44 | $expected, 45 | $actual, 46 | sprintf('Property [%s] does not match the expected value.', $this->dotPath($key)) 47 | ); 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Asserts that the property does not match the expected value. 54 | * 55 | * @param string $key 56 | * @param mixed|\Closure $expected 57 | * @return $this 58 | */ 59 | public function whereNot(string $key, $expected): self 60 | { 61 | $this->has($key); 62 | 63 | $actual = $this->prop($key); 64 | 65 | if ($expected instanceof Closure) { 66 | PHPUnit::assertFalse( 67 | $expected(is_array($actual) ? new Collection($actual) : $actual), 68 | sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key)) 69 | ); 70 | 71 | return $this; 72 | } 73 | 74 | $expected = $expected instanceof Arrayable 75 | ? $expected->toArray() 76 | : enum_value($expected); 77 | 78 | $this->ensureSorted($expected); 79 | $this->ensureSorted($actual); 80 | 81 | PHPUnit::assertNotSame( 82 | $expected, 83 | $actual, 84 | sprintf( 85 | 'Property [%s] contains a value that should be missing: [%s, %s]', 86 | $this->dotPath($key), 87 | $key, 88 | $expected 89 | ) 90 | ); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Asserts that the property is null. 97 | * 98 | * @param string $key 99 | * @return $this 100 | */ 101 | public function whereNull(string $key): self 102 | { 103 | $this->has($key); 104 | 105 | $actual = $this->prop($key); 106 | 107 | PHPUnit::assertNull( 108 | $actual, 109 | sprintf( 110 | 'Property [%s] should be null.', 111 | $this->dotPath($key), 112 | ) 113 | ); 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Asserts that the property is not null. 120 | * 121 | * @param string $key 122 | * @return $this 123 | */ 124 | public function whereNotNull(string $key): self 125 | { 126 | $this->has($key); 127 | 128 | $actual = $this->prop($key); 129 | 130 | PHPUnit::assertNotNull( 131 | $actual, 132 | sprintf( 133 | 'Property [%s] should not be null.', 134 | $this->dotPath($key), 135 | ) 136 | ); 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Asserts that all properties match their expected values. 143 | * 144 | * @param array $bindings 145 | * @return $this 146 | */ 147 | public function whereAll(array $bindings): self 148 | { 149 | foreach ($bindings as $key => $value) { 150 | $this->where($key, $value); 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * Asserts that the property is of the expected type. 158 | * 159 | * @param string $key 160 | * @param string|array $expected 161 | * @return $this 162 | */ 163 | public function whereType(string $key, $expected): self 164 | { 165 | $this->has($key); 166 | 167 | $actual = $this->prop($key); 168 | 169 | if (! is_array($expected)) { 170 | $expected = explode('|', $expected); 171 | } 172 | 173 | PHPUnit::assertContains( 174 | strtolower(gettype($actual)), 175 | $expected, 176 | sprintf('Property [%s] is not of expected type [%s].', $this->dotPath($key), implode('|', $expected)) 177 | ); 178 | 179 | return $this; 180 | } 181 | 182 | /** 183 | * Asserts that all properties are of their expected types. 184 | * 185 | * @param array $bindings 186 | * @return $this 187 | */ 188 | public function whereAllType(array $bindings): self 189 | { 190 | foreach ($bindings as $key => $value) { 191 | $this->whereType($key, $value); 192 | } 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Asserts that the property contains the expected values. 199 | * 200 | * @param string $key 201 | * @param mixed $expected 202 | * @return $this 203 | */ 204 | public function whereContains(string $key, $expected) 205 | { 206 | $actual = new Collection( 207 | $this->prop($key) ?? $this->prop() 208 | ); 209 | 210 | $missing = (new Collection($expected)) 211 | ->map(fn ($search) => enum_value($search)) 212 | ->reject(function ($search) use ($key, $actual) { 213 | if ($actual->containsStrict($key, $search)) { 214 | return true; 215 | } 216 | 217 | return $actual->containsStrict($search); 218 | }); 219 | 220 | if ($missing->whereInstanceOf('Closure')->isNotEmpty()) { 221 | PHPUnit::assertEmpty( 222 | $missing->toArray(), 223 | sprintf( 224 | 'Property [%s] does not contain a value that passes the truth test within the given closure.', 225 | $key, 226 | ) 227 | ); 228 | } else { 229 | PHPUnit::assertEmpty( 230 | $missing->toArray(), 231 | sprintf( 232 | 'Property [%s] does not contain [%s].', 233 | $key, 234 | implode(', ', array_values($missing->toArray())) 235 | ) 236 | ); 237 | } 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * Ensures that all properties are sorted the same way, recursively. 244 | * 245 | * @param mixed $value 246 | * @return void 247 | */ 248 | protected function ensureSorted(&$value): void 249 | { 250 | if (! is_array($value)) { 251 | return; 252 | } 253 | 254 | foreach ($value as &$arg) { 255 | $this->ensureSorted($arg); 256 | } 257 | 258 | ksort($value); 259 | } 260 | 261 | /** 262 | * Compose the absolute "dot" path to the given key. 263 | * 264 | * @param string $key 265 | * @return string 266 | */ 267 | abstract protected function dotPath(string $key = ''): string; 268 | 269 | /** 270 | * Ensure that the given prop exists. 271 | * 272 | * @param string $key 273 | * @param null $value 274 | * @param \Closure|null $scope 275 | * @return $this 276 | */ 277 | abstract public function has(string $key, $value = null, ?Closure $scope = null); 278 | 279 | /** 280 | * Retrieve a prop within the current scope using "dot" notation. 281 | * 282 | * @param string|null $key 283 | * @return mixed 284 | */ 285 | abstract protected function prop(?string $key = null); 286 | } 287 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LoggedExceptionCollection.php: -------------------------------------------------------------------------------- 1 | getVerbosity(), 37 | $output->isDecorated(), 38 | $output->getFormatter(), 39 | ); 40 | 41 | $this->output = $output; 42 | } 43 | 44 | /** 45 | * Writes a message to the output. 46 | * 47 | * @param string|iterable $messages 48 | * @param bool $newline 49 | * @param int $options 50 | * @return void 51 | */ 52 | public function write($messages, bool $newline = false, int $options = 0): void 53 | { 54 | $messages = (new Collection($messages))->filter(function ($message) { 55 | return ! Str::contains($message, $this->ignore); 56 | }); 57 | 58 | $this->output->write($messages->toArray(), $newline, $options); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ParallelRunner.php: -------------------------------------------------------------------------------- 1 | execute(); 20 | } 21 | } 22 | } else { 23 | class ParallelRunner implements \ParaTest\Runners\PHPUnit\RunnerInterface 24 | { 25 | use RunsInParallel; 26 | 27 | /** 28 | * Runs the test suite. 29 | * 30 | * @return void 31 | */ 32 | public function run(): void 33 | { 34 | $this->execute(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ParallelTesting.php: -------------------------------------------------------------------------------- 1 | container = $container; 74 | } 75 | 76 | /** 77 | * Set a callback that should be used when resolving options. 78 | * 79 | * @param \Closure|null $resolver 80 | * @return void 81 | */ 82 | public function resolveOptionsUsing($resolver) 83 | { 84 | $this->optionsResolver = $resolver; 85 | } 86 | 87 | /** 88 | * Set a callback that should be used when resolving the unique process token. 89 | * 90 | * @param \Closure|null $resolver 91 | * @return void 92 | */ 93 | public function resolveTokenUsing($resolver) 94 | { 95 | $this->tokenResolver = $resolver; 96 | } 97 | 98 | /** 99 | * Register a "setUp" process callback. 100 | * 101 | * @param callable $callback 102 | * @return void 103 | */ 104 | public function setUpProcess($callback) 105 | { 106 | $this->setUpProcessCallbacks[] = $callback; 107 | } 108 | 109 | /** 110 | * Register a "setUp" test case callback. 111 | * 112 | * @param callable $callback 113 | * @return void 114 | */ 115 | public function setUpTestCase($callback) 116 | { 117 | $this->setUpTestCaseCallbacks[] = $callback; 118 | } 119 | 120 | /** 121 | * Register a "setUp" test database callback. 122 | * 123 | * @param callable $callback 124 | * @return void 125 | */ 126 | public function setUpTestDatabase($callback) 127 | { 128 | $this->setUpTestDatabaseCallbacks[] = $callback; 129 | } 130 | 131 | /** 132 | * Register a "tearDown" process callback. 133 | * 134 | * @param callable $callback 135 | * @return void 136 | */ 137 | public function tearDownProcess($callback) 138 | { 139 | $this->tearDownProcessCallbacks[] = $callback; 140 | } 141 | 142 | /** 143 | * Register a "tearDown" test case callback. 144 | * 145 | * @param callable $callback 146 | * @return void 147 | */ 148 | public function tearDownTestCase($callback) 149 | { 150 | $this->tearDownTestCaseCallbacks[] = $callback; 151 | } 152 | 153 | /** 154 | * Call all of the "setUp" process callbacks. 155 | * 156 | * @return void 157 | */ 158 | public function callSetUpProcessCallbacks() 159 | { 160 | $this->whenRunningInParallel(function () { 161 | foreach ($this->setUpProcessCallbacks as $callback) { 162 | $this->container->call($callback, [ 163 | 'token' => $this->token(), 164 | ]); 165 | } 166 | }); 167 | } 168 | 169 | /** 170 | * Call all of the "setUp" test case callbacks. 171 | * 172 | * @param \Illuminate\Foundation\Testing\TestCase $testCase 173 | * @return void 174 | */ 175 | public function callSetUpTestCaseCallbacks($testCase) 176 | { 177 | $this->whenRunningInParallel(function () use ($testCase) { 178 | foreach ($this->setUpTestCaseCallbacks as $callback) { 179 | $this->container->call($callback, [ 180 | 'testCase' => $testCase, 181 | 'token' => $this->token(), 182 | ]); 183 | } 184 | }); 185 | } 186 | 187 | /** 188 | * Call all of the "setUp" test database callbacks. 189 | * 190 | * @param string $database 191 | * @return void 192 | */ 193 | public function callSetUpTestDatabaseCallbacks($database) 194 | { 195 | $this->whenRunningInParallel(function () use ($database) { 196 | foreach ($this->setUpTestDatabaseCallbacks as $callback) { 197 | $this->container->call($callback, [ 198 | 'database' => $database, 199 | 'token' => $this->token(), 200 | ]); 201 | } 202 | }); 203 | } 204 | 205 | /** 206 | * Call all of the "tearDown" process callbacks. 207 | * 208 | * @return void 209 | */ 210 | public function callTearDownProcessCallbacks() 211 | { 212 | $this->whenRunningInParallel(function () { 213 | foreach ($this->tearDownProcessCallbacks as $callback) { 214 | $this->container->call($callback, [ 215 | 'token' => $this->token(), 216 | ]); 217 | } 218 | }); 219 | } 220 | 221 | /** 222 | * Call all of the "tearDown" test case callbacks. 223 | * 224 | * @param \Illuminate\Foundation\Testing\TestCase $testCase 225 | * @return void 226 | */ 227 | public function callTearDownTestCaseCallbacks($testCase) 228 | { 229 | $this->whenRunningInParallel(function () use ($testCase) { 230 | foreach ($this->tearDownTestCaseCallbacks as $callback) { 231 | $this->container->call($callback, [ 232 | 'testCase' => $testCase, 233 | 'token' => $this->token(), 234 | ]); 235 | } 236 | }); 237 | } 238 | 239 | /** 240 | * Get a parallel testing option. 241 | * 242 | * @param string $option 243 | * @return mixed 244 | */ 245 | public function option($option) 246 | { 247 | $optionsResolver = $this->optionsResolver ?: function ($option) { 248 | $option = 'LARAVEL_PARALLEL_TESTING_'.Str::upper($option); 249 | 250 | return $_SERVER[$option] ?? false; 251 | }; 252 | 253 | return $optionsResolver($option); 254 | } 255 | 256 | /** 257 | * Gets a unique test token. 258 | * 259 | * @return string|false 260 | */ 261 | public function token() 262 | { 263 | return $this->tokenResolver 264 | ? call_user_func($this->tokenResolver) 265 | : ($_SERVER['TEST_TOKEN'] ?? false); 266 | } 267 | 268 | /** 269 | * Apply the callback if tests are running in parallel. 270 | * 271 | * @param callable $callback 272 | * @return void 273 | */ 274 | protected function whenRunningInParallel($callback) 275 | { 276 | if ($this->inParallel()) { 277 | $callback(); 278 | } 279 | } 280 | 281 | /** 282 | * Indicates if the current tests are been run in parallel. 283 | * 284 | * @return bool 285 | */ 286 | protected function inParallel() 287 | { 288 | return ! empty($_SERVER['LARAVEL_PARALLEL_TESTING']) && $this->token(); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /ParallelTestingServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 21 | $this->bootTestDatabase(); 22 | } 23 | } 24 | 25 | /** 26 | * Register the service provider. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | if ($this->app->runningInConsole()) { 33 | $this->app->singleton(ParallelTesting::class, function () { 34 | return new ParallelTesting($this->app); 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PendingCommand.php: -------------------------------------------------------------------------------- 1 | app = $app; 87 | $this->test = $test; 88 | $this->command = $command; 89 | $this->parameters = $parameters; 90 | } 91 | 92 | /** 93 | * Specify an expected question that will be asked when the command runs. 94 | * 95 | * @param string $question 96 | * @param string|bool $answer 97 | * @return $this 98 | */ 99 | public function expectsQuestion($question, $answer) 100 | { 101 | $this->test->expectedQuestions[] = [$question, $answer]; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Specify an expected confirmation question that will be asked when the command runs. 108 | * 109 | * @param string $question 110 | * @param string $answer 111 | * @return $this 112 | */ 113 | public function expectsConfirmation($question, $answer = 'no') 114 | { 115 | return $this->expectsQuestion($question, strtolower($answer) === 'yes'); 116 | } 117 | 118 | /** 119 | * Specify an expected choice question with expected answers that will be asked/shown when the command runs. 120 | * 121 | * @param string $question 122 | * @param string|array $answer 123 | * @param array $answers 124 | * @param bool $strict 125 | * @return $this 126 | */ 127 | public function expectsChoice($question, $answer, $answers, $strict = false) 128 | { 129 | $this->test->expectedChoices[$question] = [ 130 | 'expected' => $answers, 131 | 'strict' => $strict, 132 | ]; 133 | 134 | return $this->expectsQuestion($question, $answer); 135 | } 136 | 137 | /** 138 | * Specify an expected search question with an expected search string, followed by an expected choice question with expected answers. 139 | * 140 | * @param string $question 141 | * @param string|array $answer 142 | * @param string $search 143 | * @param array $answers 144 | * @return $this 145 | */ 146 | public function expectsSearch($question, $answer, $search, $answers) 147 | { 148 | return $this 149 | ->expectsQuestion($question, $search) 150 | ->expectsChoice($question, $answer, $answers); 151 | } 152 | 153 | /** 154 | * Specify output that should be printed when the command runs. 155 | * 156 | * @param string|null $output 157 | * @return $this 158 | */ 159 | public function expectsOutput($output = null) 160 | { 161 | if ($output === null) { 162 | $this->test->expectsOutput = true; 163 | 164 | return $this; 165 | } 166 | 167 | $this->test->expectedOutput[] = $output; 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Specify output that should never be printed when the command runs. 174 | * 175 | * @param string|null $output 176 | * @return $this 177 | */ 178 | public function doesntExpectOutput($output = null) 179 | { 180 | if ($output === null) { 181 | $this->test->expectsOutput = false; 182 | 183 | return $this; 184 | } 185 | 186 | $this->test->unexpectedOutput[$output] = false; 187 | 188 | return $this; 189 | } 190 | 191 | /** 192 | * Specify that the given string should be contained in the command output. 193 | * 194 | * @param string $string 195 | * @return $this 196 | */ 197 | public function expectsOutputToContain($string) 198 | { 199 | $this->test->expectedOutputSubstrings[] = $string; 200 | 201 | return $this; 202 | } 203 | 204 | /** 205 | * Specify that the given string shouldn't be contained in the command output. 206 | * 207 | * @param string $string 208 | * @return $this 209 | */ 210 | public function doesntExpectOutputToContain($string) 211 | { 212 | $this->test->unexpectedOutputSubstrings[$string] = false; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * Specify a table that should be printed when the command runs. 219 | * 220 | * @param array $headers 221 | * @param \Illuminate\Contracts\Support\Arrayable|array $rows 222 | * @param string $tableStyle 223 | * @param array $columnStyles 224 | * @return $this 225 | */ 226 | public function expectsTable($headers, $rows, $tableStyle = 'default', array $columnStyles = []) 227 | { 228 | $table = (new Table($output = new BufferedOutput)) 229 | ->setHeaders((array) $headers) 230 | ->setRows($rows instanceof Arrayable ? $rows->toArray() : $rows) 231 | ->setStyle($tableStyle); 232 | 233 | foreach ($columnStyles as $columnIndex => $columnStyle) { 234 | $table->setColumnStyle($columnIndex, $columnStyle); 235 | } 236 | 237 | $table->render(); 238 | 239 | $lines = array_filter( 240 | explode(PHP_EOL, $output->fetch()) 241 | ); 242 | 243 | foreach ($lines as $line) { 244 | $this->expectsOutput($line); 245 | } 246 | 247 | return $this; 248 | } 249 | 250 | /** 251 | * Assert that the command has the given exit code. 252 | * 253 | * @param int $exitCode 254 | * @return $this 255 | */ 256 | public function assertExitCode($exitCode) 257 | { 258 | $this->expectedExitCode = $exitCode; 259 | 260 | return $this; 261 | } 262 | 263 | /** 264 | * Assert that the command does not have the given exit code. 265 | * 266 | * @param int $exitCode 267 | * @return $this 268 | */ 269 | public function assertNotExitCode($exitCode) 270 | { 271 | $this->unexpectedExitCode = $exitCode; 272 | 273 | return $this; 274 | } 275 | 276 | /** 277 | * Assert that the command has the success exit code. 278 | * 279 | * @return $this 280 | */ 281 | public function assertSuccessful() 282 | { 283 | return $this->assertExitCode(Command::SUCCESS); 284 | } 285 | 286 | /** 287 | * Assert that the command has the success exit code. 288 | * 289 | * @return $this 290 | */ 291 | public function assertOk() 292 | { 293 | return $this->assertSuccessful(); 294 | } 295 | 296 | /** 297 | * Assert that the command does not have the success exit code. 298 | * 299 | * @return $this 300 | */ 301 | public function assertFailed() 302 | { 303 | return $this->assertNotExitCode(Command::SUCCESS); 304 | } 305 | 306 | /** 307 | * Execute the command. 308 | * 309 | * @return int 310 | */ 311 | public function execute() 312 | { 313 | return $this->run(); 314 | } 315 | 316 | /** 317 | * Execute the command. 318 | * 319 | * @return int 320 | * 321 | * @throws \Mockery\Exception\NoMatchingExpectationException 322 | */ 323 | public function run() 324 | { 325 | $this->hasExecuted = true; 326 | 327 | $mock = $this->mockConsoleOutput(); 328 | 329 | try { 330 | $exitCode = $this->app->make(Kernel::class)->call($this->command, $this->parameters, $mock); 331 | } catch (NoMatchingExpectationException $e) { 332 | if ($e->getMethodName() === 'askQuestion') { 333 | $this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.'); 334 | } 335 | 336 | throw $e; 337 | } catch (PromptValidationException) { 338 | $exitCode = Command::FAILURE; 339 | } 340 | 341 | if ($this->expectedExitCode !== null) { 342 | $this->test->assertEquals( 343 | $this->expectedExitCode, $exitCode, 344 | "Expected status code {$this->expectedExitCode} but received {$exitCode}." 345 | ); 346 | } elseif (! is_null($this->unexpectedExitCode)) { 347 | $this->test->assertNotEquals( 348 | $this->unexpectedExitCode, $exitCode, 349 | "Unexpected status code {$this->unexpectedExitCode} was received." 350 | ); 351 | } 352 | 353 | $this->verifyExpectations(); 354 | $this->flushExpectations(); 355 | 356 | $this->app->offsetUnset(OutputStyle::class); 357 | 358 | return $exitCode; 359 | } 360 | 361 | /** 362 | * Determine if expected questions / choices / outputs are fulfilled. 363 | * 364 | * @return void 365 | */ 366 | protected function verifyExpectations() 367 | { 368 | if (count($this->test->expectedQuestions)) { 369 | $this->test->fail('Question "'.Arr::first($this->test->expectedQuestions)[0].'" was not asked.'); 370 | } 371 | 372 | if (count($this->test->expectedChoices) > 0) { 373 | foreach ($this->test->expectedChoices as $question => $answers) { 374 | $assertion = $answers['strict'] ? 'assertEquals' : 'assertEqualsCanonicalizing'; 375 | 376 | $this->test->{$assertion}( 377 | $answers['expected'], 378 | $answers['actual'], 379 | 'Question "'.$question.'" has different options.' 380 | ); 381 | } 382 | } 383 | 384 | if (count($this->test->expectedOutput)) { 385 | $this->test->fail('Output "'.Arr::first($this->test->expectedOutput).'" was not printed.'); 386 | } 387 | 388 | if (count($this->test->expectedOutputSubstrings)) { 389 | $this->test->fail('Output does not contain "'.Arr::first($this->test->expectedOutputSubstrings).'".'); 390 | } 391 | 392 | if ($output = array_search(true, $this->test->unexpectedOutput)) { 393 | $this->test->fail('Output "'.$output.'" was printed.'); 394 | } 395 | 396 | if ($output = array_search(true, $this->test->unexpectedOutputSubstrings)) { 397 | $this->test->fail('Output "'.$output.'" was printed.'); 398 | } 399 | } 400 | 401 | /** 402 | * Mock the application's console output. 403 | * 404 | * @return \Mockery\MockInterface 405 | */ 406 | protected function mockConsoleOutput() 407 | { 408 | $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [ 409 | new ArrayInput($this->parameters), $this->createABufferedOutputMock(), 410 | ]); 411 | 412 | foreach ($this->test->expectedQuestions as $i => $question) { 413 | $mock->shouldReceive('askQuestion') 414 | ->once() 415 | ->ordered() 416 | ->with(Mockery::on(function ($argument) use ($question) { 417 | if (isset($this->test->expectedChoices[$question[0]])) { 418 | $this->test->expectedChoices[$question[0]]['actual'] = $argument instanceof ChoiceQuestion && ! array_is_list($this->test->expectedChoices[$question[0]]['expected']) 419 | ? $argument->getChoices() 420 | : $argument->getAutocompleterValues(); 421 | } 422 | 423 | return $argument->getQuestion() == $question[0]; 424 | })) 425 | ->andReturnUsing(function () use ($question, $i) { 426 | unset($this->test->expectedQuestions[$i]); 427 | 428 | return $question[1]; 429 | }); 430 | } 431 | 432 | $this->app->bind(OutputStyle::class, function () use ($mock) { 433 | return $mock; 434 | }); 435 | 436 | return $mock; 437 | } 438 | 439 | /** 440 | * Create a mock for the buffered output. 441 | * 442 | * @return \Mockery\MockInterface 443 | */ 444 | private function createABufferedOutputMock() 445 | { 446 | $mock = Mockery::mock(BufferedOutput::class.'[doWrite]') 447 | ->shouldAllowMockingProtectedMethods() 448 | ->shouldIgnoreMissing(); 449 | 450 | if ($this->test->expectsOutput === false) { 451 | $mock->shouldReceive('doWrite')->never(); 452 | 453 | return $mock; 454 | } 455 | 456 | if ($this->test->expectsOutput === true 457 | && count($this->test->expectedOutput) === 0 458 | && count($this->test->expectedOutputSubstrings) === 0) { 459 | $mock->shouldReceive('doWrite')->atLeast()->once(); 460 | } 461 | 462 | foreach ($this->test->expectedOutput as $i => $output) { 463 | $mock->shouldReceive('doWrite') 464 | ->once() 465 | ->ordered() 466 | ->with($output, Mockery::any()) 467 | ->andReturnUsing(function () use ($i) { 468 | unset($this->test->expectedOutput[$i]); 469 | }); 470 | } 471 | 472 | foreach ($this->test->expectedOutputSubstrings as $i => $text) { 473 | $mock->shouldReceive('doWrite') 474 | ->atLeast() 475 | ->times(0) 476 | ->withArgs(fn ($output) => str_contains($output, $text)) 477 | ->andReturnUsing(function () use ($i) { 478 | unset($this->test->expectedOutputSubstrings[$i]); 479 | }); 480 | } 481 | 482 | foreach ($this->test->unexpectedOutput as $output => $displayed) { 483 | $mock->shouldReceive('doWrite') 484 | ->atLeast() 485 | ->times(0) 486 | ->ordered() 487 | ->with($output, Mockery::any()) 488 | ->andReturnUsing(function () use ($output) { 489 | $this->test->unexpectedOutput[$output] = true; 490 | }); 491 | } 492 | 493 | foreach ($this->test->unexpectedOutputSubstrings as $text => $displayed) { 494 | $mock->shouldReceive('doWrite') 495 | ->atLeast() 496 | ->times(0) 497 | ->withArgs(fn ($output) => str_contains($output, $text)) 498 | ->andReturnUsing(function () use ($text) { 499 | $this->test->unexpectedOutputSubstrings[$text] = true; 500 | }); 501 | } 502 | 503 | return $mock; 504 | } 505 | 506 | /** 507 | * Flush the expectations from the test case. 508 | * 509 | * @return void 510 | */ 511 | protected function flushExpectations() 512 | { 513 | $this->test->expectedOutput = []; 514 | $this->test->expectedOutputSubstrings = []; 515 | $this->test->unexpectedOutput = []; 516 | $this->test->unexpectedOutputSubstrings = []; 517 | $this->test->expectedTables = []; 518 | $this->test->expectedQuestions = []; 519 | $this->test->expectedChoices = []; 520 | } 521 | 522 | /** 523 | * Handle the object's destruction. 524 | * 525 | * @return void 526 | */ 527 | public function __destruct() 528 | { 529 | if ($this->hasExecuted) { 530 | return; 531 | } 532 | 533 | $this->run(); 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /TestComponent.php: -------------------------------------------------------------------------------- 1 | component = $component; 39 | 40 | $this->rendered = $view->render(); 41 | } 42 | 43 | /** 44 | * Assert that the given string is contained within the rendered component. 45 | * 46 | * @param string $value 47 | * @param bool $escape 48 | * @return $this 49 | */ 50 | public function assertSee($value, $escape = true) 51 | { 52 | $value = $escape ? e($value) : $value; 53 | 54 | PHPUnit::assertStringContainsString((string) $value, $this->rendered); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Assert that the given strings are contained in order within the rendered component. 61 | * 62 | * @param array $values 63 | * @param bool $escape 64 | * @return $this 65 | */ 66 | public function assertSeeInOrder(array $values, $escape = true) 67 | { 68 | $values = $escape ? array_map(e(...), $values) : $values; 69 | 70 | PHPUnit::assertThat($values, new SeeInOrder($this->rendered)); 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Assert that the given string is contained within the rendered component text. 77 | * 78 | * @param string $value 79 | * @param bool $escape 80 | * @return $this 81 | */ 82 | public function assertSeeText($value, $escape = true) 83 | { 84 | $value = $escape ? e($value) : $value; 85 | 86 | PHPUnit::assertStringContainsString((string) $value, strip_tags($this->rendered)); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Assert that the given strings are contained in order within the rendered component text. 93 | * 94 | * @param array $values 95 | * @param bool $escape 96 | * @return $this 97 | */ 98 | public function assertSeeTextInOrder(array $values, $escape = true) 99 | { 100 | $values = $escape ? array_map(e(...), $values) : $values; 101 | 102 | PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->rendered))); 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Assert that the given string is not contained within the rendered component. 109 | * 110 | * @param string $value 111 | * @param bool $escape 112 | * @return $this 113 | */ 114 | public function assertDontSee($value, $escape = true) 115 | { 116 | $value = $escape ? e($value) : $value; 117 | 118 | PHPUnit::assertStringNotContainsString((string) $value, $this->rendered); 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * Assert that the given string is not contained within the rendered component text. 125 | * 126 | * @param string $value 127 | * @param bool $escape 128 | * @return $this 129 | */ 130 | public function assertDontSeeText($value, $escape = true) 131 | { 132 | $value = $escape ? e($value) : $value; 133 | 134 | PHPUnit::assertStringNotContainsString((string) $value, strip_tags($this->rendered)); 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Get the string contents of the rendered component. 141 | * 142 | * @return string 143 | */ 144 | public function __toString() 145 | { 146 | return $this->rendered; 147 | } 148 | 149 | /** 150 | * Dynamically access properties on the underlying component. 151 | * 152 | * @param string $attribute 153 | * @return mixed 154 | */ 155 | public function __get($attribute) 156 | { 157 | return $this->component->{$attribute}; 158 | } 159 | 160 | /** 161 | * Dynamically call methods on the underlying component. 162 | * 163 | * @param string $method 164 | * @param array $parameters 165 | * @return mixed 166 | */ 167 | public function __call($method, $parameters) 168 | { 169 | if (static::hasMacro($method)) { 170 | return $this->macroCall($method, $parameters); 171 | } 172 | 173 | return $this->component->{$method}(...$parameters); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /TestResponse.php: -------------------------------------------------------------------------------- 1 | baseResponse = $response; 78 | $this->baseRequest = $request; 79 | $this->exceptions = new Collection; 80 | } 81 | 82 | /** 83 | * Create a new TestResponse from another response. 84 | * 85 | * @template R of TResponse 86 | * 87 | * @param R $response 88 | * @param \Illuminate\Http\Request|null $request 89 | * @return static 90 | */ 91 | public static function fromBaseResponse($response, $request = null) 92 | { 93 | return new static($response, $request); 94 | } 95 | 96 | /** 97 | * Assert that the response has a successful status code. 98 | * 99 | * @return $this 100 | */ 101 | public function assertSuccessful() 102 | { 103 | PHPUnit::withResponse($this)->assertTrue( 104 | $this->isSuccessful(), 105 | $this->statusMessageWithDetails('>=200, <300', $this->getStatusCode()) 106 | ); 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Assert that the Precognition request was successful. 113 | * 114 | * @return $this 115 | */ 116 | public function assertSuccessfulPrecognition() 117 | { 118 | $this->assertNoContent(); 119 | 120 | PHPUnit::withResponse($this)->assertTrue( 121 | $this->headers->has('Precognition-Success'), 122 | 'Header [Precognition-Success] not present on response.' 123 | ); 124 | 125 | PHPUnit::withResponse($this)->assertSame( 126 | 'true', 127 | $this->headers->get('Precognition-Success'), 128 | 'The Precognition-Success header was found, but the value is not `true`.' 129 | ); 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Assert that the response is a server error. 136 | * 137 | * @return $this 138 | */ 139 | public function assertServerError() 140 | { 141 | PHPUnit::withResponse($this)->assertTrue( 142 | $this->isServerError(), 143 | $this->statusMessageWithDetails('>=500, < 600', $this->getStatusCode()) 144 | ); 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Assert that the response has the given status code. 151 | * 152 | * @param int $status 153 | * @return $this 154 | */ 155 | public function assertStatus($status) 156 | { 157 | $message = $this->statusMessageWithDetails($status, $actual = $this->getStatusCode()); 158 | 159 | PHPUnit::withResponse($this)->assertSame($status, $actual, $message); 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Get an assertion message for a status assertion containing extra details when available. 166 | * 167 | * @param string|int $expected 168 | * @param string|int $actual 169 | * @return string 170 | */ 171 | protected function statusMessageWithDetails($expected, $actual) 172 | { 173 | return "Expected response status code [{$expected}] but received {$actual}."; 174 | } 175 | 176 | /** 177 | * Assert whether the response is redirecting to a given URI. 178 | * 179 | * @param string|null $uri 180 | * @return $this 181 | */ 182 | public function assertRedirect($uri = null) 183 | { 184 | PHPUnit::withResponse($this)->assertTrue( 185 | $this->isRedirect(), 186 | $this->statusMessageWithDetails('201, 301, 302, 303, 307, 308', $this->getStatusCode()), 187 | ); 188 | 189 | if (! is_null($uri)) { 190 | $this->assertLocation($uri); 191 | } 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Assert whether the response is redirecting to a URI that contains the given URI. 198 | * 199 | * @param string $uri 200 | * @return $this 201 | */ 202 | public function assertRedirectContains($uri) 203 | { 204 | PHPUnit::withResponse($this)->assertTrue( 205 | $this->isRedirect(), 206 | $this->statusMessageWithDetails('201, 301, 302, 303, 307, 308', $this->getStatusCode()), 207 | ); 208 | 209 | PHPUnit::withResponse($this)->assertTrue( 210 | Str::contains($this->headers->get('Location'), $uri), 'Redirect location ['.$this->headers->get('Location').'] does not contain ['.$uri.'].' 211 | ); 212 | 213 | return $this; 214 | } 215 | 216 | /** 217 | * Assert whether the response is redirecting back to the previous location. 218 | * 219 | * @return $this 220 | */ 221 | public function assertRedirectBack() 222 | { 223 | PHPUnit::withResponse($this)->assertTrue( 224 | $this->isRedirect(), 225 | $this->statusMessageWithDetails('201, 301, 302, 303, 307, 308', $this->getStatusCode()), 226 | ); 227 | 228 | $this->assertLocation(app('url')->previous()); 229 | 230 | return $this; 231 | } 232 | 233 | /** 234 | * Assert whether the response is redirecting to a given route. 235 | * 236 | * @param \BackedEnum|string $name 237 | * @param mixed $parameters 238 | * @return $this 239 | */ 240 | public function assertRedirectToRoute($name, $parameters = []) 241 | { 242 | $uri = route($name, $parameters); 243 | 244 | PHPUnit::withResponse($this)->assertTrue( 245 | $this->isRedirect(), 246 | $this->statusMessageWithDetails('201, 301, 302, 303, 307, 308', $this->getStatusCode()), 247 | ); 248 | 249 | $this->assertLocation($uri); 250 | 251 | return $this; 252 | } 253 | 254 | /** 255 | * Assert whether the response is redirecting to a given signed route. 256 | * 257 | * @param \BackedEnum|string|null $name 258 | * @param mixed $parameters 259 | * @param bool $absolute 260 | * @return $this 261 | */ 262 | public function assertRedirectToSignedRoute($name = null, $parameters = [], $absolute = true) 263 | { 264 | if (! is_null($name)) { 265 | $uri = route($name, $parameters); 266 | } 267 | 268 | PHPUnit::withResponse($this)->assertTrue( 269 | $this->isRedirect(), 270 | $this->statusMessageWithDetails('201, 301, 302, 303, 307, 308', $this->getStatusCode()), 271 | ); 272 | 273 | $request = Request::create($this->headers->get('Location')); 274 | 275 | PHPUnit::withResponse($this)->assertTrue( 276 | $request->hasValidSignature($absolute), 'The response is not a redirect to a signed route.' 277 | ); 278 | 279 | if (! is_null($name)) { 280 | $expectedUri = rtrim($request->fullUrlWithQuery([ 281 | 'signature' => null, 282 | 'expires' => null, 283 | ]), '?'); 284 | 285 | PHPUnit::withResponse($this)->assertEquals( 286 | app('url')->to($uri), $expectedUri 287 | ); 288 | } 289 | 290 | return $this; 291 | } 292 | 293 | /** 294 | * Asserts that the response contains the given header and equals the optional value. 295 | * 296 | * @param string $headerName 297 | * @param mixed $value 298 | * @return $this 299 | */ 300 | public function assertHeader($headerName, $value = null) 301 | { 302 | PHPUnit::withResponse($this)->assertTrue( 303 | $this->headers->has($headerName), "Header [{$headerName}] not present on response." 304 | ); 305 | 306 | $actual = $this->headers->get($headerName); 307 | 308 | if (! is_null($value)) { 309 | PHPUnit::withResponse($this)->assertEquals( 310 | $value, $this->headers->get($headerName), 311 | "Header [{$headerName}] was found, but value [{$actual}] does not match [{$value}]." 312 | ); 313 | } 314 | 315 | return $this; 316 | } 317 | 318 | /** 319 | * Asserts that the response does not contain the given header. 320 | * 321 | * @param string $headerName 322 | * @return $this 323 | */ 324 | public function assertHeaderMissing($headerName) 325 | { 326 | PHPUnit::withResponse($this)->assertFalse( 327 | $this->headers->has($headerName), "Unexpected header [{$headerName}] is present on response." 328 | ); 329 | 330 | return $this; 331 | } 332 | 333 | /** 334 | * Assert that the current location header matches the given URI. 335 | * 336 | * @param string $uri 337 | * @return $this 338 | */ 339 | public function assertLocation($uri) 340 | { 341 | PHPUnit::withResponse($this)->assertEquals( 342 | app('url')->to($uri), app('url')->to($this->headers->get('Location', '')) 343 | ); 344 | 345 | return $this; 346 | } 347 | 348 | /** 349 | * Assert that the response offers a file download. 350 | * 351 | * @param string|null $filename 352 | * @return $this 353 | */ 354 | public function assertDownload($filename = null) 355 | { 356 | $contentDisposition = explode(';', $this->headers->get('content-disposition', '')); 357 | 358 | if (trim($contentDisposition[0]) !== 'attachment') { 359 | PHPUnit::withResponse($this)->fail( 360 | 'Response does not offer a file download.'.PHP_EOL. 361 | 'Disposition ['.trim($contentDisposition[0]).'] found in header, [attachment] expected.' 362 | ); 363 | } 364 | 365 | if (! is_null($filename)) { 366 | if (isset($contentDisposition[1]) && 367 | trim(explode('=', $contentDisposition[1])[0]) !== 'filename') { 368 | PHPUnit::withResponse($this)->fail( 369 | 'Unsupported Content-Disposition header provided.'.PHP_EOL. 370 | 'Disposition ['.trim(explode('=', $contentDisposition[1])[0]).'] found in header, [filename] expected.' 371 | ); 372 | } 373 | 374 | $message = "Expected file [{$filename}] is not present in Content-Disposition header."; 375 | 376 | if (! isset($contentDisposition[1])) { 377 | PHPUnit::withResponse($this)->fail($message); 378 | } else { 379 | PHPUnit::withResponse($this)->assertSame( 380 | $filename, 381 | isset(explode('=', $contentDisposition[1])[1]) 382 | ? trim(explode('=', $contentDisposition[1])[1], " \"'") 383 | : '', 384 | $message 385 | ); 386 | 387 | return $this; 388 | } 389 | } else { 390 | PHPUnit::withResponse($this)->assertTrue(true); 391 | 392 | return $this; 393 | } 394 | } 395 | 396 | /** 397 | * Asserts that the response contains the given cookie and equals the optional value. 398 | * 399 | * @param string $cookieName 400 | * @param mixed $value 401 | * @return $this 402 | */ 403 | public function assertPlainCookie($cookieName, $value = null) 404 | { 405 | $this->assertCookie($cookieName, $value, false); 406 | 407 | return $this; 408 | } 409 | 410 | /** 411 | * Asserts that the response contains the given cookie and equals the optional value. 412 | * 413 | * @param string $cookieName 414 | * @param mixed $value 415 | * @param bool $encrypted 416 | * @param bool $unserialize 417 | * @return $this 418 | */ 419 | public function assertCookie($cookieName, $value = null, $encrypted = true, $unserialize = false) 420 | { 421 | PHPUnit::withResponse($this)->assertNotNull( 422 | $cookie = $this->getCookie($cookieName, $encrypted && ! is_null($value), $unserialize), 423 | "Cookie [{$cookieName}] not present on response." 424 | ); 425 | 426 | if (! $cookie || is_null($value)) { 427 | return $this; 428 | } 429 | 430 | $cookieValue = $cookie->getValue(); 431 | 432 | PHPUnit::withResponse($this)->assertEquals( 433 | $value, $cookieValue, 434 | "Cookie [{$cookieName}] was found, but value [{$cookieValue}] does not match [{$value}]." 435 | ); 436 | 437 | return $this; 438 | } 439 | 440 | /** 441 | * Asserts that the response contains the given cookie and is expired. 442 | * 443 | * @param string $cookieName 444 | * @return $this 445 | */ 446 | public function assertCookieExpired($cookieName) 447 | { 448 | PHPUnit::withResponse($this)->assertNotNull( 449 | $cookie = $this->getCookie($cookieName, false), 450 | "Cookie [{$cookieName}] not present on response." 451 | ); 452 | 453 | $expiresAt = Carbon::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); 454 | 455 | PHPUnit::withResponse($this)->assertTrue( 456 | $cookie->getExpiresTime() !== 0 && $expiresAt->lessThan(Carbon::now()), 457 | "Cookie [{$cookieName}] is not expired, it expires at [{$expiresAt}]." 458 | ); 459 | 460 | return $this; 461 | } 462 | 463 | /** 464 | * Asserts that the response contains the given cookie and is not expired. 465 | * 466 | * @param string $cookieName 467 | * @return $this 468 | */ 469 | public function assertCookieNotExpired($cookieName) 470 | { 471 | PHPUnit::withResponse($this)->assertNotNull( 472 | $cookie = $this->getCookie($cookieName, false), 473 | "Cookie [{$cookieName}] not present on response." 474 | ); 475 | 476 | $expiresAt = Carbon::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); 477 | 478 | PHPUnit::withResponse($this)->assertTrue( 479 | $cookie->getExpiresTime() === 0 || $expiresAt->greaterThan(Carbon::now()), 480 | "Cookie [{$cookieName}] is expired, it expired at [{$expiresAt}]." 481 | ); 482 | 483 | return $this; 484 | } 485 | 486 | /** 487 | * Asserts that the response does not contain the given cookie. 488 | * 489 | * @param string $cookieName 490 | * @return $this 491 | */ 492 | public function assertCookieMissing($cookieName) 493 | { 494 | PHPUnit::withResponse($this)->assertNull( 495 | $this->getCookie($cookieName, false), 496 | "Cookie [{$cookieName}] is present on response." 497 | ); 498 | 499 | return $this; 500 | } 501 | 502 | /** 503 | * Get the given cookie from the response. 504 | * 505 | * @param string $cookieName 506 | * @param bool $decrypt 507 | * @param bool $unserialize 508 | * @return \Symfony\Component\HttpFoundation\Cookie|null 509 | */ 510 | public function getCookie($cookieName, $decrypt = true, $unserialize = false) 511 | { 512 | foreach ($this->headers->getCookies() as $cookie) { 513 | if ($cookie->getName() === $cookieName) { 514 | if (! $decrypt) { 515 | return $cookie; 516 | } 517 | 518 | $decryptedValue = CookieValuePrefix::remove( 519 | app('encrypter')->decrypt($cookie->getValue(), $unserialize) 520 | ); 521 | 522 | return new Cookie( 523 | $cookie->getName(), 524 | $decryptedValue, 525 | $cookie->getExpiresTime(), 526 | $cookie->getPath(), 527 | $cookie->getDomain(), 528 | $cookie->isSecure(), 529 | $cookie->isHttpOnly(), 530 | $cookie->isRaw(), 531 | $cookie->getSameSite(), 532 | $cookie->isPartitioned() 533 | ); 534 | } 535 | } 536 | } 537 | 538 | /** 539 | * Assert that the given string matches the response content. 540 | * 541 | * @param string $value 542 | * @return $this 543 | */ 544 | public function assertContent($value) 545 | { 546 | PHPUnit::withResponse($this)->assertSame($value, $this->getContent()); 547 | 548 | return $this; 549 | } 550 | 551 | /** 552 | * Assert that the response was streamed. 553 | * 554 | * @return $this 555 | */ 556 | public function assertStreamed() 557 | { 558 | PHPUnit::withResponse($this)->assertTrue( 559 | $this->baseResponse instanceof StreamedResponse || $this->baseResponse instanceof StreamedJsonResponse, 560 | 'Expected the response to be streamed, but it wasn\'t.' 561 | ); 562 | 563 | return $this; 564 | } 565 | 566 | /** 567 | * Assert that the response was not streamed. 568 | * 569 | * @return $this 570 | */ 571 | public function assertNotStreamed() 572 | { 573 | PHPUnit::withResponse($this)->assertTrue( 574 | ! $this->baseResponse instanceof StreamedResponse && ! $this->baseResponse instanceof StreamedJsonResponse, 575 | 'Response was unexpectedly streamed.' 576 | ); 577 | 578 | return $this; 579 | } 580 | 581 | /** 582 | * Assert that the given string matches the streamed response content. 583 | * 584 | * @param string $value 585 | * @return $this 586 | */ 587 | public function assertStreamedContent($value) 588 | { 589 | PHPUnit::withResponse($this)->assertSame($value, $this->streamedContent()); 590 | 591 | return $this; 592 | } 593 | 594 | /** 595 | * Assert that the given array matches the streamed JSON response content. 596 | * 597 | * @param array $value 598 | * @return $this 599 | */ 600 | public function assertStreamedJsonContent($value) 601 | { 602 | return $this->assertStreamedContent(json_encode($value, JSON_THROW_ON_ERROR)); 603 | } 604 | 605 | /** 606 | * Assert that the given string or array of strings are contained within the response. 607 | * 608 | * @param string|array $value 609 | * @param bool $escape 610 | * @return $this 611 | */ 612 | public function assertSee($value, $escape = true) 613 | { 614 | $value = Arr::wrap($value); 615 | 616 | $values = $escape ? array_map(e(...), $value) : $value; 617 | 618 | foreach ($values as $value) { 619 | PHPUnit::withResponse($this)->assertStringContainsString((string) $value, $this->getContent()); 620 | } 621 | 622 | return $this; 623 | } 624 | 625 | /** 626 | * Assert that the given HTML string or array of HTML strings are contained within the response. 627 | * 628 | * @param array|string $value 629 | * @return $this 630 | */ 631 | public function assertSeeHtml($value) 632 | { 633 | return $this->assertSee($value, false); 634 | } 635 | 636 | /** 637 | * Assert that the given strings are contained in order within the response. 638 | * 639 | * @param array $values 640 | * @param bool $escape 641 | * @return $this 642 | */ 643 | public function assertSeeInOrder(array $values, $escape = true) 644 | { 645 | $values = $escape ? array_map(e(...), $values) : $values; 646 | 647 | PHPUnit::withResponse($this)->assertThat($values, new SeeInOrder($this->getContent())); 648 | 649 | return $this; 650 | } 651 | 652 | /** 653 | * Assert that the given HTML strings are contained in order within the response. 654 | * 655 | * @param array $values 656 | * @return $this 657 | */ 658 | public function assertSeeHtmlInOrder(array $values) 659 | { 660 | return $this->assertSeeInOrder($values, false); 661 | } 662 | 663 | /** 664 | * Assert that the given string or array of strings are contained within the response text. 665 | * 666 | * @param string|array $value 667 | * @param bool $escape 668 | * @return $this 669 | */ 670 | public function assertSeeText($value, $escape = true) 671 | { 672 | $value = Arr::wrap($value); 673 | 674 | $values = $escape ? array_map(e(...), $value) : $value; 675 | 676 | $content = strip_tags($this->getContent()); 677 | 678 | foreach ($values as $value) { 679 | PHPUnit::withResponse($this)->assertStringContainsString((string) $value, $content); 680 | } 681 | 682 | return $this; 683 | } 684 | 685 | /** 686 | * Assert that the given strings are contained in order within the response text. 687 | * 688 | * @param array $values 689 | * @param bool $escape 690 | * @return $this 691 | */ 692 | public function assertSeeTextInOrder(array $values, $escape = true) 693 | { 694 | $values = $escape ? array_map(e(...), $values) : $values; 695 | 696 | PHPUnit::withResponse($this)->assertThat($values, new SeeInOrder(strip_tags($this->getContent()))); 697 | 698 | return $this; 699 | } 700 | 701 | /** 702 | * Assert that the given string or array of strings are not contained within the response. 703 | * 704 | * @param string|array $value 705 | * @param bool $escape 706 | * @return $this 707 | */ 708 | public function assertDontSee($value, $escape = true) 709 | { 710 | $value = Arr::wrap($value); 711 | 712 | $values = $escape ? array_map(e(...), $value) : $value; 713 | 714 | foreach ($values as $value) { 715 | PHPUnit::withResponse($this)->assertStringNotContainsString((string) $value, $this->getContent()); 716 | } 717 | 718 | return $this; 719 | } 720 | 721 | /** 722 | * Assert that the given HTML string or array of HTML strings are not contained within the response. 723 | * 724 | * @param array|string $value 725 | * @return $this 726 | */ 727 | public function assertDontSeeHtml($value) 728 | { 729 | return $this->assertDontSee($value, false); 730 | } 731 | 732 | /** 733 | * Assert that the given string or array of strings are not contained within the response text. 734 | * 735 | * @param string|array $value 736 | * @param bool $escape 737 | * @return $this 738 | */ 739 | public function assertDontSeeText($value, $escape = true) 740 | { 741 | $value = Arr::wrap($value); 742 | 743 | $values = $escape ? array_map(e(...), $value) : $value; 744 | 745 | $content = strip_tags($this->getContent()); 746 | 747 | foreach ($values as $value) { 748 | PHPUnit::withResponse($this)->assertStringNotContainsString((string) $value, $content); 749 | } 750 | 751 | return $this; 752 | } 753 | 754 | /** 755 | * Assert that the response is a superset of the given JSON. 756 | * 757 | * @param array|callable $value 758 | * @param bool $strict 759 | * @return $this 760 | */ 761 | public function assertJson($value, $strict = false) 762 | { 763 | $json = $this->decodeResponseJson(); 764 | 765 | if (is_array($value)) { 766 | $json->assertSubset($value, $strict); 767 | } else { 768 | $assert = AssertableJson::fromAssertableJsonString($json); 769 | 770 | $value($assert); 771 | 772 | if (Arr::isAssoc($assert->toArray())) { 773 | $assert->interacted(); 774 | } 775 | } 776 | 777 | return $this; 778 | } 779 | 780 | /** 781 | * Assert that the expected value and type exists at the given path in the response. 782 | * 783 | * @param string $path 784 | * @param mixed $expect 785 | * @return $this 786 | */ 787 | public function assertJsonPath($path, $expect) 788 | { 789 | $this->decodeResponseJson()->assertPath($path, $expect); 790 | 791 | return $this; 792 | } 793 | 794 | /** 795 | * Assert that the given path in the response contains all of the expected values without looking at the order. 796 | * 797 | * @param string $path 798 | * @param array $expect 799 | * @return $this 800 | */ 801 | public function assertJsonPathCanonicalizing($path, array $expect) 802 | { 803 | $this->decodeResponseJson()->assertPathCanonicalizing($path, $expect); 804 | 805 | return $this; 806 | } 807 | 808 | /** 809 | * Assert that the response has the exact given JSON. 810 | * 811 | * @param array $data 812 | * @return $this 813 | */ 814 | public function assertExactJson(array $data) 815 | { 816 | $this->decodeResponseJson()->assertExact($data); 817 | 818 | return $this; 819 | } 820 | 821 | /** 822 | * Assert that the response has the similar JSON as given. 823 | * 824 | * @param array $data 825 | * @return $this 826 | */ 827 | public function assertSimilarJson(array $data) 828 | { 829 | $this->decodeResponseJson()->assertSimilar($data); 830 | 831 | return $this; 832 | } 833 | 834 | /** 835 | * Assert that the response contains the given JSON fragments. 836 | * 837 | * @param array $data 838 | * @return $this 839 | */ 840 | public function assertJsonFragments(array $data) 841 | { 842 | foreach ($data as $fragment) { 843 | $this->assertJsonFragment($fragment); 844 | } 845 | 846 | return $this; 847 | } 848 | 849 | /** 850 | * Assert that the response contains the given JSON fragment. 851 | * 852 | * @param array $data 853 | * @return $this 854 | */ 855 | public function assertJsonFragment(array $data) 856 | { 857 | $this->decodeResponseJson()->assertFragment($data); 858 | 859 | return $this; 860 | } 861 | 862 | /** 863 | * Assert that the response does not contain the given JSON fragment. 864 | * 865 | * @param array $data 866 | * @param bool $exact 867 | * @return $this 868 | */ 869 | public function assertJsonMissing(array $data, $exact = false) 870 | { 871 | $this->decodeResponseJson()->assertMissing($data, $exact); 872 | 873 | return $this; 874 | } 875 | 876 | /** 877 | * Assert that the response does not contain the exact JSON fragment. 878 | * 879 | * @param array $data 880 | * @return $this 881 | */ 882 | public function assertJsonMissingExact(array $data) 883 | { 884 | $this->decodeResponseJson()->assertMissingExact($data); 885 | 886 | return $this; 887 | } 888 | 889 | /** 890 | * Assert that the response does not contain the given path. 891 | * 892 | * @param string $path 893 | * @return $this 894 | */ 895 | public function assertJsonMissingPath(string $path) 896 | { 897 | $this->decodeResponseJson()->assertMissingPath($path); 898 | 899 | return $this; 900 | } 901 | 902 | /** 903 | * Assert that the response has a given JSON structure. 904 | * 905 | * @param array|null $structure 906 | * @param array|null $responseData 907 | * @return $this 908 | */ 909 | public function assertJsonStructure(?array $structure = null, $responseData = null) 910 | { 911 | $this->decodeResponseJson()->assertStructure($structure, $responseData); 912 | 913 | return $this; 914 | } 915 | 916 | /** 917 | * Assert that the response has the exact JSON structure. 918 | * 919 | * @param array|null $structure 920 | * @param array|null $responseData 921 | * @return $this 922 | */ 923 | public function assertExactJsonStructure(?array $structure = null, $responseData = null) 924 | { 925 | $this->decodeResponseJson()->assertStructure($structure, $responseData, true); 926 | 927 | return $this; 928 | } 929 | 930 | /** 931 | * Assert that the response JSON has the expected count of items at the given key. 932 | * 933 | * @param int $count 934 | * @param string|null $key 935 | * @return $this 936 | */ 937 | public function assertJsonCount(int $count, $key = null) 938 | { 939 | $this->decodeResponseJson()->assertCount($count, $key); 940 | 941 | return $this; 942 | } 943 | 944 | /** 945 | * Assert that the response has the given JSON validation errors. 946 | * 947 | * @param string|array $errors 948 | * @param string $responseKey 949 | * @return $this 950 | */ 951 | public function assertJsonValidationErrors($errors, $responseKey = 'errors') 952 | { 953 | $errors = Arr::wrap($errors); 954 | 955 | PHPUnit::withResponse($this)->assertNotEmpty($errors, 'No validation errors were provided.'); 956 | 957 | $jsonErrors = Arr::get($this->json(), $responseKey) ?? []; 958 | 959 | $errorMessage = $jsonErrors 960 | ? 'Response has the following JSON validation errors:'. 961 | PHP_EOL.PHP_EOL.json_encode($jsonErrors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).PHP_EOL 962 | : 'Response does not have JSON validation errors.'; 963 | 964 | foreach ($errors as $key => $value) { 965 | if (is_int($key)) { 966 | $this->assertJsonValidationErrorFor($value, $responseKey); 967 | 968 | continue; 969 | } 970 | 971 | $this->assertJsonValidationErrorFor($key, $responseKey); 972 | 973 | foreach (Arr::wrap($value) as $expectedMessage) { 974 | $errorMissing = true; 975 | 976 | foreach (Arr::wrap($jsonErrors[$key]) as $jsonErrorMessage) { 977 | if (Str::contains($jsonErrorMessage, $expectedMessage)) { 978 | $errorMissing = false; 979 | 980 | break; 981 | } 982 | } 983 | 984 | if ($errorMissing) { 985 | PHPUnit::withResponse($this)->fail( 986 | "Failed to find a validation error in the response for key and message: '$key' => '$expectedMessage'".PHP_EOL.PHP_EOL.$errorMessage 987 | ); 988 | } 989 | } 990 | } 991 | 992 | return $this; 993 | } 994 | 995 | /** 996 | * Assert that the response has the given JSON validation errors but does not have any other JSON validation errors. 997 | * 998 | * @param string|array $errors 999 | * @param string $responseKey 1000 | * @return $this 1001 | */ 1002 | public function assertOnlyJsonValidationErrors($errors, $responseKey = 'errors') 1003 | { 1004 | $this->assertJsonValidationErrors($errors, $responseKey); 1005 | 1006 | $jsonErrors = Arr::get($this->json(), $responseKey) ?? []; 1007 | 1008 | $expectedErrorKeys = (new Collection($errors)) 1009 | ->map(fn ($value, $key) => is_int($key) ? $value : $key) 1010 | ->all(); 1011 | 1012 | $unexpectedErrorKeys = Arr::except($jsonErrors, $expectedErrorKeys); 1013 | 1014 | PHPUnit::withResponse($this)->assertTrue( 1015 | count($unexpectedErrorKeys) === 0, 1016 | 'Response has unexpected validation errors: '.(new Collection($unexpectedErrorKeys))->keys()->map(fn ($key) => "'{$key}'")->join(', ') 1017 | ); 1018 | 1019 | return $this; 1020 | } 1021 | 1022 | /** 1023 | * Assert the response has any JSON validation errors for the given key. 1024 | * 1025 | * @param string $key 1026 | * @param string $responseKey 1027 | * @return $this 1028 | */ 1029 | public function assertJsonValidationErrorFor($key, $responseKey = 'errors') 1030 | { 1031 | $jsonErrors = Arr::get($this->json(), $responseKey) ?? []; 1032 | 1033 | $errorMessage = $jsonErrors 1034 | ? 'Response has the following JSON validation errors:'. 1035 | PHP_EOL.PHP_EOL.json_encode($jsonErrors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).PHP_EOL 1036 | : 'Response does not have JSON validation errors.'; 1037 | 1038 | PHPUnit::withResponse($this)->assertArrayHasKey( 1039 | $key, 1040 | $jsonErrors, 1041 | "Failed to find a validation error in the response for key: '{$key}'".PHP_EOL.PHP_EOL.$errorMessage 1042 | ); 1043 | 1044 | return $this; 1045 | } 1046 | 1047 | /** 1048 | * Assert that the response has no JSON validation errors for the given keys. 1049 | * 1050 | * @param string|array|null $keys 1051 | * @param string $responseKey 1052 | * @return $this 1053 | */ 1054 | public function assertJsonMissingValidationErrors($keys = null, $responseKey = 'errors') 1055 | { 1056 | if ($this->getContent() === '') { 1057 | PHPUnit::withResponse($this)->assertTrue(true); 1058 | 1059 | return $this; 1060 | } 1061 | 1062 | $json = $this->json(); 1063 | 1064 | if (! Arr::has($json, $responseKey)) { 1065 | PHPUnit::withResponse($this)->assertTrue(true); 1066 | 1067 | return $this; 1068 | } 1069 | 1070 | $errors = Arr::get($json, $responseKey, []); 1071 | 1072 | if (is_null($keys) && count($errors) > 0) { 1073 | PHPUnit::withResponse($this)->fail( 1074 | 'Response has unexpected validation errors: '.PHP_EOL.PHP_EOL. 1075 | json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) 1076 | ); 1077 | } 1078 | 1079 | foreach (Arr::wrap($keys) as $key) { 1080 | PHPUnit::withResponse($this)->assertFalse( 1081 | isset($errors[$key]), 1082 | "Found unexpected validation error for key: '{$key}'" 1083 | ); 1084 | } 1085 | 1086 | return $this; 1087 | } 1088 | 1089 | /** 1090 | * Assert that the given key is a JSON array. 1091 | * 1092 | * @param string|null $key 1093 | * @return $this 1094 | */ 1095 | public function assertJsonIsArray($key = null) 1096 | { 1097 | $data = $this->json($key); 1098 | 1099 | $encodedData = json_encode($data); 1100 | 1101 | PHPUnit::withResponse($this)->assertTrue( 1102 | is_array($data) 1103 | && str_starts_with($encodedData, '[') 1104 | && str_ends_with($encodedData, ']') 1105 | ); 1106 | 1107 | return $this; 1108 | } 1109 | 1110 | /** 1111 | * Assert that the given key is a JSON object. 1112 | * 1113 | * @param string|null $key 1114 | * @return $this 1115 | */ 1116 | public function assertJsonIsObject($key = null) 1117 | { 1118 | $data = $this->json($key); 1119 | 1120 | $encodedData = json_encode($data); 1121 | 1122 | PHPUnit::withResponse($this)->assertTrue( 1123 | is_array($data) 1124 | && str_starts_with($encodedData, '{') 1125 | && str_ends_with($encodedData, '}') 1126 | ); 1127 | 1128 | return $this; 1129 | } 1130 | 1131 | /** 1132 | * Validate the decoded response JSON. 1133 | * 1134 | * @return \Illuminate\Testing\AssertableJsonString 1135 | * 1136 | * @throws \Throwable 1137 | */ 1138 | public function decodeResponseJson() 1139 | { 1140 | if ($this->baseResponse instanceof StreamedResponse || 1141 | $this->baseResponse instanceof StreamedJsonResponse) { 1142 | $testJson = new AssertableJsonString($this->streamedContent()); 1143 | } else { 1144 | $testJson = new AssertableJsonString($this->getContent()); 1145 | } 1146 | 1147 | $decodedResponse = $testJson->json(); 1148 | 1149 | if (is_null($decodedResponse) || $decodedResponse === false) { 1150 | if ($this->exception) { 1151 | throw $this->exception; 1152 | } else { 1153 | PHPUnit::withResponse($this)->fail('Invalid JSON was returned from the route.'); 1154 | } 1155 | } 1156 | 1157 | return $testJson; 1158 | } 1159 | 1160 | /** 1161 | * Return the decoded response JSON. 1162 | * 1163 | * @param string|null $key 1164 | * @return mixed 1165 | */ 1166 | public function json($key = null) 1167 | { 1168 | return $this->decodeResponseJson()->json($key); 1169 | } 1170 | 1171 | /** 1172 | * Get the JSON decoded body of the response as a collection. 1173 | * 1174 | * @param string|null $key 1175 | * @return \Illuminate\Support\Collection 1176 | */ 1177 | public function collect($key = null) 1178 | { 1179 | return new Collection($this->json($key)); 1180 | } 1181 | 1182 | /** 1183 | * Assert that the response view equals the given value. 1184 | * 1185 | * @param string $value 1186 | * @return $this 1187 | */ 1188 | public function assertViewIs($value) 1189 | { 1190 | $this->ensureResponseHasView(); 1191 | 1192 | PHPUnit::withResponse($this)->assertEquals($value, $this->original->name()); 1193 | 1194 | return $this; 1195 | } 1196 | 1197 | /** 1198 | * Assert that the response view has a given piece of bound data. 1199 | * 1200 | * @param string|array $key 1201 | * @param mixed $value 1202 | * @return $this 1203 | */ 1204 | public function assertViewHas($key, $value = null) 1205 | { 1206 | if (is_array($key)) { 1207 | return $this->assertViewHasAll($key); 1208 | } 1209 | 1210 | $this->ensureResponseHasView(); 1211 | 1212 | $actual = Arr::get($this->original->gatherData(), $key); 1213 | 1214 | if (is_null($value)) { 1215 | PHPUnit::withResponse($this)->assertTrue(Arr::has($this->original->gatherData(), $key), "Failed asserting that the data contains the key [{$key}]."); 1216 | } elseif ($value instanceof Closure) { 1217 | PHPUnit::withResponse($this)->assertTrue($value($actual), "Failed asserting that the value at [{$key}] fulfills the expectations defined by the closure."); 1218 | } elseif ($value instanceof Model) { 1219 | PHPUnit::withResponse($this)->assertTrue($value->is($actual), "Failed asserting that the model at [{$key}] matches the given model."); 1220 | } elseif ($value instanceof EloquentCollection) { 1221 | PHPUnit::withResponse($this)->assertInstanceOf(EloquentCollection::class, $actual); 1222 | PHPUnit::withResponse($this)->assertSameSize($value, $actual); 1223 | 1224 | $value->each(fn ($item, $index) => PHPUnit::withResponse($this)->assertTrue($actual->get($index)->is($item), "Failed asserting that the collection at [{$key}.[{$index}]]' matches the given collection.")); 1225 | } else { 1226 | PHPUnit::withResponse($this)->assertEquals($value, $actual, "Failed asserting that [{$key}] matches the expected value."); 1227 | } 1228 | 1229 | return $this; 1230 | } 1231 | 1232 | /** 1233 | * Assert that the response view has a given list of bound data. 1234 | * 1235 | * @param array $bindings 1236 | * @return $this 1237 | */ 1238 | public function assertViewHasAll(array $bindings) 1239 | { 1240 | foreach ($bindings as $key => $value) { 1241 | if (is_int($key)) { 1242 | $this->assertViewHas($value); 1243 | } else { 1244 | $this->assertViewHas($key, $value); 1245 | } 1246 | } 1247 | 1248 | return $this; 1249 | } 1250 | 1251 | /** 1252 | * Get a piece of data from the original view. 1253 | * 1254 | * @param string $key 1255 | * @return mixed 1256 | */ 1257 | public function viewData($key) 1258 | { 1259 | $this->ensureResponseHasView(); 1260 | 1261 | return $this->original->gatherData()[$key]; 1262 | } 1263 | 1264 | /** 1265 | * Assert that the response view is missing a piece of bound data. 1266 | * 1267 | * @param string $key 1268 | * @return $this 1269 | */ 1270 | public function assertViewMissing($key) 1271 | { 1272 | $this->ensureResponseHasView(); 1273 | 1274 | PHPUnit::withResponse($this)->assertFalse(Arr::has($this->original->gatherData(), $key)); 1275 | 1276 | return $this; 1277 | } 1278 | 1279 | /** 1280 | * Ensure that the response has a view as its original content. 1281 | * 1282 | * @return $this 1283 | */ 1284 | protected function ensureResponseHasView() 1285 | { 1286 | if (! $this->responseHasView()) { 1287 | return PHPUnit::withResponse($this)->fail('The response is not a view.'); 1288 | } 1289 | 1290 | return $this; 1291 | } 1292 | 1293 | /** 1294 | * Determine if the original response is a view. 1295 | * 1296 | * @return bool 1297 | */ 1298 | protected function responseHasView() 1299 | { 1300 | return isset($this->original) && $this->original instanceof View; 1301 | } 1302 | 1303 | /** 1304 | * Assert that the given keys do not have validation errors. 1305 | * 1306 | * @param string|array|null $keys 1307 | * @param string $errorBag 1308 | * @param string $responseKey 1309 | * @return $this 1310 | */ 1311 | public function assertValid($keys = null, $errorBag = 'default', $responseKey = 'errors') 1312 | { 1313 | if ($this->baseResponse->headers->get('Content-Type') === 'application/json') { 1314 | return $this->assertJsonMissingValidationErrors($keys, $responseKey); 1315 | } 1316 | 1317 | if ($this->session()->get('errors')) { 1318 | $errors = $this->session()->get('errors')->getBag($errorBag)->getMessages(); 1319 | } else { 1320 | $errors = []; 1321 | } 1322 | 1323 | if (empty($errors)) { 1324 | PHPUnit::withResponse($this)->assertTrue(true); 1325 | 1326 | return $this; 1327 | } 1328 | 1329 | if (is_null($keys) && count($errors) > 0) { 1330 | PHPUnit::withResponse($this)->fail( 1331 | 'Response has unexpected validation errors: '.PHP_EOL.PHP_EOL. 1332 | json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) 1333 | ); 1334 | } 1335 | 1336 | foreach (Arr::wrap($keys) as $key) { 1337 | PHPUnit::withResponse($this)->assertFalse( 1338 | isset($errors[$key]), 1339 | "Found unexpected validation error for key: '{$key}'" 1340 | ); 1341 | } 1342 | 1343 | return $this; 1344 | } 1345 | 1346 | /** 1347 | * Assert that the response has the given validation errors. 1348 | * 1349 | * @param string|array|null $errors 1350 | * @param string $errorBag 1351 | * @param string $responseKey 1352 | * @return $this 1353 | */ 1354 | public function assertInvalid($errors = null, 1355 | $errorBag = 'default', 1356 | $responseKey = 'errors') 1357 | { 1358 | if ($this->baseResponse->headers->get('Content-Type') === 'application/json') { 1359 | return $this->assertJsonValidationErrors($errors, $responseKey); 1360 | } 1361 | 1362 | $this->assertSessionHas('errors'); 1363 | 1364 | $sessionErrors = $this->session()->get('errors')->getBag($errorBag)->getMessages(); 1365 | 1366 | $errorMessage = $sessionErrors 1367 | ? 'Response has the following validation errors in the session:'. 1368 | PHP_EOL.PHP_EOL.json_encode($sessionErrors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).PHP_EOL 1369 | : 'Response does not have validation errors in the session.'; 1370 | 1371 | foreach (Arr::wrap($errors) as $key => $value) { 1372 | PHPUnit::withResponse($this)->assertArrayHasKey( 1373 | $resolvedKey = (is_int($key)) ? $value : $key, 1374 | $sessionErrors, 1375 | "Failed to find a validation error in session for key: '{$resolvedKey}'".PHP_EOL.PHP_EOL.$errorMessage 1376 | ); 1377 | 1378 | foreach (Arr::wrap($value) as $message) { 1379 | if (! is_int($key)) { 1380 | $hasError = false; 1381 | 1382 | foreach (Arr::wrap($sessionErrors[$key]) as $sessionErrorMessage) { 1383 | if (Str::contains($sessionErrorMessage, $message)) { 1384 | $hasError = true; 1385 | 1386 | break; 1387 | } 1388 | } 1389 | 1390 | if (! $hasError) { 1391 | PHPUnit::withResponse($this)->fail( 1392 | "Failed to find a validation error for key and message: '$key' => '$message'".PHP_EOL.PHP_EOL.$errorMessage 1393 | ); 1394 | } 1395 | } 1396 | } 1397 | } 1398 | 1399 | return $this; 1400 | } 1401 | 1402 | /** 1403 | * Assert that the response has the given validation errors but does not have any other validation errors. 1404 | * 1405 | * @param string|array|null $errors 1406 | * @param string $errorBag 1407 | * @param string $responseKey 1408 | * @return $this 1409 | */ 1410 | public function assertOnlyInvalid($errors = null, $errorBag = 'default', $responseKey = 'errors') 1411 | { 1412 | if ($this->baseResponse->headers->get('Content-Type') === 'application/json') { 1413 | return $this->assertOnlyJsonValidationErrors($errors, $responseKey); 1414 | } 1415 | 1416 | $this->assertSessionHas('errors'); 1417 | 1418 | $sessionErrors = $this->session()->get('errors') 1419 | ->getBag($errorBag) 1420 | ->getMessages(); 1421 | 1422 | $expectedErrorKeys = (new Collection($errors)) 1423 | ->map(fn ($value, $key) => is_int($key) ? $value : $key) 1424 | ->all(); 1425 | 1426 | $unexpectedErrorKeys = Arr::except($sessionErrors, $expectedErrorKeys); 1427 | 1428 | PHPUnit::withResponse($this)->assertTrue( 1429 | count($unexpectedErrorKeys) === 0, 1430 | 'Response has unexpected validation errors: '.(new Collection($unexpectedErrorKeys))->keys()->map(fn ($key) => "'{$key}'")->join(', ') 1431 | ); 1432 | 1433 | return $this; 1434 | } 1435 | 1436 | /** 1437 | * Assert that the session has a given value. 1438 | * 1439 | * @param string|array $key 1440 | * @param mixed $value 1441 | * @return $this 1442 | */ 1443 | public function assertSessionHas($key, $value = null) 1444 | { 1445 | if (is_array($key)) { 1446 | return $this->assertSessionHasAll($key); 1447 | } 1448 | 1449 | if (is_null($value)) { 1450 | PHPUnit::withResponse($this)->assertTrue( 1451 | $this->session()->has($key), 1452 | "Session is missing expected key [{$key}]." 1453 | ); 1454 | } elseif ($value instanceof Closure) { 1455 | PHPUnit::withResponse($this)->assertTrue($value($this->session()->get($key))); 1456 | } else { 1457 | PHPUnit::withResponse($this)->assertEquals($value, $this->session()->get($key)); 1458 | } 1459 | 1460 | return $this; 1461 | } 1462 | 1463 | /** 1464 | * Assert that the session has a given list of values. 1465 | * 1466 | * @param array $bindings 1467 | * @return $this 1468 | */ 1469 | public function assertSessionHasAll(array $bindings) 1470 | { 1471 | foreach ($bindings as $key => $value) { 1472 | if (is_int($key)) { 1473 | $this->assertSessionHas($value); 1474 | } else { 1475 | $this->assertSessionHas($key, $value); 1476 | } 1477 | } 1478 | 1479 | return $this; 1480 | } 1481 | 1482 | /** 1483 | * Assert that the session has a given value in the flashed input array. 1484 | * 1485 | * @param string|array $key 1486 | * @param mixed $value 1487 | * @return $this 1488 | */ 1489 | public function assertSessionHasInput($key, $value = null) 1490 | { 1491 | if (is_array($key)) { 1492 | foreach ($key as $k => $v) { 1493 | if (is_int($k)) { 1494 | $this->assertSessionHasInput($v); 1495 | } else { 1496 | $this->assertSessionHasInput($k, $v); 1497 | } 1498 | } 1499 | 1500 | return $this; 1501 | } 1502 | 1503 | if (is_null($value)) { 1504 | PHPUnit::withResponse($this)->assertTrue( 1505 | $this->session()->hasOldInput($key), 1506 | "Session is missing expected key [{$key}]." 1507 | ); 1508 | } elseif ($value instanceof Closure) { 1509 | PHPUnit::withResponse($this)->assertTrue($value($this->session()->getOldInput($key))); 1510 | } else { 1511 | PHPUnit::withResponse($this)->assertEquals($value, $this->session()->getOldInput($key)); 1512 | } 1513 | 1514 | return $this; 1515 | } 1516 | 1517 | /** 1518 | * Assert that the session has the given errors. 1519 | * 1520 | * @param string|array $keys 1521 | * @param mixed $format 1522 | * @param string $errorBag 1523 | * @return $this 1524 | */ 1525 | public function assertSessionHasErrors($keys = [], $format = null, $errorBag = 'default') 1526 | { 1527 | $this->assertSessionHas('errors'); 1528 | 1529 | $keys = (array) $keys; 1530 | 1531 | $errors = $this->session()->get('errors')->getBag($errorBag); 1532 | 1533 | foreach ($keys as $key => $value) { 1534 | if (is_int($key)) { 1535 | PHPUnit::withResponse($this)->assertTrue($errors->has($value), "Session missing error: $value"); 1536 | } else { 1537 | PHPUnit::withResponse($this)->assertContains(is_bool($value) ? (string) $value : $value, $errors->get($key, $format)); 1538 | } 1539 | } 1540 | 1541 | return $this; 1542 | } 1543 | 1544 | /** 1545 | * Assert that the session is missing the given errors. 1546 | * 1547 | * @param string|array $keys 1548 | * @param string|null $format 1549 | * @param string $errorBag 1550 | * @return $this 1551 | */ 1552 | public function assertSessionDoesntHaveErrors($keys = [], $format = null, $errorBag = 'default') 1553 | { 1554 | $keys = (array) $keys; 1555 | 1556 | if (empty($keys)) { 1557 | return $this->assertSessionHasNoErrors(); 1558 | } 1559 | 1560 | if (is_null($this->session()->get('errors'))) { 1561 | PHPUnit::withResponse($this)->assertTrue(true); 1562 | 1563 | return $this; 1564 | } 1565 | 1566 | $errors = $this->session()->get('errors')->getBag($errorBag); 1567 | 1568 | foreach ($keys as $key => $value) { 1569 | if (is_int($key)) { 1570 | PHPUnit::withResponse($this)->assertFalse($errors->has($value), "Session has unexpected error: $value"); 1571 | } else { 1572 | PHPUnit::withResponse($this)->assertNotContains($value, $errors->get($key, $format)); 1573 | } 1574 | } 1575 | 1576 | return $this; 1577 | } 1578 | 1579 | /** 1580 | * Assert that the session has no errors. 1581 | * 1582 | * @return $this 1583 | */ 1584 | public function assertSessionHasNoErrors() 1585 | { 1586 | $hasErrors = $this->session()->has('errors'); 1587 | 1588 | PHPUnit::withResponse($this)->assertFalse( 1589 | $hasErrors, 1590 | 'Session has unexpected errors: '.PHP_EOL.PHP_EOL. 1591 | json_encode((function () use ($hasErrors) { 1592 | $errors = []; 1593 | 1594 | $sessionErrors = $this->session()->get('errors'); 1595 | 1596 | if ($hasErrors && is_a($sessionErrors, ViewErrorBag::class)) { 1597 | foreach ($sessionErrors->getBags() as $bag => $messages) { 1598 | if (is_a($messages, MessageBag::class)) { 1599 | $errors[$bag] = $messages->all(); 1600 | } 1601 | } 1602 | } 1603 | 1604 | return $errors; 1605 | })(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 1606 | ); 1607 | 1608 | return $this; 1609 | } 1610 | 1611 | /** 1612 | * Assert that the session has the given errors. 1613 | * 1614 | * @param string $errorBag 1615 | * @param string|array $keys 1616 | * @param mixed $format 1617 | * @return $this 1618 | */ 1619 | public function assertSessionHasErrorsIn($errorBag, $keys = [], $format = null) 1620 | { 1621 | return $this->assertSessionHasErrors($keys, $format, $errorBag); 1622 | } 1623 | 1624 | /** 1625 | * Assert that the session does not have a given key. 1626 | * 1627 | * @param string|array $key 1628 | * @return $this 1629 | */ 1630 | public function assertSessionMissing($key) 1631 | { 1632 | if (is_array($key)) { 1633 | foreach ($key as $value) { 1634 | $this->assertSessionMissing($value); 1635 | } 1636 | } else { 1637 | PHPUnit::withResponse($this)->assertFalse( 1638 | $this->session()->has($key), 1639 | "Session has unexpected key [{$key}]." 1640 | ); 1641 | } 1642 | 1643 | return $this; 1644 | } 1645 | 1646 | /** 1647 | * Get the current session store. 1648 | * 1649 | * @return \Illuminate\Session\Store 1650 | */ 1651 | protected function session() 1652 | { 1653 | $session = app('session.store'); 1654 | 1655 | if (! $session->isStarted()) { 1656 | $session->start(); 1657 | } 1658 | 1659 | return $session; 1660 | } 1661 | 1662 | /** 1663 | * Dump the headers from the response and end the script. 1664 | * 1665 | * @return never 1666 | */ 1667 | public function ddHeaders() 1668 | { 1669 | $this->dumpHeaders(); 1670 | 1671 | exit(1); 1672 | } 1673 | 1674 | /** 1675 | * Dump the body of the response and end the script. 1676 | * 1677 | * @param string|null $key 1678 | * @return never 1679 | */ 1680 | public function ddBody($key = null) 1681 | { 1682 | $content = $this->content(); 1683 | 1684 | if (function_exists('json_validate') && json_validate($content)) { 1685 | $this->ddJson($key); 1686 | } 1687 | 1688 | dd($content); 1689 | } 1690 | 1691 | /** 1692 | * Dump the JSON payload from the response and end the script. 1693 | * 1694 | * @param string|null $key 1695 | */ 1696 | public function ddJson($key = null) 1697 | { 1698 | dd($this->json($key)); 1699 | } 1700 | 1701 | /** 1702 | * Dump the session from the response and end the script. 1703 | * 1704 | * @param string|array $keys 1705 | * @return never 1706 | */ 1707 | public function ddSession($keys = []) 1708 | { 1709 | $this->dumpSession($keys); 1710 | 1711 | exit(1); 1712 | } 1713 | 1714 | /** 1715 | * Dump the content from the response. 1716 | * 1717 | * @param string|null $key 1718 | * @return $this 1719 | */ 1720 | public function dump($key = null) 1721 | { 1722 | $content = $this->getContent(); 1723 | 1724 | $json = json_decode($content); 1725 | 1726 | if (json_last_error() === JSON_ERROR_NONE) { 1727 | $content = $json; 1728 | } 1729 | 1730 | if (! is_null($key)) { 1731 | dump(data_get($content, $key)); 1732 | } else { 1733 | dump($content); 1734 | } 1735 | 1736 | return $this; 1737 | } 1738 | 1739 | /** 1740 | * Dump the headers from the response. 1741 | * 1742 | * @return $this 1743 | */ 1744 | public function dumpHeaders() 1745 | { 1746 | dump($this->headers->all()); 1747 | 1748 | return $this; 1749 | } 1750 | 1751 | /** 1752 | * Dump the session from the response. 1753 | * 1754 | * @param string|array $keys 1755 | * @return $this 1756 | */ 1757 | public function dumpSession($keys = []) 1758 | { 1759 | $keys = (array) $keys; 1760 | 1761 | if (empty($keys)) { 1762 | dump($this->session()->all()); 1763 | } else { 1764 | dump($this->session()->only($keys)); 1765 | } 1766 | 1767 | return $this; 1768 | } 1769 | 1770 | /** 1771 | * Get the streamed content from the response. 1772 | * 1773 | * @return string 1774 | */ 1775 | public function streamedContent() 1776 | { 1777 | if (! is_null($this->streamedContent)) { 1778 | return $this->streamedContent; 1779 | } 1780 | 1781 | if (! $this->baseResponse instanceof StreamedResponse 1782 | && ! $this->baseResponse instanceof StreamedJsonResponse) { 1783 | PHPUnit::withResponse($this)->fail('The response is not a streamed response.'); 1784 | } 1785 | 1786 | ob_start(function (string $buffer): string { 1787 | $this->streamedContent .= $buffer; 1788 | 1789 | return ''; 1790 | }); 1791 | 1792 | $this->sendContent(); 1793 | 1794 | ob_end_clean(); 1795 | 1796 | return $this->streamedContent; 1797 | } 1798 | 1799 | /** 1800 | * Set the previous exceptions on the response. 1801 | * 1802 | * @param \Illuminate\Support\Collection $exceptions 1803 | * @return $this 1804 | */ 1805 | public function withExceptions(Collection $exceptions) 1806 | { 1807 | $this->exceptions = $exceptions; 1808 | 1809 | return $this; 1810 | } 1811 | 1812 | /** 1813 | * Dynamically access base response parameters. 1814 | * 1815 | * @param string $key 1816 | * @return mixed 1817 | */ 1818 | public function __get($key) 1819 | { 1820 | return $this->baseResponse->{$key}; 1821 | } 1822 | 1823 | /** 1824 | * Proxy isset() checks to the underlying base response. 1825 | * 1826 | * @param string $key 1827 | * @return bool 1828 | */ 1829 | public function __isset($key) 1830 | { 1831 | return isset($this->baseResponse->{$key}); 1832 | } 1833 | 1834 | /** 1835 | * Determine if the given offset exists. 1836 | * 1837 | * @param string $offset 1838 | * @return bool 1839 | */ 1840 | public function offsetExists($offset): bool 1841 | { 1842 | return $this->responseHasView() 1843 | ? isset($this->original->gatherData()[$offset]) 1844 | : isset($this->json()[$offset]); 1845 | } 1846 | 1847 | /** 1848 | * Get the value for a given offset. 1849 | * 1850 | * @param string $offset 1851 | * @return mixed 1852 | */ 1853 | public function offsetGet($offset): mixed 1854 | { 1855 | return $this->responseHasView() 1856 | ? $this->viewData($offset) 1857 | : $this->json()[$offset]; 1858 | } 1859 | 1860 | /** 1861 | * Set the value at the given offset. 1862 | * 1863 | * @param string $offset 1864 | * @param mixed $value 1865 | * @return void 1866 | * 1867 | * @throws \LogicException 1868 | */ 1869 | public function offsetSet($offset, $value): void 1870 | { 1871 | throw new LogicException('Response data may not be mutated using array access.'); 1872 | } 1873 | 1874 | /** 1875 | * Unset the value at the given offset. 1876 | * 1877 | * @param string $offset 1878 | * @return void 1879 | * 1880 | * @throws \LogicException 1881 | */ 1882 | public function offsetUnset($offset): void 1883 | { 1884 | throw new LogicException('Response data may not be mutated using array access.'); 1885 | } 1886 | 1887 | /** 1888 | * Handle dynamic calls into macros or pass missing methods to the base response. 1889 | * 1890 | * @param string $method 1891 | * @param array $args 1892 | * @return mixed 1893 | */ 1894 | public function __call($method, $args) 1895 | { 1896 | if (static::hasMacro($method)) { 1897 | return $this->macroCall($method, $args); 1898 | } 1899 | 1900 | return $this->baseResponse->{$method}(...$args); 1901 | } 1902 | } 1903 | -------------------------------------------------------------------------------- /TestResponseAssert.php: -------------------------------------------------------------------------------- 1 | injectResponseContext($e); 48 | } 49 | } 50 | 51 | /** 52 | * Pass static method calls to the Assert class. 53 | * 54 | * @param string $name 55 | * @param array $arguments 56 | * @return void 57 | * 58 | * @throws \PHPUnit\Framework\ExpectationFailedException 59 | */ 60 | public static function __callStatic($name, $arguments) 61 | { 62 | Assert::$name(...$arguments); 63 | } 64 | 65 | /** 66 | * Inject additional context from the response into the exception message. 67 | * 68 | * @param \PHPUnit\Framework\ExpectationFailedException $exception 69 | * @return \PHPUnit\Framework\ExpectationFailedException 70 | */ 71 | protected function injectResponseContext($exception) 72 | { 73 | if ($lastException = $this->response->exceptions->last()) { 74 | return $this->appendExceptionToException($lastException, $exception); 75 | } 76 | 77 | if ($this->response->baseResponse instanceof RedirectResponse) { 78 | $session = $this->response->baseResponse->getSession(); 79 | 80 | if (! is_null($session) && $session->has('errors')) { 81 | return $this->appendErrorsToException($session->get('errors')->all(), $exception); 82 | } 83 | } 84 | 85 | if ($this->response->baseResponse->headers->get('Content-Type') === 'application/json') { 86 | $testJson = new AssertableJsonString($this->response->getContent()); 87 | 88 | if (isset($testJson['errors'])) { 89 | return $this->appendErrorsToException($testJson->json(), $exception, true); 90 | } 91 | } 92 | 93 | return $exception; 94 | } 95 | 96 | /** 97 | * Append an exception to the message of another exception. 98 | * 99 | * @param \Throwable $exceptionToAppend 100 | * @param \PHPUnit\Framework\ExpectationFailedException $exception 101 | * @return \PHPUnit\Framework\ExpectationFailedException 102 | */ 103 | protected function appendExceptionToException($exceptionToAppend, $exception) 104 | { 105 | $exceptionMessage = is_string($exceptionToAppend) ? $exceptionToAppend : $exceptionToAppend->getMessage(); 106 | 107 | $exceptionToAppend = (string) $exceptionToAppend; 108 | 109 | $message = <<<"EOF" 110 | The following exception occurred during the last request: 111 | 112 | $exceptionToAppend 113 | 114 | ---------------------------------------------------------------------------------- 115 | 116 | $exceptionMessage 117 | EOF; 118 | 119 | return $this->appendMessageToException($message, $exception); 120 | } 121 | 122 | /** 123 | * Append errors to an exception message. 124 | * 125 | * @param array $errors 126 | * @param \PHPUnit\Framework\ExpectationFailedException $exception 127 | * @param bool $json 128 | * @return \PHPUnit\Framework\ExpectationFailedException 129 | */ 130 | protected function appendErrorsToException($errors, $exception, $json = false) 131 | { 132 | $errors = $json 133 | ? json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) 134 | : implode(PHP_EOL, Arr::flatten($errors)); 135 | 136 | // JSON error messages may already contain the errors, so we shouldn't duplicate them... 137 | if (str_contains($exception->getMessage(), $errors)) { 138 | return $exception; 139 | } 140 | 141 | $message = <<<"EOF" 142 | The following errors occurred during the last request: 143 | 144 | $errors 145 | EOF; 146 | 147 | return $this->appendMessageToException($message, $exception); 148 | } 149 | 150 | /** 151 | * Append a message to an exception. 152 | * 153 | * @param string $message 154 | * @param \PHPUnit\Framework\ExpectationFailedException $exception 155 | * @return \PHPUnit\Framework\ExpectationFailedException 156 | */ 157 | protected function appendMessageToException($message, $exception) 158 | { 159 | $property = new ReflectionProperty($exception, 'message'); 160 | 161 | $property->setValue( 162 | $exception, 163 | $exception->getMessage().PHP_EOL.PHP_EOL.$message.PHP_EOL 164 | ); 165 | 166 | return $exception; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /TestView.php: -------------------------------------------------------------------------------- 1 | view = $view; 41 | $this->rendered = $view->render(); 42 | } 43 | 44 | /** 45 | * Assert that the response view has a given piece of bound data. 46 | * 47 | * @param string|array $key 48 | * @param mixed $value 49 | * @return $this 50 | */ 51 | public function assertViewHas($key, $value = null) 52 | { 53 | if (is_array($key)) { 54 | return $this->assertViewHasAll($key); 55 | } 56 | 57 | if (is_null($value)) { 58 | PHPUnit::assertTrue(Arr::has($this->view->gatherData(), $key)); 59 | } elseif ($value instanceof Closure) { 60 | PHPUnit::assertTrue($value(Arr::get($this->view->gatherData(), $key))); 61 | } elseif ($value instanceof Model) { 62 | PHPUnit::assertTrue($value->is(Arr::get($this->view->gatherData(), $key))); 63 | } elseif ($value instanceof EloquentCollection) { 64 | $actual = Arr::get($this->view->gatherData(), $key); 65 | 66 | PHPUnit::assertInstanceOf(EloquentCollection::class, $actual); 67 | PHPUnit::assertSameSize($value, $actual); 68 | 69 | $value->each(fn ($item, $index) => PHPUnit::assertTrue($actual->get($index)->is($item))); 70 | } else { 71 | PHPUnit::assertEquals($value, Arr::get($this->view->gatherData(), $key)); 72 | } 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Assert that the response view has a given list of bound data. 79 | * 80 | * @param array $bindings 81 | * @return $this 82 | */ 83 | public function assertViewHasAll(array $bindings) 84 | { 85 | foreach ($bindings as $key => $value) { 86 | if (is_int($key)) { 87 | $this->assertViewHas($value); 88 | } else { 89 | $this->assertViewHas($key, $value); 90 | } 91 | } 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Assert that the response view is missing a piece of bound data. 98 | * 99 | * @param string $key 100 | * @return $this 101 | */ 102 | public function assertViewMissing($key) 103 | { 104 | PHPUnit::assertFalse(Arr::has($this->view->gatherData(), $key)); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Assert that the view's rendered content is empty. 111 | * 112 | * @return $this 113 | */ 114 | public function assertViewEmpty() 115 | { 116 | PHPUnit::assertEmpty($this->rendered); 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Assert that the given string is contained within the view. 123 | * 124 | * @param string $value 125 | * @param bool $escape 126 | * @return $this 127 | */ 128 | public function assertSee($value, $escape = true) 129 | { 130 | $value = $escape ? e($value) : $value; 131 | 132 | PHPUnit::assertStringContainsString((string) $value, $this->rendered); 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Assert that the given strings are contained in order within the view. 139 | * 140 | * @param array $values 141 | * @param bool $escape 142 | * @return $this 143 | */ 144 | public function assertSeeInOrder(array $values, $escape = true) 145 | { 146 | $values = $escape ? array_map(e(...), $values) : $values; 147 | 148 | PHPUnit::assertThat($values, new SeeInOrder($this->rendered)); 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Assert that the given string is contained within the view text. 155 | * 156 | * @param string $value 157 | * @param bool $escape 158 | * @return $this 159 | */ 160 | public function assertSeeText($value, $escape = true) 161 | { 162 | $value = $escape ? e($value) : $value; 163 | 164 | PHPUnit::assertStringContainsString((string) $value, strip_tags($this->rendered)); 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Assert that the given strings are contained in order within the view text. 171 | * 172 | * @param array $values 173 | * @param bool $escape 174 | * @return $this 175 | */ 176 | public function assertSeeTextInOrder(array $values, $escape = true) 177 | { 178 | $values = $escape ? array_map(e(...), $values) : $values; 179 | 180 | PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->rendered))); 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Assert that the given string is not contained within the view. 187 | * 188 | * @param string $value 189 | * @param bool $escape 190 | * @return $this 191 | */ 192 | public function assertDontSee($value, $escape = true) 193 | { 194 | $value = $escape ? e($value) : $value; 195 | 196 | PHPUnit::assertStringNotContainsString((string) $value, $this->rendered); 197 | 198 | return $this; 199 | } 200 | 201 | /** 202 | * Assert that the given string is not contained within the view text. 203 | * 204 | * @param string $value 205 | * @param bool $escape 206 | * @return $this 207 | */ 208 | public function assertDontSeeText($value, $escape = true) 209 | { 210 | $value = $escape ? e($value) : $value; 211 | 212 | PHPUnit::assertStringNotContainsString((string) $value, strip_tags($this->rendered)); 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * Get the string contents of the rendered view. 219 | * 220 | * @return string 221 | */ 222 | public function __toString() 223 | { 224 | return $this->rendered; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illuminate/testing", 3 | "description": "The Illuminate Testing package.", 4 | "license": "MIT", 5 | "homepage": "https://laravel.com", 6 | "support": { 7 | "issues": "https://github.com/laravel/framework/issues", 8 | "source": "https://github.com/laravel/framework" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.3", 18 | "ext-mbstring": "*", 19 | "illuminate/collections": "^13.0", 20 | "illuminate/contracts": "^13.0", 21 | "illuminate/macroable": "^13.0", 22 | "illuminate/support": "^13.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Illuminate\\Testing\\": "" 27 | } 28 | }, 29 | "extra": { 30 | "branch-alias": { 31 | "dev-master": "13.0.x-dev" 32 | } 33 | }, 34 | "suggest": { 35 | "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", 36 | "illuminate/console": "Required to assert console commands (^13.0).", 37 | "illuminate/database": "Required to assert databases (^13.0).", 38 | "illuminate/http": "Required to assert responses (^13.0).", 39 | "mockery/mockery": "Required to use mocking (^1.6).", 40 | "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1)." 41 | }, 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "minimum-stability": "dev" 46 | } 47 | --------------------------------------------------------------------------------