jqh 5 лет назад
Родитель
Сommit
43a49b00a1
45 измененных файлов с 2186 добавлено и 6 удалено
  1. 16 0
      browser-tests/.ide-helper.php
  2. 77 0
      browser-tests/Browser/AuthTest.php
  3. 28 0
      browser-tests/Browser/Components/MultipleSelect2.php
  4. 67 0
      browser-tests/Browser/Components/Select2.php
  5. 58 0
      browser-tests/Browser/IndexTest.php
  6. 40 0
      browser-tests/Browser/InstallTest.php
  7. 80 0
      browser-tests/Browser/MenuTest.php
  8. 70 0
      browser-tests/Browser/Pages/MenuPage.php
  9. 20 0
      browser-tests/Browser/Pages/Page.php
  10. 2 0
      browser-tests/Browser/console/.gitignore
  11. 2 0
      browser-tests/Browser/screenshots/.gitignore
  12. 160 0
      browser-tests/BrowserExtension.php
  13. 86 0
      browser-tests/Controllers/DropdownController.php
  14. 62 0
      browser-tests/Controllers/ReportController.php
  15. 211 0
      browser-tests/Controllers/UserController.php
  16. 113 0
      browser-tests/CreatesApplication.php
  17. 94 0
      browser-tests/DuskTestCase.php
  18. 89 0
      browser-tests/InteractsWithDatabase.php
  19. 10 0
      browser-tests/Models/File.php
  20. 10 0
      browser-tests/Models/Image.php
  21. 22 0
      browser-tests/Models/MultipleImage.php
  22. 15 0
      browser-tests/Models/Profile.php
  23. 15 0
      browser-tests/Models/Tag.php
  24. 28 0
      browser-tests/Models/Tree.php
  25. 40 0
      browser-tests/Models/User.php
  26. 61 0
      browser-tests/Repositories/Report.php
  27. 11 0
      browser-tests/Repositories/User.php
  28. 7 0
      browser-tests/TestCase.php
  29. 12 0
      browser-tests/helpers.php
  30. BIN
      browser-tests/resources/assets/test.jpg
  31. 381 0
      browser-tests/resources/config/admin.php
  32. 95 0
      browser-tests/resources/config/filesystems.php
  33. BIN
      browser-tests/resources/drivers/chromedriver-linux
  34. BIN
      browser-tests/resources/drivers/chromedriver-mac
  35. BIN
      browser-tests/resources/drivers/chromedriver-win.exe
  36. 15 0
      browser-tests/resources/lang/en/global.php
  37. 16 0
      browser-tests/resources/lang/en/user.php
  38. 98 0
      browser-tests/resources/migrations/2016_11_22_093148_create_test_tables.php
  39. 21 0
      browser-tests/resources/seeds/UserTableSeeder.php
  40. 36 0
      browser-tests/resources/seeds/factory.php
  41. 1 0
      browser-tests/resources/views/test.blade.php
  42. 10 0
      browser-tests/routes.php
  43. 1 2
      composer.json
  44. 1 1
      src/Controllers/AuthController.php
  45. 5 3
      tests/routes.php

+ 16 - 0
browser-tests/.ide-helper.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Laravel\Dusk
+{
+
+    /**
+     * @method $this whenTextAvailable(string $text, $callbackOrSeconds = null, int $seconds = null)
+     * @method $this whenElementAvailable($selector, $callbackOrSeconds = null, int $seconds = null)
+     * @method $this hasInput($field)
+     * @method $this wait(int $seconds, $callback = null)
+     * @method $this assertHidden($selector)
+     */
+    class Browser
+    {
+    }
+}

+ 77 - 0
browser-tests/Browser/AuthTest.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace Tests\Browser;
+
+use Laravel\Dusk\Browser;
+use Tests\TestCase;
+
+/**
+ * @group auth
+ */
+class AuthTest extends TestCase
+{
+    protected $login = false;
+
+    public function testLoginPage()
+    {
+        $this->browse(function (Browser $browser) {
+            $browser->visit(test_admin_path('auth/login'))
+                ->assertSee('Login');
+        });
+    }
+
+    public function testVisitWithoutLogin()
+    {
+        $this->browse(function (Browser $browser) {
+            $browser->visit(test_admin_path('/'))
+                ->assertPathIs(test_admin_path('auth/login'))
+                ->assertGuest('admin');
+        });
+    }
+
+    public function testLogin()
+    {
+        $this->browse(function (Browser $browser) {
+            $credentials = ['username' => 'admin', 'password' => 'admin'];
+
+            $browser->visit(test_admin_path('auth/login'))
+                ->assertPathIs(test_admin_path('auth/login'))
+                ->assertSee('Login')
+                ->type('username', $credentials['username'])
+                ->type('password', $credentials['password'])
+                ->press('Login')
+                ->assertPathIs(test_admin_path('/'))
+                ->assertSee('Administrator')
+                ->assertSee('Dashboard')
+                ->assertSee('Description...')
+                ->assertSee('Environment')
+                ->assertSee('PHP version')
+                ->assertSee('Laravel version')
+                ->assertSee('Extensions')
+                ->assertSee('Dependencies')
+                ->assertSee('php')
+                ->assertSee('laravel/framework');
+
+                //->assertAuthenticated('admin');
+
+            $browser->within('.main-sidebar', function (Browser $browser) {
+                $browser->assertSee('Admin')
+                    ->clickLink('Admin')
+                    ->waitForText('Users', 1)
+                    ->waitForText('Roles', 1)
+                    ->waitForText('Permission', 1)
+                    ->waitForText('Operation log', 1)
+                    ->waitForText('Menu', 1);
+            });
+        });
+    }
+
+    public function testLogout()
+    {
+        $this->browse(function (Browser $browser) {
+            $browser->visit(test_admin_path('auth/logout'))
+                ->assertPathIs(test_admin_path('auth/login'))
+                ->assertGuest('admin');
+        });
+    }
+}

+ 28 - 0
browser-tests/Browser/Components/MultipleSelect2.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Tests\Browser\Components;
+
+use Laravel\Dusk\Browser;
+
+class MultipleSelect2 extends Select2
+{
+    /**
+     * 选中下拉选框
+     *
+     * @param  Browser $browser
+     * @param  array   $values
+     * @param  int  $day
+     * @return void
+     */
+    public function choose($browser, $values)
+    {
+        $values = implode(',', (array) $values);
+
+        $browser->script(
+            <<<JS
+var values = '{$values}';
+$('{$this->selector()}').val(values.split(',')).change();
+JS
+        );
+    }
+}

+ 67 - 0
browser-tests/Browser/Components/Select2.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Tests\Browser\Components;
+
+use Laravel\Dusk\Browser;
+use Laravel\Dusk\Component as BaseComponent;
+
+class Select2 extends BaseComponent
+{
+    protected $selector;
+
+    public function __construct($selector = null)
+    {
+        $this->selector = $selector;
+    }
+
+    /**
+     * 获取组件的 root selector
+     *
+     * @return string
+     */
+    public function selector()
+    {
+        return $this->selector;
+    }
+
+    /**
+     * 浏览器包含组件的断言
+     *
+     * @param  Browser  $browser
+     * @return void
+     */
+    public function assert(Browser $browser)
+    {
+        $browser->assertHidden($this->selector())
+            ->assertVisible('@container');
+    }
+
+    /**
+     * 读取组件的元素快捷方式
+     *
+     * @return array
+     */
+    public function elements()
+    {
+        return [
+            '@container' => '.select2'
+        ];
+    }
+
+    /**
+     * 选中下拉选框
+     *
+     * @param  Browser  $browser
+     * @param  mixed    $value
+     *
+     * @return void
+     */
+    public function choose($browser, $value)
+    {
+        $browser->script(
+            <<<JS
+$('{$this->selector()}').val('{$value}').change();
+JS
+        );
+    }
+}

