Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3da59f9e93 | ||
|
|
4bc69d1989 | ||
|
|
c98a78d642 | ||
|
|
b6990a30b7 | ||
|
|
86888b21d2 | ||
|
|
140fbbfa76 | ||
|
|
a176e29388 | ||
|
|
b425c4d5d6 |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet test:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
name: .NET
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master, main ]
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
|
||||
jobs:
|
||||
playwright-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build --no-restore
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: |
|
||||
cd JoshHeaps.Net.UiTests
|
||||
dotnet build
|
||||
# Install Chromium browser
|
||||
pwsh bin/Debug/net8.0/playwright.ps1 install chromium
|
||||
# Install system dependencies manually for Ubuntu 24.04 compatibility
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libasound2t64 \
|
||||
libatk-bridge2.0-0 \
|
||||
libdrm2 \
|
||||
libxkbcommon0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libgbm1 \
|
||||
libxss1 \
|
||||
libasound2-data \
|
||||
xvfb
|
||||
|
||||
- name: Start server in background
|
||||
run: |
|
||||
cd JoshHeaps.Net
|
||||
dotnet run --urls "https://localhost:7118" &
|
||||
echo $! > server.pid
|
||||
# Wait for server to start - try health endpoint first, then chess page
|
||||
echo "Waiting for server to start..."
|
||||
timeout 60 bash -c 'until curl -k -s https://localhost:7118/health >/dev/null 2>&1 || curl -k -s https://localhost:7118/chess >/dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
|
||||
echo "Server appears to be running"
|
||||
sleep 3
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
cd JoshHeaps.Net.UiTests
|
||||
xvfb-run -a dotnet test --logger "trx;LogFileName=test-results.trx" --logger "console;verbosity=detailed"
|
||||
env:
|
||||
CI: true
|
||||
GITHUB_ACTIONS: true
|
||||
DISPLAY: :99
|
||||
|
||||
- name: Stop server
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f JoshHeaps.Net/server.pid ]; then
|
||||
kill $(cat JoshHeaps.Net/server.pid) || true
|
||||
fi
|
||||
pkill -f "dotnet run" || true
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-test-results
|
||||
path: |
|
||||
JoshHeaps.Net.UiTests/TestResults/
|
||||
JoshHeaps.Net.UiTests/test-results/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Playwright videos
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-videos
|
||||
path: JoshHeaps.Net.UiTests/test-results/videos/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,119 @@
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using NUnit.Framework;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace JoshHeaps.Net.UiTests;
|
||||
|
||||
[TestFixture]
|
||||
public class ApiTests : PageTest
|
||||
{
|
||||
private IAPIRequestContext? _apiContext;
|
||||
private TestConfiguration Config => TestConfiguration.Instance;
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
_apiContext = await Playwright.APIRequest.NewContextAsync(new()
|
||||
{
|
||||
BaseURL = Config.Test.BaseUrl,
|
||||
IgnoreHTTPSErrors = true
|
||||
});
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public async Task TearDown()
|
||||
{
|
||||
if (_apiContext != null)
|
||||
{
|
||||
await _apiContext.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task JoinGame_Returns_Valid_Response()
|
||||
{
|
||||
var response = await _apiContext!.GetAsync("/api/chess/JoinGame");
|
||||
|
||||
Assert.That(response.Status, Is.EqualTo(200));
|
||||
|
||||
var jsonResponse = await response.JsonAsync();
|
||||
var gameData = JsonSerializer.Deserialize<JsonElement>(jsonResponse.ToString()!);
|
||||
|
||||
Assert.That(gameData.TryGetProperty("id", out _), Is.True);
|
||||
Assert.That(gameData.TryGetProperty("isWhite", out _), Is.True);
|
||||
Assert.That(gameData.TryGetProperty("gameId", out _), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateCPUGame_Returns_Valid_Response()
|
||||
{
|
||||
var response = await _apiContext!.GetAsync("/api/chess/new/5");
|
||||
|
||||
Assert.That(response.Status, Is.EqualTo(200));
|
||||
|
||||
var jsonResponse = await response.JsonAsync();
|
||||
var gameData = JsonSerializer.Deserialize<JsonElement>(jsonResponse.ToString()!);
|
||||
|
||||
Assert.That(gameData.TryGetProperty("id", out _), Is.True);
|
||||
Assert.That(gameData.TryGetProperty("isWhite", out _), Is.True);
|
||||
Assert.That(gameData.TryGetProperty("gameId", out _), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetGameState_Returns_Valid_Game_Data()
|
||||
{
|
||||
// First create a game
|
||||
var createResponse = await _apiContext!.GetAsync("/api/chess/JoinGame");
|
||||
var createData = JsonSerializer.Deserialize<JsonElement>((await createResponse.JsonAsync()).ToString()!);
|
||||
var gameId = createData.GetProperty("gameId").GetString();
|
||||
|
||||
// Then get the game state
|
||||
var response = await _apiContext.GetAsync($"/api/chess/{gameId}");
|
||||
|
||||
Assert.That(response.Status, Is.EqualTo(200));
|
||||
|
||||
var jsonResponse = await response.JsonAsync();
|
||||
var gameState = JsonSerializer.Deserialize<JsonElement>(jsonResponse.ToString()!);
|
||||
|
||||
Assert.That(gameState.TryGetProperty("gameId", out _), Is.True);
|
||||
Assert.That(gameState.TryGetProperty("currentPlayer", out _), Is.True);
|
||||
Assert.That(gameState.TryGetProperty("pieces", out var pieces), Is.True);
|
||||
|
||||
// Should have 32 pieces initially
|
||||
Assert.That(pieces.GetArrayLength(), Is.EqualTo(32));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetLegalMoves_Returns_Valid_Moves()
|
||||
{
|
||||
// First create a game
|
||||
var createResponse = await _apiContext!.GetAsync("/api/chess/JoinGame");
|
||||
var createData = JsonSerializer.Deserialize<JsonElement>((await createResponse.JsonAsync()).ToString()!);
|
||||
var gameId = createData.GetProperty("gameId").GetString();
|
||||
|
||||
// Get game state to find a piece
|
||||
var gameStateResponse = await _apiContext.GetAsync($"/api/chess/{gameId}");
|
||||
var gameState = JsonSerializer.Deserialize<JsonElement>((await gameStateResponse.JsonAsync()).ToString()!);
|
||||
var pieces = gameState.GetProperty("pieces");
|
||||
var firstPiece = pieces[0];
|
||||
var pieceId = firstPiece.GetProperty("id").GetString();
|
||||
|
||||
// Get legal moves for the piece
|
||||
var response = await _apiContext.GetAsync($"/api/chess/{gameId}/legalMoves/{pieceId}");
|
||||
|
||||
Assert.That(response.Status, Is.EqualTo(200));
|
||||
|
||||
var moves = await response.JsonAsync();
|
||||
Assert.That(moves, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Invalid_GameId_Returns_NotFound()
|
||||
{
|
||||
var invalidGameId = Guid.NewGuid().ToString();
|
||||
var response = await _apiContext!.GetAsync($"/api/chess/{invalidGameId}");
|
||||
|
||||
Assert.That(response.Status, Is.EqualTo(404));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace JoshHeaps.Net.UiTests;
|
||||
|
||||
[TestFixture]
|
||||
public class ChessGameTests : PageTest
|
||||
{
|
||||
private TestConfiguration Config => TestConfiguration.Instance;
|
||||
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
return Config.GetBrowserContextOptions();
|
||||
}
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task OneTimeSetUp()
|
||||
{
|
||||
// Set Playwright browser options for headless control
|
||||
if (!Config.Playwright.Headless)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||
}
|
||||
if (Config.Playwright.SlowMotion > 0)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
await Page.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
}
|
||||
|
||||
protected async Task WaitDefault()
|
||||
{
|
||||
await Page.WaitForTimeoutAsync(Config.Test.WaitTimeout);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Chess_Page_Loads_Successfully()
|
||||
{
|
||||
await Expect(Page.Locator("h1")).ToContainTextAsync("Chess");
|
||||
await Expect(Page.Locator("#startGameBtn")).ToBeVisibleAsync();
|
||||
await Expect(Page.Locator("#startCPUGame")).ToBeVisibleAsync();
|
||||
await Expect(Page.Locator("#chessBoard")).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Chess_Board_Has_64_Squares()
|
||||
{
|
||||
var squares = Page.Locator(".chessSquare");
|
||||
await Expect(squares).ToHaveCountAsync(64);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Start_New_Game_Button_Works()
|
||||
{
|
||||
await Page.ClickAsync("#startGameBtn");
|
||||
|
||||
// Wait for game to start (pieces should appear)
|
||||
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||
|
||||
// Check that pieces have been rendered
|
||||
var pieces = Page.Locator(".chessPiece");
|
||||
await Expect(pieces).ToHaveCountAsync(32); // Standard chess has 32 pieces
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Start_CPU_Game_Shows_Difficulty_Modal()
|
||||
{
|
||||
await Page.ClickAsync("#startCPUGame");
|
||||
|
||||
// Should show difficulty modal
|
||||
await Expect(Page.Locator("#difficultyModal")).ToBeVisibleAsync();
|
||||
await Expect(Page.Locator("#difficultyModal p")).ToContainTextAsync("Set bot difficulty to:");
|
||||
|
||||
// Should have difficulty buttons 1-20
|
||||
var difficultyButtons = Page.Locator("#difficultyButtonContainer button");
|
||||
await Expect(difficultyButtons).ToHaveCountAsync(20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CPU_Game_Starts_After_Selecting_Difficulty()
|
||||
{
|
||||
await Page.ClickAsync("#startCPUGame");
|
||||
|
||||
// Select difficulty 5
|
||||
await Page.ClickAsync("#difficultyButtonContainer button:nth-child(5)");
|
||||
|
||||
// Modal should close
|
||||
await Expect(Page.Locator("#difficultyModal")).ToBeHiddenAsync();
|
||||
|
||||
// Wait for game to start
|
||||
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||
|
||||
// Check that pieces have been rendered
|
||||
var pieces = Page.Locator(".chessPiece");
|
||||
await Expect(pieces).ToHaveCountAsync(32);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Pieces_WhenClicked_HighlightLegalMoves()
|
||||
{
|
||||
// Start a new game first
|
||||
await Page.ClickAsync("#startGameBtn");
|
||||
|
||||
// Wait for pieces to be rendered
|
||||
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||
|
||||
// Find any white piece and click it
|
||||
var pawn = Page.Locator("#chessBoard div:has(img[src*='WhitePawn'])").First;
|
||||
await pawn.ClickAsync();
|
||||
|
||||
var legalMoves = Page.Locator(".legal");
|
||||
await Expect(legalMoves).ToHaveCountAsync(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace JoshHeaps.Net.UiTests;
|
||||
|
||||
[TestFixture]
|
||||
public class GameplayTests : PageTest
|
||||
{
|
||||
private TestConfiguration Config => TestConfiguration.Instance;
|
||||
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
return Config.GetBrowserContextOptions();
|
||||
}
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task OneTimeSetUp()
|
||||
{
|
||||
if (!Config.Playwright.Headless)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||
}
|
||||
if (Config.Playwright.SlowMotion > 0)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
await Page.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Scholars_Mate_Results_In_Checkmate()
|
||||
{
|
||||
// Start a multiplayer game
|
||||
await Page.ClickAsync("#startGameBtn");
|
||||
await Page.WaitForSelectorAsync(".chessPiece");
|
||||
|
||||
if (await Page.Locator("#square-55:has(img[src='/images/Chess Images/WhitePawn.svg'])").CountAsync() == 0)
|
||||
{
|
||||
await Page.ClickAsync("#startGameBtn");
|
||||
await Page.WaitForSelectorAsync(".chessPiece");
|
||||
}
|
||||
|
||||
// Create a second browser context for the second player
|
||||
await using var browser = await Playwright.Chromium.LaunchAsync();
|
||||
await using var context2 = await browser.NewContextAsync(ContextOptions());
|
||||
var page2 = await context2.NewPageAsync();
|
||||
await page2.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||
await page2.ClickAsync("#startGameBtn");
|
||||
await page2.WaitForSelectorAsync(".chessPiece");
|
||||
|
||||
/*
|
||||
* Scholar's Mate sequence:
|
||||
* 1. e4 e5
|
||||
* 2. Bc4 Nc6
|
||||
* 3. Qh5 Nf6??
|
||||
* 4. Qxf7# (checkmate)
|
||||
*
|
||||
* In source square to destination square, accounting for black ui swapping:
|
||||
* 1. 52-36 51-35
|
||||
* 2. 61-34 58-37
|
||||
* 3. 59-45 62-45
|
||||
* 4. 45-13
|
||||
*/
|
||||
|
||||
// Move 1: White plays e4 (pawn e2-e4)
|
||||
await MakeMove(Page, 52, 36); // e2 to e4
|
||||
|
||||
// Move 1: Black plays e5 (pawn e7-e5)
|
||||
await MakeMove(page2, 51, 35); // e7 to e5
|
||||
|
||||
// Move 2: White plays Bc4 (bishop f1-c4)
|
||||
await MakeMove(Page, 61, 34); // f1 to c4
|
||||
|
||||
// Move 2: Black plays Nc6 (knight b8-c6)
|
||||
await MakeMove(page2, 58, 37); // b8 to c6
|
||||
|
||||
// Move 3: White plays Qh5 (queen d1-h5)
|
||||
await MakeMove(Page, 59, 45); // d1 to h5
|
||||
|
||||
// Move 3: Black plays Nf6 (knight g8-f6) - the blunder
|
||||
await MakeMove(page2, 62, 45); // g8 to f6
|
||||
|
||||
string? alertMessage = null;
|
||||
|
||||
Page.Dialog += async (_, dialog) =>
|
||||
{
|
||||
alertMessage = dialog.Message;
|
||||
await dialog.AcceptAsync();
|
||||
};
|
||||
|
||||
// Move 4: White plays Qxf7# (queen h5-f7, checkmate)
|
||||
await MakeMove(Page, 45, 13); // h5 to f7
|
||||
|
||||
// The test passes if we can execute all moves without errors
|
||||
// Specific checkmate verification depends on how your UI handles game end
|
||||
Assert.That(alertMessage, Is.Not.Null);
|
||||
Assert.That(alertMessage, Contains.Substring("Checkmate"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Pawn_Promotion_Shows_Modal()
|
||||
{
|
||||
// Start a multiplayer game
|
||||
await Page.ClickAsync("#startGameBtn");
|
||||
await Page.WaitForSelectorAsync(".chessPiece");
|
||||
|
||||
if (await Page.Locator("#square-55:has(img[src='/images/Chess Images/WhitePawn.svg'])").CountAsync() == 0)
|
||||
{
|
||||
await Page.ClickAsync("#startGameBtn");
|
||||
await Page.WaitForSelectorAsync(".chessPiece");
|
||||
}
|
||||
|
||||
// Create a second browser context for the second player (black)
|
||||
await using var browser = await Playwright.Chromium.LaunchAsync();
|
||||
await using var context2 = await browser.NewContextAsync(ContextOptions());
|
||||
var page2 = await context2.NewPageAsync();
|
||||
await page2.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||
await page2.ClickAsync("#startGameBtn");
|
||||
await page2.WaitForSelectorAsync(".chessPiece");
|
||||
|
||||
// Exact sequence: 1. h4 g5 2. hxg5 h6 3. gxh6 a6 4. h7 a5 5. hxg8=Q
|
||||
// in square numbers: 55-39 49-33 39-30 48-40 30-23 55-47 23-15 47-39 15-6
|
||||
|
||||
// Move 1: White h4 (h2-h4, square 55 to 39)
|
||||
await MakeMove(Page, 55, 39); // h2 to h4
|
||||
|
||||
// Move 1: Black g5 (g7-g5, square 14 to 30)
|
||||
await MakeMove(page2, 49, 33); // g7 to g5
|
||||
|
||||
// Move 2: White hxg5 (h4 captures g5, square 39 to 30)
|
||||
await MakeMove(Page, 39, 30); // h4 captures g5
|
||||
|
||||
// Move 2: Black h6 (h7-h6, square 15 to 23)
|
||||
await MakeMove(page2, 48, 40); // h7 to h6
|
||||
|
||||
// Move 3: White gxh6 (g5 captures h6, square 30 to 23)
|
||||
await MakeMove(Page, 30, 23); // g5 captures h6
|
||||
|
||||
// Move 3: Black a6 (a7-a6, square 8 to 16)
|
||||
await MakeMove(page2, 55, 47); // a7 to a6
|
||||
|
||||
// Move 4: White h7 (h6-h7, square 23 to 15)
|
||||
await MakeMove(Page, 23, 15); // h6 to h7
|
||||
|
||||
// Move 4: Black a5 (a6-a5, square 16 to 24)
|
||||
await MakeMove(page2, 47, 39); // a6 to a5
|
||||
|
||||
// Move 5: White hxg8=Q (h7 captures g8 and promotes, square 15 to 6)
|
||||
await MakeMove(Page, 15, 6); // h7 captures g8 (promotion)
|
||||
|
||||
// Check if promotion modal appears
|
||||
var promotionModal = Page.Locator("#promotionModal");
|
||||
var queenOption = Page.Locator("button:has(img[alt='Queen'])");
|
||||
var promotedQueenSquare = Page.Locator("#square-6:has(img[src='/images/Chess Images/WhiteQueen.svg'])");
|
||||
var isModalVisible = await promotionModal.IsVisibleAsync();
|
||||
|
||||
await Expect(promotionModal).ToBeVisibleAsync();
|
||||
await Expect(queenOption).ToBeVisibleAsync();
|
||||
|
||||
await queenOption.ClickAsync();
|
||||
|
||||
await promotionModal.WaitForAsync(new() { State = WaitForSelectorState.Hidden });
|
||||
await Expect(promotedQueenSquare).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
private async Task MakeMove(IPage page, int fromSquare, int toSquare)
|
||||
{
|
||||
// Click on the source square/piece
|
||||
var fromSquareElement = page.Locator($"#square-{fromSquare}");
|
||||
var piece = fromSquareElement.Locator(".chessPiece");
|
||||
|
||||
if (await piece.CountAsync() > 0)
|
||||
{
|
||||
await piece.ClickAsync();
|
||||
await page.WaitForTimeoutAsync(500);
|
||||
}
|
||||
|
||||
// Click on the destination square
|
||||
var toSquareElement = page.Locator($"#square-{toSquare}");
|
||||
await toSquareElement.ClickAsync();
|
||||
await page.WaitForTimeoutAsync(1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.27.1" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.Playwright.NUnit" />
|
||||
<Using Include="NUnit.Framework" />
|
||||
<Using Include="System.Text.RegularExpressions" />
|
||||
<Using Include="System.Threading.Tasks" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.Integration.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="appsettings.CI.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,98 @@
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace JoshHeaps.Net.UiTests;
|
||||
|
||||
[TestFixture]
|
||||
public class MultiplayerTests : PageTest
|
||||
{
|
||||
private TestConfiguration Config => TestConfiguration.Instance;
|
||||
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
return Config.GetBrowserContextOptions();
|
||||
}
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task OneTimeSetUp()
|
||||
{
|
||||
if (!Config.Playwright.Headless)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||
}
|
||||
if (Config.Playwright.SlowMotion > 0)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
await Page.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Two_Players_Can_Join_Same_Game()
|
||||
{
|
||||
// This test requires running two browser contexts
|
||||
await using var browser = await Playwright.Chromium.LaunchAsync();
|
||||
await using var context1 = await browser.NewContextAsync(ContextOptions());
|
||||
await using var context2 = await browser.NewContextAsync(ContextOptions());
|
||||
|
||||
var page1 = await context1.NewPageAsync();
|
||||
var page2 = await context2.NewPageAsync();
|
||||
|
||||
await page1.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||
await page2.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||
|
||||
// Player 1 starts a game
|
||||
await page1.ClickAsync("#startGameBtn");
|
||||
await page1.Locator(".chessPiece").First.WaitForAsync();
|
||||
|
||||
// Player 2 joins the same game
|
||||
await page2.ClickAsync("#startGameBtn");
|
||||
await page2.Locator(".chessPiece").First.WaitForAsync();
|
||||
|
||||
// Both players should see pieces
|
||||
var pieces1 = page1.Locator(".chessPiece");
|
||||
var pieces2 = page2.Locator(".chessPiece");
|
||||
|
||||
await Expect(pieces1).ToHaveCountAsync(32);
|
||||
await Expect(pieces2).ToHaveCountAsync(32);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SignalR_Connection_Established()
|
||||
{
|
||||
// Start a game to trigger SignalR connection
|
||||
await Page.ClickAsync("#startGameBtn");
|
||||
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||
|
||||
// Check browser console for SignalR connection message
|
||||
var consoleLogs = new List<string>();
|
||||
Page.Console += (_, e) => consoleLogs.Add(e.Text);
|
||||
|
||||
await Page.ReloadAsync();
|
||||
await Page.WaitForTimeoutAsync(1000);
|
||||
|
||||
// Should see SignalR connected message in console
|
||||
Assert.That(consoleLogs, Does.Contain("✅ SignalR connected").Or.Contain("SignalR connected"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Move_Updates_Are_Sent_Via_SignalR()
|
||||
{
|
||||
// This would require more complex setup with two players
|
||||
// For now, just verify the SignalR methods exist in the JavaScript
|
||||
var jsContent = await Page.EvaluateAsync<string>(@"
|
||||
() => {
|
||||
return document.querySelector('script[src*=""chessLogic.js""]') ? 'chessLogic.js loaded' : 'not found';
|
||||
}
|
||||
");
|
||||
|
||||
Assert.That(jsContent, Is.EqualTo("chessLogic.js loaded"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# JoshHeaps.Net UI Tests
|
||||
|
||||
This project contains end-to-end tests for the JoshHeaps.Net website using Playwright.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install Playwright browsers:
|
||||
```bash
|
||||
# From the test project directory
|
||||
dotnet build
|
||||
# Then install browsers (requires PowerShell or appropriate command for your OS)
|
||||
bin/Debug/net8.0/playwright.cmd install # Windows
|
||||
```
|
||||
|
||||
2. Make sure the main application is running on `https://localhost:7065`
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run specific test file
|
||||
dotnet test --filter "FullyQualifiedName~ChessGameTests"
|
||||
|
||||
# Run with verbose output
|
||||
dotnet test --logger "console;verbosity=detailed"
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
### ChessGameTests
|
||||
- Basic chess page functionality
|
||||
- Game start/stop operations
|
||||
- Piece interaction and move validation
|
||||
- UI element verification
|
||||
|
||||
### MultiplayerTests
|
||||
- Two-player game scenarios
|
||||
- SignalR connection testing
|
||||
- Real-time move synchronization
|
||||
|
||||
### ApiTests
|
||||
- Chess API endpoint testing
|
||||
- Game creation and state management
|
||||
- Move validation at API level
|
||||
- Error handling
|
||||
|
||||
## Configuration
|
||||
|
||||
The tests are configured to:
|
||||
- Use headless browsers by default
|
||||
- Take screenshots on failure
|
||||
- Record videos on failure
|
||||
- Automatically start the web server if needed (port 7065)
|
||||
|
||||
## Notes
|
||||
|
||||
- Tests expect the main application to be available at `https://localhost:7065`
|
||||
- Some tests require browsers to be installed via Playwright CLI
|
||||
- Browser installations may require PowerShell on Windows systems
|
||||
@@ -0,0 +1,97 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace JoshHeaps.Net.UiTests;
|
||||
|
||||
public class TestConfiguration
|
||||
{
|
||||
private static TestConfiguration? _instance;
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
public PlaywrightSettings Playwright { get; }
|
||||
public TestSettings Test { get; }
|
||||
|
||||
private TestConfiguration()
|
||||
{
|
||||
var isCI = Environment.GetEnvironmentVariable("CI") == "true" ||
|
||||
Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true";
|
||||
|
||||
var configFile = isCI ? "appsettings.CI.json" : "appsettings.Integration.json";
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile(configFile, optional: true, reloadOnChange: false)
|
||||
.AddJsonFile("appsettings.Integration.json", optional: true, reloadOnChange: false) // fallback
|
||||
.Build();
|
||||
|
||||
Playwright = configuration.GetSection("PlaywrightSettings").Get<PlaywrightSettings>() ?? new PlaywrightSettings();
|
||||
Test = configuration.GetSection("TestSettings").Get<TestSettings>() ?? new TestSettings();
|
||||
}
|
||||
|
||||
public static TestConfiguration Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new TestConfiguration();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public BrowserNewContextOptions GetBrowserContextOptions()
|
||||
{
|
||||
return new BrowserNewContextOptions
|
||||
{
|
||||
ViewportSize = new ViewportSize
|
||||
{
|
||||
Width = Playwright.Viewport.Width,
|
||||
Height = Playwright.Viewport.Height
|
||||
},
|
||||
RecordVideoDir = Playwright.EnableVideoRecording ? Path.Combine(Directory.GetCurrentDirectory(), "test-results", "videos") : null,
|
||||
IgnoreHTTPSErrors = Playwright.IgnoreHTTPSErrors
|
||||
};
|
||||
}
|
||||
|
||||
public BrowserTypeLaunchOptions GetLaunchOptions()
|
||||
{
|
||||
return new BrowserTypeLaunchOptions
|
||||
{
|
||||
Headless = Playwright.Headless,
|
||||
SlowMo = Playwright.SlowMotion,
|
||||
Timeout = Playwright.BrowserTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class PlaywrightSettings
|
||||
{
|
||||
public bool Headless { get; set; } = true;
|
||||
public float SlowMotion { get; set; } = 0;
|
||||
public ViewportSettings Viewport { get; set; } = new();
|
||||
public float BrowserTimeout { get; set; } = 30000;
|
||||
public float ActionTimeout { get; set; } = 10000;
|
||||
public bool EnableVideoRecording { get; set; } = false;
|
||||
public bool IgnoreHTTPSErrors { get; set; } = false;
|
||||
}
|
||||
|
||||
public class ViewportSettings
|
||||
{
|
||||
public int Width { get; set; } = 1280;
|
||||
public int Height { get; set; } = 720;
|
||||
}
|
||||
|
||||
public class TestSettings
|
||||
{
|
||||
public string BaseUrl { get; set; } = "https://localhost:7118";
|
||||
public int WaitTimeout { get; set; } = 3000;
|
||||
public bool EnableScreenshots { get; set; } = true;
|
||||
public bool EnableVideoRecording { get; set; } = false;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"PlaywrightSettings": {
|
||||
"Headless": true,
|
||||
"SlowMotion": 0,
|
||||
"Viewport": {
|
||||
"Width": 1280,
|
||||
"Height": 720
|
||||
},
|
||||
"BrowserTimeout": 60000,
|
||||
"ActionTimeout": 30000,
|
||||
"EnableVideoRecording": true,
|
||||
"IgnoreHTTPSErrors": true
|
||||
},
|
||||
"TestSettings": {
|
||||
"BaseUrl": "https://localhost:7118",
|
||||
"WaitTimeout": 5000,
|
||||
"EnableScreenshots": true,
|
||||
"EnableVideoRecording": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"PlaywrightSettings": {
|
||||
"Headless": false,
|
||||
"SlowMotion": 500,
|
||||
"Viewport": {
|
||||
"Width": 1280,
|
||||
"Height": 720
|
||||
},
|
||||
"BrowserTimeout": 30000,
|
||||
"ActionTimeout": 10000,
|
||||
"IgnoreHTTPSErrors": true
|
||||
},
|
||||
"TestSettings": {
|
||||
"BaseUrl": "https://localhost:7118",
|
||||
"WaitTimeout": 3000,
|
||||
"EnableScreenshots": true,
|
||||
"EnableVideoRecording": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"use": {
|
||||
"headless": true,
|
||||
"viewport": { "width": 1280, "height": 720 },
|
||||
"ignoreHTTPSErrors": true,
|
||||
"video": "retain-on-failure",
|
||||
"screenshot": "only-on-failure"
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"use": { "browserName": "chromium" }
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"use": { "browserName": "firefox" }
|
||||
}
|
||||
],
|
||||
"webServer": {
|
||||
"command": "dotnet run --project ../JoshHeaps.Net",
|
||||
"port": 7065,
|
||||
"reuseExistingServer": true
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -5,7 +5,7 @@ VisualStudioVersion = 17.9.34728.123
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net", "JoshHeaps.Net\JoshHeaps.Net.csproj", "{9F0182CC-470F-4D1A-99F5-348D7921751E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{82206AD3-19DF-4DDA-9647-02B691B78CE8}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net.UiTests", "JoshHeaps.Net.UiTests\JoshHeaps.Net.UiTests.csproj", "{360264F4-8292-4EB3-B67D-98376C13438B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -17,10 +17,10 @@ Global
|
||||
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{360264F4-8292-4EB3-B67D-98376C13438B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{360264F4-8292-4EB3-B67D-98376C13438B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{360264F4-8292-4EB3-B67D-98376C13438B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{360264F4-8292-4EB3-B67D-98376C13438B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
Play Chess
|
||||
</button>
|
||||
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/particles'">
|
||||
Particle Simulator
|
||||
Life
|
||||
</button>
|
||||
<button id="MoreFiller" class="demoButton">
|
||||
More coming soon...
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
public class AutoIpUpdateService(
|
||||
IConfiguration config,
|
||||
@@ -102,11 +99,7 @@ public class AutoIpUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
record AAAARecord(
|
||||
[property: JsonPropertyName("comment")] string Comment,
|
||||
[property: JsonPropertyName("content")] string Content,
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("id")] string Id);
|
||||
record AAAARecord(string Comment, string Content, string Name, string Id);
|
||||
|
||||
record RecordList([property: JsonPropertyName("result")] List<AAAARecord> Result);
|
||||
record RecordList(List<AAAARecord> Result);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-radius: 5vw;
|
||||
width: 20vw;
|
||||
height: 8vw;
|
||||
width: 25vw;
|
||||
height: 10vw;
|
||||
font-size: 2vw;
|
||||
background-color: rgb(104, 255, 0, 0.39);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
getIpStatus();
|
||||
});
|
||||
|
||||
async function getIpStatus() {
|
||||
try {
|
||||
const res = await fetch(`/api/debug/IpCheck`);
|
||||
if (!res.ok) throw new Error("API failed");
|
||||
|
||||
const text = await res.text();
|
||||
console.log(text);
|
||||
document.querySelector('#IsIpChecking').textContent = text;
|
||||
} catch (err) {
|
||||
console.error("Error in api:", err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user