diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index 9ec6aca..b0e313b 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -25,7 +25,7 @@ jobs:
             type: 'StaticAnalysis'
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
 
       - name: Configure PHP
         run: |
@@ -38,7 +38,7 @@ jobs:
           echo "::set-output name=dir::$(composer config cache-files-dir)"
 
       - name: Setup cache 2/2
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ${{ steps.composer-cache.outputs.dir }}
           key: ${{ runner.os }}-composer-smoke-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }}
@@ -78,10 +78,10 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        php: ['7.4', '8.0', '8.1']
+        php: ['7.4', '8.0', '8.1', '8.2', '8.3']
         type: ['Phpunit', 'Phpunit Lowest']
         include:
-          - php: '8.1' # TODO replace with 'latest' once it represents at least PHP 8.1
+          - php: 'latest'
             type: 'Phpunit Burn'
     env:
       LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.0' && matrix.type == 'Phpunit' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}"
@@ -91,7 +91,7 @@ jobs:
         options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test --entrypoint sh mysql:8 -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password"
       mariadb:
         image: mariadb
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test
+        options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test
       postgres:
         image: postgres:12-alpine
         env:
@@ -105,13 +105,12 @@ jobs:
           ACCEPT_EULA: Y
           SA_PASSWORD: atk4_pass
       oracle:
-        image: gvenzl/oracle-xe:18
+        image: gvenzl/oracle-xe:18-slim-faststart
         env:
           ORACLE_PASSWORD: atk4_pass
-        options: --health-cmd healthcheck.sh --health-interval=10s --health-timeout=5s --health-retries=10
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
 
       - name: Configure PHP
         run: |
@@ -124,7 +123,7 @@ jobs:
           echo "::set-output name=dir::$(composer config cache-files-dir)"
 
       - name: Setup cache 2/2
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ${{ steps.composer-cache.outputs.dir }}
           key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }}
@@ -136,7 +135,7 @@ jobs:
           if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ] && [ "${{ matrix.type }}" != "Phpunit Burn" ]; then composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap --dev; fi
           if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev; fi
           if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/\* behat/\* --dev; fi
-          if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-update phpunit/phpcov; fi
+          if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-install phpunit/phpcov; fi
           composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader
           if [ "${{ matrix.type }}" = "Phpunit Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader; fi
           if [ "${{ matrix.type }}" = "Phpunit Burn" ]; then sed -i 's~ *public function runBare(): void~public function runBare(): void { gc_collect_cycles(); gc_collect_cycles(); $memDiffs = array_fill(0, '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 64; else echo 16; fi)"', 0); for ($i = -1; $i < count($memDiffs); ++$i) { $this->_runBare(); gc_collect_cycles(); gc_collect_cycles(); $mem = memory_get_usage(); if ($i !== -1) { $memDiffs[$i] = $mem - $memPrev; } $memPrev = $mem; rsort($memDiffs); if (array_sum($memDiffs) >= 4096 * 1024 || $memDiffs[2] > 0) { $this->onNotSuccessfulTest(new AssertionFailedError( "Memory leak detected! (" . implode(" + ", array_map(fn ($v) => number_format($v / 1024, 3, ".", " "), array_filter($memDiffs))) . " KB, " . ($i + 2) . " iterations)" )); } } } private function _runBare(): void~' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare('; fi
@@ -146,7 +145,8 @@ jobs:
           php -r '(new PDO("mysql:host=mysql", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");'
           php -r '(new PDO("mysql:host=mariadb", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");'
           php -r '(new PDO("pgsql:host=postgres;dbname=atk4_test", "atk4_test_user", "atk4_pass"))->exec("ALTER ROLE atk4_test_user CONNECTION LIMIT 1");'
-          if [ -n "$LOG_COVERAGE" ]; then mkdir coverage && cp tools/CoverageUtil.php demos; fi
+          /usr/lib/oracle/setup.sh
+          if [ -n "$LOG_COVERAGE" ]; then mkdir coverage; fi
 
       - name: "Run tests: SQLite"
         run: |
@@ -218,7 +218,7 @@ jobs:
 
       - name: Upload coverage logs 2/2 (only for latest Phpunit)
         if: env.LOG_COVERAGE