+ 58 - 0
browser-tests/Browser/IndexTest.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace Tests\Browser;
+
+use Laravel\Dusk\Browser;
+use Tests\TestCase;
+
+/**
+ * @group index
+ */
+class IndexTest extends TestCase
+{
+    public function testIndex()
+    {
+        $this->browse(function (Browser $browser) {
+            $browser->visit(test_admin_path('/'))
+                ->assertSee('Dashboard')
+                ->assertSee('Description...')
+                ->assertSee('Environment')
+                ->assertSee('PHP version')
+                ->assertSee('Laravel version')
+                ->assertSee('Extensions')
+                ->assertSee('Dependencies')
+                ->assertSee('php')
+                ->assertSee('laravel/framework');
+        });
+    }
+
+    public function testClickMenu()
+    {
+        $this->browse(function (Browser $browser) {
+            $browser->visit(test_admin_path('/'))
+                ->within('.main-sidebar', function (Browser $browser) {
+                    $browser
+                        ->clickLink('Admin')
+                        ->whenTextAvailable('Users', 2)
+                        ->clickLink('Users')
+                        ->assertPathIs(test_admin_path('auth/users'))
+                        ->clickLink('Roles')
+                        ->assertPathIs(test_admin_path('auth/roles'))
+                        ->clickLink('Permission')
+                        ->assertPathIs(test_admin_path('auth/permissions'))
+                        ->clickLink('Menu')
+                        ->assertPathIs(test_admin_path('auth/menu'))
+                        ->clickLink('Operation log')
+                        ->assertPathIs(test_admin_path('auth/logs'))
+                        ->clickLink('Extensions')
+                        ->assertPathIs(test_admin_path('auth/extensions'))
+                        ->clickLink('Scaffold')
+                        ->assertPathIs(test_admin_path('auth/scaffold'))
+                        ->clickLink('Routes')
+                        ->assertPathIs(test_admin_path('auth/routes'))
+                        ->clickLink('Icons')
+                        ->assertPathIs(test_admin_path('auth/icons'));
+                });
+        });
+    }
+}

+ 40 - 0
browser-tests/Browser/InstallTest.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Tests\Browser;
+
+use Tests\TestCase;
+
+/**
+ * @group install
+ */
+class InstallTest extends TestCase
+{
+    protected $login = false;
+
+    public function testInstalledDirectories()
+    {
+        $this->assertFileExists(admin_path());
+
+        $this->assertFileExists(admin_path('Controllers'));
+
+        $this->assertFileExists(admin_path('routes.php'));
+
+        $this->assertFileExists(admin_path('bootstrap.php'));
+
+        $this->assertFileExists(admin_path('Controllers/HomeController.php'));
+
+        $this->assertFileExists(admin_path('Controllers/AuthController.php'));
+
+        $this->assertFileExists(config_path('admin.php'));
+
+        $this->assertFileExists(public_path('vendor/dcat-admin'));
+
+        $this->assertFileExists(database_path('migrations/2016_01_04_173148_create_admin_tables.php'));
+
+        $this->assertFileExists(resource_path('lang/en/admin.php'));
+
+        $this->assertFileExists(resource_path('lang/zh-CN/admin.php'));
+
+        $this->assertFileExists(resource_path('lang/zh-CN/global.php'));
+    }
+}

+ 80 - 0
browser-tests/Browser/MenuTest.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace Tests\Browser;
+
+use Dcat\Admin\Models\Menu;
+use Laravel\Dusk\Browser;
+use Tests\Browser\Components\MultipleSelect2;
+use Tests\Browser\Pages\MenuPage;
+use Tests\TestCase;
+
+/**
+ * @group menu
+ */
+class MenuTest extends TestCase
+{
+    public function testMenuIndex()
+    {
+        $this->browse(function (Browser $browser) {
+            $browser->visit(new MenuPage());
+        });
+    }
+
+    public function testAddMenu()
+    {
+        $this->browse(function (Browser $browser) {
+            $item = [
+                'parent_id'   => '0',
+                'title'       => 'Test',
+                'uri'         => 'test',
+                'icon'        => 'fa-user',
+            ];
+
+            $roles = [1];
+
+            $browser->visit(new MenuPage())
+                ->select('parent_id', $item['parent_id'])
+                ->type('title', $item['title'])
+                ->type('uri', $item['uri'])
+                ->type('icon', $item['icon'])
+                ->click('.row')
+                ->within(new MultipleSelect2('select[name="roles[]"]'), function (Browser $browser) use ($item, $roles) {
+                    $browser->choose($roles);
+                })
+                ->pressAndWaitFor('Submit')
+                ->waitForText(__('admin.save_succeeded'), 2)
+                ->assertPathIs(test_admin_path('auth/menu'));
+
+            $newMenuId = Menu::query()->orderByDesc('id')->first()->id;
+
+            $this->seeInDatabase(config('admin.database.menu_table'), $item)
+                ->seeInDatabase(config('admin.database.role_menu_table'), ['role_id' => $roles, 'menu_id' => $newMenuId])
+                ->assertEquals(8, Menu::count());
+        });
+    }
+
+    public function testDeleteMenu()
+    {
+        $this->delete('admin/auth/menu/8');
+        $this->assertEquals(7, Menu::count());
+    }
+    //
+    //public function testEditMenu()
+    //{
+    //    $this->visit('admin/auth/menu/1/edit')
+    //        ->see('Menu')
+    //        ->submitForm('Submit', ['title' => 'blablabla'])
+    //        ->seePageIs('admin/auth/menu')
+    //        ->seeInDatabase(config('admin.database.menu_table'), ['title' => 'blablabla'])
+    //        ->assertEquals(7, Menu::count());
+    //}
+    //
+    //public function testEditMenuParent()
+    //{
+    //    $this->expectException(\Laravel\BrowserKitTesting\HttpException::class);
+    //
+    //    $this->visit('admin/auth/menu/5/edit')
+    //        ->see('Menu')
+    //        ->submitForm('Submit', ['parent_id' => 5]);
+    //}
+}

+ 70 - 0
browser-tests/Browser/Pages/MenuPage.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace Tests\Browser\Pages;
+
+use Laravel\Dusk\Browser;
+
+class MenuPage extends Page
+{
+    /**
+     * Get the URL for the page.
+     *
+     * @return string
+     */
+    public function url()
+    {
+        return test_admin_path('auth/menu');
+    }
+
+    /**
+     * Assert that the browser is on the page.
+     *
+     * @param  \Laravel\Dusk\Browser  $browser
+     * @return void
+     */
+    public function assert(Browser $browser)
+    {
+        $browser->assertSee('Expand')
+            ->assertSee('Collapse')
+            ->assertSee('Save')
+            ->assertSee('New')
+            ->whenAvailable('@tree', function (Browser $browser) {
+                $browser->assertSee('Menu')
+                    ->assertSee('Index')
+                    ->assertSee('Admin')
+                    ->assertSee('Users')
+                    ->assertSee('Roles')
+                    ->assertSee('Permission')
+                    ->assertSee('Menu')
+                    ->assertSee('Operation log');
+            }, 1)
+            ->within('@form', function (Browser $browser) {
+                $browser->assertSee('Parent')
+                    ->assertSee('Title')
+                    ->assertSee('Icon')
+                    ->assertSee('URI')
+                    ->assertSee('Roles')
+                    ->assertSee('Permission')
+                    ->assertSee('Select all')
+                    ->assertSelected('parent_id', 0)
+                    ->hasInput('title')
+                    ->hasInput('icon')
+                    ->hasInput('uri')
+                    ->assertButtonEnabled('Submit')
+                    ->assertButtonEnabled('Reset');
+            });
+    }
+
+    /**
+     * Get the element shortcuts for the page.
+     *
+     * @return array
+     */
+    public function elements()
+    {
+        return [
+            '@tree' => '.dd',
+            '@form' => 'form[method="POST"]',
+        ];
+    }
+}

