Add membership fee system with disability discount and fix document permissions

Features:
- Implement two fee types: entrance fee and annual fee (both NT$1,000)
- Add 50% discount for disability certificate holders
- Add disability certificate upload in member profile
- Integrate disability verification into cashier approval workflow
- Add membership fee settings in system admin

Document permissions:
- Fix hard-coded role logic in Document model
- Use permission-based authorization instead of role checks

Additional features:
- Add announcements, general ledger, and trial balance modules
- Add income management and accounting entries
- Add comprehensive test suite with factories
- Update UI translations to Traditional Chinese

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-01 09:56:01 +08:00
parent 83ce1f7fc8
commit 642b879dd4
207 changed files with 19487 additions and 3048 deletions

View File

@@ -0,0 +1,201 @@
<?php
namespace Tests\Browser;
use App\Models\FinanceDocument;
use App\Models\Issue;
use App\Models\Member;
use App\Models\MembershipPayment;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* Admin Dashboard Browser Tests
*
* Tests the admin dashboard user interface and user experience.
*/
class AdminDashboardBrowserTest extends DuskTestCase
{
use DatabaseMigrations;
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']);
}
/**
* Create an admin user for testing
*/
protected function createAdminUser(): User
{
$user = User::factory()->create([
'email' => 'admin@test.com',
'password' => Hash::make('password'),
]);
$user->assignRole('admin');
return $user;
}
/**
* Test admin can view dashboard
*/
public function test_admin_can_view_dashboard(): void
{
$admin = $this->createAdminUser();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->assertSee('管理儀表板');
});
}
/**
* Test dashboard shows statistics
*/
public function test_dashboard_shows_statistics(): void
{
$admin = $this->createAdminUser();
// Create some data
Member::factory()->count(5)->create();
Issue::factory()->count(3)->create(['created_by_user_id' => $admin->id]);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->assertPresent('.stats-card');
});
}
/**
* Test admin can navigate to members page
*/
public function test_admin_can_navigate_to_members_page(): void
{
$admin = $this->createAdminUser();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->clickLink('會員管理')
->assertPathIs('/admin/members');
});
}
/**
* Test admin can navigate to finance page
*/
public function test_admin_can_navigate_to_finance_page(): void
{
$admin = $this->createAdminUser();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->clickLink('財務管理')
->assertPathIs('/admin/finance*');
});
}
/**
* Test admin can navigate to issues page
*/
public function test_admin_can_navigate_to_issues_page(): void
{
$admin = $this->createAdminUser();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->clickLink('議題追蹤')
->assertPathIs('/admin/issues');
});
}
/**
* Test sidebar navigation works
*/
public function test_sidebar_navigation_works(): void
{
$admin = $this->createAdminUser();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->assertPresent('.sidebar')
->assertPresent('.nav-link');
});
}
/**
* Test pending approvals widget
*/
public function test_pending_approvals_widget(): void
{
$admin = $this->createAdminUser();
MembershipPayment::factory()->count(3)->create([
'status' => MembershipPayment::STATUS_PENDING,
]);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->assertSee('待審核');
});
}
/**
* Test recent activity section
*/
public function test_recent_activity_section(): void
{
$admin = $this->createAdminUser();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.dashboard'))
->assertPresent('.activity-feed');
});
}
/**
* Test admin can search members
*/
public function test_admin_can_search_members(): void
{
$admin = $this->createAdminUser();
Member::factory()->create(['full_name' => '測試搜尋會員']);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.members.index'))
->type('search', '測試搜尋')
->press('搜尋')
->assertSee('測試搜尋會員');
});
}
/**
* Test responsive sidebar collapse
*/
public function test_responsive_sidebar_collapse(): void
{
$admin = $this->createAdminUser();
$this->browse(function (Browser $browser) use ($admin) {
// Mobile view
$browser->loginAs($admin)
->resize(375, 667)
->visit(route('admin.dashboard'))
->assertPresent('.sidebar-toggle');
});
}
}

View File

