Fix improve testing

This commit is contained in:
Víctor Falcón 2025-11-26 11:52:01 +01:00
parent 5cfe3c81a1
commit 981df8cd68
10 changed files with 769 additions and 10 deletions

View File

@ -29,9 +29,9 @@ class ResetUserCategories extends Command
{
$userIdentifier = $this->argument('user');
$user = is_numeric($userIdentifier)
? User::find($userIdentifier)
: User::where('email', $userIdentifier)->first();
$user = filter_var($userIdentifier, FILTER_VALIDATE_EMAIL)
? User::where('email', $userIdentifier)->first()
: User::find($userIdentifier);
if (! $user) {
$this->error("User not found: {$userIdentifier}");

View File

@ -0,0 +1,92 @@
<?php
use App\Models\User;
it('can register a new user', function () {
$page = visit('/register');
$page->assertSee('Create an account')
->fill('name', 'Test User')
->fill('email', 'newuser@example.com')
->fill('password', 'password123')
->fill('password_confirmation', 'password123')
->click('@register-user-button')
->waitFor('Setup your encryption')
->assertUrlIs('/setup-encryption')
->assertNoJavascriptErrors();
$this->assertDatabaseHas('users', [
'email' => 'newuser@example.com',
'name' => 'Test User',
]);
});
it('shows validation errors for invalid registration', function () {
$page = visit('/register');
$page->assertSee('Create an account')
->fill('name', 'Test User')
->fill('email', 'invalid-email')
->fill('password', 'short')
->fill('password_confirmation', 'different')
->click('@register-user-button')
->wait(1)
->assertNoJavascriptErrors();
});
it('can login with valid credentials', function () {
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
'encryption_salt' => str_repeat('a', 24),
]);
$page = visit('/login');
$page->assertSee('Log in to your account')
->fill('email', 'test@example.com')
->fill('password', 'password123')
->click('@login-button')
->waitFor('Dashboard')
->assertUrlIs('/dashboard')
->assertNoJavascriptErrors();
$this->assertAuthenticated();
});
it('shows validation error for invalid login credentials', function () {
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);
$page = visit('/login');
$page->assertSee('Log in to your account')
->fill('email', 'test@example.com')
->fill('password', 'wrongpassword')
->click('@login-button')
->wait(1)
->assertSee('These credentials do not match our records')
->assertNoJavascriptErrors();
});
it('can navigate from login to register', function () {
$page = visit('/login');
$page->assertSee('Log in to your account')
->click('Sign up')
->waitFor('Create an account')
->assertUrlIs('/register')
->assertNoJavascriptErrors();
});
it('can navigate from register to login', function () {
$page = visit('/register');
$page->assertSee('Create an account')
->click('Log in')
->waitFor('Log in to your account')
->assertUrlIs('/login')
->assertNoJavascriptErrors();
});

View File

@ -0,0 +1,186 @@
<?php
use App\Models\Account;
use App\Models\Bank;
use App\Models\User;
use function Pest\Laravel\actingAs;
it('can view bank accounts page', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->assertSee('Manage your bank accounts')
->assertNoJavascriptErrors();
});
it('shows existing accounts in list', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$bank = Bank::factory()->create(['name' => 'Test Bank']);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'My Checking',
'name_iv' => str_repeat('b', 16),
'type' => 'checking',
'currency_code' => 'USD',
]);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->assertSee('Test Bank')
->assertSee('Checking')
->assertSee('USD')
->assertNoJavascriptErrors();
});
it('can open create account dialog', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->click('Create Account')
->waitFor('dialog')
->assertSee('Add a new bank account to track your transactions')
->assertNoJavascriptErrors();
});
it('can create a new bank account', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$bank = Bank::factory()->create(['name' => 'My Bank']);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->click('Create Account')
->waitFor('dialog')
->fill('#display_name', 'My Savings Account')
->click('Select a bank...')
->wait(0.5)
->fill('input[placeholder="Search banks..."]', 'My Bank')
->wait(0.5)
->click('My Bank')
->click('Select account type')
->wait(0.3)
->click('Savings')
->click('Select currency')
->wait(0.3)
->click('EUR')
->click('button[type="submit"]')
->wait(2)
->assertNoJavascriptErrors();
$this->assertDatabaseHas('accounts', [
'user_id' => $user->id,
'bank_id' => $bank->id,
'type' => 'savings',
'currency_code' => 'EUR',
]);
});
it('shows empty state when no accounts exist', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->assertSee('No accounts found')
->assertNoJavascriptErrors();
});
it('can filter accounts by name', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$bank = Bank::factory()->create(['name' => 'Test Bank']);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'Checking Account',
'name_iv' => str_repeat('b', 16),
]);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'Savings Account',
'name_iv' => str_repeat('c', 16),
]);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->fill('input[placeholder="Filter accounts..."]', 'Checking')
->wait(0.5)
->assertNoJavascriptErrors();
});
it('can edit an existing account via dropdown menu', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$bank = Bank::factory()->create(['name' => 'Test Bank']);
$account = Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'Old Account Name',
'name_iv' => str_repeat('b', 16),
'type' => 'checking',
'currency_code' => 'USD',
]);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->click('button[aria-label="Open menu"]')
->wait(0.3)
->click('Edit')
->waitFor('dialog')
->assertSee('Edit Account')
->fill('#display_name', 'Updated Account Name')
->click('Save')
->wait(2)
->assertNoJavascriptErrors();
});
it('can delete an account via dropdown menu', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$bank = Bank::factory()->create(['name' => 'Test Bank']);
$account = Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'Account To Delete',
'name_iv' => str_repeat('b', 16),
]);
actingAs($user);
$page = visit('/settings/accounts');
$page->assertSee('Bank accounts')
->click('button[aria-label="Open menu"]')
->wait(0.3)
->click('Delete')
->waitFor('dialog')
->assertSee('Delete Account')
->click('Delete')
->wait(2)
->assertNoJavascriptErrors();
$this->assertDatabaseMissing('accounts', [
'id' => $account->id,
]);
});