-        uses: codecov/codecov-action@v1
+        uses: codecov/codecov-action@v3
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           file: coverage/merged.xml
@@ -231,7 +231,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        php: ['7.4', '8.0', '8.1']
+        php: ['7.4', '8.0', '8.1', '8.2', '8.3']
         type: ['Chrome', 'Chrome Lowest']
         include:
           - php: 'latest'
@@ -246,7 +246,7 @@ jobs:
         options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test --entrypoint sh mysql:8 -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password"
       mariadb:
         image: mariadb
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test
+        options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test
       postgres:
         image: postgres:12-alpine
         env:
@@ -260,13 +260,12 @@ jobs:
           ACCEPT_EULA: Y
           SA_PASSWORD: atk4_pass
       oracle:
-        image: gvenzl/oracle-xe:18
+        image: gvenzl/oracle-xe:18-slim-faststart
         env:
           ORACLE_PASSWORD: atk4_pass
-        options: --health-cmd healthcheck.sh --health-interval=10s --health-timeout=5s --health-retries=10
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
 
       - name: Configure PHP
         run: |
@@ -279,7 +278,7 @@ jobs:
           echo "::set-output name=dir::$(composer config cache-files-dir)"
 
       - name: Setup cache 2/2
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ${{ steps.composer-cache.outputs.dir }}
           key: ${{ runner.os }}-composer-behat-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }}
@@ -317,6 +316,7 @@ jobs:
           php -r '(new PDO("mysql:host=mysql", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");'
           php -r '(new PDO("mysql:host=mariadb", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");'
           php -r '(new PDO("pgsql:host=postgres;dbname=atk4_test", "atk4_test_user", "atk4_pass"))->exec("ALTER ROLE atk4_test_user CONNECTION LIMIT 1");'
+          /usr/lib/oracle/setup.sh
           if [ -n "$LOG_COVERAGE" ]; then mkdir coverage && cp tools/CoverageUtil.php demos; fi
           sed -i "s~'https://raw.githack.com/atk4/ui/develop/public.*~'/vendor/atk4/ui/public',~" vendor/atk4/ui/src/App.php
           ci_wait_until () { timeout 30 sh -c "until { $1 2> /dev/null; }; do sleep 0.02; done" || timeout 15 sh -c "$1" || { echo "health timeout: $1"; exit 1; }; }
@@ -399,7 +399,7 @@ jobs:
 
       - name: Upload coverage logs 2/2 (only for latest Chrome)
         if: env.LOG_COVERAGE
-        uses: codecov/codecov-action@v1
+        uses: codecov/codecov-action@v3
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           file: coverage/merged.xml
diff --git a/composer.json b/composer.json
index a35e120..f5c8804 100644
--- a/composer.json
+++ b/composer.json
@@ -10,18 +10,12 @@
         }
     ],
     "require": {
-        "php": "<8.4",
-        "atk4/ui": "dev-develop"
-    },
-    "require-release": {
-        "php": ">=7.4 <8.2",
-        "atk4/ui": "3.1"
+        "php": ">=7.4 <8.4",
+        "atk4/ui": "5.0"
     },
     "require-dev": {
-        "behat/behat": "^3.9",
-        "behat/mink": "^1.9",
+        "atk4/behat-mink-selenium2-driver": "^1.6.2",
         "behat/mink-extension": "^2.3.1",
-        "behat/mink-selenium2-driver": "^1.5",
         "ergebnis/composer-normalize": "^2.13",
         "friendsofphp/php-cs-fixer": "^3.0",
         "instaclick/php-webdriver": "^1.4.7",
@@ -33,6 +27,10 @@
         "symfony/console": "^4.4.30 || ^5.3.7",
         "symfony/css-selector": "^4.4.24 || ^5.2.9"
     },
