Bạn kiểm thử được gần như toàn bộ tích hợp mà không cần portal, nhờ chế độ standalone của SDK. Những gì chỉ portal mới có (luật chống gian lận, cooldown quảng cáo, lưu dữ liệu trên server) thì kiểm trên portal thử nghiệm.
Chế độ standalone
Mở game trực tiếp, không qua iframe của portal, SDK chạy ở chế độ standalone:
- Người chơi là
local-<id>, giữ tronglocalStorage(khoáwsg:local-id). - Bảng xếp hạng ở
localStoragekhoáwsg:<gameId>:lb; storage ởwsg:<gameId>:storage:<key>. Xoá các khoá này trong DevTools để bắt đầu lại từ đầu. - Quảng cáo giả chờ 1 giây rồi trả
shown: true; rewarded luôn córewarded: true. - Chuyển tab vẫn phát
visibilityvàpause/resume. - Không có luật chống gian lận, cooldown quảng cáo hay giới hạn sự kiện của server.
Khởi tạo với debug: true để xem log [WSG] trong console (lời gọi bị bỏ qua, tên track sai, nộp điểm ngoài khung gameplay…). Thêm ?lang=en vào URL để kiểm thử ngôn ngữ, hoặc tham số riêng của bạn (?level=3) rồi đọc bằng WSG.getEntryParams().
Chạy trên máy với cấu trúc giống production
Trên origin game, SDK nằm ở /sdk/v1/wsg-sdk.js còn game nằm ở /<slug>/<version>/. Dựng lại đúng cấu trúc đó trên máy để bắt sớm lỗi đường dẫn tuyệt đối:
mkdir -p play-local/sdk/v1 play-local/tap-rush/1.0.0
curl -o play-local/sdk/v1/wsg-sdk.js https://games.leonogames.com/sdk/v1/wsg-sdk.js
cp -R dist/. play-local/tap-rush/1.0.0/
python3 -m http.server 8080 --directory play-local
# mở http://localhost:8080/tap-rush/1.0.0/Bất kỳ static server nào cũng được (npx serve play-local, nginx…), miễn phục vụ thư mục play-local ở gốc. Khi game chạy đúng ở đây, nó sẽ chạy đúng dưới https://games.leonogames.com.
Mẹo
Trong lúc phát triển với dev server riêng (Vite, webpack), bạn có thể tạm trỏ thẻ script tới URL đầy đủ https://games.leonogames.com/sdk/v1/wsg-sdk.js. Nhớ đổi lại /sdk/v1/wsg-sdk.js trước khi build bản nộp.
Chạy trong portal
Studio bên ngoài: gửi bản build cho đội vận hành để đưa lên môi trường thử nghiệm. Khi bản đó đã lên origin game, bạn mở trực tiếp được tại https://games.leonogames.com/<slug>/<version>/index.html (chạy standalone); khi game được mở cho người chơi, trang của nó nằm ở https://leonogames.com/games/<slug>.
Chế độ test của portal: mở trang game với ?wsg_test=1, ví dụ https://leonogames.com/games/<slug>?wsg_test=1. Portal chuyển tham số này vào URL của iframe, nên test hook kill() hoạt động được trong portal và WSG.getEntryParams() có wsg_test: '1'.
Những thứ nên kiểm trên portal:
WSG.environmentlàportal; overlay tải biến mất đúng lúc gọiloadingFinished().- Điểm được nhận (xem
reasonnếu bị từ chối); tab bảng xếp hạng dưới khung game cập nhật sau khi nộp. - Hai interstitial liên tiếp: lần hai trả
cooldown. Bấm Esc trong lúc rewarded chạy:dismissed, không thưởng. - Chuyển tab: portal phủ overlay "Đang tạm dừng", game nhận
pauserồiresume. - Tải lại trang và chơi tiếp: dữ liệu
WSG.storagevẫn còn.
Đội nội bộ có repo chạy được cả hệ thống ở máy:
pnpm install
pnpm db:migrate && pnpm db:seed # cần Postgres và Redis đang chạy
pnpm sync:sdk # build SDK cho games-server
pnpm dev # api :4100, web :3000, games-server :4000
pnpm wsg publish games/my-game # terminal khác; đọc WSG_ADMIN_TOKEN... từ .env ở gốc repo
# mở http://localhost:3000/games/my-gameTest hooks
Test hooks là một object gắn ở window.__WSG_TEST__ để công cụ tự động (Playwright, pipeline kiểm thử của portal) điều khiển game mà không cần bấm vào canvas. Bắt buộc với game trong repo WebSuperGames (template đã cài sẵn), khuyến nghị với studio bên ngoài vì giúp đội vận hành duyệt game nhanh hơn.
interface WsgTestHooks {
ready: true;
getState(): {
scene: 'boot' | 'preload' | 'menu' | 'game' | 'gameover' | string;
score: number;
alive: boolean;
[k: string]: unknown;
};
start(): void; // từ menu (hoặc game over) vào gameplay
input(action: 'tap' | 'left' | 'right' | 'up' | 'down' | 'pause', opts?: { x?: number; y?: number }): void;
setSeed(seed: number): void; // RNG xác định, gọi trước start()
setTimeScale(scale: number): void; // tăng tốc để test nhanh, ví dụ 5
kill(): void; // ép game over ngay
}- Gắn hooks sau khi menu sẵn sàng (sau
loadingFinished()), vớiready: true. getState().scenelà tên màn hình hiện tại viết thường; có thể thêm trường riêng của game.kill()chỉ được có tác dụng khiWSG.environment !== 'portal'hoặc URL có?wsg_test=1, để không ai ép game over trên portal thật.
Ví dụ thêm hooks vào game Tap Rush ở trang Bắt đầu nhanh. Trước hết sửa hai chỗ trong game.js: thêm let random = Math.random; rồi dùng random() thay cho Math.random() trong moveTarget(); thêm timeScale: 1 vào state và đổi dòng trừ thời gian thành state.timeLeft -= dt * state.timeScale;. Sau đó thêm đoạn dưới và gọi installTestHooks() ở cuối boot():
function mulberry32(seed) {
let a = seed >>> 0;
return function () {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function installTestHooks() {
window.__WSG_TEST__ = {
ready: true,
getState: () => ({
scene: state.phase === 'playing' ? 'game' : state.phase === 'over' ? 'gameover' : 'menu',
score: state.score,
alive: state.phase === 'playing',
timeLeft: state.timeLeft,
}),
start: () => {
if (state.phase !== 'playing' && !state.busy) startRound();
},
input: (action) => {
if (action === 'tap' && state.phase === 'playing' && !state.paused) {
state.score += 1;
moveTarget();
}
},
setSeed: (seed) => {
random = mulberry32(seed);
},
setTimeScale: (scale) => {
state.timeScale = scale;
},
kill: () => {
const testMode = new URLSearchParams(location.search).get('wsg_test') === '1';
if (WSG.environment === 'portal' && !testMode) return; // không ép game over trên portal thật
if (state.phase === 'playing') state.timeLeft = 0;
},
};
}Game conformance
_conformance là một trang HTML và JavaScript thuần, không build, gọi lần lượt mọi API của SDK và hiện kết quả từng bước (mỗi bước một ô có data-testid là tên hàm và data-status là ok hoặc fail), đồng thời đếm các sự kiện pause, resume, audio, visibility, authchange. Trang có nút để chạy tay từng API, và ?autorun=1 để chạy hết một lượt khi mở trực tiếp.
Thứ tự các bước: init → loadingStart → loadingProgress(50) → loadingFinished → gameplayStart → track('conformance_ping') → storage.set({ k: 1 }) → storage.get('k') → leaderboard.submit(<1..1000>) → leaderboard.top → leaderboard.me → gameplayStop → ads.interstitial → ads.interstitial lần hai (trên portal mong cooldown) → ads.rewarded → share → player.getToken.
Dùng nó làm mẫu tham chiếu khi so hành vi của game bạn: trên portal tại https://leonogames.com/games/_conformance (nếu môi trường có phát hành), hoặc mở trực tiếp https://games.leonogames.com/_conformance/<version>/?autorun=1 (phiên bản hiện tại ghi trên trang game). Mã nguồn nằm ở games/_conformance trong repo.
Smoke test với Playwright
Mẫu smoke test chuẩn của portal: mở game, chờ hooks, chơi một lượt bằng hooks, ép game over, chụp ảnh và bảo đảm không có lỗi console. Chạy trên thư mục play-local ở phần chạy trên máy.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
use: { baseURL: 'http://localhost:8080' },
projects: [
{ name: 'desktop', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['Pixel 7'] } },
],
webServer: {
command: 'python3 -m http.server 8080 --directory play-local',
url: 'http://localhost:8080/sdk/v1/wsg-sdk.js',
reuseExistingServer: true,
},
});import { expect, test } from '@playwright/test';
declare global {
interface Window {
__WSG_TEST__?: {
ready: true;
getState(): { scene: string; score: number; alive: boolean; [k: string]: unknown };
start(): void;
input(action: 'tap' | 'left' | 'right' | 'up' | 'down' | 'pause', opts?: { x?: number; y?: number }): void;
setSeed(seed: number): void;
setTimeScale(scale: number): void;
kill(): void;
};
}
}
test('smoke: menu → game → gameover, không lỗi console', async ({ page }, testInfo) => {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', (err) => errors.push(String(err)));
await page.goto('/tap-rush/1.0.0/?wsg_test=1');
await page.waitForFunction(() => window.__WSG_TEST__?.ready === true);
expect(await page.evaluate(() => window.__WSG_TEST__!.getState().scene)).toBe('menu');
await page.evaluate(() => {
window.__WSG_TEST__!.setSeed(42);
window.__WSG_TEST__!.start();
});
await page.waitForFunction(() => window.__WSG_TEST__!.getState().scene === 'game');
for (let i = 0; i < 16; i++) {
await page.evaluate(() => window.__WSG_TEST__!.input('tap'));
await page.waitForTimeout(300);
const s = await page.evaluate(() => window.__WSG_TEST__!.getState());
expect(s.alive || s.scene === 'gameover').toBe(true);
}
await page.evaluate(() => window.__WSG_TEST__!.kill());
await page.waitForFunction(() => window.__WSG_TEST__!.getState().scene === 'gameover');
const final = await page.evaluate(() => window.__WSG_TEST__!.getState());
expect(final.score).toBeGreaterThanOrEqual(0);
await page.screenshot({ path: `e2e/__screenshots__/${testInfo.project.name}-gameover.png` });
expect(errors).toEqual([]);
});npm install -D @playwright/test
npx playwright install chromium
npx playwright test