View File

@ -0,0 +1,145 @@
<?php
use App\Models\Category;
use App\Models\User;
use function Pest\Laravel\actingAs;
it('can view categories page', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
actingAs($user);
$page = visit('/settings/categories');
$page->assertSee('Categories settings')
->assertSee('Manage your transaction categories')
->assertNoJavascriptErrors();
});
it('shows existing categories in list', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$category = Category::factory()->create([
'user_id' => $user->id,
'name' => 'Groceries',
'icon' => 'ShoppingCart',
'color' => 'green',
]);
actingAs($user);
$page = visit('/settings/categories');
$page->assertSee('Categories settings')
->assertSee('Groceries')
->assertNoJavascriptErrors();
});
it('can open create category dialog', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
actingAs($user);
$page = visit('/settings/categories');
$page->assertSee('Categories settings')
->click('Create Category')
->waitFor('dialog')
->assertSee('Add a new category to organize your transactions')
->assertNoJavascriptErrors();
});
it('can create a new category', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
actingAs($user);
$page = visit('/settings/categories');
$page->assertSee('Categories settings')
->click('Create Category')
->waitFor('dialog')
->fill('name', 'Entertainment')
->click('Select an icon')
->wait(0.3)
->click('Film')
->click('Select a color')
->wait(0.3)
->click('purple')
->click('button[type="submit"]')
->wait(2)
->assertSee('Entertainment')
->assertNoJavascriptErrors();
$this->assertDatabaseHas('categories', [
'user_id' => $user->id,
'name' => 'Entertainment',
'icon' => 'Film',
'color' => 'purple',
]);
});
it('can filter categories by name', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create([
'user_id' => $user->id,
'name' => 'Groceries',
]);
Category::factory()->create([
'user_id' => $user->id,
'name' => 'Entertainment',
]);
actingAs($user);
$page = visit('/settings/categories');
$page->assertSee('Categories settings')
->fill('input[placeholder="Filter categories..."]', 'Groceries')
->wait(0.5)
->assertSee('Groceries')
->assertNoJavascriptErrors();
});
it('shows empty state when no categories exist', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
actingAs($user);
$page = visit('/settings/categories');
$page->assertSee('Categories settings')
->assertSee('No categories found')
->assertNoJavascriptErrors();
});
it('can edit an existing category via context menu', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$category = Category::factory()->create([
'user_id' => $user->id,
'name' => 'Old Category',
'icon' => 'Tag',
'color' => 'blue',
]);
actingAs($user);
$page = visit('/settings/categories');
$page->assertSee('Old Category')
->click('button[aria-label="Open menu"]')
->wait(0.3)
->click('Edit')
->waitFor('dialog')
->assertSee('Edit Category')
->fill('name', 'Updated Category')
->click('Save')
->wait(2)
->assertSee('Updated Category')
->assertNoJavascriptErrors();
$this->assertDatabaseHas('categories', [
'id' => $category->id,
'name' => 'Updated Category',
]);
});

View File