@@ -0,0 +1,254 @@
<?php
namespace Tests\Browser;
use App\Models\Document;
use App\Models\DocumentCategory;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* Document Management Browser Tests
*
* Tests the document management user interface and user experience.
*/
class DocumentManagementBrowserTest extends DuskTestCase
{
use DatabaseMigrations;
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
$this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']);
}
/**
* Create an admin user
*/
protected function createAdmin(): User
{
$user = User::factory()->create([
'email' => 'admin@test.com',
'password' => Hash::make('password'),
]);
$user->assignRole('admin');
return $user;
}
/**
* Test document list page loads
*/
public function test_document_list_page_loads(): void
{
$admin = $this->createAdmin();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('documents.index'))
->assertSee('文件管理');
});
}
/**
* Test can upload document
*/
public function test_can_upload_document(): void
{
$admin = $this->createAdmin();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('documents.create'))
->assertSee('上傳文件')
->assertPresent('input[name="title"]')
->assertPresent('input[type="file"]');
});
}
/**
* Test document categories are displayed
*/
public function test_document_categories_are_displayed(): void
{
$admin = $this->createAdmin();
DocumentCategory::factory()->create(['name' => '會議紀錄']);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('documents.index'))
->assertSee('會議紀錄');
});
}
/**
* Test can filter by category
*/
public function test_can_filter_by_category(): void
{
$admin = $this->createAdmin();
$category1 = DocumentCategory::factory()->create(['name' => '類別A']);
$category2 = DocumentCategory::factory()->create(['name' => '類別B']);
Document::factory()->create([
'title' => '文件A',
'category_id' => $category1->id,
'uploaded_by' => $admin->id,
]);
Document::factory()->create([
'title' => '文件B',
'category_id' => $category2->id,
'uploaded_by' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $category1) {
$browser->loginAs($admin)
->visit(route('documents.index'))
->select('category_id', $category1->id)
->press('篩選')
->assertSee('文件A')
->assertDontSee('文件B');
});
}
/**
* Test can search documents
*/
public function test_can_search_documents(): void
{
$admin = $this->createAdmin();
Document::factory()->create([
'title' => '特殊搜尋文件',
'uploaded_by' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('documents.index'))
->type('search', '特殊搜尋')
->press('搜尋')
->assertSee('特殊搜尋文件');
});
}
/**
* Test can view document details
*/
public function test_can_view_document_details(): void
{
$admin = $this->createAdmin();
$document = Document::factory()->create([
'title' => '詳細資料文件',
'description' => '這是文件描述',
'uploaded_by' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $document) {
$browser->loginAs($admin)
->visit(route('documents.show', $document))
->assertSee('詳細資料文件')
->assertSee('這是文件描述');
});
}
/**
* Test download button is present
*/
public function test_download_button_is_present(): void
{
$admin = $this->createAdmin();
$document = Document::factory()->create([
'uploaded_by' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $document) {
$browser->loginAs($admin)
->visit(route('documents.show', $document))
->assertSee('下載');
});
}
/**
* Test can delete document
*/
public function test_can_delete_document(): void
{
$admin = $this->createAdmin();
$document = Document::factory()->create([
'title' => '待刪除文件',
'uploaded_by' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $document) {
$browser->loginAs($admin)
->visit(route('documents.show', $document))
->press('刪除')
->acceptDialog()
->waitForLocation('/documents')
->assertDontSee('待刪除文件');
});
}
/**
* Test file size is displayed
*/
public function test_file_size_is_displayed(): void
{
$admin = $this->createAdmin();
$document = Document::factory()->create([
'uploaded_by' => $admin->id,
'file_size' => 1024 * 1024, // 1MB
]);
$this->browse(function (Browser $browser) use ($admin, $document) {
$browser->loginAs($admin)
->visit(route('documents.show', $document))
->assertSee('MB');
});
}
/**
* Test upload date is displayed
*/
public function test_upload_date_is_displayed(): void
{
$admin = $this->createAdmin();
$document = Document::factory()->create([
'uploaded_by' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $document) {
$browser->loginAs($admin)
->visit(route('documents.show', $document))
->assertPresent('.upload-date');
});
}
/**
* Test grid and list view toggle
*/
public function test_grid_and_list_view_toggle(): void
{
$admin = $this->createAdmin();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('documents.index'))
->assertPresent('.view-toggle');
});
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ExampleTest extends DuskTestCase
{
/**
* A basic browser test example.
*/
public function testBasicExample(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertSee('Laravel');
});
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace Tests\Browser;
use App\Models\FinanceDocument;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* Finance Workflow Browser Tests
*
* Tests the finance workflow user interface and user experience.
*/
class FinanceWorkflowBrowserTest extends DuskTestCase
{
use DatabaseMigrations;
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']);
}
/**
* Create a cashier user
*/
protected function createCashier(): User
{
$user = User::factory()->create([
'email' => 'cashier@test.com',
'password' => Hash::make('password'),
]);
$user->assignRole('finance_cashier');
return $user;
}
/**
* Create an accountant user
*/
protected function createAccountant(): User
{
$user = User::factory()->create([
'email' => 'accountant@test.com',
'password' => Hash::make('password'),
]);
$user->assignRole('finance_accountant');
return $user;
}
/**
* Test finance dashboard loads
*/
public function test_finance_dashboard_loads(): void
{
$cashier = $this->createCashier();
$this->browse(function (Browser $browser) use ($cashier) {
$browser->loginAs($cashier)
->visit(route('admin.finance.index'))
->assertSee('財務管理');
});
}
/**
* Test can create finance document
*/
public function test_can_create_finance_document(): void
{
$accountant = $this->createAccountant();
$this->browse(function (Browser $browser) use ($accountant) {
$browser->loginAs($accountant)
->visit(route('admin.finance-documents.create'))
->assertSee('新增財務單據')
->assertPresent('input[name="title"]')
->assertPresent('input[name="amount"]');
});
}
/**
* Test finance document list shows status
*/
public function test_finance_document_list_shows_status(): void
{
$cashier = $this->createCashier();
FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_PENDING,
'title' => '測試單據',
]);
$this->browse(function (Browser $browser) use ($cashier) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.index'))
->assertSee('測試單據')
->assertSee('待審核');
});
}
/**
* Test cashier can see approve button
*/
public function test_cashier_can_see_approve_button(): void
{
$cashier = $this->createCashier();
$document = FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_PENDING,
]);
$this->browse(function (Browser $browser) use ($cashier, $document) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.show', $document))
->assertSee('核准');
});
}
/**
* Test cashier can see reject button
*/
public function test_cashier_can_see_reject_button(): void
{
$cashier = $this->createCashier();
$document = FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_PENDING,
]);
$this->browse(function (Browser $browser) use ($cashier, $document) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.show', $document))
->assertSee('退回');
});
}
/**
* Test approval requires confirmation
*/
public function test_approval_requires_confirmation(): void
{
$cashier = $this->createCashier();
$document = FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_PENDING,
]);
$this->browse(function (Browser $browser) use ($cashier, $document) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.show', $document))
->press('核准')
->assertDialogOpened();
});
}
/**
* Test document amount is formatted
*/
public function test_document_amount_is_formatted(): void
{
$cashier = $this->createCashier();
FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_PENDING,
'amount' => 15000,
]);
$this->browse(function (Browser $browser) use ($cashier) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.index'))
->assertSee('15,000');
});
}
/**
* Test filter by status works
*/
public function test_filter_by_status_works(): void
{
$cashier = $this->createCashier();
FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_PENDING,
'title' => '待審核單據',
]);
FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
'title' => '已核准單據',
]);
$this->browse(function (Browser $browser) use ($cashier) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.index'))
->select('status', FinanceDocument::STATUS_PENDING)
->press('篩選')
->assertSee('待審核單據')
->assertDontSee('已核准單據');
});
}
/**
* Test rejection requires reason
*/
public function test_rejection_requires_reason(): void
{
$cashier = $this->createCashier();
$document = FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_PENDING,
]);
$this->browse(function (Browser $browser) use ($cashier, $document) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.show', $document))
->press('退回')
->waitFor('.modal')
->assertSee('退回原因');
});
}
/**
* Test document history is visible
*/
public function test_document_history_is_visible(): void
{
$cashier = $this->createCashier();
$document = FinanceDocument::factory()->create([
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
]);
$this->browse(function (Browser $browser) use ($cashier, $document) {
$browser->loginAs($cashier)
->visit(route('admin.finance-documents.show', $document))
->assertSee('審核歷程');
});
}
}

