diff --git a/.github/workflows/run-stub-tests.yml b/.github/workflows/run-stub-tests.yml new file mode 100644 index 00000000..caa21b9a --- /dev/null +++ b/.github/workflows/run-stub-tests.yml @@ -0,0 +1,103 @@ +name: run-stub-tests + +on: [push, pull_request] + +jobs: + stub-test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest] + php: [8.1, 8.0] + laravel: [9.*] + dependency-version: [prefer-lowest, prefer-stable] + + name: Test Stubs ${{ matrix.os }} - P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql, fileinfo + coverage: none + + - name: Setup Laravel + run: | + composer create-project laravel/laravel:^9.3 . + composer require protonemedia/laravel-splade + + - name: Remove installed Splade (Unix) + run: rm -rf vendor/protonemedia/laravel-splade + if: matrix.os == 'ubuntu-latest' + + - name: Remove installed Splade (Windows) + run: rd "vendor/protonemedia/laravel-splade" /s /q + shell: cmd + if: matrix.os == 'windows-latest' + + - name: Checkout code + uses: actions/checkout@v3.1.0 + with: + path: "vendor/protonemedia/laravel-splade" + + - name: Install Splade + run: | + composer dump + php artisan splade:install + + - name: Install NPM dependencies + run: | + npm i + npm i autosize choices.js flatpickr + + - name: Remove installed Splade and copy front-end build from Checkout (Unix) + run: | + rm -rf node_modules/@protonemedia/laravel-splade/dist + cp -R vendor/protonemedia/laravel-splade/dist node_modules/@protonemedia/laravel-splade/ + if: matrix.os == 'ubuntu-latest' + + - name: Remove installed Splade and copy front-end build from Checkout (Windows) + run: | + rd "node_modules/@protonemedia/laravel-splade/dist" /s /q + mkdir "node_modules/@protonemedia/laravel-splade/dist" + xcopy "vendor/protonemedia/laravel-splade/dist" "node_modules/@protonemedia/laravel-splade/dist" /E /I + shell: cmd + if: matrix.os == 'windows-latest' + + - name: Compile assets + run: npm run build + + - name: Run Laravel Server (Unix) + run: php artisan serve & + if: matrix.os == 'ubuntu-latest' + + - name: Run Test (Unix) + run: php vendor/protonemedia/laravel-splade/TestStubs.php + if: matrix.os == 'ubuntu-latest' + + - name: Run Laravel Server (Windows) and Run Test + run: | + start /b cmd /v:on /c "(php artisan serve) &" + php vendor/protonemedia/laravel-splade/TestStubs.php + shell: cmd + if: matrix.os == 'windows-latest' + + - name: Start SSR server (Unix) + run: | + echo "SPLADE_SSR_ENABLED=true" >> .env + node bootstrap/ssr/ssr.mjs & + if: matrix.os == 'ubuntu-latest' + + - name: Run Test command (Unix) + run: php artisan splade:ssr-test + if: matrix.os == 'ubuntu-latest' + + - name: Start SSR server (Windows) and Run Test command + run: | + echo "SPLADE_SSR_ENABLED=true" >> .env + node bootstrap/ssr/ssr.mjs & + php artisan splade:ssr-test + if: matrix.os == 'windows-latest' diff --git a/.github/workflows/run-table-tests.yml b/.github/workflows/run-table-tests.yml new file mode 100644 index 00000000..60de4d1d --- /dev/null +++ b/.github/workflows/run-table-tests.yml @@ -0,0 +1,204 @@ +name: run-table-tests + +on: [push, pull_request] + +jobs: + table-test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [8.1, 8.0] + laravel: [9.*] + db: [mysql, postgres, sqlite] + ssr: [true, false] + dependency-version: [prefer-lowest, prefer-stable] + exclude: + - db: mysql + ssr: true + - db: postgres + ssr: true + + name: Test P${{ matrix.php }} - L${{ matrix.laravel }} - DB ${{ matrix.db }} - SSR ${{ matrix.ssr }} - ${{ matrix.dependency-version }} + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: no + MYSQL_USER: protone_media_db_test + MYSQL_DATABASE: protone_media_db_test_mysql + MYSQL_PASSWORD: secret + MYSQL_ROOT_PASSWORD: secret + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + postgres: + image: postgres:15.0 + env: + POSTGRES_USER: protone_media_db_test + POSTGRES_PASSWORD: secret + POSTGRES_DB: protone_media_db_test_postgres + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v3.1.0 + + - name: Cache node modules + id: cache-npm + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - if: ${{ steps.cache-npm.outputs.cache-hit == 'false' }} + name: List the state of node modules + continue-on-error: true + run: npm list + + - name: "Install locked dependencies with npm" + run: | + npm ci --ignore-scripts + + - name: Build package + run: | + npm run build + npm pack + rm -rf node_modules + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql + coverage: none + + - name: Prepare environment file (MySQL) + if: ${{ matrix.db == 'mysql' }} + run: | + cd app + cp .env.example.mysql .env + + - name: Prepare environment file (PostgreSQL) + if: ${{ matrix.db == 'postgres' }} + run: | + cd app + cp .env.example.postgres .env + + - name: Prepare environment file (SQLite) + if: ${{ matrix.db == 'sqlite' }} + run: | + cd app + cp .env.example .env + touch database/database.sqlite + + - name: Prepare demo app + run: | + cd app + npm upgrade + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + npm run build + php artisan dusk:chrome-driver `/opt/google/chrome/chrome --version | cut -d " " -f3 | cut -d "." -f1` + + - name: Start Chrome Driver + run: | + cd app + ./vendor/laravel/dusk/bin/chromedriver-linux & + + - name: Start SSR server + run: | + cd app + sed -i -e "s|SPLADE_SSR_ENABLED=false|SPLADE_SSR_ENABLED=true|g" .env + node bootstrap/ssr/ssr.mjs & + if: matrix.ssr == true + + - name: Migrate DB and Run Laravel Server (MySQL) + run: | + cd app + php artisan migrate:fresh --seed + php artisan serve & + if: ${{ matrix.db == 'mysql' }} + env: + DB_PORT: ${{ job.services.mysql.ports[3306] }} + + - name: Migrate DB and Run Laravel Server (PostgreSQL) + run: | + cd app + php artisan migrate:fresh --seed + php artisan serve & + if: ${{ matrix.db == 'postgres' }} + env: + DB_PORT: ${{ job.services.postgres.ports[5432] }} + + - name: Migrate DB and Run Laravel Server (SQLite) + if: ${{ matrix.db == 'sqlite' }} + run: | + cd app + php artisan migrate:fresh --seed + php artisan serve & + + - name: Execute Dusk tests (only table tests - MySQL) + if: ${{ matrix.db == 'mysql' }} + uses: nick-invision/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: cd app && php artisan dusk --stop-on-error --stop-on-failure --group=table + env: + DB_PORT: ${{ job.services.mysql.ports[3306] }} + + - name: Execute Dusk tests (only table tests - PostgreSQL) + if: ${{ matrix.db == 'postgres' }} + uses: nick-invision/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: cd app && php artisan dusk --stop-on-error --stop-on-failure --group=table + env: + DB_PORT: ${{ job.services.postgres.ports[5432] }} + + - name: Execute Dusk tests (only table tests - SQLite) + if: ${{ matrix.db == 'sqlite' }} + uses: nick-invision/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: cd app && php artisan dusk --stop-on-error --stop-on-failure --group=table + + - name: Upload Screenshots + if: failure() + uses: actions/upload-artifact@v3 + with: + name: screenshots + path: app/tests/Browser/screenshots + + - name: Upload Snapshots + if: failure() + uses: actions/upload-artifact@v3 + with: + name: snapshots + path: app/tests/Browser/__snapshots__ + + - name: Upload Console Logs + if: failure() + uses: actions/upload-artifact@v3 + with: + name: console + path: app/tests/Browser/console + + - name: Upload Logs + if: failure() + uses: actions/upload-artifact@v3 + with: + name: logs + path: app/storage/logs diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 934a3e9f..e094e4dc 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -93,12 +93,12 @@ jobs: cd app php artisan test - - name: Execute Dusk tests + - name: Execute Dusk tests (except table tests) uses: nick-invision/retry@v2 with: timeout_minutes: 10 max_attempts: 3 - command: cd app && php artisan dusk --stop-on-error --stop-on-failure + command: cd app && php artisan dusk --stop-on-error --stop-on-failure --exclude-group=table - name: Upload Screenshots if: failure() @@ -127,102 +127,3 @@ jobs: with: name: logs path: app/storage/logs - - stub-tests: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: true - matrix: - os: [ubuntu-latest, windows-latest] - php: [8.1, 8.0] - laravel: [9.*] - dependency-version: [prefer-lowest, prefer-stable] - - name: Test Stubs ${{ matrix.os }} - P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - - steps: - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql, fileinfo - coverage: none - - - name: Setup Laravel - run: | - composer create-project laravel/laravel:^9.3 . - composer require protonemedia/laravel-splade - - - name: Remove installed Splade (Unix) - run: rm -rf vendor/protonemedia/laravel-splade - if: matrix.os == 'ubuntu-latest' - - - name: Remove installed Splade (Windows) - run: rd "vendor/protonemedia/laravel-splade" /s /q - shell: cmd - if: matrix.os == 'windows-latest' - - - name: Checkout code - uses: actions/checkout@v3.1.0 - with: - path: "vendor/protonemedia/laravel-splade" - - - name: Install Splade - run: | - composer dump - php artisan splade:install - - - name: Install NPM dependencies - run: | - npm i - npm i autosize choices.js flatpickr - - - name: Remove installed Splade and copy front-end build from Checkout (Unix) - run: | - rm -rf node_modules/@protonemedia/laravel-splade/dist - cp -R vendor/protonemedia/laravel-splade/dist node_modules/@protonemedia/laravel-splade/ - if: matrix.os == 'ubuntu-latest' - - - name: Remove installed Splade and copy front-end build from Checkout (Windows) - run: | - rd "node_modules/@protonemedia/laravel-splade/dist" /s /q - mkdir "node_modules/@protonemedia/laravel-splade/dist" - xcopy "vendor/protonemedia/laravel-splade/dist" "node_modules/@protonemedia/laravel-splade/dist" /E /I - shell: cmd - if: matrix.os == 'windows-latest' - - - name: Compile assets - run: npm run build - - - name: Run Laravel Server (Unix) - run: php artisan serve & - if: matrix.os == 'ubuntu-latest' - - - name: Run Test (Unix) - run: php vendor/protonemedia/laravel-splade/TestStubs.php - if: matrix.os == 'ubuntu-latest' - - - name: Run Laravel Server (Windows) and Run Test - run: | - start /b cmd /v:on /c "(php artisan serve) &" - php vendor/protonemedia/laravel-splade/TestStubs.php - shell: cmd - if: matrix.os == 'windows-latest' - - - name: Start SSR server (Unix) - run: | - echo "SPLADE_SSR_ENABLED=true" >> .env - node bootstrap/ssr/ssr.mjs & - if: matrix.os == 'ubuntu-latest' - - - name: Run Test command (Unix) - run: php artisan splade:ssr-test - if: matrix.os == 'ubuntu-latest' - - - name: Start SSR server (Windows) and Run Test command - run: | - echo "SPLADE_SSR_ENABLED=true" >> .env - node bootstrap/ssr/ssr.mjs & - php artisan splade:ssr-test - if: matrix.os == 'windows-latest' diff --git a/app/.env.example.mysql b/app/.env.example.mysql new file mode 100644 index 00000000..a1ca7ec9 --- /dev/null +++ b/app/.env.example.mysql @@ -0,0 +1,54 @@ +APP_NAME="Laravel Splade" +APP_ENV=local +APP_KEY=base64:TBVjDAO1zLKHWGrb6ZI3JWSoJgUWkB8Wf6GfQdes4h8= +APP_DEBUG=true +APP_URL=http://localhost:8000 + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_DATABASE=protone_media_db_test_mysql +DB_USERNAME=protone_media_db_test +DB_PASSWORD=secret +DB_PORT=3306 + +BROADCAST_DRIVER=pusher +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailhog +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +PUSHER_APP_ID=1234567 +PUSHER_APP_KEY=12345678901234567890 +PUSHER_APP_SECRET=12345678901234567890 +PUSHER_APP_CLUSTER=eu +PUSHER_HOST=127.0.0.1 +PUSHER_PORT=6001 +PUSHER_SCHEME=http + +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +DEBUGBAR_ENABLED=false +SPLADE_SSR_ENABLED=false \ No newline at end of file diff --git a/app/.env.example.postgres b/app/.env.example.postgres new file mode 100644 index 00000000..2bdbc0c7 --- /dev/null +++ b/app/.env.example.postgres @@ -0,0 +1,54 @@ +APP_NAME="Laravel Splade" +APP_ENV=local +APP_KEY=base64:TBVjDAO1zLKHWGrb6ZI3JWSoJgUWkB8Wf6GfQdes4h8= +APP_DEBUG=true +APP_URL=http://localhost:8000 + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_DATABASE=protone_media_db_test_postgres +DB_USERNAME=protone_media_db_test +DB_PASSWORD=secret +DB_PORT=5432 + +BROADCAST_DRIVER=pusher +CACHE_DRIVER=file +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailhog +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +PUSHER_APP_ID=1234567 +PUSHER_APP_KEY=12345678901234567890 +PUSHER_APP_SECRET=12345678901234567890 +PUSHER_APP_CLUSTER=eu +PUSHER_HOST=127.0.0.1 +PUSHER_PORT=6001 +PUSHER_SCHEME=http + +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +DEBUGBAR_ENABLED=false +SPLADE_SSR_ENABLED=false \ No newline at end of file diff --git a/app/app/Http/Controllers/TableController.php b/app/app/Http/Controllers/TableController.php index ebb6609f..7d52b133 100644 --- a/app/app/Http/Controllers/TableController.php +++ b/app/app/Http/Controllers/TableController.php @@ -34,6 +34,28 @@ public function boolean() ]); } + public function caseSensitive() + { + return view('table.users', [ + 'users' => SpladeTable::for(User::query()->orderBy('name')) + ->withGlobalSearch(columns: ['name']) + ->column('name') + ->ignoreCase(false) + ->paginate(10), + ]); + } + + public function caseInsensitive() + { + return view('table.users', [ + 'users' => SpladeTable::for(User::query()->orderBy('name')) + ->withGlobalSearch(columns: ['name']) + ->column('name') + ->ignoreCase(true) + ->paginate(10), + ]); + } + public function overflow() { $users = User::query()->orderBy('name')->paginate(1); diff --git a/app/config/database.php b/app/config/database.php index 9a3ddfb1..0ab2fa39 100644 --- a/app/config/database.php +++ b/app/config/database.php @@ -43,6 +43,41 @@ 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + ], /* diff --git a/app/database/factories/UserFactory.php b/app/database/factories/UserFactory.php index d9d037f2..4b5b2769 100644 --- a/app/database/factories/UserFactory.php +++ b/app/database/factories/UserFactory.php @@ -19,7 +19,7 @@ public function definition() { return [ 'name' => fake()->name(), - 'email' => Str::random(16) . fake()->safeEmail(), + 'email' => Str::random(5) . fake()->safeEmail(), 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), diff --git a/app/database/migrations/2014_10_12_000000_create_users_table.php b/app/database/migrations/2014_10_12_000000_create_users_table.php index f70cb282..00065d34 100644 --- a/app/database/migrations/2014_10_12_000000_create_users_table.php +++ b/app/database/migrations/2014_10_12_000000_create_users_table.php @@ -16,7 +16,7 @@ public function up() Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); - $table->string('email')->unique(); + $table->string('email'); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->string('language_code'); diff --git a/app/routes/web.php b/app/routes/web.php index 186b7c74..e365a412 100644 --- a/app/routes/web.php +++ b/app/routes/web.php @@ -226,6 +226,9 @@ Route::get('/rowSlideover', [TableController::class, 'rowSlideover'])->name('table.rowSlideover'); Route::post('/touch', [TableController::class, 'touch'])->name('table.touch'); + Route::get('/caseSensitive', [TableController::class, 'caseSensitive'])->name('table.caseSensitive'); + Route::get('/caseInsensitive', [TableController::class, 'caseInsensitive'])->name('table.caseInsensitive'); + Route::get('/relationsAndExports', [TableController::class, 'relationsAndExports'])->name('table.relationsAndExports'); // @todo refactor into matrix diff --git a/app/tests/Browser/Table/AutoFillTest.php b/app/tests/Browser/Table/AutoFillTest.php index bb9f1729..8815306a 100644 --- a/app/tests/Browser/Table/AutoFillTest.php +++ b/app/tests/Browser/Table/AutoFillTest.php @@ -7,6 +7,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class AutoFillTest extends DuskTestCase { /** diff --git a/app/tests/Browser/Table/BulkActionTest.php b/app/tests/Browser/Table/BulkActionTest.php index d2785c4c..24342cea 100644 --- a/app/tests/Browser/Table/BulkActionTest.php +++ b/app/tests/Browser/Table/BulkActionTest.php @@ -6,18 +6,21 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class BulkActionTest extends DuskTestCase { /** @test */ public function it_can_perform_one_row() { $this->browse(function (Browser $browser) { - $longTimeAgo = now()->subCentury(); + $someTimeAgo = now()->subDecade(); $this->assertEquals(30, Project::count()); - Project::withoutTouching(function () use ($longTimeAgo) { - Project::query()->update(['updated_at' => $longTimeAgo]); + Project::withoutTouching(function () use ($someTimeAgo) { + Project::query()->update(['updated_at' => $someTimeAgo]); }); $browser->visit('table/relationsAndExports') @@ -29,7 +32,7 @@ public function it_can_perform_one_row() ->press('@action.touch-timestamp') ->waitForText('Timestamps updated!'); - $this->assertEquals(29, Project::whereUpdatedAt($longTimeAgo)->count()); + $this->assertEquals(29, Project::whereUpdatedAt($someTimeAgo)->count()); }); } @@ -37,12 +40,12 @@ public function it_can_perform_one_row() public function it_can_perform_one_all_rows_on_the_current_page_with_a_confirmation() { $this->browse(function (Browser $browser) { - $longTimeAgo = now()->subCentury(); + $someTimeAgo = now()->subDecade(); $this->assertEquals(30, Project::count()); - Project::withoutTouching(function () use ($longTimeAgo) { - Project::query()->update(['updated_at' => $longTimeAgo]); + Project::withoutTouching(function () use ($someTimeAgo) { + Project::query()->update(['updated_at' => $someTimeAgo]); }); $browser->visit('table/relationsAndExports') @@ -60,7 +63,7 @@ public function it_can_perform_one_all_rows_on_the_current_page_with_a_confirmat ->press('@splade-confirm-confirm') ->waitForText('Timestamps updated!'); - $this->assertEquals(15, Project::whereUpdatedAt($longTimeAgo)->count()); + $this->assertEquals(15, Project::whereUpdatedAt($someTimeAgo)->count()); }); } @@ -68,12 +71,12 @@ public function it_can_perform_one_all_rows_on_the_current_page_with_a_confirmat public function it_can_perform_one_all_rows_on_the_all_pages_with_a_custom_confirmation() { $this->browse(function (Browser $browser) { - $longTimeAgo = now()->subCentury(); + $someTimeAgo = now()->subDecade(); $this->assertEquals(30, Project::count()); - Project::withoutTouching(function () use ($longTimeAgo) { - Project::query()->update(['updated_at' => $longTimeAgo]); + Project::withoutTouching(function () use ($someTimeAgo) { + Project::query()->update(['updated_at' => $someTimeAgo]); }); $browser->visit('table/relationsAndExports') @@ -89,7 +92,7 @@ public function it_can_perform_one_all_rows_on_the_all_pages_with_a_custom_confi ->press('@splade-confirm-confirm') ->waitForText('Timestamps updated!'); - $this->assertEquals(0, Project::whereUpdatedAt($longTimeAgo)->count()); + $this->assertEquals(0, Project::whereUpdatedAt($someTimeAgo)->count()); }); } } diff --git a/app/tests/Browser/Table/CaseTest.php b/app/tests/Browser/Table/CaseTest.php new file mode 100644 index 00000000..81949b24 --- /dev/null +++ b/app/tests/Browser/Table/CaseTest.php @@ -0,0 +1,59 @@ +browse(function (Browser $browser) { + $firstUser = User::query() + ->orderBy('name') + ->first(); + + $firstUser->forceFill([ + 'name' => strtoupper($firstUser->name), + ])->save(); + + $browser->visit('table/caseInsensitive') + ->waitForText(strtoupper($firstUser->name)) + ->type('searchInput-global', strtolower($firstUser->name)) + ->waitForText(strtoupper($firstUser->name)); + }); + } + + /** @test */ + public function it_can_search_case_sensitive() + { + if (DB::connection() instanceof SQLiteConnection) { + return $this->markTestSkipped('SQLite supports case sensitive queries through a global setting.'); + } + + $this->browse(function (Browser $browser) { + $firstUser = User::query() + ->orderBy('name') + ->first(); + + $firstUser->forceFill([ + 'name' => strtoupper($firstUser->name), + ])->save(); + + $browser->visit('table/caseSensitive') + ->waitForText(strtoupper($firstUser->name)) + ->type('searchInput-global', strtolower($firstUser->name)) + ->waitUntilMissingText(strtoupper($firstUser->name)) + ->type('searchInput-global', strtoupper($firstUser->name)) + ->waitForText(strtoupper($firstUser->name)); + }); + } +} diff --git a/app/tests/Browser/Table/ColumnTest.php b/app/tests/Browser/Table/ColumnTest.php index c5460dc1..ffd47680 100644 --- a/app/tests/Browser/Table/ColumnTest.php +++ b/app/tests/Browser/Table/ColumnTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class ColumnTest extends DuskTestCase { /** diff --git a/app/tests/Browser/Table/CustomTest.php b/app/tests/Browser/Table/CustomTest.php index bd34cd51..9f74cb4d 100644 --- a/app/tests/Browser/Table/CustomTest.php +++ b/app/tests/Browser/Table/CustomTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class CustomTest extends DuskTestCase { /** @test */ diff --git a/app/tests/Browser/Table/DropdownTest.php b/app/tests/Browser/Table/DropdownTest.php index c01db59c..75d5ca40 100644 --- a/app/tests/Browser/Table/DropdownTest.php +++ b/app/tests/Browser/Table/DropdownTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class DropdownTest extends DuskTestCase { /** @test */ diff --git a/app/tests/Browser/Table/ExportTest.php b/app/tests/Browser/Table/ExportTest.php index 2102864c..1c2bab02 100644 --- a/app/tests/Browser/Table/ExportTest.php +++ b/app/tests/Browser/Table/ExportTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class ExportTest extends DuskTestCase { /** @test */ diff --git a/app/tests/Browser/Table/FilterTest.php b/app/tests/Browser/Table/FilterTest.php index dfcdffeb..243a1647 100644 --- a/app/tests/Browser/Table/FilterTest.php +++ b/app/tests/Browser/Table/FilterTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class FilterTest extends DuskTestCase { /** diff --git a/app/tests/Browser/Table/GlobalSearchTest.php b/app/tests/Browser/Table/GlobalSearchTest.php index c8b7583b..7320cbfe 100644 --- a/app/tests/Browser/Table/GlobalSearchTest.php +++ b/app/tests/Browser/Table/GlobalSearchTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class GlobalSearchTest extends DuskTestCase { /** diff --git a/app/tests/Browser/Table/InputSearchTest.php b/app/tests/Browser/Table/InputSearchTest.php index aaf637ed..87938b55 100644 --- a/app/tests/Browser/Table/InputSearchTest.php +++ b/app/tests/Browser/Table/InputSearchTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class InputSearchTest extends DuskTestCase { /** diff --git a/app/tests/Browser/Table/PaginationTest.php b/app/tests/Browser/Table/PaginationTest.php index d88a00c5..b5825263 100644 --- a/app/tests/Browser/Table/PaginationTest.php +++ b/app/tests/Browser/Table/PaginationTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class PaginationTest extends DuskTestCase { public function simpleUrls() diff --git a/app/tests/Browser/Table/RelationsTest.php b/app/tests/Browser/Table/RelationsTest.php index b705f03a..bb559374 100644 --- a/app/tests/Browser/Table/RelationsTest.php +++ b/app/tests/Browser/Table/RelationsTest.php @@ -8,6 +8,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class RelationsTest extends DuskTestCase { /** @test */ diff --git a/app/tests/Browser/Table/ResetTest.php b/app/tests/Browser/Table/ResetTest.php index 18dda776..d8ac373c 100644 --- a/app/tests/Browser/Table/ResetTest.php +++ b/app/tests/Browser/Table/ResetTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class ResetTest extends DuskTestCase { /** diff --git a/app/tests/Browser/Table/RowLinkTest.php b/app/tests/Browser/Table/RowLinkTest.php index 61f8d624..fd1412ef 100644 --- a/app/tests/Browser/Table/RowLinkTest.php +++ b/app/tests/Browser/Table/RowLinkTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class RowLinkTest extends DuskTestCase { /** @test */ diff --git a/app/tests/Browser/Table/SortTest.php b/app/tests/Browser/Table/SortTest.php index 8a434d09..b019cd67 100644 --- a/app/tests/Browser/Table/SortTest.php +++ b/app/tests/Browser/Table/SortTest.php @@ -6,6 +6,9 @@ use Laravel\Dusk\Browser; use Tests\DuskTestCase; +/** + * @group table + */ class SortTest extends DuskTestCase { /** diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 0be288b2..00a222dc 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -35,6 +35,18 @@ public function for() return []; } + /** + * Helper method to create a new SpladeTable instance. + * + * @return \ProtoneMedia\Splade\SpladeTable + */ + public static function build(...$arguments): SpladeTable + { + $table = new static(...$arguments); + + return $table->make(); + } + /** * Creates a new SpladeTable instance with the resource or * query builder from the 'for()' method of this class. @@ -82,7 +94,7 @@ public function makeExporter(int $key): ?TableExporter $export = $table->getExports()[$key]; return new TableExporter( - $this->make(), + $table, $export->filename, $export->type, $export->events diff --git a/src/SpladeQueryBuilder.php b/src/SpladeQueryBuilder.php index c39acba1..6f1484ff 100644 --- a/src/SpladeQueryBuilder.php +++ b/src/SpladeQueryBuilder.php @@ -3,11 +3,12 @@ namespace ProtoneMedia\Splade; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\MySqlConnection; +use Illuminate\Database\PostgresConnection; use Illuminate\Database\Query\Builder as BaseQueryBuilder; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use Kirschbaum\PowerJoins\PowerJoins; use ProtoneMedia\Splade\Table\Column; @@ -138,47 +139,39 @@ public function parseTermsIntoCollection(string $terms): Collection ->reject(function ($term = null) { return is_null($term) || trim($term) === ''; }) - ->values() - ->map(function (string $term) { - return $this->ignoreCase ? Str::lower($term) : $term; - }); + ->values(); } /** * Formats the terms and returns the right where operator for the given search method. * + * @param \Illuminate\Database\Eloquent\Builder $builder * @param string $term * @param string|null $searchMethod * @return array */ - private function getTermAndWhereOperator(string $term, ?string $searchMethod = null): array + private function getTermAndWhereOperator(EloquentBuilder $builder, string $term, ?string $searchMethod = null): array { + $like = 'LIKE'; + + if ($builder->getConnection() instanceof MySqlConnection) { + $like = $this->ignoreCase ? 'LIKE' : 'LIKE BINARY'; + } + + if ($builder->getConnection() instanceof PostgresConnection) { + $like = $this->ignoreCase ? 'ILIKE' : 'LIKE'; + } + $searchMethod = $searchMethod ?: SearchInput::WILDCARD; return match ($searchMethod) { SearchInput::EXACT => [$term, '='], - SearchInput::WILDCARD => ["%{$term}%", 'LIKE'], - SearchInput::WILDCARD_LEFT => ["%{$term}", 'LIKE'], - SearchInput::WILDCARD_RIGHT => ["{$term}%", 'LIKE'], + SearchInput::WILDCARD => ["%{$term}%", $like], + SearchInput::WILDCARD_LEFT => ["%{$term}", $like], + SearchInput::WILDCARD_RIGHT => ["{$term}%", $like], }; } - /** - * Qualify the column by the model's table and wrap it. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @param string $column - * @return string - */ - private function qualifyColumn(EloquentBuilder $builder, string $column): string - { - $column = $builder->qualifyColumn($column); - - return $this->ignoreCase - ? $builder->getGrammar()->wrap($column) - : $column; - } - private function applyConstraint(array $columns, string $terms) { $terms = $this->parseTerms @@ -190,15 +183,11 @@ private function applyConstraint(array $columns, string $terms) $this->builder->where(function (EloquentBuilder $builder) use ($columns, $terms) { $terms->each(function (string $term) use ($builder, $columns) { Collection::wrap($columns)->each(function (?string $searchMethod, string $column) use ($builder, $term) { - [$term, $whereOperator] = $this->getTermAndWhereOperator($term, $searchMethod); + [$term, $whereOperator] = $this->getTermAndWhereOperator($builder, $term, $searchMethod); if (!Str::contains($column, '.')) { // Not a relationship, but a column on the table. - $column = $this->qualifyColumn($builder, $column); - - return $this->ignoreCase - ? $builder->orWhere(DB::raw("LOWER({$column})"), $whereOperator, $term) - : $builder->orWhere($column, $whereOperator, $term); + return $builder->orWhere($builder->qualifyColumn($column), $whereOperator, $term); } // Split the column into the relationship name and the key on the relationship. @@ -206,11 +195,7 @@ private function applyConstraint(array $columns, string $terms) $key = Str::afterLast($column, '.'); $builder->orWhereHas($relation, function (EloquentBuilder $relation) use ($key, $whereOperator, $term) { - $column = $this->qualifyColumn($relation, $key); - - return $this->ignoreCase - ? $relation->where(DB::raw("LOWER({$column})"), $whereOperator, $term) - : $relation->where($column, $whereOperator, $term); + return $relation->where($relation->qualifyColumn($key), $whereOperator, $term); }); }); });