User account
New user account
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
index 16968c84..02dc12bf 100644
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -1,123 +1,123 @@
User Accounts
Create user
Primary Email
-
+
{{ user.email }}
Delete
Do you really want to delete this user permanently?
This will delete all account data and withdraw the permission to access the email account.
Please note that this action cannot be undone.
diff --git a/src/resources/vue/Widgets/Status.vue b/src/resources/vue/Widgets/Status.vue
index ec3ed2bd..2549569f 100644
--- a/src/resources/vue/Widgets/Status.vue
+++ b/src/resources/vue/Widgets/Status.vue
@@ -1,177 +1,177 @@
We are preparing your account.
We are preparing the domain.
We are preparing the user account.
Some features may be missing or readonly at the moment.
The process never ends? Press the "Refresh" button, please.
Refresh
Your account is almost ready.
The domain is almost ready.
The user account is almost ready.
Verify your domain to finish the setup process.
-
+
Verify
Verify domain
{{ state.title || 'Initializing...' }}
diff --git a/src/tests/Browser/Admin/LogonTest.php b/src/tests/Browser/Admin/LogonTest.php
index b221169d..b258c23b 100644
--- a/src/tests/Browser/Admin/LogonTest.php
+++ b/src/tests/Browser/Admin/LogonTest.php
@@ -1,145 +1,145 @@
browse(function (Browser $browser) {
$browser->visit(new Home())
->with(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
})
->assertMissing('@second-factor-input')
->assertMissing('@forgot-password');
});
}
/**
* Test redirect to /login if user is unauthenticated
*/
public function testLogonRedirect(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/dashboard');
// Checks if we're really on the login page
$browser->waitForLocation('/login')
->on(new Home());
});
}
/**
* Logon with wrong password/user test
*/
public function testLogonWrongCredentials(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(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());
});
}
/**
* Successful logon test
*/
public function testLogonSuccessful(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true);
// Checks if we're really on Dashboard page
$browser->on(new Dashboard())
->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
})
->assertUser('jeroen@jeroen.jeroen');
// Test that visiting '/' with logged in user does not open logon form
// but "redirects" to the dashboard
$browser->visit('/')->on(new Dashboard());
});
}
/**
* Logout test
*
* @depends testLogonSuccessful
*/
public function testLogout(): void
{
$this->browse(function (Browser $browser) {
$browser->on(new Dashboard());
// Click the Logout button
$browser->within(new Menu(), function ($browser) {
- $browser->click('.link-logout');
+ $browser->clickMenuItem('logout');
});
// We expect the logon page
$browser->waitForLocation('/login')
->on(new Home());
// with default menu
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
});
// Success toast message
$browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
/**
* Logout by URL test
*/
public function testLogoutByURL(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true);
// Checks if we're really on Dashboard page
$browser->on(new Dashboard());
// Use /logout url, and expect the logon page
$browser->visit('/logout')
->waitForLocation('/login')
->on(new Home());
// with default menu
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
});
// Success toast message
$browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
}
diff --git a/src/tests/Browser/Components/Menu.php b/src/tests/Browser/Components/Menu.php
index e19139a4..544e4c38 100644
--- a/src/tests/Browser/Components/Menu.php
+++ b/src/tests/Browser/Components/Menu.php
@@ -1,94 +1,113 @@
mode = $mode;
}
/**
* Get the root selector for the component.
*
* @return string
*/
public function selector()
{
return '#' . $this->mode . '-menu';
}
/**
* Assert that the browser page contains the component.
*
* @param \Laravel\Dusk\Browser $browser
*
* @return void
*/
public function assert($browser)
{
$browser->assertVisible($this->selector());
}
/**
* Assert that menu contains only specified menu items.
*
* @param \Laravel\Dusk\Browser $browser
* @param array $items List of menu items
+ * @param string $active Expected active item
*
* @return void
*/
- public function assertMenuItems($browser, array $items)
+ public function assertMenuItems($browser, array $items, string $active = null)
{
- // TODO: On mobile the links will not be visible
+ // On mobile the links are not visible, show them first (wait for transition)
+ if ($browser->isPhone()) {
+ $browser->click('@toggler')->waitFor('.navbar-collapse.show');
+ }
foreach ($items as $item) {
$browser->assertVisible('.link-' . $item);
}
// Check number of items, to make sure there's no extra items
PHPUnit::assertCount(count($items), $browser->elements('li'));
+
+ if ($active) {
+ $browser->assertPresent(".link-{$active}.active");
+ }
+
+ if ($browser->isPhone()) {
+ $browser->click('@toggler')->waitUntilMissing('.navbar-collapse.show');
+ }
}
/**
- * Assert that specified menu item is active
+ * Click menu link.
*
- * @param \Laravel\Dusk\Browser $browser
- * @param string $item Menu item name
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ * @param string $name Menu item name
*
* @return void
*/
- public function assertActiveItem($browser, string $item)
+ public function clickMenuItem($browser, string $name)
{
- // TODO: On mobile the links will not be visible
+ // On mobile the links are not visible, show them first (wait for transition)
+ if ($browser->isPhone()) {
+ $browser->click('@toggler')->waitFor('.navbar-collapse.show');
+ }
- $browser->assertVisible(".link-{$item}.active");
+ $browser->click('.link-' . $name);
+
+ if ($browser->isPhone()) {
+ $browser->waitUntilMissing('.navbar-collapse.show');
+ }
}
/**
* Get the element shortcuts for the component.
*
* @return array
*/
public function elements()
{
$selector = $this->selector();
return [
'@list' => ".navbar-nav",
'@brand' => ".navbar-brand",
'@toggler' => ".navbar-toggler",
];
}
}
diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php
index 89fa7d0a..1d4eb6df 100644
--- a/src/tests/Browser/LogonTest.php
+++ b/src/tests/Browser/LogonTest.php
@@ -1,196 +1,207 @@
browse(function (Browser $browser) {
$browser->visit(new Home())
->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
- })
- ->within(new Menu('footer'), function ($browser) {
+ });
+
+ if ($browser->isDesktop()) {
+ $browser->within(new Menu('footer'), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'webmail']);
});
+ } else {
+ $browser->assertMissing('#footer-menu .navbar-nav');
+ }
});
}
/**
* Test redirect to /login if user is unauthenticated
*/
public function testLogonRedirect(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/dashboard');
// Checks if we're really on the login page
$browser->waitForLocation('/login')
->on(new Home());
});
}
/**
* Logon with wrong password/user test
*/
public function testLogonWrongCredentials(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'wrong');
// Error message
$browser->assertToast(Toast::TYPE_ERROR, 'Invalid username or password.');
// Checks if we're still on the logon page
$browser->on(new Home());
});
}
/**
* Successful logon test
*/
public function testLogonSuccessful(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true);
// Checks if we're really on Dashboard page
$browser->on(new Dashboard())
->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
- })
- ->within(new Menu('footer'), function ($browser) {
+ });
+
+ if ($browser->isDesktop()) {
+ $browser->within(new Menu('footer'), function ($browser) {
$browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
- })
- ->assertUser('john@kolab.org');
+ });
+ } else {
+ $browser->assertMissing('#footer-menu .navbar-nav');
+ }
+
+ $browser->assertUser('john@kolab.org');
// Assert no "Account status" for this account
$browser->assertMissing('@status');
// Goto /domains and assert that the link on logo element
// leads to the dashboard
$browser->visit('/domains')
->waitForText('Domains')
->click('a.navbar-brand')
->on(new Dashboard());
// Test that visiting '/' with logged in user does not open logon form
// but "redirects" to the dashboard
$browser->visit('/')->on(new Dashboard());
});
}
/**
* Logout test
*
* @depends testLogonSuccessful
*/
public function testLogout(): void
{
$this->browse(function (Browser $browser) {
$browser->on(new Dashboard());
// Click the Logout button
$browser->within(new Menu(), function ($browser) {
- $browser->click('.link-logout');
+ $browser->clickMenuItem('logout');
});
// We expect the logon page
$browser->waitForLocation('/login')
->on(new Home());
// with default menu
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
});
// Success toast message
$browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
/**
* Logout by URL test
*/
public function testLogoutByURL(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true);
// Checks if we're really on Dashboard page
$browser->on(new Dashboard());
// Use /logout url, and expect the logon page
$browser->visit('/logout')
->waitForLocation('/login')
->on(new Home());
// with default menu
$browser->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
});
// Success toast message
$browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out');
});
}
/**
* Test 2-Factor Authentication
*
* @depends testLogoutByURL
*/
public function test2FA(): void
{
$this->browse(function (Browser $browser) {
// Test missing 2fa code
$browser->on(new Home())
->type('@email-input', 'ned@kolab.org')
->type('@password-input', 'simple123')
->press('form button')
->waitFor('@second-factor-input.is-invalid + .invalid-feedback')
->assertSeeIn(
'@second-factor-input.is-invalid + .invalid-feedback',
'Second factor code is required.'
)
->assertFocused('@second-factor-input')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
// Test invalid code
$browser->type('@second-factor-input', '123456')
->press('form button')
->waitUntilMissing('@second-factor-input.is-invalid')
->waitFor('@second-factor-input.is-invalid + .invalid-feedback')
->assertSeeIn(
'@second-factor-input.is-invalid + .invalid-feedback',
'Second factor code is invalid.'
)
->assertFocused('@second-factor-input')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
$code = \App\Auth\SecondFactor::code('ned@kolab.org');
// Test valid (TOTP) code
$browser->type('@second-factor-input', $code)
->press('form button')
->waitUntilMissing('@second-factor-input.is-invalid')
->waitForLocation('/dashboard')->on(new Dashboard());
});
}
}
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
index 005fe6c2..0a279817 100644
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -1,533 +1,536 @@
deleteTestUser('signuptestdusk@' . \config('app.domain'));
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
}
public function tearDown(): void
{
$this->deleteTestUser('signuptestdusk@' . \config('app.domain'));
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
parent::tearDown();
}
/**
* Test signup code verification with a link
*/
public function testSignupCodeByLink(): void
{
// Test invalid code (invalid format)
$this->browse(function (Browser $browser) {
// Register Signup page element selectors we'll be using
$browser->onWithoutAssert(new Signup());
// TODO: Test what happens if user is logged in
$browser->visit('/signup/invalid-code');
// TODO: According to https://github.com/vuejs/vue-router/issues/977
// it is not yet easily possible to display error page component (route)
// without changing the URL
// TODO: Instead of css selector we should probably define page/component
// and use it instead
$browser->waitFor('#error-page');
});
// Test invalid code (valid format)
$this->browse(function (Browser $browser) {
$browser->visit('/signup/XXXXX-code');
// 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, 'Form validation error');
});
// Test valid code
$this->browse(function (Browser $browser) {
$code = SignupCode::create([
'data' => [
'email' => 'User@example.org',
'first_name' => 'User',
'last_name' => 'Name',
'plan' => 'individual',
'voucher' => '',
]
]);
$browser->visit('/signup/' . $code->short_code . '-' . $code->code)
->waitFor('@step3')
->assertMissing('@step1')
->assertMissing('@step2');
// FIXME: Find a nice way to read javascript data without using hidden inputs
$this->assertSame($code->code, $browser->value('@step2 #signup_code'));
// TODO: Test if the signup process can be completed
});
}
/**
* Test signup "welcome" page
*/
public function testSignupStep0(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Signup());
$browser->assertVisible('@step0')
->assertMissing('@step1')
->assertMissing('@step2')
->assertMissing('@step3');
$browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
- $browser->assertActiveItem('signup');
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login'], 'signup');
});
$browser->waitFor('@step0 .plan-selector > .plan-box');
// Assert first plan box and press the button
$browser->with('@step0 .plan-selector > .plan-individual', function ($step) {
$step->assertVisible('button')
->assertSeeIn('button', 'Individual Account')
->assertVisible('.plan-description')
->click('button');
});
$browser->waitForLocation('/signup/individual')
->assertVisible('@step1')
->assertMissing('@step0')
->assertMissing('@step2')
->assertMissing('@step3')
->assertFocused('@step1 #signup_first_name');
// Click Back button
$browser->click('@step1 [type=button]')
->waitForLocation('/signup')
->assertVisible('@step0')
->assertMissing('@step1')
->assertMissing('@step2')
->assertMissing('@step3');
// Choose the group account plan
$browser->click('@step0 .plan-selector > .plan-group button')
->waitForLocation('/signup/group')
->assertVisible('@step1')
->assertMissing('@step0')
->assertMissing('@step2')
->assertMissing('@step3')
->assertFocused('@step1 #signup_first_name');
// TODO: Test if 'plan' variable is set properly in vue component
});
}
/**
* Test 1st step of the signup process
*/
public function testSignupStep1(): void
{
$this->browse(function (Browser $browser) {
- $browser->visit('/signup/individual')->onWithoutAssert(new Signup());
-
- $browser->assertVisible('@step1');
-
- $browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
- $browser->assertActiveItem('signup');
- });
+ $browser->visit('/signup/individual')
+ ->onWithoutAssert(new Signup());
// Here we expect two text inputs and Back and Continue buttons
$browser->with('@step1', function ($step) {
$step->assertVisible('#signup_last_name')
->assertVisible('#signup_first_name')
->assertFocused('#signup_first_name')
->assertVisible('#signup_email')
->assertVisible('[type=button]')
->assertVisible('[type=submit]');
});
// Submit empty form
// Email is required, so after pressing Submit
// we expect focus to be moved to the email input
$browser->with('@step1', function ($step) {
$step->click('[type=submit]');
$step->assertFocused('#signup_email');
});
+ $browser->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login'], 'signup');
+ });
+
// Submit invalid email, and first_name
// We expect both inputs to have is-invalid class added, with .invalid-feedback element
$browser->with('@step1', function ($step) use ($browser) {
$step->type('#signup_first_name', str_repeat('a', 250))
->type('#signup_email', '@test')
->click('[type=submit]')
->waitFor('#signup_email.is-invalid')
->assertVisible('#signup_first_name.is-invalid')
->assertVisible('#signup_email + .invalid-feedback')
->assertVisible('#signup_last_name + .invalid-feedback')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid data
// We expect error state on email input to be removed, and Step 2 form visible
$browser->with('@step1', function ($step) {
$step->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]')
->assertMissing('#signup_email.is-invalid')
->assertMissing('#signup_email + .invalid-feedback');
});
$browser->waitUntilMissing('@step2 #signup_code[value=""]');
$browser->waitFor('@step2');
$browser->assertMissing('@step1');
});
}
/**
* Test 2nd Step of the signup process
*
* @depends testSignupStep1
*/
public function testSignupStep2(): void
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step2')
->assertMissing('@step0')
->assertMissing('@step1')
->assertMissing('@step3');
// Here we expect one text input, Back and Continue buttons
$browser->with('@step2', function ($step) {
$step->assertVisible('#signup_short_code')
->assertFocused('#signup_short_code')
->assertVisible('[type=button]')
->assertVisible('[type=submit]');
});
// Test Back button functionality
$browser->click('@step2 [type=button]')
->waitFor('@step1')
->assertFocused('@step1 #signup_first_name')
->assertMissing('@step2');
// Submit valid Step 1 data (again)
$browser->with('@step1', function ($step) {
$step->type('#signup_first_name', 'User')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]');
});
$browser->waitFor('@step2');
$browser->assertMissing('@step1');
// Submit invalid code
// We expect code input to have is-invalid class added, with .invalid-feedback element
$browser->with('@step2', function ($step) use ($browser) {
$step->type('#signup_short_code', 'XXXXX');
$step->click('[type=submit]');
$step->waitFor('#signup_short_code.is-invalid')
->assertVisible('#signup_short_code + .invalid-feedback')
->assertFocused('#signup_short_code')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid code
// We expect error state on code input to be removed, and Step 3 form visible
$browser->with('@step2', function ($step) {
// Get the code and short_code from database
// FIXME: Find a nice way to read javascript data without using hidden inputs
$code = $step->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code);
$step->click('[type=submit]');
$step->assertMissing('#signup_short_code.is-invalid');
$step->assertMissing('#signup_short_code + .invalid-feedback');
});
$browser->waitFor('@step3');
$browser->assertMissing('@step2');
});
}
/**
* Test 3rd Step of the signup process
*
* @depends testSignupStep2
*/
public function testSignupStep3(): void
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step3');
// Here we expect 3 text inputs, Back and Continue buttons
$browser->with('@step3', function ($step) {
$step->assertVisible('#signup_login');
$step->assertVisible('#signup_password');
$step->assertVisible('#signup_confirm');
$step->assertVisible('select#signup_domain');
$step->assertVisible('[type=button]');
$step->assertVisible('[type=submit]');
$step->assertFocused('#signup_login');
$step->assertValue('select#signup_domain', \config('app.domain'));
$step->assertValue('#signup_login', '');
$step->assertValue('#signup_password', '');
$step->assertValue('#signup_confirm', '');
// TODO: Test domain selector
});
// Test Back button
$browser->click('@step3 [type=button]');
$browser->waitFor('@step2');
$browser->assertFocused('@step2 #signup_short_code');
$browser->assertMissing('@step3');
// TODO: Test form reset when going back
// Submit valid code again
$browser->with('@step2', function ($step) {
$code = $step->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code);
$step->click('[type=submit]');
});
$browser->waitFor('@step3');
// Submit invalid data
$browser->with('@step3', function ($step) use ($browser) {
$step->assertFocused('#signup_login')
->type('#signup_login', '*')
->type('#signup_password', '12345678')
->type('#signup_confirm', '123456789')
->click('[type=submit]')
->waitFor('#signup_login.is-invalid')
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .invalid-feedback')
->assertFocused('#signup_login')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid data (valid login, invalid password)
$browser->with('@step3', function ($step) use ($browser) {
$step->type('#signup_login', 'SignupTestDusk')
->click('[type=submit]')
->waitFor('#signup_password.is-invalid')
->assertVisible('#signup_password + .invalid-feedback')
->assertMissing('#signup_login.is-invalid')
->assertMissing('#signup_domain + .invalid-feedback')
->assertFocused('#signup_password')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit valid data
$browser->with('@step3', function ($step) {
$step->type('#signup_confirm', '12345678');
$step->click('[type=submit]');
});
// At this point we should be auto-logged-in to dashboard
$browser->waitUntilMissing('@step3')
->waitUntilMissing('.app-loader')
->on(new Dashboard())
->assertUser('signuptestdusk@' . \config('app.domain'));
// Logout the user
- $browser->click('a.link-logout');
+ $browser->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
});
}
/**
* Test signup for a group account
*/
public function testSignupGroup(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Signup());
// Choose the group account plan
$browser->waitFor('@step0 .plan-group button')
->click('@step0 .plan-group button');
// Submit valid data
// We expect error state on email input to be removed, and Step 2 form visible
$browser->whenAvailable('@step1', function ($step) {
$step->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]');
});
// Submit valid code
$browser->whenAvailable('@step2', function ($step) {
// Get the code and short_code from database
// FIXME: Find a nice way to read javascript data without using hidden inputs
$code = $step->value('#signup_code');
$code = SignupCode::find($code);
$step->type('#signup_short_code', $code->short_code)
->click('[type=submit]');
});
// Here we expect 4 text inputs, Back and Continue buttons
$browser->whenAvailable('@step3', function ($step) {
$step->assertVisible('#signup_login')
->assertVisible('#signup_password')
->assertVisible('#signup_confirm')
->assertVisible('input#signup_domain')
->assertVisible('[type=button]')
->assertVisible('[type=submit]')
->assertFocused('#signup_login')
->assertValue('input#signup_domain', '')
->assertValue('#signup_login', '')
->assertValue('#signup_password', '')
->assertValue('#signup_confirm', '');
});
// Submit invalid login and password data
$browser->with('@step3', function ($step) use ($browser) {
$step->assertFocused('#signup_login')
->type('#signup_login', '*')
->type('#signup_domain', 'test.com')
->type('#signup_password', '12345678')
->type('#signup_confirm', '123456789')
->click('[type=submit]')
->waitFor('#signup_login.is-invalid')
->assertVisible('#signup_domain + .invalid-feedback')
->assertVisible('#signup_password.is-invalid')
->assertVisible('#signup_password + .invalid-feedback')
->assertFocused('#signup_login')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid domain
$browser->with('@step3', function ($step) use ($browser) {
$step->type('#signup_login', 'admin')
->type('#signup_domain', 'aaa')
->type('#signup_password', '12345678')
->type('#signup_confirm', '12345678')
->click('[type=submit]')
->waitUntilMissing('#signup_login.is-invalid')
->waitFor('#signup_domain.is-invalid + .invalid-feedback')
->assertMissing('#signup_password.is-invalid')
->assertMissing('#signup_password + .invalid-feedback')
->assertFocused('#signup_domain')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// Submit invalid domain
$browser->with('@step3', function ($step) {
$step->type('#signup_domain', 'user-domain-signup.com')
->click('[type=submit]');
});
// At this point we should be auto-logged-in to dashboard
$browser->waitUntilMissing('@step3')
->waitUntilMissing('.app-loader')
->on(new Dashboard())
->assertUser('admin@user-domain-signup.com');
- $browser->click('a.link-logout');
+ $browser->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
});
}
/**
* Test signup with voucher
*/
public function testSignupVoucherLink(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/signup/voucher/TEST')
->onWithoutAssert(new Signup())
->waitFor('@step0')
->click('.plan-individual button')
->whenAvailable('@step1', function (Browser $browser) {
$browser->type('#signup_first_name', 'Test')
->type('#signup_last_name', 'User')
->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
->click('[type=submit]');
})
->whenAvailable('@step2', function (Browser $browser) {
// Get the code and short_code from database
// FIXME: Find a nice way to read javascript data without using hidden inputs
$code = $browser->value('#signup_code');
$this->assertNotEmpty($code);
$code = SignupCode::find($code);
$browser->type('#signup_short_code', $code->short_code)
->click('[type=submit]');
})
->whenAvailable('@step3', function (Browser $browser) {
// Assert that the code is filled in the input
// Change it and test error handling
$browser->assertValue('#signup_voucher', 'TEST')
->type('#signup_voucher', 'TESTXX')
->type('#signup_login', 'signuptestdusk')
->type('#signup_password', '123456789')
->type('#signup_confirm', '123456789')
->click('[type=submit]')
->waitFor('#signup_voucher.is-invalid')
->assertVisible('#signup_voucher + .invalid-feedback')
->assertFocused('#signup_voucher')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
// Submit the correct code
->type('#signup_voucher', 'TEST')
->click('[type=submit]');
})
->waitUntilMissing('@step3')
->waitUntilMissing('.app-loader')
->on(new Dashboard())
->assertUser('signuptestdusk@' . \config('app.domain'))
// Logout the user
- ->click('a.link-logout');
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
});
$user = $this->getTestUser('signuptestdusk@' . \config('app.domain'));
$discount = Discount::where('code', 'TEST')->first();
$this->assertSame($discount->id, $user->wallets()->first()->discount_id);
}
}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
index 705a1d4a..361d6d2f 100644
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -1,560 +1,560 @@
'John',
'last_name' => 'Doe',
'organization' => 'Kolab Developers',
];
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('julia.roberts@kolab.org');
$john = User::where('email', 'john@kolab.org')->first();
$john->setSettings($this->profile);
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
Entitlement::where('entitleable_id', $john->id)->whereIn('cost', [25, 100])->delete();
$wallet = $john->wallets()->first();
$wallet->discount()->dissociate();
$wallet->save();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser('julia.roberts@kolab.org');
$john = User::where('email', 'john@kolab.org')->first();
$john->setSettings($this->profile);
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
Entitlement::where('entitleable_id', $john->id)->whereIn('cost', [25, 100])->delete();
$wallet = $john->wallets()->first();
$wallet->discount()->dissociate();
$wallet->save();
parent::tearDown();
}
/**
* Test user info page (unauthenticated)
*/
public function testInfoUnauth(): void
{
// Test that the page requires authentication
$this->browse(function (Browser $browser) {
$user = User::where('email', 'john@kolab.org')->first();
$browser->visit('/user/' . $user->id)->on(new Home());
});
}
/**
* Test users list page (unauthenticated)
*/
public function testListUnauth(): void
{
// Test that the page requires authentication
$this->browse(function (Browser $browser) {
$browser->visit('/users')->on(new Home());
});
}
/**
* Test users list page
*/
public function testList(): void
{
// Test that the page requires authentication
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->on(new Dashboard())
->assertSeeIn('@links .link-users', 'User accounts')
->click('@links .link-users')
->on(new UserList())
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 4)
->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org')
->assertVisible('tbody tr:nth-child(1) button.button-delete')
->assertVisible('tbody tr:nth-child(2) button.button-delete')
->assertVisible('tbody tr:nth-child(3) button.button-delete')
->assertVisible('tbody tr:nth-child(4) button.button-delete');
});
});
}
/**
* Test user account editing page (not profile page)
*
* @depends testList
*/
public function testInfo(): void
{
$this->browse(function (Browser $browser) {
$browser->on(new UserList())
->click('@table tr:nth-child(3) a')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
->with('@form', function (Browser $browser) {
// Assert form content
$browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
->assertSeeIn('div.row:nth-child(1) #status', 'Active')
->assertFocused('div.row:nth-child(2) input')
->assertSeeIn('div.row:nth-child(2) label', 'First name')
->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name'])
->assertSeeIn('div.row:nth-child(3) label', 'Last name')
->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name'])
->assertSeeIn('div.row:nth-child(4) label', 'Organization')
->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['organization'])
->assertSeeIn('div.row:nth-child(5) label', 'Email')
->assertValue('div.row:nth-child(5) input[type=text]', 'john@kolab.org')
->assertDisabled('div.row:nth-child(5) input[type=text]')
->assertSeeIn('div.row:nth-child(6) label', 'Email aliases')
->assertVisible('div.row:nth-child(6) .list-input')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertListInputValue(['john.doe@kolab.org'])
->assertValue('@input', '');
})
->assertSeeIn('div.row:nth-child(7) label', 'Password')
->assertValue('div.row:nth-child(7) input[type=password]', '')
->assertSeeIn('div.row:nth-child(8) label', 'Confirm password')
->assertValue('div.row:nth-child(8) input[type=password]', '')
->assertSeeIn('button[type=submit]', 'Submit');
// Clear some fields and submit
$browser->type('#first_name', '')
->type('#last_name', '')
->click('button[type=submit]');
})
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
// Test error handling (password)
$browser->with('@form', function (Browser $browser) {
$browser->type('#password', 'aaaaaa')
->type('#password_confirmation', '')
->click('button[type=submit]')
->waitFor('#password + .invalid-feedback')
->assertSeeIn('#password + .invalid-feedback', 'The password confirmation does not match.')
->assertFocused('#password')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
});
// TODO: Test password change
// Test form error handling (aliases)
$browser->with('@form', function (Browser $browser) {
// TODO: For some reason, clearing the input value
// with ->type('#password', '') does not work, maybe some dusk/vue intricacy
// For now we just use the default password
$browser->type('#password', 'simple123')
->type('#password_confirmation', 'simple123')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
->click('button[type=submit]')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
})
->with('@form', function (Browser $browser) {
$browser->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertFormError(2, 'The specified alias is invalid.', false);
});
});
// Test adding aliases
$browser->with('@form', function (Browser $browser) {
$browser->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->removeListEntry(2)
->addListEntry('john.test@kolab.org');
})
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
});
$john = User::where('email', 'john@kolab.org')->first();
$alias = UserAlias::where('user_id', $john->id)->where('alias', 'john.test@kolab.org')->first();
$this->assertTrue(!empty($alias));
// Test subscriptions
$browser->with('@form', function (Browser $browser) {
$browser->assertSeeIn('div.row:nth-child(9) label', 'Subscriptions')
->assertVisible('@skus.row:nth-child(9)')
->with('@skus', function ($browser) {
$browser->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.name', 'User Mailbox')
->assertSeeIn('tbody tr:nth-child(1) td.price', '4,44 CHF/month')
->assertChecked('tbody tr:nth-child(1) td.selection input')
->assertDisabled('tbody tr:nth-child(1) td.selection input')
->assertTip(
'tbody tr:nth-child(1) td.buttons button',
'Just a mailbox'
)
// Storage SKU
->assertSeeIn('tbody tr:nth-child(2) td.name', 'Storage Quota')
->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month')
->assertChecked('tbody tr:nth-child(2) td.selection input')
->assertDisabled('tbody tr:nth-child(2) td.selection input')
->assertTip(
'tbody tr:nth-child(2) td.buttons button',
'Some wiggle room'
)
->with(new QuotaInput('tbody tr:nth-child(2) .range-input'), function ($browser) {
$browser->assertQuotaValue(2)->setQuotaValue(3);
})
->assertSeeIn('tr:nth-child(2) td.price', '0,25 CHF/month')
// groupware SKU
->assertSeeIn('tbody tr:nth-child(3) td.name', 'Groupware Features')
->assertSeeIn('tbody tr:nth-child(3) td.price', '5,55 CHF/month')
->assertChecked('tbody tr:nth-child(3) td.selection input')
->assertEnabled('tbody tr:nth-child(3) td.selection input')
->assertTip(
'tbody tr:nth-child(3) td.buttons button',
'Groupware functions like Calendar, Tasks, Notes, etc.'
)
// ActiveSync SKU
->assertSeeIn('tbody tr:nth-child(4) td.name', 'Activesync')
->assertSeeIn('tbody tr:nth-child(4) td.price', '1,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(4) td.selection input')
->assertEnabled('tbody tr:nth-child(4) td.selection input')
->assertTip(
'tbody tr:nth-child(4) td.buttons button',
'Mobile synchronization'
)
// 2FA SKU
->assertSeeIn('tbody tr:nth-child(5) td.name', '2-Factor Authentication')
->assertSeeIn('tbody tr:nth-child(5) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(5) td.selection input')
->assertEnabled('tbody tr:nth-child(5) td.selection input')
->assertTip(
'tbody tr:nth-child(5) td.buttons button',
'Two factor authentication for webmail and administration panel'
)
->click('tbody tr:nth-child(4) td.selection input');
})
->assertMissing('@skus table + .hint')
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
});
$expected = ['activesync', 'groupware', 'mailbox', 'storage', 'storage', 'storage'];
$this->assertUserEntitlements($john, $expected);
// Test subscriptions interaction
$browser->with('@form', function (Browser $browser) {
$browser->with('@skus', function ($browser) {
// Uncheck 'groupware', expect activesync unchecked
- $browser->click('@sku-input-groupware')
- ->assertNotChecked('@sku-input-groupware')
- ->assertNotChecked('@sku-input-activesync')
- ->assertEnabled('@sku-input-activesync')
- ->assertNotReadonly('@sku-input-activesync')
+ $browser->click('#sku-input-groupware')
+ ->assertNotChecked('#sku-input-groupware')
+ ->assertNotChecked('#sku-input-activesync')
+ ->assertEnabled('#sku-input-activesync')
+ ->assertNotReadonly('#sku-input-activesync')
// Check 'activesync', expect an alert
- ->click('@sku-input-activesync')
+ ->click('#sku-input-activesync')
->assertDialogOpened('Activesync requires Groupware Features.')
->acceptDialog()
- ->assertNotChecked('@sku-input-activesync')
+ ->assertNotChecked('#sku-input-activesync')
// Check '2FA', expect 'activesync' unchecked and readonly
- ->click('@sku-input-2fa')
- ->assertChecked('@sku-input-2fa')
- ->assertNotChecked('@sku-input-activesync')
- ->assertReadonly('@sku-input-activesync')
+ ->click('#sku-input-2fa')
+ ->assertChecked('#sku-input-2fa')
+ ->assertNotChecked('#sku-input-activesync')
+ ->assertReadonly('#sku-input-activesync')
// Uncheck '2FA'
- ->click('@sku-input-2fa')
- ->assertNotChecked('@sku-input-2fa')
- ->assertNotReadonly('@sku-input-activesync');
+ ->click('#sku-input-2fa')
+ ->assertNotChecked('#sku-input-2fa')
+ ->assertNotReadonly('#sku-input-activesync');
});
});
});
}
/**
* Test user adding page
*
* @depends testList
*/
public function testNewUser(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->assertSeeIn('button.create-user', 'Create user')
->click('button.create-user')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'New user account')
->with('@form', function (Browser $browser) {
// Assert form content
$browser->assertFocused('div.row:nth-child(1) input')
->assertSeeIn('div.row:nth-child(1) label', 'First name')
->assertValue('div.row:nth-child(1) input[type=text]', '')
->assertSeeIn('div.row:nth-child(2) label', 'Last name')
->assertValue('div.row:nth-child(2) input[type=text]', '')
->assertSeeIn('div.row:nth-child(3) label', 'Organization')
->assertValue('div.row:nth-child(3) input[type=text]', '')
->assertSeeIn('div.row:nth-child(4) label', 'Email')
->assertValue('div.row:nth-child(4) input[type=text]', '')
->assertEnabled('div.row:nth-child(4) input[type=text]')
->assertSeeIn('div.row:nth-child(5) label', 'Email aliases')
->assertVisible('div.row:nth-child(5) .list-input')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertListInputValue([])
->assertValue('@input', '');
})
->assertSeeIn('div.row:nth-child(6) label', 'Password')
->assertValue('div.row:nth-child(6) input[type=password]', '')
->assertSeeIn('div.row:nth-child(7) label', 'Confirm password')
->assertValue('div.row:nth-child(7) input[type=password]', '')
->assertSeeIn('div.row:nth-child(8) label', 'Package')
// assert packages list widget, select "Lite Account"
->with('@packages', function ($browser) {
$browser->assertElementsCount('tbody tr', 2)
->assertSeeIn('tbody tr:nth-child(1)', 'Groupware Account')
->assertSeeIn('tbody tr:nth-child(2)', 'Lite Account')
->assertSeeIn('tbody tr:nth-child(1) .price', '9,99 CHF/month')
->assertSeeIn('tbody tr:nth-child(2) .price', '4,44 CHF/month')
->assertChecked('tbody tr:nth-child(1) input')
->click('tbody tr:nth-child(2) input')
->assertNotChecked('tbody tr:nth-child(1) input')
->assertChecked('tbody tr:nth-child(2) input');
})
->assertMissing('@packages table + .hint')
->assertSeeIn('button[type=submit]', 'Submit');
// Test browser-side required fields and error handling
$browser->click('button[type=submit]')
->assertFocused('#email')
->type('#email', 'invalid email')
->click('button[type=submit]')
->assertFocused('#password')
->type('#password', 'simple123')
->click('button[type=submit]')
->assertFocused('#password_confirmation')
->type('#password_confirmation', 'simple')
->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.');
});
// Test form error handling (aliases)
$browser->with('@form', function (Browser $browser) {
$browser->type('#email', 'julia.roberts@kolab.org')
->type('#password_confirmation', 'simple123')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
->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
$browser->with('@form', function (Browser $browser) {
$browser->type('#first_name', 'Julia')
->type('#last_name', 'Roberts')
->type('#organization', 'Test Org')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->removeListEntry(1)
->addListEntry('julia.roberts2@kolab.org');
})
->click('button[type=submit]');
})
->assertToast(Toast::TYPE_SUCCESS, 'User created successfully.')
// check redirection to users list
->waitForLocation('/users')
->on(new UserList())
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 5)
->assertSeeIn('tbody tr:nth-child(4) a', 'julia.roberts@kolab.org');
});
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
$alias = UserAlias::where('user_id', $julia->id)->where('alias', 'julia.roberts2@kolab.org')->first();
$this->assertTrue(!empty($alias));
$this->assertUserEntitlements($julia, ['mailbox', 'storage', 'storage']);
$this->assertSame('Julia', $julia->getSetting('first_name'));
$this->assertSame('Roberts', $julia->getSetting('last_name'));
$this->assertSame('Test Org', $julia->getSetting('organization'));
});
}
/**
* Test user delete
*
* @depends testNewUser
*/
public function testDeleteUser(): void
{
// First create a new user
$john = $this->getTestUser('john@kolab.org');
$julia = $this->getTestUser('julia.roberts@kolab.org');
$package_kolab = \App\Package::where('title', 'kolab')->first();
$john->assignPackage($package_kolab, $julia);
// Test deleting non-controller user
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 5)
->assertSeeIn('tbody tr:nth-child(4) a', 'julia.roberts@kolab.org')
->click('tbody tr:nth-child(4) button.button-delete');
})
->with(new Dialog('#delete-warning'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Delete julia.roberts@kolab.org')
->assertFocused('@button-cancel')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Delete')
->click('@button-cancel');
})
->whenAvailable('@table', function (Browser $browser) {
$browser->click('tbody tr:nth-child(4) button.button-delete');
})
->with(new Dialog('#delete-warning'), function (Browser $browser) {
$browser->click('@button-action');
})
->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')
->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org');
});
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
$this->assertTrue(empty($julia));
// Test clicking Delete on the controller record redirects to /profile/delete
$browser
->with('@table', function (Browser $browser) {
$browser->click('tbody tr:nth-child(3) button.button-delete');
})
->waitForLocation('/profile/delete');
});
// Test that non-controller user cannot see/delete himself on the users list
// Note: Access to /profile/delete page is tested in UserProfileTest.php
$this->browse(function (Browser $browser) {
$browser->visit('/logout')
->on(new Home())
->submitLogon('jack@kolab.org', 'simple123', true)
->visit(new UserList())
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 0);
});
});
// Test that controller user (Ned) can see/delete all the users ???
$this->browse(function (Browser $browser) {
$browser->visit('/logout')
->on(new Home())
->submitLogon('ned@kolab.org', 'simple123', true)
->visit(new UserList())
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 4)
->assertElementsCount('tbody button.button-delete', 4);
});
// TODO: Test the delete action in details
});
// TODO: Test what happens with the logged in user session after he's been deleted by another user
}
/**
* Test discounted sku/package prices in the UI
*/
public function testDiscountedPrices(): void
{
// Add 10% discount
$discount = Discount::where('code', 'TEST')->first();
$john = User::where('email', 'john@kolab.org')->first();
$wallet = $john->wallet();
$wallet->discount()->associate($discount);
$wallet->save();
// SKUs on user edit page
$this->browse(function (Browser $browser) {
$browser->visit('/logout')
->on(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->visit(new UserList())
->click('@table tr:nth-child(2) a')
->on(new UserInfo())
->with('@form', function (Browser $browser) {
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '3,99 CHF/month¹')
// Storage SKU
->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month¹')
->with($quota_input, function (Browser $browser) {
$browser->setQuotaValue(100);
})
->assertSeeIn('tr:nth-child(2) td.price', '21,56 CHF/month¹')
// groupware SKU
->assertSeeIn('tbody tr:nth-child(3) td.price', '4,99 CHF/month¹')
// ActiveSync SKU
->assertSeeIn('tbody tr:nth-child(4) td.price', '0,90 CHF/month¹')
// 2FA SKU
->assertSeeIn('tbody tr:nth-child(5) td.price', '0,00 CHF/month¹');
})
->assertSeeIn('@skus table + .hint', '¹ applied discount: 10% - Test voucher');
});
});
// Packages on new user page
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->click('button.create-user')
->on(new UserInfo())
->with('@form', function (Browser $browser) {
$browser->whenAvailable('@packages', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 2)
->assertSeeIn('tbody tr:nth-child(1) .price', '8,99 CHF/month¹') // Groupware
->assertSeeIn('tbody tr:nth-child(2) .price', '3,99 CHF/month¹'); // Lite
})
->assertSeeIn('@packages table + .hint', '¹ applied discount: 10% - Test voucher');
});
});
}
}