Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117740753
D1177.1775161215.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
51 KB
Referenced Files
None
Subscribers
None
D1177.1775161215.diff
View Options
diff --git a/src/package.json b/src/package.json
--- a/src/package.json
+++ b/src/package.json
@@ -11,7 +11,6 @@
"lint": "eslint --ext .js,.vue resources && stylelint \"resources/sass/*.scss\" \"resources/vue/*.vue\""
},
"devDependencies": {
- "@deveodk/vue-toastr": "^1.1.0",
"axios": "^0.19",
"bootstrap": "^4.4.1",
"cross-env": "^5.1",
@@ -24,7 +23,6 @@
"@fortawesome/vue-fontawesome": "^0.1.9",
"jquery": "^3.4.1",
"laravel-mix": "^4.0.7",
- "lodash": "^4.17.13",
"popper.js": "^1.12",
"resolve-url-loader": "^2.3.1",
"sass": "^1.15.2",
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -7,14 +7,14 @@
require('./bootstrap')
import AppComponent from '../vue/App'
-import MenuComponent from '../vue/Menu'
+import MenuComponent from '../vue/Widgets/Menu'
import store from './store'
const app = new Vue({
el: '#app',
components: {
- 'app-component': AppComponent,
- 'menu-component': MenuComponent
+ AppComponent,
+ MenuComponent,
},
store,
router: window.router,
@@ -258,7 +258,7 @@
error_msg = error.request ? error.request.statusText : error.message
}
- app.$toastr('error', error_msg || "Server Error", 'Error')
+ app.$toast.error(error_msg || "Server Error")
// Pass the error as-is
return Promise.reject(error)
diff --git a/src/resources/js/bootstrap.js b/src/resources/js/bootstrap.js
--- a/src/resources/js/bootstrap.js
+++ b/src/resources/js/bootstrap.js
@@ -1,17 +1,13 @@
-window._ = require('lodash')
-
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
-try {
- window.Popper = require('popper.js').default
- window.$ = window.jQuery = require('jquery')
+window.Popper = require('popper.js').default
+window.$ = window.jQuery = require('jquery')
- require('bootstrap')
-} catch (e) {}
+require('bootstrap')
/**
* We'll load Vue, VueRouter and global components
@@ -19,18 +15,13 @@
import FontAwesomeIcon from './fontawesome'
import VueRouter from 'vue-router'
-import VueToastr from '@deveodk/vue-toastr'
+import Toast from '../vue/Widgets/Toast'
import store from './store'
window.Vue = require('vue')
Vue.component('svg-icon', FontAwesomeIcon)
-Vue.use(VueToastr, {
- defaultPosition: 'toast-bottom-right',
- defaultTimeout: 5000
-})
-
const vTooltip = (el, binding) => {
const t = []
@@ -55,6 +46,7 @@
}
})
+Vue.use(Toast)
Vue.use(VueRouter)
diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js
--- a/src/resources/js/fontawesome.js
+++ b/src/resources/js/fontawesome.js
@@ -9,7 +9,9 @@
import {
faCheck,
+ faCheckCircle,
faGlobe,
+ faExclamationCircle,
faInfoCircle,
faLock,
faKey,
@@ -26,8 +28,10 @@
// Register only these icons we need
library.add(
- faCheckSquare,
faCheck,
+ faCheckCircle,
+ faCheckSquare,
+ faExclamationCircle,
faGlobe,
faInfoCircle,
faLock,
diff --git a/src/resources/sass/_variables.scss b/src/resources/sass/_variables.scss
--- a/src/resources/sass/_variables.scss
+++ b/src/resources/sass/_variables.scss
@@ -20,3 +20,4 @@
// App colors
$menu-bg-color: #f6f5f3;
+$main-color: #f1a539;
diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss
--- a/src/resources/sass/app.scss
+++ b/src/resources/sass/app.scss
@@ -6,22 +6,39 @@
// Bootstrap
@import '~bootstrap/scss/bootstrap';
-// Toastr
-@import '~@deveodk/vue-toastr/dist/@deveodk/vue-toastr.css';
-
-// Fixes Toastr incompatibility with Bootstrap
-.toast-container > .toast {
- opacity: 1;
-}
-
@import 'menu';
+@import 'toast';
+@import 'forms';
-nav + .container {
- margin-top: 120px;
+html,
+body,
+body > .outer-container {
+ height: 100%;
}
#app {
- margin-bottom: 2rem;
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+
+ & > nav {
+ flex-shrink: 0;
+ z-index: 1;
+ }
+
+ & > div.container {
+ flex-grow: 1;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ }
+
+ & > .filler {
+ flex-grow: 1;
+ }
+
+ & > div.container + .filler {
+ display: none;
+ }
}
#error-page {
@@ -79,40 +96,6 @@
font-weight: bold;
}
-.list-input {
- & > div {
- &:not(:last-child) {
- margin-bottom: -1px;
-
- input,
- a.btn {
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 0;
- }
- }
-
- &:not(:first-child) {
- input,
- a.btn {
- border-top-right-radius: 0;
- border-top-left-radius: 0;
- }
- }
- }
-
- input.is-invalid {
- z-index: 2;
- }
-}
-
-.range-input {
- display: flex;
-
- label {
- margin-right: 0.5em;
- }
-}
-
tfoot.table-fake-body {
background-color: #f8f8f8;
color: grey;
diff --git a/src/resources/sass/forms.scss b/src/resources/sass/forms.scss
new file mode 100644
--- /dev/null
+++ b/src/resources/sass/forms.scss
@@ -0,0 +1,34 @@
+
+.list-input {
+ & > div {
+ &:not(:last-child) {
+ margin-bottom: -1px;
+
+ input,
+ a.btn {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+
+ &:not(:first-child) {
+ input,
+ a.btn {
+ border-top-right-radius: 0;
+ border-top-left-radius: 0;
+ }
+ }
+ }
+
+ input.is-invalid {
+ z-index: 2;
+ }
+}
+
+.range-input {
+ display: flex;
+
+ label {
+ margin-right: 0.5em;
+ }
+}
diff --git a/src/resources/sass/menu.scss b/src/resources/sass/menu.scss
--- a/src/resources/sass/menu.scss
+++ b/src/resources/sass/menu.scss
@@ -1,5 +1,5 @@
-#primary-menu {
- background-color: #f6f5f3;
+#header-menu {
+ background-color: $menu-bg-color;
padding: 0;
line-height: 85px;
@@ -25,7 +25,7 @@
}
&:hover {
- color: #f1a539;
+ color: $main-color;
text-decoration: underline;
}
@@ -35,11 +35,22 @@
}
}
+#footer-menu {
+ background-color: $main-color;
+ height: 100px;
+
+ .navbar-brand {
+ img {
+ width: 170px;
+ }
+ }
+}
+
@include media-breakpoint-up(lg) {
- #primary-menu {
+ #header-menu {
a.menulogin {
text-transform: uppercase;
- border: 2px solid #f1a539;
+ border: 2px solid $main-color;
border-radius: 21px;
line-height: 21px;
letter-spacing: 1px;
@@ -49,16 +60,22 @@
&:focus,
&:hover {
text-decoration: none;
- background-color: #f1a539;
+ background-color: $main-color;
color: #fff;
font-weight: normal;
}
}
}
+
+ .navbar {
+ .navbar {
+ justify-content: flex-end;
+ }
+ }
}
@include media-breakpoint-down(md) {
- #primary-menu {
+ #header-menu {
.navbar-nav {
padding-bottom: 1em;
}
@@ -71,13 +88,13 @@
}
@include media-breakpoint-down(sm) {
- #primary-menu {
+ #header-menu {
padding: 0 1em;
}
}
@media (max-width: 340px) {
- #primary-menu {
+ #header-menu {
.navbar-brand img {
width: 160px;
}
diff --git a/src/resources/sass/toast.scss b/src/resources/sass/toast.scss
new file mode 100644
--- /dev/null
+++ b/src/resources/sass/toast.scss
@@ -0,0 +1,46 @@
+.toast-container {
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ margin: 0.5rem;
+ width: 320px;
+ z-index: 10;
+}
+
+.toast {
+ background-color: rgba(52, 58, 64, 0.95);
+
+ &:not(:last-child) {
+ margin-bottom: 0.3rem;
+ }
+}
+
+.toast-header {
+ background-color: #343a40;
+ border-color: #555;
+ color: #fff;
+
+ strong {
+ flex: 1;
+ }
+
+ svg {
+ font-size: 1.2em;
+ margin-right: 0.5rem;
+ }
+
+ button.close {
+ color: #eee;
+ opacity: 1 !important;
+ text-shadow: none;
+ font-size: 1.2rem;
+
+ &:hover {
+ color: #fff;
+ }
+ }
+}
+
+.toast-body {
+ color: #fff;
+}
diff --git a/src/resources/views/root.blade.php b/src/resources/views/root.blade.php
--- a/src/resources/views/root.blade.php
+++ b/src/resources/views/root.blade.php
@@ -2,7 +2,9 @@
@section('title', "Home")
@section('content')
<div id="app">
- <menu-component></menu-component>
+ <menu-component mode="header"></menu-component>
<app-component></app-component>
+ <div class="filler"></div>
+ <menu-component mode="footer"></menu-component>
</div>
@endsection
diff --git a/src/resources/vue/Admin/Dashboard.vue b/src/resources/vue/Admin/Dashboard.vue
--- a/src/resources/vue/Admin/Dashboard.vue
+++ b/src/resources/vue/Admin/Dashboard.vue
@@ -71,7 +71,7 @@
}
if (response.data.message) {
- this.$toastr('info', response.data.message)
+ this.$toast.info(response.data.message)
}
this.users = response.data.list
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -382,7 +382,7 @@
axios.put('/api/v4/wallets/' + this.user.wallets[0].id, { discount: this.wallet_discount_id })
.then(response => {
if (response.data.status == 'success') {
- this.$toastr('success', response.data.message)
+ this.$toast.success(response.data.message)
this.wallet_discount = response.data.discount
this.wallet_discount_id = response.data.discount_id || ''
this.wallet_discount_description = response.data.discount_description
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -87,7 +87,7 @@
if (response.data.status == 'success') {
this.domain.isConfirmed = true
this.parseStatusInfo(response.data.statusInfo)
- this.$toastr('success', response.data.message)
+ this.$toast.success(response.data.message)
}
})
},
diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue
--- a/src/resources/vue/Login.vue
+++ b/src/resources/vue/Login.vue
@@ -1,5 +1,5 @@
<template>
- <div class="container d-flex flex-column align-items-center">
+ <div class="container d-flex flex-column align-items-center justify-content-center">
<div class="card col-sm-8 col-lg-6">
<div class="card-body">
<h1 class="card-title text-center mb-3">Please sign in</h1>
diff --git a/src/resources/vue/Logout.vue b/src/resources/vue/Logout.vue
--- a/src/resources/vue/Logout.vue
+++ b/src/resources/vue/Logout.vue
@@ -5,7 +5,7 @@
mounted () {
axios.post('/api/auth/logout')
.then(response => {
- this.$toastr('success', response.data.message)
+ this.$toast.success(response.data.message)
})
this.$root.logoutUser()
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -251,7 +251,7 @@
axios[method](location, this.user)
.then(response => {
if (response.data.status == 'success') {
- this.$toastr('success', response.data.message)
+ this.$toast.success(response.data.message)
}
// on new user redirect to users list
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -89,7 +89,7 @@
axios.delete('/api/v4/users/' + id)
.then(response => {
if (response.data.status == 'success') {
- this.$toastr('success', response.data.message)
+ this.$toast.success(response.data.message)
$('#user' + id).remove()
}
})
diff --git a/src/resources/vue/User/Profile.vue b/src/resources/vue/User/Profile.vue
--- a/src/resources/vue/User/Profile.vue
+++ b/src/resources/vue/User/Profile.vue
@@ -99,7 +99,7 @@
delete this.profile.password_confirm
if (response.data.status == 'success') {
- this.$toastr('success', response.data.message)
+ this.$toast.success(response.data.message)
}
})
}
diff --git a/src/resources/vue/User/ProfileDelete.vue b/src/resources/vue/User/ProfileDelete.vue
--- a/src/resources/vue/User/ProfileDelete.vue
+++ b/src/resources/vue/User/ProfileDelete.vue
@@ -40,7 +40,7 @@
.then(response => {
if (response.data.status == 'success') {
this.$root.logoutUser()
- this.$toastr('success', response.data.message)
+ this.$toast.success(response.data.message)
}
})
}
diff --git a/src/resources/vue/Widgets/ListInput.vue b/src/resources/vue/Widgets/ListInput.vue
--- a/src/resources/vue/Widgets/ListInput.vue
+++ b/src/resources/vue/Widgets/ListInput.vue
@@ -24,8 +24,8 @@
<script>
export default {
props: {
- 'list': { type: Array, default: () => [] },
- 'id': { type: String, default: '' }
+ list: { type: Array, default: () => [] },
+ id: { type: String, default: '' }
},
methods: {
addItem() {
diff --git a/src/resources/vue/Menu.vue b/src/resources/vue/Widgets/Menu.vue
rename from src/resources/vue/Menu.vue
rename to src/resources/vue/Widgets/Menu.vue
--- a/src/resources/vue/Menu.vue
+++ b/src/resources/vue/Widgets/Menu.vue
@@ -1,16 +1,20 @@
<template>
- <nav id="primary-menu" class="navbar navbar-expand-lg navbar-light fixed-top">
+ <nav :id="mode + '-menu'" class="navbar navbar-expand-lg navbar-light">
<div class="container">
<router-link class="navbar-brand" :to="{ name: 'dashboard' }">
- <img src="/images/logo_header.png" :alt="app_name">
+ <img :src="'/images/logo_' + mode + '.png'" :alt="app_name">
</router-link>
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
+ <button v-if="mode == 'header'" class="navbar-toggler" type="button"
+ data-toggle="collapse" :data-target="'#' + mode + '-menu-navbar'"
+ aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"
+ >
<span class="navbar-toggler-icon"></span>
</button>
- <div id="navbar" class="collapse navbar-collapse justify-content-end">
+ <div :id="mode + '-menu-navbar'" class="navbar collapse navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item" v-if="!logged_in">
- <router-link class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">Signup</router-link>
+ <router-link v-if="!$root.isAdmin" class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">Signup</router-link>
+ <a v-else class="nav-link link-signup" :href="app_url + '/signup'">Signup</a>
</li>
<li class="nav-item" v-if="!logged_in">
<a class="nav-link link-explore" href="https://kolabnow.com">Explore</a>
@@ -24,6 +28,9 @@
<li class="nav-item" v-if="logged_in">
<a class="nav-link link-contact" href="https://kolabnow.com/contact">Contact</a>
</li>
+ <li class="nav-item" v-if="!logged_in && mode == 'footer'">
+ <a class="nav-link link-tos" href="https://kolabnow.com/tos">ToS</a>
+ </li>
<li class="nav-item" v-if="logged_in">
<a class="nav-link menulogin link-webmail" href="https://kolabnow.com/apps" target="_blank">Webmail</a>
</li>
@@ -44,6 +51,9 @@
<script>
export default {
+ props: {
+ mode: { type: String, default: 'header' }
+ },
data() {
return {
app_name: window.config['app.name'],
@@ -56,7 +66,9 @@
},
mounted() {
// On mobile close the menu when the menu item is clicked
- $('#navbar').on('click', function() { $(this).removeClass('show') })
+ if (this.mode == 'header') {
+ $('#header-menu .navbar').on('click', function() { $(this).removeClass('show') })
+ }
}
}
</script>
diff --git a/src/resources/vue/Widgets/Toast.vue b/src/resources/vue/Widgets/Toast.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/Toast.vue
@@ -0,0 +1,104 @@
+<template>
+ <div class="toast-container" aria-live="polite" aria-atomic="true"></div>
+</template>
+
+<script>
+ import ToastMessage from './ToastMessage.vue'
+
+ export default {
+ methods: {
+ addToast(data) {
+ const msg = Vue.extend(ToastMessage)
+ const instance = new msg({ propsData: { data: data } })
+ instance.$mount()
+ $(instance.$el).prependTo(this.$el)
+ },
+ processObjectData(data) {
+ if (typeof data === 'object' && data.msg !== undefined) {
+ if (data.type === undefined) {
+ data.type = this.defaultType
+ }
+ if (data.timeout === undefined) {
+ data.timeout = this.defaultTimeout
+ }
+
+ return data
+ }
+
+ return {
+ msg: data.toString(),
+ type: this.defaultType,
+ timeout: this.defaultTimeout
+ }
+ },
+ error(msg, title) {
+ let data = this.processObjectData(msg)
+
+ data.type = 'error'
+
+ if (title !== undefined) {
+ data.title = title
+ }
+
+ if (!msg.timeout) {
+ data.timeout *= 2
+ }
+
+ return this.addToast(data)
+ },
+ success(msg, title) {
+ let data = this.processObjectData(msg)
+
+ data.type = 'success'
+
+ if (title !== undefined) {
+ data.title = title
+ }
+
+ return this.addToast(data)
+ },
+ warning(msg, title) {
+ let data = this.processObjectData(msg)
+
+ data.type = 'warning'
+
+ if (title !== undefined) {
+ data.title = title
+ }
+
+ if (!msg.timeout) {
+ data.timeout *= 2
+ }
+
+ return this.addToast(data)
+ },
+ info(msg, title) {
+ let data = this.processObjectData(msg)
+
+ data.type = 'info'
+
+ if (title !== undefined) {
+ data.title = title
+ }
+
+ return this.addToast(data)
+ }
+ },
+ // Plugin installer method
+ install(Vue, options) {
+ const defaultOptions = {
+ defaultType: 'info',
+ defaultTimeout: 5000
+ }
+
+ options = $.extend(defaultOptions, [options || {}])
+
+ const Comp = Vue.extend(this)
+ const vm = new Comp({ data: options }).$mount()
+
+ document.body.appendChild(vm.$el)
+
+ Vue.prototype.$toast = vm
+ }
+ }
+</script>
diff --git a/src/resources/vue/Widgets/ToastMessage.vue b/src/resources/vue/Widgets/ToastMessage.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/ToastMessage.vue
@@ -0,0 +1,60 @@
+<template>
+ <div :class="'toast hide toast-' + data.type" role="alert" aria-live="assertive" aria-atomic="true">
+ <div class="toast-header">
+ <svg-icon icon="info-circle" :class="className()" v-if="data.type == 'info'"></svg-icon>
+ <svg-icon icon="check-circle" :class="className()" v-else-if="data.type == 'success'"></svg-icon>
+ <svg-icon icon="exclamation-circle" :class="className()" v-else-if="data.type == 'error'"></svg-icon>
+ <svg-icon icon="exclamation-circle" :class="className()" v-else-if="data.type == 'warning'"></svg-icon>
+ <strong :class="className()">{{ data.title || title() }}</strong>
+ <button type="button" class="close" data-dismiss="toast" aria-label="Close">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="toast-body">
+ {{ data.msg }}
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ props: {
+ data: { type: Object, default: () => { return {} } }
+ },
+ mounted() {
+ $(this.$el).on('hidden.bs.toast', () => {
+ (this.$el).remove()
+ this.$destroy()
+ })
+ .toast({
+ animation: true,
+ autohide: true,
+ delay: this.data.timeout
+ })
+ .toast('show')
+ },
+ methods: {
+ className() {
+ switch (this.data.type) {
+ case 'error':
+ return 'text-danger'
+ case 'warning':
+ case 'info':
+ case 'success':
+ return 'text-' + this.data.type
+ }
+ },
+ title() {
+ const type = this.data.type
+ switch (type) {
+ case 'info':
+ return 'Information';
+ case 'error':
+ case 'warning':
+ case 'success':
+ return type.charAt(0).toUpperCase() + type.slice(1)
+ }
+ }
+ }
+ }
+</script>
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -40,7 +40,8 @@
{
return $this->click($selector)
->withinBody(function ($browser) use ($content) {
- $browser->assertSeeIn('div.tooltip .tooltip-inner', $content);
+ $browser->waitFor('div.tooltip .tooltip-inner')
+ ->assertSeeIn('div.tooltip .tooltip-inner', $content);
})
->click($selector);
}
@@ -48,7 +49,7 @@
/**
* Assert Toast element content (and close it)
*/
- public function assertToast($type, $title, $message)
+ public function assertToast(string $type, string $message, $title = null)
{
return $this->withinBody(function ($browser) use ($type, $title, $message) {
$browser->with(new Toast($type), function (Browser $browser) use ($title, $message) {
diff --git a/src/tests/Browser/Admin/DashboardTest.php b/src/tests/Browser/Admin/DashboardTest.php
--- a/src/tests/Browser/Admin/DashboardTest.php
+++ b/src/tests/Browser/Admin/DashboardTest.php
@@ -49,7 +49,7 @@
// Test search with no results
$browser->type('@search input', 'unknown')
->click('@search form button')
- ->assertToast(Toast::TYPE_INFO, '', '0 user accounts have been found.')
+ ->assertToast(Toast::TYPE_INFO, '0 user accounts have been found.')
->assertMissing('@search table');
$john = $this->getTestUser('john@kolab.org');
@@ -59,7 +59,7 @@
// Test search with multiple results
$browser->type('@search input', 'john.doe.external@gmail.com')
->click('@search form button')
- ->assertToast(Toast::TYPE_INFO, '', '2 user accounts have been found.')
+ ->assertToast(Toast::TYPE_INFO, '2 user accounts have been found.')
->whenAvailable('@search table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 2);
// TODO: Assert table content
diff --git a/src/tests/Browser/Admin/DomainTest.php b/src/tests/Browser/Admin/DomainTest.php
--- a/src/tests/Browser/Admin/DomainTest.php
+++ b/src/tests/Browser/Admin/DomainTest.php
@@ -60,8 +60,8 @@
->on(new Dashboard())
->visit($user_page)
->on($user_page)
- ->pause(500)
->click('@nav #tab-domains')
+ ->pause(1000)
->click('@user-domains table tbody tr:first-child td a');
$browser->on($domain_page)
diff --git a/src/tests/Browser/Admin/LogonTest.php b/src/tests/Browser/Admin/LogonTest.php
--- a/src/tests/Browser/Admin/LogonTest.php
+++ b/src/tests/Browser/Admin/LogonTest.php
@@ -57,17 +57,11 @@
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
- ->submitLogon('jeroen@jeroen.jeroen', 'wrong');
-
- // Error message
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Invalid username or password.')
- ->closeToast();
- });
-
- // Checks if we're still on the logon page
- $browser->on(new Home());
+ ->submitLogon('jeroen@jeroen.jeroen', 'wrong')
+ // Error message
+ ->assertToast(Toast::TYPE_ERROR, 'Invalid username or password.')
+ // Checks if we're still on the logon page
+ ->on(new Home());
});
}
@@ -118,11 +112,7 @@
});
// Success toast message
- $browser->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('Successfully logged out')
- ->closeToast();
- });
+ $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
@@ -149,11 +139,7 @@
});
// Success toast message
- $browser->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('Successfully logged out')
- ->closeToast();
- });
+ $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
}
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -371,7 +371,7 @@
->click('@body select option:nth-child(2)')
->click('@button-action');
})
- ->assertToast(Toast::TYPE_SUCCESS, '', 'User wallet updated successfully.')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.')
->assertSeeIn('#discount span', '10% - Test voucher')
->click('@nav #tab-subscriptions')
->with('@user-subscriptions', function (Browser $browser) {
@@ -388,7 +388,7 @@
->click('@body select option:nth-child(1)')
->click('@button-action');
})
- ->assertToast(Toast::TYPE_SUCCESS, '', 'User wallet updated successfully.')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.')
->assertSeeIn('#discount span', 'none')
->click('@nav #tab-subscriptions')
->with('@user-subscriptions', function (Browser $browser) {
diff --git a/src/tests/Browser/Components/Menu.php b/src/tests/Browser/Components/Menu.php
--- a/src/tests/Browser/Components/Menu.php
+++ b/src/tests/Browser/Components/Menu.php
@@ -7,6 +7,18 @@
class Menu extends BaseComponent
{
+ protected $mode;
+
+ /**
+ * Object constructor
+ *
+ * @param string $mode Menu mode ('header' or 'footer')
+ */
+ public function __construct($mode = 'header')
+ {
+ $this->mode = $mode;
+ }
+
/**
* Get the root selector for the component.
*
@@ -14,7 +26,7 @@
*/
public function selector()
{
- return '#primary-menu';
+ return '#' . $this->mode . '-menu';
}
/**
@@ -27,7 +39,6 @@
public function assert($browser)
{
$browser->assertVisible($this->selector());
- $browser->assertVisible('@brand');
}
/**
@@ -75,9 +86,9 @@
$selector = $this->selector();
return [
- '@list' => "$selector .navbar-nav",
- '@brand' => "$selector .navbar-brand",
- '@toggler' => "$selector .navbar-toggler",
+ '@list' => ".navbar-nav",
+ '@brand' => ".navbar-brand",
+ '@toggler' => ".navbar-toggler",
];
}
}
diff --git a/src/tests/Browser/Components/Toast.php b/src/tests/Browser/Components/Toast.php
--- a/src/tests/Browser/Components/Toast.php
+++ b/src/tests/Browser/Components/Toast.php
@@ -2,6 +2,7 @@
namespace Tests\Browser\Components;
+use Facebook\WebDriver\WebDriverBy;
use Laravel\Dusk\Component as BaseComponent;
use PHPUnit\Framework\Assert as PHPUnit;
@@ -52,21 +53,34 @@
public function elements()
{
return [
- '@title' => ".toast-title",
- '@message' => ".toast-message",
+ '@title' => ".toast-header > strong",
+ '@message' => ".toast-body",
];
}
/**
* Assert title of the toast element
*/
- public function assertToastTitle($browser, string $title)
+ public function assertToastTitle($browser, string $title = null)
{
if (empty($title)) {
- $browser->assertMissing('@title');
- } else {
- $browser->assertSeeIn('@title', $title);
+ switch ($this->type) {
+ case self::TYPE_ERROR:
+ $title = 'Error';
+ break;
+ case self::TYPE_SUCCESS:
+ $title = 'Success';
+ break;
+ case self::TYPE_WARNING:
+ $title = 'Warning';
+ break;
+ case self::TYPE_INFO:
+ $title = 'Information';
+ break;
+ }
}
+
+ $browser->assertSeeIn('@title', $title);
}
/**
@@ -82,6 +96,6 @@
*/
public function closeToast($browser)
{
- $this->element->click();
+ $this->element->findElements(WebDriverBy::cssSelector('button.close'))[0]->click();
}
}
diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php
--- a/src/tests/Browser/DomainTest.php
+++ b/src/tests/Browser/DomainTest.php
@@ -74,11 +74,7 @@
$browser->assertSeeIn('pre', $domain->namespace);
})
->assertMissing('@verify')
- ->with(new Toast(Toast::TYPE_SUCCESS), function ($browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('Domain verified successfully')
- ->closeToast();
- });
+ ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.');
// Check that confirmed domain page contains only the config box
$browser->visit('/domain/' . $domain->id)
diff --git a/src/tests/Browser/ErrorTest.php b/src/tests/Browser/ErrorTest.php
--- a/src/tests/Browser/ErrorTest.php
+++ b/src/tests/Browser/ErrorTest.php
@@ -16,19 +16,21 @@
public function testError404Page()
{
$this->browse(function (Browser $browser) {
- $browser->visit('/unknown');
+ $browser->visit('/unknown')
+ ->waitFor('#app > #error-page')
+ ->assertVisible('#app > #header-menu')
+ ->assertVisible('#app > #footer-menu');
- $browser->waitFor('#app > #error-page');
- $browser->assertVisible('#app > #primary-menu');
$this->assertSame('404', $browser->text('#error-page .code'));
$this->assertSame('Not Found', $browser->text('#error-page .message'));
});
$this->browse(function (Browser $browser) {
- $browser->visit('/login/unknown');
+ $browser->visit('/login/unknown')
+ ->waitFor('#app > #error-page')
+ ->assertVisible('#app > #header-menu')
+ ->assertVisible('#app > #footer-menu');
- $browser->waitFor('#app > #error-page');
- $browser->assertVisible('#app > #primary-menu');
$this->assertSame('404', $browser->text('#error-page .code'));
$this->assertSame('Not Found', $browser->text('#error-page .message'));
});
diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php
--- a/src/tests/Browser/LogonTest.php
+++ b/src/tests/Browser/LogonTest.php
@@ -19,10 +19,13 @@
public function testLogonMenu(): void
{
$this->browse(function (Browser $browser) {
- $browser->visit(new Home());
- $browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
- });
+ $browser->visit(new Home())
+ ->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
+ })
+ ->within(new Menu('footer'), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'webmail']);
+ });
});
}
@@ -50,11 +53,7 @@
->submitLogon('john@kolab.org', 'wrong');
// Error message
- $browser->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Invalid username or password.')
- ->closeToast();
- });
+ $browser->assertToast(Toast::TYPE_ERROR, 'Invalid username or password.');
// Checks if we're still on the logon page
$browser->on(new Home());
@@ -75,6 +74,9 @@
->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
})
+ ->within(new Menu('footer'), function ($browser) {
+ $browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
+ })
->assertUser('john@kolab.org');
// Assert no "Account status" for this account
@@ -118,11 +120,7 @@
});
// Success toast message
- $browser->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('Successfully logged out')
- ->closeToast();
- });
+ $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
@@ -149,11 +147,7 @@
});
// Success toast message
- $browser->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('Successfully logged out')
- ->closeToast();
- });
+ $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
@@ -176,7 +170,7 @@
'Second factor code is required.'
)
->assertFocused('@second-factor-input')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
// Test invalid code
$browser->type('@second-factor-input', '123456')
@@ -188,7 +182,7 @@
'Second factor code is invalid.'
)
->assertFocused('@second-factor-input')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
$code = \App\Auth\SecondFactor::code('ned@kolab.org');
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -66,7 +66,7 @@
// FIXME: User will not be able to continue anyway, so we should
// either display 1st step or 404 error page
$browser->waitFor('@step1')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Test valid code
@@ -188,7 +188,7 @@
->click('[type=submit]')
->waitFor('#signup_email.is-invalid')
->assertVisible('#signup_email + .invalid-feedback')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid data
@@ -254,7 +254,7 @@
$step->waitFor('#signup_short_code.is-invalid')
->assertVisible('#signup_short_code + .invalid-feedback')
->assertFocused('#signup_short_code')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid code
@@ -341,7 +341,7 @@
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .invalid-feedback')
->assertFocused('#signup_login')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid data (valid login, invalid password)
@@ -353,7 +353,7 @@
->assertMissing('#signup_login.is-invalid')
->assertMissing('#signup_domain + .invalid-feedback')
->assertFocused('#signup_password')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid data
@@ -433,7 +433,7 @@
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .invalid-feedback')
->assertFocused('#signup_login')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid domain
@@ -444,11 +444,11 @@
->type('#signup_confirm', '12345678')
->click('[type=submit]')
->waitUntilMissing('#signup_login.is-invalid')
- ->assertVisible('#signup_domain.is-invalid + .invalid-feedback')
+ ->waitFor('#signup_domain.is-invalid + .invalid-feedback')
->assertMissing('#signup_password.is-invalid')
->assertMissing('#signup_password + .invalid-feedback')
->assertFocused('#signup_domain')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error');
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid domain
@@ -506,7 +506,7 @@
->waitFor('#signup_voucher.is-invalid')
->assertVisible('#signup_voucher + .invalid-feedback')
->assertFocused('#signup_voucher')
- ->assertToast(Toast::TYPE_ERROR, 'Error', 'Form validation error')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
// Submit the correct code
->type('#signup_voucher', 'TEST')
->click('[type=submit]');
diff --git a/src/tests/Browser/UserProfileTest.php b/src/tests/Browser/UserProfileTest.php
--- a/src/tests/Browser/UserProfileTest.php
+++ b/src/tests/Browser/UserProfileTest.php
@@ -100,11 +100,7 @@
->select('#country', '')
->click('button[type=submit]');
})
- ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('User data updated successfully')
- ->closeToast();
- });
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
// Test error handling
$browser->with('@form', function (Browser $browser) {
@@ -117,12 +113,8 @@
'#external_email + .invalid-feedback',
'The external email must be a valid email address.'
)
- ->assertFocused('#phone');
- })
- ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
+ ->assertFocused('#phone')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
});
}
@@ -184,11 +176,7 @@
->waitForLocation('/profile/delete')
->click('#user-delete .button-delete')
->waitForLocation('/login')
- ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('User deleted successfully.')
- ->closeToast();
- });
+ ->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.');
$this->assertTrue($user->fresh()->trashed());
});
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -160,11 +160,7 @@
->type('#last_name', '')
->click('button[type=submit]');
})
- ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('User data updated successfully')
- ->closeToast();
- });
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
// Test error handling (password)
$browser->with('@form', function (Browser $browser) {
@@ -173,12 +169,8 @@
->click('button[type=submit]')
->waitFor('#password + .invalid-feedback')
->assertSeeIn('#password + .invalid-feedback', 'The password confirmation does not match.')
- ->assertFocused('#password');
- })
- ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
+ ->assertFocused('#password')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// TODO: Test password change
@@ -193,12 +185,8 @@
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
- ->click('button[type=submit]');
- })
- ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error');
})
->with('@form', function (Browser $browser) {
$browser->with(new ListInput('#aliases'), function (Browser $browser) {
@@ -212,12 +200,8 @@
$browser->removeListEntry(2)
->addListEntry('john.test@kolab.org');
})
- ->click('button[type=submit]');
- })
- ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('User data updated successfully')
- ->closeToast();
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
});
$john = User::where('email', 'john@kolab.org')->first();
@@ -282,12 +266,8 @@
->click('tbody tr:nth-child(4) td.selection input');
})
->assertMissing('@skus table + .hint')
- ->click('button[type=submit]');
- })
- ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('User data updated successfully')
- ->closeToast();
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
});
$expected = ['activesync', 'groupware', 'mailbox', 'storage', 'storage', 'storage'];
@@ -380,15 +360,9 @@
->click('button[type=submit]')
->assertFocused('#password_confirmation')
->type('#password_confirmation', 'simple')
- ->click('button[type=submit]');
- })
- ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- })
- ->with('@form', function (Browser $browser) {
- $browser->assertSeeIn('#email + .invalid-feedback', 'The specified email is invalid.')
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ ->assertSeeIn('#email + .invalid-feedback', 'The specified email is invalid.')
->assertSeeIn('#password + .invalid-feedback', 'The password confirmation does not match.');
});
@@ -399,17 +373,11 @@
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
- ->click('button[type=submit]');
- })
- ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) {
- $browser->assertToastTitle('Error')
- ->assertToastMessage('Form validation error')
- ->closeToast();
- })
- ->with('@form', function (Browser $browser) {
- $browser->with(new ListInput('#aliases'), function (Browser $browser) {
- $browser->assertFormError(1, 'The specified alias is invalid.', false);
- });
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ ->with(new ListInput('#aliases'), function (Browser $browser) {
+ $browser->assertFormError(1, 'The specified alias is invalid.', false);
+ });
});
// Successful account creation
@@ -420,11 +388,7 @@
})
->click('button[type=submit]');
})
- ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('User created successfully')
- ->closeToast();
- })
+ ->assertToast(Toast::TYPE_SUCCESS, 'User created successfully.')
// check redirection to users list
->waitForLocation('/users')
->on(new UserList())
@@ -474,11 +438,7 @@
->with(new Dialog('#delete-warning'), function (Browser $browser) {
$browser->click('@button-action');
})
- ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
- $browser->assertToastTitle('')
- ->assertToastMessage('User deleted successfully')
- ->closeToast();
- })
+ ->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.')
->with('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 4)
->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
diff --git a/src/tests/TestCaseDusk.php b/src/tests/TestCaseDusk.php
--- a/src/tests/TestCaseDusk.php
+++ b/src/tests/TestCaseDusk.php
@@ -56,7 +56,7 @@
$options->setExperimentalOption('mobileEmulation', ['userAgent' => $ua]);
$options->addArguments(['--window-size=800,640']);
} else {
- $options->addArguments(['--window-size=2560,1440']);
+ $options->addArguments(['--window-size=1280,1024']);
}
// Make sure downloads dir exists and is empty
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Apr 2, 8:20 PM (2 d, 44 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18743592
Default Alt Text
D1177.1775161215.diff (51 KB)
Attached To
Mode
D1177: Menu and Toast improvements
Attached
Detach File
Event Timeline