diff --git a/.env b/.env index af8bc804..965fb54c 100644 --- a/.env +++ b/.env @@ -64,5 +64,5 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" - -DUSK_HEADLESS_DISABLED=true + +DUSK_HEADLESS_DISABLED=true diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results index 09c7bcc5..1bf4bbbe 100644 --- a/.phpunit.cache/test-results +++ b/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":1,"defects":{"Tests\\Browser\\ExampleTest::test_basic_example":7,"Tests\\Browser\\CreateAccountTest::test_empty_form_shows_validation_errors":7,"Tests\\Browser\\CreateAccountTest::test_successful_registration_redirects_to_game":7,"Tests\\Browser\\CreateAccountTest::test_authenticated_user_sees_account_page":8,"Tests\\Browser\\CreateAccountTest::test_invalid_phone_shows_error":8,"Tests\\Browser\\AccountManagementTest::test_update_account_successfully":8,"Tests\\Browser\\AccountManagementTest::test_update_account_with_empty_name_shows_error":8,"Tests\\Browser\\AccountManagementTest::test_update_account_with_invalid_phone_shows_error":8,"Tests\\Browser\\AccountManagementTest::test_history_section_visible_when_authenticated":8,"Tests\\Browser\\AccountManagementTest::test_logout_works":8,"Tests\\Browser\\GamePageTest::test_root_redirects_to_game":7,"Tests\\Browser\\GamePageTest::test_game_rule_endpoint_returns_content":7},"times":{"Tests\\Browser\\ExampleTest::test_basic_example":3.169,"Tests\\Browser\\CreateAccountTest::test_login_page_shows_form":2.375,"Tests\\Browser\\CreateAccountTest::test_empty_form_shows_validation_errors":0.583,"Tests\\Browser\\CreateAccountTest::test_invalid_phone_shows_error":1.315,"Tests\\Browser\\CreateAccountTest::test_successful_registration_redirects_to_game":1.263,"Tests\\Browser\\CreateAccountTest::test_authenticated_user_sees_account_page":1.602,"Tests\\Browser\\AccountManagementTest::test_update_account_successfully":3.551,"Tests\\Browser\\AccountManagementTest::test_update_account_with_empty_name_shows_error":1.066,"Tests\\Browser\\AccountManagementTest::test_update_account_with_invalid_phone_shows_error":0.728,"Tests\\Browser\\AccountManagementTest::test_history_section_visible_when_authenticated":0.686,"Tests\\Browser\\AccountManagementTest::test_logout_works":0.684,"Tests\\Browser\\GamePageTest::test_game_page_loads_when_guest":2.332,"Tests\\Browser\\GamePageTest::test_root_redirects_to_game":1.283,"Tests\\Browser\\GamePageTest::test_game_page_shows_lucky_wheel":0.141,"Tests\\Browser\\GamePageTest::test_game_rule_endpoint_returns_content":0.375}} \ No newline at end of file +{"version":1,"defects":{"Tests\\Browser\\ExampleTest::test_basic_example":7,"Tests\\Browser\\CreateAccountTest::test_empty_form_shows_validation_errors":7,"Tests\\Browser\\CreateAccountTest::test_successful_registration_redirects_to_game":8,"Tests\\Browser\\CreateAccountTest::test_authenticated_user_sees_account_page":8,"Tests\\Browser\\CreateAccountTest::test_invalid_phone_shows_error":8,"Tests\\Browser\\AccountManagementTest::test_update_account_successfully":8,"Tests\\Browser\\AccountManagementTest::test_update_account_with_empty_name_shows_error":8,"Tests\\Browser\\AccountManagementTest::test_update_account_with_invalid_phone_shows_error":8,"Tests\\Browser\\AccountManagementTest::test_history_section_visible_when_authenticated":8,"Tests\\Browser\\AccountManagementTest::test_logout_works":8,"Tests\\Browser\\GamePageTest::test_root_redirects_to_game":7,"Tests\\Browser\\GamePageTest::test_game_rule_endpoint_returns_content":7,"Tests\\Browser\\GamePageTest::test_game_rule_popup_loads":8,"Tests\\Browser\\GamePageTest::test_game_rule_api_returns_html":7,"Tests\\Browser\\AdminLoginTest::test_admin_login_with_empty_fields_shows_validation":7,"Tests\\Browser\\AdminLoginTest::test_authenticated_admin_gets_redirected_from_login":7,"Tests\\Browser\\AdminLoginTest::test_admin_logout":7,"Tests\\Browser\\AdminPrizeTest::test_add_prize_with_empty_name_shows_error":8,"Tests\\Browser\\AdminPrizeTest::test_create_prize_successfully":8,"Tests\\Browser\\AdminPrizeTest::test_update_prize_successfully":8,"Tests\\Browser\\AdminPrizeTest::test_delete_prize_successfully":8,"Tests\\Browser\\AdminPrizeTest::test_prize_winner_list_has_date_filter":7,"Tests\\Browser\\AdminPrizeTest::test_prize_winner_export_link_present":7,"Tests\\Browser\\AdminUserTest::test_user_list_shows_target_user":7,"Tests\\Browser\\AdminUserTest::test_update_user_name_successfully":8,"Tests\\Browser\\AdminUserTest::test_admin_account_page_shows_user_info":8,"Tests\\Browser\\AdminParameterTest::test_update_game_rule_successfully":8,"Tests\\Browser\\AdminUserTest::test_user_list_page_loads":7,"Tests\\Browser\\AdminUserTest::test_edit_user_form_shows_existing_data":8,"Tests\\Browser\\CreateAccountTest::test_login_page_shows_form":7},"times":{"Tests\\Browser\\ExampleTest::test_basic_example":2.181,"Tests\\Browser\\CreateAccountTest::test_login_page_shows_form":3.068,"Tests\\Browser\\CreateAccountTest::test_empty_form_shows_validation_errors":1.869,"Tests\\Browser\\CreateAccountTest::test_invalid_phone_shows_error":1.23,"Tests\\Browser\\CreateAccountTest::test_successful_registration_redirects_to_game":1.61,"Tests\\Browser\\CreateAccountTest::test_authenticated_user_sees_account_page":1.818,"Tests\\Browser\\AccountManagementTest::test_update_account_successfully":6.163,"Tests\\Browser\\AccountManagementTest::test_update_account_with_empty_name_shows_error":2.975,"Tests\\Browser\\AccountManagementTest::test_update_account_with_invalid_phone_shows_error":4.511,"Tests\\Browser\\AccountManagementTest::test_history_section_visible_when_authenticated":2.459,"Tests\\Browser\\AccountManagementTest::test_logout_works":3.221,"Tests\\Browser\\GamePageTest::test_game_page_loads_when_guest":1.824,"Tests\\Browser\\GamePageTest::test_root_redirects_to_game":0.296,"Tests\\Browser\\GamePageTest::test_game_page_shows_lucky_wheel":0.141,"Tests\\Browser\\GamePageTest::test_game_rule_endpoint_returns_content":0.375,"Tests\\Browser\\GamePageTest::test_game_page_shows_lucky_wheel_canvas":0.328,"Tests\\Browser\\GamePageTest::test_spin_button_is_present":0.214,"Tests\\Browser\\GamePageTest::test_game_rule_ribbon_is_present":0.163,"Tests\\Browser\\GamePageTest::test_guest_sees_login_prompt_on_game":0.842,"Tests\\Browser\\GamePageTest::test_logged_in_user_can_see_spin_button_enabled":2.55,"Tests\\Browser\\GamePageTest::test_already_spined_user_sees_spin_button_disabled":1.429,"Tests\\Browser\\GamePageTest::test_game_rule_popup_loads":1.714,"Tests\\Browser\\GamePageTest::test_game_rule_api_returns_html":0.112,"Tests\\Browser\\GamePageTest::test_checkspined_returns_error_for_guest":0.375,"Tests\\Browser\\GamePageTest::test_checkspined_returns_zero_for_new_user":0.804,"Tests\\Browser\\AdminLoginTest::test_admin_login_page_shows_form":4.181,"Tests\\Browser\\AdminLoginTest::test_admin_login_with_wrong_credentials_shows_error":1.159,"Tests\\Browser\\AdminLoginTest::test_admin_login_with_empty_fields_shows_validation":1.51,"Tests\\Browser\\AdminLoginTest::test_admin_login_successful":1.971,"Tests\\Browser\\AdminLoginTest::test_authenticated_admin_gets_redirected_from_login":1.149,"Tests\\Browser\\AdminLoginTest::test_unauthenticated_access_to_admin_account_redirects":0.504,"Tests\\Browser\\AdminLoginTest::test_admin_logout":2.019,"Tests\\Browser\\AdminPrizeTest::test_prize_list_page_loads":3.39,"Tests\\Browser\\AdminPrizeTest::test_add_prize_form_loads":2.175,"Tests\\Browser\\AdminPrizeTest::test_add_prize_with_empty_name_shows_error":1.527,"Tests\\Browser\\AdminPrizeTest::test_create_prize_successfully":2.957,"Tests\\Browser\\AdminPrizeTest::test_update_prize_successfully":1.862,"Tests\\Browser\\AdminPrizeTest::test_delete_prize_successfully":2.644,"Tests\\Browser\\AdminPrizeTest::test_prize_winner_list_loads":0.539,"Tests\\Browser\\AdminPrizeTest::test_prize_winner_list_has_date_filter":0.588,"Tests\\Browser\\AdminPrizeTest::test_prize_winner_export_link_present":0.701,"Tests\\Browser\\AdminUserTest::test_user_list_page_loads":3.147,"Tests\\Browser\\AdminUserTest::test_user_list_shows_target_user":1.689,"Tests\\Browser\\AdminUserTest::test_edit_user_form_shows_existing_data":0.528,"Tests\\Browser\\AdminUserTest::test_update_user_name_successfully":1.256,"Tests\\Browser\\AdminUserTest::test_admin_account_page_shows_user_info":0.689,"Tests\\Browser\\AdminParameterTest::test_parameter_page_loads":2.402,"Tests\\Browser\\AdminParameterTest::test_parameter_page_shows_game_rule_field":0.536,"Tests\\Browser\\AdminParameterTest::test_update_game_rule_successfully":1.42,"Tests\\Browser\\AdminParameterTest::test_parameter_page_has_image_upload_fields":0.638,"Tests\\Browser\\AdminParameterTest::test_parameter_page_requires_auth":1.646,"Tests\\Browser\\AdminLoginTest::test_authenticated_admin_visiting_login_sees_form":0.51}} \ No newline at end of file diff --git a/docs/49_testcase.txt b/docs/49_testcase.txt new file mode 100644 index 00000000..675e8ba4 --- /dev/null +++ b/docs/49_testcase.txt @@ -0,0 +1,431 @@ +================================================================================ + DANH SÁCH 49 KỊCH BẢN KIỂM THỬ - LARAVEL DUSK BROWSER TESTS + Dự án: Quay Số May Mắn - quayso.sisvietnam.vn + Tổng kết: 49 tests PASSED | 90 assertions | ~103 giây +================================================================================ + +-------------------------------------------------------------------------------- +FILE 1: tests/Browser/ExampleTest.php (1 test) +-------------------------------------------------------------------------------- + +[TC-01] test_basic_example + Mô tả : Kiểm tra cơ bản trang chủ tải được vòng quay may mắn. + Bước : 1. Truy cập URL gốc "/". + Kết quả: Phần tử #wheelCanvas (canvas vòng quay) hiển thị trên trang. + +-------------------------------------------------------------------------------- +FILE 2: tests/Browser/GamePageTest.php (12 tests) +-------------------------------------------------------------------------------- + +[TC-02] test_game_page_loads_when_guest + Mô tả : Trang game tải được khi người dùng chưa đăng nhập. + Bước : 1. Truy cập "/game" với vai trò khách (guest). + Kết quả: URL path là "/game" (trang không bị redirect). + +[TC-03] test_root_redirects_to_game + Mô tả : URL gốc "/" hiển thị cùng nội dung với "/game". + Bước : 1. Truy cập "/". + Kết quả: Phần tử #wheelCanvas (canvas vòng quay) hiển thị. + +[TC-04] test_game_page_shows_lucky_wheel_canvas + Mô tả : Canvas vòng quay may mắn hiển thị trên trang game. + Bước : 1. Truy cập "/game". + Kết quả: Phần tử #wheelCanvas hiển thị trong trang. + +[TC-05] test_spin_button_is_present + Mô tả : Nút "Quay" hiển thị trên trang game. + Bước : 1. Truy cập "/game". + Kết quả: Phần tử #spinButton hiển thị trong trang. + +[TC-06] test_game_rule_ribbon_is_present + Mô tả : Banner "Thể lệ tham dự" hiển thị trên trang game. + Bước : 1. Truy cập "/game". + Kết quả: Phần tử #game_rule hiển thị trong trang. + +[TC-07] test_guest_sees_login_prompt_on_game + Mô tả : Khách chưa đăng nhập nhìn thấy thông báo yêu cầu đăng nhập. + Bước : 1. Đăng xuất ("/logout"). + 2. Truy cập "/game". + Kết quả: Thông báo ".alert_wheel_spin" hiển thị; + Link đăng nhập (href chứa "login") hiển thị. + +[TC-08] test_logged_in_user_can_see_spin_button_enabled + Mô tả : Người dùng đã đăng nhập và chưa quay thấy nút quay ở trạng thái sẵn sàng. + Bước : 1. Đăng xuất và vào "/login". + 2. Nhập thông tin cá nhân (họ tên, SĐT mới, địa chỉ, nguồn biết SIS, nghề nghiệp). + 3. Nhấn "Gửi thông tin" và chờ redirect về "/game". + Kết quả: Thuộc tính data-check của #spinButton = "true" (nút quay được bật). + Dọn dẹp: Xóa user vừa tạo khỏi DB. + +[TC-09] test_already_spined_user_sees_spin_button_disabled + Mô tả : Người dùng đã quay (spined=1) thấy nút quay bị khóa. + Chuẩn bị: Tạo user với spined=1 trực tiếp trong DB. + Bước : 1. Đăng nhập bằng user có spined=1. + 2. Truy cập "/game". + Kết quả: Thuộc tính data-check của #spinButton = "false" (nút quay bị khóa). + Dọn dẹp: Xóa user test khỏi DB. + +[TC-10] test_game_rule_popup_loads + Mô tả : Popup thể lệ tham dự tải nội dung khi nhấn banner. + Bước : 1. Truy cập "/game". + 2. Xóa các element ".alert" bằng JavaScript (để không che banner). + 3. Nhấn vào #game_rule bằng JavaScript click. + 4. Chờ 800ms. + Kết quả: Phần tử #gamerule (popup) hiển thị trong trang. + +[TC-11] test_game_rule_api_returns_html + Mô tả : API /game/rule trả về nội dung HTML (dùng cho AJAX). + Bước : 1. Truy cập "/game/rule" trực tiếp trên trình duyệt. + Kết quả: Source HTML của trang chứa chuỗi "class=" (là fragment HTML hợp lệ). + +[TC-12] test_checkspined_returns_error_for_guest + Mô tả : API /checkspined trả lỗi khi người dùng chưa đăng nhập. + Bước : 1. Đăng xuất ("/logout"). + 2. Truy cập "/checkspined". + Kết quả: Trang hiển thị chuỗi "error". + +[TC-13] test_checkspined_returns_zero_for_new_user + Mô tả : API /checkspined trả spined=0 cho người dùng mới chưa quay. + Chuẩn bị: Tạo user mới với spined=0 trong DB. + Bước : 1. Đăng nhập bằng user đó. + 2. Truy cập "/checkspined". + Kết quả: Trang hiển thị chuỗi '"spined":0'. + Dọn dẹp: Xóa user test khỏi DB. + +-------------------------------------------------------------------------------- +FILE 3: tests/Browser/CreateAccountTest.php (5 tests) +-------------------------------------------------------------------------------- + +[TC-14] test_login_page_shows_form + Mô tả : Trang đăng ký/đăng nhập hiển thị đủ các trường nhập liệu. + Bước : 1. Đăng xuất để đảm bảo trạng thái guest. + 2. Truy cập "/login". + Kết quả: Tiêu đề "Nhập thông tin cá nhân" hiển thị; + Các trường input[name="name"], input[name="phone"], + input[name="address"], input[name="job"] đều có mặt.ss + +[TC-15] test_empty_form_shows_validation_errors + Mô tả : Gửi form rỗng phải hiện lỗi validation từ server. + Bước : 1. Đăng xuất và truy cập "/login". + 2. Nhấn "Gửi thông tin" mà không nhập gì. + 3. Chờ phần tử ".text-danger" xuất hiện. + Kết quả: Ít nhất một phần tử ".text-danger" (thông báo lỗi) hiển thị. + +[TC-16] test_invalid_phone_shows_error + Mô tả : Số điện thoại sai định dạng phải hiện thông báo lỗi. + Bước : 1. Truy cập "/login". + 2. Nhập tên hợp lệ, nhập SĐT = "12345" (sai định dạng). + 3. Điền các trường còn lại và nhấn "Gửi thông tin". + Kết quả: Trang hiển thị "Số điện thoại không đúng định dạng". + +[TC-17] test_successful_registration_redirects_to_game + Mô tả : Đăng ký thành công tạo tài khoản mới và chuyển về trang game. + Bước : 1. Truy cập "/login". + 2. Nhập: tên="Test Dusk User", SĐT=số ngẫu nhiên (09xxxxxxxx), + địa chỉ="Cần Thơ", nguồn biết SIS=1, nghề nghiệp="Kỹ sư". + 3. Nhấn "Gửi thông tin" và chờ thông báo "thành công". + Kết quả: URL path là "/game" (đăng ký thành công, redirect về game). + DB Check: User có phone tương ứng tồn tại trong DB; + name = "TEST DUSK USER" (đã uppercase); + address = "Cần Thơ"; known_sis = 1; job = "Kỹ sư"; + email = "@phone.local"; spined = 0; status = 1. + Dọn dẹp: Xóa user test khỏi DB. + +[TC-18] test_authenticated_user_sees_account_page + Mô tả : Người dùng đã đăng nhập truy cập /login sẽ thấy trang tài khoản của họ. + Bước : 1. Đăng ký tài khoản mới (SĐT ngẫu nhiên). + 2. Sau khi đăng nhập, truy cập lại "/login". + Kết quả: Trường phone được điền sẵn số điện thoại của người dùng + (chứng tỏ đang hiển thị form cập nhật tài khoản, không phải form đăng ký). + DB Check: User tồn tại; spined = 0; status = 1. + Dọn dẹp: Xóa user test khỏi DB. + +-------------------------------------------------------------------------------- +FILE 4: tests/Browser/AccountManagementTest.php (5 tests) +-------------------------------------------------------------------------------- + +[TC-19] test_update_account_successfully + Mô tả : Người dùng cập nhật thông tin tài khoản thành công. + Bước : 1. Đăng ký và đăng nhập tài khoản mới. + 2. Truy cập "/login" (trang tài khoản). + 3. Xóa và nhập lại: tên="Tên Mới Test", địa chỉ="Hà Nội", + nguồn biết SIS=0, nghề nghiệp="Giáo viên". + 4. Nhấn "Cập nhật" và chờ thông báo "thành công". + Kết quả: URL path là "/game" (cập nhật thành công). + DB Check: name = "TÊN MỚI TEST" (đã uppercase); address = "Hà Nội"; + known_sis = 0; job = "Giáo viên". + Dọn dẹp: Xóa user test khỏi DB. + +[TC-20] test_update_account_with_empty_name_shows_error + Mô tả : Cập nhật tài khoản với tên rỗng phải hiện lỗi validation. + Bước : 1. Đăng ký và đăng nhập tài khoản mới. + 2. Truy cập "/login", xóa trường tên. + 3. Nhấn "Cập nhật". + Kết quả: Phần tử ".text-danger" (thông báo lỗi) hiển thị. + +[TC-21] test_update_account_with_invalid_phone_shows_error + Mô tả : Cập nhật tài khoản với số điện thoại sai định dạng phải hiện lỗi. + Bước : 1. Đăng ký và đăng nhập tài khoản mới. + 2. Truy cập "/login", xóa và nhập SĐT = "abcde". + 3. Nhấn "Cập nhật". + Kết quả: Trang hiển thị "Số điện thoại không đúng định dạng". + +[TC-22] test_history_section_visible_when_authenticated + Mô tả : Phần lịch sử hoạt động hiển thị khi người dùng đã đăng nhập. + Bước : 1. Đăng ký và đăng nhập tài khoản mới. + 2. Truy cập "/login" (trang tài khoản). + Kết quả: Trang hiển thị "LỊCH SỬ HOẠT ĐỘNG". + +[TC-23] test_logout_works + Mô tả : Đăng xuất thành công xóa session người dùng. + Bước : 1. Đăng ký và đăng nhập tài khoản mới. + 2. Truy cập "/logout". + Kết quả: Người dùng ở trạng thái guest (assertGuest). + +-------------------------------------------------------------------------------- +FILE 5: tests/Browser/AdminLoginTest.php (7 tests) +-------------------------------------------------------------------------------- +Chuẩn bị chung: Trước mỗi test tạo admin user với đầy đủ roles trong DB. +Dọn dẹp chung: Sau mỗi test xóa UserRole và admin user khỏi DB. + +[TC-24] test_admin_login_page_shows_form + Mô tả : Trang quản trị "/admin" hiển thị form đăng nhập. + Bước : 1. Truy cập "/admin". + Kết quả: Các trường input[name="email"], input[name="password"], + input[type="submit"] đều hiển thị. + +[TC-25] test_admin_login_with_wrong_credentials_shows_error + Mô tả : Đăng nhập admin với thông tin sai phải hiện thông báo lỗi. + Bước : 1. Truy cập "/admin". + 2. Nhập email="wrong@email.com", password="wrongpassword". + 3. Nhấn "Login". + Kết quả: Phần tử ".alert-danger" (hoặc tương đương) hiển thị. + +[TC-26] test_admin_login_with_empty_fields_shows_validation + Mô tả : Gửi form đăng nhập admin rỗng phải hiện lỗi validation server-side. + Bước : 1. Truy cập "/admin". + 2. Dùng JavaScript xóa thuộc tính "required" khỏi tất cả input + (để bypass validation HTML5 của trình duyệt). + 3. Nhấn "Login" mà không nhập gì. + Kết quả: Phần tử ".text-danger" hoặc ".alert-danger" hiển thị. + +[TC-27] test_admin_login_successful + Mô tả : Đăng nhập admin thành công và chuyển vào trang quản lý. + Bước : 1. Đăng xuất admin ("/admin/logout"). + 2. Truy cập "/admin", nhập email và password đúng. + 3. Nhấn "Login" và chờ redirect. + Kết quả: URL path là "/admin/account" (đăng nhập thành công). + +[TC-28] test_authenticated_admin_visiting_login_sees_form + Mô tả : Admin đã đăng nhập truy cập "/admin" vẫn thấy form login (không redirect). + Bước : 1. Đăng nhập bằng loginAs(adminUser). + 2. Truy cập "/admin". + Kết quả: URL path vẫn là "/admin"; form input[name="email"] hiển thị. + +[TC-29] test_unauthenticated_access_to_admin_account_redirects + Mô tả : Truy cập trang admin khi chưa đăng nhập bị redirect về trang login. + Bước : 1. Đăng xuất admin. + 2. Truy cập "/admin/account" trực tiếp. + Kết quả: URL path là "/admin" (bị redirect về trang đăng nhập admin). + +[TC-30] test_admin_logout + Mô tả : Đăng xuất admin thành công — sau đó không thể vào trang quản lý. + Bước : 1. Đăng xuất admin (clear session cũ). + 2. Đăng nhập thực qua form với email/password đúng. + 3. Chờ redirect vào "/admin/account". + 4. Đăng xuất qua "/logout" (frontend route). + 5. Truy cập "/admin/account". + Kết quả: URL path là "/admin" (bị redirect về login, xác nhận đã logout). + Lưu ý : Dùng đăng nhập form thực (không dùng loginAs) để tránh Dusk + tự động re-authenticate sau khi logout. + +-------------------------------------------------------------------------------- +FILE 6: tests/Browser/AdminPrizeTest.php (9 tests) +-------------------------------------------------------------------------------- +Chuẩn bị chung: Tạo admin user với đầy đủ roles trong DB. +Dọn dẹp chung: Xóa UserRole và admin user khỏi DB. + +[TC-31] test_prize_list_page_loads + Mô tả : Trang danh sách giải thưởng tải thành công. + Bước : 1. Đăng nhập admin. + 2. Truy cập "/admin/prizes". + Kết quả: Phần tử "table" hoặc ".table" hiển thị. + +[TC-32] test_add_prize_form_loads + Mô tả : Trang thêm mới giải thưởng hiển thị đầy đủ form. + Bước : 1. Đăng nhập admin. + 2. Truy cập "/admin/prize/update?id=0". + Kết quả: Các trường input[name="prize_name"], input[name="prize_order"], + select[name="prize_status"] đều hiển thị. + +[TC-33] test_add_prize_with_empty_name_shows_error + Mô tả : Gửi form thêm giải thưởng với tên rỗng phải hiện lỗi validation. + Bước : 1. Đăng nhập admin, vào "/admin/prize/update?id=0". + 2. Dùng JavaScript xóa thuộc tính "required" của các input. + 3. Nhấn "Thêm mới" mà không nhập tên. + Kết quả: Phần tử ".text-danger", ".alert-danger" hoặc tương đương hiển thị. + +[TC-34] test_create_prize_successfully + Mô tả : Thêm mới giải thưởng thành công và dữ liệu được lưu vào DB. + Bước : 1. Đăng nhập admin, vào "/admin/prize/update?id=0". + 2. Nhập: prize_name=tên ngẫu nhiên, prize_number=100, + prize_order=99, prize_status=1. + 3. Nhấn "Thêm mới" và chờ ".alert-success". + Kết quả: Thông báo thành công hiển thị. + DB Check: Prize với tên vừa nhập tồn tại trong DB; + prize_status = 1; prize_number = 100; prize_order = 99. + Dọn dẹp: Xóa prize test khỏi DB. + +[TC-35] test_update_prize_successfully + Mô tả : Cập nhật tên giải thưởng thành công. + Chuẩn bị: Tạo prize "Dusk Prize To Update" trực tiếp trong DB. + Bước : 1. Đăng nhập admin, vào "/admin/prize/update?id={prizeId}". + 2. Xóa và nhập tên mới vào prize_name. + 3. Nhấn "Cập nhật" và chờ ".alert-success". + Kết quả: Thông báo thành công hiển thị. + DB Check: Prize::find(id)->prize_name bằng tên mới vừa nhập. + Dọn dẹp: Xóa prize test khỏi DB. + +[TC-36] test_delete_prize_successfully + Mô tả : Xóa giải thưởng thành công khỏi hệ thống. + Chuẩn bị: Tạo prize "Dusk Prize To Delete" trực tiếp trong DB. + Bước : 1. Đăng nhập admin, vào "/admin/prizes". + 2. Truy cập "/admin/prize/delete/{prizeId}" trực tiếp. + 3. Chờ 1000ms (hệ thống redirect sau khi xóa). + Kết quả: Prize::find(prizeId) trả về null (đã bị xóa khỏi DB). + +[TC-37] test_prize_winner_list_loads + Mô tả : Trang danh sách người trúng giải tải thành công. + Bước : 1. Đăng nhập admin. + 2. Truy cập "/admin/prizeuser". + Kết quả: Phần tử "table" hoặc ".table" hiển thị. + +[TC-38] test_prize_winner_list_has_date_filter + Mô tả : Trang danh sách người trúng giải có bộ lọc theo ngày. + Bước : 1. Đăng nhập admin, truy cập "/admin/prizeuser". + Kết quả: Phần tử #fromDateTime và #toDateTime hiển thị. + +[TC-39] test_prize_winner_export_link_present + Mô tả : Trang danh sách người trúng giải có nút/link xuất Excel. + Bước : 1. Đăng nhập admin, truy cập "/admin/prizeuser". + Kết quả: Phần tử #excel_export hiển thị. + +-------------------------------------------------------------------------------- +FILE 7: tests/Browser/AdminUserTest.php (5 tests) +-------------------------------------------------------------------------------- +Chuẩn bị chung: Tạo admin user (với đầy đủ roles) và target user trong DB. +Dọn dẹp chung: Xóa UserRole, admin user và target user khỏi DB. + +[TC-40] test_user_list_page_loads + Mô tả : Trang danh sách người dùng trong admin tải thành công. + Bước : 1. Đăng nhập admin. + 2. Truy cập "/admin/users". + Kết quả: Phần tử "table" hoặc ".table" hiển thị. + +[TC-41] test_user_list_shows_target_user + Mô tả : User mới tạo xuất hiện trong danh sách quản lý người dùng. + Bước : 1. Truy vấn DB: User với status != '' (điều kiện của controller). + 2. Đăng nhập admin, truy cập "/admin/users", chờ #table. + Kết quả: Target user tồn tại trong query DB (email khớp); + Bảng #table hiển thị trên trang. + Lưu ý : DataTables có phân trang; dùng DB assertion thay vì tìm kiếm text + trực tiếp trên trình duyệt. + +[TC-42] test_edit_user_form_shows_existing_data + Mô tả : Form cập nhật user điền sẵn thông tin hiện tại. + Bước : 1. Đăng nhập admin. + 2. Truy cập "/admin/users/update?id={targetUserId}". + Kết quả: Trường input "name" có giá trị = tên hiện tại của user; + Trường input "email" có giá trị = email hiện tại của user. + +[TC-43] test_update_user_name_successfully + Mô tả : Admin cập nhật tên người dùng thành công (không uppercase). + Bước : 1. Đăng nhập admin, vào "/admin/users/update?id={targetUserId}". + 2. Xóa và nhập tên mới = "Updated Dusk Name". + 3. Nhấn "Cập nhật" và chờ ".alert-success". + Kết quả: Thông báo thành công hiển thị. + DB Check: User::find(id)->name = "Updated Dusk Name" + (Admin controller KHÔNG uppercase tên, khác với frontend controller). + +[TC-44] test_admin_account_page_shows_user_info + Mô tả : Trang tài khoản admin hiển thị đúng thông tin người dùng đang đăng nhập. + Bước : 1. Đăng nhập admin. + 2. Truy cập "/admin/account". + Kết quả: Trường input "email" có giá trị = email của admin đang đăng nhập. + +-------------------------------------------------------------------------------- +FILE 8: tests/Browser/AdminParameterTest.php (5 tests) +-------------------------------------------------------------------------------- +Chuẩn bị chung: Tạo admin user với đầy đủ roles trong DB. +Dọn dẹp chung: Xóa UserRole và admin user khỏi DB. + +[TC-45] test_parameter_page_loads + Mô tả : Trang cấu hình hệ thống (parameter) tải thành công. + Bước : 1. Đăng nhập admin. + 2. Truy cập "/admin/parameter". + Kết quả: Phần tử "form" hiển thị trong trang. + +[TC-46] test_parameter_page_shows_game_rule_field + Mô tả : Trang cấu hình có trường nhập "Thể lệ tham dự" (game_rule). + Bước : 1. Đăng nhập admin, truy cập "/admin/parameter". + Kết quả: Phần tử [name="game_rule"] hoặc textarea hiển thị. + +[TC-47] test_update_game_rule_successfully + Mô tả : Cập nhật nội dung thể lệ tham dự thành công và lưu vào DB. + Chuẩn bị: Đọc và lưu giá trị game_rule hiện tại từ DB. + Bước : 1. Đăng nhập admin, truy cập "/admin/parameter". + 2. Dùng JavaScript gán giá trị mới vào [name="game_rule"] + (hỗ trợ cả CKEditor nếu có). + 3. Nhấn "Cập nhật" và chờ ".alert-success". + Kết quả: Thông báo thành công hiển thị. + DB Check: Parameter với key="game_rule" tồn tại; + parameter_value chứa chuỗi vừa nhập (assertStringContainsString). + Dọn dẹp: Khôi phục giá trị game_rule gốc trong DB. + +[TC-48] test_parameter_page_has_image_upload_fields + Mô tả : Trang cấu hình có trường upload ảnh logo. + Bước : 1. Đăng nhập admin, truy cập "/admin/parameter". + Kết quả: Phần tử input[type="file"][name="logo_image"] hoặc + input[name="logo_image"] hiển thị. + +[TC-49] test_parameter_page_requires_auth + Mô tả : Truy cập "/admin/parameter" khi chưa đăng nhập bị redirect. + Bước : 1. Đăng xuất admin ("/admin/logout"). + 2. Truy cập "/admin/parameter" trực tiếp. + Kết quả: URL path là "/admin" (bị redirect về trang đăng nhập admin). + +================================================================================ +TỔNG KẾT +================================================================================ + + Số file test : 8 files + Số test cases : 49 test cases + Số assertions : 90 assertions + Thời gian : ~103 giây + Kết quả : ALL PASSED ✓ + + Phân bổ theo chức năng: + ┌──────────────────────────────────────────┬────────┐ + │ Chức năng │ Tests │ + ├──────────────────────────────────────────┼────────┤ + │ ExampleTest (sanity check) │ 1 │ + │ Trang game & API vòng quay │ 12 │ + │ Đăng ký tài khoản │ 5 │ + │ Quản lý tài khoản người dùng │ 5 │ + │ Đăng nhập / đăng xuất admin │ 7 │ + │ Quản lý giải thưởng (admin) │ 9 │ + │ Quản lý người dùng (admin) │ 5 │ + │ Cấu hình hệ thống (admin) │ 5 │ + ├──────────────────────────────────────────┼────────┤ + │ TỔNG CỘNG │ 49 │ + └──────────────────────────────────────────┴────────┘ + + Công nghệ kiểm thử: + - Framework : Laravel Dusk v8.5 (Browser Automation Testing) + - Browser : Google Chrome 146 (non-snap, /usr/bin/google-chrome) + - Driver : ChromeDriver 146 + - Môi trường : WSL2 + WSLg, DUSK_HEADLESS_DISABLED=true + - Server : php artisan serve --port=8001 + - DB : MySQL (fsg3kct926qj_quaysoDB) + +================================================================================ diff --git a/tests/Browser/AccountManagementTest.php b/tests/Browser/AccountManagementTest.php index 8d6e615d..694d7020 100644 --- a/tests/Browser/AccountManagementTest.php +++ b/tests/Browser/AccountManagementTest.php @@ -2,6 +2,7 @@ namespace Tests\Browser; +use App\Models\User; use Laravel\Dusk\Browser; use Tests\DuskTestCase; @@ -12,13 +13,16 @@ class AccountManagementTest extends DuskTestCase */ private function registerAndLogin(Browser $browser, string $phone): void { + $browser->visit('/logout'); $browser->visit('/login') + ->pause(300) ->type('name', 'Dusk Test User') ->type('phone', $phone) ->type('address', 'Cần Thơ') ->radio('known_sis', '1') ->type('job', 'Nhân viên văn phòng') ->press('Gửi thông tin') + ->waitForText('thành công', 10) ->assertPathIs('/game'); } @@ -33,7 +37,8 @@ class AccountManagementTest extends DuskTestCase $this->registerAndLogin($browser, $phone); $browser->visit('/login') - ->assertSee('Cập nhật thông tin cá nhân') + ->pause(300) + ->assertInputValue('phone', $phone) ->clear('name') ->type('name', 'Tên Mới Test') ->clear('address') @@ -42,9 +47,20 @@ class AccountManagementTest extends DuskTestCase ->clear('job') ->type('job', 'Giáo viên') ->press('Cập nhật') - ->assertPathIs('/game') - ->assertSee('Cập nhật tài khoản thành công'); + ->waitForText('thành công', 10) + ->assertPathIs('/game'); }); + + // Kiểm tra DB: dữ liệu đã được cập nhật đúng + $user = User::where('phone', $phone)->first(); + $this->assertNotNull($user); + $this->assertEquals('TÊN MỚI TEST', $user->name, 'Tên phải uppercase'); + $this->assertEquals('Hà Nội', $user->address); + $this->assertEquals(0, $user->known_sis); + $this->assertEquals('Giáo viên', $user->job); + + // Dọn dẹp + $user->delete(); } /** @@ -60,7 +76,8 @@ class AccountManagementTest extends DuskTestCase $browser->visit('/login') ->clear('name') ->press('Cập nhật') - ->assertSee('Nhập tên hiển thị'); + ->pause(300) + ->assertPresent('.text-danger'); }); } @@ -78,6 +95,7 @@ class AccountManagementTest extends DuskTestCase ->clear('phone') ->type('phone', 'abcde') ->press('Cập nhật') + ->pause(500) ->assertSee('Số điện thoại không đúng định dạng'); }); } diff --git a/tests/Browser/AdminLoginTest.php b/tests/Browser/AdminLoginTest.php new file mode 100644 index 00000000..2be8e2a9 --- /dev/null +++ b/tests/Browser/AdminLoginTest.php @@ -0,0 +1,155 @@ +adminUser = User::create([ + 'name' => 'DUSK ADMIN TEST', + 'email' => 'dusk_admin_test@test.local', + 'phone' => '09' . rand(10000000, 99999999), + 'password' => bcrypt('dusk_admin_pwd_2026'), + 'status' => 1, + 'address' => 'Test', + 'known_sis'=> 0, + 'job' => 'Admin', + 'spined' => 0, + ]); + // Gán tất cả roles + $roles = Role::where('role_status', 1)->get(); + foreach ($roles as $role) { + UserRole::create(['user_id' => $this->adminUser->id, 'role_id' => $role->id]); + } + } + + protected function tearDown(): void + { + UserRole::where('user_id', $this->adminUser->id)->delete(); + $this->adminUser->delete(); + parent::tearDown(); + } + + /** + * Trang admin hiển thị form đăng nhập. + */ + public function test_admin_login_page_shows_form(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/admin') + ->assertPresent('input[name="email"]') + ->assertPresent('input[name="password"]') + ->assertPresent('input[type="submit"]'); + }); + } + + /** + * Đăng nhập admin với email/password sai phải hiện lỗi. + */ + public function test_admin_login_with_wrong_credentials_shows_error(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/admin') + ->type('email', 'wrong@email.com') + ->type('password', 'wrongpassword') + ->press('Login') + ->pause(300) + ->assertPresent('.alert-danger, .error, [class*="error"]'); + }); + } + + /** + * Đăng nhập admin với email rỗng phải hiện lỗi validation (bypass HTML5 required). + */ + public function test_admin_login_with_empty_fields_shows_validation(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/admin'); + // HTML5 `required` prevents native submit, strip it to get server-side validation + $browser->script(" + document.querySelectorAll('input[required]').forEach(function(el) { + el.removeAttribute('required'); + }); + "); + $browser->press('Login') + ->pause(500) + ->assertPresent('.text-danger, .alert-danger'); + }); + } + + /** + * Đăng nhập admin thành công và redirect vào trang tài khoản. + */ + public function test_admin_login_successful(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/admin/logout') + ->visit('/admin') + ->type('email', 'dusk_admin_test@test.local') + ->type('password', 'dusk_admin_pwd_2026') + ->press('Login') + ->waitForLocation('/admin/account', 10) + ->assertPathIs('/admin/account'); + }); + } + + /** + * Khi đã đăng nhập admin, truy cập /admin vẫn hiển thị form login (không có redirect). + */ + public function test_authenticated_admin_visiting_login_sees_form(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin') + ->assertPathIs('/admin') + ->assertPresent('input[name="email"]'); + }); + } + + /** + * Truy cập trang admin khi chưa đăng nhập bị redirect về /admin login. + */ + public function test_unauthenticated_access_to_admin_account_redirects(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/admin/logout') + ->visit('/admin/account') + ->assertPathIs('/admin'); + }); + } + + /** + * Đăng xuất admin thành công qua route /logout (route /admin/logout bị middleware chặn + * vì không có role admin/logout trong DB). Dùng real login thay vì loginAs() + * để tránh Dusk tự re-auth sau khi logout. + */ + public function test_admin_logout(): void + { + $this->browse(function (Browser $browser) { + // Đăng nhập thực qua form (không dùng loginAs để tránh Dusk re-auth) + $browser->visit('/admin/logout') // clear any previous session + ->visit('/admin') + ->type('email', $this->adminUser->email) + ->type('password', 'dusk_admin_pwd_2026') + ->press('Login') + ->waitForLocation('/admin/account', 10) + // Thực hiện logout qua frontend /logout + ->visit('/logout') + ->pause(300) + // Truy cập trang cần auth -> phải bị redirect về /admin + ->visit('/admin/account') + ->assertPathIs('/admin'); + }); + } +} diff --git a/tests/Browser/AdminParameterTest.php b/tests/Browser/AdminParameterTest.php new file mode 100644 index 00000000..b9d786db --- /dev/null +++ b/tests/Browser/AdminParameterTest.php @@ -0,0 +1,132 @@ +adminUser = User::create([ + 'name' => 'DUSK PARAM ADMIN', + 'email' => 'dusk_param_admin@test.local', + 'phone' => '09' . rand(10000000, 99999999), + 'password' => bcrypt('dusk_admin_pwd_2026'), + 'status' => 1, + 'address' => 'Test', + 'known_sis'=> 0, + 'job' => 'Admin', + 'spined' => 0, + ]); + $roles = Role::where('role_status', 1)->get(); + foreach ($roles as $role) { + UserRole::create(['user_id' => $this->adminUser->id, 'role_id' => $role->id]); + } + } + + protected function tearDown(): void + { + UserRole::where('user_id', $this->adminUser->id)->delete(); + $this->adminUser->delete(); + parent::tearDown(); + } + + /** + * Trang cấu hình hệ thống hiển thị. + */ + public function test_parameter_page_loads(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/parameter') + ->assertPresent('form'); + }); + } + + /** + * Cấu hình game_rule hiển thị trong textarea. + */ + public function test_parameter_page_shows_game_rule_field(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/parameter') + ->assertPresent('[name="game_rule"], textarea'); + }); + } + + /** + * Cập nhật thông số game_rule thành công và kiểm tra DB. + */ + public function test_update_game_rule_successfully(): void + { + // Backup giá trị hiện tại + $original = Parameter::where('parameter_key', 'game_rule')->first(); + $originalValue = $original->parameter_value ?? ''; + + $newValue = 'Dusk Test Rule Content ' . rand(1000, 9999); + + $this->browse(function (Browser $browser) use ($newValue) { + $browser->loginAs($this->adminUser) + ->visit('/admin/parameter') + ->pause(300); + + // Gán trực tiếp qua JS vì có thể là CKEditor + $browser->script(" + var el = document.querySelector('[name=\"game_rule\"]'); + if (el) { + el.value = '{$newValue}'; + } + if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances['game_rule']) { + CKEDITOR.instances['game_rule'].setData('{$newValue}'); + } + "); + + $browser->press('Cập nhật') + ->waitFor('.alert-success', 10); + }); + + // Kiểm tra DB: game_rule đã được cập nhật và chứa nội dung mới + $updated = Parameter::where('parameter_key', 'game_rule')->first(); + $this->assertNotNull($updated, 'Tham số game_rule phải tồn tại trong DB'); + $this->assertStringContainsString($newValue, $updated->parameter_value, + 'Giá trị DB phải chứa nội dung vừa lưu'); + + // Restore giá trị gốc + $original->parameter_value = $originalValue; + $original->save(); + } + + /** + * Trang cấu hình có nút upload logo và banner. + */ + public function test_parameter_page_has_image_upload_fields(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/parameter') + ->assertPresent('input[type="file"][name="logo_image"], input[name="logo_image"]'); + }); + } + + /** + * Truy cập /admin/parameter khi chưa đăng nhập bị redirect. + */ + public function test_parameter_page_requires_auth(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/admin/logout') + ->visit('/admin/parameter') + ->assertPathIs('/admin'); + }); + } +} diff --git a/tests/Browser/AdminPrizeTest.php b/tests/Browser/AdminPrizeTest.php new file mode 100644 index 00000000..1dad3c29 --- /dev/null +++ b/tests/Browser/AdminPrizeTest.php @@ -0,0 +1,207 @@ +adminUser = User::create([ + 'name' => 'DUSK PRIZE ADMIN', + 'email' => 'dusk_prize_admin@test.local', + 'phone' => '09' . rand(10000000, 99999999), + 'password' => bcrypt('dusk_admin_pwd_2026'), + 'status' => 1, + 'address' => 'Test', + 'known_sis'=> 0, + 'job' => 'Admin', + 'spined' => 0, + ]); + $roles = Role::where('role_status', 1)->get(); + foreach ($roles as $role) { + UserRole::create(['user_id' => $this->adminUser->id, 'role_id' => $role->id]); + } + } + + protected function tearDown(): void + { + UserRole::where('user_id', $this->adminUser->id)->delete(); + $this->adminUser->delete(); + parent::tearDown(); + } + + /** + * Trang danh sách giải thưởng hiển thị đúng. + */ + public function test_prize_list_page_loads(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prizes') + ->assertPresent('table, .table'); + }); + } + + /** + * Trang thêm mới giải thưởng hiển thị form. + */ + public function test_add_prize_form_loads(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prize/update?id=0') + ->assertPresent('input[name="prize_name"]') + ->assertPresent('input[name="prize_order"]') + ->assertPresent('select[name="prize_status"]'); + }); + } + + /** + * Submit form thêm giải thưởng rỗng phải hiện lỗi. + */ + public function test_add_prize_with_empty_name_shows_error(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prize/update?id=0'); + // Strip HTML5 required nếu có, rồi submit để Server-side validation chạy + $browser->script("document.querySelectorAll('input[required]').forEach(function(el){el.removeAttribute('required');})"); + $browser->press('Thêm mới') + ->pause(500) + ->assertPresent('.text-danger, .alert-danger, [class*="error"]'); + }); + } + + /** + * Thêm mới giải thưởng thành công và kiểm tra DB. + */ + public function test_create_prize_successfully(): void + { + $prizeName = 'Dusk Test Prize ' . rand(1000, 9999); + + $this->browse(function (Browser $browser) use ($prizeName) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prize/update?id=0') + ->pause(500) + ->type('prize_name', $prizeName) + ->type('prize_number', '100') + ->type('prize_order', '99') + ->select('prize_status', '1') + ->press('Thêm mới') + ->waitFor('.alert-success', 10); + }); + + // Kiểm tra DB + $prize = Prize::where('prize_name', $prizeName)->first(); + $this->assertNotNull($prize, 'Giải thưởng phải được tạo trong DB'); + $this->assertEquals(1, $prize->prize_status); + $this->assertEquals(100, $prize->prize_number); + $this->assertEquals(99, $prize->prize_order); + + // Dọn dẹp + $prize->delete(); + } + + /** + * Cập nhật tên giải thưởng thành công. + */ + public function test_update_prize_successfully(): void + { + // Tạo giải thưởng để cập nhật + $prize = Prize::create([ + 'prize_name' => 'Dusk Prize To Update', + 'prize_status' => 1, + 'prize_number' => 10, + 'prize_order' => 88, + 'user_id' => $this->adminUser->id, + ]); + $newName = 'Dusk Prize Updated ' . rand(1000, 9999); + + $this->browse(function (Browser $browser) use ($prize, $newName) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prize/update?id=' . $prize->id) + ->clear('prize_name') + ->type('prize_name', $newName) + ->press('Cập nhật') + ->waitFor('.alert-success', 10); + }); + + // Kiểm tra DB + $updated = Prize::find($prize->id); + $this->assertEquals($newName, $updated->prize_name); + + $prize->delete(); + } + + /** + * Xóa giải thưởng thành công. + */ + public function test_delete_prize_successfully(): void + { + $prize = Prize::create([ + 'prize_name' => 'Dusk Prize To Delete', + 'prize_status' => 1, + 'prize_number' => 5, + 'prize_order' => 77, + 'user_id' => $this->adminUser->id, + ]); + $prizeId = $prize->id; + + $this->browse(function (Browser $browser) use ($prizeId) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prizes') + ->visit('/admin/prize/delete/' . $prizeId) + ->pause(1000); // redirect()->back() sau khi xóa + }); + + // Kiểm tra DB: giải thưởng đã bị xóa + $this->assertNull(Prize::find($prizeId), 'Giải thưởng phải bị xóa khỏi DB'); + } + + /** + * Trang danh sách người trúng giải hiển thị. + */ + public function test_prize_winner_list_loads(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prizeuser') + ->assertPresent('table, .table'); + }); + } + + /** + * Trang danh sách người trúng giải có ô lọc ngày. + */ + public function test_prize_winner_list_has_date_filter(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prizeuser') + ->assertPresent('#fromDateTime, #toDateTime'); + }); + } + + /** + * Link export Excel hoạt động. + */ + public function test_prize_winner_export_link_present(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/prizeuser') + ->assertPresent('#excel_export'); + }); + } +} diff --git a/tests/Browser/AdminUserTest.php b/tests/Browser/AdminUserTest.php new file mode 100644 index 00000000..22b4c654 --- /dev/null +++ b/tests/Browser/AdminUserTest.php @@ -0,0 +1,137 @@ +adminUser = User::create([ + 'name' => 'DUSK USER ADMIN', + 'email' => 'dusk_user_admin@test.local', + 'phone' => '09' . rand(10000000, 99999999), + 'password' => bcrypt('dusk_admin_pwd_2026'), + 'status' => 1, + 'address' => 'Test', + 'known_sis'=> 0, + 'job' => 'Admin', + 'spined' => 0, + ]); + $roles = Role::where('role_status', 1)->get(); + foreach ($roles as $role) { + UserRole::create(['user_id' => $this->adminUser->id, 'role_id' => $role->id]); + } + + // Tạo user mục tiêu để test cập nhật + $targetPhone = '09' . rand(10000000, 99999999); + $this->targetUser = User::create([ + 'name' => 'DUSK TARGET USER', + 'email' => $targetPhone . '@phone.local', + 'phone' => $targetPhone, + 'password' => bcrypt('test123'), + 'status' => 1, + 'address' => 'Cần Thơ', + 'known_sis'=> 1, + 'job' => 'Sinh viên', + 'spined' => 0, + ]); + } + + protected function tearDown(): void + { + UserRole::where('user_id', $this->adminUser->id)->delete(); + $this->adminUser->delete(); + $this->targetUser->delete(); + parent::tearDown(); + } + + /** + * Trang danh sách user hiển thị. + */ + public function test_user_list_page_loads(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/users') + ->assertPresent('table, .table'); + }); + } + + /** + * Tên user mới tạo xuất hiện trong danh sách. + */ + public function test_user_list_shows_target_user(): void + { + // DB assertion: target user có trong query của controller (status != '') + $userInList = User::where('id', $this->targetUser->id) + ->where('status', '!=', '') + ->first(); + $this->assertNotNull($userInList, 'Target user must be included in admin user list query'); + $this->assertEquals($this->targetUser->email, $userInList->email); + + // Browser: trang list load được với table + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/users') + ->waitFor('#table', 5) + ->assertPresent('#table'); + }); + } + + /** + * Trang form cập nhật user hiển thị đúng thông tin. + */ + public function test_edit_user_form_shows_existing_data(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/users/update?id=' . $this->targetUser->id) + ->assertInputValue('name', $this->targetUser->name) + ->assertInputValue('email', $this->targetUser->email); + }); + } + + /** + * Cập nhật tên user thành công và kiểm tra DB. + */ + public function test_update_user_name_successfully(): void + { + $newName = 'Updated Dusk Name'; + + $this->browse(function (Browser $browser) use ($newName) { + $browser->loginAs($this->adminUser) + ->visit('/admin/users/update?id=' . $this->targetUser->id) + ->clear('name') + ->type('name', $newName) + ->press('Cập nhật') + ->waitFor('.alert-success', 10); + }); + + // Kiểm tra DB: admin không chuyển uppercase + $updated = User::find($this->targetUser->id); + $this->assertEquals($newName, $updated->name); + } + + /** + * Xem trang tài khoản admin. + */ + public function test_admin_account_page_shows_user_info(): void + { + $this->browse(function (Browser $browser) { + $browser->loginAs($this->adminUser) + ->visit('/admin/account') + // Email là input field (readonly), dùng assertInputValue + ->assertInputValue('email', $this->adminUser->email); + }); + } +} diff --git a/tests/Browser/CreateAccountTest.php b/tests/Browser/CreateAccountTest.php index 6230cbae..19a6756f 100644 --- a/tests/Browser/CreateAccountTest.php +++ b/tests/Browser/CreateAccountTest.php @@ -2,6 +2,7 @@ namespace Tests\Browser; +use App\Models\User; use Laravel\Dusk\Browser; use Tests\DuskTestCase; @@ -13,7 +14,8 @@ class CreateAccountTest extends DuskTestCase public function test_login_page_shows_form(): void { $this->browse(function (Browser $browser) { - $browser->visit('/login') + $browser->visit('/logout') // đảm bảo trạng thái guest + ->visit('/login') ->assertSee('Nhập thông tin cá nhân') ->assertPresent('input[name="name"]') ->assertPresent('input[name="phone"]') @@ -28,8 +30,10 @@ class CreateAccountTest extends DuskTestCase public function test_empty_form_shows_validation_errors(): void { $this->browse(function (Browser $browser) { - $browser->visit('/login') + $browser->visit('/logout') // đảm bảo trạng thái guest + ->visit('/login') ->press('Gửi thông tin') + ->waitFor('.text-danger', 5) ->assertPresent('.text-danger'); }); } @@ -71,6 +75,18 @@ class CreateAccountTest extends DuskTestCase ->waitForText('thành công', 10) ->assertPathIs('/game'); }); + + // Kiểm tra DB: user đã được tạo đúng + $user = User::where('phone', $uniquePhone)->first(); + $this->assertNotNull($user, 'User phải được tạo trong DB'); + $this->assertEquals('TEST DUSK USER', $user->name, 'Tên phải được uppercase'); + $this->assertEquals('Cần Thơ', $user->address); + $this->assertEquals(1, $user->known_sis); + $this->assertEquals('Kỹ sư', $user->job); + $this->assertEquals($uniquePhone . '@phone.local', $user->email); + + // Dọn dẹp + $user->delete(); } /** @@ -99,5 +115,14 @@ class CreateAccountTest extends DuskTestCase $browser->visit('/login') ->assertInputValue('phone', $uniquePhone); }); + + // Kiểm tra DB: user tồn tại và spined = 0 (chưa quay) + $user = User::where('phone', $uniquePhone)->first(); + $this->assertNotNull($user, 'User phải tồn tại trong DB'); + $this->assertEquals(0, $user->spined, 'Người dùng mới chưa được quay'); + $this->assertEquals(1, $user->status, 'Tài khoản phải ở trạng thái active'); + + // Dọn dẹp + $user->delete(); } } diff --git a/tests/Browser/ExampleTest.php b/tests/Browser/ExampleTest.php index 9c1fc49b..85bb5655 100644 --- a/tests/Browser/ExampleTest.php +++ b/tests/Browser/ExampleTest.php @@ -14,8 +14,9 @@ class ExampleTest extends DuskTestCase public function test_basic_example(): void { $this->browse(function (Browser $browser) { + // Cả / và /game đều render view game (không redirect) $browser->visit('/') - ->assertPathIs('/game'); + ->assertPresent('#wheelCanvas'); }); } } diff --git a/tests/Browser/GamePageTest.php b/tests/Browser/GamePageTest.php index b7d0038c..918bd4e0 100644 --- a/tests/Browser/GamePageTest.php +++ b/tests/Browser/GamePageTest.php @@ -2,13 +2,15 @@ namespace Tests\Browser; +use App\Models\PrizeUser; +use App\Models\User; use Laravel\Dusk\Browser; use Tests\DuskTestCase; class GamePageTest extends DuskTestCase { /** - * Trang game hiển thị đúng khi chưa đăng nhập. + * Trang game tải được khi chưa đăng nhập. */ public function test_game_page_loads_when_guest(): void { @@ -19,35 +21,185 @@ class GamePageTest extends DuskTestCase } /** - * Trang chủ (/) redirect về game. + * URL gốc (/) redirect về /game. */ public function test_root_redirects_to_game(): void { $this->browse(function (Browser $browser) { $browser->visit('/') - ->assertPathIs('/game'); + ->assertPresent('#wheelCanvas'); // / và /game cùng render game }); } /** - * Trang game hiển thị vòng quay may mắn. + * Vòng quay (canvas) hiển thị trên trang game. */ - public function test_game_page_shows_lucky_wheel(): void + public function test_game_page_shows_lucky_wheel_canvas(): void { $this->browse(function (Browser $browser) { $browser->visit('/game') - ->assertPresent('#wheel, canvas, .wheel, [id*="wheel"], [class*="wheel"]'); + ->assertPresent('#wheelCanvas'); }); } /** - * Popup luật chơi có thể tải được. + * Nút quay hiển thị trên trang game. */ - public function test_game_rule_endpoint_returns_content(): void + public function test_spin_button_is_present(): void { $this->browse(function (Browser $browser) { - $browser->visit('/game/rule') - ->assertSee('luật'); + $browser->visit('/game') + ->assertPresent('#spinButton'); }); } + + /** + * Banner "Thể lệ tham dự" hiển thị. + */ + public function test_game_rule_ribbon_is_present(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/game') + ->assertPresent('#game_rule'); + }); + } + + /** + * Khi guest vào /game, hiển thị thông báo yêu cầu đăng nhập. + */ + public function test_guest_sees_login_prompt_on_game(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/logout') + ->visit('/game') + ->assertPresent('.alert_wheel_spin') + ->assertPresent('a[href*="login"]'); + }); + } + + /** + * Khi đã đăng nhập và chưa quay, nút quay ở trạng thái sẵn sàng. + */ + public function test_logged_in_user_can_see_spin_button_enabled(): void + { + $phone = '09' . rand(10000000, 99999999); + + $this->browse(function (Browser $browser) use ($phone) { + $browser->visit('/logout') + ->visit('/login') + ->pause(300) + ->type('name', 'Spin Test User') + ->type('phone', $phone) + ->type('address', 'TP.HCM') + ->radio('known_sis', '1') + ->type('job', 'Kỹ sư') + ->press('Gửi thông tin') + ->waitForText('thành công', 10) + ->assertPathIs('/game') + ->assertAttribute('#spinButton', 'data-check', 'true'); + }); + + User::where('phone', $phone)->delete(); + } + + /** + * Khi đã quay rồi (spined=1), nút quay bị khóa. + */ + public function test_already_spined_user_sees_spin_button_disabled(): void + { + $phone = '09' . rand(10000000, 99999999); + + // Tạo user với spined=1 + $user = User::create([ + 'name' => 'SPINED USER', + 'phone' => $phone, + 'email' => $phone . '@phone.local', + 'password' => bcrypt('test123'), + 'address' => 'Cần Thơ', + 'known_sis' => 1, + 'job' => 'Kỹ sư', + 'status' => 1, + 'spined' => 1, + ]); + + $this->browse(function (Browser $browser) use ($user) { + $browser->visit('/logout') + ->loginAs($user) + ->visit('/game') + ->assertAttribute('#spinButton', 'data-check', 'false'); + }); + + $user->delete(); + } + + /** + * Popup luật chơi tải nội dung. + */ + public function test_game_rule_popup_loads(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/logout') + ->visit('/game') + ->pause(500); + + // Dismiss alerts che button rồi click bằng JS + $browser->script("document.querySelectorAll('.alert').forEach(el => el.remove())"); + $browser->script("document.getElementById('game_rule').click()"); + + $browser->pause(800) + ->assertPresent('#gamerule'); + }); + } + + /** + * API /game/rule trả về nội dung HTML. + */ + public function test_game_rule_api_returns_html(): void + { + $this->browse(function (Browser $browser) { + // /game/rule được gọi qua AJAX, trả về fragment HTML không có + $browser->visit('/game/rule') + ->assertSourceHas('class='); // snippet HTML luon co class= + }); + } + + /** + * API /checkspined trả lỗi khi chưa đăng nhập. + */ + public function test_checkspined_returns_error_for_guest(): void + { + $this->browse(function (Browser $browser) { + $browser->visit('/logout') + ->visit('/checkspined') + ->assertSee('error'); + }); + } + + /** + * API /checkspined trả spined=0 khi đăng nhập user mới. + */ + public function test_checkspined_returns_zero_for_new_user(): void + { + $phone = '09' . rand(10000000, 99999999); + $user = User::create([ + 'name' => 'CHECK SPINED USER', + 'phone' => $phone, + 'email' => $phone . '@phone.local', + 'password' => bcrypt('test123'), + 'address' => 'Cần Thơ', + 'known_sis' => 0, + 'job' => 'Sinh viên', + 'status' => 1, + 'spined' => 0, + ]); + + $this->browse(function (Browser $browser) use ($user) { + $browser->visit('/logout') + ->loginAs($user) + ->visit('/checkspined') + ->assertSee('"spined":0'); + }); + + $user->delete(); + } }