Merge remote-tracking branch 'upstream/v5-develop' into v5-569

This commit is contained in:
Benjamin Beganović 2021-07-14 13:09:39 +02:00
commit f0aa62100f
86 changed files with 2417 additions and 4787 deletions

View File

@ -14,9 +14,11 @@ namespace App\Console\Commands;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits; use App\DataMapper\FeesAndLimits;
use App\Events\Invoice\InvoiceWasCreated; use App\Events\Invoice\InvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Factory\GroupSettingFactory; use App\Factory\GroupSettingFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory; use App\Factory\InvoiceItemFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\SubscriptionFactory; use App\Factory\SubscriptionFactory;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyTaskStatuses;
@ -48,6 +50,7 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use stdClass;
class CreateSingleAccount extends Command class CreateSingleAccount extends Command
{ {
@ -225,16 +228,19 @@ class CreateSingleAccount extends Command
$client = $company->clients->random(); $client = $company->clients->random();
$this->info('creating task for client #'.$client->id); $this->info('creating task for client #' . $client->id);
$this->createTask($client); $this->createTask($client);
$client = $company->clients->random(); $client = $company->clients->random();
$this->info('creating project for client #'.$client->id); $this->info('creating project for client #' . $client->id);
$this->createProject($client); $this->createProject($client);
$this->info('creating credit for client #'.$client->id); $this->info('creating credit for client #' . $client->id);
$this->createCredit($client); $this->createCredit($client);
$this->info('creating recurring invoice for client # ' . $client->id);
$this->createRecurringInvoice($client);
} }
$this->createGateways($company, $user); $this->createGateways($company, $user);
@ -435,6 +441,10 @@ class CreateSingleAccount extends Command
$invoice = $invoice_calc->getInvoice(); $invoice = $invoice_calc->getInvoice();
if ($this->gateway === 'braintree') {
$invoice->amount = 100; // Braintree sandbox only allows payments under 2,000 to complete successfully.
}
$invoice->save(); $invoice->save();
$invoice->service()->createInvitations()->markSent(); $invoice->service()->createInvitations()->markSent();
@ -619,7 +629,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -642,7 +652,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -663,7 +673,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -684,11 +694,96 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.wepay') && ($this->gateway == 'all' || $this->gateway == 'wepay')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '8fdeed552015b3c7b44ed6c8ebd9e992';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.wepay'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.braintree') && ($this->gateway == 'all' || $this->gateway == 'braintree')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'f7ec488676d310683fb51802d076d713';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.braintree'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
$cg->save(); $cg->save();
} }
} }
private function createRecurringInvoice($client)
{
$faker = Factory::create();
$invoice = RecurringInvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
$dateable = Carbon::now()->subDays(rand(0, 90));
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$invoice->tax_name1 = 'GST';
$invoice->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$invoice->tax_name2 = 'VAT';
$invoice->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$invoice->tax_name3 = 'CA Sales Tax';
$invoice->tax_rate3 = 5;
}
$invoice->custom_value1 = $faker->date;
$invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->status_id = RecurringInvoice::STATUS_ACTIVE;
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
$invoice->save();
event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}
} }

View File

@ -32,6 +32,7 @@ class General extends Component
'first_name' => ['sometimes'], 'first_name' => ['sometimes'],
'last_name' => ['sometimes'], 'last_name' => ['sometimes'],
'email' => ['required', 'email'], 'email' => ['required', 'email'],
'phone' => ['sometimes'],
]; ];
public function mount() public function mount()

View File

@ -87,6 +87,7 @@
"fakerphp/faker": "^1.14", "fakerphp/faker": "^1.14",
"filp/whoops": "^2.7", "filp/whoops": "^2.7",
"friendsofphp/php-cs-fixer": "^2.16", "friendsofphp/php-cs-fixer": "^2.16",
"laravel/dusk": "^6.15",
"mockery/mockery": "^1.3.1", "mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^5.0", "nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.0", "phpunit/phpunit": "^9.0",

143
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "25a0cbf18fc238305c7ea640c49ba89a", "content-hash": "d2beb37ff5fbee59ad4bb792e944eb10",
"packages": [ "packages": [
{ {
"name": "asm/php-ansible", "name": "asm/php-ansible",
@ -11950,6 +11950,79 @@
}, },
"time": "2020-07-09T08:09:16+00:00" "time": "2020-07-09T08:09:16+00:00"
}, },
{
"name": "laravel/dusk",
"version": "v6.15.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
"reference": "45b55fa20321086c4f8cc4e712cbe54db644e21c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/dusk/zipball/45b55fa20321086c4f8cc4e712cbe54db644e21c",
"reference": "45b55fa20321086c4f8cc4e712cbe54db644e21c",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-zip": "*",
"illuminate/console": "^6.0|^7.0|^8.0",
"illuminate/support": "^6.0|^7.0|^8.0",
"nesbot/carbon": "^2.0",
"php": "^7.2|^8.0",
"php-webdriver/webdriver": "^1.9.0",
"symfony/console": "^4.3|^5.0",
"symfony/finder": "^4.3|^5.0",
"symfony/process": "^4.3|^5.0",
"vlucas/phpdotenv": "^3.0|^4.0|^5.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^4.16|^5.17.1|^6.12.1",
"phpunit/phpunit": "^7.5.15|^8.4|^9.0"
},
"suggest": {
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Dusk\\DuskServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Dusk\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Dusk provides simple end-to-end testing and browser automation.",
"keywords": [
"laravel",
"testing",
"webdriver"
],
"support": {
"issues": "https://github.com/laravel/dusk/issues",
"source": "https://github.com/laravel/dusk/tree/v6.15.0"
},
"time": "2021-04-06T14:14:57+00:00"
},
{ {
"name": "maximebf/debugbar", "name": "maximebf/debugbar",
"version": "v1.16.5", "version": "v1.16.5",
@ -12503,6 +12576,72 @@
}, },
"time": "2020-10-14T08:39:05+00:00" "time": "2020-10-14T08:39:05+00:00"
}, },
{
"name": "php-webdriver/webdriver",
"version": "1.11.1",
"source": {
"type": "git",
"url": "https://github.com/php-webdriver/php-webdriver.git",
"reference": "da16e39968f8dd5cfb7d07eef91dc2b731c69880"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/da16e39968f8dd5cfb7d07eef91dc2b731c69880",
"reference": "da16e39968f8dd5cfb7d07eef91dc2b731c69880",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-zip": "*",
"php": "^5.6 || ~7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.12",
"symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0"
},
"replace": {
"facebook/webdriver": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.0",
"ondram/ci-detector": "^2.1 || ^3.5 || ^4.0",
"php-coveralls/php-coveralls": "^2.4",
"php-mock/php-mock-phpunit": "^1.1 || ^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9",
"squizlabs/php_codesniffer": "^3.5",
"symfony/var-dumper": "^3.3 || ^4.0 || ^5.0"
},
"suggest": {
"ext-SimpleXML": "For Firefox profile creation"
},
"type": "library",
"autoload": {
"psr-4": {
"Facebook\\WebDriver\\": "lib/"
},
"files": [
"lib/Exception/TimeoutException.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
"homepage": "https://github.com/php-webdriver/php-webdriver",
"keywords": [
"Chromedriver",
"geckodriver",
"php",
"selenium",
"webdriver"
],
"support": {
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.11.1"
},
"time": "2021-05-21T15:12:49+00:00"
},
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "2.2.0", "version": "2.2.0",
@ -14742,5 +14881,5 @@
"platform-dev": { "platform-dev": {
"php": "^7.3|^7.4|^8.0" "php": "^7.3|^7.4|^8.0"
}, },
"plugin-api-version": "2.0.0" "plugin-api-version": "2.1.0"
} }

View File

@ -82,6 +82,8 @@ return [
'checkout' => env('CHECKOUT_KEYS', ''), 'checkout' => env('CHECKOUT_KEYS', ''),
'travis' => env('TRAVIS', false), 'travis' => env('TRAVIS', false),
'test_email' => env('TEST_EMAIL', 'test@example.com'), 'test_email' => env('TEST_EMAIL', 'test@example.com'),
'wepay' => env('WEPAY_KEYS', ''),
'braintree' => env('BRAINTREE_KEYS', ''),
], ],
'contact' => [ 'contact' => [
'email' => env('MAIL_FROM_ADDRESS'), 'email' => env('MAIL_FROM_ADDRESS'),

View File

@ -1,10 +0,0 @@
{
"video": false,
"baseUrl": "http://localhost:8080/",
"chromeWebSecurity": false,
"env": {
"runningEnvironment": "docker"
},
"viewportWidth": 1280,
"viewportHeight": 800
}

View File

@ -1,48 +0,0 @@
import { second } from '../fixtures/example.json';
describe('Checkout Credit Card Payments', () => {
beforeEach(() => {
// cy.useGateway(second);
cy.clientLogin();
});
it('should be able to complete payment using checkout credit card', () => {
cy.visit('/client/invoices');
cy.get('#unpaid-checkbox').click();
cy.get('[data-cy=pay-now')
.first()
.click();
cy.location('pathname').should('eq', '/client/invoices/payment');
cy.get('[data-cy=payment-methods-dropdown').click();
cy.get('[data-cy=payment-method')
.first()
.click();
cy.wait(8000);
cy.get('.cko-pay-now.show')
.first()
.click();
cy.wait(3000);
cy.getWithinIframe('[data-checkout="card-number"]').type(
'4242424242424242'
);
cy.getWithinIframe('[data-checkout="expiry-month"]').type('12');
cy.getWithinIframe('[data-checkout="expiry-year"]').type('30');
cy.getWithinIframe('[data-checkout="cvv"]').type('100');
cy.getWithinIframe('.form-submit')
.first()
.click();
cy.wait(5000);
cy.url().should('contain', '/client/payments');
});
});

View File

@ -1,10 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes",
"first": "VolejRejNm",
"second": "Wpmbk5ezJn",
"url": "http://localhost:8000"
}

View File

@ -1,37 +0,0 @@
describe('Credits', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show credits page', () => {
cy.visit('/client/credits');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/credits');
});
});
it('should show credits text', () => {
cy.visit('/client/credits');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Credits');
});
/* it('should have required table elements', () => {
cy.visit('/client/credits');
cy.get('body')
.find('table.credits-table > tbody > tr')
.first()
.find('a')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/credits/VolejRejNm');
});
});*/
});

View File

@ -1,73 +0,0 @@
context('Invoices', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show invoices page', () => {
cy.visit('/client/invoices');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/invoices');
});
});
it('should show invoices text', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Invoices');
});
it('should show download and pay now buttons', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('button[value="download"]')
.first()
.should('contain.text', 'Download');
cy.get('body')
.find('button[value="payment"]')
.first()
.should('contain.text', 'Pay Now');
});
it('should have per page options dropdown', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
it('should have required table elements', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('table.invoices-table > tbody > tr')
.first()
.find('.button-link')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/invoices/VolejRejNm');
});
});
it('should filter table content', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('#paid-checkbox')
.check();
cy.get('body')
.find('table.invoices-table > tbody > tr')
.first()
.should('not.contain', 'Overdue');
});
});

View File

@ -1,48 +0,0 @@
context('Login', () => {
beforeEach(() => {
cy.visit('/client/login');
});
it('should type into login form elements', () => {
cy.get('#test_email')
.invoke('val')
.then(emailValue => {
cy.get('#email')
.type(emailValue)
.should('have.value', emailValue);
});
cy.get('#test_password')
.invoke('val')
.then(passwordValue => {
cy.get('#password')
.type(passwordValue)
.should('have.value', passwordValue);
});
});
it('should login into client portal', () => {
cy.get('#test_email')
.invoke('val')
.then(emailValue => {
cy.get('#test_password')
.invoke('val')
.then(passwordValue => {
cy.get('#email')
.type(emailValue)
.should('have.value', emailValue);
cy.get('#password')
.type(passwordValue)
.should('have.value', passwordValue);
cy.get('#loginBtn')
.contains('Login')
.click();
cy.location().should(location => {
expect(location.pathname).to.eq(
'/client/invoices'
);
});
});
});
});
});

View File

@ -1,30 +0,0 @@
context('Payment methods', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show payment methods page', () => {
cy.visit('/client/payment_methods');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/payment_methods');
});
});
it('should show payment methods text', () => {
cy.visit('/client/payment_methods');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Payment Method');
});
it('should have per page options dropdown', () => {
cy.visit('/client/payment_methods');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
});

View File

@ -1,30 +0,0 @@
context('Payments', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show payments page', () => {
cy.visit('/client/payments');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/payments');
});
});
it('should show payments text', () => {
cy.visit('/client/payments');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Payments');
});
it('should have per page options dropdown', () => {
cy.visit('/client/payments');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
});

View File

@ -1,73 +0,0 @@
describe('Quotes', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show quotes page', () => {
cy.visit('/client/quotes');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/quotes');
});
});
it('should show quotes text', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Quotes');
});
it('should show download and approve buttons', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('button[value="download"]')
.first()
.should('contain.text', 'Download');
cy.get('body')
.find('button[value="approve"]')
.first()
.should('contain.text', 'Approve');
});
it('should have per page options dropdown', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
it('should have required table elements', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('table.quotes-table > tbody > tr')
.first()
.find('.button-link')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/quotes/VolejRejNm');
});
});
it('should filter table content', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('#draft-checkbox')
.check();
cy.get('body')
.find('table.quotes-table > tbody > tr')
.first()
.should('not.contain', 'Sent');
});
});

View File

@ -1,31 +0,0 @@
context('Recurring invoices', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show recurring invoices page', () => {
cy.visit('/client/recurring_invoices');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/recurring_invoices');
});
});
it('should show reucrring invoices text', () => {
cy.visit('/client/recurring_invoices');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Recurring Invoices');
});
it('should have per page options dropdown', () => {
cy.visit('/client/recurring_invoices');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
});

View File