+ 20 - 0
browser-tests/Browser/Pages/Page.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Tests\Browser\Pages;
+
+use Laravel\Dusk\Page as BasePage;
+
+abstract class Page extends BasePage
+{
+    /**
+     * Get the global element shortcuts for the site.
+     *
+     * @return array
+     */
+    public static function siteElements()
+    {
+        return [
+            '@element' => '#selector',
+        ];
+    }
+}

+ 2 - 0
browser-tests/Browser/console/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 2 - 0
browser-tests/Browser/screenshots/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 160 - 0
browser-tests/BrowserExtension.php

@@ -0,0 +1,160 @@
+<?php
+
+namespace Tests;
+
+use Facebook\WebDriver\Exception\TimeoutException;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Str;
+use Laravel\Dusk\Browser;
+use PHPUnit\Framework\Assert as PHPUnit;
+
+trait BrowserExtension
+{
+    public function extendBrowser()
+    {
+        $this->extendBrowserWhenTextAvailable();
+        $this->extendBrowserWhenElementAvailable();
+        $this->extendBrowserWait();
+        $this->extendBrowserAssertHasInput();
+        $this->extendBrowserAssertHidden();
+    }
+
+    private function extendBrowserAssertHidden()
+    {
+        Browser::macro('assertHidden', function ($selector) {
+            $fullSelector = $this->resolver->format($selector);
+
+            PHPUnit::assertTrue(
+                $this->resolver->findOrFail($selector)->isDisplayed(),
+                "Element [{$fullSelector}] is visible."
+            );
+
+            return $this;
+        });
+    }
+
+    private function extendBrowserWait()
+    {
+        $self = $this;
+
+        Browser::macro('wait', function ($seconds, \Closure $callback = null) use ($self) {
+            $delayBrowser = $self->makeDelayBrowser($this);
+
+            try {
+                $this->waitUsing($seconds, 200, function () {});
+            } catch (TimeoutException $e) {
+                $callback && $callback();
+
+                $delayBrowser();
+            }
+
+            return $delayBrowser;
+        });
+    }
+
+    private function extendBrowserAssertHasInput()
+    {
+        Browser::macro('hasInput', function ($field) {
+            /* @var \Facebook\WebDriver\Remote\RemoteWebElement $element */
+            $this->resolver->resolveForTyping($field);
+
+            return $this;
+        });
+    }
+
+    private function extendBrowserWhenElementAvailable()
+    {
+        $self = $this;
+
+        Browser::macro('whenElementAvailable', function ($selector, $callbackOrSeconds = null, $seconds = null) use ($self) {
+            /* @var Browser $this */
+
+            $callback = null;
+            if (is_callable($callbackOrSeconds)) {
+                $callback = $callbackOrSeconds;
+            } elseif (is_int($callbackOrSeconds)) {
+                $seconds = $callbackOrSeconds;
+            }
+
+            $delayBrowser = $self->makeDelayBrowser($this);
+
+            $this->waitFor($selector, $seconds)->with($selector, function ($value) use ($callback, $delayBrowser) {
+                $callback && $callback($value);
+
+                return $delayBrowser();
+            });
+
+            return $delayBrowser;
+        });
+    }
+
+    private function extendBrowserWhenTextAvailable()
+    {
+        $self = $this;
+
+        Browser::macro('whenTextAvailable', function ($text, $callbackOrSeconds = null, $seconds = null) use ($self) {
+            $callback = null;
+
+            if (is_callable($callbackOrSeconds)) {
+                $callback = $callbackOrSeconds;
+            } elseif (is_int($callbackOrSeconds)) {
+                $seconds = $callbackOrSeconds;
+            }
+
+            $delayBrowser = $self->makeDelayBrowser($this);
+            $text = Arr::wrap($text);
+            $message = $this->formatTimeOutMessage('Waited %s seconds for text', implode("', '", $text));
+
+            $this->waitUsing($seconds, 100, function () use ($text, $callback, $delayBrowser)  {
+                $results = Str::contains($this->resolver->findOrFail('')->getText(), $text);
+
+                if ($results) {
+                    $callback && $callback($this);
+
+                    $delayBrowser();
+                }
+
+                return $results;
+            }, $message);
+
+            return $delayBrowser;
+        });
+    }
+
+    public function makeDelayBrowser($browser)
+    {
+        return new class($browser) {
+            protected $browser;
+
+            protected $callbacks = [];
+
+            public function __construct(Browser $browser)
+            {
+                $this->browser = $browser;
+            }
+
+            public function __call($method, $arguments = [])
+            {
+                $this->callbacks[] = [
+                    'method'    => $method,
+                    'arguments' => $arguments,
+                ];
+
+                return $this;
+            }
+
+            public function __invoke()
+            {
+                $browser = $this->browser;
+
+                foreach ($this->callbacks as $value) {
+                    $method = $value['method'];
+
+                    $browser = $browser->{$method}(...$value['arguments']);
+                }
+
+                return $browser;
+            }
+        };
+    }
+}

+ 86 - 0
browser-tests/Controllers/DropdownController.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace Tests\Controllers;
+
+use Dcat\Admin\Admin;
+use Dcat\Admin\Layout\Content;
+use Dcat\Admin\Layout\Row;
+use Dcat\Admin\Widgets\Box;
+use Dcat\Admin\Widgets\Dropdown;
+use Illuminate\Routing\Controller;
+
+class DropdownController extends Controller
+{
+    protected $tian = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
+    protected $di = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
+
+    public function index(Content $content)
+    {
+        return $content->header('Dropdown Menu')
+            ->row(function (Row $row) {
+                $row->column(3, $this->example1());
+                $row->column(3, $this->example2());
+                $row->column(3, $this->example3());
+            });
+    }
+
+    protected function example1()
+    {
+        $menu1 = Dropdown::make($this->tian)->button('天干');
+
+        $menu2 = Dropdown::make()
+            ->button('使用标题')
+            ->buttonClass('btn btn-sm btn-inverse')
+            ->options($this->tian, '天干')
+            ->options($this->di, '地支');
+
+        $menu3 = Dropdown::make([1, 2, 3, Dropdown::DIVIDER, 4, 5])->button('中间加分隔线');
+
+        return Box::make(
+            'Example1',
+            $menu1->render().' &nbsp; '.$menu2->render().' &nbsp; '.$menu3->render()
+        );
+    }
+
+    protected function example2()
+    {
+        $menu = Dropdown::make($this->tian);
+
+        $menu->map(function ($v, $k) {
+            if ($k === 7) {
+                $this->divider();
+            }
+            $k++;
+
+            return "{$k}. $v";
+        });
+
+        return Box::make('Example2', function () use ($menu) {
+            return "<div class='dropdown'><a class='btn no-shadow text-muted' data-toggle='dropdown' href='javascript:void(0)'><i class='ti-email'></i> 自定义按钮 </a>{$menu->render()}</div>";
+        });
+    }
+
+    protected function example3()
+    {
+        $menu1 = Dropdown::make()
+            ->options($this->tian, '天干')
+            ->options($this->di, '地支')
+            ->click()
+            ->buttonClass('btn btn-sm btn-light')
+            ->map(function ($v, $k) {
+                $k++;
+
+                return "<a class='test_item' data-id='$k', data-value='{$v}' data-test='Hello world.' href='javascript:void(0)'>{$k}. $v</a>";
+            });
+
+        Admin::script(
+            <<<'JS'
+$('.test_item').click(function () {
+    LA.info("Selected: " + JSON.stringify($(this).data()));
+});
+JS
+        );
+
+        return Box::make('Example3', $menu1);
+    }
+}