+    "conflict": {
+        "behat/behat": "<3.9",
+        "behat/mink": "<1.9"
+    },
     "minimum-stability": "dev",
     "prefer-stable": true,
     "autoload": {
diff --git a/demos/_includes/Model/Post.php b/demos/_includes/Model/Post.php
index 1b4ddd2..e9390a3 100644
--- a/demos/_includes/Model/Post.php
+++ b/demos/_includes/Model/Post.php
@@ -4,7 +4,9 @@
 
 namespace Atk4\TextEditor\Demos\Model;
 
-class Post extends \Atk4\Data\Model
+use Atk4\Data\Model;
+
+class Post extends Model
 {
     public $table = 'post';
 
diff --git a/demos/index.php b/demos/index.php
index 447876a..be644c1 100644
--- a/demos/index.php
+++ b/demos/index.php
@@ -6,6 +6,7 @@
 
 use Atk4\TextEditor\Demos\Model\Post;
 use Atk4\TextEditor\TextEditor;
+use Atk4\Ui\App;
 use Atk4\Ui\Button;
 use Atk4\Ui\Form;
 use Atk4\Ui\Form\Control\Input;
@@ -16,7 +17,7 @@
 
 require_once __DIR__ . '/../vendor/autoload.php';
 
-/** @var \Atk4\Ui\App $app */
+/** @var App $app */
 require __DIR__ . '/init-app.php';
 
 $app->initLayout([Centered::class]);
@@ -30,7 +31,7 @@
     'placeholder' => 'test placeholder',
 ]);
 
