Skip to content

Commit

Permalink
Add testing for new layouts paradigm
Browse files Browse the repository at this point in the history
Kurt Friars committed Nov 21, 2023
1 parent 915b875 commit 948b82f
Showing 37 changed files with 487 additions and 47 deletions.
11 changes: 7 additions & 4 deletions config/contentable.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<?php

return [

'model' => \Plank\Contentable\Models\Content::class,
'cache' => [
'ttl' => 10800,
'content' => [
'model' => \Plank\Contentable\Models\Content::class,
],
'layouts' => [
'folder' => 'layouts',
'mode' => \Plank\Contentable\Enums\LayoutMode::Blade,
'model' => \Plank\Contentable\Models\Layout::class,
],
'cache' => [
'ttl' => 10800,
],
];
86 changes: 86 additions & 0 deletions tests/Feature/LayoutSyncTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

use Plank\Contentable\Enums\LayoutType;
use Plank\Contentable\Tests\Helper\Models\Layout;
use Plank\Contentable\Tests\Helper\Models\Page;
use Plank\Contentable\Tests\Helper\Models\Product;

use function Pest\Laravel\artisan;

describe('It syncs Layouts for Blade', function () {
beforeEach(function () {
setBladePath('sync');
});

it('creates global and layoutable layouts on sync', function () {
artisan('contentable:sync')
->assertExitCode(0);

expect($layout = Layout::where('key', 'default')->first())->not->toBeNull();
expect($layout->name)->toBe('Default');
expect($layout->type)->toBe(LayoutType::Global);
expect($layout->layoutable)->toBeNull();

expect($layout = Layout::where('key', 'holidays')->first())->not->toBeNull();
expect($layout->name)->toBe('Holidays');
expect($layout->type)->toBe(LayoutType::Global);
expect($layout->layoutable)->toBeNull();

expect($layout = Layout::where('key', 'pages.index')->first())->not->toBeNull();
expect($layout->name)->toBe('Page Index');
expect($layout->type)->toBe(LayoutType::Index);
expect($layout->layoutable)->toBe(Page::layoutKey());

expect($layout = Layout::where('key', 'pages.show')->first())->not->toBeNull();
expect($layout->name)->toBe('Page Details');
expect($layout->type)->toBe(LayoutType::Show);
expect($layout->layoutable)->toBe(Page::layoutKey());

expect($layout = Layout::where('key', 'pages.landing')->first())->not->toBeNull();
expect($layout->name)->toBe('Landing Page');
expect($layout->type)->toBe(LayoutType::Custom);
expect($layout->layoutable)->toBe(Page::layoutKey());

expect($layout = Layout::where('key', 'products.index')->first())->not->toBeNull();
expect($layout->name)->toBe('Product Index');
expect($layout->type)->toBe(LayoutType::Index);
expect($layout->layoutable)->toBe(Product::layoutKey());

expect($layout = Layout::where('key', 'products.show')->first())->not->toBeNull();
expect($layout->name)->toBe('Product Details');
expect($layout->type)->toBe(LayoutType::Show);
expect($layout->layoutable)->toBe(Product::layoutKey());

expect($layout = Layout::where('key', 'products.promo')->first())->not->toBeNull();
expect($layout->name)->toBe('Promo Product');
expect($layout->type)->toBe(LayoutType::Custom);
expect($layout->layoutable)->toBe(Product::layoutKey());
});

it('does not delete missing global layouts on sync', function () {
Layout::factory()->create([
'key' => 'custom.global',
'name' => 'Custom Global',
'type' => LayoutType::Custom,
]);

artisan('contentable:sync')
->assertExitCode(0);

expect(Layout::where('key', 'custom.global')->exists())->toBeTrue();
});

it('does not delete missing layoutable layouts on sync', function () {
Layout::factory()->create([
'key' => 'pages.y2k',
'name' => 'Y2K Page',
'type' => LayoutType::Custom,
'layoutable' => Page::layoutKey(),
]);

artisan('contentable:sync')
->assertExitCode(0);

expect(Layout::where('key', 'pages.y2k')->exists())->toBeTrue();
});
});
215 changes: 186 additions & 29 deletions tests/Feature/LayoutsTest.php
Original file line number Diff line number Diff line change
@@ -1,45 +1,202 @@
<?php

use Plank\Contentable\Enums\LayoutMode;
use Plank\Contentable\Enums\LayoutType;
use Plank\Contentable\Exceptions\MissingLayoutException;
use Plank\Contentable\Tests\Helper\Models\Layout;
use Plank\Contentable\Tests\Helper\Models\Page;
use Plank\Contentable\Tests\Helper\Models\Post;
use Plank\Contentable\Tests\Helper\Models\Product;

beforeEach(function () {
Layout::factory()->create([
'identifier' => 'pages.show',
]);
use function Pest\Laravel\artisan;

Layout::factory()->create([
'identifier' => 'pages.index',
]);
describe('It throws errors when layouts do not exist', function () {
it('throws an error when the Detail layout doesnt exist', function () {
Product::factory()->create()->layout();
})->throws(MissingLayoutException::class);

Layout::factory()->create([
'identifier' => 'promotions',
]);
it('throws an error when the Index layout doesnt exist', function () {
Product::indexLayout();
})->throws(MissingLayoutException::class);
});

it('finds the show layout using the default key', function () {
$page = Page::factory()->create();
describe('It returns Blade Layouts for Layoutables', function () {
beforeEach(function () {
setBladePath('sync');
artisan('contentable:sync')->assertExitCode(0);
});

expect($layout = $page->layout())->toBeInstanceOf(Layout::class);
expect($layout->layoutKey())->toBe('pages.show');
expect($layout->bladeTemplate())->toBe('layouts.pages.show');
expect($layout->inertiaComponent())->toBe('Pages/Show');
it('can return a global Blade Layout', function () {
$layout = Layout::query()
->where('key', 'default')
->first();

$page = Page::factory()->create([
'layout_id' => $layout->id,
]);

expect($layout = $page->layout())->not->toBeNull();
expect($layout->name)->toBe('Default');
expect($layout->type)->toBe(LayoutType::Global);
expect($layout->layoutable)->toBeNull();
});

it('can return a layoutable Blade Layout', function () {
$layout = Layout::query()
->where('key', 'pages.landing')
->first();

$page = Page::factory()->create([
'layout_id' => $layout->id,
]);

expect($layout = $page->layout())->not->toBeNull();
expect($layout->name)->toBe('Landing Page');
expect($layout->type)->toBe(LayoutType::Custom);
expect($layout->layoutable)->toBe(Page::layoutKey());
});

it('can return an index Blade layout', function () {
expect($layout = Product::indexLayout())->not->toBeNull();
expect($layout->name)->toBe('Product Index');
expect($layout->type)->toBe(LayoutType::Index);
expect($layout->layoutable)->toBe(Product::layoutKey());
});
});

it('finds the index layout using the default key', function () {
expect($layout = Page::indexLayout())->toBeInstanceOf(Layout::class);
expect($layout->layoutKey())->toBe('pages.index');
expect($layout->bladeTemplate())->toBe('layouts.pages.index');
expect($layout->inertiaComponent())->toBe('Pages/Index');
describe('It returns Inertia Layouts for Layoutables', function () {
beforeEach(function () {
config()->set('contentable.layouts.mode', LayoutMode::InertiaJsx);
setInertiaPath('Sync');

artisan('contentable:sync')->assertExitCode(0);
});

it('can return a global Inertia Layout', function () {
$layout = Layout::query()
->where('key', 'Default')
->first();


$page = Page::factory()->create([
'layout_id' => $layout->id,
]);

expect($layout = $page->layout())->not->toBeNull();
expect($layout->name)->toBe('Default');
expect($layout->type)->toBe(LayoutType::Global);
expect($layout->layoutable)->toBeNull();
});

it('can return a layoutable Inertia Layout', function () {
$layout = Layout::query()
->where('key', 'Pages/Landing')
->first();

$page = Page::factory()->create([
'layout_id' => $layout->id,
]);

expect($layout = $page->layout())->not->toBeNull();
expect($layout->name)->toBe('Landing Page');
expect($layout->type)->toBe(LayoutType::Custom);
expect($layout->layoutable)->toBe(Page::layoutKey());
});

it('can return an index Inertia layout', function () {
expect($layout = Product::indexLayout())->not->toBeNull();
expect($layout->name)->toBe('Product Index');
expect($layout->type)->toBe(LayoutType::Index);
expect($layout->layoutable)->toBe(Product::layoutKey());
});
});

it('finds the show layout using a custom key', function () {
$page = Page::factory()->create([
'title' => 'Promotions',
]);
describe('It returns layout options for blade', function () {
beforeEach(function () {
setBladePath('sync');
artisan('contentable:sync')->assertExitCode(0);
});

it('returns all but excluded layouts for models that include global layouts', function () {
$page = Page::factory()->create();

expect($layouts = $page->layouts())->toHaveCount(4);

expect($layout = $layouts->where('key', 'holidays')->first())->toBeNull();

expect($layout = $layouts->where('key', 'default')->first())->not->toBeNull();
expect($layout->name)->toBe('Default');
expect($layout->type)->toBe(LayoutType::Global);
expect($layout->layoutable)->toBeNull();

expect($layout = $layouts->where('key', 'pages.index')->first())->not->toBeNull();
expect($layout->name)->toBe('Page Index');
expect($layout->type)->toBe(LayoutType::Index);
expect($layout->layoutable)->toBe(Page::layoutKey());

expect($layout = $layouts->where('key', 'pages.show')->first())->not->toBeNull();
expect($layout->name)->toBe('Page Details');
expect($layout->type)->toBe(LayoutType::Show);
expect($layout->layoutable)->toBe(Page::layoutKey());

expect($layout = $layouts->where('key', 'pages.landing')->first())->not->toBeNull();
expect($layout->name)->toBe('Landing Page');
expect($layout->type)->toBe(LayoutType::Custom);
expect($layout->layoutable)->toBe(Page::layoutKey());
});

it('excludes all global layouts for layoutables which should not use them', function () {
$post = Post::factory()->create();

expect($layouts = $post->layouts())->toHaveCount(3);

expect($layout = $layouts->where('key', 'holidays')->first())->toBeNull();
expect($layout = $layouts->where('key', 'default')->first())->toBeNull();

expect($layout = $layouts->where('key', 'posts.index')->first())->not->toBeNull();
expect($layout->name)->toBe('Post Index');
expect($layout->type)->toBe(LayoutType::Index);
expect($layout->layoutable)->toBe(Post::layoutKey());

expect($layout = $layouts->where('key', 'posts.show')->first())->not->toBeNull();
expect($layout->name)->toBe('Post Details');
expect($layout->type)->toBe(LayoutType::Show);
expect($layout->layoutable)->toBe(Post::layoutKey());

expect($layout = $layouts->where('key', 'posts.featured')->first())->not->toBeNull();
expect($layout->name)->toBe('Featured Post');
expect($layout->type)->toBe(LayoutType::Custom);
expect($layout->layoutable)->toBe(Post::layoutKey());
});

it('shows all layouts when global layouts are available and non are excluded', function () {
$product = Product::factory()->create();

expect($layouts = $product->layouts())->toHaveCount(5);

expect($layout = $layouts->where('key', 'holidays')->first())->not->toBeNull();
expect($layout->name)->toBe('Holidays');
expect($layout->type)->toBe(LayoutType::Global);
expect($layout->layoutable)->toBeNull();

expect($layout = $layouts->where('key', 'default')->first())->not->toBeNull();
expect($layout->name)->toBe('Default');
expect($layout->type)->toBe(LayoutType::Global);
expect($layout->layoutable)->toBeNull();

expect($layout = $layouts->where('key', 'products.index')->first())->not->toBeNull();
expect($layout->name)->toBe('Product Index');
expect($layout->type)->toBe(LayoutType::Index);
expect($layout->layoutable)->toBe(Product::layoutKey());

expect($layout = $layouts->where('key', 'products.show')->first())->not->toBeNull();
expect($layout->name)->toBe('Product Details');
expect($layout->type)->toBe(LayoutType::Show);
expect($layout->layoutable)->toBe(Product::layoutKey());

expect($layout = $page->layout())->toBeInstanceOf(Layout::class);
expect($layout->layoutKey())->toBe('promotions');
expect($layout->bladeTemplate())->toBe('layouts.promotions');
expect($layout->inertiaComponent())->toBe('Promotions');
expect($layout = $layouts->where('key', 'products.promo')->first())->not->toBeNull();
expect($layout->name)->toBe('Promo Product');
expect($layout->type)->toBe(LayoutType::Custom);
expect($layout->layoutable)->toBe(Product::layoutKey());
});
});
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
3 changes: 2 additions & 1 deletion tests/Helper/Database/Factories/LayoutFactory.php
Original file line number Diff line number Diff line change
@@ -18,7 +18,8 @@ public function definition()
}

return [
'identifier' => implode('.', $this->faker->words(2)),
'key' => $key = implode('.', $this->faker->words(2)),
'name' => (string) str($key)->replace('.', ' ')->title(),
'meta' => $meta,
];
}
26 changes: 26 additions & 0 deletions tests/Helper/Database/Factories/PostFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Plank\Contentable\Tests\Helper\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Plank\Contentable\Tests\Helper\Models\Post;

class PostFactory extends Factory
{
protected $model = Post::class;

public function definition()
{
return [
'title' => $title = $this->faker->sentence,
'slug' => str($title)->slug(),
];
}

public function configure()
{
return $this->afterMaking(function (Post $post) {
$post->slug = (string) str($post->title)->slug();
});
}
}
20 changes: 20 additions & 0 deletions tests/Helper/Database/Factories/ProductFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Plank\Contentable\Tests\Helper\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Plank\Contentable\Tests\Helper\Models\Product;

class ProductFactory extends Factory
{
protected $model = Product::class;

public function definition()
{
return [
'title' => $this->faker->words(3, true),
'code' => $this->faker->unique()->numberBetween(10000, 99999),
'price_in_cents' => $this->faker->numberBetween(99, 99999),
];
}
}
5 changes: 4 additions & 1 deletion tests/Helper/Database/Migrations/create_layouts_table.php
Original file line number Diff line number Diff line change
@@ -10,7 +10,10 @@ public function up()
{
Schema::create('layouts', function (Blueprint $table) {
$table->id();
$table->string('identifier')->unique()->nullable();
$table->string('key')->unique();
$table->string('name')->unique();
$table->string('layoutable')->nullable();
$table->string('type');
$table->json('meta')->nullable();
$table->timestamps();
});
2 changes: 1 addition & 1 deletion tests/Helper/Database/Migrations/create_pages_table.php
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ public function up()
$table->id();
$table->string('title');
$table->string('slug');
$table->boolean('paywall')->default(false);
$table->foreignId('layout_id')->nullable()->constrained();
$table->timestamps();
});
}
24 changes: 24 additions & 0 deletions tests/Helper/Database/Migrations/create_posts_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug');
$table->foreignId('layout_id')->nullable()->constrained();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('posts');
}
};
25 changes: 25 additions & 0 deletions tests/Helper/Database/Migrations/create_products_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('code');
$table->integer('price_in_cents');
$table->foreignId('layout_id')->nullable()->constrained();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('products');
}
};
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions tests/Helper/Models/Layout.php
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
namespace Plank\Contentable\Tests\Helper\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Plank\Contentable\Enums\LayoutType;
use Plank\Contentable\Models\Layout as PackageLayout;

class Layout extends PackageLayout
@@ -14,6 +15,7 @@ class Layout extends PackageLayout
protected $guarded = ['id'];

protected $casts = [
'type' => LayoutType::class,
'meta' => 'json',
];
}
15 changes: 4 additions & 11 deletions tests/Helper/Models/Page.php
Original file line number Diff line number Diff line change
@@ -13,18 +13,11 @@ class Page extends Model implements Contentable, Layoutable
{
use HasContent;
use HasFactory;
use HasLayouts {
layoutKey as traitLayoutKey;
}
use HasLayouts;

protected $guarded = ['id'];

public function layoutKey(): string
{
if ($this->title === 'Promotions') {
return 'promotions';
}

return $this->traitLayoutKey();
}
protected array $excludedLayouts = [
'holidays',
];
}
21 changes: 21 additions & 0 deletions tests/Helper/Models/Post.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Plank\Contentable\Tests\Helper\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Plank\Contentable\Concerns\HasContent;
use Plank\Contentable\Concerns\HasLayouts;
use Plank\Contentable\Contracts\Contentable;
use Plank\Contentable\Contracts\Layoutable;