+ 62 - 0
browser-tests/Controllers/ReportController.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace Tests\Controllers;
+
+use App\Http\Controllers\Controller;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Layout\Content;
+use Tests\Repositories\Report;
+
+class ReportController extends Controller
+{
+    public function index(Content $content)
+    {
+        return $content
+            ->header('报表')
+            ->body($this->grid());
+    }
+
+    protected function grid()
+    {
+        $grid = new Grid(new Report());
+
+        // 开启responsive插件
+        $grid->responsive();
+
+        $grid->disableActions();
+        $grid->disableBatchDelete();
+        $grid->disableCreateButton();
+
+        $grid->rowSelector()->style('success')->click();
+
+        $grid->combine('avgCost', ['avgMonthCost', 'avgQuarterCost', 'avgYearCost'])->responsive()->help('test');
+        $grid->combine('avgVist', ['avgMonthVist', 'avgQuarterVist', 'avgYearVist'])->responsive();
+        $grid->combine('top', ['topCost', 'topVist', 'topIncr'])->responsive()->style('color:#1867c0');
+
+        $grid->content->limit(50)->responsive();
+        $grid->cost->sortable()->responsive();
+        $grid->avgMonthCost->responsive();
+        $grid->avgQuarterCost->responsive()->setHeaderAttributes(['style' => 'color:#5b69bc']);
+        $grid->avgYearCost->responsive();
+        $grid->avgMonthVist->responsive();
+        $grid->avgQuarterVist->responsive();
+        $grid->avgYearVist->responsive();
+        $grid->incrs->hide();
+        $grid->avgVists->hide();
+        $grid->topCost->responsive();
+        $grid->topVist->responsive();
+        $grid->topIncr->responsive();
+        $grid->date->sortable()->responsive();
+
+        $grid->filter(function (Grid\Filter $filter) {
+            $filter->scope(1, admin_trans_field('month'))->where('date', 2019, '<=');
+            $filter->scope(2, admin_trans_label('quarter'))->where('date', 2019, '<=');
+            $filter->scope(3, admin_trans_label('year'))->where('date', 2019, '<=');
+
+            $filter->equal('content');
+            $filter->equal('cost');
+        });
+
+        return $grid;
+    }
+}

+ 211 - 0
browser-tests/Controllers/UserController.php

@@ -0,0 +1,211 @@
+<?php
+
+namespace Tests\Controllers;
+
+use App\Http\Controllers\Controller;
+use Dcat\Admin\Controllers\HasResourceActions;
+use Dcat\Admin\Form;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Layout\Content;
+use Dcat\Admin\Show;
+use Tests\Models\Tag;
+use Tests\Repositories\User;
+
+class UserController extends Controller
+{
+    use HasResourceActions;
+
+    /**
+     * Index interface.
+     *
+     * @return Content
+     */
+    public function index(Content $content)
+    {
+        $content->header('All users');
+        $content->description('description');
+
+        return $content->body($this->grid());
+    }
+
+    /**
+     * Edit interface.
+     *
+     * @param $id
+     *
+     * @return Content
+     */
+    public function edit(Content $content, $id)
+    {
+        $content->header('Edit user');
+        $content->description('description');
+
+        $content->body($this->form()->edit($id));
+
+        return $content;
+    }
+
+    /**
+     * Create interface.
+     *
+     * @return Content
+     */
+    public function create(Content $content)
+    {
+        $content->header('Create user');
+
+        return $content->body($this->form());
+    }
+
+    /**
+     * Show interface.
+     *
+     * @param mixed   $id
+     * @param Content $content
+     *
+     * @return Content
+     */
+    public function show($id, Content $content)
+    {
+        return $content
+            ->header('User')
+            ->description('Detail')
+            ->body($this->detail($id));
+    }
+
+    /**
+     * Make a grid builder.
+     *
+     * @return Grid
+     */
+    protected function grid()
+    {
+        $grid = new Grid(new User());
+
+        $grid->model()->with(['tags', 'profile']);
+
+        $grid->id('ID')->sortable();
+
+        $grid->username();
+        $grid->email();
+        $grid->mobile();
+        $grid->full_name();
+        $grid->avatar()->display(function ($avatar) {
+            return "<img src='{$avatar}' />";
+        });
+        $grid->column('profile.postcode', 'Post code');
+        $grid->column('profile.address');
+        $grid->column('profile.color');
+        $grid->column('profile.start_at', '开始时间');
+        $grid->column('profile.end_at', '结束时间');
+
+        $grid->column('column1_not_in_table')->display(function () {
+            return 'full name:'.$this->full_name;
+        });
+
+        $grid->column('column2_not_in_table')->display(function () {
+            return $this->email.'#'.$this->profile['color'];
+        });
+
+        $grid->tags()->display(function ($tags) {
+            $tags = collect($tags)->map(function ($tag) {
+                return "<code>{$tag['name']}</code>";
+            })->toArray();
+
+            return implode('', $tags);
+        });
+
+        $grid->created_at();
+        $grid->updated_at();
+
+        $grid->filter(function (Grid\Filter $filter) {
+            $filter->equal('id');
+            $filter->like('username');
+            $filter->like('email');
+            $filter->like('profile.postcode');
+            $filter->between('profile.start_at')->datetime();
+            $filter->between('profile.end_at')->datetime();
+        });
+
+        $grid->actions(function (Grid\Displayers\Actions $actions) {
+            if ($actions->getKey() % 2 == 0) {
+                $actions->append('<a href="/" class="btn btn-xs btn-danger">detail</a>');
+            }
+        });
+
+        return $grid;
+    }
+
+    /**
+     * Make a show builder.
+     *
+     * @param mixed $id
+     *
+     * @return Show
+     */
+    protected function detail($id)
+    {
+        return Show::make($id, new User(), function (Show $show) {
+            $show->id('ID');
+            $show->username();
+            $show->email;
+
+            $show->divider();
+
+            $show->full_name();
+            $show->field('profile.postcode');
+
+            $show->tags->json();
+        });
+    }
+
+    /**
+     * Make a form builder.
+     *
+     * @return Form
+     */
+    protected function form()
+    {
+        Form::extend('map', Form\Field\Map::class);
+        Form::extend('editor', Form\Field\Editor::class);
+
+        $form = new Form(new User());
+
+        $form->disableDeleteButton();
+
+        $form->display('id', 'ID');
+        $form->text('username');
+        $form->email('email')->rules('required');
+        $form->mobile('mobile');
+        $form->image('avatar')->help('上传头像', 'fa-image');
+        $form->ignore(['password_confirmation']);
+        $form->password('password')->rules('confirmed');
+        $form->password('password_confirmation');
+
+        $form->divider();
+
+        $form->text('profile.first_name');
+        $form->text('profile.last_name');
+        $form->text('profile.postcode')->help('Please input your postcode');
+        $form->textarea('profile.address')->rows(15);
+        $form->map('profile.latitude', 'profile.longitude', 'Position');
+        $form->color('profile.color');
+        $form->datetime('profile.start_at');
+        $form->datetime('profile.end_at');
+
+        $form->multipleSelect('tags', 'Tags')->options(Tag::all()->pluck('name', 'id'))->customFormat(function ($value) {
+            if (! $value) {
+                return [];
+            }
+
+            return array_column($value, 'id');
+        });
+
+        $form->display('created_at', 'Created At');
+        $form->display('updated_at', 'Updated At');
+
+        $form->html('<a html-field>html...</a>');
+
+        return $form;
+    }
+}

