├── LICENSE.md ├── README.md ├── composer.json ├── phpstan-baseline.neon └── src ├── Exceptions └── RootElementNotFound.php ├── InteractsWithViews.php ├── MojitoServiceProvider.php └── ViewAssertion.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nuno Maduro 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Mojito example 3 |

4 | 5 |

6 | Build Status 7 | Total Downloads 8 | Latest Version 9 | License 10 |

11 | 12 | ## About Mojito 13 | 14 | Mojito was created by, and is maintained by [Nuno Maduro](https://github.com/nunomaduro), and is a lightweight package for testing Laravel views in isolation. 15 | 16 | ## Installation & Usage 17 | 18 | > **Requires [PHP 8.0+](https://php.net/releases/)** 19 | 20 | Require Mojito using [Composer](https://getcomposer.org): 21 | 22 | ```bash 23 | composer require nunomaduro/laravel-mojito --dev 24 | ``` 25 | 26 | How to use: 27 | 28 | ```php 29 | class WelcomeTest extends TestCase 30 | { 31 | // First, add the `InteractsWithViews` trait to your test case class. 32 | use InteractsWithViews; 33 | 34 | public function testDisplaysLaravel() 35 | { 36 | // Then, get started with Mojito using the `assertView` method. 37 | $this->assertView('welcome')->contains('Laravel'); 38 | } 39 | } 40 | ``` 41 | 42 | Optionally, you can also perform view testing from your HTTP Tests: 43 | 44 | ```php 45 | class WelcomeTest extends TestCase 46 | { 47 | public function testDisplaysLaravel() 48 | { 49 | $response = $this->get('/'); 50 | 51 | $response->assertStatus(200); 52 | 53 | $response->assertView()->contains('Laravel'); 54 | } 55 | } 56 | ``` 57 | 58 | ### `contains` 59 | 60 | Asserts that the view contains the given text. 61 | 62 | ```php 63 | $this->assertView('button')->contains('Click me'); 64 | $this->assertView('button', ['submitText' => 'Cancel'])->contains('Cancel'); 65 | 66 | $this->assertView('welcome')->in('title')->contains('Laravel'); 67 | $this->assertView('welcome')->in('.content')->contains('Nova'); 68 | ``` 69 | 70 | ### `empty` 71 | 72 | Asserts that the view has no text content. 73 | 74 | _Note: empty html nodes are not considered in this check._ 75 | 76 | ```php 77 | $this->assertView('empty')->in('.empty-div')->empty(); 78 | ``` 79 | 80 | ### `first` 81 | 82 | Filters the view and returns only the first element matching the selector. 83 | 84 | ```php 85 | $this->assertView('welcome')->first('.links a')->contains('Docs'); 86 | ``` 87 | 88 | ### `has` 89 | 90 | Asserts that the view has the given selector. 91 | 92 | ```php 93 | $this->assertView('button')->has('button'); 94 | 95 | $this->assertView('welcome')->has('head'); 96 | $this->assertView('welcome')->in('body')->has('.content'); 97 | ``` 98 | 99 | ### `hasAttribute` 100 | 101 | Asserts that the view **root element** has the given attribute value. 102 | 103 | ```php 104 | $this->assertView('button')->hasAttribute('attribute', 'value'); 105 | $this->assertView('button')->hasAttribute('data-attribute', 'value'); 106 | 107 | $this->assertView('welcome')->hasAttribute('lang', 'en'); 108 | $this->assertView('welcome')->in('head')->first('meta')->hasAttribute('charset','utf-8'); 109 | ``` 110 | 111 | ### `hasClass` 112 | 113 | Asserts that the view has an element with the given class. 114 | 115 | ```php 116 | $this->assertView('button')->hasClass('btn'); 117 | 118 | $this->assertView('welcome')->in('.content')->at('div > p', 0)->hasClass('title'); 119 | ``` 120 | 121 | ### `hasLink` 122 | 123 | Asserts that the view has an element with the given link. 124 | 125 | ```php 126 | $this->assertView('button')->hasLink(route('welcome')); 127 | 128 | $this->assertView('welcome')->in('.links')->first('a')->hasLink('https://laravel.com/docs'); 129 | $this->assertView('welcome')->in('.links')->at('a', 6)->hasLink('https://vapor.laravel.com'); 130 | $this->assertView('welcome')->in('.links')->last('a')->hasLink('https://github.com/laravel/laravel'); 131 | ``` 132 | 133 | ### `in` 134 | 135 | Filters the view and returns only the elements matching the selector. 136 | 137 | ```php 138 | $this->assertView('welcome')->in('.links a')->contains('Laracast'); 139 | ``` 140 | 141 | ### `last` 142 | 143 | Filters the view and returns only the last element matching the selector. 144 | 145 | ```php 146 | $this->assertView('welcome')->last('.links a')->contains('GitHub'); 147 | ``` 148 | 149 | ### `hasMeta` 150 | Asserts that the view has a given metatag in the head section. 151 | 152 | ```php 153 | $response->assertView()->hasMeta(['property' => 'og:title']); 154 | $response->assertView()->hasMeta(['property' => 'og:title', 'content' => 'Laravel']); 155 | ``` 156 | 157 | 158 | ### Macroable 159 | 160 | Feel free to add your own macros to the `ViewAssertion::class`. 161 | 162 | ```php 163 | use NunoMaduro\LaravelMojito\ViewAssertion; 164 | 165 | // Within a service provider: 166 | ViewAssertion::macro('hasCharset', function (string $charset) { 167 | return $this->in('head')->first('meta')->hasAttribute('charset', $charset); 168 | }); 169 | 170 | // In your tests: 171 | $this->assertView('welcome')->hasCharset('utf-8'); 172 | ``` 173 | 174 | 175 | ## Contributing 176 | 177 | Thank you for considering to contribute to Mojito. All the contribution guidelines are mentioned [here](CONTRIBUTING.md). 178 | 179 | You can have a look at the [CHANGELOG](CHANGELOG.md) for constant updates & detailed information about the changes. You can also follow the twitter account for latest announcements or just come say hi!: [@enunomaduro](https://twitter.com/enunomaduro) 180 | 181 | ## Support the development 182 | **Do you like this project? Support it by donating** 183 | 184 | - GitHub sponsors: [Donate](https://github.com/sponsors/nunomaduro) 185 | - PayPal: [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L) 186 | - Patreon: [Donate](https://www.patreon.com/nunomaduro) 187 | 188 | ## License 189 | 190 | Mojito is an open-sourced software licensed under the [MIT license](LICENSE.md). 191 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nunomaduro/laravel-mojito", 3 | "description": "A lightweight package for testing Laravel views.", 4 | "keywords": [ 5 | "php", 6 | "laravel", 7 | "package", 8 | "testing" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Nuno Maduro", 14 | "email": "enunomaduro@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.0", 19 | "symfony/css-selector": "^6.0", 20 | "symfony/dom-crawler": "^6.0" 21 | }, 22 | "require-dev": { 23 | "illuminate/contracts": "^9.0|^10.0", 24 | "illuminate/support": "^9.0|^10.0", 25 | "illuminate/view": "^9.0|^10.0", 26 | "laravel/pint": "^1.5", 27 | "pestphp/pest": "^1.22", 28 | "pestphp/pest-plugin-parallel": "^1.2", 29 | "phpstan/phpstan": "^1.9.17", 30 | "phpstan/phpstan-strict-rules": "^1.4.5" 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Tests\\": "tests/" 35 | }, 36 | "files": [ 37 | "tests/bootstrap.php" 38 | ] 39 | }, 40 | "minimum-stability": "dev", 41 | "prefer-stable": true, 42 | "autoload": { 43 | "psr-4": { 44 | "NunoMaduro\\LaravelMojito\\": "src/" 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true, 49 | "preferred-install": "dist", 50 | "allow-plugins": { 51 | "pestphp/pest-plugin": true 52 | } 53 | }, 54 | "scripts": { 55 | "lint": "pint", 56 | "test:lint": "pint --test", 57 | "test:types": "phpstan analyse --ansi", 58 | "test:unit": "pest -p --colors=always", 59 | "test": [ 60 | "@test:lint", 61 | "@test:types", 62 | "@test:unit" 63 | ] 64 | }, 65 | "extra": { 66 | "laravel": { 67 | "providers": [ 68 | "NunoMaduro\\LaravelMojito\\MojitoServiceProvider" 69 | ] 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Method NunoMaduro\\\\LaravelMojito\\\\ViewAssertion\\:\\:assert\\(\\) is unused\\.$#" 5 | count: 1 6 | path: src/ViewAssertion.php 7 | -------------------------------------------------------------------------------- /src/Exceptions/RootElementNotFound.php: -------------------------------------------------------------------------------- 1 | render()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MojitoServiceProvider.php: -------------------------------------------------------------------------------- 1 | getContent()); 18 | }; 19 | 20 | if (class_exists(\Illuminate\Testing\TestResponse::class)) { 21 | \Illuminate\Testing\TestResponse::macro('assertView', $macro); 22 | } else { 23 | \Illuminate\Foundation\Testing\TestResponse::macro('assertView', $macro); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ViewAssertion.php: -------------------------------------------------------------------------------- 1 | html = $html; 35 | 36 | $this->crawler = new Crawler($this->html); 37 | 38 | // If the view is not a full HTML document, the Crawler will try to fix it 39 | // adding an html and a body tag, so we need to crawl back down 40 | // to the relevant portion of HTML 41 | if (strpos($html, '') === false) { 42 | $this->crawler = $this->crawler->children()->children(); 43 | } 44 | } 45 | 46 | /** 47 | * Creates a new view assertion with the given selector. 48 | */ 49 | public function in(string $selector): ViewAssertion 50 | { 51 | $this->has($selector); 52 | 53 | $filteredHtml = $this->crawler->children()->filter($selector)->each(function ($node) { 54 | return $node->outerHtml(); 55 | }); 56 | 57 | return new self(collect($filteredHtml)->implode('')); 58 | } 59 | 60 | /** 61 | * Creates a new view assertion with the given selector at the given position. 62 | */ 63 | public function at(string $selector, int $position): ViewAssertion 64 | { 65 | $node = $this->crawler->filter($selector)->eq($position); 66 | 67 | return new self($node->outerHtml()); 68 | } 69 | 70 | /** 71 | * Creates a new view assertion with the given selector at the first position. 72 | */ 73 | public function first(string $selector): ViewAssertion 74 | { 75 | $node = $this->crawler->filter($selector)->first(); 76 | 77 | return new self($node->outerHtml()); 78 | } 79 | 80 | /** 81 | * Creates a new view assertion with the given selector at the last position. 82 | */ 83 | public function last(string $selector): ViewAssertion 84 | { 85 | $node = $this->crawler->filter($selector)->last(); 86 | 87 | return new self($node->outerHtml()); 88 | } 89 | 90 | /** 91 | * Asserts that the view contains the given text. 92 | */ 93 | public function contains(string $text): ViewAssertion 94 | { 95 | Assert::assertStringContainsString( 96 | (string) $text, 97 | $this->html, 98 | "Failed asserting that the text `{$text}` exists within `{$this->html}`." 99 | ); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Asserts that the view, at given selector, has no content. 106 | */ 107 | public function empty(): ViewAssertion 108 | { 109 | $content = ''; 110 | foreach ($this->crawler->getIterator() as $node) { 111 | $content .= trim($node->textContent); 112 | } 113 | 114 | Assert::assertEmpty( 115 | $content, 116 | "Failed asserting that the text `{$content}` is empty." 117 | ); 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Asserts that the view, at the **root element**, contains the given attribute value. 124 | */ 125 | public function hasAttribute(string $attribute, string $value): ViewAssertion 126 | { 127 | Assert::assertSame( 128 | $value, 129 | $this->getRootElement()->getAttribute($attribute), 130 | "Failed asserting that the {$attribute} `{$value}` exists within `{$this->html}`." 131 | ); 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Asserts that the view contains an element with the given class. 138 | */ 139 | public function hasClass(string $class): ViewAssertion 140 | { 141 | return $this->has(".$class"); 142 | } 143 | 144 | /** 145 | * Asserts that the view contains an element with the given link. 146 | */ 147 | public function hasLink(string $link): ViewAssertion 148 | { 149 | return $this->has("a[href='{$link}']"); 150 | } 151 | 152 | /** 153 | * Asserts that the view contains the given selector. 154 | */ 155 | public function has(string $selector): ViewAssertion 156 | { 157 | Assert::assertThat( 158 | $this->crawler, 159 | new CrawlerSelectorExists($selector), 160 | "Failed asserting that `{$selector}` exists within `{$this->html}`." 161 | ); 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Asserts that the view head has a meta tag with the given attributes array. 168 | */ 169 | public function hasMeta(array $attributes): ViewAssertion 170 | { 171 | $this->has('head'); 172 | 173 | $properties = implode('][', array_map( 174 | function ($value, $key) { 175 | return sprintf("%s='%s'", $key, $value); 176 | }, 177 | $attributes, 178 | array_keys($attributes) 179 | )); 180 | 181 | return $this->has("meta[{$properties}]"); 182 | } 183 | 184 | /** 185 | * Returns the node of the current root element. 186 | */ 187 | private function getRootElement(): DOMNode 188 | { 189 | $node = $this->crawler->getNode(0); 190 | 191 | if ($node === null) { 192 | throw new RootElementNotFound($this->crawler->outerHtml()); 193 | } 194 | 195 | return $node; 196 | } 197 | 198 | /** 199 | * Runs the given assertion, and fires the given error message on error. 200 | * 201 | * @deprecated it will be removed in the next major version since redundant. 202 | * we are keeping it for now since, despite being private, it can be used by macros 203 | */ 204 | private function assert(callable $assertion, string $message): void 205 | { 206 | try { 207 | $assertion(); 208 | } catch (AssertionFailedError $e) { 209 | throw new AssertionFailedError(sprintf($message, sprintf('`%s`', $this->html))); 210 | } 211 | } 212 | } 213 | --------------------------------------------------------------------------------