Skip to content

Commit

Permalink
Introduce htmx tests with Playwright
Browse files Browse the repository at this point in the history
Co-authored-by: Stéphane Épardaud <[email protected]>
  • Loading branch information
ia3andy and FroMage committed Jan 18, 2024
1 parent 639d5a8 commit 5a3fc43
Show file tree
Hide file tree
Showing 19 changed files with 404 additions and 10 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ jobs:
if: startsWith(matrix.os, 'windows')

- uses: actions/checkout@v3
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
java-version: 17
cache: 'maven'

- name: Build with Maven
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ jobs:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}

- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
java-version: 17
cache: 'maven'
server-id: ossrh
server-username: MAVEN_USERNAME
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/includes/attributes.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
:quarkus-version: 3.6.4
:quarkus-version: 3.6.5
:quarkus-renarde-version: 3.0.7
:maven-version: 3.8.1+

Expand Down
6 changes: 6 additions & 0 deletions integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@
<artifactId>quarkus-renarde-backoffice</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.playwright</groupId>
<artifactId>quarkus-playwright</artifactId>
<version>0.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkiverse.renarde</groupId>
<artifactId>quarkus-renarde-oidc-tests</artifactId>
Expand Down
6 changes: 6 additions & 0 deletions integration-tests/src/main/java/rest/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ static class Templates {
public static native TemplateInstance test(User user);

public static native TemplateInstance csrf();

public static native TemplateInstance gravatar();
}

@POST
Expand All @@ -68,6 +70,10 @@ public TemplateInstance index() {
return Templates.index();
}

public TemplateInstance gravatar() {
return Templates.gravatar();
}

