diff --git a/composer.json b/composer.json index d55c20e..2e4b74d 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,12 @@ "illuminate/filesystem": "^12.0|^13.0", "illuminate/routing": "^12.0|^13.0", "illuminate/support": "^12.0|^13.0", - "laravel/ranger": "^0.2.4", - "laravel/surveyor": "^0.2.1", + "laravel/ranger": "dev-main as 0.2.x-dev", + "laravel/surveyor": "dev-template-bindings-via-static-method-dispatch-on-model-subclasses as 0.2.x-dev", "phpstan/phpdoc-parser": "^2.3" }, "require-dev": { + "laravel/framework": "12.x-dev|^13.9.0", "inertiajs/inertia-laravel": "^2.0", "laravel/pint": "^1.21", "orchestra/testbench": "^10.1|^11.0" @@ -74,5 +75,11 @@ "Laravel\\Wayfinder\\WayfinderServiceProvider" ] } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/levikl/surveyor" + } + ] } diff --git a/tests/EloquentProductController.test.ts b/tests/EloquentProductController.test.ts index e808b7a..e03472b 100644 --- a/tests/EloquentProductController.test.ts +++ b/tests/EloquentProductController.test.ts @@ -1,23 +1,19 @@ -import { readFileSync } from "fs"; -import { join } from "path"; -import { describe, expect, test } from "vitest"; +import { describe, expectTypeOf, test } from "vitest"; -describe("EloquentProductController", () => { - const typesPath = join( - __dirname, - "../workbench/resources/js/wayfinder/types.d.ts", - ); +import type { App, Inertia } from "../workbench/resources/js/wayfinder/types"; - const types = () => readFileSync(typesPath, "utf-8"); +type NotAny = 0 extends (1 & T) ? never : T; +describe("EloquentProductController", () => { test("Index narrows products to App.Models.Product[]", () => { - expect(types()).toContain("{ products: App.Models.Product[] }"); - expect(types()).not.toContain( - "products: Illuminate.Database.Eloquent.Collection", - ); + type Subject = NotAny; + + expectTypeOf().toEqualTypeOf(); }); test("Show narrows product to App.Models.Product", () => { - expect(types()).toContain("{ product: App.Models.Product }"); + type Subject = NotAny; + + expectTypeOf().toEqualTypeOf(); }); }); diff --git a/tests/PaginatedEloquentProductController.test.ts b/tests/PaginatedEloquentProductController.test.ts new file mode 100644 index 0000000..8e045e1 --- /dev/null +++ b/tests/PaginatedEloquentProductController.test.ts @@ -0,0 +1,43 @@ +import { describe, expectTypeOf, test } from "vitest"; + +import type { App, Inertia } from "../workbench/resources/js/wayfinder/types"; + +type NotAny = 0 extends (1 & T) ? never : T; + +describe("PaginatedEloquentProductController", () => { + test("Product::paginate()) emits expected types", () => { + type Subject = Inertia.Pages.PaginatedEloquentProducts.Paginate["products"]; + + expectTypeOf>().toEqualTypeOf< App.Models.Product[] >(); + + // TODO: add generic return type to laravel/framework LengthAwarePaginator's linkCollection() docblock and this passes + // expectTypeOf>().toEqualTypeOf< + // { url: string | null; label: string; active: boolean; page?: number | null }[] + // >(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test("simplePaginate narrows products.data to App.Models.Product[]", () => { + type Subject = Inertia.Pages.PaginatedEloquentProducts.SimplePaginate["products"]; + + expectTypeOf>().toEqualTypeOf(); + }); + + test("cursorPaginate narrows products.data to App.Models.Product[]", () => { + type Subject = Inertia.Pages.PaginatedEloquentProducts.CursorPaginate["products"]; + + expectTypeOf>().toEqualTypeOf(); + }); +}); diff --git a/workbench/app/Http/Controllers/PaginatedEloquentProductController.php b/workbench/app/Http/Controllers/PaginatedEloquentProductController.php new file mode 100644 index 0000000..76378be --- /dev/null +++ b/workbench/app/Http/Controllers/PaginatedEloquentProductController.php @@ -0,0 +1,31 @@ + Product::paginate(5), + ]); + } + + public function simplePaginate(): Response + { + return Inertia::render('PaginatedEloquentProducts/SimplePaginate', [ + 'products' => Product::simplePaginate(5), + ]); + } + + public function cursorPaginate(): Response + { + return Inertia::render('PaginatedEloquentProducts/CursorPaginate', [ + 'products' => Product::cursorPaginate(5), + ]); + } +} diff --git a/workbench/routes/web.php b/workbench/routes/web.php index f22624f..3423879 100644 --- a/workbench/routes/web.php +++ b/workbench/routes/web.php @@ -18,6 +18,7 @@ use App\Http\Controllers\NavigationItemController; use App\Http\Controllers\Nested\NestedController; use App\Http\Controllers\OptionalController; +use App\Http\Controllers\PaginatedEloquentProductController; use App\Http\Controllers\ParameterNameController; use App\Http\Controllers\PostController; use App\Http\Controllers\Prism\Prism\PrismController as NestedPrismController; @@ -50,6 +51,10 @@ Route::get('/eloquent-products', [EloquentProductController::class, 'index'])->name('eloquent-products.index'); Route::get('/eloquent-products/{product}', [EloquentProductController::class, 'show'])->name('eloquent-products.show'); +Route::get('/paginated-eloquent-products/paginate', [PaginatedEloquentProductController::class, 'paginate'])->name('paginated-eloquent-products.paginate'); +Route::get('/paginated-eloquent-products/simple-paginate', [PaginatedEloquentProductController::class, 'simplePaginate'])->name('paginated-eloquent-products.simple-paginate'); +Route::get('/paginated-eloquent-products/cursor-paginate', [PaginatedEloquentProductController::class, 'cursorPaginate'])->name('paginated-eloquent-products.cursor-paginate'); + Route::get('/date-time', [DateTimeController::class, 'show'])->name('date-time.show'); Route::get('/dashboard', function () {