diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e4a908d3..13c1881bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,29 @@ jobs: - name: Clean build run: ./gradlew clean build --no-daemon --stacktrace + - name: Verify guest PvP UCI regression test ran in build + run: | + python - <<'PY' + import glob + import xml.etree.ElementTree as ET + + test_name = "guestPlayersCanPlayFullPvPGameFromUciSample()" + files = glob.glob("webapp-service-layer/build/test-results/test/TEST-*.xml") + + for path in files: + root = ET.parse(path).getroot() + for testcase in root.findall(".//testcase"): + if testcase.attrib.get("name") == test_name: + if testcase.find("failure") is not None: + raise SystemExit(f"{test_name} failed in {path}") + if testcase.find("skipped") is not None: + raise SystemExit(f"{test_name} was skipped in {path}") + print(f"Verified: {test_name} passed in {path}") + raise SystemExit(0) + + raise SystemExit(f"Could not find {test_name} in JUnit reports") + PY + - name: Publish test report if: always() uses: mikepenz/action-junit-report@v6 diff --git a/README.md b/README.md index 5babf70ab..478b5fb20 100644 --- a/README.md +++ b/README.md @@ -690,6 +690,38 @@ Under `webapp/src/main/resources/public/js` there's usually a sub-folder for eac Complex app like PvP is usually organized with a **page** which updates the GUI, a **controller** which connects to WebSockets and/or calls REST endpoints, sometimes a DTO file and/or a separate REST client file. +### Page tests (E2E) + +If you want browser-level page tests, prefer **Playwright** over Selenium for this project: + +- easier setup and test authoring +- built-in waiting/retry behavior +- isolated browser contexts for multi-user scenarios + +For PvP tests, run each player in a different private context so each one gets a different guest ID: + +```javascript +import { test, chromium } from '@playwright/test'; + +test('pvp with 2 guest sessions', async () => { + const browser = await chromium.launch(); // `async ({ browser })` can be used with Playwright fixtures too + const player1Context = await browser.newContext(); + const player2Context = await browser.newContext(); + + const player1Page = await player1Context.newPage(); + const player2Page = await player2Context.newPage(); + + await player1Page.goto('http://localhost:8080'); + await player2Page.goto('http://localhost:8080'); + + await player1Context.close(); + await player2Context.close(); + await browser.close(); +}); +``` + +Use two separate browsers only if you specifically need different browser engines (for example Chromium vs Firefox). + ## JavaScript Libraries Some widgets from the [https://elephantchess.io](https://elephantchess.io) front-end are available as JavaScript diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index ade4ac232..92a3aea3d 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -518,7 +518,53 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { } @Test - fun `guests users not allowed to join games with option allowGuests == false`() = runTest { + fun guestPlayersCanPlayFullPvPGameFromUciSample() = runTest { + val request = CreateGameRequest( + inviterColor = RED, + isRated = false, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = true, + alwaysVisibleInLobby = false, + privateInvite = false + ) + + val createResponse = pvpGameService.createGame(guestId1, request) + assertEquals(CREATED, createResponse.eventType) + assertEquals(RED, createResponse.color) + + pvpGameService.joinGame(guestId2, JoinGameRequest(createResponse.gameId)) + assertEquals(1, countGameByStatus(JOINED)) + assertEquals(0, countGameByStatus(CREATED)) + + val gameMoves = assertNotNull( + gameMovesCache.listAll().firstOrNull { it.endsInCheckmate() }, + "Expected at least one Xiangqi game in uci.txt that ends in checkmate" + ) + + gameMoves.uciMoves.dropLast(1).forEachIndexed { i, move -> + val result = pvpGameService.playMove( + userId = userIdToPlay(createResponse.gameId), + request = PlayMoveRequest(createResponse.gameId, move) + ) + + assertEquals(i + 1, result.updatedIndex) + assertNull(result.gameEventType) + assertNull(result.ratingUpdate) + } + + val lastMoveResult = pvpGameService.playMove( + userId = userIdToPlay(createResponse.gameId), + request = PlayMoveRequest(createResponse.gameId, gameMoves.uciMoves.last()) + ) + + assertEquals(CHECKMATED, lastMoveResult.gameEventType) + assertNull(lastMoveResult.ratingUpdate) + } + + @Test + fun `guest users not allowed to join games with option allowGuests == false`() = runTest { val request1 = CreateGameRequest( inviterColor = RED, isRated = true,