Compare commits

..

2 Commits

Author SHA1 Message Date
Julian Kropff ce6fc1bf57 Add: Game UI test:
- Ensures FXML loads and controller initializes without errors.
2026-04-26 23:50:08 +02:00
Julian Kropff 05b3324cbd Add: Game UI Taskbar tests:
- input validation behavior
- UI state updates based on game state
- correct enabling/disabling of action buttons
2026-04-26 23:45:39 +02:00
33 changed files with 519 additions and 1203 deletions
-11
View File
@@ -1,11 +0,0 @@
# Besprechung vom 26.04.2024
## Fortschritt für die Implementierung der Unit Tests
Die Unit Tests für alle zentralen Komponenten des Netzwerkes wurden erfolgreich implementiert.
Bei dieser Gelegenheit wurden auch Bugs behoben und die Codequalität verbessert.
In einzelnen Bereichen wurde noch nicht die vorgenommene Quote von 50% erreicht, weitere Unit-Tests sollen bis zum nächsten Meilenstein folgen. Komponenten beteiligt an den Kernbereichen sind größtenteils abgedeckt.
## Perspektivische Änderungen für den letzten Meilenstein
Ein getroffener Kompromiss um den vierten Meilenstein fristgerecht zu erreichen, war das zurückstellen der Codequalität.
In der Zeit bis zum sechsten Meilenstein sollen diese Provisorien noch einmal überarbeitet und finalisiert werden.
+1 -3
View File
@@ -36,9 +36,7 @@ None, as milestone 3 covered the assessment of individual technical understandin
## Milestone 5 ## Milestone 5
### [26. April 2026](26-04.md) *(Meeting notes to be added here when available)*
- Progress on unit tests and code quality improvements
- Discussion of compromises made for MS4 and plans for finalizing code quality by MS6
## Milestone 6 ## Milestone 6
+10 -226
View File
@@ -1,94 +1,29 @@
# Casono Manual # Casono Manual
> Hinweis: Eine vereinfachte Einführung finden Sie in der Datei [Casono Rules Easy Description](casono-rules-easy-description.md).
> Dieses Dokument beschreibt den Ablauf eines Pokerspiels anhand eines Beispiels.
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Start Game](#start-game) * [1. Spielübersicht](#1-spielübersicht)
* [Server starten](#server-starten)
* [Client starten](#client-starten)
* [UI](#ui)
* [Lobby UI](#lobby-ui)
* [Eine Lobby erstellen](#eine-lobby-erstellen)
* [Einer Lobby beitreten](#einer-lobby-beitreten)
* [Den Username ändern](#den-username-ändern)
* [Highscores](#highscores)
* [Game UI](#game-ui)
* [Casono Browser](#casono-browser)
* [Casono Rules](#casono-rules)
* [1. Spielübersicht](#1-spielübersicht)
* [Grundregeln](#grundregeln) * [Grundregeln](#grundregeln)
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button) * [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
* [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze) * [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze)
* [4. Spielablauf im Detail](#4-spielablauf-im-detail) * [4. Spielablauf im Detail](#4-spielablauf-im-detail)
* [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde) * [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde)
* [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten) * [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten)
* [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte) * [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte)
* [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte) * [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte)
* [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich) * [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich)
* [6. Poker-Handrangfolge](#6-poker-handrangfolge) * [6. Poker-Handrangfolge](#6-poker-handrangfolge)
* [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien) * [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien)
* [Reihenfolge beachten](#reihenfolge-beachten) * [Reihenfolge beachten](#reihenfolge-beachten)
* [Klare Aktionen](#klare-aktionen) * [Klare Aktionen](#klare-aktionen)
* [Ein Spieler eine Hand](#ein-spieler--eine-hand) * [Ein Spieler eine Hand](#ein-spieler--eine-hand)
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze) * [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
* [8. Strategische Einordnung](#8-strategische-einordnung) * [8. Strategische Einordnung](#8-strategische-einordnung)
* [Casono Rules Easy Description](#casono-rules-easy-description)
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
* [Beispiel Preflop](#beispiel-preflop)
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
* [Beispiel Flop](#beispiel-flop)
* [Turn (4. Karte)](#turn-4-karte)
* [River (5. Karte)](#river-5-karte)
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
* [Fazit](#fazit)
<!-- vim-markdown-toc --> <!-- vim-markdown-toc -->
# Start Game
## Server starten
```bash
./gradlew run --no-configuration-cache --args="server 1234"
```
## Client starten
```bash
./gradlew run --no-configuration-cache --args="client 127.0.0.1:1234 DeinName"
```
# UI
## Lobby UI
### Eine Lobby erstellen
![Create a Lobby](./images/create-a-lobby.png)
### Einer Lobby beitreten
![Join a Lobby](./images/join-a-lobby.png)
### Den Username ändern
![Change Username](./images/change-username.png)
### Highscores
![View Highscores](./images/view-high-scores.png)
## Game UI
![Game UI](./images/game-ui.png)
![Taskbar](./images/taskbar.png)
## Casono Browser
![Casono Browser](./images/casono-browser.png)
# Casono Rules
## 1. Spielübersicht ## 1. Spielübersicht
Texas Holdem ist ein strategisches Kartenspiel für mehrere Spieler. Texas Holdem ist ein strategisches Kartenspiel für mehrere Spieler.
@@ -225,154 +160,3 @@ Texas Holdem ist kein reines Glücksspiel. Der Erfolg basiert auf:
- Wahrscheinlichkeiten (Mathematik) - Wahrscheinlichkeiten (Mathematik)
- Einschätzung von Gegnern (Psychologie) - Einschätzung von Gegnern (Psychologie)
- Positionsspiel und Timing - Positionsspiel und Timing
# Casono Rules Easy Description
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
Damit Du in diesem Erlebnisraum starten kannst, ist es wie bei jedem Spiel notwendig, zuerst die Grundregeln und den Spielablauf zu verstehen.
Also los geht es:
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20000 Chips ($)**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
![1. image](./images/1-1.svg)
Die Spieler sitzen in folgender Reihenfolge: Julian, Mathis, Jona und Lars. Einer davon hat den Dealer-Button, der bestimmt, wer die Karten austeilt und von wo die Runde beginnt. Dieser Button wandert nach jeder Runde im Uhrzeigersinn weiter und verändert damit die Position ständig.
Regel: *34 Button Placement and Movement 🟢*
## Blinds (Small Blind & Big Blind)
Bevor die Karten verteilt werden, gibt es zwei Pflicht-Einsätze:
Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so hoch wie der Small Blind.
![2. image](./images/2-1.svg)
Regel: *32 Dead Button 🟡*
Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überhaupt beginnt, weil jeder schon “im Spiel” ist.
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
## Erste Setzrunde (Preflop)
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
Jetzt muss jeder Spieler entscheiden:
* Fold (aussteigen)
* Call (mitgehen)
* Raise (erhöhen)
Regel: *40 Methods of Betting 🟢*
Regel: *41 Methods of Calling 🟢*
Regel: *42 Methods of Raising 🟢*
Regel: *50 Acting in Turn 🟢*
## Beispiel Preflop
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
![3. image](./images/3-1.svg)
Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also foldet er und steigt aus.
![4. image](./images/4-1.svg)
Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil seine Hand spielbar ist.
![5. image](./images/5-1.svg)
Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf **1200 Chips**.
![6. image](./images/6-1.svg)
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst erhöhen oder aussteigen.
## Flop (3 Gemeinschaftskarten)
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler zusätzliche Informationen bekommen.
Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
![7. image](./images/7-1.svg)
Es beginnt eine neue Setzrunde.
Regel: *49 Accepted Action 🟢*
## Beispiel Flop
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
![8. image](./images/8-1.svg)
## Turn (4. Karte)
Jetzt kommt die **4. Gemeinschaftskarte**.
Wieder beginnt eine neue Setzrunde.
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
![9. image](./images/9-1.png)
Regel: *53 Action Out of Turn 🟡*
## River (5. Karte)
Jetzt wird die letzte Gemeinschaftskarte aufgedeckt.
Dies ist die letzte Entscheidung im Spiel.
Jona setzt **5000 Chips**.
![10. image](./images/10-1.png)
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
Regel: *54 Pot Size Bets 🟡*
## Showdown (Gewinnentscheidung)
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
![11. image](./images/11-1.png)
Regel: *12 Cards Speak at Showdown 🟢*
Regel: *16 Face Up for All-Ins 🟢*
Regel: *17 Non All-In Showdowns 🟢*
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den gesamten Pot.
## Poker Hand Rankings (Gewichtung)
Die Kartenkombinationen sind klar geordnet von schwach bis extrem stark:
<img src="./images/12.png" height="600">
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
Jona: 2. Paar:
![13. image](./images/13-1.png)
Lars: 1. Paar:
![14. image](./images/14-1.png)
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
## Fazit
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht, versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
Regel: *67 One Player One Hand 🟢*
Regel: *52 Incorrect Bets 🟡*
Regel: *57 Non-Standard Betting 🟡*
@@ -23,9 +23,8 @@ const marked = {
`; `;
}); });
md = md.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { md = md.replace(/```([\s\S]*?)```/g, (_, code) => {
const langClass = lang ? ` language-${lang}` : ""; return `<pre class="cm-code"><code>${escapeHtml(code)}</code></pre>`;
return `<pre class="cm-code${langClass}"><code>${escapeHtml(code.trim())}</code></pre>`;
}); });
md = md.replace(/!\[(.*?)\]\((.*?)\)/gim, md = md.replace(/!\[(.*?)\]\((.*?)\)/gim,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

+2 -194
View File
@@ -130,49 +130,8 @@
const markdown = ` const markdown = `
# Casono Manual # Casono Manual
# Start Game Hinweis: Eine vereinfachte Einführung finden Sie in der Datei [Casono Rules Easy Description](casono-rules-easy-description.md).
Dieses Dokument beschreibt den Ablauf eines Pokerspiels anhand eines Beispiels.
## Server starten
\`\`\`bash
./gradlew run --no-configuration-cache --args="server 1234"
\`\`\`
## Client starten
\`\`\`bash
./gradlew run --no-configuration-cache --args="client 127.0.0.1:1234 DeinName"
\`\`\`
# UI
## Lobby UI
### Eine Lobby erstellen
![Create a Lobby](./images/create-a-lobby.png)
### Einer Lobby beitreten
![Join a Lobby](./images/join-a-lobby.png)
### Den Username ändern
![Change Username](./images/change-username.png)
### Highscores
![View Highscores](./images/view-high-scores.png)
## Game UI
![Game UI](./images/game-ui.png)
![Taskbar](./images/taskbar.png)
## Casono Browser
![Casono Browser](./images/casono-browser.png)
# Casono Rules
## 1. Spielübersicht ## 1. Spielübersicht
@@ -310,157 +269,6 @@ Texas Holdem ist kein reines Glücksspiel. Der Erfolg basiert auf:
- Wahrscheinlichkeiten (Mathematik) - Wahrscheinlichkeiten (Mathematik)
- Einschätzung von Gegnern (Psychologie) - Einschätzung von Gegnern (Psychologie)
- Positionsspiel und Timing - Positionsspiel und Timing
# Casono Rules Easy Description
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
Damit Du in diesem Erlebnisraum starten kannst, ist es wie bei jedem Spiel notwendig, zuerst die Grundregeln und den Spielablauf zu verstehen.
Also los geht es:
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20000 Chips ($)**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
![1. image](./images/1-1.svg)
Die Spieler sitzen in folgender Reihenfolge: Julian, Mathis, Jona und Lars. Einer davon hat den Dealer-Button, der bestimmt, wer die Karten austeilt und von wo die Runde beginnt. Dieser Button wandert nach jeder Runde im Uhrzeigersinn weiter und verändert damit die Position ständig.
Regel: *34 Button Placement and Movement 🟢*
## Blinds (Small Blind & Big Blind)
Bevor die Karten verteilt werden, gibt es zwei Pflicht-Einsätze:
Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so hoch wie der Small Blind.
![2. image](./images/2-1.svg)
Regel: *32 Dead Button 🟡*
Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überhaupt beginnt, weil jeder schon “im Spiel” ist.
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
## Erste Setzrunde (Preflop)
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
Jetzt muss jeder Spieler entscheiden:
* Fold (aussteigen)
* Call (mitgehen)
* Raise (erhöhen)
Regel: *40 Methods of Betting 🟢*
Regel: *41 Methods of Calling 🟢*
Regel: *42 Methods of Raising 🟢*
Regel: *50 Acting in Turn 🟢*
## Beispiel Preflop
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
![3. image](./images/3-1.svg)
Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also foldet er und steigt aus.
![4. image](./images/4-1.svg)
Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil seine Hand spielbar ist.
![5. image](./images/5-1.svg)
Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf **1200 Chips**.
![6. image](./images/6-1.svg)
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst erhöhen oder aussteigen.
## Flop (3 Gemeinschaftskarten)
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler zusätzliche Informationen bekommen.
Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
![7. image](./images/7-1.svg)
Es beginnt eine neue Setzrunde.
Regel: *49 Accepted Action 🟢*
## Beispiel Flop
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
![8. image](./images/8-1.svg)
## Turn (4. Karte)
Jetzt kommt die **4. Gemeinschaftskarte**.
Wieder beginnt eine neue Setzrunde.
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
![9. image](./images/9-1.png)
Regel: *53 Action Out of Turn 🟡*
## River (5. Karte)
Jetzt wird die letzte Gemeinschaftskarte aufgedeckt.
Dies ist die letzte Entscheidung im Spiel.
Jona setzt **5000 Chips**.
![10. image](./images/10-1.png)
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
Regel: *54 Pot Size Bets 🟡*
## Showdown (Gewinnentscheidung)
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
![11. image](./images/11-1.png)
Regel: *12 Cards Speak at Showdown 🟢*
Regel: *16 Face Up for All-Ins 🟢*
Regel: *17 Non All-In Showdowns 🟢*
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den gesamten Pot.
## Poker Hand Rankings (Gewichtung)
Die Kartenkombinationen sind klar geordnet von schwach bis extrem stark:
<img src="./images/12.png" height="600">
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
Jona: 2. Paar:
![13. image](./images/13-1.png)
Lars: 1. Paar:
![14. image](./images/14-1.png)
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
## Fazit
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht, versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
Regel: *67 One Player One Hand 🟢*
Regel: *52 Incorrect Bets 🟡*
Regel: *57 Non-Standard Betting 🟡*
`; `;
document.getElementById("content").innerHTML = document.getElementById("content").innerHTML =
@@ -1,13 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.List;
/** Interface used to create test instances of the ChatClient */
public interface ChatClientInterface {
List<Message> getMessages();
void sendMessage(Message message);
List<String> getUsers();
}
@@ -33,9 +33,9 @@ public class ChatController {
private volatile String username; private volatile String username;
private ClientService clientService; private final ClientService clientService;
private final ChatClientInterface chatClient; private final ChatClient chatClient;
public ChatBoxController getChatBoxController() { public ChatBoxController getChatBoxController() {
return chatBoxController; return chatBoxController;
@@ -112,39 +112,6 @@ public class ChatController {
REFRESH_TIME); REFRESH_TIME);
} }
/**
* Another constructor to give the ChatClient directly instead of the Client Service used only
* for test purposes
*
* @param username
*/
public ChatController(String username, ChatClientInterface chatClient) {
this.username = username;
this.chatClient = chatClient;
chatModelMap = new LinkedHashMap<>();
localUserList = new ArrayList<>();
this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class);
this.serverEventListener = this::handleServerEvent;
this.activeChatControllers = new HashMap<>();
this.timer = new Timer(true);
timer.schedule(
new TimerTask() {
@Override
public void run() {
try {
receiveMessage();
checkWhisperUsers();
} catch (RuntimeException e) {
logger.warn("Chat refresh failed: {}", e.getMessage());
}
}
},
0,
REFRESH_TIME);
}
private void registerAsActiveController(ClientService clientService) { private void registerAsActiveController(ClientService clientService) {
synchronized (ACTIVE_CONTROLLERS) { synchronized (ACTIVE_CONTROLLERS) {
ChatController oldController = ACTIVE_CONTROLLERS.get(clientService); ChatController oldController = ACTIVE_CONTROLLERS.get(clientService);
@@ -2,6 +2,8 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.function.Consumer; import java.util.function.Consumer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/** /**
* ChatModel, stores the data for a specific chat * ChatModel, stores the data for a specific chat
@@ -10,7 +12,7 @@ import java.util.function.Consumer;
*/ */
public class ChatModel { public class ChatModel {
private final ArrayList<Consumer<Message>> listeners = new ArrayList<>(); private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
public ArrayList<Message> messages; public ArrayList<Message> messages;
@@ -22,6 +24,8 @@ public class ChatModel {
/** The person to send the message to If the chat is a whisper chat */ /** The person to send the message to If the chat is a whisper chat */
private String target; private String target;
private final IntegerProperty count;
public int lobbyId; public int lobbyId;
/** /**
@@ -33,9 +37,10 @@ public class ChatModel {
* @param target The username of the whisper recipient, or null for other chat types. * @param target The username of the whisper recipient, or null for other chat types.
*/ */
public ChatModel(ChatType chattype, String username, int lobbyId, String target) { public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
this.messages = new ArrayList<>(); this.messages = new ArrayList<Message>();
this.chattype = chattype; this.chattype = chattype;
this.username = username; this.username = username;
this.count = new SimpleIntegerProperty(0);
this.lobbyId = lobbyId; this.lobbyId = lobbyId;
this.target = target; this.target = target;
} }
@@ -57,7 +62,7 @@ public class ChatModel {
*/ */
public synchronized void addMessage(Message msg) { public synchronized void addMessage(Message msg) {
messages.add(msg); messages.add(msg);
listeners.forEach((l) -> l.accept(messages.getLast())); listeners.stream().forEach((l) -> l.accept(messages.getLast()));
} }
/** /**
@@ -15,8 +15,8 @@ public class Message {
private final String message; private final String message;
public String sender; public String sender;
public String timestamp; public String timestamp;
public int lobbyId; public int lobbyId = 0;
public String target; public String target = null;
/** /**
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages * Constructs a Message with a provided timestamp. Typically used when reconstructing messages
@@ -100,7 +100,7 @@ public class Message {
* @return A formatted string containing all message attributes for server transmission. * @return A formatted string containing all message attributes for server transmission.
*/ */
public String toArgsString() { public String toArgsString() {
String gameIdString; String gameIdString = "";
if (lobbyId >= 0) { if (lobbyId >= 0) {
gameIdString = " GAME=" + lobbyId; gameIdString = " GAME=" + lobbyId;
} else { } else {
@@ -1,6 +1,5 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network; package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatClientInterface;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList; import java.util.ArrayList;
@@ -13,7 +12,7 @@ import org.apache.logging.log4j.Logger;
* from the server. It uses the ClientService to send commands and receive responses from the * from the server. It uses the ClientService to send commands and receive responses from the
* server. * server.
*/ */
public class ChatClient implements ChatClientInterface { public class ChatClient {
private final ClientService clientService; private final ClientService clientService;
private final Logger logger; private final Logger logger;
@@ -1,7 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui; package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController.ChatKey;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
@@ -21,6 +20,7 @@ import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@@ -30,10 +30,16 @@ public class ChatBoxController {
private final ChatController chatController; private final ChatController chatController;
@FXML private VBox chatBox;
@FXML private TabPane chatTabPane; @FXML private TabPane chatTabPane;
@FXML private MenuButton addWhisperChatButton; @FXML private MenuButton addWhisperChatButton;
@FXML private HBox menuBox;
private FXMLLoader fxmlLoader;
private final List<String> activeWhisperChats; private final List<String> activeWhisperChats;
@FXML private final Map<String, Tab> usernameTabMap; @FXML private final Map<String, Tab> usernameTabMap;
@@ -70,7 +76,7 @@ public class ChatBoxController {
ChatModel globalChatModel = new ChatModel(global, username, -1, null); ChatModel globalChatModel = new ChatModel(global, username, -1, null);
chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel); chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel);
addChatTab("GLOBAL", globalChatModel, global); addChatTab("GLOBAL", globalChatModel, global);
addWhisperChatButton.setOnAction(_ -> addWhisperChatButton.show()); addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show());
} }
/** /**
@@ -90,7 +96,7 @@ public class ChatBoxController {
MenuItem menuItem = new MenuItem(targetUserName); MenuItem menuItem = new MenuItem(targetUserName);
addWhisperChatButton.getItems().add(menuItem); addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction( menuItem.setOnAction(
_ -> event ->
addWhisperChat( addWhisperChat(
targetUserName, targetUserName,
new ChatModel( new ChatModel(
@@ -124,7 +130,7 @@ public class ChatBoxController {
if (oldUsername.equals(item.getText())) { if (oldUsername.equals(item.getText())) {
item.setText(newUsername); item.setText(newUsername);
item.setOnAction( item.setOnAction(
_ -> event ->
addWhisperChat( addWhisperChat(
newUsername, newUsername,
new ChatModel( new ChatModel(
@@ -159,21 +165,11 @@ public class ChatBoxController {
if (!activeWhisperChats.contains(target)) { if (!activeWhisperChats.contains(target)) {
activeWhisperChats.add(target); activeWhisperChats.add(target);
ChatType whisper = ChatType.WHISPER; ChatType whisper = ChatType.WHISPER;
chatController.getChatModelMap().put(new ChatKey(whisper, target), chatModel); chatController
ChatViewController chatViewController = addChatTab(target, chatModel, whisper); .getChatModelMap()
Tab chatTab = chatViewController.getChatTab(); .put(new ChatController.ChatKey(whisper, target), chatModel);
chatTab.setOnCloseRequest( addChatTab(target, chatModel, whisper);
_ -> {
chatViewController.tabIsOpen = false;
chatTabPane.getTabs().remove(chatTab);
});
} else { } else {
ChatViewController chatViewController =
chatController.activeChatControllers.get(new ChatKey(ChatType.WHISPER, target));
if (!chatViewController.tabIsOpen) {
reopenChatTab(chatViewController.getChatTab());
chatViewController.tabIsOpen = true;
}
chatTabPane.getSelectionModel().select(usernameTabMap.get(target)); chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
} }
} }
@@ -187,39 +183,30 @@ public class ChatBoxController {
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat. * @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded. * @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
*/ */
public ChatViewController addChatTab(String title, ChatModel chatModel, ChatType chatType) { public void addChatTab(String title, ChatModel chatModel, ChatType chatType) {
String ressource = "/ui-structure/components/chatui/chattab.fxml"; String ressource = "/ui-structure/components/chatui/chattab.fxml";
URL resource = getClass().getResource(ressource); URL resource = getClass().getResource(ressource);
FXMLLoader fxmlLoader = new FXMLLoader(resource); FXMLLoader fxmlLoader = new FXMLLoader(resource);
return runOnPlatformSynchronized( runOnPlatformSynchronized(
() -> { () -> {
try { try {
ChatViewController chatViewController = ChatViewController chatViewController =
new ChatViewController(this.chatController, chatModel, this); new ChatViewController(
this.chatController, chatModel, this.username);
ChatKey chatKey; chatController.activeChatControllers.put(
if (chatType.equals(ChatType.WHISPER)) { new ChatController.ChatKey(chatType), chatViewController);
chatKey = new ChatKey(chatType, title);
} else {
chatKey = new ChatKey(chatType);
}
chatController.activeChatControllers.put(chatKey, chatViewController);
fxmlLoader.setController(chatViewController); fxmlLoader.setController(chatViewController);
Node load = fxmlLoader.load(); Node load = fxmlLoader.load();
VBox.setVgrow(load, Priority.ALWAYS); VBox.setVgrow(load, Priority.ALWAYS);
VBox vbox = new VBox(); VBox vbox = new VBox();
VBox.setVgrow(vbox, Priority.ALWAYS); VBox.setVgrow(vbox, Priority.ALWAYS);
vbox.getChildren().add(load); vbox.getChildren().add(load);
chatModel.addListener(chatViewController::showMessage); chatModel.addListener((msg) -> chatViewController.showMessage(msg));
Tab newChat = new Tab(title, vbox); Tab newChat = new Tab(title, vbox);
usernameTabMap.put(title, newChat); usernameTabMap.put(title, newChat);
chatTabPane.getTabs().add(newChat); this.chatTabPane.getTabs().add(newChat);
this.chatTabPane.getSelectionModel().select(newChat); this.chatTabPane.getSelectionModel().select(newChat);
chatViewController.setChatTab(newChat); return newChat;
if (!chatType.equals(ChatType.WHISPER)) {
newChat.setClosable(false);
}
return chatViewController;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -280,12 +267,4 @@ public class ChatBoxController {
} }
} }
} }
public void reopenChatTab(Tab chatTab) {
Platform.runLater(
() -> {
chatTabPane.getTabs().add(chatTab);
chatTabPane.getSelectionModel().select(chatTab);
});
}
} }
@@ -11,48 +11,37 @@ import javafx.fxml.Initializable;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
/** Responsible for the presentation of the ChatModel to the Client */ /** Responsible for the presentation of the ChatModel to the Client */
public class ChatViewController implements Initializable { public class ChatViewController implements Initializable {
private final ChatBoxController chatBoxController;
@FXML private Button sendButton; @FXML private Button sendButton;
@FXML private TextField inputField; @FXML private TextField inputField;
@FXML private VBox chatInterfaceVBox;
@FXML private HBox controlBar;
@FXML private ScrollPane scrollPane; @FXML private ScrollPane scrollPane;
@FXML private VBox chat; @FXML private VBox chat;
public Tab getChatTab() {
return chatTab;
}
public void setChatTab(Tab chatTab) {
this.chatTab = chatTab;
}
@FXML private Tab chatTab;
public Boolean tabIsOpen;
private final ChatModel chatModel; private final ChatModel chatModel;
private final String username;
private final ChatController controller; private final ChatController controller;
private static final int CHAT_PADDING = 20; private static final int CHAT_PADDING = 20;
public ChatViewController( public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
ChatController chatController,
ChatModel chatModel,
ChatBoxController chatBoxController) {
this.controller = chatController; this.controller = chatController;
this.username = username;
this.chatModel = chatModel; this.chatModel = chatModel;
this.chatBoxController = chatBoxController;
tabIsOpen = true;
} }
/** /**
@@ -65,8 +54,8 @@ public class ChatViewController implements Initializable {
*/ */
@Override @Override
public void initialize(URL location, ResourceBundle resourceBundle) { public void initialize(URL location, ResourceBundle resourceBundle) {
inputField.setOnAction(_ -> sendMessage()); inputField.setOnAction(event -> sendMessage());
sendButton.setOnAction(_ -> sendMessage()); sendButton.setOnAction(event -> sendMessage());
scrollPane.vvalueProperty().bind(chat.heightProperty()); scrollPane.vvalueProperty().bind(chat.heightProperty());
} }
@@ -99,10 +88,6 @@ public class ChatViewController implements Initializable {
* @param msg The {@link Message} object containing the content and metadata to display. * @param msg The {@link Message} object containing the content and metadata to display.
*/ */
public void showMessage(Message msg) { public void showMessage(Message msg) {
if (!tabIsOpen) {
tabIsOpen = true;
chatBoxController.reopenChatTab(chatTab);
}
Platform.runLater( Platform.runLater(
() -> { () -> {
String msgText = String msgText =
@@ -3,14 +3,13 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.control.skin.TabPaneSkin.TabContentRegion?>
<VBox xmlns="http://javafx.com/javafx/21" <VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1" xmlns:fx="http://javafx.com/fxml/1"
fx:id="chatBox" fx:id="chatBox"
VBox.vgrow="ALWAYS" VBox.vgrow="ALWAYS"
stylesheets="@chatui.css" stylesheets="@chatui.css"
styleClass="chat-box" styleClass="chat-box"
spacing="20" spacing="10"
maxWidth="Infinity"> maxWidth="Infinity">
<HBox spacing="10" <HBox spacing="10"
@@ -18,14 +17,14 @@
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/> <Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region HBox.hgrow="ALWAYS"/> <Region HBox.hgrow="ALWAYS"/>
<MenuButton fx:id="addWhisperChatButton" styleClass="menu-button" text="WHISPER CHAT" <MenuButton fx:id="addWhisperChatButton" styleClass="yellow-button" text="WHISPER CHAT"
alignment="CENTER_RIGHT"> alignment="CENTER_RIGHT">
</MenuButton> </MenuButton>
</HBox> </HBox>
<TabPane fx:id="chatTabPane" <TabPane fx:id="chatTabPane"
VBox.vgrow="ALWAYS" VBox.vgrow="ALWAYS"
styleClass="tab-pane"> styleClass="tab-header-area">
</TabPane> </TabPane>
@@ -8,7 +8,7 @@
<VBox fx:id="chatInterfaceVBox" <VBox fx:id="chatInterfaceVBox"
maxWidth="Infinity" maxWidth="Infinity"
spacing="10" spacing="10"
styleClass="tab-chat-box" styleClass="chat-box"
stylesheets="@chatui.css" stylesheets="@chatui.css"
VBox.vgrow="ALWAYS" VBox.vgrow="ALWAYS"
xmlns="http://javafx.com/javafx/17.0.12" xmlns="http://javafx.com/javafx/17.0.12"
@@ -5,17 +5,7 @@
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12; -fx-border-width: 12;
-fx-padding: 20; -fx-padding: 20;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15); /*-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);*/
}
.tab-chat-box {
-fx-background-color: #0d9e3b;
-fx-background-radius: 58;
-fx-border-radius: 50;
-fx-border-color: #0d763b #0d623b #0d623b #0d763b;
-fx-border-width: 12;
-fx-padding: 20;
} }
.chat-header { .chat-header {
@@ -173,25 +163,25 @@
-fx-border-width: 5; -fx-border-width: 5;
-fx-padding: 10; -fx-padding: 10;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15); -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
-fx-min-height: 10; -fx-max-height: 10;
} }
.tab-pane .tab { .tab-pane .tab {
-fx-background-color: #0d983b; -fx-background-color: #0d9e3b;
-fx-background-radius: 28; -fx-background-radius: 18;
-fx-border-radius: 10; -fx-border-radius: 10;
-fx-border-color: #0d873b #0d733b #0d733b #0d873b; -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5; -fx-border-width: 5;
-fx-padding: 6 15; -fx-padding: 10;
} }
.tab-pane .tab:selected { .tab-pane .tab:selected {
-fx-background-color: #0d873b; -fx-background-color: #0d763b;
-fx-background-radius: 18; -fx-background-radius: 18;
-fx-border-radius: 10; -fx-border-radius: 10;
-fx-border-color: #0d763b #0d623b #0d623b #0d763b; -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5; -fx-border-width: 5;
-fx-padding: 6 15; -fx-padding: 10;
} }
.tab-pane .tab .tab-close-button { .tab-pane .tab .tab-close-button {
@@ -202,6 +192,7 @@
-fx-background-color: #0d9e3b; -fx-background-color: #0d9e3b;
} }
.tab .tab-label { .tab .tab-label {
-fx-alignment: CENTER; -fx-alignment: CENTER;
-fx-text-fill: #ffffff; -fx-text-fill: #ffffff;
@@ -213,36 +204,3 @@
-fx-alignment: CENTER; -fx-alignment: CENTER;
-fx-text-fill: #ffffff; -fx-text-fill: #ffffff;
} }
.menu-button {
-fx-background-color: #b8860b;
-fx-border-color: #ffd700;
-fx-text-fill: #ffffff;
-fx-background-radius: 12;
-fx-border-radius: 12;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-border-width: 2;
-fx-padding: 6 15;
-fx-cursor: hand;
}
.menu-button .context-menu {
-fx-background-color: #b8860b;
-fx-background-radius: 4;
-fx-padding: 6 15;
-fx-cursor: hand;
}
.menu-button:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.menu-button .menu-item {
-fx-background-color: #b8860b;
-fx-border-color: #ffd700;
-fx-text-fill: #ffffff
}
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<!--
Dieses Fenster wird später mit den Serveranfragen verknüpft,
um Nachrichten von Mitspielern und Systemmeldungen in Echtzeit anzuzeigen.
Der Chat unterscheidet intern zwischen:
- Player-to-Player (Privater 2-Personen-Chat)
- Lobby-Chat (Aktueller Raum)
- Globaler Chat (Gesamter Server)
Features, die später implementiert werden sollen:
- Schicke Chat-Bubbles mit Namen und Uhrzeit.
-->
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
alignment="CENTER"
stylesheets="@chatui.css">
<!-- Innenabstand: Schafft oben, rechts und unten 30 Pixel Platz, links nur 10 Pixel (asymmetrisch) -->
<padding>
<Insets top="30" right="30" bottom="30" left="10"/>
</padding>
<children>
<VBox VBox.vgrow="ALWAYS"
styleClass="chat-box"
spacing="10"
maxWidth="Infinity">
<children>
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region minHeight="4" styleClass="chat-separator"/>
<!-- Chatnachrichten werden hier hinzugefügt -->
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
<content>
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
</content>
</ScrollPane>
<!-- Eingabebereich -->
<HBox spacing="10">
<!-- TODO: Größe des TextFields dynamisch anpassen -->
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
<Button fx:id="refreshButton" text="NEU" onAction="#showMessage" styleClass="yellow-button"/>
</HBox>
</children>
</VBox>
</children>
</VBox>
@@ -0,0 +1,155 @@
.chat-box {
-fx-background-color: #0d9e3b;
-fx-background-radius: 58;
-fx-border-radius: 50;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12;
-fx-padding: 20;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
}
.chat-header {
-fx-text-fill: #ffffff;
-fx-font-size: 22px;
-fx-font-weight: bold;
-fx-padding: 0 0 10 0;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.chat-separator {
-fx-background-color: #ffffff;
-fx-min-height: 4px;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.black-input-field {
-fx-background-color: #1a1a1a;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #444444;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.black-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.black-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #000000;
}
.gray-input-field {
-fx-background-color: #333333;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #666666;
-fx-color-color: #cccccc;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.gray-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.gray-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #333333;
}
.yellow-button {
-fx-background-color: #b8860b;
-fx-border-color: #ffd700;
-fx-text-fill: #ffffff;
}
.red-button {
-fx-background-color: #8b0000;
-fx-border-color: #ff4444;
-fx-text-fill: #ffffff;
}
.gray-button {
-fx-background-color: #333333;
-fx-border-color: #666666;
-fx-text-fill: #cccccc;
}
.gray-button, .yellow-button, .red-button {
-fx-background-radius: 12;
-fx-border-radius: 12;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-border-width: 2;
-fx-padding: 6 15;
-fx-cursor: hand;
}
.gray-button:hover, .yellow-button:hover, .red-button:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.chat-scroll-pane {
-fx-background-color: transparent;
-fx-background: transparent;
-fx-border-color: transparent;
}
.chat-VBox {
-fx-background-color: transparent;
-fx-background: transparent;
}
.scroll-pane {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.scroll-pane .scroll-bar:vertical {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar .track {
-fx-background-color: #5c3d10;
-fx-background-insets: 0;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb {
-fx-background-color: #3d260a;
-fx-background-insets: 0;
-fx-background-radius: 5;
-fx-pref-width: 6;
-fx-pref-height: 6;
-fx-padding: 0;
}
.scroll-pane .scroll-bar:horizontal {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar:horizontal .track {
-fx-background-color: #5c3d10;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar:horizontal .thumb {
-fx-background-color: #3d260a;
-fx-background-radius: 5;
-fx-pref-height: 6;
}
.scroll-pane .scroll-bar:horizontal .thumb:hover {
-fx-pref-height: 10;
}
@@ -1,41 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class ChatClientTest implements ChatClientInterface {
private ChatClientTest otherChatClient;
public final Queue<Message> messageQueue;
public ChatClientTest() {
this.messageQueue = new LinkedList<>();
}
public ChatClientTest(ChatClientTest otherChatClient) {
this.otherChatClient = otherChatClient;
this.messageQueue = new LinkedList<>();
}
@Override
public void sendMessage(Message message) {
otherChatClient.messageQueue.add(message);
}
@Override
public List<Message> getMessages() {
List<Message> messages = new LinkedList<>();
while (!messageQueue.isEmpty()) {
Message message = messageQueue.poll();
messages.add(message);
}
return messages;
}
@Override
public List<String> getUsers() {
return List.of();
}
}
@@ -1,151 +1,21 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat; package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import java.util.ArrayList;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ChatControllerTest { public class ChatControllerTest {
ChatController chatController1;
ChatController chatController2;
String name1 = "testperson1";
String name2 = "testperson1";
@BeforeEach @BeforeEach
public void setup() { public void setup() {}
ChatClientTest receivingChatClient = new ChatClientTest();
ChatClientTest sendingChatClient = new ChatClientTest(receivingChatClient);
this.chatController1 = new ChatController(name1, sendingChatClient);
this.chatController2 = new ChatController(name2, receivingChatClient);
}
@AfterEach @AfterEach
public void teardown() {} public void teardown() {}
@Test /*
public void sendGlobalMessage() { * @Test
ChatModel global = new ChatModel(ChatType.GLOBAL, name2, -1, null); * public void testChatController() {
chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.GLOBAL), global); * Client client = new Client("user1");
* client.startApplication();
Message msg1 = new Message(ChatType.GLOBAL, -1, name1, null, "Hallo Welt"); * }
Message msg2 = new Message(ChatType.GLOBAL, -1, name1, null, "$$Hallo$$Welt$$"); */
Message msg3 = new Message(ChatType.GLOBAL, -1, name1, null, "_*'=(&%$/§&%");
Message msg4 = new Message(ChatType.LOBBY, -1, name1, null, "Guten Tag");
Message msg5 = new Message(ChatType.LOBBY, -1, name1, null, "$/&)$)(&=)$=");
Message msg6 = new Message(ChatType.GLOBAL, -1, name1, null, "NAME=ANDERER_NUTZER");
Message msg7 = new Message(ChatType.GLOBAL, -1, name1, null, "________");
ArrayList<Message> messages = new ArrayList<>();
messages.add(msg1);
messages.add(msg2);
messages.add(msg3);
messages.add(msg4);
messages.add(msg5);
messages.add(msg6);
messages.add(msg7);
chatController1.onSendToNetwork(msg1);
chatController1.onSendToNetwork(msg2);
chatController1.onSendToNetwork(msg3);
chatController1.onSendToNetwork(msg4);
chatController1.onSendToNetwork(msg5);
chatController1.onSendToNetwork(msg6);
chatController1.onSendToNetwork(msg7);
chatController2.receiveMessage();
assertEquals(5, global.messages.size());
assertEquals(messages.get(0), global.messages.get(0));
assertEquals(messages.get(1), global.messages.get(1));
assertEquals(messages.get(2), global.messages.get(2));
assertNotEquals(messages.get(3), global.messages.get(3));
assertNotEquals(messages.get(4), global.messages.get(4));
assertEquals(messages.get(5), global.messages.get(3));
assertEquals(messages.get(6), global.messages.get(4));
}
@Test
public void sendLobbyMessage() {
ChatModel lobby = new ChatModel(ChatType.LOBBY, name2, 1, null);
chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.LOBBY), lobby);
Message msg1 = new Message(ChatType.LOBBY, 1, name1, null, "Hallo Welt");
Message msg2 = new Message(ChatType.LOBBY, 1, name1, null, "$$Hallo$$Welt$$");
Message msg3 = new Message(ChatType.LOBBY, 1, name1, null, "_*'=(&%$/§&%");
Message msg4 = new Message(ChatType.LOBBY, 4, name1, null, "Guten Tag");
Message msg5 = new Message(ChatType.LOBBY, 5, name1, null, "$/&)$)(&=)$=");
Message msg6 = new Message(ChatType.LOBBY, 1, name1, null, "NAME=ANDERER_NUTZER");
Message msg7 = new Message(ChatType.LOBBY, 1, name1, null, "________");
ArrayList<Message> messages = new ArrayList<>();
messages.add(msg1);
messages.add(msg2);
messages.add(msg3);
messages.add(msg4);
messages.add(msg5);
messages.add(msg6);
messages.add(msg7);
for (Message msg : messages) {
chatController1.onSendToNetwork(msg);
}
chatController2.receiveMessage();
assertEquals(5, lobby.messages.size());
assertEquals(messages.get(0), lobby.messages.get(0));
assertEquals(messages.get(1), lobby.messages.get(1));
assertEquals(messages.get(2), lobby.messages.get(2));
assertNotEquals(messages.get(3), lobby.messages.get(3));
assertNotEquals(messages.get(4), lobby.messages.get(4));
assertEquals(messages.get(5), lobby.messages.get(3));
assertEquals(messages.get(6), lobby.messages.get(4));
}
@Test
public void sendWhisperMessage() {
ChatModel whisper = new ChatModel(ChatType.WHISPER, name2, -1, name1);
chatController2
.getChatModelMap()
.put(new ChatController.ChatKey(ChatType.WHISPER, name1), whisper);
Message msg1 = new Message(ChatType.WHISPER, -1, name1, name2, "Hallo Welt");
Message msg2 = new Message(ChatType.WHISPER, -1, name1, name2, "$$Hallo$$Welt$$");
Message msg3 = new Message(ChatType.WHISPER, -1, name1, name2, "_*'=(&%$/§&%");
Message msg4 = new Message(ChatType.WHISPER, -1, name1, name2, "NAME=ANDERER_NUTZER");
Message msg5 = new Message(ChatType.WHISPER, -1, name1, name2, "________");
ArrayList<Message> messages = new ArrayList<>();
messages.add(msg1);
messages.add(msg2);
messages.add(msg3);
messages.add(msg4);
messages.add(msg5);
for (Message msg : messages) {
chatController1.onSendToNetwork(msg);
}
chatController2.receiveMessage();
assertEquals(5, whisper.messages.size());
assertEquals(messages.get(0), whisper.messages.get(0));
assertEquals(messages.get(1), whisper.messages.get(1));
assertEquals(messages.get(2), whisper.messages.get(2));
assertEquals(messages.get(3), whisper.messages.get(3));
assertEquals(messages.get(4), whisper.messages.get(4));
}
} }
@@ -0,0 +1,56 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/** Smoke test for Taskbar UI. Ensures FXML loads and controller initializes without errors. */
class CasinoGameControllerUITest {
private static boolean fxStarted = false;
/** Initializes JavaFX once before all tests. */
@BeforeAll
static void startJavaFX() throws Exception {
if (!fxStarted) {
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(
() -> {
Platform.setImplicitExit(false);
latch.countDown();
});
} catch (IllegalStateException ignored) {
latch.countDown();
}
assertTrue(latch.await(10, TimeUnit.SECONDS), "JavaFX did not start");
fxStarted = true;
}
}
/** Ensures FXML loads and controller initializes without errors. */
@Test
void taskbarLoadsWithoutErrors() throws Exception {
URL url = getClass().getResource("/ui-structure/gameuicomponents/Taskbar.fxml");
assertNotNull(url);
FXMLLoader loader = new FXMLLoader(url);
Parent root = loader.load();
TaskbarController controller = loader.getController();
assertNotNull(root);
assertNotNull(controller);
}
}
@@ -1,37 +1,175 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents; package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.*;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState; import ch.unibas.dmi.dbis.cs108.casono.client.game.*;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import java.lang.reflect.Field;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import java.lang.reflect.Method;
import java.net.URL;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/**
* TODO: Refactor UI tests to avoid reflection and test behavior via public APIs or real UI actions
* (preferably using TestFX) for better maintainability and robustness.
*
* <p>These tests verify: - input validation behavior - UI state updates based on game state -
* correct enabling/disabling of action buttons
*/
class TaskbarControllerInputValidationTest { class TaskbarControllerInputValidationTest {
private static boolean fxStarted = false;
/** Initializes JavaFX once before all tests. */
@BeforeAll
static void initJavaFX() throws Exception {
if (fxStarted) {
return;
}
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(
() -> {
Platform.setImplicitExit(false);
latch.countDown();
});
} catch (IllegalStateException ignored) {
latch.countDown();
}
latch.await(10, TimeUnit.SECONDS);
fxStarted = true;
}
/** Ensures invalid input values do not enable the BET button. */
@Test @Test
void updateLogicRunsWithoutCrash() { void invalidInputShouldBeRejected() throws Exception {
Fixture f = load();
String[] invalidInputs = {"", "abc", "-1", "999999999999"};
for (String input : invalidInputs) {
setText(f.input, input);
invokePrivate(f.controller, "refreshBetInputUi");
Button bet = get(f.controller, "betButton", Button.class);
assertFalse(bet.isVisible(), "Invalid input must not enable BET button: " + input);
}
}
/** Ensures valid input enables the BET button. */
@Test
void validInputShouldEnableBet() throws Exception {
Fixture f = load();
GameState state = createGameState();
setField(f.controller, "myPlayerId", PlayerId.of("me"));
setField(f.controller, "lastState", state);
setField(f.controller, "inputActionAllowed", true);
f.controller.update(state, PlayerId.of("me"));
setText(f.input, "200");
invokePrivate(f.controller, "refreshBetInputUi");
Button bet = get(f.controller, "betButton", Button.class);
assertTrue(bet.isVisible(), "BET button should be enabled for valid input");
}
/** Ensures all UI actions are disabled when the game is finished. */
@Test
void finishedStateShouldDisableUi() throws Exception {
Fixture f = load();
GameState state = createGameState();
state.phase = "FINISHED";
f.controller.update(state, PlayerId.of("me"));
assertTrue(f.input.isDisabled());
assertTrue(get(f.controller, "callButton", Button.class).isDisabled());
assertTrue(get(f.controller, "foldButton", Button.class).isDisabled());
assertTrue(get(f.controller, "raiseButton", Button.class).isDisabled());
}
/** Creates a minimal game state for testing. */
private GameState createGameState() {
GameState state = new GameState(); GameState state = new GameState();
state.players = List.of( Player me = new Player(PlayerId.of("me"), 20000);
new Player(PlayerId.of("me"), 20000), Player other = new Player(PlayerId.of("other"), 20000);
new Player(PlayerId.of("other"), 20000)
);
state.players = List.of(me, other);
state.phase = "PREFLOP"; state.phase = "PREFLOP";
state.currentBet = 200; state.currentBet = 200;
state.activePlayer = 0; state.activePlayer = 0;
DummyController controller = new DummyController(); return state;
controller.update(state, PlayerId.of("me"));
assertTrue(true);
} }
static class DummyController { /** Loads the JavaFX controller and its UI components. */
void update(GameState state, PlayerId id) { private Fixture load() throws Exception {
URL url = getClass().getResource("/ui-structure/gameuicomponents/Taskbar.fxml");
assertNotNull(url);
FXMLLoader loader = new FXMLLoader(url);
Parent root = loader.load();
root.applyCss();
root.layout();
TaskbarController controller = loader.getController();
TextField input = get(controller, "taskbarInput", TextField.class);
Button bet = get(controller, "betButton", Button.class);
return new Fixture(controller, input, bet);
} }
private record Fixture(TaskbarController controller, TextField input, Button betButton) {}
private static void setText(TextField field, String value) {
field.setText(value);
} }
private static void invokePrivate(Object obj, String method) throws Exception {
Method m = obj.getClass().getDeclaredMethod(method);
m.setAccessible(true);
m.invoke(obj);
}
private static <T> T get(Object obj, String field, Class<T> type) throws Exception {
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
return type.cast(f.get(obj));
}
/** Sets a private field value using reflection. */
private static void setField(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
/** Gets a private field value using reflection. */
// private static <T> T getField(Object obj, String fieldName, Class<T> type) throws Exception {
// Field field = obj.getClass().getDeclaredField(fieldName);
// field.setAccessible(true);
// return type.cast(field.get(obj));
// }
} }
@@ -1,55 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
import static org.junit.jupiter.api.Assertions.*;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;
public class CommandParserDispatcherTest {
private class DummyRequest extends Request {
public DummyRequest(RequestContext context) {
super(context);
}
}
@Test
void testSuccessfulParsing() {
CommandParserDispatcher dispatcher = new CommandParserDispatcher();
RequestContext ctx = new RequestContext(new SessionId(UUID.randomUUID()), 7411);
PrimitiveRequest primitiveRequest = new PrimitiveRequest(ctx, "TEST_CMD", List.of());
dispatcher.register(
"TEST_CMD",
new CommandParser<Request>() {
@Override
public Request parse(PrimitiveRequest primitiveRequest) {
return new DummyRequest(primitiveRequest.context());
}
});
Request result = dispatcher.parse(primitiveRequest);
assertTrue(result instanceof DummyRequest);
assertEquals(ctx, result.getContext());
}
@Test
void testUnknownCommandThrows() {
CommandParserDispatcher dispatcher = new CommandParserDispatcher();
RequestContext ctx = new RequestContext(new SessionId(UUID.randomUUID()), 42);
PrimitiveRequest primitiveRequest = new PrimitiveRequest(ctx, "UNKNOWN_CMD", List.of());
UnknownCommandException ex =
assertThrows(
UnknownCommandException.class, () -> dispatcher.parse(primitiveRequest));
assertEquals("UNKNOWN_CMD", ex.getMessage());
}
}
@@ -1,49 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class EventBusTest {
private EventBus eventBus;
private class TestEvent implements Event {}
@BeforeEach
void setUp() {
eventBus = new EventBus();
}
@Test
void testSingleSubscriberReceivesEvent() {
AtomicBoolean called = new AtomicBoolean(false);
eventBus.subscribe(TestEvent.class, e -> called.set(true));
eventBus.publish(new TestEvent());
assertTrue(called.get());
}
@Test
void testMultipleSubscribersReceiveEvent() {
AtomicInteger counter = new AtomicInteger(0);
eventBus.subscribe(TestEvent.class, e -> counter.incrementAndGet());
eventBus.subscribe(TestEvent.class, e -> counter.incrementAndGet());
eventBus.publish(new TestEvent());
assertEquals(2, counter.get());
}
@Test
void testPublishNullThrowsNpe() {
assertThrows(NullPointerException.class, () -> eventBus.publish((Event) null));
}
@Test
void testSubscribingToBaseEventDoesNotReceiveSubclass() {
AtomicBoolean called = new AtomicBoolean(false);
eventBus.subscribe(Event.class, e -> called.set(true));
eventBus.publish(new TestEvent());
assertFalse(called.get());
}
}
@@ -1,84 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class ResponseBodyBuilderTest {
@Test
void testSingleParameter() {
ResponseBody body = ResponseBody.builder().param("param1", "value1").build();
assertEquals(1, body.nodes().size());
ResponseParameter parameter = (ResponseParameter) body.nodes().get(0);
assertEquals("param1", parameter.key());
assertEquals("value1", parameter.value());
}
@Test
void testSingleBlock() {
ResponseBody body =
ResponseBody.builder()
.block("block1", block -> block.param("param1", "value1"))
.build();
assertEquals(1, body.nodes().size());
ResponseBlock block = (ResponseBlock) body.nodes().get(0);
assertEquals(1, block.children().size());
assertEquals("block1", block.tag());
ResponseParameter parameter = (ResponseParameter) block.children().get(0);
assertEquals("param1", parameter.key());
assertEquals("value1", parameter.value());
}
@Test
void testMultipleParameter() {
ResponseBody body =
ResponseBody.builder().param("param1", "value1").param("param2", "value2").build();
assertEquals(2, body.nodes().size());
ResponseParameter parameter;
parameter = (ResponseParameter) body.nodes().get(0);
assertEquals("param1", parameter.key());
assertEquals("value1", parameter.value());
parameter = (ResponseParameter) body.nodes().get(1);
assertEquals("param2", parameter.key());
assertEquals("value2", parameter.value());
}
@Test
void testMultipleBlocks() {
ResponseBody body =
ResponseBody.builder()
.block("block1", block -> block.param("param1", "value1"))
.block(
"block2",
block -> {
block.param("param2", "value2");
block.param("param3", "value3");
})
.build();
assertEquals(2, body.nodes().size());
ResponseBlock block;
ResponseParameter parameter;
block = (ResponseBlock) body.nodes().get(0);
assertEquals("block1", block.tag());
assertEquals(1, block.children().size());
parameter = (ResponseParameter) block.children().get(0);
assertEquals("param1", parameter.key());
assertEquals("value1", parameter.value());
block = (ResponseBlock) body.nodes().get(1);
assertEquals("block2", block.tag());
assertEquals(2, block.children().size());
parameter = (ResponseParameter) block.children().get(0);
assertEquals("param2", parameter.key());
assertEquals("value2", parameter.value());
}
}
@@ -1,130 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.Response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBlock;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;
public class ResponseEncoderTest {
private class TestResponse extends Response {
public TestResponse(RequestContext context, ResponseBody body) {
super(context, body);
}
@Override
public String prefix() {
return "+OK";
}
}
@Test
void testEncodeParameterSimple() {
ResponseBody body = ResponseBody.builder().param("param", "value").build();
RequestContext ctx = new RequestContext(new SessionId(), 42);
Response response = new TestResponse(ctx, body);
PrimitiveResponse pr = ResponseEncoder.encode(response);
String expected = "+OK\n\tparam=value\nEND";
assertEquals(expected, pr.payload());
}
@Test
void testEncodeParameterMasking() {
ResponseBody body = ResponseBody.builder().param("msg", "It's a me, malario").build();
RequestContext ctx = new RequestContext(new SessionId(), 42);
Response response = new TestResponse(ctx, body);
PrimitiveResponse pr = ResponseEncoder.encode(response);
String expected = "+OK\n\tmsg='It\\'s a me, malario'\nEND";
assertEquals(expected, pr.payload());
}
@Test
void testEncodeNestedBlock() {
ResponseBody body =
ResponseBody.builder().block("TAG", b -> b.param("param", "key")).build();
RequestContext ctx = new RequestContext(new SessionId(UUID.randomUUID()), 42);
Response response = new TestResponse(ctx, body);
PrimitiveResponse pr = ResponseEncoder.encode(response);
String expected = "+OK\n\tTAG\n\t\tparam=key\n\tEND\nEND";
assertEquals(expected, pr.payload());
}
@Test
void testPrivateEncodeParameter()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
var param = new ResponseParameter("p", "v");
StringBuilder sb = new StringBuilder();
Method m =
ResponseEncoder.class.getDeclaredMethod(
"encodeParameter", ResponseParameter.class, StringBuilder.class, int.class);
m.setAccessible(true);
m.invoke(null, param, sb, 1);
assertEquals("\tp=v", sb.toString());
}
@Test
void testWordNoMask()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method m = ResponseEncoder.class.getDeclaredMethod("maskIfNeeded", String.class);
m.setAccessible(true);
String noMask = (String) m.invoke(null, "abc");
assertEquals("abc", noMask);
}
@Test
void testMaskString()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method m = ResponseEncoder.class.getDeclaredMethod("maskIfNeeded", String.class);
m.setAccessible(true);
String withSpace = (String) m.invoke(null, "a b");
assertEquals("'a b'", withSpace);
}
@Test
void testEscapeSingleQuote()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method m = ResponseEncoder.class.getDeclaredMethod("maskIfNeeded", String.class);
m.setAccessible(true);
String withSpace = (String) m.invoke(null, "a b");
assertEquals("'a b'", withSpace);
}
@Test
void testPrivateEncodeBlock()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
var child = new ResponseParameter("a", "b");
var block = new ResponseBlock("TAG", List.of(child));
StringBuilder sb = new StringBuilder();
Method m =
ResponseEncoder.class.getDeclaredMethod(
"encodeBlock", ResponseBlock.class, StringBuilder.class, int.class);
m.setAccessible(true);
m.invoke(null, block, sb, 1);
String expected = "\tTAG\n\t\ta=b\n\tEND";
assertEquals(expected, sb.toString());
}
}
@@ -1,60 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import java.lang.reflect.Field;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class SessionDisconnectJobTest {
private EventBus eventBus;
private SessionManager sessionManager;
@BeforeEach
void initEnvironment() {
eventBus = new EventBus();
sessionManager = new SessionManager(eventBus, null);
}
@Test
void testDisconnectValidSession() {
SessionDisconnectJob disconnectJob =
new SessionDisconnectJob(sessionManager, eventBus, Duration.ofSeconds(30));
sessionManager.create(null, null);
sessionManager.create(null, null);
AtomicBoolean disconnectOccurred = new AtomicBoolean(false);
eventBus.subscribe(DisconnectEvent.class, (event) -> disconnectOccurred.set(true));
disconnectJob.run();
assertFalse(disconnectOccurred.get());
}
@Test
void testDisconnectExpiredSession()
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
SessionDisconnectJob disconnectJob =
new SessionDisconnectJob(sessionManager, eventBus, Duration.ofSeconds(30));
sessionManager.create(null, null);
Session expiredSession = sessionManager.create(null, null);
Field lastActivityField = Session.class.getDeclaredField("lastActivity");
lastActivityField.setAccessible(true);
lastActivityField.set(expiredSession, Instant.now().minus(35, ChronoUnit.SECONDS));
AtomicReference<DisconnectEvent> disconnectEvent = new AtomicReference<>();
eventBus.subscribe(DisconnectEvent.class, (event) -> disconnectEvent.set(event));
disconnectJob.run();
assertEquals(disconnectEvent.get().sessionId(), expiredSession.getId());
}
}