gameUser = User::create([ 'name' => 'DUSK GAME LOGIC USER', 'phone' => $phone, 'email' => $phone . '@phone.local', 'password' => bcrypt('test123'), 'address' => 'Cần Thơ', 'known_sis' => 1, 'job' => 'Tester', 'status' => 1, 'spined' => 0, ]); // Tạo giải thưởng với prize_number rất lớn để wheel luôn có ít nhất 1 segment $this->testPrize = Prize::create([ 'prize_name' => 'Dusk Logic Test Prize', 'prize_status' => 1, 'prize_number' => 9999, 'prize_order' => 99, 'user_id' => 0, ]); } /** * Dọn dẹp tất cả dữ liệu test sau mỗi test method. */ protected function tearDown(): void { PrizeUser::where('user_id', $this->gameUser->id)->delete(); $this->gameUser->delete(); $this->testPrize->delete(); parent::tearDown(); } // ========================================================================= // [TC-50] Ngăn click spam lên nút quay // ========================================================================= /** * Click liên tục vào #spinButton phải bị chặn bởi cờ isSpinning trong game.js — * chỉ tạo đúng 1 bản ghi PrizeUser dù kích nhiều lần. * * Cơ chế bảo vệ trong luckywheel.blade.php: * let isSpinning = false; * spinButton.addEventListener("click", (event) => { * if (!isSpinning) { * isSpinning = true; // khóa ngay sau click đầu * spinWheel(); * } * }); * * LƯU Ý THỜI GIAN: Spin thực tế mất 10–15 giây. * waitForText timeout = 30 giây để bao phủ. */ public function test_prevent_double_click_spam_on_spin_button(): void { $this->browse(function (Browser $browser) { $browser->loginAs($this->gameUser) ->visit('/game') ->pause(800); // Chờ wheel canvas render xong // Kích 4 lần click liên tiếp trong cùng một JS execution tick. // Click #1 → isSpinning = true, vòng quay bắt đầu. // Click #2, #3, #4 → bị block bởi guard `if (!isSpinning)`. $browser->script(" var btn = document.getElementById('spinButton'); btn.click(); btn.click(); btn.click(); btn.click(); "); // Chờ popup kết quả xuất hiện (AJAX /updateprizeuser trả về luckywheel_prize view) $browser->waitForText('XIN CHÚC MỪNG', 30); // Buffer thêm để DB transaction commit hoàn tất sau AJAX success callback $browser->pause(1500); }); // Assertion: chỉ đúng 1 bản ghi PrizeUser được tạo // → Chứng minh 3 click sau bị isSpinning guard ngăn thành công $count = PrizeUser::where('user_id', $this->gameUser->id)->count(); $this->assertEquals( 1, $count, "Phải có đúng 1 bản ghi PrizeUser — click spam bị ngăn bởi cờ isSpinning" ); } // ========================================================================= // [TC-51] Luồng quay hoàn chỉnh // ========================================================================= /** * Luồng quay đầy đủ: click #spinButton → animation → popup kết quả → * DB cập nhật user.spined = 1 và tạo bản ghi PrizeUser có user_id khớp. * * LƯU Ý PLACEHOLDER: Selector `.reward-popup-class` trong yêu cầu ban đầu * ánh xạ tới `#result` + text "XIN CHÚC MỪNG" trong app này. * Nếu UI thay đổi, cập nhật waitForText() cho phù hợp. */ public function test_complete_spin_reward_flow(): void { $this->browse(function (Browser $browser) { $browser->loginAs($this->gameUser) ->visit('/game') ->pause(800); // Chờ wheel canvas render xong // Xác nhận nút quay ở trạng thái sẵn sàng (user chưa quay) $browser->assertAttribute('#spinButton', 'data-check', 'true'); // Click nút quay (1 lần duy nhất) $browser->click('#spinButton'); // Chờ div#result hiển thị popup kết quả (AJAX response từ /updateprizeuser) // → View luckywheel_prize.blade.php render text "XIN CHÚC MỪNG" $browser->waitForText('XIN CHÚC MỪNG', 30); // Buffer để đảm bảo DB write hoàn tất sau AJAX success handler $browser->pause(1500); }); // DB assertion 1: user.spined phải là 1 sau khi quay xong $user = User::find($this->gameUser->id); $this->assertEquals( 1, $user->spined, "user.spined phải được cập nhật thành 1 sau khi quay thành công" ); // DB assertion 2: bản ghi PrizeUser phải tồn tại với user_id khớp $prizeUser = PrizeUser::where('user_id', $this->gameUser->id)->first(); $this->assertNotNull( $prizeUser, "Phải có bản ghi PrizeUser trong DB sau khi quay thành công" ); $this->assertEquals( $this->gameUser->id, $prizeUser->user_id, "PrizeUser.user_id phải khớp với người dùng vừa quay" ); } // ========================================================================= // [TC-52] Vòng quay khi hết giải thưởng (prize_number = 0) // ========================================================================= /** * Kiểm tra trạng thái trang game khi TẤT CẢ giải thưởng đã hết (prize_number = 0). * * HÀNH VI HIỆN TẠI được kiểm tra: * - Controller không thêm segment nào vào prize_arr → game_data = "[]" * - Wheel canvas render rỗng nhưng #spinButton vẫn hiển thị * * TODO — HÀNH VI MONG MUỐN (cần implement thêm vào frontend): * Option A: Disable #spinButton với thông báo "Hết giải thưởng" * Option B: Hiển thị element có class .no-prize-message * Khi implement xong, bỏ comment các assertions phía dưới. * * Dữ liệu được KHÔI PHỤC trong finally block dù test pass hay fail. */ public function test_spin_when_out_of_stock(): void { // Lưu trữ prize_number gốc của tất cả giải thưởng $originalNumbers = Prize::pluck('prize_number', 'id')->toArray(); // Đặt prize_number = 0 cho TẤT CẢ giải thưởng → mô phỏng "hết hàng" Prize::query()->update(['prize_number' => 0]); try { $this->browse(function (Browser $browser) { $browser->loginAs($this->gameUser) ->visit('/game') ->pause(500); // HÀNH VI HIỆN TẠI: page source chứa game_data rỗng // vì IndexController không có prize nào thỏa count_user < prize_number (= 0) $browser->assertSourceHas('game_data = "[]"'); // Trang vẫn tải bình thường (không crash) $browser->assertPresent('#wheelCanvas'); $browser->assertPresent('#spinButton'); // TODO: Bỏ comment khi đã thêm xử lý "hết giải" vào frontend: // $browser->assertSee('Đã hết giải thưởng'); // $browser->assertPresent('.no-prize-message'); // $browser->assertAttribute('#spinButton', 'disabled', 'disabled'); }); } finally { // LUÔN khôi phục dữ liệu gốc — kể cả khi test fail foreach ($originalNumbers as $id => $number) { Prize::where('id', $id)->update(['prize_number' => $number]); } } } }