@ -0,0 +1,233 @@
<?php
use App\Models\Account;
use App\Models\Bank;
use App\Models\Category;
use App\Models\User;
use function Pest\Laravel\actingAs;
it('can open import transactions drawer', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
$bank = Bank::factory()->create(['name' => 'Test Bank']);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'Test Account',
'name_iv' => str_repeat('b', 16),
]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(0.3)
->click('Import Transactions')
->waitFor('drawer')
->assertSee('Import Transactions')
->assertSee('Select the account to import transactions into')
->assertNoJavascriptErrors();
});
it('shows no accounts message when none exist', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(0.3)
->click('Import Transactions')
->waitFor('drawer')
->assertSee('No accounts found')
->assertNoJavascriptErrors();
});
it('can select account for import', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
$bank = Bank::factory()->create(['name' => 'My Bank']);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'My Checking',
'name_iv' => str_repeat('b', 16),
'currency_code' => 'USD',
]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(0.3)
->click('Import Transactions')
->waitFor('drawer')
->assertSee('My Bank')
->assertSee('USD')
->click('label')
->wait(0.3)
->click('Next')
->waitFor('Drop your file here')
->assertSee('Supports CSV, XLS, and XLSX files')
->assertNoJavascriptErrors();
});
it('can upload a CSV file for import', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
$bank = Bank::factory()->create(['name' => 'My Bank']);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'My Checking',
'name_iv' => str_repeat('b', 16),
'currency_code' => 'USD',
]);
actingAs($user);
$page = visit('/transactions');
$testFile = __DIR__.'/assets/test-transactions.csv';
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(0.3)
->click('Import Transactions')
->waitFor('drawer')
->click('label')
->wait(0.3)
->click('Next')
->waitFor('Drop your file here')
->attach('input[type="file"]', $testFile)
->wait(1)
->assertSee('test-transactions.csv')
->assertNoJavascriptErrors();
});
it('can complete full import flow', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
$bank = Bank::factory()->create(['name' => 'My Bank']);
$account = Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'My Checking',
'name_iv' => str_repeat('b', 16),
'currency_code' => 'USD',
]);
actingAs($user);
$page = visit('/transactions');
$testFile = __DIR__.'/assets/test-transactions.csv';
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(0.3)
->click('Import Transactions')
->waitFor('drawer')
->click('label')
->wait(0.5)
->click('Next')
->waitFor('Drop your file here')
->attach('input[type="file"]', $testFile)
->wait(1)
->assertSee('test-transactions.csv')
->click('Next')
->wait(1)
->waitFor('Transaction Date')
->click('#date-column')
->wait(0.3)
->click('Date')
->click('#description-column')
->wait(0.3)
->click('Description')
->click('#amount-column')
->wait(0.3)
->click('Amount')
->wait(0.5)
->click('Preview Transactions')
->wait(2)
->assertSee('Total')
->assertSee('New')
->assertNoJavascriptErrors();
});
it('shows column mapping step after file upload', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
$bank = Bank::factory()->create(['name' => 'My Bank']);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'My Checking',
'name_iv' => str_repeat('b', 16),
'currency_code' => 'USD',
]);
actingAs($user);
$page = visit('/transactions');
$testFile = __DIR__.'/assets/test-transactions.csv';
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(0.3)
->click('Import Transactions')
->waitFor('drawer')
->click('label')
->wait(0.5)
->click('Next')
->waitFor('Drop your file here')
->attach('input[type="file"]', $testFile)
->wait(1)
->click('Next')
->wait(1)
->assertSee('Transaction Date')
->assertSee('Description')
->assertSee('Amount')
->assertSee('Balance (Optional)')
->assertNoJavascriptErrors();
});
it('can navigate back through import steps', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
$bank = Bank::factory()->create(['name' => 'My Bank']);
Account::factory()->create([
'user_id' => $user->id,
'bank_id' => $bank->id,
'name' => 'My Checking',
'name_iv' => str_repeat('b', 16),
'currency_code' => 'USD',
]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->click('button[aria-label="More actions"]')
->wait(0.3)
->click('Import Transactions')
->waitFor('drawer')
->click('label')
->wait(0.5)
->click('Next')
->waitFor('Drop your file here')
->click('Back')
->wait(0.5)
->assertSee('Select the account to import transactions into')
->assertNoJavascriptErrors();
});

View File