+ 113 - 0
browser-tests/CreatesApplication.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace Tests;
+
+use Dcat\Admin\Models\Administrator;
+use Illuminate\Contracts\Console\Kernel;
+use Illuminate\Filesystem\Filesystem;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+trait CreatesApplication
+{
+    /**
+     * Creates the application.
+     *
+     * @return \Illuminate\Foundation\Application
+     */
+    public function createApplication()
+    {
+        $app = require __DIR__.'/../bootstrap/app.php';
+
+        $app->make(Kernel::class)->bootstrap();
+
+        return $app;
+    }
+
+
+    protected function boot()
+    {
+        $this->config();
+
+        $this->artisan('admin:publish');
+
+        Schema::defaultStringLength(191);
+
+        $this->artisan('admin:install');
+
+        $this->migrateTestTables();
+
+        if (file_exists($routes = admin_path('routes.php'))) {
+            require $routes;
+        }
+
+        require __DIR__.'/helpers.php';
+
+        require __DIR__.'/routes.php';
+
+        require __DIR__.'/resources/seeds/factory.php';
+
+        view()->addNamespace('admin-tests', __DIR__.'/resources/views');
+    }
+
+    protected function destory()
+    {
+        (new \CreateAdminTables())->down();
+
+        (new \CreateTestTables())->down();
+
+        DB::select("delete from `migrations` where `migration` = '2016_01_04_173148_create_admin_tables'");
+        DB::select("delete from `migrations` where `migration` = '2016_11_22_093148_create_test_tables'");
+
+        Artisan::call('migrate:rollback');
+    }
+
+    /**
+     * run package database migrations.
+     *
+     * @return void
+     */
+    public function migrateTestTables()
+    {
+        $fileSystem = new Filesystem();
+
+        $fileSystem->requireOnce(__DIR__.'/resources/migrations/2016_11_22_093148_create_test_tables.php');
+
+        (new \CreateTestTables())->up();
+    }
+
+    protected function config()
+    {
+        $adminConfig = require __DIR__.'/resources/config/admin.php';
+
+        $config = $this->app['config'];
+
+        $config->set('database.default', 'mysql');
+        $config->set('database.connections.mysql.host', env('MYSQL_HOST', 'localhost'));
+        $config->set('database.connections.mysql.database', 'laravel_dcat_admin_test');
+        $config->set('database.connections.mysql.username', env('MYSQL_USER', 'root'));
+        $config->set('database.connections.mysql.password', env('MYSQL_PASSWORD', ''));
+        $config->set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF');
+        $config->set('filesystems', require __DIR__.'/resources/config/filesystems.php');
+        $config->set('admin', $adminConfig);
+        $config->set('app.debug', true);
+
+        foreach (Arr::dot(Arr::get($adminConfig, 'auth'), 'auth.') as $key => $value) {
+            $this->app['config']->set($key, $value);
+        }
+    }
+
+    /**
+     * @return Administrator
+     */
+    protected function getUser()
+    {
+        if ($this->user) {
+            return $this->user;
+        }
+
+        return $this->user = Administrator::first();
+    }
+}

+ 94 - 0
browser-tests/DuskTestCase.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace Tests;
+
+use Dcat\Admin\Models\Administrator;
+use Facebook\WebDriver\Chrome\ChromeOptions;
+use Facebook\WebDriver\Remote\DesiredCapabilities;
+use Facebook\WebDriver\Remote\RemoteWebDriver;
+use Laravel\Dusk\Browser;
+use Laravel\Dusk\TestCase as BaseTestCase;
+
+abstract class DuskTestCase extends BaseTestCase
+{
+    use CreatesApplication, BrowserExtension, InteractsWithDatabase;
+
+    /**
+     * @var Administrator
+     */
+    protected $user;
+
+    /**
+     * @var bool
+     */
+    protected $login = true;
+
+    public function login(Browser $browser)
+    {
+        $browser->loginAs($this->getUser(), 'admin');
+    }
+
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->extendBrowser();
+
+        $this->boot();
+    }
+
+
+    public function tearDown(): void
+    {
+        $this->destory();
+
+        parent::tearDown();
+    }
+
+    /**
+     * Prepare for Dusk test execution.
+     *
+     * @beforeClass
+     * @return void
+     */
+    public static function prepare()
+    {
+        static::startChromeDriver();
+    }
+
+    /**
+     * @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver
+     *
+     * @return \Laravel\Dusk\Browser
+     */
+    protected function newBrowser($driver)
+    {
+        $browser = parent::newBrowser($driver)->resize(1440, 1080);
+
+        if ($this->login) {
+            $this->login($browser);
+        }
+
+        return $browser;
+    }
+
+    /**
+     * Create the RemoteWebDriver instance.
+     *
+     * @return \Facebook\WebDriver\Remote\RemoteWebDriver
+     */
+    protected function driver()
+    {
+        $options = (new ChromeOptions)->addArguments([
+            '--disable-gpu',
+            '--headless',
+            '--window-size=1920,1080',
+        ]);
+
+        return RemoteWebDriver::create(
+            'http://localhost:9515', DesiredCapabilities::chrome()->setCapability(
+                ChromeOptions::CAPABILITY_W3C, $options
+            )
+        );
+    }
+}

+ 89 - 0
browser-tests/InteractsWithDatabase.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace Tests;
+
+trait InteractsWithDatabase
+{
+    /**
+     * Assert that a given where condition exists in the database.
+     *
+     * @param  string  $table
+     * @param  array  $data
+     * @param  string  $connection
+     * @return $this
+     */
+    protected function seeInDatabase($table, array $data, $connection = null)
+    {
+        $database = $this->app->make('db');
+
+        $connection = $connection ?: $database->getDefaultConnection();
+
+        $count = $database->connection($connection)->table($table)->where($data)->count();
+
+        $this->assertGreaterThan(0, $count, sprintf(
+            'Unable to find row in database table [%s] that matched attributes [%s].', $table, json_encode($data)
+        ));
+
+        return $this;
+    }
+
+    /**
+     * Assert that a given where condition does not exist in the database.
+     *
+     * @param  string  $table
+     * @param  array  $data
+     * @param  string  $connection
+     * @return $this
+     */
+    protected function missingFromDatabase($table, array $data, $connection = null)
+    {
+        return $this->notSeeInDatabase($table, $data, $connection);
+    }
+
+    /**
+     * Assert that a given where condition does not exist in the database.
+     *
+     * @param  string  $table
+     * @param  array  $data
+     * @param  string  $connection
+     * @return $this
+     */
+    protected function dontSeeInDatabase($table, array $data, $connection = null)
+    {
+        return $this->notSeeInDatabase($table, $data, $connection);
+    }
+
+    /**
+     * Assert that a given where condition does not exist in the database.
+     *
+     * @param  string  $table
+     * @param  array  $data
+     * @param  string  $connection
+     * @return $this
+     */
+    protected function notSeeInDatabase($table, array $data, $connection = null)
+    {
+        $database = $this->app->make('db');
+
+        $connection = $connection ?: $database->getDefaultConnection();
+
+        $count = $database->connection($connection)->table($table)->where($data)->count();
+
+        $this->assertEquals(0, $count, sprintf(
+            'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
+        ));
+
+        return $this;
+    }
+
+    /**
+     * Seed a given database connection.
+     *
+     * @param  string  $class
+     * @return void
+     */
+    public function seed($class = 'DatabaseSeeder')
+    {
+        $this->artisan('db:seed', ['--class' => $class]);
+    }
+}

+ 10 - 0
browser-tests/Models/File.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Tests\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class File extends Model
+{
+    protected $table = 'test_files';
+}

+ 10 - 0
browser-tests/Models/Image.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Tests\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Image extends Model
+{
+    protected $table = 'test_images';
+}

