feat(sprint-15 web 2026-04-25): Live Activity Feed page (transparency operations)
Page consommant /api/audit-trail/recent avec auto-refresh PrimeFaces toutes les 10s. Transparency opérationnelle UnionFlow — chaque utilisateur voit selon son scope. REST client AuditTrailRestClient - Nouvelle méthode recent(scope, orgId, userId, limit) Bean LiveFeedBean (@ViewScoped) - Polling pilote externe via p:poll interval=10 - 3 scopes : SELF (défaut, n'importe quel rôle), ORG (admin/officer), ALL (compliance/contrôleur) - Helpers : couleurAction (8 mappings), couleurSod (3 cas), tempsRelatif (s/m/h/j/futur) - Limit clamp [1, 500] - Compteur de refresh visible dans l'UI (debug) Page /pages/secure/conformite/live-feed.xhtml - Panel scope avec selectOneMenu + p:ajax change → rafraîchir - Conditional inputs : orgId si scope=ORG, userId si scope=SELF - p:poll interval=10 listener=rafraichir autoStart=true - DataTable opérations : index, "il y a Xs", acteur+rôle, action coloré, entité, description, SoD tag - Tag refresh counter (visible feedback) Centralisation - ViewPaths.CONFORMITE_LIVE_FEED + getter ViewPathsBean - menu.xhtml : entrée Live Feed sous sous-menu Conformité (icon pi-bolt) Tests (13/13 verts) - couleurAction × 4 (danger/success/info/autres) - couleurSod - tempsRelatif × 6 (null, secondes, minutes, heures, jours, futur) - setLimit clamp × 4 - defaults
This commit is contained in:
@@ -115,6 +115,7 @@ public final class ViewPaths {
|
|||||||
public static final String CONFORMITE_RAPPORTS_TRIMESTRIELS = "/pages/secure/conformite/rapports-trimestriels";
|
public static final String CONFORMITE_RAPPORTS_TRIMESTRIELS = "/pages/secure/conformite/rapports-trimestriels";
|
||||||
public static final String CONFORMITE_BENEFICIAIRES_EFFECTIFS = "/pages/secure/conformite/beneficiaires-effectifs";
|
public static final String CONFORMITE_BENEFICIAIRES_EFFECTIFS = "/pages/secure/conformite/beneficiaires-effectifs";
|
||||||
public static final String CONFORMITE_AUDIT_TRAIL = "/pages/secure/conformite/audit-trail";
|
public static final String CONFORMITE_AUDIT_TRAIL = "/pages/secure/conformite/audit-trail";
|
||||||
|
public static final String CONFORMITE_LIVE_FEED = "/pages/secure/conformite/live-feed";
|
||||||
|
|
||||||
// ─── Admin technique ─────────────────────────────────────────────────────
|
// ─── Admin technique ─────────────────────────────────────────────────────
|
||||||
public static final String ADMIN_PISPI_READINESS = "/pages/secure/admin/pispi-readiness";
|
public static final String ADMIN_PISPI_READINESS = "/pages/secure/admin/pispi-readiness";
|
||||||
|
|||||||
@@ -50,4 +50,13 @@ public interface AuditTrailRestClient {
|
|||||||
@PathParam("orgId") UUID orgId,
|
@PathParam("orgId") UUID orgId,
|
||||||
@QueryParam("from") String from,
|
@QueryParam("from") String from,
|
||||||
@QueryParam("to") String to);
|
@QueryParam("to") String to);
|
||||||
|
|
||||||
|
/** Live Activity Feed (Sprint 15) — N opérations les plus récentes selon scope. */
|
||||||
|
@GET
|
||||||
|
@Path("/recent")
|
||||||
|
List<AuditTrailOperationResponse> recent(
|
||||||
|
@QueryParam("scope") String scope,
|
||||||
|
@QueryParam("orgId") UUID orgId,
|
||||||
|
@QueryParam("userId") UUID userId,
|
||||||
|
@QueryParam("limit") int limit);
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/main/java/dev/lions/unionflow/client/view/LiveFeedBean.java
Normal file
104
src/main/java/dev/lions/unionflow/client/view/LiveFeedBean.java
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package dev.lions.unionflow.client.view;
|
||||||
|
|
||||||
|
import dev.lions.unionflow.client.service.AuditTrailRestClient;
|
||||||
|
import dev.lions.unionflow.server.api.dto.audit.response.AuditTrailOperationResponse;
|
||||||
|
import jakarta.faces.application.FacesMessage;
|
||||||
|
import jakarta.faces.context.FacesContext;
|
||||||
|
import jakarta.faces.view.ViewScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bean Live Activity Feed (Sprint 15 — transparency opérationnelle).
|
||||||
|
*
|
||||||
|
* <p>Polling 10s sur {@code /api/audit-trail/recent} avec choix du scope :
|
||||||
|
* <ul>
|
||||||
|
* <li>SELF — opérations de l'utilisateur courant (défaut)</li>
|
||||||
|
* <li>ORG — opérations de l'organisation active</li>
|
||||||
|
* <li>ALL — toutes opérations (compliance/contrôleur uniquement)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>L'auto-refresh est piloté côté UI via {@code <p:poll>} qui appelle {@link #rafraichir()}.
|
||||||
|
*/
|
||||||
|
@Named
|
||||||
|
@ViewScoped
|
||||||
|
public class LiveFeedBean implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final Logger LOG = Logger.getLogger(LiveFeedBean.class);
|
||||||
|
|
||||||
|
@Inject @RestClient AuditTrailRestClient client;
|
||||||
|
|
||||||
|
/** SELF (défaut, n'importe quel rôle), ORG (admin/officer), ALL (compliance/contrôleur). */
|
||||||
|
private String scope = "SELF";
|
||||||
|
private UUID orgId;
|
||||||
|
private UUID userId;
|
||||||
|
private int limit = 50;
|
||||||
|
|
||||||
|
private List<AuditTrailOperationResponse> operations = Collections.emptyList();
|
||||||
|
private long compteur;
|
||||||
|
private String erreur;
|
||||||
|
|
||||||
|
public void rafraichir() {
|
||||||
|
erreur = null;
|
||||||
|
try {
|
||||||
|
operations = client.recent(scope, orgId, userId, limit);
|
||||||
|
compteur++;
|
||||||
|
LOG.debugf("LiveFeed (%s) refresh #%d : %d ops", scope, compteur,
|
||||||
|
operations == null ? 0 : operations.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warnf("LiveFeed refresh échoué : %s", e.getMessage());
|
||||||
|
erreur = "Échec rafraîchissement : " + e.getMessage();
|
||||||
|
FacesContext.getCurrentInstance().addMessage(null,
|
||||||
|
new FacesMessage(FacesMessage.SEVERITY_WARN, "Erreur LiveFeed", erreur));
|
||||||
|
operations = Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCouleurAction(String actionType) {
|
||||||
|
if (actionType == null) return "secondary";
|
||||||
|
return switch (actionType) {
|
||||||
|
case "DELETE", "PAYMENT_FAILED" -> "danger";
|
||||||
|
case "VALIDATE", "PAYMENT_CONFIRMED", "AID_REQUEST_APPROVED" -> "success";
|
||||||
|
case "UPDATE", "PAYMENT_INITIATED", "BUDGET_APPROVED" -> "info";
|
||||||
|
case "CREATE" -> "primary";
|
||||||
|
case "EXPORT" -> "warning";
|
||||||
|
default -> "secondary";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCouleurSod(Boolean sodCheckPassed) {
|
||||||
|
if (sodCheckPassed == null) return "secondary";
|
||||||
|
return sodCheckPassed ? "success" : "danger";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pour l'affichage relatif "il y a Xs". */
|
||||||
|
public String tempsRelatif(java.time.LocalDateTime dt) {
|
||||||
|
if (dt == null) return "—";
|
||||||
|
long secondes = java.time.Duration.between(dt, java.time.LocalDateTime.now()).getSeconds();
|
||||||
|
if (secondes < 0) return "à l'instant";
|
||||||
|
if (secondes < 60) return "il y a " + secondes + "s";
|
||||||
|
if (secondes < 3600) return "il y a " + (secondes / 60) + "m";
|
||||||
|
if (secondes < 86400) return "il y a " + (secondes / 3600) + "h";
|
||||||
|
return "il y a " + (secondes / 86400) + "j";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters / setters
|
||||||
|
public String getScope() { return scope; }
|
||||||
|
public void setScope(String scope) { this.scope = scope; }
|
||||||
|
public UUID getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(UUID orgId) { this.orgId = orgId; }
|
||||||
|
public UUID getUserId() { return userId; }
|
||||||
|
public void setUserId(UUID userId) { this.userId = userId; }
|
||||||
|
public int getLimit() { return limit; }
|
||||||
|
public void setLimit(int limit) { this.limit = Math.max(1, Math.min(limit, 500)); }
|
||||||
|
public List<AuditTrailOperationResponse> getOperations() { return operations; }
|
||||||
|
public long getCompteur() { return compteur; }
|
||||||
|
public String getErreur() { return erreur; }
|
||||||
|
}
|
||||||
@@ -110,6 +110,7 @@ public class ViewPathsBean implements Serializable {
|
|||||||
public String getConformiteRapportsTrimestriels() { return ViewPaths.CONFORMITE_RAPPORTS_TRIMESTRIELS; }
|
public String getConformiteRapportsTrimestriels() { return ViewPaths.CONFORMITE_RAPPORTS_TRIMESTRIELS; }
|
||||||
public String getConformiteBeneficiairesEffectifs() { return ViewPaths.CONFORMITE_BENEFICIAIRES_EFFECTIFS; }
|
public String getConformiteBeneficiairesEffectifs() { return ViewPaths.CONFORMITE_BENEFICIAIRES_EFFECTIFS; }
|
||||||
public String getConformiteAuditTrail() { return ViewPaths.CONFORMITE_AUDIT_TRAIL; }
|
public String getConformiteAuditTrail() { return ViewPaths.CONFORMITE_AUDIT_TRAIL; }
|
||||||
|
public String getConformiteLiveFeed() { return ViewPaths.CONFORMITE_LIVE_FEED; }
|
||||||
|
|
||||||
// ─── Admin technique ─────────────────────────────────────────────────────
|
// ─── Admin technique ─────────────────────────────────────────────────────
|
||||||
public String getAdminPispiReadiness() { return ViewPaths.ADMIN_PISPI_READINESS; }
|
public String getAdminPispiReadiness() { return ViewPaths.ADMIN_PISPI_READINESS; }
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:h="http://xmlns.jcp.org/jsf/html"
|
||||||
|
xmlns:f="http://xmlns.jcp.org/jsf/core"
|
||||||
|
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
|
||||||
|
xmlns:p="http://primefaces.org/ui"
|
||||||
|
template="/templates/main-template.xhtml">
|
||||||
|
|
||||||
|
<ui:define name="title">UnionFlow - Live Activity Feed</ui:define>
|
||||||
|
|
||||||
|
<ui:define name="content">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-900 font-medium text-3xl m-0">
|
||||||
|
<i class="pi pi-bolt mr-2 text-yellow-500" />
|
||||||
|
Live Activity Feed
|
||||||
|
</h2>
|
||||||
|
<p class="text-600 mt-1 mb-0">
|
||||||
|
Transparency opérationnelle — les opérations sensibles défilent en temps réel.
|
||||||
|
Refresh auto toutes les 10 secondes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p:tag value="Refresh ##{liveFeedBean.compteur}" severity="info"
|
||||||
|
icon="pi pi-refresh" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h:form id="form-livefeed">
|
||||||
|
<p:messages closable="true" />
|
||||||
|
|
||||||
|
<p:panel header="Scope" styleClass="mb-3">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<p:outputLabel for="scope" value="Périmètre" />
|
||||||
|
<p:selectOneMenu id="scope" value="#{liveFeedBean.scope}"
|
||||||
|
styleClass="w-full">
|
||||||
|
<f:selectItem itemValue="SELF" itemLabel="Mes opérations" />
|
||||||
|
<f:selectItem itemValue="ORG" itemLabel="Mon organisation" />
|
||||||
|
<f:selectItem itemValue="ALL" itemLabel="Toutes (compliance/contrôleur)" />
|
||||||
|
<p:ajax event="change" listener="#{liveFeedBean.rafraichir}"
|
||||||
|
update="form-livefeed" />
|
||||||
|
</p:selectOneMenu>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-4" jsf:rendered="#{liveFeedBean.scope eq 'ORG'}">
|
||||||
|
<p:outputLabel for="orgId" value="Organisation (UUID)" />
|
||||||
|
<p:inputText id="orgId" value="#{liveFeedBean.orgId}"
|
||||||
|
styleClass="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-4" jsf:rendered="#{liveFeedBean.scope eq 'SELF'}">
|
||||||
|
<p:outputLabel for="userId" value="Utilisateur (UUID)" />
|
||||||
|
<p:inputText id="userId" value="#{liveFeedBean.userId}"
|
||||||
|
styleClass="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-2">
|
||||||
|
<p:outputLabel for="lim" value="Limite (1-500)" />
|
||||||
|
<p:inputNumber id="lim" value="#{liveFeedBean.limit}"
|
||||||
|
minValue="1" maxValue="500"
|
||||||
|
decimalPlaces="0" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-3 flex align-items-end">
|
||||||
|
<p:commandButton value="Rafraîchir"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
action="#{liveFeedBean.rafraichir}"
|
||||||
|
update="form-livefeed"
|
||||||
|
styleClass="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p:panel>
|
||||||
|
|
||||||
|
<!-- Auto-refresh toutes les 10 secondes (10000 ms) -->
|
||||||
|
<p:poll interval="10" listener="#{liveFeedBean.rafraichir}"
|
||||||
|
update="form-livefeed" autoStart="true" />
|
||||||
|
|
||||||
|
<p:dataTable value="#{liveFeedBean.operations}" var="o"
|
||||||
|
emptyMessage="Aucune opération récente"
|
||||||
|
rowIndexVar="idx">
|
||||||
|
<p:column headerText="#" style="width: 60px;">
|
||||||
|
<h:outputText value="#{idx + 1}" />
|
||||||
|
</p:column>
|
||||||
|
<p:column headerText="Quand">
|
||||||
|
<h:outputText value="#{liveFeedBean.tempsRelatif(o.operationAt)}"
|
||||||
|
styleClass="text-700 font-semibold" />
|
||||||
|
<br/>
|
||||||
|
<small class="text-500">#{o.operationAt}</small>
|
||||||
|
</p:column>
|
||||||
|
<p:column headerText="Acteur">
|
||||||
|
<h:outputText value="#{o.userEmail}" />
|
||||||
|
<br/>
|
||||||
|
<small class="text-500" jsf:rendered="#{o.roleActif != null}">
|
||||||
|
#{o.roleActif}
|
||||||
|
</small>
|
||||||
|
</p:column>
|
||||||
|
<p:column headerText="Action">
|
||||||
|
<p:tag value="#{o.actionType}"
|
||||||
|
severity="#{liveFeedBean.getCouleurAction(o.actionType)}" />
|
||||||
|
</p:column>
|
||||||
|
<p:column headerText="Entité">
|
||||||
|
<h:outputText value="#{o.entityType}" />
|
||||||
|
</p:column>
|
||||||
|
<p:column headerText="Description">
|
||||||
|
<h:outputText value="#{o.description}" />
|
||||||
|
</p:column>
|
||||||
|
<p:column headerText="SoD">
|
||||||
|
<p:tag value="#{o.sodCheckPassed ? 'OK' : 'VIOLATION'}"
|
||||||
|
severity="#{liveFeedBean.getCouleurSod(o.sodCheckPassed)}"
|
||||||
|
rendered="#{o.sodCheckPassed != null}" />
|
||||||
|
<h:outputText value="—" rendered="#{o.sodCheckPassed == null}" />
|
||||||
|
</p:column>
|
||||||
|
</p:dataTable>
|
||||||
|
</h:form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ui:define>
|
||||||
|
</ui:composition>
|
||||||
@@ -115,6 +115,7 @@
|
|||||||
<p:menuitem id="m_rapports_trimestriels" value="Rapports trimestriels" icon="pi pi-file-pdf" outcome="#{paths.conformiteRapportsTrimestriels}" rendered="#{menuBean.rapportsTrimestrielsVisible}" />
|
<p:menuitem id="m_rapports_trimestriels" value="Rapports trimestriels" icon="pi pi-file-pdf" outcome="#{paths.conformiteRapportsTrimestriels}" rendered="#{menuBean.rapportsTrimestrielsVisible}" />
|
||||||
<p:menuitem id="m_ubo" value="Bénéficiaires Effectifs" icon="pi pi-users" outcome="#{paths.conformiteBeneficiairesEffectifs}" rendered="#{menuBean.beneficiairesEffectifsVisible}" />
|
<p:menuitem id="m_ubo" value="Bénéficiaires Effectifs" icon="pi pi-users" outcome="#{paths.conformiteBeneficiairesEffectifs}" rendered="#{menuBean.beneficiairesEffectifsVisible}" />
|
||||||
<p:menuitem id="m_audit_trail" value="Audit Trail" icon="pi pi-history" outcome="#{paths.conformiteAuditTrail}" rendered="#{menuBean.auditTrailViewerVisible}" />
|
<p:menuitem id="m_audit_trail" value="Audit Trail" icon="pi pi-history" outcome="#{paths.conformiteAuditTrail}" rendered="#{menuBean.auditTrailViewerVisible}" />
|
||||||
|
<p:menuitem id="m_live_feed" value="Live Activity Feed" icon="pi pi-bolt" outcome="#{paths.conformiteLiveFeed}" />
|
||||||
<p:menuitem id="m_role_delegations" value="Délégations de rôles" icon="pi pi-share-alt" outcome="#{paths.adminRoleDelegations}" rendered="#{menuBean.roleDelegationsVisible}" />
|
<p:menuitem id="m_role_delegations" value="Délégations de rôles" icon="pi pi-share-alt" outcome="#{paths.adminRoleDelegations}" rendered="#{menuBean.roleDelegationsVisible}" />
|
||||||
<p:menuitem id="m_pispi_readiness" value="PI-SPI Readiness" icon="pi pi-cog" outcome="#{paths.adminPispiReadiness}" rendered="#{menuBean.pispiReadinessVisible}" />
|
<p:menuitem id="m_pispi_readiness" value="PI-SPI Readiness" icon="pi pi-cog" outcome="#{paths.adminPispiReadiness}" rendered="#{menuBean.pispiReadinessVisible}" />
|
||||||
</p:submenu>
|
</p:submenu>
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package dev.lions.unionflow.client.view;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class LiveFeedBeanTest {
|
||||||
|
|
||||||
|
private LiveFeedBean bean;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
bean = new LiveFeedBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── couleurAction (mêmes règles que AuditTrailViewerBean) ────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("couleurAction — DELETE/PAYMENT_FAILED → danger")
|
||||||
|
void couleurDanger() {
|
||||||
|
assertEquals("danger", bean.getCouleurAction("DELETE"));
|
||||||
|
assertEquals("danger", bean.getCouleurAction("PAYMENT_FAILED"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("couleurAction — VALIDATE/PAYMENT_CONFIRMED/AID_REQUEST_APPROVED → success")
|
||||||
|
void couleurSuccess() {
|
||||||
|
assertEquals("success", bean.getCouleurAction("VALIDATE"));
|
||||||
|
assertEquals("success", bean.getCouleurAction("PAYMENT_CONFIRMED"));
|
||||||
|
assertEquals("success", bean.getCouleurAction("AID_REQUEST_APPROVED"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("couleurAction — UPDATE/PAYMENT_INITIATED/BUDGET_APPROVED → info")
|
||||||
|
void couleurInfo() {
|
||||||
|
assertEquals("info", bean.getCouleurAction("UPDATE"));
|
||||||
|
assertEquals("info", bean.getCouleurAction("PAYMENT_INITIATED"));
|
||||||
|
assertEquals("info", bean.getCouleurAction("BUDGET_APPROVED"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("couleurAction — CREATE → primary, EXPORT → warning, autres → secondary")
|
||||||
|
void couleurAutres() {
|
||||||
|
assertEquals("primary", bean.getCouleurAction("CREATE"));
|
||||||
|
assertEquals("warning", bean.getCouleurAction("EXPORT"));
|
||||||
|
assertEquals("secondary", bean.getCouleurAction(null));
|
||||||
|
assertEquals("secondary", bean.getCouleurAction("INCONNU"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("couleurSod — true→success, false→danger, null→secondary")
|
||||||
|
void couleurSod() {
|
||||||
|
assertEquals("success", bean.getCouleurSod(true));
|
||||||
|
assertEquals("danger", bean.getCouleurSod(false));
|
||||||
|
assertEquals("secondary", bean.getCouleurSod(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tempsRelatif ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("tempsRelatif — null → '—'")
|
||||||
|
void tempsRelatifNull() {
|
||||||
|
assertEquals("—", bean.tempsRelatif(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("tempsRelatif — 30s ago → 'il y a 30s'")
|
||||||
|
void tempsRelatifSecondes() {
|
||||||
|
LocalDateTime past = LocalDateTime.now().minusSeconds(30);
|
||||||
|
assertTrue(bean.tempsRelatif(past).startsWith("il y a "));
|
||||||
|
assertTrue(bean.tempsRelatif(past).endsWith("s"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("tempsRelatif — 5min ago → 'il y a 5m'")
|
||||||
|
void tempsRelatifMinutes() {
|
||||||
|
LocalDateTime past = LocalDateTime.now().minusMinutes(5);
|
||||||
|
assertEquals("il y a 5m", bean.tempsRelatif(past));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("tempsRelatif — 2h ago → 'il y a 2h'")
|
||||||
|
void tempsRelatifHeures() {
|
||||||
|
LocalDateTime past = LocalDateTime.now().minusHours(2);
|
||||||
|
assertEquals("il y a 2h", bean.tempsRelatif(past));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("tempsRelatif — 3 jours ago → 'il y a 3j'")
|
||||||
|
void tempsRelatifJours() {
|
||||||
|
LocalDateTime past = LocalDateTime.now().minusDays(3);
|
||||||
|
assertEquals("il y a 3j", bean.tempsRelatif(past));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("tempsRelatif — futur → 'à l'instant'")
|
||||||
|
void tempsRelatifFutur() {
|
||||||
|
LocalDateTime futur = LocalDateTime.now().plusMinutes(1);
|
||||||
|
assertEquals("à l'instant", bean.tempsRelatif(futur));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Limit clamping ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("setLimit clamp [1, 500]")
|
||||||
|
void setLimitClamp() {
|
||||||
|
bean.setLimit(0);
|
||||||
|
assertEquals(1, bean.getLimit());
|
||||||
|
bean.setLimit(1000);
|
||||||
|
assertEquals(500, bean.getLimit());
|
||||||
|
bean.setLimit(50);
|
||||||
|
assertEquals(50, bean.getLimit());
|
||||||
|
bean.setLimit(-100);
|
||||||
|
assertEquals(1, bean.getLimit());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Defaults ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Defaults — scope=SELF, limit=50, compteur=0")
|
||||||
|
void defaults() {
|
||||||
|
assertEquals("SELF", bean.getScope());
|
||||||
|
assertEquals(50, bean.getLimit());
|
||||||
|
assertEquals(0, bean.getCompteur());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user