View File

@@ -0,0 +1,254 @@
<?php
namespace Tests\Browser;
use App\Models\Issue;
use App\Models\IssueLabel;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* Issue Tracker Browser Tests
*
* Tests the issue tracking user interface and user experience.
*/
class IssueTrackerBrowserTest extends DuskTestCase
{
use DatabaseMigrations;
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']);
}
/**
* Create an admin user
*/
protected function createAdmin(): User
{
$user = User::factory()->create([
'email' => 'admin@test.com',
'password' => Hash::make('password'),
]);
$user->assignRole('admin');
return $user;
}
/**
* Test issue list page loads
*/
public function test_issue_list_page_loads(): void
{
$admin = $this->createAdmin();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.issues.index'))
->assertSee('議題列表');
});
}
/**
* Test can create new issue
*/
public function test_can_create_new_issue(): void
{
$admin = $this->createAdmin();
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.issues.create'))
->assertSee('新增議題')
->type('title', '測試議題標題')
->type('description', '這是測試議題描述')
->select('priority', Issue::PRIORITY_HIGH)
->press('建立')
->waitForLocation('/admin/issues/*')
->assertSee('測試議題標題');
});
}
/**
* Test issue shows status badge
*/
public function test_issue_shows_status_badge(): void
{
$admin = $this->createAdmin();
Issue::factory()->create([
'created_by_user_id' => $admin->id,
'title' => '狀態測試議題',
'status' => Issue::STATUS_IN_PROGRESS,
]);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.issues.index'))
->assertSee('狀態測試議題')
->assertSee('進行中');
});
}
/**
* Test can assign issue
*/
public function test_can_assign_issue(): void
{
$admin = $this->createAdmin();
$assignee = User::factory()->create(['name' => '被指派者']);
$issue = Issue::factory()->create([
'created_by_user_id' => $admin->id,
'assignee_id' => null,
]);
$this->browse(function (Browser $browser) use ($admin, $issue) {
$browser->loginAs($admin)
->visit(route('admin.issues.show', $issue))
->assertSee('指派')
->select('assignee_id', '被指派者');
});
}
/**
* Test can add comment
*/
public function test_can_add_comment(): void
{
$admin = $this->createAdmin();
$issue = Issue::factory()->create([
'created_by_user_id' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $issue) {
$browser->loginAs($admin)
->visit(route('admin.issues.show', $issue))
->type('content', '這是一則測試留言')
->press('新增留言')
->waitForText('這是一則測試留言')
->assertSee('這是一則測試留言');
});
}
/**
* Test can change issue status
*/
public function test_can_change_issue_status(): void
{
$admin = $this->createAdmin();
$issue = Issue::factory()->create([
'created_by_user_id' => $admin->id,
'status' => Issue::STATUS_NEW,
]);
$this->browse(function (Browser $browser) use ($admin, $issue) {
$browser->loginAs($admin)
->visit(route('admin.issues.show', $issue))
->select('status', Issue::STATUS_IN_PROGRESS)
->press('更新狀態')
->waitForText('進行中');
});
}
/**
* Test can filter by status
*/
public function test_can_filter_by_status(): void
{
$admin = $this->createAdmin();
Issue::factory()->create([
'created_by_user_id' => $admin->id,
'title' => '新議題',
'status' => Issue::STATUS_NEW,
]);
Issue::factory()->create([
'created_by_user_id' => $admin->id,
'title' => '已關閉議題',
'status' => Issue::STATUS_CLOSED,
]);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.issues.index'))
->select('status', Issue::STATUS_NEW)
->press('篩選')
->assertSee('新議題')
->assertDontSee('已關閉議題');
});
}
/**
* Test can filter by priority
*/
public function test_can_filter_by_priority(): void
{
$admin = $this->createAdmin();
Issue::factory()->create([
'created_by_user_id' => $admin->id,
'title' => '高優先議題',
'priority' => Issue::PRIORITY_HIGH,
]);
Issue::factory()->create([
'created_by_user_id' => $admin->id,
'title' => '低優先議題',
'priority' => Issue::PRIORITY_LOW,
]);
$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit(route('admin.issues.index'))
->select('priority', Issue::PRIORITY_HIGH)
->press('篩選')
->assertSee('高優先議題')
->assertDontSee('低優先議題');
});
}
/**
* Test issue number is displayed
*/
public function test_issue_number_is_displayed(): void
{
$admin = $this->createAdmin();
$issue = Issue::factory()->create([
'created_by_user_id' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $issue) {
$browser->loginAs($admin)
->visit(route('admin.issues.show', $issue))
->assertSee($issue->issue_number);
});
}
/**
* Test can add labels to issue
*/
public function test_can_add_labels_to_issue(): void
{
$admin = $this->createAdmin();
$label = IssueLabel::factory()->create(['name' => '緊急']);
$issue = Issue::factory()->create([
'created_by_user_id' => $admin->id,
]);
$this->browse(function (Browser $browser) use ($admin, $issue) {
$browser->loginAs($admin)
->visit(route('admin.issues.show', $issue))
->assertPresent('.labels-section');
});
}
}