+ 22 - 0
browser-tests/Models/MultipleImage.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace Tests\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class MultipleImage extends Model
+{
+    protected $table = 'test_multiple_images';
+
+    public function setPicturesAttribute($pictures)
+    {
+        if (is_array($pictures)) {
+            $this->attributes['pictures'] = json_encode($pictures);
+        }
+    }
+
+    public function getPicturesAttribute($pictures)
+    {
+        return json_decode($pictures, true) ?: [];
+    }
+}

+ 15 - 0
browser-tests/Models/Profile.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Tests\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Profile extends Model
+{
+    protected $table = 'test_user_profiles';
+
+    public function user()
+    {
+        return $this->belongsTo(User::class, 'user_id');
+    }
+}

+ 15 - 0
browser-tests/Models/Tag.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Tests\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Tag extends Model
+{
+    protected $table = 'test_tags';
+
+    public function users()
+    {
+        return $this->belongsToMany(User::class, 'test_user_tags', 'tag_id', 'user_id');
+    }
+}

+ 28 - 0
browser-tests/Models/Tree.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Tests\Models;
+
+use Encore\Admin\Traits\AdminBuilder;
+use Encore\Admin\Traits\ModelTree;
+use Illuminate\Database\Eloquent\Model;
+
+class Tree extends Model
+{
+    use AdminBuilder, ModelTree;
+
+    /**
+     * Create a new Eloquent model instance.
+     *
+     * @param array $attributes
+     */
+    public function __construct(array $attributes = [])
+    {
+        $connection = config('admin.database.connection') ?: config('database.default');
+
+        $this->setConnection($connection);
+
+        $this->setTable(config('admin.database.menu_table'));
+
+        parent::__construct($attributes);
+    }
+}

+ 40 - 0
browser-tests/Models/User.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Tests\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class User extends Model
+{
+    protected $table = 'test_users';
+
+    protected $appends = ['full_name', 'position'];
+
+    public function profile()
+    {
+        return $this->hasOne(Profile::class, 'user_id');
+    }
+
+    public function getFullNameAttribute()
+    {
+        if (! $this->profile) {
+            return;
+        }
+
+        return "{$this->profile['first_name']} {$this->profile['last_name']}";
+    }
+
+    public function getPositionAttribute()
+    {
+        if (! $this->profile) {
+            return;
+        }
+
+        return "{$this->profile->latitude} {$this->profile->longitude}";
+    }
+
+    public function tags()
+    {
+        return $this->belongsToMany(Tag::class, 'test_user_tags', 'user_id', 'tag_id');
+    }
+}

+ 61 - 0
browser-tests/Repositories/Report.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Tests\Repositories;
+
+use Dcat\Admin\Grid;
+use Dcat\Admin\Repositories\Repository;
+use Faker\Factory;
+use Illuminate\Pagination\LengthAwarePaginator;
+
+class Report extends Repository
+{
+    public function get(Grid\Model $model)
+    {
+        $items = $this->fetch();
+
+        $paginator = new LengthAwarePaginator(
+            $items,
+            1000,
+            $model->getPerPage(), // 传入每页显示行数
+            $model->getCurrentPage() // 传入当前页码
+        );
+
+        // 必须设置链接
+        $paginator->setPath(\url()->current());
+
+        return $paginator;
+    }
+
+    /**
+     * 这里生成假数据演示报表功能.
+     *
+     * @return array
+     */
+    public function fetch()
+    {
+        $faker = Factory::create();
+
+        $data = [];
+
+        for ($i = 0; $i < 20; $i++) {
+            $data[] = [
+                'content'        => $faker->text,
+                'cost'           => $faker->randomFloat(),
+                'avgMonthCost'   => $faker->randomFloat(),
+                'avgQuarterCost' => $faker->randomFloat(),
+                'avgYearCost'    => $faker->randomFloat(),
+                'incrs'          => $faker->numberBetween(1, 999999999),
+                'avgMonthVist'   => $faker->numberBetween(1, 999999),
+                'avgQuarterVist' => $faker->numberBetween(1, 999999),
+                'avgYearVist'    => $faker->numberBetween(1, 999999),
+                'avgVists'       => $faker->numberBetween(1, 999999),
+                'topCost'        => $faker->numberBetween(1, 999999999),
+                'topVist'        => $faker->numberBetween(1, 9999990009),
+                'topIncr'        => $faker->numberBetween(1, 99999999),
+                'date'           => $faker->date(),
+            ];
+        }
+
+        return $data;
+    }
+}

+ 11 - 0
browser-tests/Repositories/User.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace Tests\Repositories;
+
+use Dcat\Admin\Repositories\EloquentRepository;
+use Tests\Models\User as Model;
+
+class User extends EloquentRepository
+{
+    protected $eloquentClass = Model::class;
+}

+ 7 - 0
browser-tests/TestCase.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Tests;
+
+abstract class TestCase extends DuskTestCase
+{
+}

+ 12 - 0
browser-tests/helpers.php

@@ -0,0 +1,12 @@
+<?php
+
+if (! function_exists('test_admin_path')) {
+    function test_admin_path($path)
+    {
+        if (is_object($path)) {
+            return $path;
+        }
+
+        return admin_base_path($path);
+    }
+}

BIN
browser-tests/resources/assets/test.jpg


+ 381 - 0
browser-tests/resources/config/admin.php