@ -0,0 +1,96 @@
<?php
use App\Models\Account;
use App\Models\Category;
use App\Models\User;
use function Pest\Laravel\actingAs;
it('can view transactions page', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
Account::factory()->create(['user_id' => $user->id]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->assertSee('View and manage your transactions')
->assertNoJavascriptErrors();
});
it('can open add transaction dialog', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
Account::factory()->create(['user_id' => $user->id]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->click('Add Transaction')
->waitFor('dialog')
->assertSee('Create Transaction')
->assertNoJavascriptErrors();
});
it('can create a transaction', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
$category = Category::factory()->create(['user_id' => $user->id]);
$account = Account::factory()->create(['user_id' => $user->id]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->click('Add Transaction')
->waitFor('dialog')
->fill('description', 'Test Transaction')
->click('Select Account')
->wait(0.5)
->click($account->name)
->click('Select Category')
->wait(0.5)
->click($category->name)
->fill('#amount', '50.00')
->click('Create')
->wait(2)
->assertNoJavascriptErrors();
$this->assertDatabaseHas('transactions', [
'user_id' => $user->id,
'amount' => 5000,
]);
});
it('shows empty state when no transactions exist', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
Account::factory()->create(['user_id' => $user->id]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->assertSee('No transactions found')
->assertNoJavascriptErrors();
});
it('can filter transactions by search text', function () {
$user = User::factory()->create(['encryption_salt' => str_repeat('a', 24)]);
Category::factory()->create(['user_id' => $user->id]);
Account::factory()->create(['user_id' => $user->id]);
actingAs($user);
$page = visit('/transactions');
$page->assertSee('Transactions')
->fill('input[placeholder="Search transactions..."]', 'grocery')
->wait(0.5)
->assertNoJavascriptErrors();
});

View File

@ -0,0 +1,7 @@
Date,Description,Amount
2024-01-15,Grocery Store Purchase,-50.25
2024-01-16,Coffee Shop,-5.50
2024-01-17,Salary Deposit,2500.00
2024-01-18,Electric Bill,-125.00
2024-01-19,Restaurant Dinner,-45.75
1 Date Description Amount
2 2024-01-15 Grocery Store Purchase -50.25
3 2024-01-16 Coffee Shop -5.50
4 2024-01-17 Salary Deposit 2500.00
5 2024-01-18 Electric Bill -125.00
6 2024-01-19 Restaurant Dinner -45.75

View File

@ -12,11 +12,11 @@ test('command resets categories for a user by ID', function () {
$this->artisan('categories:reset', ['user' => $user->id])
->expectsOutput("Resetting categories for user: {$user->name} ({$user->email})")
->expectsOutput('Deleted 5 existing categories.')
->expectsOutput('Created 49 default categories.')
->expectsOutput('Created 63 default categories.')
->expectsOutput('✓ Categories reset successfully!')
->assertSuccessful();
expect($user->fresh()->categories()->count())->toBe(49);
expect($user->fresh()->categories()->count())->toBe(63);
$categoryNames = $user->categories->pluck('name')->toArray();
expect($categoryNames)->toContain('Food', 'Transportation', 'Salary', 'Insurance');
@ -31,11 +31,11 @@ test('command resets categories for a user by email', function () {
$this->artisan('categories:reset', ['user' => 'test@example.com'])
->expectsOutput("Resetting categories for user: {$user->name} (test@example.com)")
->expectsOutput('Deleted 3 existing categories.')
->expectsOutput('Created 49 default categories.')
->expectsOutput('Created 63 default categories.')
->expectsOutput('✓ Categories reset successfully!')
->assertSuccessful();
expect($user->fresh()->categories()->count())->toBe(49);
expect($user->fresh()->categories()->count())->toBe(63);
});
test('command works when user has no existing categories', function () {
@ -46,11 +46,11 @@ test('command works when user has no existing categories', function () {
$this->artisan('categories:reset', ['user' => $user->id])
->expectsOutput("Resetting categories for user: {$user->name} ({$user->email})")
->expectsOutput('No existing categories found.')
->expectsOutput('Created 49 default categories.')
->expectsOutput('Created 63 default categories.')
->expectsOutput('✓ Categories reset successfully!')
->assertSuccessful();
expect($user->fresh()->categories()->count())->toBe(49);
expect($user->fresh()->categories()->count())->toBe(63);
});
test('command fails when user is not found by ID', function () {

View File

@ -183,7 +183,7 @@ test('default categories are created when user registers', function () {
$service = new \App\Actions\CreateDefaultCategories;
$service->handle($user);
expect($user->categories()->count())->toBe(49);
expect($user->categories()->count())->toBe(63);
$categoryNames = $user->categories->pluck('name')->toArray();
expect($categoryNames)->toContain('Food', 'Transportation', 'Salary', 'Insurance');