View File

@@ -0,0 +1,211 @@
<?php
namespace Tests\Browser;
use App\Models\Member;
use App\Models\MembershipPayment;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* Member Dashboard Browser Tests
*
* Tests the member dashboard user interface and user experience.
*/
class MemberDashboardBrowserTest extends DuskTestCase
{
use DatabaseMigrations;
protected User $memberUser;
protected Member $member;
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']);
}
/**
* Create a member user for testing
*/
protected function createMemberUser(): User
{
$user = User::factory()->create([
'email' => 'member@test.com',
'password' => Hash::make('password'),
]);
Member::factory()->create([
'user_id' => $user->id,
'membership_status' => Member::STATUS_ACTIVE,
]);
return $user;
}
/**
* Test member can view dashboard
*/
public function test_member_can_view_dashboard(): void
{
$user = $this->createMemberUser();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.dashboard'))
->assertSee('會員')
->assertSee('儀表板');
});
}
/**
* Test member sees membership status
*/
public function test_member_sees_membership_status(): void
{
$user = $this->createMemberUser();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.dashboard'))
->assertSee('會員狀態');
});
}
/**
* Test member can navigate to payment page
*/
public function test_member_can_navigate_to_payment_page(): void
{
$user = $this->createMemberUser();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.dashboard'))
->clickLink('繳費')
->assertPathIs('/member/payments*');
});
}
/**
* Test member can view payment history
*/
public function test_member_can_view_payment_history(): void
{
$user = $this->createMemberUser();
$member = $user->member;
MembershipPayment::factory()->count(3)->create([
'member_id' => $member->id,
'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.payments.index'))
->assertSee('繳費紀錄');
});
}
/**
* Test member can submit new payment
*/
public function test_member_can_submit_new_payment(): void
{
$user = $this->createMemberUser();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.payments.create'))
->assertSee('提交繳費')
->assertPresent('input[name="amount"]')
->assertPresent('input[type="file"]');
});
}
/**
* Test member sees pending payment status
*/
public function test_member_sees_pending_payment_status(): void
{
$user = $this->createMemberUser();
$member = $user->member;
MembershipPayment::factory()->create([
'member_id' => $member->id,
'status' => MembershipPayment::STATUS_PENDING,
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.payments.index'))
->assertSee('待審核');
});
}
/**
* Test member can update profile
*/
public function test_member_can_update_profile(): void
{
$user = $this->createMemberUser();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.profile.edit'))
->assertPresent('input[name="phone"]')
->assertPresent('input[name="address"]');
});
}
/**
* Test member dashboard shows expiry date
*/
public function test_member_dashboard_shows_expiry_date(): void
{
$user = $this->createMemberUser();
$user->member->update([
'membership_expires_at' => now()->addYear(),
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.dashboard'))
->assertSee('到期');
});
}
/**
* Test logout functionality
*/
public function test_logout_functionality(): void
{
$user = $this->createMemberUser();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(route('member.dashboard'))
->click('@user-menu')
->clickLink('登出')
->assertPathIs('/');
});
}
/**
* Test member cannot access admin pages
*/
public function test_member_cannot_access_admin_pages(): void
{
$user = $this->createMemberUser();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/admin/dashboard')
->assertPathIsNot('/admin/dashboard');
});
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Tests\Browser;
use App\Models\Member;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* Member Registration Browser Tests
*
* Tests the member registration user interface and user experience.
*/
class MemberRegistrationBrowserTest extends DuskTestCase
{
use DatabaseMigrations;
/**
* Test registration page loads correctly
*/
public function test_registration_page_loads_correctly(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->assertSee('會員註冊')
->assertPresent('input[name="full_name"]')
->assertPresent('input[name="email"]')
->assertPresent('input[name="password"]');
});
}
/**
* Test form validation messages display
*/
public function test_form_validation_messages_display(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->press('註冊')
->assertSee('必填');
});
}
/**
* Test successful registration redirects correctly
*/
public function test_successful_registration_redirects(): void
{
$this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']);
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->type('full_name', '測試會員')
->type('email', 'test@example.com')
->type('password', 'password123')
->type('password_confirmation', 'password123')
->type('national_id', 'A123456789')
->type('birthday', '1990-01-01')
->type('phone', '0912345678')
->type('address', '台北市信義區')
->press('註冊')
->waitForLocation('/member/dashboard')
->assertPathIs('/member/dashboard');
});
}
/**
* Test email field validation
*/
public function test_email_field_validation(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->type('email', 'invalid-email')
->press('註冊')
->assertSee('電子郵件');
});
}
/**
* Test password confirmation validation
*/
public function test_password_confirmation_validation(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->type('password', 'password123')
->type('password_confirmation', 'different')
->press('註冊')
->assertSee('密碼');
});
}
/**
* Test national ID format validation
*/
public function test_national_id_format_validation(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->type('national_id', 'invalid')
->press('註冊')
->assertSee('身分證');
});
}
/**
* Test duplicate email detection
*/
public function test_duplicate_email_detection(): void
{
User::factory()->create(['email' => 'existing@example.com']);
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->type('email', 'existing@example.com')
->press('註冊')
->assertSee('電子郵件');
});
}
/**
* Test file upload for ID photo
*/
public function test_file_upload_for_id_photo(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->assertPresent('input[type="file"]');
});
}
/**
* Test form preserves input on validation error
*/
public function test_form_preserves_input_on_validation_error(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(route('register.member'))
->type('full_name', '測試會員')
->type('email', 'test@example.com')
->press('註冊')
->assertInputValue('full_name', '測試會員')
->assertInputValue('email', 'test@example.com');
});
}
/**
* Test registration form is responsive
*/
public function test_registration_form_is_responsive(): void
{
$this->browse(function (Browser $browser) {
// Test on mobile viewport
$browser->resize(375, 667)
->visit(route('register.member'))
->assertPresent('input[name="full_name"]');
// Test on desktop viewport
$browser->resize(1920, 1080)
->visit(route('register.member'))
->assertPresent('input[name="full_name"]');
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class HomePage extends Page
{
/**
* Get the URL for the page.
*/
public function url(): string
{
return '/';
}
/**
* Assert that the browser is on the page.
*/
public function assert(Browser $browser): void
{
//
}
/**
* Get the element shortcuts for the page.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@element' => '#selector',
];
}
}

View File

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

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

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

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