@ -1,298 +0,0 @@
/// <reference types="cypress" />
context('Actions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions')
})
// https://on.cypress.io/interacting-with-elements
it('.type() - type into a DOM element', () => {
// https://on.cypress.io/type
cy.get('.action-email')
.type('fake@email.com').should('have.value', 'fake@email.com')
// .type() with special character sequences
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
.type('{del}{selectall}{backspace}')
// .type() with key modifiers
.type('{alt}{option}') //these are equivalent
.type('{ctrl}{control}') //these are equivalent
.type('{meta}{command}{cmd}') //these are equivalent
.type('{shift}')
// Delay each keypress by 0.1 sec
.type('slow.typing@email.com', { delay: 100 })
.should('have.value', 'slow.typing@email.com')
cy.get('.action-disabled')
// Ignore error checking prior to type
// like whether the input is visible or disabled
.type('disabled error checking', { force: true })
.should('have.value', 'disabled error checking')
})
it('.focus() - focus on a DOM element', () => {
// https://on.cypress.io/focus
cy.get('.action-focus').focus()
.should('have.class', 'focus')
.prev().should('have.attr', 'style', 'color: orange;')
})
it('.blur() - blur off a DOM element', () => {
// https://on.cypress.io/blur
cy.get('.action-blur').type('About to blur').blur()
.should('have.class', 'error')
.prev().should('have.attr', 'style', 'color: red;')
})
it('.clear() - clears an input or textarea element', () => {
// https://on.cypress.io/clear
cy.get('.action-clear').type('Clear this text')
.should('have.value', 'Clear this text')
.clear()
.should('have.value', '')
})
it('.submit() - submit a form', () => {
// https://on.cypress.io/submit
cy.get('.action-form')
.find('[type="text"]').type('HALFOFF')
cy.get('.action-form').submit()
.next().should('contain', 'Your form has been submitted!')
})
it('.click() - click on a DOM element', () => {
// https://on.cypress.io/click
cy.get('.action-btn').click()
// You can click on 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// clicking in the center of the element is the default
cy.get('#action-canvas').click()
cy.get('#action-canvas').click('topLeft')
cy.get('#action-canvas').click('top')
cy.get('#action-canvas').click('topRight')
cy.get('#action-canvas').click('left')
cy.get('#action-canvas').click('right')
cy.get('#action-canvas').click('bottomLeft')
cy.get('#action-canvas').click('bottom')
cy.get('#action-canvas').click('bottomRight')
// .click() accepts an x and y coordinate
// that controls where the click occurs :)
cy.get('#action-canvas')
.click(80, 75) // click 80px on x coord and 75px on y coord
.click(170, 75)
.click(80, 165)
.click(100, 185)
.click(125, 190)
.click(150, 185)
.click(170, 165)
// click multiple elements by passing multiple: true
cy.get('.action-labels>.label').click({ multiple: true })
// Ignore error checking prior to clicking
cy.get('.action-opacity>.btn').click({ force: true })
})
it('.dblclick() - double click on a DOM element', () => {
// https://on.cypress.io/dblclick
// Our app has a listener on 'dblclick' event in our 'scripts.js'
// that hides the div and shows an input on double click
cy.get('.action-div').dblclick().should('not.be.visible')
cy.get('.action-input-hidden').should('be.visible')
})
it('.rightclick() - right click on a DOM element', () => {
// https://on.cypress.io/rightclick
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
// that hides the div and shows an input on right click
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
cy.get('.rightclick-action-input-hidden').should('be.visible')
})
it('.check() - check a checkbox or radio element', () => {
// https://on.cypress.io/check
// By default, .check() will check all
// matching checkbox or radio elements in succession, one after another
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
.check().should('be.checked')
cy.get('.action-radios [type="radio"]').not('[disabled]')
.check().should('be.checked')
// .check() accepts a value argument
cy.get('.action-radios [type="radio"]')
.check('radio1').should('be.checked')
// .check() accepts an array of values
cy.get('.action-multiple-checkboxes [type="checkbox"]')
.check(['checkbox1', 'checkbox2']).should('be.checked')
// Ignore error checking prior to checking
cy.get('.action-checkboxes [disabled]')
.check({ force: true }).should('be.checked')
cy.get('.action-radios [type="radio"]')
.check('radio3', { force: true }).should('be.checked')
})
it('.uncheck() - uncheck a checkbox element', () => {
// https://on.cypress.io/uncheck
// By default, .uncheck() will uncheck all matching
// checkbox elements in succession, one after another
cy.get('.action-check [type="checkbox"]')
.not('[disabled]')
.uncheck().should('not.be.checked')
// .uncheck() accepts a value argument
cy.get('.action-check [type="checkbox"]')
.check('checkbox1')
.uncheck('checkbox1').should('not.be.checked')
// .uncheck() accepts an array of values
cy.get('.action-check [type="checkbox"]')
.check(['checkbox1', 'checkbox3'])
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
// Ignore error checking prior to unchecking
cy.get('.action-check [disabled]')
.uncheck({ force: true }).should('not.be.checked')
})
it('.select() - select an option in a <select> element', () => {
// https://on.cypress.io/select
// at first, no option should be selected
cy.get('.action-select')
.should('have.value', '--Select a fruit--')
// Select option(s) with matching text content
cy.get('.action-select').select('apples')
// confirm the apples were selected
// note that each value starts with "fr-" in our HTML
cy.get('.action-select').should('have.value', 'fr-apples')
cy.get('.action-select-multiple')
.select(['apples', 'oranges', 'bananas'])
// when getting multiple values, invoke "val" method first
.invoke('val')
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
// Select option(s) with matching value
cy.get('.action-select').select('fr-bananas')
// can attach an assertion right away to the element
.should('have.value', 'fr-bananas')
cy.get('.action-select-multiple')
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
.invoke('val')
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
// assert the selected values include oranges
cy.get('.action-select-multiple')
.invoke('val').should('include', 'fr-oranges')
})
it('.scrollIntoView() - scroll an element into view', () => {
// https://on.cypress.io/scrollintoview
// normally all of these buttons are hidden,
// because they're not within
// the viewable area of their parent
// (we need to scroll to see them)
cy.get('#scroll-horizontal button')
.should('not.be.visible')
// scroll the button into view, as if the user had scrolled
cy.get('#scroll-horizontal button').scrollIntoView()
.should('be.visible')
cy.get('#scroll-vertical button')
.should('not.be.visible')
// Cypress handles the scroll direction needed
cy.get('#scroll-vertical button').scrollIntoView()
.should('be.visible')
cy.get('#scroll-both button')
.should('not.be.visible')
// Cypress knows to scroll to the right and down
cy.get('#scroll-both button').scrollIntoView()
.should('be.visible')
})
it('.trigger() - trigger an event on a DOM element', () => {
// https://on.cypress.io/trigger
// To interact with a range input (slider)
// we need to set its value & trigger the
// event to signal it changed
// Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event
cy.get('.trigger-input-range')
.invoke('val', 25)
.trigger('change')
.get('input[type=range]').siblings('p')
.should('have.text', '25')
})
it('cy.scrollTo() - scroll the window or element to a position', () => {
// https://on.cypress.io/scrollTo
// You can scroll to 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// if you chain .scrollTo() off of cy, we will
// scroll the entire window
cy.scrollTo('bottom')
cy.get('#scrollable-horizontal').scrollTo('right')
// or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels
cy.get('#scrollable-vertical').scrollTo(250, 250)
// or you can scroll to a specific percentage
// of the (width, height) of the element
cy.get('#scrollable-both').scrollTo('75%', '25%')
// control the easing of the scroll (default is 'swing')
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
// control the duration of the scroll (in ms)
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
})
})

View File

@ -1,42 +0,0 @@
/// <reference types="cypress" />
context('Aliasing', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/aliasing')
})
it('.as() - alias a DOM element for later use', () => {
// https://on.cypress.io/as
// Alias a DOM element for use later
// We don't have to traverse to the element
// later in our code, we reference it with @
cy.get('.as-table').find('tbody>tr')
.first().find('td').first()
.find('button').as('firstBtn')
// when we reference the alias, we place an
// @ in front of its name
cy.get('@firstBtn').click()
cy.get('@firstBtn')
.should('have.class', 'btn-success')
.and('contain', 'Changed')
})
it('.as() - alias a route for later use', () => {
// Alias the route to wait for its response
cy.server()
cy.route('GET', 'comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// https://on.cypress.io/wait
cy.wait('@getComment').its('status').should('eq', 200)
})
})

View File

