From 2be3de5547ca7331d6d1ca21de13242f3f5bbc42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 11:08:09 +0000 Subject: [PATCH 1/9] Initial plan From 75bf5725c98c0e9bd76a2e0018d223449a83991a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 11:12:36 +0000 Subject: [PATCH 2/9] docs: recommend Playwright for page-level PvP tests Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/40b5c7be-9532-4fe2-80b4-34427adc6e34 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 5babf70ab..ed95fac07 100644 --- a/README.md +++ b/README.md @@ -690,6 +690,39 @@ 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, expect, chromium } from '@playwright/test'; + +test('pvp with 2 guest sessions', async () => { + const browser = await chromium.launch(); + const player1Context = await browser.newContext(); // isolated storage + const player2Context = await browser.newContext(); // isolated storage + + const player1Page = await player1Context.newPage(); + const player2Page = await player2Context.newPage(); + + await player1Page.goto('http://localhost:8080'); + await player2Page.goto('http://localhost:8080'); + + await expect(player1Page).toHaveURL(/.*/); + await expect(player2Page).toHaveURL(/.*/); + + 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 From e19381c697285d195fea29b087fd946ef26f1073 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 11:13:23 +0000 Subject: [PATCH 3/9] docs: tighten Playwright example assertions Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/40b5c7be-9532-4fe2-80b4-34427adc6e34 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index ed95fac07..8ca3bbeba 100644 --- a/README.md +++ b/README.md @@ -701,7 +701,7 @@ If you want browser-level page tests, prefer **Playwright** over Selenium for th For PvP tests, run each player in a different private context so each one gets a different guest id: ```javascript -import { test, expect, chromium } from '@playwright/test'; +import { test, chromium } from '@playwright/test'; test('pvp with 2 guest sessions', async () => { const browser = await chromium.launch(); @@ -714,9 +714,6 @@ test('pvp with 2 guest sessions', async () => { await player1Page.goto('http://localhost:8080'); await player2Page.goto('http://localhost:8080'); - await expect(player1Page).toHaveURL(/.*/); - await expect(player2Page).toHaveURL(/.*/); - await browser.close(); }); ``` From e27f950e049ce88636468c849cd0138c7536dbc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 11:14:05 +0000 Subject: [PATCH 4/9] docs: polish Playwright PvP guidance wording Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/40b5c7be-9532-4fe2-80b4-34427adc6e34 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ca3bbeba..83a22903f 100644 --- a/README.md +++ b/README.md @@ -698,7 +698,7 @@ If you want browser-level page tests, prefer **Playwright** over Selenium for th - 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: +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'; @@ -714,6 +714,8 @@ test('pvp with 2 guest sessions', async () => { await player1Page.goto('http://localhost:8080'); await player2Page.goto('http://localhost:8080'); + await player1Context.close(); + await player2Context.close(); await browser.close(); }); ``` From 3428c45a34beaf15c0a368f607ad635ce0f64618 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 11:14:54 +0000 Subject: [PATCH 5/9] docs: clarify Playwright example lifecycle note Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/40b5c7be-9532-4fe2-80b4-34427adc6e34 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 83a22903f..478b5fb20 100644 --- a/README.md +++ b/README.md @@ -704,9 +704,9 @@ For PvP tests, run each player in a different private context so each one gets a import { test, chromium } from '@playwright/test'; test('pvp with 2 guest sessions', async () => { - const browser = await chromium.launch(); - const player1Context = await browser.newContext(); // isolated storage - const player2Context = await browser.newContext(); // isolated storage + 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(); From 9774321f5152287c6bf3f51670fb46f33f0dbf43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 12:58:23 +0000 Subject: [PATCH 6/9] test: add guest-vs-guest PvP regression and run it in CI Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/0109cd65-0c12-473b-a3fd-4ed58d581b1c Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .github/workflows/build.yml | 3 ++ .../services/PlayerVsPlayerGameServiceTest.kt | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e4a908d3..82730ffb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,9 @@ jobs: - name: Clean build run: ./gradlew clean build --no-daemon --stacktrace + - name: Verify guest PvP UCI regression test + run: ./gradlew --no-daemon :webapp-service-layer:test --tests io.elephantchess.servicelayer.services.PlayerVsPlayerGameServiceTest.guestPlayersCanPlayFullPvpGameFromUciSample + - name: Publish test report if: always() uses: mikepenz/action-junit-report@v6 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..736c1db2b 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 @@ -517,6 +517,50 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertEquals(1_000, fetchManchuRapidRating(userId2.id)) } + @Test + 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 = gameMovesCache.findByGameId("4Q815fbI") + assertTrue { gameMoves.endsInCheckmate() } + + 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 `guests users not allowed to join games with option allowGuests == false`() = runTest { val request1 = CreateGameRequest( From 09f8aae10972a01a189a02ef633200940d4be9d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 13:05:29 +0000 Subject: [PATCH 7/9] test: harden guest PvP regression selection and CI verification Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/0109cd65-0c12-473b-a3fd-4ed58d581b1c Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .github/workflows/build.yml | 24 +++++++++++++++++-- .../services/PlayerVsPlayerGameServiceTest.kt | 7 ++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82730ffb2..edd1a50b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,8 +44,28 @@ jobs: - name: Clean build run: ./gradlew clean build --no-daemon --stacktrace - - name: Verify guest PvP UCI regression test - run: ./gradlew --no-daemon :webapp-service-layer:test --tests io.elephantchess.servicelayer.services.PlayerVsPlayerGameServiceTest.guestPlayersCanPlayFullPvpGameFromUciSample + - 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() 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 736c1db2b..e01cb8431 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,7 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { } @Test - fun guestPlayersCanPlayFullPvpGameFromUciSample() = runTest { + fun guestPlayersCanPlayFullPvPGameFromUciSample() = runTest { val request = CreateGameRequest( inviterColor = RED, isRated = false, @@ -538,7 +538,10 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertEquals(1, countGameByStatus(JOINED)) assertEquals(0, countGameByStatus(CREATED)) - val gameMoves = gameMovesCache.findByGameId("4Q815fbI") + val gameMoves = assertNotNull( + gameMovesCache.listAll().firstOrNull { it.endsInCheckmate() }, + "Expected at least one Xiangqi game in uci.txt that ends in checkmate" + ) assertTrue { gameMoves.endsInCheckmate() } gameMoves.uciMoves.dropLast(1).forEachIndexed { i, move -> From c13b309cfe77af6c81657616d798a5db84fe0fb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 13:07:12 +0000 Subject: [PATCH 8/9] ci: polish JUnit verification message Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/0109cd65-0c12-473b-a3fd-4ed58d581b1c Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .../servicelayer/services/PlayerVsPlayerGameServiceTest.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index edd1a50b4..13c1881bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,7 @@ jobs: print(f"Verified: {test_name} passed in {path}") raise SystemExit(0) - raise SystemExit(f"Could not find {test_name} in junit reports") + raise SystemExit(f"Could not find {test_name} in JUnit reports") PY - name: Publish test report 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 e01cb8431..08b25d7bd 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 @@ -542,7 +542,6 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { gameMovesCache.listAll().firstOrNull { it.endsInCheckmate() }, "Expected at least one Xiangqi game in uci.txt that ends in checkmate" ) - assertTrue { gameMoves.endsInCheckmate() } gameMoves.uciMoves.dropLast(1).forEachIndexed { i, move -> val result = pvpGameService.playMove( From 29c791c2cfc02aec254979f4b1afa4da0f8ba4de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 13:09:10 +0000 Subject: [PATCH 9/9] test: fix guest-users test title typo Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/0109cd65-0c12-473b-a3fd-4ed58d581b1c Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../servicelayer/services/PlayerVsPlayerGameServiceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 08b25d7bd..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 @@ -564,7 +564,7 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { } @Test - fun `guests users not allowed to join games with option allowGuests == false`() = runTest { + fun `guest users not allowed to join games with option allowGuests == false`() = runTest { val request1 = CreateGameRequest( inviterColor = RED, isRated = true,