diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..cb616581
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: maven
+ directory: app
+ schedule:
+ interval: daily
+ target-branch: dev
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: daily
+ target-branch: dev
\ No newline at end of file
diff --git a/.github/workflows/simple-build-test.yml b/.github/workflows/simple-build-test.yml
new file mode 100644
index 00000000..4bde0ed8
--- /dev/null
+++ b/.github/workflows/simple-build-test.yml
@@ -0,0 +1,55 @@
+name: Simple build and test
+
+on:
+ push:
+ branches:
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '.github/dependabot.yml'
+ - '.github/workflows/dependabot-automerge.yml'
+ pull_request:
+ branches:
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '.github/dependabot.yml'
+ - '.github/workflows/dependabot-automerge.yml'
+ workflow_dispatch:
+
+concurrency:
+ group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}"
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ jvm-build-test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java:
+ - '17'
+ - '21'
+ services:
+ ollama:
+ image: ollama/ollama
+ ports:
+ - 11434:11434
+ name: "jvm-build-test-${{ matrix.java }}"
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Java ${{ matrix.java }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ matrix.java }}
+ distribution: temurin
+ cache: maven
+
+ - name: "build-test-jvm-java${{ matrix.java }}"
+ working-directory: app
+ run: |
+ ./mvnw -B clean verify \
+ -Dquarkus.langchain4j.chat-model.provider=ollama \
+ -Dquarkus.http.host=0.0.0.0 \
+ -Dmaven.compiler.release=${{ matrix.java }}
\ No newline at end of file
diff --git a/app/pom.xml b/app/pom.xml
index 8ccf8275..1989c556 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -6,11 +6,14 @@
insurance-app
1.0.0-SNAPSHOT
+ 3.26.0
3.12.1
17
UTF-8
UTF-8
0.15.1
+ 1.0.0
+ 2.3.7
quarkus-bom
io.quarkus.platform
3.11.0
@@ -67,18 +70,51 @@
io.quarkiverse.quinoa
quarkus-quinoa
- 2.3.7
+ ${quarkus.quinoa.version}
io.quarkus
quarkus-junit5
test
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
+
+ io.quarkus
+ quarkus-panache-mock
+ test
+
io.rest-assured
rest-assured
test
+
+ io.quarkiverse.quinoa
+ quarkus-quinoa-testing
+ ${quarkus.quinoa.version}
+ test
+
+
+ io.quarkiverse.playwright
+ quarkus-playwright
+ ${quarkus.playwright.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
@@ -114,6 +150,9 @@
org.jboss.logmanager.LogManager
${maven.home}
+
+ http://localhost:8081/api
+
@@ -133,6 +172,9 @@
org.jboss.logmanager.LogManager
${maven.home}
+
+ http://localhost:8081/api
+
diff --git a/app/src/main/java/org/parasol/ai/ClaimService.java b/app/src/main/java/org/parasol/ai/ClaimService.java
index 1c96a119..05d7a630 100644
--- a/app/src/main/java/org/parasol/ai/ClaimService.java
+++ b/app/src/main/java/org/parasol/ai/ClaimService.java
@@ -1,13 +1,13 @@
package org.parasol.ai;
+import jakarta.enterprise.context.SessionScoped;
+
import org.parasol.model.ClaimBotQuery;
-import org.parasol.model.ClaimBotQueryResponse;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.smallrye.mutiny.Multi;
-import jakarta.enterprise.context.SessionScoped;
@RegisterAiService
@SessionScoped
@@ -22,9 +22,9 @@ public interface ClaimService {
)
@UserMessage("""
Claim Summary:
- {{claim}}
+ {{query.claim}}
- Question: {{query}}
+ Question: {{query.query}}
""")
- Multi chat(String claim, String query);
+ Multi chat(ClaimBotQuery query);
}
diff --git a/app/src/main/java/org/parasol/model/Claim.java b/app/src/main/java/org/parasol/model/Claim.java
index 460a3d4b..9e839de4 100644
--- a/app/src/main/java/org/parasol/model/Claim.java
+++ b/app/src/main/java/org/parasol/model/Claim.java
@@ -1,16 +1,20 @@
package org.parasol.model;
-import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
+import io.quarkus.hibernate.orm.panache.PanacheEntity;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+
@Entity
+@JsonNaming(SnakeCaseStrategy.class)
public class Claim extends PanacheEntity {
-
- public String claim_number;
+ public String claimNumber;
public String category;
- public String policy_number;
- public String client_name;
+ public String policyNumber;
+ public String clientName;
public String subject;
@Column(length = 5000)
public String body;
diff --git a/app/src/main/java/org/parasol/model/ClaimBotQuery.java b/app/src/main/java/org/parasol/model/ClaimBotQuery.java
index 5dd020fd..d342f661 100644
--- a/app/src/main/java/org/parasol/model/ClaimBotQuery.java
+++ b/app/src/main/java/org/parasol/model/ClaimBotQuery.java
@@ -1,11 +1,5 @@
package org.parasol.model;
-import com.fasterxml.jackson.annotation.JsonCreator;
-
public record ClaimBotQuery(String claim, String query) {
- @JsonCreator
- public ClaimBotQuery {
- }
-
}
diff --git a/app/src/main/java/org/parasol/model/ClaimBotQueryResponse.java b/app/src/main/java/org/parasol/model/ClaimBotQueryResponse.java
index 8a8a278a..fc192b33 100644
--- a/app/src/main/java/org/parasol/model/ClaimBotQueryResponse.java
+++ b/app/src/main/java/org/parasol/model/ClaimBotQueryResponse.java
@@ -1,11 +1,5 @@
package org.parasol.model;
-import com.fasterxml.jackson.annotation.JsonCreator;
-
public record ClaimBotQueryResponse(String type, String token, String source) {
- @JsonCreator
- public ClaimBotQueryResponse {
- }
-
}
diff --git a/app/src/main/java/org/parasol/resources/ClaimWebsocketChatBot.java b/app/src/main/java/org/parasol/resources/ClaimWebsocketChatBot.java
index 559cb8e4..b54bb3c0 100644
--- a/app/src/main/java/org/parasol/resources/ClaimWebsocketChatBot.java
+++ b/app/src/main/java/org/parasol/resources/ClaimWebsocketChatBot.java
@@ -4,26 +4,37 @@
import org.parasol.model.ClaimBotQuery;
import org.parasol.model.ClaimBotQueryResponse;
+import io.quarkus.logging.Log;
+import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+
import io.smallrye.mutiny.Multi;
@WebSocket(path = "/ws/query")
public class ClaimWebsocketChatBot {
- private final ClaimService bot;
+ private final ClaimService bot;
public ClaimWebsocketChatBot(ClaimService bot) {
this.bot = bot;
}
@OnOpen
- public void onOpen() {
- System.out.println("Websocket opened");
+ public void onOpen(WebSocketConnection connection) {
+ Log.infof("Websocket connection %s opened", connection.id());
+ }
+
+ @OnClose
+ public void onClose(WebSocketConnection connection) {
+ Log.infof("Websocket connection %s closed", connection.id());
}
+
@OnTextMessage
public Multi onMessage(ClaimBotQuery query) {
- return bot.chat(query.claim(), query.query())
+ return bot.chat(query)
+ .invoke(response -> Log.debugf("Got chat response: %s", response))
.map(resp -> new ClaimBotQueryResponse("token", resp, ""));
}
}
diff --git a/app/src/main/java/org/parasol/resources/GreetingResource.java b/app/src/main/java/org/parasol/resources/GreetingResource.java
deleted file mode 100644
index 992cbcd4..00000000
--- a/app/src/main/java/org/parasol/resources/GreetingResource.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.parasol.resources;
-
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
-
-@Path("/hello")
-public class GreetingResource {
-
- @GET
- @Produces(MediaType.TEXT_PLAIN)
- public String hello() {
- return "Hello from Quarkus REST";
- }
-}
diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties
index 63da5042..616c68ca 100644
--- a/app/src/main/resources/application.properties
+++ b/app/src/main/resources/application.properties
@@ -14,18 +14,23 @@ quarkus.langchain4j.openai.base-url=http://localhost:8000/v1
# Ollama
quarkus.langchain4j.ollama.timeout=600s
-# quarkus.langchain4j.ollama.base-url=http://127.0.0.1:11434
quarkus.langchain4j.ollama.model-id=llama3
quarkus.langchain4j.ollama.chat-model.temperature=0.3
quarkus.http.host=0.0.0.0
quarkus.http.port=8005
quarkus.http.cors=true
-quarkus.dev-ui.cors.enabled=false
quarkus.http.cors.origins=*
+quarkus.dev-ui.cors.enabled=false
+#%dev.quarkus.http.cors.origins=/.*/
+
+quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
+quarkus.log.category."org.parasol".level=DEBUG
+%dev,test.quarkus.log.console.level=DEBUG
# Quinoa
quarkus.quinoa.package-manager-install=true
quarkus.quinoa.package-manager-install.node-version=22.2.0
+quarkus.quinoa.package-manager-install.npm-version=10.8.1
quarkus.quinoa.build-dir=dist
quarkus.quinoa.enable-spa-routing=true
diff --git a/app/src/test/java/org/parasol/resources/ClaimResourceTests.java b/app/src/test/java/org/parasol/resources/ClaimResourceTests.java
new file mode 100644
index 00000000..005820a7
--- /dev/null
+++ b/app/src/test/java/org/parasol/resources/ClaimResourceTests.java
@@ -0,0 +1,110 @@
+package org.parasol.resources;
+
+import static io.restassured.RestAssured.get;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import jakarta.ws.rs.core.Response.Status;
+
+import org.junit.jupiter.api.Test;
+import org.parasol.model.Claim;
+
+import io.quarkus.panache.mock.PanacheMock;
+import io.quarkus.test.junit.QuarkusTest;
+
+import io.restassured.http.ContentType;
+
+@QuarkusTest
+class ClaimResourceTests {
+ @Test
+ void getAllNoneFound() {
+ PanacheMock.mock(Claim.class);
+
+ when(Claim.listAll())
+ .thenReturn(List.of());
+
+ get("/api/db/claims").then()
+ .statusCode(Status.OK.getStatusCode())
+ .contentType(ContentType.JSON)
+ .body("$.size()", is(0));
+
+ PanacheMock.verify(Claim.class).listAll();
+ PanacheMock.verifyNoMoreInteractions(Claim.class);
+ }
+
+ @Test
+ void getAllSomeFound() {
+ PanacheMock.mock(Claim.class);
+ when(Claim.listAll())
+ .thenReturn(List.of(createClaim()));
+
+ var claims = get("/api/db/claims").then()
+ .statusCode(Status.OK.getStatusCode())
+ .contentType(ContentType.JSON)
+ .extract().body()
+ .jsonPath().getList(".", Claim.class);
+
+ assertThat(claims)
+ .isNotNull()
+ .singleElement()
+ .usingRecursiveComparison()
+ .isEqualTo(createClaim());
+
+ PanacheMock.verify(Claim.class).listAll();
+ PanacheMock.verifyNoMoreInteractions(Claim.class);
+ }
+
+ @Test
+ void getOneNotFound() {
+ PanacheMock.mock(Claim.class);
+ when(Claim.findById(1))
+ .thenReturn(null);
+
+ get("/api/db/claims/{id}", 1).then()
+ .statusCode(Status.NO_CONTENT.getStatusCode())
+ .contentType(ContentType.JSON)
+ .body(blankOrNullString());
+
+ PanacheMock.verify(Claim.class).findById(1);
+ PanacheMock.verifyNoMoreInteractions(Claim.class);
+ }
+
+ @Test
+ void getOneFound() {
+ PanacheMock.mock(Claim.class);
+ when(Claim.findById(1))
+ .thenReturn(createClaim());
+
+ var claim = get("/api/db/claims/{id}", 1).then()
+ .statusCode(Status.OK.getStatusCode())
+ .contentType(ContentType.JSON)
+ .extract().as(Claim.class);
+
+ assertThat(claim)
+ .isNotNull()
+ .usingRecursiveComparison()
+ .isEqualTo(createClaim());
+
+ PanacheMock.verify(Claim.class).findById(1);
+ PanacheMock.verifyNoMoreInteractions(Claim.class);
+ }
+
+ private static Claim createClaim() {
+ var claim = new Claim();
+ claim.claimNumber = "001";
+ claim.category = "Auto";
+ claim.policyNumber = "123";
+ claim.clientName = "client";
+ claim.subject = "collision";
+ claim.body = "body";
+ claim.summary = "Car was damaged in accident";
+ claim.location = "driveway";
+ claim.time = "afternoon";
+ claim.sentiment = "Very bad";
+
+ return claim;
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/org/parasol/resources/ClaimWebsocketChatBotTests.java b/app/src/test/java/org/parasol/resources/ClaimWebsocketChatBotTests.java
new file mode 100644
index 00000000..2052791e
--- /dev/null
+++ b/app/src/test/java/org/parasol/resources/ClaimWebsocketChatBotTests.java
@@ -0,0 +1,134 @@
+package org.parasol.resources;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.Mockito.*;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+
+import jakarta.inject.Inject;
+
+import org.jboss.logging.Logger;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatcher;
+import org.parasol.ai.ClaimService;
+import org.parasol.model.ClaimBotQuery;
+import org.parasol.model.ClaimBotQueryResponse;
+
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.websockets.next.CloseReason;
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnError;
+import io.quarkus.websockets.next.OnOpen;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocketClient;
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnector;
+
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+
+@QuarkusTest
+class ClaimWebsocketChatBotTests {
+ private static final String CLAIM = "This is the claim details";
+ private static final String QUERY = "Should I approve this claim?";
+ private static final List RESPONSE = List.of("You", "should", "not", "approve", "this", "claim");
+
+ @InjectMock
+ ClaimService claimService;
+
+ @TestHTTPResource("/")
+ URI claimChatBotRootUri;
+
+ @Inject
+ WebSocketConnector connector;
+
+ @Test
+ void chatBotWorks() {
+ ArgumentMatcher matcher = query ->
+ Objects.nonNull(query) &&
+ QUERY.equals(query.query()) &&
+ CLAIM.equals(query.claim());
+
+ // A Multi which will return our response with a 0.5 second delay between each item
+ var delayedMulti = Multi.createFrom().iterable(RESPONSE)
+ .onItem().call(() -> Uni.createFrom().nullItem().onItem().delayIt().by(Duration.ofMillis(500)));
+
+ // Set up our AI mock
+ when(this.claimService.chat(argThat(matcher)))
+ .thenReturn(delayedMulti);
+
+ // Create a WebSocket connection and wait for the connection to establish
+ var connection = connectClient();
+
+ // Send our query
+ connection.sendTextAndAwait(new ClaimBotQuery(CLAIM, QUERY));
+
+ // Wait for the server to respond with what we expect
+ await()
+ .atMost(Duration.ofMinutes(5))
+ .until(() -> ClientEndpoint.MESSAGES.size() == RESPONSE.size());
+
+ // Verify the messages are what we expected
+ assertThat(ClientEndpoint.MESSAGES)
+ .hasSameElementsAs(RESPONSE);
+
+ // Close the connection
+ connection.closeAndAwait();
+
+ // Verify the AI chat was called with the correct parameters
+ verify(this.claimService).chat(argThat(matcher));
+ verifyNoMoreInteractions(this.claimService);
+ }
+
+ private WebSocketClientConnection connectClient() {
+ var connection = this.connector
+ .baseUri(this.claimChatBotRootUri)
+ .connectAndAwait();
+
+ waitForClientToStart();
+
+ return connection;
+ }
+
+ private static void waitForClientToStart() {
+ await()
+ .atMost(Duration.ofMinutes(5))
+ .until(() -> "CONNECT".equals(ClientEndpoint.MESSAGES.poll()));
+ }
+
+ @WebSocketClient(path = "/ws/query", clientId = "c1")
+ static class ClientEndpoint {
+ private final Logger logger = Logger.getLogger(ClientEndpoint.class);
+ static final BlockingQueue MESSAGES = new LinkedBlockingDeque<>();
+
+ @OnOpen
+ void open(WebSocketClientConnection connection) {
+ this.logger.infof("[CLIENT] Opening endpoint %s", connection.id());
+ MESSAGES.offer("CONNECT");
+ }
+
+ @OnTextMessage
+ void textMessage(ClaimBotQueryResponse message) {
+ this.logger.infof("[CLIENT] Got message: %s", message.token());
+ MESSAGES.offer(message.token());
+ }
+
+ @OnError
+ void error(Throwable error) {
+ this.logger.errorf(error, "[CLIENT] Encountered an error");
+ }
+
+ @OnClose
+ void close(CloseReason closeReason, WebSocketClientConnection connection) {
+ this.logger.infof("[CLIENT] Closing endpoint %s: %s: %s", connection.id(), closeReason.getCode(), closeReason.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/org/parasol/ui/ClaimsDetailPageTests.java b/app/src/test/java/org/parasol/ui/ClaimsDetailPageTests.java
new file mode 100644
index 00000000..ab88c127
--- /dev/null
+++ b/app/src/test/java/org/parasol/ui/ClaimsDetailPageTests.java
@@ -0,0 +1,109 @@
+package org.parasol.ui;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import jakarta.ws.rs.core.Response.Status;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.Test;
+import org.parasol.model.Claim;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+
+import com.microsoft.playwright.BrowserContext;
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Response;
+import com.microsoft.playwright.assertions.PlaywrightAssertions;
+import com.microsoft.playwright.options.LoadState;
+import io.quarkiverse.playwright.InjectPlaywright;
+import io.quarkiverse.playwright.WithPlaywright;
+import io.quarkiverse.quinoa.testing.QuinoaTestProfiles;
+
+@QuarkusTest
+@TestProfile(QuinoaTestProfiles.Enable.class)
+@WithPlaywright
+public class ClaimsDetailPageTests {
+ @InjectPlaywright
+ BrowserContext context;
+
+ @ConfigProperty(name = "quarkus.http.test-port")
+ int quarkusPort;
+
+ @Test
+ void pageLoads() {
+ var claim = Claim.findAll().firstResultOptional().orElseThrow(() -> new IllegalArgumentException("Can not find a claim in the database to use for tests"));
+ var page = loadPage(claim);
+
+ PlaywrightAssertions.assertThat(page)
+ .hasTitle("Claim Detail");
+
+ PlaywrightAssertions.assertThat(page.getByText(claim.claimNumber))
+ .isVisible();
+
+ PlaywrightAssertions.assertThat(page.getByText(claim.summary))
+ .isVisible();
+
+ PlaywrightAssertions.assertThat(page.getByText(claim.sentiment))
+ .isVisible();
+
+ var openChatButton = page.getByLabel("OpenChat");
+ PlaywrightAssertions.assertThat(openChatButton)
+ .isVisible();
+
+ openChatButton.click();
+
+ PlaywrightAssertions.assertThat(page.getByText("Hi! I am Parasol Assistant. How can I help you today?"))
+ .isVisible();
+
+ assertThat(page.locator(".chat-answer-text").count())
+ .isOne();
+
+ var askMeAnythingField = page.getByPlaceholder("Ask me anything...");
+ PlaywrightAssertions.assertThat(askMeAnythingField)
+ .isVisible();
+ askMeAnythingField.fill("Should I approve this claim?");
+
+ var sendQueryButton = page.getByLabel("SendQuery");
+ PlaywrightAssertions.assertThat(sendQueryButton)
+ .isVisible();
+ sendQueryButton.click();
+
+ // Wait for the answer text to have at least one piece of text in the answer
+ await()
+ .atMost(Duration.ofSeconds(30))
+ .until(() -> getChatResponseText(page).isPresent());
+
+ assertThat(getChatResponseText(page))
+ .isNotNull()
+ .isPresent();
+ }
+
+ private Optional getChatResponseText(Page page) {
+ return page.locator(".chat-answer-text").all().stream()
+ .map(Locator::textContent)
+ .map(String::trim)
+ .filter(answer -> !"Hi! I am Parasol Assistant. How can I help you today?".equals(answer))
+ .findFirst()
+ .filter(s -> !s.isEmpty());
+ }
+
+ private Page loadPage(Claim claim) {
+ var page = this.context.newPage();
+ var response = page.navigate("http://localhost:%d/ClaimDetail/%d".formatted(this.quarkusPort, claim.id));
+
+ assertThat(response)
+ .isNotNull()
+ .extracting(Response::status)
+ .isEqualTo(Status.OK.getStatusCode());
+
+ page.waitForLoadState(LoadState.NETWORKIDLE);
+
+ return page;
+ }
+}
diff --git a/app/src/test/java/org/parasol/ui/ClaimsListPageTests.java b/app/src/test/java/org/parasol/ui/ClaimsListPageTests.java
new file mode 100644
index 00000000..ec292992
--- /dev/null
+++ b/app/src/test/java/org/parasol/ui/ClaimsListPageTests.java
@@ -0,0 +1,145 @@
+package org.parasol.ui;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import jakarta.ws.rs.core.Response.Status;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+
+import com.microsoft.playwright.BrowserContext;
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Response;
+import com.microsoft.playwright.assertions.PlaywrightAssertions;
+import com.microsoft.playwright.options.AriaRole;
+import com.microsoft.playwright.options.LoadState;
+import io.quarkiverse.playwright.InjectPlaywright;
+import io.quarkiverse.playwright.WithPlaywright;
+import io.quarkiverse.quinoa.testing.QuinoaTestProfiles;
+
+@QuarkusTest
+@TestProfile(QuinoaTestProfiles.Enable.class)
+@WithPlaywright
+public class ClaimsListPageTests {
+ private static final int NB_CLAIMS = 6;
+
+ @InjectPlaywright
+ BrowserContext context;
+
+ @ConfigProperty(name = "quarkus.http.test-port")
+ int quarkusPort;
+
+ @Test
+ void pageLoads() {
+ var page = loadPage();
+
+ PlaywrightAssertions.assertThat(page)
+ .hasTitle("Claims List");
+ }
+
+ @Test
+ void correctTable() {
+ var table = getAndVerifyClaimsTable(NB_CLAIMS);
+ var tableColumns = table.getByRole(AriaRole.COLUMNHEADER).all();
+
+ assertThat(tableColumns)
+ .isNotNull()
+ .hasSize(5)
+ .extracting(Locator::textContent)
+ .containsExactly(
+"Claim Number",
+ "Category",
+ "Client Name",
+ "Policy Number",
+ "Status"
+ );
+
+ var rows = getTableBodyRows(table);
+ assertThat(rows)
+ .isNotNull()
+ .hasSize(NB_CLAIMS);
+
+ var firstRow = rows.get(0).getByRole(AriaRole.GRIDCELL).all();
+ assertThat(firstRow)
+ .isNotNull()
+ .hasSize(5);
+
+ assertThat(firstRow.get(0))
+ .isNotNull()
+ .extracting(
+ Locator::textContent,
+ l -> l.getByRole(AriaRole.LINK).getAttribute("href")
+ )
+ .containsExactly(
+ "CLM195501",
+ "/ClaimDetail/1".formatted(this.quarkusPort)
+ );
+
+ assertThat(firstRow.get(1))
+ .isNotNull()
+ .extracting(Locator::textContent)
+ .isEqualTo("Multiple vehicle");
+
+ assertThat(firstRow.get(2))
+ .isNotNull()
+ .extracting(Locator::textContent)
+ .isEqualTo("Marty McFly");
+
+ assertThat(firstRow.get(3))
+ .isNotNull()
+ .extracting(Locator::textContent)
+ .isEqualTo("AC-987654321");
+
+ assertThat(firstRow.get(4))
+ .isNotNull()
+ .extracting(Locator::textContent)
+ .isEqualTo("Processed");
+ }
+
+ private List getTableBodyRows(Locator table) {
+ // Rowgroup 1 is the header row
+ // Rowgroup 2 is the body rows
+ var rowGroups = table.getByRole(AriaRole.ROWGROUP).all();
+ assertThat(rowGroups)
+ .isNotNull()
+ .hasSize(2);
+
+ return rowGroups.get(1).getByRole(AriaRole.ROW).all();
+ }
+
+ private Locator getAndVerifyClaimsTable(Page page, int expectedNumRows) {
+ var table = page.getByRole(AriaRole.GRID);
+ assertThat(table).isNotNull();
+
+ var tableBodyRows = getTableBodyRows(table);
+ assertThat(tableBodyRows)
+ .isNotNull()
+ .hasSize(expectedNumRows);
+
+ return table;
+ }
+
+ private Locator getAndVerifyClaimsTable(int expectedNumRows) {
+ return getAndVerifyClaimsTable(loadPage(), expectedNumRows);
+ }
+
+ private Page loadPage() {
+ var page = this.context.newPage();
+ var response = page.navigate("http://localhost:%d/ClaimsList".formatted(this.quarkusPort));
+
+ assertThat(response)
+ .isNotNull()
+ .extracting(Response::status)
+ .isEqualTo(Status.OK.getStatusCode());
+
+ page.waitForLoadState(LoadState.NETWORKIDLE);
+
+ return page;
+ }
+}