@ -1,168 +0,0 @@
/// <reference types="cypress" />
context('Assertions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions')
})
describe('Implicit Assertions', () => {
it('.should() - make an assertion about the current subject', () => {
// https://on.cypress.io/should
cy.get('.assertion-table')
.find('tbody tr:last')
.should('have.class', 'success')
.find('td')
.first()
// checking the text of the <td> element in various ways
.should('have.text', 'Column content')
.should('contain', 'Column content')
.should('have.html', 'Column content')
// chai-jquery uses "is()" to check if element matches selector
.should('match', 'td')
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke('text')
.should('match', /column content/i)
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get('.assertion-table')
.find('tbody tr:last')
// finds first <td> element with text content matching regular expression
.contains('td', /column content/i)
.should('be.visible')
// for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents
})
it('.and() - chain multiple assertions together', () => {
// https://on.cypress.io/and
cy.get('.assertions-link')
.should('have.class', 'active')
.and('have.attr', 'href')
.and('include', 'cypress.io')
})
})
describe('Explicit Assertions', () => {
// https://on.cypress.io/assertions
it('expect - make an assertion about a specified subject', () => {
// We can use Chai's BDD style assertions
expect(true).to.be.true
const o = { foo: 'bar' }
expect(o).to.equal(o)
expect(o).to.deep.equal({ foo: 'bar' })
// matching text using regular expression
expect('FooBar').to.match(/bar$/i)
})
it('pass your own callback function to should()', () => {
// Pass a function to should that can have any number
// of explicit assertions within it.
// The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out.
cy.get('.assertions-p')
.find('p')
.should(($p) => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text())
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get()
// array should have length of 3
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
// use second argument to expect(...) to provide clear
// message with each assertion
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
'Some text from first p',
'More text from second p',
'And even more text from third p',
])
})
})
it('finds element by class name regex', () => {
cy.get('.docs-header')
.find('div')
// .should(cb) callback function will be retried
.should(($div) => {
expect($div).to.have.length(1)
const className = $div[0].className
expect(className).to.match(/heading-/)
})
// .then(cb) callback is not retried,
// it either passes or fails
.then(($div) => {
expect($div, 'text content').to.have.text('Introduction')
})
})
it('can throw any error', () => {
cy.get('.docs-header')
.find('div')
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error('Did not find 1 element')
}
const className = $div[0].className
if (!className.match(/heading-/)) {
throw new Error(`Could not find class "heading-" in ${className}`)
}
})
})
it('matches unknown text between two elements', () => {
/**
* Text from the first element.
* @type {string}
*/
let text
/**
* Normalizes passed text,
* useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
cy.get('.two-elements')
.find('.first')
.then(($first) => {
// save text from the first element
text = normalizeText($first.text())
})
cy.get('.two-elements')
.find('.second')
.should(($div) => {
// we can massage text before comparing
const secondText = normalizeText($div.text())
expect(secondText, 'second text').to.equal(text)
})
})
it('assert - assert shape of an object', () => {
const person = {
name: 'Joe',
age: 20,
}
assert.isObject(person, 'value is object')
})
})
})

View File

@ -1,97 +0,0 @@
/// <reference types="cypress" />
context('Connectors', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/connectors')
})
it('.each() - iterate over an array of elements', () => {
// https://on.cypress.io/each
cy.get('.connectors-each-ul>li')
.each(($el, index, $list) => {
console.log($el, index, $list)
})
})
it('.its() - get properties on the current subject', () => {
// https://on.cypress.io/its
cy.get('.connectors-its-ul>li')
// calls the 'length' property yielding that value
.its('length')
.should('be.gt', 2)
})
it('.invoke() - invoke a function on the current subject', () => {
// our div is hidden in our script.js
// $('.connectors-div').hide()
// https://on.cypress.io/invoke
cy.get('.connectors-div').should('be.hidden')
// call the jquery method 'show' on the 'div.container'
.invoke('show')
.should('be.visible')
})
it('.spread() - spread an array as individual args to callback function', () => {
// https://on.cypress.io/spread
const arr = ['foo', 'bar', 'baz']
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo')
expect(bar).to.eq('bar')
expect(baz).to.eq('baz')
})
})
describe('.then()', () => {
it('invokes a callback function with the current subject', () => {
// https://on.cypress.io/then
cy.get('.connectors-list > li')
.then(($lis) => {
expect($lis, '3 items').to.have.length(3)
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
})
})
it('yields the returned value to the next command', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
return 2
})
.then((num) => {
expect(num).to.equal(2)
})
})
it('yields the original subject without return', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
// note that nothing is returned from this callback
})
.then((num) => {
// this callback receives the original unchanged value 1
expect(num).to.equal(1)
})
})
it('yields the value yielded by the last Cypress command inside', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
// note how we run a Cypress command
// the result yielded by this Cypress command
// will be passed to the second ".then"
cy.wrap(2)
})
.then((num) => {
// this callback receives the value yielded by "cy.wrap(2)"
expect(num).to.equal(2)
})
})
})
})

View File

@ -1,78 +0,0 @@
/// <reference types="cypress" />
context('Cookies', () => {
beforeEach(() => {
Cypress.Cookies.debug(true)
cy.visit('https://example.cypress.io/commands/cookies')
// clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare
cy.clearCookies()
})
it('cy.getCookie() - get a browser cookie', () => {
// https://on.cypress.io/getcookie
cy.get('#getCookie .set-a-cookie').click()
// cy.getCookie() yields a cookie object
cy.getCookie('token').should('have.property', 'value', '123ABC')
})
it('cy.getCookies() - get browser cookies', () => {
// https://on.cypress.io/getcookies
cy.getCookies().should('be.empty')
cy.get('#getCookies .set-a-cookie').click()
// cy.getCookies() yields an array of cookies
cy.getCookies().should('have.length', 1).should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token')
expect(cookies[0]).to.have.property('value', '123ABC')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
})
})
it('cy.setCookie() - set a browser cookie', () => {
// https://on.cypress.io/setcookie
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
// cy.getCookie() yields a cookie object
cy.getCookie('foo').should('have.property', 'value', 'bar')
})
it('cy.clearCookie() - clear a browser cookie', () => {
// https://on.cypress.io/clearcookie
cy.getCookie('token').should('be.null')
cy.get('#clearCookie .set-a-cookie').click()
cy.getCookie('token').should('have.property', 'value', '123ABC')
// cy.clearCookies() yields null
cy.clearCookie('token').should('be.null')
cy.getCookie('token').should('be.null')
})
it('cy.clearCookies() - clear browser cookies', () => {
// https://on.cypress.io/clearcookies
cy.getCookies().should('be.empty')
cy.get('#clearCookies .set-a-cookie').click()
cy.getCookies().should('have.length', 1)
// cy.clearCookies() yields null
cy.clearCookies()
cy.getCookies().should('be.empty')
})
})

View File

@ -1,222 +0,0 @@
/// <reference types="cypress" />
context('Cypress.Commands', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/custom-commands
it('.add() - create a custom command', () => {
Cypress.Commands.add('console', {
prevSubject: true,
}, (subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || 'log'
// log the subject to the console
// @ts-ignore TS7017
console[method]('The subject is', subject)
// whatever we return becomes the new subject
// we don't want to change the subject so
// we return whatever was passed in
return subject
})
// @ts-ignore TS2339
cy.get('button').console('info').then(($button) => {
// subject is still $button
})
})
})
context('Cypress.Cookies', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/cookies
it('.debug() - enable or disable debugging', () => {
Cypress.Cookies.debug(true)
// Cypress will now log in the console when
// cookies are set or cleared
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
})
it('.preserveOnce() - preserve cookies by key', () => {
// normally cookies are reset after each test
cy.getCookie('fakeCookie').should('not.be.ok')
// preserving a cookie will not clear it when
// the next test starts
cy.setCookie('lastCookie', '789XYZ')
Cypress.Cookies.preserveOnce('lastCookie')
})
it('.defaults() - set defaults for all cookies', () => {
// now any cookie with the name 'session_id' will
// not be cleared before each new test runs
Cypress.Cookies.defaults({
whitelist: 'session_id',
})
})
})
context('Cypress.Server', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// Permanently override server options for
// all instances of cy.server()
// https://on.cypress.io/cypress-server
it('.defaults() - change default config of server', () => {
Cypress.Server.defaults({
delay: 0,
force404: false,
})
})
})
context('Cypress.arch', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get CPU architecture name of underlying OS', () => {
// https://on.cypress.io/arch
expect(Cypress.arch).to.exist
})
})
context('Cypress.config()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get and set configuration options', () => {
// https://on.cypress.io/config
let myConfig = Cypress.config()
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
expect(myConfig).to.have.property('baseUrl', null)
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
expect(myConfig).to.have.property('requestTimeout', 5000)
expect(myConfig).to.have.property('responseTimeout', 30000)
expect(myConfig).to.have.property('viewportHeight', 660)
expect(myConfig).to.have.property('viewportWidth', 1000)
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
expect(myConfig).to.have.property('waitForAnimations', true)
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
// this will change the config for the rest of your tests!
Cypress.config('pageLoadTimeout', 20000)
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
Cypress.config('pageLoadTimeout', 60000)
})
})
context('Cypress.dom', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/dom
it('.isHidden() - determine if a DOM element is hidden', () => {
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
let visibleP = Cypress.$('.dom-p p.visible').get(0)
// our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
expect(Cypress.dom.isHidden(visibleP)).to.be.false
})
})
context('Cypress.env()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// We can set environment variables for highly dynamic values
// https://on.cypress.io/environment-variables
it('Get environment variables', () => {
// https://on.cypress.io/env
// set multiple environment variables
Cypress.env({
host: 'veronica.dev.local',
api_server: 'http://localhost:8888/v1/',
})
// get environment variable
expect(Cypress.env('host')).to.eq('veronica.dev.local')
// set environment variable
Cypress.env('api_server', 'http://localhost:8888/v2/')
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
// get all environment variable
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
})
})
context('Cypress.log', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Control what is printed to the Command Log', () => {
// https://on.cypress.io/cypress-log
})
})
context('Cypress.platform', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get underlying OS name', () => {
// https://on.cypress.io/platform
expect(Cypress.platform).to.be.exist
})
})
context('Cypress.version', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current version of Cypress being run', () => {
// https://on.cypress.io/version
expect(Cypress.version).to.be.exist
})
})
context('Cypress.spec', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current spec information', () => {
// https://on.cypress.io/spec
// wrap the object so we can inspect it easily by clicking in the command log
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
})
})

View File

@ -1,114 +0,0 @@
/// <reference types="cypress" />
/// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler
// @ts-ignore
const requiredExample = require('../../fixtures/example')
context('Files', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/files')
})
beforeEach(() => {
// load example.json fixture file and store
// in the test context object
cy.fixture('example.json').as('example')
})
it('cy.fixture() - load a fixture', () => {
// https://on.cypress.io/fixture
// Instead of writing a response inline you can
// use a fixture file's content.
cy.server()
cy.fixture('example.json').as('comment')
// when application makes an Ajax request matching "GET comments/*"
// Cypress will intercept it and reply with object
// from the "comment" alias
cy.route('GET', 'comments/*', '@comment').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
// you can also just write the fixture in the route
cy.route('GET', 'comments/*', 'fixture:example.json').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
// or write fx to represent fixture
// by default it assumes it's .json
cy.route('GET', 'comments/*', 'fx:example').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
})
it('cy.fixture() or require - load a fixture', function () {
// we are inside the "function () { ... }"
// callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback
expect(this.example, 'fixture in the test context')
.to.deep.equal(requiredExample)
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
// @ts-ignore
cy.wrap(this.example, 'fixture vs require')
.should('deep.equal', requiredExample)
})
it('cy.readFile() - read file contents', () => {
// https://on.cypress.io/readfile
// You can read a file and yield its contents
// The filePath is relative to your project's root.
cy.readFile('cypress.json').then((json) => {
expect(json).to.be.an('object')
})
})
it('cy.writeFile() - write to a file', () => {
// https://on.cypress.io/writefile
// You can write to a file
// Use a response from a request to automatically
// generate a fixture file for use later
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
cy.writeFile('cypress/fixtures/users.json', response.body)
})
cy.fixture('users').should((users) => {
expect(users[0].name).to.exist
})
// JavaScript arrays and objects are stringified
// and formatted into text.
cy.writeFile('cypress/fixtures/profile.json', {
id: 8739,
name: 'Jane',
email: 'jane@example.com',
})
cy.fixture('profile').should((profile) => {
expect(profile.name).to.eq('Jane')
})
})
})

View File

@ -1,52 +0,0 @@
/// <reference types="cypress" />
context('Local Storage', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/local-storage')
})
// Although local storage is automatically cleared
// in between tests to maintain a clean state
// sometimes we need to clear the local storage manually
it('cy.clearLocalStorage() - clear all data in local storage', () => {
// https://on.cypress.io/clearlocalstorage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
// clearLocalStorage() yields the localStorage object
cy.clearLocalStorage().should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.be.null
expect(ls.getItem('prop3')).to.be.null
})
// Clear key matching string in Local Storage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.clearLocalStorage('prop1').should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.eq('blue')
expect(ls.getItem('prop3')).to.eq('magenta')
})
// Clear keys matching regex in Local Storage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.clearLocalStorage(/prop1|2/).should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.be.null
expect(ls.getItem('prop3')).to.eq('magenta')
})
})
})

View File

@ -1,32 +0,0 @@
/// <reference types="cypress" />
context('Location', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/location')
})
it('cy.hash() - get the current URL hash', () => {
// https://on.cypress.io/hash
cy.hash().should('be.empty')
})
it('cy.location() - get window.location', () => {
// https://on.cypress.io/location
cy.location().should((location) => {
expect(location.hash).to.be.empty
expect(location.href).to.eq('https://example.cypress.io/commands/location')
expect(location.host).to.eq('example.cypress.io')
expect(location.hostname).to.eq('example.cypress.io')
expect(location.origin).to.eq('https://example.cypress.io')
expect(location.pathname).to.eq('/commands/location')
expect(location.port).to.eq('')
expect(location.protocol).to.eq('https:')
expect(location.search).to.be.empty
})
})
it('cy.url() - get the current URL', () => {
// https://on.cypress.io/url
cy.url().should('eq', 'https://example.cypress.io/commands/location')
})
})

View File

@ -1,92 +0,0 @@
/// <reference types="cypress" />
context('Misc', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/misc')
})
it('.end() - end the command chain', () => {
// https://on.cypress.io/end
// cy.end is useful when you want to end a chain of commands
// and force Cypress to re-query from the root element
cy.get('.misc-table').within(() => {
// ends the current chain and yields null
cy.contains('Cheryl').click().end()
// queries the entire table again
cy.contains('Charles').click()
})
})
it('cy.exec() - execute a system command', () => {
// execute a system command.
// so you can take actions necessary for
// your test outside the scope of Cypress.
// https://on.cypress.io/exec
// we can use Cypress.platform string to
// select appropriate command
// https://on.cypress/io/platform
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
// on CircleCI Windows build machines we have a failure to run bash shell
// https://github.com/cypress-io/cypress/issues/5169
// so skip some of the tests by passing flag "--env circle=true"
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
if (isCircleOnWindows) {
return
}
cy.exec('echo Jane Lane')
.its('stdout').should('contain', 'Jane Lane')
if (Cypress.platform === 'win32') {
cy.exec('print cypress.json')
.its('stderr').should('be.empty')
} else {
cy.exec('cat cypress.json')
.its('stderr').should('be.empty')
cy.exec('pwd')
.its('code').should('eq', 0)
}
})
it('cy.focused() - get the DOM element that has focus', () => {
// https://on.cypress.io/focused
cy.get('.misc-form').find('#name').click()
cy.focused().should('have.id', 'name')
cy.get('.misc-form').find('#description').click()
cy.focused().should('have.id', 'description')
})
context('Cypress.Screenshot', function () {
it('cy.screenshot() - take a screenshot', () => {
// https://on.cypress.io/screenshot
cy.screenshot('my-image')
})
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
Cypress.Screenshot.defaults({
blackout: ['.foo'],
capture: 'viewport',
clip: { x: 0, y: 0, width: 200, height: 200 },
scale: false,
disableTimersAndAnimations: true,
screenshotOnRunFailure: true,
beforeScreenshot () { },
afterScreenshot () { },
})
})
})
it('cy.wrap() - wrap an object', () => {
// https://on.cypress.io/wrap
cy.wrap({ foo: 'bar' })
.should('have.property', 'foo')
.and('include', 'bar')
})
})

View File

@ -1,56 +0,0 @@
/// <reference types="cypress" />
context('Navigation', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io')
cy.get('.navbar-nav').contains('Commands').click()
cy.get('.dropdown-menu').contains('Navigation').click()
})
it('cy.go() - go back or forward in the browser\'s history', () => {
// https://on.cypress.io/go
cy.location('pathname').should('include', 'navigation')
cy.go('back')
cy.location('pathname').should('not.include', 'navigation')
cy.go('forward')
cy.location('pathname').should('include', 'navigation')
// clicking back
cy.go(-1)
cy.location('pathname').should('not.include', 'navigation')
// clicking forward
cy.go(1)
cy.location('pathname').should('include', 'navigation')
})
it('cy.reload() - reload the page', () => {
// https://on.cypress.io/reload
cy.reload()
// reload the page without using the cache
cy.reload(true)
})
it('cy.visit() - visit a remote url', () => {
// https://on.cypress.io/visit
// Visit any sub-domain of your current domain
// Pass options to the visit
cy.visit('https://example.cypress.io/commands/navigation', {
timeout: 50000, // increase total time for the visit to resolve
onBeforeLoad (contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true
},
onLoad (contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true
},
})
})
})

View File

@ -1,195 +0,0 @@
/// <reference types="cypress" />
context('Network Requests', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/network-requests')
})
// Manage AJAX / XHR requests in your app
it('cy.server() - control behavior of network requests and responses', () => {
// https://on.cypress.io/server
cy.server().should((server) => {
// the default options on server
// you can override any of these options
expect(server.delay).to.eq(0)
expect(server.method).to.eq('GET')
expect(server.status).to.eq(200)
expect(server.headers).to.be.null
expect(server.response).to.be.null
expect(server.onRequest).to.be.undefined
expect(server.onResponse).to.be.undefined
expect(server.onAbort).to.be.undefined
// These options control the server behavior
// affecting all requests
// pass false to disable existing route stubs
expect(server.enable).to.be.true
// forces requests that don't match your routes to 404
expect(server.force404).to.be.false
// whitelists requests from ever being logged or stubbed
expect(server.whitelist).to.be.a('function')
})
cy.server({
method: 'POST',
delay: 1000,
status: 422,
response: {},
})
// any route commands will now inherit the above options
// from the server. anything we pass specifically
// to route will override the defaults though.
})
it('cy.request() - make an XHR request', () => {
// https://on.cypress.io/request
cy.request('https://jsonplaceholder.cypress.io/comments')
.should((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.length(500)
expect(response).to.have.property('headers')
expect(response).to.have.property('duration')
})
})
it('cy.request() - verify response using BDD syntax', () => {
cy.request('https://jsonplaceholder.cypress.io/comments')
.then((response) => {
// https://on.cypress.io/assertions
expect(response).property('status').to.equal(200)
expect(response).property('body').to.have.length(500)
expect(response).to.include.keys('headers', 'duration')
})
})
it('cy.request() with query parameters', () => {
// will execute request
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
cy.request({
url: 'https://jsonplaceholder.cypress.io/comments',
qs: {
postId: 1,
id: 3,
},
})
.its('body')
.should('be.an', 'array')
.and('have.length', 1)
.its('0') // yields first element of the array
.should('contain', {
postId: 1,
id: 3,
})
})
it('cy.request() - pass result to the second request', () => {
// first, let's find out the userId of the first user we have
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body') // yields the response object
.its('0') // yields the first element of the returned list
// the above two commands its('body').its('0')
// can be written as its('body.0')
// if you do not care about TypeScript checks
.then((user) => {
expect(user).property('id').to.be.a('number')
// make a new post on behalf of the user
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
})
// note that the value here is the returned value of the 2nd request
// which is the new post object
.then((response) => {
expect(response).property('status').to.equal(201) // new entity created
expect(response).property('body').to.contain({
id: 101, // there are already 100 posts, so new entity gets id 101
title: 'Cypress Test Runner',
})
// we don't know the user id here - since it was in above closure
// so in this test just confirm that the property is there
expect(response.body).property('userId').to.be.a('number')
})
})
it('cy.request() - save response in the shared test context', () => {
// https://on.cypress.io/variables-and-aliases
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body').its('0') // yields the first element of the returned list
.as('user') // saves the object in the test context
.then(function () {
// NOTE 👀
// By the time this callback runs the "as('user')" command
// has saved the user object in the test context.
// To access the test context we need to use
// the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object!
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: this.user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
.its('body').as('post') // save the new post from the response
})
.then(function () {
// When this callback runs, both "cy.request" API commands have finished
// and the test context has "user" and "post" objects set.
// Let's verify them.
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
})
})
it('cy.route() - route responses to matching requests', () => {
// https://on.cypress.io/route
let message = 'whoa, this comment does not exist'
cy.server()
// Listen to GET to comments/1
cy.route('GET', 'comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// https://on.cypress.io/wait
cy.wait('@getComment').its('status').should('eq', 200)
// Listen to POST to comments
cy.route('POST', '/comments').as('postComment')
// we have code that posts a comment when
// the button is clicked in scripts.js
cy.get('.network-post').click()
cy.wait('@postComment').should((xhr) => {
expect(xhr.requestBody).to.include('email')
expect(xhr.requestHeaders).to.have.property('Content-Type')
expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()')
})
// Stub a response to PUT comments/ ****
cy.route({
method: 'PUT',
url: 'comments/*',
status: 404,
response: { error: message },
delay: 500,
}).as('putComment')
// we have code that puts a comment when
// the button is clicked in scripts.js
cy.get('.network-put').click()
cy.wait('@putComment')
// our 404 statusCode logic in scripts.js executed
cy.get('.network-put-comment').should('contain', message)
})
})

View File

@ -1,114 +0,0 @@
/// <reference types="cypress" />
context('Querying', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/querying')
})
// The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery
it('cy.get() - query DOM elements', () => {
// https://on.cypress.io/get
cy.get('#query-btn').should('contain', 'Button')
cy.get('.query-btn').should('contain', 'Button')
cy.get('#querying .well>button:first').should('contain', 'Button')
// ↲
// Use CSS selectors just like jQuery
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
// 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method
cy.get('[data-test-id="test-example"]')
.invoke('attr', 'data-test-id')
.should('equal', 'test-example')
// or you can get element's CSS property
cy.get('[data-test-id="test-example"]')
.invoke('css', 'position')
.should('equal', 'static')
// or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]')
.should('have.attr', 'data-test-id', 'test-example')
.and('have.css', 'position', 'static')
})
it('cy.contains() - query DOM elements with matching content', () => {
// https://on.cypress.io/contains
cy.get('.query-list')
.contains('bananas')
.should('have.class', 'third')
// we can pass a regexp to `.contains()`
cy.get('.query-list')
.contains(/^b\w+/)
.should('have.class', 'third')
cy.get('.query-list')
.contains('apples')
.should('have.class', 'first')
// passing a selector to contains will
// yield the selector containing the text
cy.get('#querying')
.contains('ul', 'oranges')
.should('have.class', 'query-list')
cy.get('.query-button')
.contains('Save Form')
.should('have.class', 'btn')
})
it('.within() - query DOM elements within a specific element', () => {
// https://on.cypress.io/within
cy.get('.query-form').within(() => {
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
})
})
it('cy.root() - query the root DOM element', () => {
// https://on.cypress.io/root
// By default, root is the document
cy.root().should('match', 'html')
cy.get('.query-ul').within(() => {
// In this within, the root is now the ul DOM element
cy.root().should('have.class', 'query-ul')
})
})
it('best practices - selecting elements', () => {
// https://on.cypress.io/best-practices#Selecting-Elements
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
// Worst - too generic, no context
cy.get('button').click()
// Bad. Coupled to styling. Highly subject to change.
cy.get('.btn.btn-large').click()
// Average. Coupled to the `name` attribute which has HTML semantics.
cy.get('[name=submission]').click()
// Better. But still coupled to styling or JS event listeners.
cy.get('#main').click()
// Slightly better. Uses an ID but also ensures the element
// has an ARIA role attribute
cy.get('#main[role=button]').click()
// Much better. But still coupled to text content that may change.
cy.contains('Submit').click()
// Best. Insulated from all changes.
cy.get('[data-cy=submit]').click()
})
})
})

View File

@ -1,95 +0,0 @@
/// <reference types="cypress" />
context('Spies, Stubs, and Clock', () => {
it('cy.spy() - wrap a method in a spy', () => {
// https://on.cypress.io/spy
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
foo () {},
}
const spy = cy.spy(obj, 'foo').as('anyArgs')
obj.foo()
expect(spy).to.be.called
})
it('cy.spy() retries until assertions pass', () => {
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
/**
* Prints the argument passed
* @param x {any}
*/
foo (x) {
console.log('obj.foo called with', x)
},
}
cy.spy(obj, 'foo').as('foo')
setTimeout(() => {
obj.foo('first')
}, 500)
setTimeout(() => {
obj.foo('second')
}, 2500)
cy.get('@foo').should('have.been.calledTwice')
})
it('cy.stub() - create a stub and/or replace a function with stub', () => {
// https://on.cypress.io/stub
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
/**
* prints both arguments to the console
* @param a {string}
* @param b {string}
*/
foo (a, b) {
console.log('a', a, 'b', b)
},
}
const stub = cy.stub(obj, 'foo').as('foo')
obj.foo('foo', 'bar')
expect(stub).to.be.called
})
it('cy.clock() - control time in the browser', () => {
// https://on.cypress.io/clock
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#clock-div').click()
.should('have.text', '1489449600')
})
it('cy.tick() - move time in the browser', () => {
// https://on.cypress.io/tick
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#tick-div').click()
.should('have.text', '1489449600')
cy.tick(10000) // 10 seconds passed
cy.get('#tick-div').click()
.should('have.text', '1489449610')
})
})