@@ -0,0 +1,381 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin name
+    |--------------------------------------------------------------------------
+    |
+    | This value is the name of dcat-admin, This setting is displayed on the
+    | login page.
+    |
+    */
+    'name' => 'Dcat Admin',
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin logo
+    |--------------------------------------------------------------------------
+    |
+    | The logo of all admin pages. You can also set it as an image by using a
+    | `img` tag, eg '<img src="http://logo-url" alt="Admin logo">'.
+    |
+    */
+    'logo' => '<span>Dcat</span> Admin',
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin mini logo
+    |--------------------------------------------------------------------------
+    |
+    | The logo of all admin pages when the sidebar menu is collapsed. You can
+    | also set it as an image by using a `img` tag, eg
+    | '<img src="http://logo-url" alt="Admin logo">'.
+    |
+    */
+    'logo-mini' => 'Da',
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin route settings
+    |--------------------------------------------------------------------------
+    |
+    | The routing configuration of the admin page, including the path prefix,
+    | the controller namespace, and the default middleware. If you want to
+    | access through the root path, just set the prefix to empty string.
+    |
+    */
+    'route' => [
+
+        'prefix' => env('ADMIN_ROUTE_PREFIX', 'admin'),
+
+        'namespace' => 'App\\Admin\\Controllers',
+
+        'middleware' => ['web', 'admin'],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin install directory
+    |--------------------------------------------------------------------------
+    |
+    | The installation directory of the controller and routing configuration
+    | files of the administration page. The default is `app/Admin`, which must
+    | be set before running `artisan admin::install` to take effect.
+    |
+    */
+    'directory' => app_path('Admin'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin html title
+    |--------------------------------------------------------------------------
+    |
+    | Html title for all pages.
+    |
+    */
+    'title' => 'Admin',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Assets hostname
+    |--------------------------------------------------------------------------
+    |
+   */
+    'assets_server' => env('ADMIN_ASSETS_SERVER'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Cdn setting
+    |--------------------------------------------------------------------------
+    |
+   */
+    'cdn' => env('ADMIN_CDN', false),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Access via `https`
+    |--------------------------------------------------------------------------
+    |
+    | If your page is going to be accessed via https, set it to `true`.
+    |
+    */
+    'https' => env('ADMIN_HTTPS', false),
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin auth setting
+    |--------------------------------------------------------------------------
+    |
+    | Authentication settings for all admin pages. Include an authentication
+    | guard and a user provider setting of authentication driver.
+    |
+    | You can specify a controller for `login` `logout` and other auth routes.
+    |
+    */
+    'auth' => [
+        'enable' => true,
+
+        'controller' => Dcat\Admin\Controllers\AuthController::class,
+
+        'login_view' => 'admin::login',
+
+        'guard' => 'admin',
+
+        'guards' => [
+            'admin' => [
+                'driver'   => 'session',
+                'provider' => 'admin',
+            ],
+        ],
+
+        'providers' => [
+            'admin' => [
+                'driver' => 'eloquent',
+                'model'  => Dcat\Admin\Models\Administrator::class,
+            ],
+        ],
+
+        // Add "remember me" to login form
+        'remember' => true,
+
+        // All method to path like: auth/users/*/edit
+        // or specific method to path like: get:auth/users.
+        'except' => [
+            'auth/login',
+            'auth/logout',
+        ],
+
+    ],
+
+    'grid' => [
+
+        /*
+        |--------------------------------------------------------------------------
+        | The global Grid action display class.
+        |--------------------------------------------------------------------------
+        */
+        'grid_action_class' => Dcat\Admin\Grid\Displayers\DropdownActions::class,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin helpers setting.
+    |--------------------------------------------------------------------------
+    */
+    'helpers' => [
+        'enable' => true,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin permission setting
+    |--------------------------------------------------------------------------
+    |
+    | Permission settings for all admin pages.
+    |
+    */
+    'permission' => [
+        // Whether enable permission.
+        'enable' => true,
+
+        // All method to path like: auth/users/*/edit
+        // or specific method to path like: get:auth/users.
+        'except' => [
+            '/',
+            'auth/login',
+            'auth/logout',
+            'auth/setting',
+        ],
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin menu setting
+    |--------------------------------------------------------------------------
+    |
+    */
+    'menu' => [
+        'cache' => [
+            // enable cache or not
+            'enable' => true,
+            'store'  => 'file',
+        ],
+
+        // Whether enable menu bind to a permission.
+        'bind_permission' => true,
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin upload setting
+    |--------------------------------------------------------------------------
+    |
+    | File system configuration for form upload files and images, including
+    | disk and upload path.
+    |
+    */
+    'upload' => [
+
+        // Disk in `config/filesystem.php`.
+        'disk' => 'admin',
+
+        // Image and file upload path under the disk above.
+        'directory' => [
+            'image' => 'images',
+            'file'  => 'files',
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin database settings
+    |--------------------------------------------------------------------------
+    |
+    | Here are database settings for dcat-admin builtin model & tables.
+    |
+    */
+    'database' => [
+
+        // Database connection for following tables.
+        'connection' => '',
+
+        // User tables and model.
+        'users_table' => 'admin_users',
+        'users_model' => Dcat\Admin\Models\Administrator::class,
+
+        // Role table and model.
+        'roles_table' => 'admin_roles',
+        'roles_model' => Dcat\Admin\Models\Role::class,
+
+        // Permission table and model.
+        'permissions_table' => 'admin_permissions',
+        'permissions_model' => Dcat\Admin\Models\Permission::class,
+
+        // Menu table and model.
+        'menu_table' => 'admin_menu',
+        'menu_model' => Dcat\Admin\Models\Menu::class,
+
+        // Pivot table for table above.
+        'operation_log_table'    => 'admin_operation_log',
+        'user_permissions_table' => 'admin_user_permissions',
+        'role_users_table'       => 'admin_role_users',
+        'role_permissions_table' => 'admin_role_permissions',
+        'role_menu_table'        => 'admin_role_menu',
+        'permission_menu_table'  => 'admin_permission_menu',
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | User operation log setting
+    |--------------------------------------------------------------------------
+    |
+    | By setting this option to open or close operation log in dcat-admin.
+    |
+    */
+    'operation_log' => [
+
+        'enable' => true,
+
+        // Only logging allowed methods in the list
+        'allowed_methods' => ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'],
+
+        // Routes that will not log to database.
+        // All method to path like: auth/logs/*/edit
+        // or specific method to path like: get:auth/logs.
+        'except' => [
+            'auth/logs*',
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Admin map field provider
+    |--------------------------------------------------------------------------
+    |
+    | Supported: "tencent", "google", "yandex".
+    |
+    */
+    'map_provider' => 'google',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application Skin
+    |--------------------------------------------------------------------------
+    |
+    | This value is the skin of admin pages.
+    | @see https://adminlte.io/docs/2.4/layout
+    |
+    | Supported:
+    |    "skin-blue-light", "skin-black", "skin-black-light".
+    |
+    */
+    'skin' => 'skin-black',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application layout
+    |--------------------------------------------------------------------------
+    |
+    | This value is the layout of admin pages.
+    | @see https://adminlte.io/docs/2.4/layout
+    |
+    | Supported: "fixed", "layout-boxed", "layout-top-nav", "sidebar-collapse",
+    | "sidebar-mini".
+    |
+    */
+    'layout' => ['sidebar-mini', 'fixed'],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Login page background image
+    |--------------------------------------------------------------------------
+    |
+    | This value is used to set the background image of login page.
+    |
+    */
+    'login_background_image' => '',
+
+    /*
+    |--------------------------------------------------------------------------
+    | The exception handler class
+    |--------------------------------------------------------------------------
+    |
+    */
+    'exception_handler' => \Dcat\Admin\Exception\Handler::class,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Enable default breadcrumb
+    |--------------------------------------------------------------------------
+    |
+    | Whether enable default breadcrumb for every page content.
+    */
+    'enable_default_breadcrumb' => true,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Extension Directory
+    |--------------------------------------------------------------------------
+    |
+    | When you use command `php artisan admin:extend` to generate extensions,
+    | the extension files will be generated in this directory.
+    */
+    'extension_dir' => app_path('Admin/Extensions'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Settings for extensions.
+    |--------------------------------------------------------------------------
+    |
+    | You can find all available extensions here
+    | https://github.com/dcat-admin-extensions.
+    |
+    */
+    'extensions' => [
+
+    ],
+];

+ 95 - 0
browser-tests/resources/config/filesystems.php

@@ -0,0 +1,95 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Filesystem Disk
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the default filesystem disk that should be used
+    | by the framework. A "local" driver, as well as a variety of cloud
+    | based drivers are available for your choosing. Just store away!
+    |
+    | Supported: "local", "ftp", "s3", "rackspace"
+    |
+    */
+
+    'default' => 'public',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Cloud Filesystem Disk
+    |--------------------------------------------------------------------------
+    |
+    | Many applications store files both locally and in the cloud. For this
+    | reason, you may specify a default "cloud" driver here. This driver
+    | will be bound as the Cloud disk implementation in the container.
+    |
+    */
+
+    'cloud' => 's3',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Filesystem Disks
+    |--------------------------------------------------------------------------
+    |
+    | Here you may configure as many filesystem "disks" as you wish, and you
+    | may even configure multiple disks of the same driver. Defaults have
+    | been setup for each driver as an example of the required options.
+    |
+    */
+
+    'disks' => [
+
+        'local' => [
+            'driver' => 'local',
+            'root'   => storage_path('app'),
+        ],
+
+        'public' => [
+            'driver'     => 'local',
+            'root'       => storage_path('app/public'),
+            'visibility' => 'public',
+        ],
+
+        's3' => [
+            'driver' => 's3',
+            'key'    => 'your-key',
+            'secret' => 'your-secret',
+            'region' => 'your-region',
+            'bucket' => 'your-bucket',
+        ],
+
+        'admin' => [
+            'driver'     => 'local',
+            'root'       => public_path('uploads'),
+            'visibility' => 'public',
+            'url'        => 'http://localhost:8000/uploads/',
+        ],
+
+        'qiniu' => [
+            'driver'  => 'qiniu',
+            'domains' => [
+                'default' => 'of8kfibjo.bkt.clouddn.com', //你的七牛域名
+                'https'   => 'dn-yourdomain.qbox.me',         //你的HTTPS域名
+                'custom'  => 'static.abc.com',                //你的自定义域名
+            ],
+            'access_key' => 'tIyz5h5IDT1-PQS22iRrI4dCBEktWj76O-ls856K',  //AccessKey
+            'secret_key' => 'TCU2GuSlbzxKgnixYO_-pdo4odbXttm1RNNvEwSD',  //SecretKey
+            'bucket'     => 'laravel',  //Bucket名字
+            'notify_url' => '',  //持久化处理回调地址
+        ],
+
+        'aliyun' => [
+            'driver'     => 'oss',
+            'access_id'  => 'LTAIsOQNIDQN78Jr',
+            'access_key' => 'ChsYewaCxm1mi7AIBPRniuncEbFHNO',
+            'bucket'     => 'laravel-admin',
+            'endpoint'   => 'oss-cn-shanghai.aliyuncs.com',
+        ],
+
+    ],
+
+];

BIN
browser-tests/resources/drivers/chromedriver-linux


BIN
browser-tests/resources/drivers/chromedriver-mac


BIN
browser-tests/resources/drivers/chromedriver-win.exe


+ 15 - 0
browser-tests/resources/lang/en/global.php

@@ -0,0 +1,15 @@
+<?php
+
+return [
+    'fields' => [
+        'id' => 'ID',
+
+        'profile' => [
+            'address' => '地址',
+        ],
+
+    ],
+    'labels' => [
+        'List' => '列表',
+    ],
+];

+ 16 - 0
browser-tests/resources/lang/en/user.php

@@ -0,0 +1,16 @@
+<?php
+
+return [
+    'fields' => [
+        'username' => '用户名',
+        'email'    => '邮箱',
+
+        'profile' => [
+            'postcode' => '邮政编码',
+        ],
+
+    ],
+    'labels' => [
+        'user' => '用户',
+    ],
+];

+ 98 - 0
browser-tests/resources/migrations/2016_11_22_093148_create_test_tables.php

@@ -0,0 +1,98 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+
+class CreateTestTables extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('test_images', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('image1');
+            $table->string('image2');
+            $table->string('image3');
+            $table->string('image4');
+            $table->string('image5');
+            $table->string('image6');
+            $table->timestamps();
+        });
+
+        Schema::create('test_multiple_images', function (Blueprint $table) {
+            $table->increments('id');
+            $table->text('pictures');
+            $table->timestamps();
+        });
+
+        Schema::create('test_files', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('file1');
+            $table->string('file2');
+            $table->string('file3');
+            $table->string('file4');
+            $table->string('file5');
+            $table->string('file6');
+            $table->timestamps();
+        });
+
+        Schema::create('test_users', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('username');
+            $table->string('email');
+            $table->string('mobile')->nullable();
+            $table->string('avatar')->nullable();
+            $table->string('password');
+            $table->timestamps();
+        });
+
+        Schema::create('test_user_profiles', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('user_id');
+            $table->string('first_name')->nullable();
+            $table->string('last_name')->nullable();
+            $table->string('postcode')->nullable();
+            $table->string('address')->nullable();
+            $table->string('latitude')->nullable();
+            $table->string('longitude')->nullable();
+            $table->string('color')->nullable();
+            $table->timestamp('start_at')->nullable();
+            $table->timestamp('end_at')->nullable();
+
+            $table->timestamps();
+        });
+
+        Schema::create('test_tags', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('name');
+            $table->timestamps();
+        });
+
+        Schema::create('test_user_tags', function (Blueprint $table) {
+            $table->integer('user_id');
+            $table->integer('tag_id');
+            $table->index(['user_id', 'tag_id']);
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('test_images');
+        Schema::dropIfExists('test_multiple_images');
+        Schema::dropIfExists('test_files');
+        Schema::dropIfExists('test_users');
+        Schema::dropIfExists('test_user_profiles');
+        Schema::dropIfExists('test_tags');
+        Schema::dropIfExists('test_user_tags');
+    }
+}

+ 21 - 0
browser-tests/resources/seeds/UserTableSeeder.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Tests\Seeds;
+
+use Tests\Models\Profile;
+use Tests\Models\Tag;
+use Tests\Models\User;
+use Illuminate\Database\Seeder;
+
+class UserTableSeeder extends Seeder
+{
+    public function run()
+    {
+        factory(User::class, 50)
+            ->create()
+            ->each(function ($u) {
+                $u->profile()->save(factory(Profile::class)->make());
+                $u->tags()->saveMany(factory(Tag::class, 5)->make());
+            });
+    }
+}

+ 36 - 0
browser-tests/resources/seeds/factory.php

@@ -0,0 +1,36 @@
+<?php
+
+use Faker\Generator as Faker;
+use Illuminate\Database\Eloquent\Factory;
+
+$factory = app(Factory::class);
+
+$factory->define(\Tests\Models\User::class, function (Faker $faker) {
+    return [
+        'username' => $faker->userName,
+        'email'    => $faker->email,
+        'mobile'   => $faker->phoneNumber,
+        'avatar'   => $faker->imageUrl(),
+        'password' => bcrypt('123456'),
+    ];
+});
+
+$factory->define(\Tests\Models\Profile::class, function (Faker $faker) {
+    return [
+        'first_name' => $faker->firstName,
+        'last_name'  => $faker->lastName,
+        'postcode'   => $faker->postcode,
+        'address'    => $faker->address,
+        'latitude'   => $faker->latitude,
+        'longitude'  => $faker->longitude,
+        'color'      => $faker->hexColor,
+        'start_at'   => $faker->dateTime,
+        'end_at'     => $faker->dateTime,
+    ];
+});
+
+$factory->define(\Tests\Models\Tag::class, function (Faker $faker) {
+    return [
+        'name' => $faker->word,
+    ];
+});

+ 1 - 0
browser-tests/resources/views/test.blade.php

@@ -0,0 +1 @@
+<h1>Hello world</h1>

+ 10 - 0
browser-tests/routes.php

@@ -0,0 +1,10 @@
+<?php
+
+Route::group([
+    'prefix'     => config('admin.route.prefix'),
+    'namespace'  => 'Tests\Controllers',
+    'middleware' => ['web', 'admin'],
+], function ($router) {
+    $router->resource('tests/users', UserController::class);
+    $router->resource('tests/report', ReportController::class);
+});

+ 1 - 2
composer.json

@@ -26,8 +26,7 @@
         "phpunit/phpunit": "^7.5",
         "fzaninotto/faker": "^1.4",
         "mockery/mockery": "^1.0",
-        "matt-allan/laravel-code-style": "^0.3.0",
-        "beyondcode/dusk-dashboard": "^1.2"
+        "matt-allan/laravel-code-style": "^0.3.0"
     },
     "autoload": {
         "psr-4": {

+ 1 - 1
src/Controllers/AuthController.php

@@ -226,7 +226,7 @@ class AuthController extends Controller
      */
     protected function redirectPath()
     {
-        return $this->redirectTo ?: config('admin.route.prefix');
+        return $this->redirectTo ?: admin_url('/');
     }
 
     /**

+ 5 - 3
tests/routes.php

@@ -1,10 +1,12 @@
 <?php
 
+use Illuminate\Routing\Router;
+
 Route::group([
     'prefix'     => config('admin.route.prefix'),
     'namespace'  => 'Dcat\Admin\Tests\Controllers',
     'middleware' => ['web', 'admin'],
-], function ($router) {
-    $router->resource('tests/users', UserController::class);
-    $router->resource('tests/report', ReportController::class);
+], function (Router $router) {
+    $router->resource('tests/users', 'UserController');
+    $router->resource('tests/report', 'ReportController');
 });