21834ec9a0
CI / build-and-test (pull_request) Successful in 2m24s
Adds a non-chart dashboard widget: a large value with its label, an optional signed delta versus the previous period, and an optional sparkline. Four tiles now sit above the charts, 3x1 each. - KpiTile draws no surface of its own and fills the grid item's, styled from the --dialect-* token layer; the delta's up/down colors are new tokens rather than literals, so they follow the light/dark toggle. - Value and delta share one row: stacking them costs a line the tile does not have at its default height of one grid cell. - SparklineChart is a chrome-free line chart built on DialectTheme, so it shares the palette and font with the real charts. - ApexCharts' sparkline.enabled drops the axes on the initial render but updateOptions (how the theme toggle recolors a chart) brings the y-axis labels back; they are hidden in CSS, which reaches the chart SVG since it renders into light DOM. The registry registration the issue asks for is left out: #21 is blocked, so there is no widget picker to register with yet. The tiles are wired into DashboardView the same way the chart widgets are, and their values come from the translation bundle until the data service (#20) lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
161 lines
5.8 KiB
Java
161 lines
5.8 KiB
Java
package com.example.views;
|
|
|
|
import com.example.Application;
|
|
import com.example.components.BarChart;
|
|
import com.example.components.GridStackItem;
|
|
import com.example.components.GridStackLayout;
|
|
import com.example.components.KpiTile;
|
|
import com.example.components.LineChart;
|
|
import com.example.components.PieChart;
|
|
import com.vaadin.browserless.SpringBrowserlessTest;
|
|
import com.vaadin.browserless.ViewPackages;
|
|
import com.vaadin.browserless.internal.ElementUtilsKt;
|
|
import com.vaadin.flow.component.button.Button;
|
|
import com.vaadin.flow.component.html.Div;
|
|
import com.vaadin.flow.dom.DomEvent;
|
|
import com.vaadin.flow.internal.JacksonUtils;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
|
|
import java.util.List;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
@SpringBootTest(classes = Application.class)
|
|
@ViewPackages(classes = DashboardView.class)
|
|
class DashboardViewTest extends SpringBrowserlessTest {
|
|
|
|
/** Four KPI tiles plus three charts and the hint card. */
|
|
private static final int DEFAULT_WIDGETS = 8;
|
|
|
|
@Test
|
|
void view_rendersGridWithDefaultWidgets() {
|
|
navigate(DashboardView.class);
|
|
|
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
|
assertNotNull(grid);
|
|
|
|
List<GridStackItem.Position> layout = grid.getLayout();
|
|
assertEquals(DEFAULT_WIDGETS, layout.size(), "expected the default widgets");
|
|
assertTrue(layout.stream().allMatch(p -> p.w() > 0 && p.h() > 0));
|
|
}
|
|
|
|
@Test
|
|
void kpiTiles_renderValueAndDelta() {
|
|
navigate(DashboardView.class);
|
|
|
|
List<KpiTile> tiles = $view(KpiTile.class).all();
|
|
assertEquals(4, tiles.size(), "expected the four KPI tiles");
|
|
|
|
KpiTile revenue = tiles.getFirst();
|
|
assertEquals(translate("kpi.revenueTotal"), revenue.getLabel());
|
|
assertEquals(translate("kpi.revenueTotal.value"), revenue.getValue());
|
|
// +12.4 — sign is explicit, decimal separator is the locale's.
|
|
assertTrue(revenue.getDeltaText().matches("\\+12[.,]4 %"),
|
|
"unexpected delta text: " + revenue.getDeltaText());
|
|
|
|
KpiTile orders = tiles.get(1);
|
|
assertTrue(orders.getDeltaText().startsWith("-"),
|
|
"a negative delta keeps its minus sign: " + orders.getDeltaText());
|
|
assertNotNull(orders.getSparkline(), "each tile carries a sparkline");
|
|
}
|
|
|
|
@Test
|
|
void kpiTiles_areOneRowHigh() {
|
|
navigate(DashboardView.class);
|
|
|
|
List<GridStackItem.Position> kpis = $view(GridStackLayout.class).first().getLayout()
|
|
.stream().filter(p -> p.id().startsWith("kpi-")).toList();
|
|
|
|
assertEquals(4, kpis.size());
|
|
assertTrue(kpis.stream().allMatch(p -> p.w() == 3 && p.h() == 1),
|
|
"KPI tiles default to a quarter row, one cell high");
|
|
}
|
|
|
|
@Test
|
|
void view_rendersTheThreeCharts() {
|
|
navigate(DashboardView.class);
|
|
|
|
assertNotNull($view(LineChart.class).first());
|
|
assertNotNull($view(BarChart.class).first());
|
|
assertNotNull($view(PieChart.class).first());
|
|
}
|
|
|
|
@Test
|
|
void addAndCloseWidget_changesItemCount() {
|
|
navigate(DashboardView.class);
|
|
|
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
|
int initial = grid.getLayout().size();
|
|
|
|
button("gridstack.addWidget").click();
|
|
assertEquals(initial + 1, grid.getLayout().size());
|
|
|
|
clickCloseButton($view(GridStackItem.class).last());
|
|
assertEquals(initial, grid.getLayout().size());
|
|
}
|
|
|
|
@Test
|
|
void closeButton_removesTheClickedWidgetOnly() {
|
|
navigate(DashboardView.class);
|
|
|
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
|
GridStackItem first = $view(GridStackItem.class).first();
|
|
String closedId = first.getItemId();
|
|
|
|
clickCloseButton(first);
|
|
|
|
List<GridStackItem.Position> layout = grid.getLayout();
|
|
assertEquals(DEFAULT_WIDGETS - 1, layout.size());
|
|
assertFalse(layout.stream().anyMatch(p -> closedId.equals(p.id())),
|
|
"the closed widget must be gone from the layout");
|
|
}
|
|
|
|
@Test
|
|
void closeListener_firesOnceItemIsDetached() {
|
|
navigate(DashboardView.class);
|
|
|
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
|
GridStackItem item = $view(GridStackItem.class).first();
|
|
int[] fired = {0};
|
|
item.addCloseListener(e -> fired[0]++);
|
|
|
|
item.close();
|
|
|
|
assertEquals(1, fired[0]);
|
|
assertEquals(DEFAULT_WIDGETS - 1, grid.getLayout().size());
|
|
}
|
|
|
|
@Test
|
|
void nonClosableItem_hasNoCloseButton() {
|
|
navigate(DashboardView.class);
|
|
|
|
GridStackItem item = $view(GridStackItem.class).first();
|
|
assertTrue(item.isClosable(), "items are closable by default");
|
|
|
|
item.setClosable(false);
|
|
assertFalse(item.isClosable());
|
|
assertFalse(item.getChildren().anyMatch(child -> child == item.getCloseButton()));
|
|
}
|
|
|
|
/** Fires the DOM click the close button listens for, rather than calling
|
|
* {@code close()} directly, so the button's own wiring is covered too. */
|
|
private void clickCloseButton(GridStackItem item) {
|
|
Div closeButton = item.getCloseButton();
|
|
ElementUtilsKt._fireDomEvent(closeButton.getElement(),
|
|
new DomEvent(closeButton.getElement(), "click", JacksonUtils.createObjectNode()));
|
|
}
|
|
|
|
private Button button(String translationKey) {
|
|
return $view(Button.class).withText(translate(translationKey)).first();
|
|
}
|
|
|
|
private String translate(String translationKey) {
|
|
return getCurrentView().getElement().getComponent()
|
|
.orElseThrow().getTranslation(translationKey);
|
|
}
|
|
}
|