View File

@ -1,121 +0,0 @@
/// <reference types="cypress" />
context('Traversal', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/traversal')
})
it('.children() - get child DOM elements', () => {
// https://on.cypress.io/children
cy.get('.traversal-breadcrumb')
.children('.active')
.should('contain', 'Data')
})
it('.closest() - get closest ancestor DOM element', () => {
// https://on.cypress.io/closest
cy.get('.traversal-badge')
.closest('ul')
.should('have.class', 'list-group')
})
it('.eq() - get a DOM element at a specific index', () => {
// https://on.cypress.io/eq
cy.get('.traversal-list>li')
.eq(1).should('contain', 'siamese')
})
it('.filter() - get DOM elements that match the selector', () => {
// https://on.cypress.io/filter
cy.get('.traversal-nav>li')
.filter('.active').should('contain', 'About')
})
it('.find() - get descendant DOM elements of the selector', () => {
// https://on.cypress.io/find
cy.get('.traversal-pagination')
.find('li').find('a')
.should('have.length', 7)
})
it('.first() - get first DOM element', () => {
// https://on.cypress.io/first
cy.get('.traversal-table td')
.first().should('contain', '1')
})
it('.last() - get last DOM element', () => {
// https://on.cypress.io/last
cy.get('.traversal-buttons .btn')
.last().should('contain', 'Submit')
})
it('.next() - get next sibling DOM element', () => {
// https://on.cypress.io/next
cy.get('.traversal-ul')
.contains('apples').next().should('contain', 'oranges')
})
it('.nextAll() - get all next sibling DOM elements', () => {
// https://on.cypress.io/nextall
cy.get('.traversal-next-all')
.contains('oranges')
.nextAll().should('have.length', 3)
})
it('.nextUntil() - get next sibling DOM elements until next el', () => {
// https://on.cypress.io/nextuntil
cy.get('#veggies')
.nextUntil('#nuts').should('have.length', 3)
})
it('.not() - remove DOM elements from set of DOM elements', () => {
// https://on.cypress.io/not
cy.get('.traversal-disabled .btn')
.not('[disabled]').should('not.contain', 'Disabled')
})
it('.parent() - get parent DOM element from DOM elements', () => {
// https://on.cypress.io/parent
cy.get('.traversal-mark')
.parent().should('contain', 'Morbi leo risus')
})
it('.parents() - get parent DOM elements from DOM elements', () => {
// https://on.cypress.io/parents
cy.get('.traversal-cite')
.parents().should('match', 'blockquote')
})
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
// https://on.cypress.io/parentsuntil
cy.get('.clothes-nav')
.find('.active')
.parentsUntil('.clothes-nav')
.should('have.length', 2)
})
it('.prev() - get previous sibling DOM element', () => {
// https://on.cypress.io/prev
cy.get('.birds').find('.active')
.prev().should('contain', 'Lorikeets')
})
it('.prevAll() - get all previous sibling DOM elements', () => {
// https://on.cypress.io/prevAll
cy.get('.fruits-list').find('.third')
.prevAll().should('have.length', 2)
})
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
// https://on.cypress.io/prevUntil
cy.get('.foods-list').find('#nuts')
.prevUntil('#veggies').should('have.length', 3)
})
it('.siblings() - get all sibling DOM elements', () => {
// https://on.cypress.io/siblings
cy.get('.traversal-pills .active')
.siblings().should('have.length', 2)
})
})

View File

@ -1,133 +0,0 @@
/// <reference types="cypress" />
context('Utilities', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/utilities')
})
it('Cypress._ - call a lodash method', () => {
// https://on.cypress.io/_
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
let ids = Cypress._.chain(response.body).map('id').take(3).value()
expect(ids).to.deep.eq([1, 2, 3])
})
})
it('Cypress.$ - call a jQuery method', () => {
// https://on.cypress.io/$
let $li = Cypress.$('.utility-jquery li:first')
cy.wrap($li)
.should('not.have.class', 'active')
.click()
.should('have.class', 'active')
})
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
// https://on.cypress.io/blob
cy.get('.utility-blob').then(($div) =>
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
// get the dataUrl string for the javascript-logo
Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
.then((dataUrl) => {
// create an <img> element and set its src to the dataUrl
let img = Cypress.$('<img />', { src: dataUrl })
// need to explicitly return cy here since we are initially returning
// the Cypress.Blob.imgSrcToDataURL promise to our test
// append the image
$div.append(img)
cy.get('.utility-blob img').click()
.should('have.attr', 'src', dataUrl)
}))
})
it('Cypress.minimatch - test out glob patterns against strings', () => {
// https://on.cypress.io/minimatch
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
matchBase: true,
})
expect(matching, 'matching wildcard').to.be.true
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
matchBase: true,
})
expect(matching, 'comments').to.be.false
// ** matches against all downstream path segments
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
matchBase: true,
})
expect(matching, 'comments').to.be.true
// whereas * matches only the next path segment
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
matchBase: false,
})
expect(matching, 'comments').to.be.false
})
it('Cypress.moment() - format or parse dates using a moment method', () => {
// https://on.cypress.io/moment
const time = Cypress.moment('2014-04-25T19:38:53.196Z').utc().format('h:mm A')
expect(time).to.be.a('string')
cy.get('.utility-moment').contains('3:38 PM')
.should('have.class', 'badge')
// the time in the element should be between 3pm and 5pm
const start = Cypress.moment('3:00 PM', 'LT')
const end = Cypress.moment('5:00 PM', 'LT')
cy.get('.utility-moment .badge')
.should(($el) => {
// parse American time like "3:38 PM"
const m = Cypress.moment($el.text().trim(), 'LT')
// display hours + minutes + AM|PM
const f = 'h:mm A'
expect(m.isBetween(start, end),
`${m.format(f)} should be between ${start.format(f)} and ${end.format(f)}`).to.be.true
})
})
it('Cypress.Promise - instantiate a bluebird promise', () => {
// https://on.cypress.io/promise
let waited = false
/**
* @return Bluebird<string>
*/
function waitOneSecond () {
// return a promise that resolves after 1 second
// @ts-ignore TS2351 (new Cypress.Promise)
return new Cypress.Promise((resolve, reject) => {
setTimeout(() => {
// set waited to true
waited = true
// resolve with 'foo' string
resolve('foo')
}, 1000)
})
}
cy.then(() =>
// return a promise to cy.then() that
// is awaited until it resolves
// @ts-ignore TS7006
waitOneSecond().then((str) => {
expect(str).to.eq('foo')
expect(waited).to.be.true
}))
})
})

View File

@ -1,59 +0,0 @@
/// <reference types="cypress" />
context('Viewport', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/viewport')
})
it('cy.viewport() - set the viewport size and dimension', () => {
// https://on.cypress.io/viewport
cy.get('#navbar').should('be.visible')
cy.viewport(320, 480)
// the navbar should have collapse since our screen is smaller
cy.get('#navbar').should('not.be.visible')
cy.get('.navbar-toggle').should('be.visible').click()
cy.get('.nav').find('a').should('be.visible')
// lets see what our app looks like on a super large screen
cy.viewport(2999, 2999)
// cy.viewport() accepts a set of preset sizes
// to easily set the screen to a device's width and height
// We added a cy.wait() between each viewport change so you can see
// the change otherwise it is a little too fast to see :)
cy.viewport('macbook-15')
cy.wait(200)
cy.viewport('macbook-13')
cy.wait(200)
cy.viewport('macbook-11')
cy.wait(200)
cy.viewport('ipad-2')
cy.wait(200)
cy.viewport('ipad-mini')
cy.wait(200)
cy.viewport('iphone-6+')
cy.wait(200)
cy.viewport('iphone-6')
cy.wait(200)
cy.viewport('iphone-5')
cy.wait(200)
cy.viewport('iphone-4')
cy.wait(200)
cy.viewport('iphone-3')
cy.wait(200)
// cy.viewport() accepts an orientation for all presets
// the default orientation is 'portrait'
cy.viewport('ipad-2', 'portrait')
cy.wait(200)
cy.viewport('iphone-4', 'landscape')
cy.wait(200)
// The viewport will be reset back to the default dimensions
// in between tests (the default can be set in cypress.json)
})
})

View File

@ -1,34 +0,0 @@
/// <reference types="cypress" />
context('Waiting', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/waiting')
})
// BE CAREFUL of adding unnecessary wait times.
// https://on.cypress.io/best-practices#Unnecessary-Waiting
// https://on.cypress.io/wait
it('cy.wait() - wait for a specific amount of time', () => {
cy.get('.wait-input1').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input2').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input3').type('Wait 1000ms after typing')
cy.wait(1000)
})
it('cy.wait() - wait for a specific route', () => {
cy.server()
// Listen to GET to comments/1
cy.route('GET', 'comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// wait for GET comments/1
cy.wait('@getComment').its('status').should('eq', 200)
})
})

View File

@ -1,22 +0,0 @@
/// <reference types="cypress" />
context('Window', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/window')
})
it('cy.window() - get the global window object', () => {
// https://on.cypress.io/window
cy.window().should('have.property', 'top')
})
it('cy.document() - get the document object', () => {
// https://on.cypress.io/document
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
})
it('cy.title() - get the title', () => {
// https://on.cypress.io/title
cy.title().should('include', 'Kitchen Sink')
})
})

View File

@ -1,106 +0,0 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
describe('Authorize.net: Credit card test', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account authorizenet');
});
beforeEach(() => {
cy.viewport('macbook-13');
cy.wait(5000);
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout').visit('/client/login');
});
it('should pay with new card', function () {
cy.visit('/client/invoices').then((contentWindow) => {
cy.get('[data-cy=pay-now]').first().click()
.get('[data-cy=pay-now-dropdown]').click()
.get('[data-cy=pay-with-0]').click();
cy.get('#card_number').type('4007000000027')
.get('#cardholder_name').type('Invoice Ninja Rocks')
.get('[class=expiry]').type('12/28')
.get('[name=cvc]').type('100');
cy.get('#pay-now').click();
});
cy.location('pathname', {timeout: 60000}).should('include', '/client/payments/VolejRejNm');
});
it('should pay with new card & save credit card for future use', function () {
cy.visit('/client/invoices').then((contentWindow) => {
cy.get('[data-cy=pay-now]').first().click()
.get('[data-cy=pay-now-dropdown]').click()
.get('[data-cy=pay-with-0]').click();
cy.get('#card_number').type('4007000000027')
.get('#cardholder_name').type('Invoice Ninja Rocks')
.get('[class=expiry]').type('12/28')
.get('[name=cvc]').type('100');
cy.get('[name=token-billing-checkbox]').first().check();
cy.get('#pay-now').click();
});
cy.location('pathname', {timeout: 60000}).should('include', '/client/payments/Wpmbk5ezJn');
});
it('should pay with saved card (token)', function () {
cy.visit('/client/invoices')
.get('[data-cy=pay-now]').first().click()
.get('[data-cy=pay-now-dropdown]').click()
.get('[data-cy=pay-with-0]').click();
cy.get('[name=payment-type]').first().check();
cy.get('#pay-now').click();
cy.wait(2000);
cy.location('pathname', {timeout: 60000}).should('include', '/client/payments/Opnel5aKBz');
});
it('should be able to remove payment method', function () {
cy.visit('/client/payment_methods')
.get('[data-cy=view-payment-method]').click();
cy.get('#open-delete-popup').click();
cy.get('[data-cy=confirm-payment-removal]').click();
cy.url().should('contain', '/client/payment_methods');
cy.get('body').contains('Payment method has been successfully removed.');
});
it('should be able to add credit card (standalone)', function () {
cy.visit('/client/payment_methods')
.get('[data-cy=add-payment-method]').click()
.get('[data-cy=add-credit-card-link]').click();
cy.get('#card_number').type('4007000000027')
.get('#cardholder_name').type('Invoice Ninja Rocks')
.get('[class=expiry]').type('12/28')
.get('[name=cvc]').type('100');
cy.get('#card_button').click();
cy.location('pathname', {timeout: 60000}).should('include', '/client/payment_methods');
});
});

View File

@ -1,75 +0,0 @@
context('Checkout.com: Credit card testing', () => {
beforeEach(() => {
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout');
});
it('should not be able to add payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=add-payment-method]').click();
cy.get('[data-cy=add-credit-card-link]').click();
cy.get('[data-ref=gateway-container]')
.contains('This payment method can be can saved for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.');
});
it('should pay with new card', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy
.get('#braintree-hosted-field-number')
.wait(5000)
.iframeLoaded()
.its('document')
.getInDocument('#credit-card-number')
.type(4111111111111111)
cy
.get('#braintree-hosted-field-expirationDate')
.wait(5000)
.iframeLoaded()
.its('document')
.getInDocument('#expiration')
.type(1224)
cy.get('#pay-now').click();
cy.url().should('contain', '/client/payments/VolejRejNm');
});
it('should pay with saved card (token)', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('[name=payment-type]').first().check();
cy.get('#pay-now-with-token').click();
cy.url().should('contain', '/client/payments/Opnel5aKBz');
});
it('should be able to remove payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=view-payment-method]').click();
cy.get('#open-delete-popup').click();
cy.get('[data-cy=confirm-payment-removal]').click();
cy.url().should('contain', '/client/payment_methods');
cy.get('body').contains('Payment method has been successfully removed.');
});
});

View File

@ -1,87 +0,0 @@
context('Checkout.com: Credit card testing', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account checkout');
});
beforeEach(() => {
cy.viewport('macbook-13');
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout');
});
it('should not be able to add payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=add-payment-method]').click();
cy.get('[data-cy=add-credit-card-link]').click();
cy.get('[data-ref=gateway-container]')
.contains('Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.');
});
it('should pay with new card', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.getWithinIframe('#checkout-frames-card-number').type('4658584090000001');
cy.getWithinIframe('#checkout-frames-expiry-date').type('12/22');
cy.getWithinIframe('#checkout-frames-cvv').type('257');
cy.get('#pay-button').click();
cy.url().should('contain', '/client/payments/VolejRejNm');
});
it('should pay with new card & save credit card for future use', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('[name=token-billing-checkbox]').first().check();
cy.getWithinIframe('#checkout-frames-card-number').type('4543474002249996');
cy.getWithinIframe('#checkout-frames-expiry-date').type('12/22');
cy.getWithinIframe('#checkout-frames-cvv').type('956');
cy.get('#pay-button').click();
cy.url().should('contain', '/client/payments/Wpmbk5ezJn');
});
it('should pay with saved card (token)', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('[name=payment-type]').first().check();
cy.get('#pay-now-with-token').click();
cy.url().should('contain', '/client/payments/Opnel5aKBz');
});
it('should be able to remove payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=view-payment-method]').click();
cy.get('#open-delete-popup').click();
cy.get('[data-cy=confirm-payment-removal]').click();
cy.url().should('contain', '/client/payment_methods');
cy.get('body').contains('Payment method has been successfully removed.');
});
});