@Blocking
public TemplateInstance oidcWelcome() {
return Templates.oidcWelcome();
Expand Down
108 changes: 108 additions & 0 deletions integration-tests/src/main/java/rest/HtmxApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package rest;

import java.util.Collection;

import jakarta.inject.Inject;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;

import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;

import io.quarkiverse.renarde.htmx.HxController;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import service.ContactService;

public class HtmxApp extends HxController {

@Inject
ContactService contactService;

@CheckedTemplate
static class Templates {

public static native TemplateInstance index(Collection<ContactService.Contact> contacts);

public static native TemplateInstance index$list(Collection<ContactService.Contact> contacts);

}

@CheckedTemplate(basePath = "HtmxApp/partials")
static class Partials {

public static native TemplateInstance view(ContactService.Contact contact);

public static native TemplateInstance edit(ContactService.Contact contact);

}

@Path("")
public TemplateInstance index() {
if (isHxRequest()) {
return Templates.index$list(contactService.contacts().values());
}
return Templates.index(contactService.contacts().values());
}

public TemplateInstance view(@RestPath int id) {
if (!contactService.contacts().containsKey(id)) {
throw new IllegalArgumentException("Invalid id: " + id);
}
return Partials.view(contactService.contacts().get(id));
}

public TemplateInstance edit(@RestPath int id) {
if (!contactService.contacts().containsKey(id)) {
throw new IllegalArgumentException("Invalid id: " + id);
}
return Partials.edit(contactService.contacts().get(id));
}

@PUT
public void save(@RestPath int id, @RestForm @NotBlank @Pattern(regexp = "[A-Z][a-z]+") String firstName,
@RestForm @NotBlank @Pattern(regexp = "[A-Z]+") String lastName,
@RestForm @Email String email) {
if (!contactService.contacts().containsKey(id)) {
throw new IllegalArgumentException("Invalid id: " + id);
}

if (validationFailed()) {
edit(id);
}

final ContactService.Contact contact = contactService.contacts().get(id);
contact.firstName = firstName;
contact.lastName = lastName;
contact.email = email;
view(id);
}

@PUT
public void lock(@RestPath int id) {
if (!contactService.contacts().containsKey(id)) {
throw new IllegalArgumentException("Invalid id: " + id);
}
final ContactService.Contact contact = contactService.contacts().get(id);
contact.locked = !contact.locked;
if (contact.locked) {
view(id);
} else {
edit(id);
}
}

@DELETE
public void delete(@RestPath int id) {
if (!contactService.contacts().containsKey(id)) {
throw new IllegalArgumentException("Invalid id: " + id);
}
contactService.contacts().remove(id);
hx(HxResponseHeader.TRIGGER, "refreshList");
}

}
51 changes: 51 additions & 0 deletions integration-tests/src/main/java/service/ContactService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package service;

import java.util.HashMap;
import java.util.Map;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class ContactService {
private static final Contact JOE = new Contact(6, "Joe", "BLOW", "[email protected]");
private static final Contact FOO = new Contact(7, "Foo", "BLOW", "[email protected]");
private static final Contact BAR = new Contact(10, "Bar", "AWESOME", "[email protected]");

private final Map<Integer, Contact> contacts = new HashMap<>();

public ContactService() {
reset();
}

public Map<Integer, Contact> contacts() {
return contacts;
}

public void reset() {
contacts.clear();
contacts.put(JOE.id, JOE.clone());
contacts.put(FOO.id, FOO.clone());
contacts.put(BAR.id, BAR.clone());
}

public static class Contact {
public final int id;
public String firstName;
public String lastName;
public String email;

public boolean locked = false;

public Contact(int id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}

@Override
public Contact clone() {
return new Contact(id, firstName, lastName, email);
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{#gravatar "[email protected]" size="200" alt="Gravatar" class="foo bar" aria-label="my gravatar" /}
{#gravatar "[email protected]" /}
32 changes: 32 additions & 0 deletions integration-tests/src/main/resources/templates/HtmxApp/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Htmx Powered</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<script src="/htmx.js"></script>
<link rel="stylesheet" media="screen" href="/webjars/bootstrap/css/bootstrap.min.css">
<style>
input:read-only {
background-color: #e9ecef;
}
</style>
</head>
<body hx-headers='{"{inject:csrf.headerName}":"{inject:csrf.token}"}'>
<main class="container">

<h1>Htmx Powered</h1>
<div hx-get="{uri:HtmxApp.index}" hx-trigger="refreshList from:body">
{#fragment id="list"}
{#for contact in contacts}
<div class="row mb-4">
{#include HtmxApp/partials/view /}
</div>
{/for}
{/fragment}
</div>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="col" hx-target='this' hx-swap="outerHTML" aria-label="Editing {contact.firstName} {contact.lastName}">
{#form hx-put=uri:HtmxApp.save(contact.id) style="display: contents;"}
{#formElement name="firstName" label="First Name"}
{#input name="firstName" value=contact.firstName readonly=contact.locked/}
{/formElement}
{#formElement name="lastName" label="Last Name"}
{#input name="lastName" value=contact.lastName readonly=contact.locked/}
{/formElement}
{#formElement name="email" label="Email Address"}
{#input name="email" type="email" value=contact.email readonly=contact.locked/}
{/formElement}

{#if !contact.locked}
<button class="btn btn-primary" aria-label="Save">Save</button>
{/if}
<button class="btn btn-warning" hx-get="{uri:HtmxApp.view(contact.id)}" aria-label="Cancel">Cancel</button>
{/form}
{#if !contact.locked}
<button class="btn btn-danger" hx-delete="{uri:HtmxApp.delete(contact.id)}" aria-label="Delete">Delete</button>
{/if}
<button class="btn btn-secondary" hx-put="{uri:HtmxApp.lock(contact.id)}" aria-label="{contact.locked ? 'Unlock' : 'Lock'}">{contact.locked ? 'Unlock' : 'Lock'}</button>

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div class="col" hx-target="this" hx-swap="outerHTML" aria-label="Viewing {contact.firstName} {contact.lastName}">
<div aria-label="Details">
<div><label>First Name</label>: {contact.firstName}</div>
<div><label>Last Name</label>: {contact.lastName}</div>
<div><label>Email</label>: {contact.email}</div>
</div>
<button hx-get="{uri:HtmxApp.edit(contact.id)}" class="btn btn-primary" aria-label="Edit">
Click To Edit
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="mb-3 form-group">
<label class="form-label" for="{name}">{label}</label>
{nested-content}
{#ifError name}
<span class="invalid-feedback" aria-label="Error for {name}">{#error name/}</span>
{/ifError}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input type="{type ?: 'text'}" class="{class ?: 'form-control'} {#ifError name}is-invalid{/ifError}" {#if readonly}readonly ‹{/if}value="{inject:flash.get(name) ?: value}" {_args.skip('class', 'type', 'readonly').asHtmlAttributes.safe}/>
Loading

0 comments on commit 5a3fc43

Please sign in to comment.