Bỏ qua tới nội dung
Các trang tài liệu

Nhà phát triển · Hướng dẫn

Kiểm thử

Chạy game ở chế độ standalone, dựng thư mục giống production trên máy, chạy trong portal, test hooks, game conformance và smoke test Playwright.

Trên trang này

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ữ trong localStorage (khoá wsg:local-id).
  • Bảng xếp hạng ở localStorage khoá 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 visibility và 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:

Bash
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.environment là portal; overlay tải biến mất đúng lúc gọi loadingFinished().
  • Điểm được nhận (xem reason nế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 pause rồi resume.
  • Tải lại trang và chơi tiếp: dữ liệu WSG.storage vẫn còn.

Đội nội bộ có repo chạy được cả hệ thống ở máy:

Bash
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-game

Test 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.

TypeScript
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ới ready: true.
  • getState().scene là 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 khi WSG.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():

JavaScriptgame.js (thêm vào)
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.

TypeScriptplaywright.config.ts
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,
  },
});
TypeScripte2e/smoke.spec.ts
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([]);
});
Bash
npm install -D @playwright/test
npx playwright install chromium
npx playwright test