View File

@ -1,248 +0,0 @@
import axios from 'axios';
describe('Stripe: ACH testing', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account stripe');
});
beforeEach(() => {
let headers = {
'X-API-Token': 'company-token-test',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json; charset=utf-8',
};
let gatewaysBody = {
"gateway_key": "d14dd26a37cecc30fdd65700bfb55b23",
"accepted_credit_cards": 0,
"require_shipping_address": true,
"require_billing_address": true,
"require_client_name": false,
"require_client_phone": false,
"require_contact_name": false,
"require_contact_email": false,
"require_cvv": true,
"update_details": true,
"fees_and_limits": {
"1": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
},
"2": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
},
"6": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
}
},
"system_logs": [],
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"config": "{\"apiKey\":\"sk_test_Yorqvz45sZWSSUmvCfoKF8e6\",\"publishableKey\":\"pk_test_P1riKDKD0pdNTkHwBWEZ8DR0\",\"enable_ach\":\"1\",\"enable_sofort\":\"1\",\"enable_apple_pay\":\"0\",\"enable_alipay\":\"0\"}",
"token_billing": "off",
"test_mode": true,
"label": "Stripe",
"created_at": 1612791181,
"updated_at": 1612792176,
"archived_at": 0,
"id": "VolejRejNm",
"loadedAt": 1612792176934,
"require_postal_code": false,
"is_deleted": false
};
let clientBody = {
"group_settings_id": "",
"name": "Batz LLC",
"display_name": "Batz LLC",
"balance": 8323.7,
"credit_balance": 0,
"paid_to_date": 0,
"client_hash": "DxrMypcMdnYJvfebfeoXUi2Iyear6LkNq7Twi0H9",
"address1": "45804",
"address2": "47988 Rex Mall",
"city": "New Macberg",
"state": "Florida",
"postal_code": "43089-5809",
"country_id": "840",
"phone": "",
"private_notes": "Neque libero eos adipisci quae. Non voluptas quaerat ea nisi repudiandae in. Voluptatem error aut est distinctio perspiciatis quis.",
"public_notes": "",
"website": "https://www.wintheiser.com/non-velit-nisi-culpa-sit-optio-omnis-ipsum-pariatur",
"industry_id": "",
"size_id": "",
"vat_number": "157764830",
"id_number": "",
"number": "0001",
"shipping_address1": "5181",
"shipping_address2": "66797 Jedediah Isle Suite 479",
"shipping_city": "Lake Rosariomouth",
"shipping_state": "Nevada",
"shipping_postal_code": "31693",
"shipping_country_id": "4",
"settings": {
"currency_id": "1"
},
"last_login": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"contacts": [
{
"first_name": "Rita",
"last_name": "Pouros",
"email": "user@example.com",
"password": "**********",
"phone": "+1-331-663-8498",
"contact_key": "hNQkBU6RM6tG2pwu4J7dCfuq2ZdH6Q8anEvKnyoL",
"is_primary": true,
"send_email": true,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"last_login": 0,
"link": "https://localhost:8080/client/key_login/hNQkBU6RM6tG2pwu4J7dCfuq2ZdH6Q8anEvKnyoL",
"created_at": 1612792539,
"updated_at": 1612792539,
"archived_at": 0,
"id": "VolejRejNm"
},
{
"first_name": "Danika",
"last_name": "Hauck",
"email": "bbrakus@example.net",
"password": "**********",
"phone": "662-968-5275 x48146",
"contact_key": "4hWqvVUv2bwYIOb25rWmQhbhadnl5yneTzglGZ32",
"is_primary": false,
"send_email": true,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"last_login": 0,
"link": "https://localhost:8080/client/key_login/4hWqvVUv2bwYIOb25rWmQhbhadnl5yneTzglGZ32",
"created_at": 1612792539,
"updated_at": 1612792539,
"archived_at": 0,
"id": "Wpmbk5ezJn"
}
],
"activities": [],
"ledger": [],
"gateway_tokens": [],
"documents": [],
"system_logs": [],
"created_at": 1612792539,
"updated_at": 1612792565,
"archived_at": 0,
"id": "VolejRejNm",
"isChanged": true,
"is_deleted": false,
"user_id": "VolejRejNm",
"assigned_user_id": ""
};
axios.put('https://localhost:8080/api/v1/company_gateways/VolejRejNm', gatewaysBody, {headers})
axios.put('https://localhost:8080/api/v1/clients/VolejRejNm', clientBody, {headers}); // Set country to US.
cy.viewport('macbook-13');
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout').visit('/client/login');
});
it('should be able to add bank account & verify it', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=add-payment-method]').click();
cy.get('[data-cy=add-bank-account-link]').click();
cy.get('#account-holder-name').type('Invoice Ninja Rocks');
cy.get('#country').select('US');
cy.get('#currency').select('USD');
cy.get('#routing-number').type('110000000');
cy.get('#account-number').type('000123456789');
cy.get('#accept-terms').check();
cy.get('#save-button').click();
cy.url().should('contain', 'method=2');
cy.get('[data-cy=verification-1st]').type('32');
cy.get('[data-cy=verification-2nd]').type('45');
cy.get('#pay-now').click();
cy.get('body').contains('Verification completed successfully');
});
it('should be able to pay the invoice', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-2]').click();
cy.get('[name=payment-type]').first().check();
cy.get('#pay-now').click();
cy.url().should('contain', '/client/payments/');
});
it('should be able to remove payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=view-payment-method]').click();
cy.get('#open-delete-popup').click();
cy.get('[data-cy=confirm-payment-removal]').click();
cy.url().should('contain', '/client/payment_methods');
cy.get('body').contains('Payment method has been successfully removed.');
});
});

View File

@ -1,209 +0,0 @@
import axios from "axios";
describe('Stripe: Alipay testing', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account checkout');
});
beforeEach(() => {
let headers = {
'X-API-Token': 'company-token-test',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json; charset=utf-8',
};
let gatewaysBody = {
"gateway_key": "d14dd26a37cecc30fdd65700bfb55b23",
"accepted_credit_cards": 0,
"require_shipping_address": true,
"require_billing_address": true,
"require_client_name": false,
"require_client_phone": false,
"require_contact_name": false,
"require_contact_email": false,
"require_cvv": true,
"update_details": true,
"fees_and_limits": {
"1": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
},
"2": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
},
"6": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
}
},
"system_logs": [],
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"config": "{\"apiKey\":\"sk_test_Yorqvz45sZWSSUmvCfoKF8e6\",\"publishableKey\":\"pk_test_P1riKDKD0pdNTkHwBWEZ8DR0\",\"enable_ach\":\"0\",\"enable_sofort\":\"0\",\"enable_apple_pay\":\"0\",\"enable_alipay\":\"1\"}",
"token_billing": "off",
"test_mode": true,
"label": "Stripe",
"created_at": 1612791181,
"updated_at": 1612792176,
"archived_at": 0,
"id": "VolejRejNm",
"loadedAt": 1612792176934,
"require_postal_code": false,
"is_deleted": false
};
let clientBody = {
"group_settings_id": "",
"name": "Batz LLC",
"display_name": "Batz LLC",
"balance": 8323.7,
"credit_balance": 0,
"paid_to_date": 0,
"client_hash": "DxrMypcMdnYJvfebfeoXUi2Iyear6LkNq7Twi0H9",
"address1": "45804",
"address2": "47988 Rex Mall",
"city": "New Macberg",
"state": "Florida",
"postal_code": "43089-5809",
"country_id": "840",
"phone": "",
"private_notes": "Neque libero eos adipisci quae. Non voluptas quaerat ea nisi repudiandae in. Voluptatem error aut est distinctio perspiciatis quis.",
"public_notes": "",
"website": "https://www.wintheiser.com/non-velit-nisi-culpa-sit-optio-omnis-ipsum-pariatur",
"industry_id": "",
"size_id": "",
"vat_number": "157764830",
"id_number": "",
"number": "0001",
"shipping_address1": "5181",
"shipping_address2": "66797 Jedediah Isle Suite 479",
"shipping_city": "Lake Rosariomouth",
"shipping_state": "Nevada",
"shipping_postal_code": "31693",
"shipping_country_id": "4",
"settings": {
"currency_id": "1"
},
"last_login": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"contacts": [
{
"first_name": "Rita",
"last_name": "Pouros",
"email": "user@example.com",
"password": "**********",
"phone": "+1-331-663-8498",
"contact_key": "hNQkBU6RM6tG2pwu4J7dCfuq2ZdH6Q8anEvKnyoL",
"is_primary": true,
"send_email": true,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"last_login": 0,
"link": "https://localhost:8080/client/key_login/hNQkBU6RM6tG2pwu4J7dCfuq2ZdH6Q8anEvKnyoL",
"created_at": 1612792539,
"updated_at": 1612792539,
"archived_at": 0,
"id": "VolejRejNm"
},
{
"first_name": "Danika",
"last_name": "Hauck",
"email": "bbrakus@example.net",
"password": "**********",
"phone": "662-968-5275 x48146",
"contact_key": "4hWqvVUv2bwYIOb25rWmQhbhadnl5yneTzglGZ32",
"is_primary": false,
"send_email": true,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"last_login": 0,
"link": "https://localhost:8080/client/key_login/4hWqvVUv2bwYIOb25rWmQhbhadnl5yneTzglGZ32",
"created_at": 1612792539,
"updated_at": 1612792539,
"archived_at": 0,
"id": "Wpmbk5ezJn"
}
],
"activities": [],
"ledger": [],
"gateway_tokens": [],
"documents": [],
"system_logs": [],
"created_at": 1612792539,
"updated_at": 1612792565,
"archived_at": 0,
"id": "VolejRejNm",
"isChanged": true,
"is_deleted": false,
"user_id": "VolejRejNm",
"assigned_user_id": ""
};
axios.put('https://localhost:8080/api/v1/company_gateways/VolejRejNm', gatewaysBody, {headers})
axios.put('https://localhost:8080/api/v1/clients/VolejRejNm', clientBody, {headers}); // Set country to US.
cy.viewport('macbook-13');
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout').visit('/client/login');
});
it('should be able to pay using Alipay', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-2]').click();
cy.get('#pay-now').click();
cy.get('.common-ButtonGroup > .common-Button--default').click();
cy.url().should('contain', '/client/payments/');
});
});

View File

@ -1,95 +0,0 @@
describe('Stripe: Credit card testing', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account stripe');
});
beforeEach(() => {
cy.viewport('macbook-13');
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout').visit('/client/login');
});
it('should pay with new card', function () {
cy.visit('/client/invoices')
.get('[data-cy=pay-now]').first().click()
.get('[data-cy=pay-now-dropdown]').click()
.get('[data-cy=pay-with-0]').click();
cy.get('#cardholder-name').type('Invoice Ninja Rocks');
cy.getWithinIframe('[name=cardnumber]').type('4242424242424242');
cy.getWithinIframe('[name=exp-date]').type('04/24');
cy.getWithinIframe('[name=cvc]').type('242');
cy.getWithinIframe('[name=postal]').type('42424');
cy.get('#pay-now').click();
cy.location('pathname', {timeout: 60000}).should('include', '/client/payments/VolejRejNm');
});
it('should pay with new card & save credit card for future use', function () {
cy.visit('/client/invoices')
.get('[data-cy=pay-now]').first().click()
.get('[data-cy=pay-now-dropdown]').click()
.get('[data-cy=pay-with-0]').click();
cy.get('#cardholder-name').type('Invoice Ninja Rocks');
cy.getWithinIframe('[name=cardnumber]').type('4242424242424242');
cy.getWithinIframe('[name=exp-date]').type('04/24');
cy.getWithinIframe('[name=cvc]').type('242');
cy.getWithinIframe('[name=postal]').type('42424');
cy.get('[name=token-billing-checkbox]').first().check();
cy.get('#pay-now').click();
cy.location('pathname', {timeout: 60000}).should('include', '/client/payments/Wpmbk5ezJn');
});
it('should pay with saved card (token)', function () {
cy.visit('/client/invoices')
.get('[data-cy=pay-now]').first().click()
.get('[data-cy=pay-now-dropdown]').click()
.get('[data-cy=pay-with-0]').click();
cy.get('[name=payment-type]').first().check();
cy.get('#pay-now').click();
cy.location('pathname', {timeout: 60000}).should('include', '/client/payments/Opnel5aKBz');
});
it('should be able to remove payment method', function () {
cy.visit('/client/payment_methods')
.get('[data-cy=view-payment-method]').click();
cy.get('#open-delete-popup').click();
cy.get('[data-cy=confirm-payment-removal]').click();
cy.url().should('contain', '/client/payment_methods');
cy.get('body').contains('Payment method has been successfully removed.');
});
it('should be able to add credit card (standalone)', function () {
cy.visit('/client/payment_methods')
.get('[data-cy=add-payment-method]').click()
.get('[data-cy=add-credit-card-link]').click();
cy.get('#cardholder-name').type('Invoice Ninja Rocks');
cy.getWithinIframe('[name=cardnumber]').type('4242424242424242');
cy.getWithinIframe('[name=exp-date]').type('04/24');
cy.getWithinIframe('[name=cvc]').type('242');
cy.getWithinIframe('[name=postal]').type('42424');
cy.get('#authorize-card').click();
cy.location('pathname', {timeout: 60000})
.should('include', '/client/payment_methods')
.get('[data-cy=pm-last4]').contains('**** 4242');
});
});

View File