-$form->onSubmit(function ($f) {
+$form->onSubmit(static function ($f) {
     $view = new Message();
     $view->setApp($f->getApp());
     $view->invokeInit();
@@ -46,18 +47,18 @@
 /** @var TextEditor $editor */
 $editor = $form->getControl('body');
 
-Button::addTo($app, ['set editor content with random value'])->on('click', function ($jq) use ($editor) {
+Button::addTo($app, ['set editor content with random value'])->on('click', static function ($jq) use ($editor) {
     return $editor->jsSetHtml(true, (string) random_int(0, 10000));
 });
 
-Button::addTo($app, ['get editor content'])->on('click', function ($jq, $content) {
+Button::addTo($app, ['get editor content'])->on('click', static function ($jq, $content) {
     return $content;
 }, [$editor->jsGetHtml()]);
 
-Button::addTo($app, ['refresh editor'])->on('click', function ($jq) use ($editor) {
+Button::addTo($app, ['refresh editor'])->on('click', static function ($jq) use ($editor) {
     return $editor->jsReload();
 });
 
-Button::addTo($app, ['refresh input'])->on('click', function ($jq) use ($input) {
+Button::addTo($app, ['refresh input'])->on('click', static function ($jq) use ($input) {
     return $input->jsReload();
 });
diff --git a/demos/init-app.php b/demos/init-app.php
index 17692c0..48b2bd2 100644
--- a/demos/init-app.php
+++ b/demos/init-app.php
@@ -6,13 +6,15 @@
 
 use Atk4\Data\Persistence;
 use Atk4\Ui\App;
+use Atk4\Ui\Exception;
+use PHPUnit\Framework\TestCase;
 
 date_default_timezone_set('UTC');
 
 require_once __DIR__ . '/init-autoloader.php';
 
 // collect coverage for HTTP tests 1/2
-if (file_exists(__DIR__ . '/CoverageUtil.php') && !class_exists(\PHPUnit\Framework\TestCase::class, false)) {
+if (file_exists(__DIR__ . '/CoverageUtil.php') && !class_exists(TestCase::class, false)) {
     require_once __DIR__ . '/CoverageUtil.php';
     \CoverageUtil::start();
 }
@@ -20,8 +22,8 @@
 $app = new App(['title' => 'ATK4 :: Trumbowyg Demo']);
 
 // collect coverage for HTTP tests 2/2
-if (file_exists(__DIR__ . '/CoverageUtil.php') && !class_exists(\PHPUnit\Framework\TestCase::class, false)) {
-    $app->onHook(\Atk4\Ui\App::HOOK_BEFORE_EXIT, function () {
+if (file_exists(__DIR__ . '/CoverageUtil.php') && !class_exists(TestCase::class, false)) {
+    $app->onHook(App::HOOK_BEFORE_EXIT, static function () {
         \CoverageUtil::saveData();
     });
 }
@@ -32,5 +34,5 @@
     $app->db = $db;
     unset($db);
 } catch (\Throwable $e) {
-    throw new \Atk4\Ui\Exception('Database error: ' . $e->getMessage());
+    throw new Exception('Database error: ' . $e->getMessage());
 }
diff --git a/demos/init-autoloader.php b/demos/init-autoloader.php
index 88f7229..c48e9f2 100644
--- a/demos/init-autoloader.php
+++ b/demos/init-autoloader.php
@@ -5,9 +5,10 @@
 namespace Atk4\TextEditor\Demos;
 
 use Atk4\TextEditor\Tests\TextEditorTest;
+use Composer\Autoload\ClassLoader;
 
 $isRootProject = file_exists(__DIR__ . '/../vendor/autoload.php');
-/** @var \Composer\Autoload\ClassLoader $loader */
+/** @var ClassLoader $loader */
 $loader = require dirname(__DIR__, $isRootProject ? 1 : 4) . '/vendor/autoload.php';
 if (!$isRootProject && !class_exists(TextEditorTest::class)) {
     throw new \Error('Demos can be run only if atk4/login is a root composer project or if dev files are autoloaded');
diff --git a/demos/init-db.php b/demos/init-db.php
index 265eb2b..fa2baf3 100644
--- a/demos/init-db.php
+++ b/demos/init-db.php
@@ -6,12 +6,14 @@
 
 namespace Atk4\TextEditor\Demos;
 
+use Atk4\Ui\Exception;
+
 try {
     require_once file_exists(__DIR__ . '/db.php')
         ? __DIR__ . '/db.php'
         : __DIR__ . '/db.default.php';
 } catch (\PDOException $e) {
     // do not show $e unless you can secure DSN!
-    throw (new \Atk4\Ui\Exception('This demo requires access to the database. See "demos/init-db.php"'))
+    throw (new Exception('This demo requires access to the database. See "demos/init-db.php"'))
         ->addMoreInfo('PDO error', $e->getMessage());
 }
diff --git a/src/TextEditor.php b/src/TextEditor.php
index 2dbbe85..e759ecc 100644
--- a/src/TextEditor.php
+++ b/src/TextEditor.php
@@ -11,8 +11,8 @@ class TextEditor extends Textarea
 {
     protected static array $loaded_assets = [];
 
-    //public $assets_path = '/assets';
-    public string $assets_path = 'https://cdnjs.cloudflare.com/ajax/libs/Trumbowyg/2.25.1';
+    // public $assets_path = '/assets';
+    public string $assets_path = 'https://cdnjs.cloudflare.com/ajax/libs/Trumbowyg/2.27.3';
     public bool $option_resetCss = true;
     public bool $option_autogrow = true;
     public array $editor_options = [
diff --git a/tests-behat/Context.php b/tests-behat/Context.php
index 1072171..16d9c60 100644
--- a/tests-behat/Context.php
+++ b/tests-behat/Context.php
@@ -22,7 +22,7 @@ public function iTypeInEditor(string $name, string $text): void
      */
     public function modalIsOpenWithRawText(string $text, string $tag = 'div'): void
     {
-        $html = $this->getElementInPage('.modal.visible.active.front')->getHtml();
+        $html = $this->findElement(null, '.modal.visible.active.front')->getHtml();
 
         if (empty($html)) {
             throw new \Exception('Modal html is empty');
diff --git a/tests-behat/editor.feature b/tests-behat/editor.feature
index ae461c0..2aa42d8 100644
--- a/tests-behat/editor.feature
+++ b/tests-behat/editor.feature
@@ -35,6 +35,6 @@ Feature: Editor
     Given I am on "index.php"
     When I fill in "subject" with "the subject"
     When I type in editor "body" with text "editor content"
-    When I click icon using css ".trumbowyg-viewHTML-button"
+    When I click using selector ".trumbowyg-viewHTML-button"
     When I press button "Save"
     Then Modal is open with raw text "body : &lt;p&gt;editor content&lt;/p&gt;" in tag "p"
\ No newline at end of file
diff --git a/tests/TextEditorTest.php b/tests/TextEditorTest.php
index 0e599c4..e2f9883 100644
--- a/tests/TextEditorTest.php
+++ b/tests/TextEditorTest.php
@@ -22,86 +22,72 @@ protected function setUp(): void
 
     public function testInit(): void
     {
-        ob_start();
-        try {
-            $app = $this->getApp();
-
-            $app->initLayout([Centered::class]);
-
-            $form = Form::addTo($app);
-            $form->addControl('subject');
-            $form->addControl('editor', [
-                TextEditor::class,
-                'placeholder' => 'test placeholder',
-            ]);
-            $app->run();
-        } finally {
-            $output = ob_get_clean();
-        }
-
-        $this->assertSame(1, substr_count($output, (new TextEditor())->assets_path . '/trumbowyg.js'));
-        $this->assertSame(1, substr_count($output, (new TextEditor())->assets_path . '/ui/trumbowyg.css'));
+        $app = $this->getApp();
+        $app->initLayout([Centered::class]);
+
+        $form = Form::addTo($app);
+        $form->addControl('subject');
+        $form->addControl('editor', [
+            TextEditor::class,
+            'placeholder' => 'test placeholder',
+        ]);
+        $app->run();
+
+        $this->assertSame(1, substr_count($app->output, (new TextEditor())->assets_path . '/trumbowyg.js'));
+        $this->assertSame(1, substr_count($app->output, (new TextEditor())->assets_path . '/ui/trumbowyg.css'));
     }
 
     public function testCheckDouble(): void
     {
-        ob_start();
-        try {
-            $app = $this->getApp();
-
-            $app->initLayout([Centered::class]);
-
-            $form = Form::addTo($app);
-            $form->addControl('subject');
-            $form->addControl('editor', [
-                TextEditor::class,
-                'placeholder' => 'test placeholder',
-            ]);
-            $form->addControl('editor2', [
-                TextEditor::class,
-                'placeholder' => 'test placeholder',
-            ]);
-            $app->run();
-        } finally {
-            $output = ob_get_clean();
-        }
-
-        $this->assertSame(1, substr_count($output, (new TextEditor())->assets_path . '/trumbowyg.js'));
-        $this->assertSame(1, substr_count($output, (new TextEditor())->assets_path . '/ui/trumbowyg.css'));
+        $app = $this->getApp();
+
+        $app->initLayout([Centered::class]);
+
+        $form = Form::addTo($app);
+        $form->addControl('subject');
+        $form->addControl('editor', [
+            TextEditor::class,
+            'placeholder' => 'test placeholder',
+        ]);
+        $form->addControl('editor2', [
+            TextEditor::class,
+            'placeholder' => 'test placeholder',
+        ]);
+        $app->run();
+
+        $this->assertSame(1, substr_count($app->output, (new TextEditor())->assets_path . '/trumbowyg.js'));
+        $this->assertSame(1, substr_count($app->output, (new TextEditor())->assets_path . '/ui/trumbowyg.css'));
     }
 
     public function testPlugin(): void
     {
-        ob_start();
-        try {
-            $app = $this->getApp();
-
-            $app->initLayout([Centered::class]);
-
-            $form = Form::addTo($app);
-            $form->addControl('subject');
-            $form->addControl('editor', [
-                TextEditor::class,
-                'placeholder' => 'test placeholder',
-                'plugins' => [
-                    'base64',
-                ],
-            ]);
-            $app->run();
-        } finally {
-            $output = ob_get_clean();
-        }
-
-        $this->assertStringContainsString('plugins/base64', $output);
+        $app = $this->getApp();
+
+        $app->initLayout([Centered::class]);
+
+        $form = Form::addTo($app);
+        $form->addControl('subject');
+        $form->addControl('editor', [
+            TextEditor::class,
+            'placeholder' => 'test placeholder',
+            'plugins' => [
+                'base64',
+            ],
+        ]);
+        $app->run();
+
+        $this->assertStringContainsString('plugins/base64', $app->output);
     }
 
-    private function getApp(): App
+    private function getApp(): AppFormTestMock
     {
-        return new App([
-            'catch_exceptions' => false,
-            'always_run' => false,
-            'catch_runaway_callbacks' => false,
-            'call_exit' => false,
+        $_SERVER['REQUEST_URI'] = '/';
+
+        return new AppFormTestMock([
+            'catchExceptions' => false,
+            'alwaysRun' => false,
+            'catchRunawayCallbacks' => false,
+            'callExit' => false,
         ]);
     }
 }
@@ -110,7 +96,7 @@ class AppFormTestMock extends App
 {
     public string $output;
 
-    protected function outputResponse(string $data, array $headers): void
+    protected function outputResponse(string $data): void
     {
         $this->output = $data;
     }
diff --git a/tools/CoverageUtil.php b/tools/CoverageUtil.php
index 90a0d84..0062191 100644
--- a/tools/CoverageUtil.php
+++ b/tools/CoverageUtil.php
@@ -20,7 +20,7 @@ private function __construct()
     public static function start(): void
     {
         if (self::$coverage !== null) {
-            throw new \Error('Coverage already started');
+            throw new Error('Coverage already started');
         }
 
         $filter = new Filter();