228 lines
8.9 KiB
PHP
228 lines
8.9 KiB
PHP
<?php
|
||
|
||
namespace Tests\Browser;
|
||
|
||
use App\Models\Prize;
|
||
use App\Models\PrizeUser;
|
||
use App\Models\User;
|
||
use Laravel\Dusk\Browser;
|
||
use Tests\DuskTestCase;
|
||
|
||
/**
|
||
* GameLogicTest — Kiểm thử logic trò chơi vòng quay may mắn.
|
||
*
|
||
* Yêu cầu: Server đang chạy trên APP_URL (php artisan serve --port=8001)
|
||
*
|
||
* [TC-50] test_prevent_double_click_spam_on_spin_button
|
||
* [TC-51] test_complete_spin_reward_flow
|
||
* [TC-52] test_spin_when_out_of_stock
|
||
*/
|
||
class GameLogicTest extends DuskTestCase
|
||
{
|
||
private User $gameUser;
|
||
private Prize $testPrize;
|
||
|
||
/**
|
||
* Tạo user chưa quay (spined=0) và một giải thưởng chắc chắn có sẵn trước mỗi test.
|
||
*/
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
$phone = '09' . rand(10000000, 99999999);
|
||
$this->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]);
|
||
}
|
||
}
|
||
}
|
||
}
|