@ -1,209 +0,0 @@
import axios from "axios";
describe('Stripe: SOFORT testing', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account checkout');
});
beforeEach(() => {
let headers = {
'X-API-Token': 'company-token-test',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json; charset=utf-8',
};
let gatewaysBody = {
"gateway_key": "d14dd26a37cecc30fdd65700bfb55b23",
"accepted_credit_cards": 0,
"require_shipping_address": true,
"require_billing_address": true,
"require_client_name": false,
"require_client_phone": false,
"require_contact_name": false,
"require_contact_email": false,
"require_cvv": true,
"update_details": true,
"fees_and_limits": {
"1": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
},
"2": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
},
"6": {
"min_limit": -1,
"max_limit": -1,
"fee_amount": 0,
"fee_percent": 0,
"fee_cap": 0,
"fee_tax_rate1": 0,
"fee_tax_name1": "",
"fee_tax_rate2": 0,
"fee_tax_name2": "",
"fee_tax_rate3": 0,
"fee_tax_name3": "",
"adjust_fee_percent": false,
"is_enabled": true
}
},
"system_logs": [],
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"config": "{\"apiKey\":\"sk_test_Yorqvz45sZWSSUmvCfoKF8e6\",\"publishableKey\":\"pk_test_P1riKDKD0pdNTkHwBWEZ8DR0\",\"enable_ach\":\"1\",\"enable_sofort\":\"1\",\"enable_apple_pay\":\"0\",\"enable_alipay\":\"0\"}",
"token_billing": "off",
"test_mode": true,
"label": "Stripe",
"created_at": 1612791181,
"updated_at": 1612792176,
"archived_at": 0,
"id": "VolejRejNm",
"loadedAt": 1612792176934,
"require_postal_code": false,
"is_deleted": false
};
let clientBody = {
"group_settings_id": "",
"name": "Batz LLC",
"display_name": "Batz LLC",
"balance": 8323.7,
"credit_balance": 0,
"paid_to_date": 0,
"client_hash": "DxrMypcMdnYJvfebfeoXUi2Iyear6LkNq7Twi0H9",
"address1": "45804",
"address2": "47988 Rex Mall",
"city": "New Macberg",
"state": "Florida",
"postal_code": "43089-5809",
"country_id": "276",
"phone": "",
"private_notes": "Neque libero eos adipisci quae. Non voluptas quaerat ea nisi repudiandae in. Voluptatem error aut est distinctio perspiciatis quis.",
"public_notes": "",
"website": "https://www.wintheiser.com/non-velit-nisi-culpa-sit-optio-omnis-ipsum-pariatur",
"industry_id": "",
"size_id": "",
"vat_number": "157764830",
"id_number": "",
"number": "0001",
"shipping_address1": "5181",
"shipping_address2": "66797 Jedediah Isle Suite 479",
"shipping_city": "Lake Rosariomouth",
"shipping_state": "Nevada",
"shipping_postal_code": "31693",
"shipping_country_id": "4",
"settings": {
"currency_id": "1"
},
"last_login": 0,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"contacts": [
{
"first_name": "Rita",
"last_name": "Pouros",
"email": "user@example.com",
"password": "**********",
"phone": "+1-331-663-8498",
"contact_key": "hNQkBU6RM6tG2pwu4J7dCfuq2ZdH6Q8anEvKnyoL",
"is_primary": true,
"send_email": true,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"last_login": 0,
"link": "https://localhost:8080/client/key_login/hNQkBU6RM6tG2pwu4J7dCfuq2ZdH6Q8anEvKnyoL",
"created_at": 1612792539,
"updated_at": 1612792539,
"archived_at": 0,
"id": "VolejRejNm"
},
{
"first_name": "Danika",
"last_name": "Hauck",
"email": "bbrakus@example.net",
"password": "**********",
"phone": "662-968-5275 x48146",
"contact_key": "4hWqvVUv2bwYIOb25rWmQhbhadnl5yneTzglGZ32",
"is_primary": false,
"send_email": true,
"custom_value1": "",
"custom_value2": "",
"custom_value3": "",
"custom_value4": "",
"last_login": 0,
"link": "https://localhost:8080/client/key_login/4hWqvVUv2bwYIOb25rWmQhbhadnl5yneTzglGZ32",
"created_at": 1612792539,
"updated_at": 1612792539,
"archived_at": 0,
"id": "Wpmbk5ezJn"
}
],
"activities": [],
"ledger": [],
"gateway_tokens": [],
"documents": [],
"system_logs": [],
"created_at": 1612792539,
"updated_at": 1612792565,
"archived_at": 0,
"id": "VolejRejNm",
"isChanged": true,
"is_deleted": false,
"user_id": "VolejRejNm",
"assigned_user_id": ""
};
axios.put('https://localhost:8080/api/v1/company_gateways/VolejRejNm', gatewaysBody, {headers})
axios.put('https://localhost:8080/api/v1/clients/VolejRejNm', clientBody, {headers}); // Set country to US.
cy.viewport('macbook-13');
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout').visit('/client/login');
});
it('should be able to pay using SOFORT', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-2]').click();
cy.get('#pay-now').click();
cy.get('.common-ButtonGroup > .common-Button--default').click();
cy.url().should('contain', '/client/payments/');
});
});

View File

@ -1,24 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
config.ignoreTestFiles = "**/examples/*.spec.js";
return config;
}

View File

@ -1,35 +0,0 @@
import axios from 'axios';
const baseUrl = Cypress.config().baseUrl.endsWith('/')
? Cypress.config().baseUrl.slice(0, -1)
: Cypress.config().baseUrl;
Cypress.Commands.add('createAdminAccount', () => {
let body = {
first_name: "Cypress",
last_name: "Testing",
email: "cypress_testing@example.com",
password: "password",
terms_of_service: true,
privacy_policy: true,
report_errors: true,
};
let headers = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
};
return axios.post(`${baseUrl}/api/v1/signup?first_load=true`, body, headers)
.then(response => {
console.log('Data from the request', response.data.data[0]);
return response.data.data[0];
})
.catch(e => {
throw "Unable to create an account for admin.";
});
});
Cypress.Commands.add('createClientAccount', () => {
// ..
});

View File

@ -1,6 +0,0 @@
Cypress.Commands.add('artisan', (cmd) => {
let environment = Cypress.env('runningEnvironment');
let prefix = environment === 'docker' ? 'docker-compose run --rm artisan' : 'php artisan';
return cy.exec(`${prefix} ${cmd}`);
});

View File

@ -1,108 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
const axios = require('axios');
const fixture = require('../fixtures/example.json');
Cypress.Commands.add('clientLogin', () => {
cy.visit('/client/login');
cy.get('#test_email')
.invoke('val')
.then((emailValue) => {
cy.get('#test_password')
.invoke('val')
.then((passwordValue) => {
cy.get('#email')
.type(emailValue)
.should('have.value', emailValue);
cy.get('#password')
.type(passwordValue)
.should('have.value', passwordValue);
cy.get('#loginBtn')
.contains('Login')
.click();
});
});
});
Cypress.Commands.add('iframeLoaded', {prevSubject: 'element'}, ($iframe) => {
const contentWindow = $iframe.prop('contentWindow');
return new Promise((resolve) => {
if (contentWindow) {
resolve(contentWindow);
} else {
$iframe.on('load', () => {
resolve(contentWindow);
});
}
});
});
Cypress.Commands.add(
'getInDocument',
{
prevSubject:
'Permission denied to access property "document" on cross-origin object',
},
(document, selector) => Cypress.$(selector, document)
);
Cypress.Commands.add('getWithinIframe', (targetElement) =>
cy
.get('iframe')
.iframeLoaded()
.its('document')
.getInDocument(targetElement)
);
Cypress.Commands.add('useGateway', (gateway) => {
let body = {
settings: {
entity: 'App\\Models\\Client',
industry_id: '',
size_id: '',
currency_id: '1',
company_gateway_ids: gateway,
},
};
let options = {
headers: {
'X-Api-Secret': 'superdoopersecrethere',
'X-Api-Token':
'S0x8behDk8HG8PI0i8RXdpf2AVud5b993pE8vata7xmm4RgW6u3NeGC8ibWIUjZv',
'X-Requested-With': 'XMLHttpRequest',
},
};
axios
.put(
`http://localhost:8000/api/v1/clients/${fixture.first}`,
body,
options
)
.then((response) => console.log(response))
.catch((error) => console.log(error.message));
});

View File

@ -1,22 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import './artisan';
import './account';
// Alternatively you can use CommonJS syntax:
// require('./commands')

64
dusk.sh Normal file
View File

@ -0,0 +1,64 @@
#!/bin/bash
n=1
TYPE=${!n}
if [ -z "$TYPE" ]; then
TYPE="all"
fi
echo "$ RUNNING: '$TYPE'"
echo "$ php artisan optimize"
php artisan optimize
echo "=========================================="
GENERIC_TESTS=`find tests/Browser/ClientPortal/ -maxdepth 1 -type f -name '*.php'`
if [ $TYPE == 'gateways' ]; then
GENERIC_TESTS=""
echo "$ Skippping generic tests."
echo "=========================================="
fi
for TEST_CLASS in $GENERIC_TESTS; do
echo "Test class: $TEST_CLASS"
echo "$ php artisan migrate:fresh --seed"
php artisan migrate:fresh --seed &> /dev/null
echo "$ php artisan ninja:create-single-account"
php artisan ninja:create-single-account &> /dev/null
echo "$ php artisan dusk $TEST_CLASS"
php -d memory_limit=1G artisan dusk ${@:2} --stop-on-error --stop-on-failure $TEST_CLASS || exit 1
echo "=========================================="
done || exit 1
GATEWAY_TESTS=`find tests/Browser/ClientPortal/Gateways/ -type f -name '*.php'`
if [ $TYPE == 'generic' ]; then
GATEWAY_TESTS=""
echo "$ Skippping gateway tests."
echo "=========================================="
fi
for TEST_CLASS in $GATEWAY_TESTS; do
echo "Test class: $TEST_CLASS"
echo "$ php artisan migrate:fresh --seed"
php artisan migrate:fresh --seed &> /dev/null
echo "$ php artisan ninja:create-single-account"
php artisan ninja:create-single-account &> /dev/null
echo "$ php artisan dusk $TEST_CLASS"
php -d memory_limit=1G artisan dusk ${@:2} --stop-on-error --stop-on-failure $TEST_CLASS || exit 1
echo "=========================================="
done || exit 1
echo 'All tests completed successfully.'

1059
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,14 +7,11 @@
"watch-poll": "npm run watch -- --watch-poll", "watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production", "prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
"test": "cypress run",
"test:gui": "cypress open"
}, },
"devDependencies": { "devDependencies": {
"@babel/compat-data": "7.9.0", "@babel/compat-data": "7.9.0",
"@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-class-properties": "^7.13.0",
"cypress": "^4.12.1",
"laravel-mix-purgecss": "^5.0.0", "laravel-mix-purgecss": "^5.0.0",
"vue-template-compiler": "^2.6.12" "vue-template-compiler": "^2.6.12"
}, },

View File

@ -23,7 +23,10 @@
<div class="rounded bg-white shadow-xs"> <div class="rounded bg-white shadow-xs">
<div class="py-1"> <div class="py-1">
@foreach($multiple_contacts as $contact) @foreach($multiple_contacts as $contact)
<a data-turbolinks="false" href="{{ route('client.switch_company', $contact->hashed_id) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ $contact->company->present()->name() }} - {{ $contact->client->present()->name()}}</a> <a data-turbolinks="false"
href="{{ route('client.switch_company', $contact->hashed_id) }}"
class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ $contact->company->present()->name() }}
- {{ $contact->client->present()->name()}}</a>
@endforeach @endforeach
</div> </div>
</div> </div>
@ -32,18 +35,28 @@
@endif @endif
<div @click.away="open = false" class="ml-3 relative" x-data="{ open: false }"> <div @click.away="open = false" class="ml-3 relative" x-data="{ open: false }">
<div> <div>
<button @click="open = !open" class="max-w-xs flex items-center text-sm rounded-full focus:outline-none focus:shadow-outline"> <button data-ref="client-profile-dropdown" @click="open = !open"
<img class="h-8 w-8 rounded-full" src="{{ auth()->user()->avatar() }}" alt="" /> class="max-w-xs flex items-center text-sm rounded-full focus:outline-none focus:shadow-outline">
<img class="h-8 w-8 rounded-full" src="{{ auth()->user()->avatar() }}" alt=""/>
<span class="ml-2">{{ auth()->user()->present()->name() }}</span> <span class="ml-2">{{ auth()->user()->present()->name() }}</span>
</button> </button>
</div> </div>
<div x-show="open" style="display:none;" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg"> <div x-show="open" style="display:none;" x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg">
<div class="py-1 rounded-md bg-white shadow-xs"> <div class="py-1 rounded-md bg-white shadow-xs">
<a href="{{ route('client.profile.edit', ['client_contact' => auth()->user()->hashed_id]) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"> <a data-ref="client-profile-dropdown-settings"
href="{{ route('client.profile.edit', ['client_contact' => auth()->user()->hashed_id]) }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.profile') }} {{ ctrans('texts.profile') }}
</a> </a>
<a href="{{ route('client.logout') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"> <a href="{{ route('client.logout') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.logout') }} {{ ctrans('texts.logout') }}
</a> </a>
</div> </div>

View File

@ -99,7 +99,7 @@
@csrf @csrf
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}"> <input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="action" value="payment"> <input type="hidden" name="action" value="payment">
<button class="px-2 py-1 mr-3 text-xs uppercase button button-primary bg-primary" data-cy="pay-now"> <button class="px-2 py-1 mr-3 text-xs uppercase button button-primary bg-primary" dusk="pay-now">
{{ ctrans('texts.pay_now') }} {{ ctrans('texts.pay_now') }}
</button> </button>
</form> </form>

View File

@ -1,10 +1,10 @@
<div> <div>
@unless(count($methods) == 0) @unless(count($methods) == 0)
<div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false" <div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false"
class="relative inline-block text-left" data-cy="payment-methods-dropdown"> class="relative inline-block text-left" dusk="payment-methods-dropdown">
<div> <div>
<div class="rounded-md shadow-sm"> <div class="rounded-md shadow-sm">
<button data-cy="pay-now-dropdown" @click="open = !open" type="button" <button dusk="pay-now-dropdown" @click="open = !open" type="button"
class="button button-primary bg-primary hover:bg-primary-darken inline-flex items-center"> class="button button-primary bg-primary hover:bg-primary-darken inline-flex items-center">
{{ ctrans('texts.pay_now') }} {{ ctrans('texts.pay_now') }}
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20">
@ -20,19 +20,19 @@
<div class="py-1"> <div class="py-1">
@foreach($methods as $index => $method) @foreach($methods as $index => $method)
@if($method['label'] == 'Custom') @if($method['label'] == 'Custom')
<a href="#" @click="{ open = false }" data-cy="pay-with-custom" <a href="#" @click="{ open = false }" dusk="pay-with-custom"
data-company-gateway-id="{{ $method['company_gateway_id'] }}" data-company-gateway-id="{{ $method['company_gateway_id'] }}"
data-gateway-type-id="{{ $method['gateway_type_id'] }}" data-gateway-type-id="{{ $method['gateway_type_id'] }}"
class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
data-cy="payment-method"> dusk="payment-method">
{{ \App\Models\CompanyGateway::find($method['company_gateway_id'])->firstOrFail()->getConfigField('name') }} {{ \App\Models\CompanyGateway::find($method['company_gateway_id'])->firstOrFail()->getConfigField('name') }}
</a> </a>
@elseif($total > 0) @elseif($total > 0)
<a href="#" @click="{ open = false }" data-cy="pay-with-{{ $index }}" <a href="#" @click="{ open = false }" dusk="pay-with-{{ $index }}"
data-company-gateway-id="{{ $method['company_gateway_id'] }}" data-company-gateway-id="{{ $method['company_gateway_id'] }}"
data-gateway-type-id="{{ $method['gateway_type_id'] }}" data-gateway-type-id="{{ $method['gateway_type_id'] }}"
class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
data-cy="payment-method"> dusk="payment-method">
{{ $method['label'] }} {{ $method['label'] }}
</a> </a>
@endif @endif

View File

@ -11,11 +11,11 @@
<input type="hidden" name="source" value="{{ $token->token }}"> <input type="hidden" name="source" value="{{ $token->token }}">
@component('portal.ninja2020.components.general.card-element', ['title' => '#1 ' . ctrans('texts.amount_cents')]) @component('portal.ninja2020.components.general.card-element', ['title' => '#1 ' . ctrans('texts.amount_cents')])
<input type="text" name="transactions[]" class="w-full input" required data-cy="verification-1st"> <input type="text" name="transactions[]" class="w-full input" required dusk="verification-1st">
@endcomponent @endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => '#2 ' . ctrans('texts.amount_cents')]) @component('portal.ninja2020.components.general.card-element', ['title' => '#2 ' . ctrans('texts.amount_cents')])
<input type="text" name="transactions[]" class="w-full input" required data-cy="verification-2nd"> <input type="text" name="transactions[]" class="w-full input" required dusk="verification-2nd">
@endcomponent @endcomponent
@component('portal.ninja2020.gateways.includes.pay_now', ['type' => 'submit']) @component('portal.ninja2020.gateways.includes.pay_now', ['type' => 'submit'])

View File

@ -100,6 +100,7 @@
type="text" type="text"
class="input mt-0 mr-4 relative" class="input mt-0 mr-4 relative"
name="payable_invoices[{{$key}}][amount]" name="payable_invoices[{{$key}}][amount]"
dusk="underpayment-input"
value="{{ $invoice->partial > 0 ? $invoice->partial : $invoice->balance }}"/> value="{{ $invoice->partial > 0 ? $invoice->partial : $invoice->balance }}"/>
</label> </label>
</div> </div>