class Post extends Model implements Contentable, Layoutable
{
use HasContent;
use HasFactory;
use HasLayouts;

protected $guarded = ['id'];

protected bool $globalLayouts = false;
}
19 changes: 19 additions & 0 deletions tests/Helper/Models/Product.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Plank\Contentable\Tests\Helper\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Plank\Contentable\Concerns\HasContent;
use Plank\Contentable\Concerns\HasLayouts;
use Plank\Contentable\Contracts\Contentable;
use Plank\Contentable\Contracts\Layoutable;

class Product extends Model implements Contentable, Layoutable
{
use HasContent;
use HasFactory;
use HasLayouts;

protected $guarded = ['id'];
}
60 changes: 60 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -3,3 +3,63 @@
use Plank\Contentable\Tests\TestCase;

uses(TestCase::class)->in(__DIR__);

function setBladePath(string $path = ''): void
{
$fixtures = str(realpath(__DIR__).'/Helper/Blade/')
->append($path)
->rtrim('/')
->explode('/');

$folder = $fixtures->pop();
$path = $fixtures->implode('/');

config()->set('view.paths', [$path]);
config()->set('contentable.layouts.folder', $folder);
}

function setInertiaPath(string $path = ''): void
{
$fixtures = str(realpath(__DIR__).'/Helper/Inertia/')
->append($path)
->rtrim('/')
->explode('/');

$folder = $fixtures->pop();
$path = $fixtures->implode('/');

$targetPath = resource_path('js');
$targetFolder = $targetPath.'/Pages';

if (! file_exists($targetPath)) {
mkdir($targetPath, 0755, true);
}

if (file_exists($targetFolder)) {
if (is_link($targetFolder)) {
unlink($targetFolder);
} else {
rmdir($targetFolder);
}
}

symlink(
$path,
$targetFolder
);

config()->set('contentable.layouts.folder', $folder);
}

function clearInertiaPath(): void
{
$target = resource_path('js/Pages');

if (file_exists($target)) {
if (is_link($target)) {
unlink($target);
} else {
rmdir($target);
}
}
}

0 comments on commit 948b82f

Please sign in to comment.