View File

@ -36,7 +36,7 @@
<form action="{{ route('client.payment_methods.destroy', [$payment_method->hashed_id, 'method' => $payment_method->gateway_type->id]) }}" method="post"> <form action="{{ route('client.payment_methods.destroy', [$payment_method->hashed_id, 'method' => $payment_method->gateway_type->id]) }}" method="post">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="button button-danger button-block" data-cy="confirm-payment-removal"> <button type="submit" class="button button-danger button-block" dusk="confirm-payment-removal">
{{ ctrans('texts.remove') }} {{ ctrans('texts.remove') }}
</button> </button>
</form> </form>

View File

@ -7,7 +7,7 @@
</div> <!-- End of left-side --> </div> <!-- End of left-side -->
<div class="mt-5 md:mt-0 md:col-span-2"> <div class="mt-5 md:mt-0 md:col-span-2">
<form wire:submit.prevent="submit"> <form wire:submit.prevent="submit" id="update_client">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="shadow overflow-hidden rounded"> <div class="shadow overflow-hidden rounded">
@ -15,7 +15,9 @@
<div class="grid grid-cols-6 gap-6"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="first_name" class="input-label">@lang('texts.first_name')</label> <label for="first_name" class="input-label">@lang('texts.first_name')</label>
<input id="first_name" class="input w-full {{ in_array('contact_first_name', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" name="first_name" wire:model.defer="first_name" /> <input id="contact_first_name"
class="input w-full {{ in_array('contact_first_name', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="first_name" wire:model.defer="first_name"/>
@error('first_name') @error('first_name')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -25,7 +27,9 @@
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="last_name" class="input-label">@lang('texts.last_name')</label> <label for="last_name" class="input-label">@lang('texts.last_name')</label>
<input id="last_name" class="input w-full {{ in_array('contact_last_name', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" name="last_name" wire:model.defer="last_name" /> <input id="contact_last_name"
class="input w-full {{ in_array('contact_last_name', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="last_name" wire:model.defer="last_name"/>
@error('last_name') @error('last_name')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -35,7 +39,9 @@
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<label for="email_address" class="input-label">@lang('texts.email_address')</label> <label for="email_address" class="input-label">@lang('texts.email_address')</label>
<input id="email_address" class="input w-full {{ in_array('contact_email', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" type="email" name="email" wire:model.defer="email" /> <input id="contact_email_address"
class="input w-full {{ in_array('contact_email', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
type="email" name="email" wire:model.defer="email"/>
@error('email') @error('email')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -44,8 +50,9 @@
</div> </div>
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<label for="phone" class="input-label">@lang('texts.phone')</label> <label for="contact_phone" class="input-label">@lang('texts.phone')</label>
<input id="phone" class="input w-full" name="phone" wire:model.defer="phone" /> <input id="contact_phone" class="input w-full" name="phone"
wire:model.defer="phone"/>
@error('phone') @error('phone')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -54,8 +61,9 @@
</div> </div>
<div class="col-span-6 sm:col-span-6 lg:col-span-3"> <div class="col-span-6 sm:col-span-6 lg:col-span-3">
<label for="password" class="input-label">@lang('texts.password')</label> <label for="contact_password" class="input-label">@lang('texts.password')</label>
<input id="password" class="input w-full" name="password" wire:model.defer="password" type="password" /> <input id="contact_password" class="input w-full" name="password"
wire:model.defer="password" type="password"/>
@error('password') @error('password')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -64,8 +72,11 @@
</div> </div>
<div class="col-span-6 sm:col-span-3 lg:col-span-3"> <div class="col-span-6 sm:col-span-3 lg:col-span-3">
<label for="state" class="input-label">@lang('texts.confirm_password')</label> <label for="contact_password_confirmation"
<input id="state" class="input w-full" name="password_confirmation" wire:model.defer="password_confirmation" type="password" /> class="input-label">@lang('texts.confirm_password')</label>
<input id="contact_password_confirmation" class="input w-full"
name="password_confirmation"
wire:model.defer="password_confirmation" type="password"/>
@error('password_confirmation') @error('password_confirmation')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -75,7 +86,8 @@
</div> </div>
</div> </div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button class="button button-primary bg-primary">{{ $saved }}</button> <button data-ref="update-contact-details"
class="button button-primary bg-primary">{{ $saved }}</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -13,8 +13,8 @@
<div class="px-4 py-5 bg-white sm:p-6"> <div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="street" class="input-label">{{ ctrans('texts.name') }}</label> <label for="client_name" class="input-label">{{ ctrans('texts.name') }}</label>
<input id="name" class="input w-full" name="name" wire:model.defer="name"/> <input id="client_name" class="input w-full" name="name" wire:model.defer="name"/>
@error('name') @error('name')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -23,8 +23,10 @@
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="street" class="input-label">{{ ctrans('texts.vat_number') }}</label> <label for="client_vat_number"
<input id="vat_number" class="input w-full" name="vat_number" wire:model.defer="vat_number"/> class="input-label">{{ ctrans('texts.vat_number') }}</label>
<input id="client_vat_number" class="input w-full" name="vat_number"
wire:model.defer="vat_number"/>
@error('vat_number') @error('vat_number')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -33,8 +35,8 @@
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="street" class="input-label">{{ ctrans('texts.phone') }}</label> <label for="client_phone" class="input-label">{{ ctrans('texts.phone') }}</label>
<input id="phone" class="input w-full" name="phone" wire:model.defer="phone"/> <input id="client_phone" class="input w-full" name="phone" wire:model.defer="phone"/>
@error('phone') @error('phone')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}
@ -44,10 +46,12 @@
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<label for="website" class="input-label">{{ ctrans('texts.website') }}</label> <label for="client_website"
class="input-label">{{ ctrans('texts.website') }}</label>
<span class="text-xs ml-2 text-gray-600">E.g. https://example.com</span> <span class="text-xs ml-2 text-gray-600">E.g. https://example.com</span>
</div> </div>
<input id="website" class="input w-full" name="website" wire:model.defer="website"/> <input id="client_website" class="input w-full" name="website"
wire:model.defer="website"/>
@error('website') @error('website')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}

View File

@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="mt-5 md:mt-0 md:col-span-2"> <div class="mt-5 md:mt-0 md:col-span-2">
<form wire:submit.prevent="submit" method="POST" id="update_contact"> <form wire:submit.prevent="submit" method="POST" id="update_billing_address">
@csrf @csrf
<div class="px-4 py-5 bg-white sm:p-6"> <div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6"> <div class="grid grid-cols-6 gap-6">

View File

@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="mt-5 md:mt-0 md:col-span-2"> <div class="mt-5 md:mt-0 md:col-span-2">
<form wire:submit.prevent="submit" method="POST" id="update_contact"> <form wire:submit.prevent="submit" method="POST" id="update_shipping_address">
@csrf @csrf
<div class="shadow overflow-hidden rounded"> <div class="shadow overflow-hidden rounded">
<div class="px-4 py-5 bg-white sm:p-6"> <div class="px-4 py-5 bg-white sm:p-6">

View File

@ -0,0 +1,45 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class CreditsTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.credits.index')
->assertSeeIn('span[data-ref="meta-title"]', 'Credits')
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class DocumentsTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.documents.index')
->assertSee('Documents')
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\AuthorizeNet;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class CreditCardTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
if (getenv('GITHUB_ACTIONS')) {
$this->markTestSkipped('Skipping Authorize.net (GitHub Actions)');
}
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', '3b6621f970ab18887c4f6dca78d3f8bb')->restore();
}
public function testPayWithNewCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->type('card-number', '4007000000027')
->type('card-holders-name', 'John Doe')
->type('.expiry', '12/28')
->type('cvc', '100')
->press('Pay Now')
->waitForText('Details of the payment', 60);
});
}
public function testPayWithNewCardAndSaveForFutureUse()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->radio('#proxy_is_default', true)
->type('card-number', '4007000000027')
->type('card-holders-name', 'John Doe')
->type('.expiry', '12/28')
->type('cvc', '100')
->press('Pay Now')
->waitForText('Details of the payment', 60)
->visitRoute('client.payment_methods.index')
->clickLink('View')
->assertSee('0027');
});
}
public function testPayWithSavedCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->click('.toggle-payment-with-token')
->press('Pay Now')
->waitForText('Details of the payment', 60);
});
}
public function testRemoveCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->clickLink('View')
->press('Remove Payment Method')
->waitForText('Confirmation')
->click('@confirm-payment-removal')
->assertSee('Payment method has been successfully removed.');
});
}
public function testAddingCreditCardStandalone()
{
$this->markTestIncomplete("E00117 OTS Service Error 'Field validation error.'");
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->press('Add Payment Method')
->clickLink('Credit Card')
->type('card-number', '4012888818888')
->type('card-holders-name', 'John Doe')
->type('.expiry', '12/28')
->type('cvc', '900')
->press('Add Payment Method')
->waitForText('0027', 60);
});
}
}

View File

@ -0,0 +1,146 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\Braintree;
use App\DataMapper\FeesAndLimits;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class CreditCardTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', 'f7ec488676d310683fb51802d076d713')->restore();
$cg = CompanyGateway::where('gateway_key', 'f7ec488676d310683fb51802d076d713')->firstOrFail();
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits();
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
$company = Company::first();
$settings = $company->settings;
$settings->client_portal_allow_under_payment = true;
$settings->client_portal_allow_over_payment = true;
$company->settings = $settings;
$company->save();
}
public function testPayWithNewCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->type('@underpayment-input', '100')
->press('Pay Now')
->clickLink('Credit Card')
->waitFor('#braintree-hosted-field-number', 60)
->withinFrame('#braintree-hosted-field-number', function (Browser $browser) {
$browser->type('credit-card-number', '4111111111111111');
})
->withinFrame('#braintree-hosted-field-expirationDate', function (Browser $browser) {
$browser->type('expiration', '04/25');
})
->press('Pay Now')
->waitForText('Details of the payment', 60);
});
}
public function testPayWithNewCardAndSaveForFuture()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->type('@underpayment-input', '100')
->press('Pay Now')
->clickLink('Credit Card')
->waitFor('#braintree-hosted-field-number', 60)
->withinFrame('#braintree-hosted-field-number', function (Browser $browser) {
$browser->typeSlowly('credit-card-number', '4005519200000004');
})
->withinFrame('#braintree-hosted-field-expirationDate', function (Browser $browser) {
$browser->typeSlowly('expiration', '04/25');
})
->radio('#proxy_is_default', true)
->press('Pay Now')
->waitForText('Details of the payment', 60)
->visitRoute('client.payment_methods.index')
->clickLink('View')
->assertSee('0004');
});
}
public function testPayWithSavedCard()
{
$this->markTestSkipped('Works in "real" browser, otherwise giving error code 0.');
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->type('@underpayment-input', '100')
->press('Pay Now')
->clickLink('Credit Card')
->click('.toggle-payment-with-token')
->click('#pay-now-with-token')
->waitForText('Details of the payment', 60);
});
}
public function testRemoveCreditCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->clickLink('View')
->press('Remove Payment Method')
->waitForText('Confirmation')
->click('@confirm-payment-removal')
->assertSee('Payment method has been successfully removed.');
});
}
public function testAddingPaymentMethodShouldntBePossible()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->press('Add Payment Method')
->clickLink('Credit Card')
->assertSee('This payment method can be can saved for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.');
});
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\Braintree;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class PayPalTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', 'f7ec488676d310683fb51802d076d713')->restore();
}
public function testOffsitePayment()
{
$this->markTestSkipped('Sometimes after redirect PayPal shows the register-like page with credit card, sometimes is login page.');
}
}

View File

@ -0,0 +1,120 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\CheckoutCom;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class CreditCardTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', '3758e7f7c6f4cecf0f4f348b9a00f456')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testAddingPaymentMethodShouldntBePossible()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->press('Add Payment Method')
->clickLink('Credit Card')
->assertSee('Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.');
});
}
public function testPayWithNewCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->withinFrame('iframe', function (Browser $browser) {
$browser
->type('cardnumber', '4242424242424242')
->type('exp-date', '04/22')
->type('cvc', '100');
})
->press('#pay-button')
->waitForText('Details of the payment', 60);
});
}
public function testPayWithNewCardAndSaveForFutureUse()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->withinFrame('iframe', function (Browser $browser) {
$browser
->type('cardnumber', '4242424242424242')
->type('exp-date', '04/22')
->type('cvc', '100');
})
->radio('#proxy_is_default', true)
->press('#pay-button')
->waitForText('Details of the payment', 60)
->visitRoute('client.payment_methods.index')
->clickLink('View')
->assertSee('4242');
});
}
public function testPayWithSavedCreditCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->click('.toggle-payment-with-token')
->click('#pay-now-with-token')
->waitForText('Details of the payment', 60);
});
}
public function testRemoveCreditCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->clickLink('View')
->press('Remove Payment Method')
->waitForText('Confirmation')
->click('@confirm-payment-removal')
->assertSee('Payment method has been successfully removed.');
});
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\PayPal;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class PayPalTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testOffsitePayment()
{
$this->markTestSkipped('Sometimes after redirect PayPal shows the register-like page with credit card, sometimes is login page.');
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\Stripe;
use App\DataMapper\FeesAndLimits;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class ACHTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
$this->disableCompanyGateways();
// Enable Stripe.
CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->restore();
// Enable ACH.
$cg = CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->firstOrFail();
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits();
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
// ACH required US to be billing country.
$client = Client::first();
$client->country_id = 840;
$client->save();
}
public function testAddingACHAccountAndVerifyingIt()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->press('Add Payment Method')
->clickLink('Bank Account')
->type('#account-holder-name', 'John Doe')
->select('#country', 'US')
->select('#currency', 'USD')
->type('#routing-number', '110000000')
->type('#account-number', '000123456789')
->check('#accept-terms')
->press('Add Payment Method')
->waitForText('ACH (Verification)', 60)
->type('@verification-1st', '32')
->type('@verification-2nd', '45')
->press('Complete Verification')
->assertSee('Verification completed successfully')
->assertSee('Bank Transfer');
});
}
public function testPayingWithExistingACH()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Bank Transfer')
->click('.toggle-payment-with-token')
->press('Pay Now')
->waitForText('Details of the payment', 60);
});
}
public function testRemoveACHAccount()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->clickLink('View')
->press('Remove Payment Method')
->waitForText('Confirmation')
->click('@confirm-payment-removal')
->assertSee('Payment method has been successfully removed.');
});
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\Stripe;
use App\DataMapper\FeesAndLimits;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class AlipayTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
$this->disableCompanyGateways();
// Enable Stripe.
CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->restore();
// Enable Alipay.
$cg = CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->firstOrFail();
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::ALIPAY} = new FeesAndLimits();
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
// Setting country to DEU (276).
$client = Client::first();
$client->country_id = 276;
$client->save();
}
public function testPayingWithAlipay()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Alipay')
->press('Pay Now')
->waitForText('Alipay test payment page', 120)
->press('.common-Button.common-Button--default')
->waitForText('Details of the payment', 60);
});
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\Stripe;
use App\DataMapper\FeesAndLimits;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class CreditCardTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
$this->disableCompanyGateways();
// Enable Stripe.
CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->restore();
$cg = CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->firstOrFail();
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits();
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
public function testPaymentWithNewCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->click('@pay-now-dropdown')
->clickLink('Credit Card')
->type('#cardholder-name', 'John Doe')
->withinFrame('iframe', function (Browser $browser) {
$browser
->type('cardnumber', '4242 4242 4242 4242')
->type('exp-date', '04/22')
->type('cvc', '242');
})
->click('#pay-now')
->waitForText('Details of the payment', 60);
});
}
public function testPayWithNewCardAndSaveForFutureUse()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->click('@pay-now-dropdown')
->clickLink('Credit Card')
->type('#cardholder-name', 'John Doe')
->withinFrame('iframe', function (Browser $browser) {
$browser
->type('cardnumber', '4242 4242 4242 4242')
->type('exp-date', '04/22')
->type('cvc', '242');
})
->radio('#proxy_is_default', true)
->click('#pay-now')
->waitForText('Details of the payment', 60)
->visitRoute('client.payment_methods.index')
->clickLink('View')
->assertSee('4242');
});
}
public function testPayWithSavedCreditCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->click('@pay-now-dropdown')
->clickLink('Credit Card')
->click('.toggle-payment-with-token')
->click('#pay-now')
->waitForText('Details of the payment', 60);
});
}
public function testRemoveCreditCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->clickLink('View')
->press('Remove Payment Method')
->waitForText('Confirmation')
->click('@confirm-payment-removal')
->assertSee('Payment method has been successfully removed.');
});
}
public function testAddingCreditCardStandalone()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->press('Add Payment Method')
->clickLink('Credit Card')
->type('#cardholder-name', 'John Doe')
->withinFrame('iframe', function (Browser $browser) {
$browser
->type('cardnumber', '4242 4242 4242 4242')
->type('exp-date', '04/22')
->type('cvc', '242');
})
->press('Add Payment Method')
->waitForText('**** 4242');
});
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\Stripe;
use App\DataMapper\FeesAndLimits;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class SofortTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
$this->disableCompanyGateways();
// Enable Stripe.
CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->restore();
// Enable SOFORT.
$cg = CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->firstOrFail();
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::SOFORT} = new FeesAndLimits();
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
// SOFORT required ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'] to be billing country.
// Setting country to DEU (276).
$client = Client::first();
$client->country_id = 276;
$client->save();
}
public function testPayingWithSofort()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Sofort')
->press('Pay Now')
->waitForText('Sofort test payment page', 120)
->press('.common-Button.common-Button--default')
->waitForText('Details of the payment', 60);
});
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\WePay;
use App\DataMapper\FeesAndLimits;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class ACHTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', '8fdeed552015b3c7b44ed6c8ebd9e992')->restore();
// Enable ACH.
$cg = CompanyGateway::where('gateway_key', '8fdeed552015b3c7b44ed6c8ebd9e992')->firstOrFail();
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits();
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPayingWithNoPreauthorizedIsntPossible()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Bank Transfer')
->assertSee('To pay with a bank account, first you have to add it as payment method.');
});
}
}

View File

@ -0,0 +1,123 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\WePay;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class CreditCardTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', '8fdeed552015b3c7b44ed6c8ebd9e992')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPayWithNewCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->type('card-number', '4003830171874018')
->type('card-holders-name', 'John Doe')
->type('.expiry', '12/28')
->type('cvc', '100')
->press('Pay Now')
->waitForText('Details of the payment', 60);
});
}
public function testPayWithNewCardAndSaveForFutureUse()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->type('card-number', '4003830171874018')
->type('card-holders-name', 'John Doe')
->type('.expiry', '12/28')
->type('cvc', '100')
->radio('#proxy_is_default', true)
->press('Pay Now')
->waitForText('Details of the payment', 60)
->visitRoute('client.payment_methods.index')
->clickLink('View')
->assertSee('4018');
});
}
public function testPayWithSavedCreditCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->click('.toggle-payment-with-token')
->press('Pay Now')
->waitForText('Details of the payment', 60);
});
}
public function testRemoveCreditCard()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->clickLink('View')
->press('Remove Payment Method')
->waitForText('Confirmation')
->click('@confirm-payment-removal')
->assertSee('Payment method has been successfully removed.');
});
}
public function testAddingCreditCardStandalone()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->press('Add Payment Method')
->clickLink('Credit Card')
->waitForText('Credit Card')
->type('#cardholder_name', 'John Doe')
->type('card-number', '4003830171874018')
->type('card-holders-name', 'John Doe')
->type('.expiry', '12/28')
->type('cvc', '100')
->press('Add Payment Method')
->waitForText(4018, 60);
});
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class InvoicesTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->assertSeeIn('span[data-ref="meta-title"]', 'Invoices')
->visitRoute('client.logout');
});
}
public function testClickingPayNowWithoutInvoices()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->press('Pay Now')
->assertSee('No payable invoices selected. Make sure you are not trying to pay draft invoice or invoice with zero balance due.')
->visitRoute('client.logout');
});
}
public function testClickingDownloadWithoutInvoices()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->press('Download')
->assertSee('No items selected.')
->visitRoute('client.logout');
});
}
public function testCheckingInvoiceAndClickingPayNow()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->check('.form-check.form-check-child')
->press('Pay Now')
->assertPathIs('/client/invoices/payment')
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class LoginTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
}
public function testLoginPage()
{
$this->browse(function (Browser $browser) {
$browser
->visit(route('client.login'))
->assertSee('Client Portal')
->type('email', 'user@example.com')
->type('password', 'password')
->press('Login');
$browser->assertPathIs('/client/invoices');
});
}
public function testLoginFormValidation()
{
$this->browse(function (Browser $browser) {
$browser
->visit(route('client.login'))
->press('Login')
->assertSee('The email field is required.')
->assertSee('The password field is required.');
});
}
public function testForgotPasswordLink()
{
$this->browse(function (Browser $browser) {
$browser
->visit(route('client.login'))
->assertSeeLink('Forgot your password?')
->clickLink('Forgot your password?')
->assertPathIs('/client/password/reset');
});
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class PaymentMethodsTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payment_methods.index')
->assertSeeIn('span[data-ref="meta-title"]', 'Payment Methods')
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class PaymentsTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.payments.index')
->assertSeeIn('span[data-ref="meta-title"]', 'Payments')
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,210 @@
<?php
namespace Tests\Browser\ClientPortal;
use Faker\Factory;
use Faker\Generator;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class ProfileSettingsTest extends DuskTestCase
{
/** @var Generator */
public $faker;
protected function setUp(): void
{
parent::setUp();
$this->faker = Factory::create();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('button[data-ref="client-profile-dropdown"]')
->click('a[data-ref="client-profile-dropdown-settings"]')
->waitForText('Client Information')
->assertSeeIn('span[data-ref="meta-title"]', 'Client Information')
->visitRoute('client.logout');
});
}
public function testClientDetailsUpdate()
{
$original = [
'name' => $this->faker->name,
'vat_number' => (string)$this->faker->randomNumber(6),
'phone' => $this->faker->phoneNumber,
'website' => $this->faker->url,
];
$this->browse(function (Browser $browser) use ($original) {
$browser
->visitRoute('client.invoices.index')
->click('button[data-ref="client-profile-dropdown"]')
->click('a[data-ref="client-profile-dropdown-settings"]')
->waitForText('Client Information');
$browser
->with('#update_contact', function (Browser $form) use ($original) {
$form
->type('#client_name', $original['name'])
->type('#client_vat_number', $original['vat_number'])
->type('#client_phone', $original['phone'])
->type('#client_website', $original['website'])
->press('Save');
})
->pause(2000)
->refresh();
$updated = [
'name' => $browser->value('#client_name'),
'vat_number' => $browser->value('#client_vat_number'),
'phone' => $browser->value('#client_phone'),
'website' => $browser->value('#client_website')
];
$this->assertSame($original, $updated);
});
}
public function testContactDetailsUpdate()
{
$original = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email_address' => 'user@example.com',
'phone' => $this->faker->phoneNumber,
];
$this->browse(function (Browser $browser) use ($original) {
$browser
->visitRoute('client.invoices.index')
->click('button[data-ref="client-profile-dropdown"]')
->click('a[data-ref="client-profile-dropdown-settings"]')
->waitForText('Client Information');
$browser
->with('#update_client', function (Browser $form) use ($original) {
$form
->type('#contact_first_name', $original['first_name'])
->type('#contact_last_name', $original['last_name'])
->scrollIntoView('#contact_email_address')
->type('#contact_email_address', $original['email_address'])
->type('#contact_phone', $original['phone'])
->click('button[data-ref="update-contact-details"]');
})
->pause(2000)
->refresh();
$updated = [
'first_name' => $browser->value('#contact_first_name'),
'last_name' => $browser->value('#contact_last_name'),
'email_address' => $browser->value('#contact_email_address'),
'phone' => $browser->value('#contact_phone'),
];
$this->assertSame($original, $updated);
});
}
public function testBillingAddressUpdate()
{
$original = [
'street' => $this->faker->streetName,
'apt' => $this->faker->streetAddress,
'city' => $this->faker->city,
'state' => $this->faker->state,
'postal_code' => $this->faker->postcode,
];
$this->browse(function (Browser $browser) use ($original) {
$browser
->visitRoute('client.invoices.index')
->click('button[data-ref="client-profile-dropdown"]')
->click('a[data-ref="client-profile-dropdown-settings"]')
->waitForText('Client Information');
$browser
->with('#update_billing_address', function (Browser $form) use ($original) {
$form
->type('#address1', $original['street'])
->type('#address2', $original['apt'])
->type('#city', $original['city'])
->type('#state', $original['state'])
->type('#postal_code', $original['postal_code'])
->select('#country')
->press('Save');
})
->pause(1000)
->refresh();
$updated = [
'street' => $browser->value('#address1'),
'apt' => $browser->value('#address2'),
'city' => $browser->value('#city'),
'state' => $browser->value('#state'),
'postal_code' => $browser->value('#postal_code'),
];
$this->assertSame($original, $updated);
});
}
public function testShippingAddressUpdate()
{
$original = [
'street' => $this->faker->streetName,
'apt' => $this->faker->streetAddress,
'city' => $this->faker->city,
'state' => $this->faker->state,
'postal_code' => $this->faker->postcode,
];
$this->browse(function (Browser $browser) use ($original) {
$browser
->visitRoute('client.invoices.index')
->click('button[data-ref="client-profile-dropdown"]')
->click('a[data-ref="client-profile-dropdown-settings"]')
->waitForText('Client Information');
$browser
->with('#update_shipping_address', function (Browser $form) use ($original) {
$form
->type('#shipping_address1', $original['street'])
->type('#shipping_address2', $original['apt'])
->type('#shipping_city', $original['city'])
->type('#shipping_state', $original['state'])
->type('#shipping_postal_code', $original['postal_code'])
->select('#shipping_country')
->press('Save');
})
->pause(1000)
->refresh();
$updated = [
'street' => $browser->value('#shipping_address1'),
'apt' => $browser->value('#shipping_address2'),
'city' => $browser->value('#shipping_city'),
'state' => $browser->value('#shipping_state'),
'postal_code' => $browser->value('#shipping_postal_code'),
];
$this->assertSame($original, $updated);
});
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use App\Models\Quote;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class QuotesTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.quotes.index')
->assertSeeIn('span[data-ref="meta-title"]', 'Quotes')
->visitRoute('client.logout');
});
}
public function testClickingApproveWithoutQuotesDoesntWork()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.quotes.index')
->press('Approve')
->assertPathIs('/client/quotes');
});
}
public function testApprovingQuotes()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.quotes.index')
->check('.form-check.form-check-child')
->press('Approve')
->assertPathIs('/client/quotes/approve')
->press('Approve')
->assertPathIs('/client/quotes')
->assertSee('Quote(s) approved successfully.')
->visitRoute('client.logout');
});
}
public function testQuotesWithSentStatusCanOnlyBeApproved()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.quotes.index')
->check('.form-check.form-check-child')
->press('Approve')
->assertPathIs('/client/quotes')
->assertDontSee('Quote(s) approved successfully.')
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use App\Models\RecurringInvoice;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class RecurringInvoicesTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.recurring_invoices.index')
->assertSeeIn('span[data-ref="meta-title"]', 'Recurring Invoices')
->visitRoute('client.logout');
});
}
public function testRequestingCancellation()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.recurring_invoices.index')
->clickLink('View')
->assertSee('Cancellation')
->press('Request Cancellation')
->pause(1000)
->waitForText('Request cancellation')
->press('Confirm')
->pause(5000)
->assertPathIs(
route('client.recurring_invoices.request_cancellation', RecurringInvoice::first()->hashed_id, false)
)
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class SubscriptionsTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPageLoads()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.subscriptions.index')
->assertSeeIn('span[data-ref="meta-title"]', 'Subscriptions')
->visitRoute('client.logout');
});
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Tests\Browser\Pages\ClientPortal;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;
class Login extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url(): string
{
return '/client/login';
}
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
$browser->assertPathIs($this->url());
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [
'@element' => '#selector',
];
}
public function auth(Browser $browser)
{
$browser
->visitRoute('client.login')
->type('email', 'user@example.com')
->type('password', 'password')
->press('Login');
}
}

2
tests/Browser/console/.gitignore vendored Normal file
View File

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

2
tests/Browser/screenshots/.gitignore vendored Normal file
View File

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

72
tests/DuskTestCase.php Normal file
View File

@ -0,0 +1,72 @@
<?php
namespace Tests;
use App\Models\CompanyGateway;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Laravel\Dusk\TestCase as BaseTestCase;
abstract class DuskTestCase extends BaseTestCase
{
use CreatesApplication;
/**
* Prepare for Dusk test execution.
*
* @beforeClass
* @return void
*/
public static function prepare()
{
if (!static::runningInSail()) {
static::startChromeDriver();
}
}
/**
* Create the RemoteWebDriver instance.
*
* @return RemoteWebDriver
*/
protected function driver()
{
$options = (new ChromeOptions)->addArguments(collect([
'--window-size=1920,1080',
])->unless($this->hasHeadlessDisabled(), function ($items) {
return $items->merge([
'--disable-gpu',
'--headless',
]);
})->all());
return RemoteWebDriver::create(
$_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
/**
* Determine whether the Dusk command has disabled headless mode.
*
* @return bool
*/
protected function hasHeadlessDisabled()
{
return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
isset($_ENV['DUSK_HEADLESS_DISABLED']);
}
/**
* Disable all company gateways, test classes should enable them per need.
*
* @return void
*/
public function disableCompanyGateways()
{
CompanyGateway::where('company_id', 1)->delete();
}
}

View File

@ -10,7 +10,7 @@
* @license https://www.elastic.co/licensing/elastic-license * @license https://www.elastic.co/licensing/elastic-license
*/ */
namespace Tests\ClientPortal; namespace Tests\Feature\ClientPortal;
use App\Http\Livewire\CreditsTable; use App\Http\Livewire\CreditsTable;
use App\Models\Account; use App\Models\Account;
@ -23,6 +23,7 @@ use Faker\Factory;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire; use Livewire\Livewire;
use Tests\TestCase; use Tests\TestCase;
use function now;
class CreditsTest extends TestCase class CreditsTest extends TestCase
{ {

View File

@ -0,0 +1,90 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\ClientPortal;
use App\Http\Livewire\InvoicesTable;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\User;
use Faker\Factory;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use Tests\TestCase;
class InvoicesTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->faker = Factory::create();
}
public function testInvoiceTableFilters()
{
$account = Account::factory()->create();
$user = User::factory()->create(
['account_id' => $account->id, 'email' => $this->faker->safeEmail]
);
$company = Company::factory()->create(['account_id' => $account->id]);
$client = Client::factory()->create(['company_id' => $company->id, 'user_id' => $user->id]);
ClientContact::factory()->count(2)->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
]);
$sent = Invoice::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
]);
$paid = Invoice::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_PAID,
]);
$unpaid = Invoice::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_UNPAID,
]);
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(InvoicesTable::class, ['company' => $company])
->assertSee($sent->number)
->assertSee($paid->number)
->assertSee($unpaid->number);
Livewire::test(InvoicesTable::class, ['company' => $company])
->set('status', ['paid'])
->assertSee($paid->number)
->assertDontSee($unpaid->number);
}
}