修复视觉验收夹具与运行态对比度

This commit is contained in:
张家训
2026-08-01 19:27:31 +08:00
parent 1290e3dc01
commit 086077beb6
10 changed files with 320 additions and 37 deletions
@@ -121,9 +121,20 @@ public class MainWindow {
menuButton.setFocusTraversable(true);
menuButton.setOnAction(event -> navigationRail.toggle());
Label brand = new Label("WorkUtils", IconFactory.create(AppIcon.APP, 20));
brand.setContentDisplay(ContentDisplay.LEFT);
brand.setFont(Theme.sectionTitle());
FontIcon brandIcon = IconFactory.create(AppIcon.APP, 18);
StackPane brandIconContainer = new StackPane(brandIcon);
brandIconContainer.setId("app-brand-icon-container");
brandIconContainer.getStyleClass().add("app-brand-icon");
brandIconContainer.setMinSize(32, 32);
brandIconContainer.setPrefSize(32, 32);
brandIconContainer.setMaxSize(32, 32);
brandIconContainer.setAccessibleText("WorkUtils 应用图标");
brandIconContainer.setFocusTraversable(false);
Label brandText = new Label("WorkUtils");
brandText.setFont(Theme.sectionTitle());
HBox brand = new HBox(Theme.S2, brandIconContainer, brandText);
brand.setAlignment(Pos.CENTER_LEFT);
toolbarChevron = IconFactory.create(AppIcon.CHEVRON_RIGHT, 14);
toolbarChevron.setVisible(false);
@@ -9,6 +9,7 @@ import javafx.application.Platform;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
@@ -54,7 +55,13 @@ public final class NavigationRail extends VBox {
buildBottomToggle();
Separator separator = new Separator();
separator.setId("navigation-rail-separator");
separator.getStyleClass().add(Theme.CSS_NAV_SEP);
separator.setPadding(Insets.EMPTY);
separator.setMinHeight(1);
separator.setPrefHeight(1);
separator.setMaxHeight(1);
separator.setFocusTraversable(false);
getChildren().addAll(itemBox, separator, collapseButton);
expanded.addListener((observable, oldValue, newValue) -> updateAppearance());
+39 -1
View File
@@ -30,7 +30,18 @@
}
.nav-separator {
-fx-background-color: -workutils-border;
-fx-padding: 0;
-fx-min-height: 1px;
-fx-pref-height: 1px;
-fx-max-height: 1px;
-fx-background-color: transparent;
}
.nav-separator .line {
-fx-border-color: -workutils-border transparent transparent transparent;
-fx-border-width: 1px 0 0 0;
-fx-border-insets: 0;
-fx-padding: 0;
}
/* ============ ToggleButton 导航 ============ */
@@ -99,6 +110,22 @@
-fx-max-height: 48px;
}
.app-brand-icon {
-fx-alignment: center;
-fx-background-color: -workutils-primary;
-fx-background-radius: 6px;
-fx-min-width: 32px;
-fx-pref-width: 32px;
-fx-max-width: 32px;
-fx-min-height: 32px;
-fx-pref-height: 32px;
-fx-max-height: 32px;
}
.app-brand-icon .app-icon {
-fx-icon-color: white;
}
/* ---- 内容区 ---- */
.content-area {
-fx-background-color: -workutils-bg;
@@ -153,6 +180,17 @@
-fx-background-color: derive(-workutils-primary, -12%);
}
.btn-primary:disabled:running {
-fx-opacity: 1;
-fx-background-color: -workutils-primary;
-fx-text-fill: white;
}
.btn-primary:disabled:running .loading-spinner {
-fx-progress-color: white;
-fx-opacity: 1;
}
.btn-danger {
-fx-background-color: -workutils-danger;
-fx-text-fill: white;
@@ -18,6 +18,7 @@ public final class FxTestSupport {
Thread.currentThread().interrupt();
throw new AssertionError(interrupted);
}
Platform.setImplicitExit(false);
}
public static <T> T fx(Callable<T> action) throws Exception {
@@ -0,0 +1,17 @@
package com.example.workutils.system.ui;
import javafx.application.Platform;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
class FxTestSupportTest {
@Test
void keepsToolkitAliveAcrossSequentialJavaFxTestClasses() throws Exception {
FxTestSupport.fx(() -> { });
assertFalse(Platform.isImplicitExit(),
"closing the last test Stage must not terminate the shared JavaFX toolkit");
}
}
@@ -1,9 +1,16 @@
package com.example.workutils.system.ui;
import com.example.workutils.system.ui.async.AsyncOperationController;
import com.example.workutils.system.ui.async.LoadingButtonBinder;
import com.example.workutils.system.ui.icon.AppIcon;
import com.example.workutils.system.ui.icon.IconFactory;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.CssParser;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.Styleable;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
@@ -20,8 +27,10 @@ import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -32,6 +41,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static com.example.workutils.system.ui.FxTestSupport.fx;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -89,6 +99,46 @@ class LightThemeCssRuntimeTest {
});
}
@Test
void disabledRunningPrimaryButtonKeepsPrimaryContrastAndWhiteSpinner() throws Exception {
fx(() -> {
AtomicReference<Runnable> pendingOperation = new AtomicReference<>();
AsyncOperationController<Void> controller =
new AsyncOperationController<>(pendingOperation::set, Runnable::run);
Button button = new Button(
"开始生成", IconFactory.createButtonIcon(AppIcon.EXPORT, "开始生成"));
button.getStyleClass().add(Theme.CSS_BTN_PRIMARY);
LoadingButtonBinder.bind(button, controller, "开始生成");
VBox root = new VBox(button);
Scene scene = new Scene(root, 320, 120);
scene.getStylesheets().add(copyRealStylesheetToUniqueUrl());
root.applyCss();
root.layout();
controller.start("正在生成预览", context -> null, ignored -> { });
root.applyCss();
root.layout();
assertTrue(button.isDisabled(), "running buttons must remain non-interactive");
assertTrue(button.getPseudoClassStates().contains(
PseudoClass.getPseudoClass("running")));
assertEquals(Color.web("#2563eb"),
button.getBackground().getFills().get(0).getFill());
assertEquals(Color.WHITE, button.getTextFill());
assertEquals(1, button.getOpacity(), 0.001,
"AtlantaFX disabled opacity must not wash out a running primary action");
ProgressIndicator spinner = assertInstanceOf(
ProgressIndicator.class, button.getGraphic());
assertEquals(14, spinner.getPrefWidth());
assertEquals(14, spinner.getPrefHeight());
assertEquals(Color.WHITE, computedCssValue(spinner, "-fx-progress-color"));
assertTrue(pendingOperation.get() != null,
"the no-op executor must keep the fixture in RUNNING state");
});
}
@Test
void repeatedUniqueLoadsPreserveAndRestoreParserErrorState() throws Exception {
fx(() -> {
@@ -267,6 +317,15 @@ class LightThemeCssRuntimeTest {
.orElseThrow();
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static Object computedCssValue(Styleable styleable, String property) {
CssMetaData metadata = styleable.getCssMetaData().stream()
.filter(candidate -> candidate.getProperty().equals(property))
.findFirst()
.orElseThrow(() -> new AssertionError("Missing CSS property " + property));
return metadata.getStyleableProperty(styleable).getValue();
}
private record Fixture(
Label pageTitle,
Label sectionTitle,
@@ -23,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.fail;
class NativeUiSceneVisualAcceptanceFixtureTest {
@Test
void capturesSixDistinctRealJavaFxScenesWithoutSynthesizingNativeChrome() throws Exception {
void capturesTenDistinctRealJavaFxScenesWithoutSynthesizingNativeChrome() throws Exception {
Assumptions.assumeTrue(NativeUiVisualAcceptanceFixture.captureEnabled(),
() -> "Enable explicitly with -D"
+ NativeUiVisualAcceptanceFixture.ENABLED_PROPERTY + "=true");
@@ -34,7 +34,7 @@ class NativeUiSceneVisualAcceptanceFixtureTest {
@SuppressWarnings("unchecked")
List<Object> captures = (List<Object>) capture.invoke(null);
assertEquals(6, captures.size());
assertEquals(10, captures.size());
List<String> actualNames = new ArrayList<>();
Set<String> imageHashes = new HashSet<>();
for (Object captured : captures) {
@@ -64,8 +64,12 @@ class NativeUiSceneVisualAcceptanceFixtureTest {
"scene-running-1440.png",
"scene-welcome-expanded-1024.png",
"scene-welcome-collapsed-1024.png",
"scene-running-1024.png"), actualNames);
assertEquals(6, imageHashes.size(),
"scene-running-1024.png",
"scene-selected-expanded-1440.png",
"scene-selected-collapsed-1440.png",
"scene-selected-expanded-1024.png",
"scene-selected-collapsed-1024.png"), actualNames);
assertEquals(10, imageHashes.size(),
"Every real Scene state and width must produce a distinct image");
}
@@ -18,10 +18,13 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.control.Labeled;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.robot.Robot;
import javafx.stage.Screen;
import javafx.stage.Stage;
@@ -39,6 +42,8 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import org.kordamp.ikonli.javafx.FontIcon;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -62,11 +67,20 @@ final class NativeUiVisualAcceptanceFixture {
new CaptureSpec("welcome-expanded-1024.png", View.WELCOME, true, 1024, 768),
new CaptureSpec("welcome-collapsed-1024.png", View.WELCOME, false, 1024, 768),
new CaptureSpec("running-1024.png", View.RUNNING, true, 1024, 768));
private static final List<CaptureSpec> SCENE_CAPTURE_SPECS = sceneCaptureSpecs();
private static final List<String> NAVIGATION_LABELS = List.of(
"Git仓库同步",
"Git分支评审",
"文本对比",
"MyBatis SQL导出",
"MyBatis逆向工程",
"扫描功能",
"API 接口文档导出");
private static final List<String> REQUIRED_IMAGE_NAMES = CAPTURE_SPECS.stream()
.map(CaptureSpec::fileName)
.toList();
private static final List<String> REQUIRED_SCENE_IMAGE_NAMES = CAPTURE_SPECS.stream()
private static final List<String> REQUIRED_SCENE_IMAGE_NAMES = SCENE_CAPTURE_SPECS.stream()
.map(spec -> "scene-" + spec.fileName())
.toList();
@@ -78,6 +92,10 @@ final class NativeUiVisualAcceptanceFixture {
return REQUIRED_SCENE_IMAGE_NAMES;
}
static List<String> navigationLabels() {
return NAVIGATION_LABELS;
}
static boolean captureEnabled() {
return "true".equals(System.getProperty(ENABLED_PROPERTY));
}
@@ -105,18 +123,18 @@ final class NativeUiVisualAcceptanceFixture {
*/
static List<CapturedImage> captureConfiguredSceneOutput() throws Exception {
Path outputDirectory = configuredOutputDirectory("JavaFX Scene snapshot capture");
FixtureWindow fixture = FxTestSupport.fx(() -> FixtureWindow.open(outputDirectory));
try {
List<CapturedImage> captures = new ArrayList<>();
for (CaptureSpec spec : CAPTURE_SPECS) {
List<CapturedImage> captures = new ArrayList<>();
for (CaptureSpec spec : SCENE_CAPTURE_SPECS) {
FixtureWindow fixture = FxTestSupport.fx(() -> FixtureWindow.open(outputDirectory));
try {
FxTestSupport.fx(() -> fixture.configure(spec));
settleNativeWindow();
captures.add(FxTestSupport.fx(() -> fixture.captureScene(spec)));
} finally {
FxTestSupport.fx(fixture::close);
}
return List.copyOf(captures);
} finally {
FxTestSupport.fx(fixture::close);
}
return List.copyOf(captures);
}
private static Path configuredOutputDirectory(String captureLabel) throws IOException {
@@ -175,6 +193,7 @@ final class NativeUiVisualAcceptanceFixture {
private enum View {
WELCOME,
SELECTED,
RUNNING
}
@@ -189,6 +208,7 @@ final class NativeUiVisualAcceptanceFixture {
private final MainWindow mainWindow;
private final Stage stage;
private final NavigationRail navigationRail;
private final Label toolbarTitle;
private final ManualExecutor manualExecutor = new ManualExecutor();
private final AsyncOperationController<Void> operationController =
new AsyncOperationController<>(manualExecutor, Runnable::run);
@@ -197,14 +217,10 @@ final class NativeUiVisualAcceptanceFixture {
private final OperationStatusPane operationStatusPane =
new OperationStatusPane(operationController, false);
private final VBox runningPane;
private int apiDocOpens;
private FixtureWindow(Path outputDirectory) throws ReflectiveOperationException {
this.outputDirectory = outputDirectory;
navItems = navigationItems();
mainWindow = new MainWindow(navItems, new WorkUtilsProperties(), () -> { });
stage = field(mainWindow, "stage", Stage.class);
navigationRail = field(mainWindow, "navigationRail", NavigationRail.class);
operationButton.setId("visual-fixture-operation");
operationButton.getStyleClass().add(Theme.CSS_BTN_PRIMARY);
operationButton.setAccessibleText("开始生成视觉验收预览");
@@ -217,6 +233,12 @@ final class NativeUiVisualAcceptanceFixture {
operationStatusPane);
runningPane.setPadding(new Insets(Theme.S6));
navItems = navigationItems(this::openApiDocFixture);
mainWindow = new MainWindow(navItems, new WorkUtilsProperties(), () -> { });
stage = field(mainWindow, "stage", Stage.class);
navigationRail = field(mainWindow, "navigationRail", NavigationRail.class);
toolbarTitle = field(mainWindow, "toolbarTitle", Label.class);
stage.setMinWidth(0);
stage.setMinHeight(0);
assertSame(StageStyle.DECORATED, stage.getStyle(),
@@ -240,7 +262,7 @@ final class NativeUiVisualAcceptanceFixture {
if (spec.view() == View.WELCOME) {
mainWindow.refreshNavItems(navItems);
} else {
showRunningState();
showApiDocState(spec.view() == View.RUNNING);
}
navigationRail.setExpanded(spec.expanded());
stage.toFront();
@@ -301,14 +323,28 @@ final class NativeUiVisualAcceptanceFixture {
stage.close();
}
private void showRunningState() {
mainWindow.embedRaw(null, "任务执行", runningPane);
if (operationController.snapshot().state() != OperationState.RUNNING) {
private void showApiDocState(boolean running) {
int opensBefore = apiDocOpens;
navigationRail.selectPlugin("api-doc");
assertEquals(opensBefore + 1, apiDocOpens,
"The safe api-doc fixture action must run once per selection");
if (running && operationController.snapshot().state() != OperationState.RUNNING) {
operationController.start("正在生成预览", context -> null, ignored -> { });
}
assertEquals(OperationState.RUNNING, operationController.snapshot().state());
assertTrue(manualExecutor.hasPendingTask(),
"The executor must hold the operation without executing it");
if (running) {
assertEquals(OperationState.RUNNING, operationController.snapshot().state());
assertTrue(manualExecutor.hasPendingTask(),
"The executor must hold the operation without executing it");
} else {
assertEquals(OperationState.IDLE, operationController.snapshot().state());
assertFalse(manualExecutor.hasPendingTask());
}
}
private void openApiDocFixture() {
apiDocOpens++;
mainWindow.embedPlugin(
"api-doc", null, "API 接口文档导出", runningPane);
}
private void validateShell(CaptureSpec spec) {
@@ -327,6 +363,10 @@ final class NativeUiVisualAcceptanceFixture {
assertEquals(DesignTokens.HEADER_HEIGHT, header.getBoundsInParent().getHeight(), 0.5);
validateSettingsControl(scene, header);
if (spec.view() != View.WELCOME) {
validateSelectedApiDocState(scene);
}
if (spec.view() == View.RUNNING) {
assertEquals(OperationState.RUNNING, operationController.snapshot().state());
assertTrue(operationButton.isVisible());
@@ -338,6 +378,23 @@ final class NativeUiVisualAcceptanceFixture {
}
}
private void validateSelectedApiDocState(Scene scene) {
ToggleButton apiDoc = (ToggleButton) scene.lookup("#nav-api-doc");
assertNotNull(apiDoc);
assertTrue(apiDoc.isSelected());
assertEquals("api-doc", navigationRail.selectedPluginId());
assertTrue(apiDoc.getStyleClass().contains(Theme.CSS_NAV_CELL_ACTIVE));
assertEquals(Color.web("#dbeafe"),
apiDoc.getBackground().getFills().get(0).getFill());
assertEquals(3,
apiDoc.getBorder().getStrokes().get(0).getWidths().getLeft());
assertEquals(Color.web("#2563eb"), apiDoc.getTextFill());
assertEquals(Color.web("#2563eb"),
((FontIcon) apiDoc.getGraphic()).getIconColor());
assertTrue(toolbarTitle.isVisible());
assertEquals("API 接口文档导出", toolbarTitle.getText());
}
private void validateSettingsControl(Scene scene, Node header) {
List<ButtonBase> settingsControls = descendants(scene.getRoot()).stream()
.filter(ButtonBase.class::isInstance)
@@ -377,14 +434,17 @@ final class NativeUiVisualAcceptanceFixture {
stage.setY(desktop.getMinY() + (desktop.getHeight() - spec.stageHeight()) / 2);
}
private static List<MainWindow.NavItem> navigationItems() {
private static List<MainWindow.NavItem> navigationItems(Runnable openApiDoc) {
return List.of(
nav(AppIcon.GIT_SYNC, "Git仓库同步", "同步本地仓库", "git-sync"),
nav(AppIcon.GIT_REVIEW, "Git分支评审", "评审分支变更", "git-review"),
nav(AppIcon.DIFF, "文本对比", "比较文本差异", "diff"),
nav(AppIcon.MYBATIS_GENERATOR, "代码生成", "生成 MyBatis 代码", "generator"),
nav(AppIcon.GIT_SYNC, "Git 同步", "同步本地仓库", "git-sync"),
nav(AppIcon.API_DOC, "API 文档", "导出接口文档", "api-doc"),
nav(AppIcon.SQL_EXPORT, "SQL 导出", "导出查询结果", "sql-export"),
nav(AppIcon.SCANNER, "关键词扫描", "扫描代码关键词", "scan"));
nav(AppIcon.SQL_EXPORT, "MyBatis SQL导出", "导出查询结果", "mybatisexport"),
nav(AppIcon.MYBATIS_GENERATOR, "MyBatis逆向工程", "生成 MyBatis 代码",
"mybatisgenerator"),
nav(AppIcon.SCANNER, "扫描功能", "扫描代码关键词", "scan"),
new MainWindow.NavItem(AppIcon.API_DOC, "API 接口文档导出",
"导出接口文档", openApiDoc, "api-doc"));
}
private static MainWindow.NavItem nav(AppIcon icon, String name,
@@ -400,6 +460,19 @@ final class NativeUiVisualAcceptanceFixture {
}
}
private static List<CaptureSpec> sceneCaptureSpecs() {
List<CaptureSpec> specs = new ArrayList<>(CAPTURE_SPECS);
specs.add(new CaptureSpec(
"selected-expanded-1440.png", View.SELECTED, true, 1440, 900));
specs.add(new CaptureSpec(
"selected-collapsed-1440.png", View.SELECTED, false, 1440, 900));
specs.add(new CaptureSpec(
"selected-expanded-1024.png", View.SELECTED, true, 1024, 768));
specs.add(new CaptureSpec(
"selected-collapsed-1024.png", View.SELECTED, false, 1024, 768));
return List.copyOf(specs);
}
private static List<Node> descendants(Parent parent) {
List<Node> descendants = new ArrayList<>();
Deque<Node> pending = new ArrayDeque<>(parent.getChildrenUnmodifiable());
@@ -49,10 +49,29 @@ class NativeUiVisualAcceptanceFixtureContractTest {
"scene-running-1440.png",
"scene-welcome-expanded-1024.png",
"scene-welcome-collapsed-1024.png",
"scene-running-1024.png"),
"scene-running-1024.png",
"scene-selected-expanded-1440.png",
"scene-selected-collapsed-1440.png",
"scene-selected-expanded-1024.png",
"scene-selected-collapsed-1024.png"),
requiredSceneImageNames.invoke(null));
}
@Test
void fixtureUsesTheApprovedSevenNavigationLabelsInOrder() throws Exception {
Method navigationLabels = method("navigationLabels");
assertEquals(List.of(
"Git仓库同步",
"Git分支评审",
"文本对比",
"MyBatis SQL导出",
"MyBatis逆向工程",
"扫描功能",
"API 接口文档导出"),
navigationLabels.invoke(null));
}
@Test
void fixtureIsDisabledUnlessTheExplicitPropertyIsTrue() throws Exception {
Method captureEnabled = method("captureEnabled");
@@ -11,6 +11,7 @@ import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
@@ -56,10 +57,12 @@ class NavigationRailTest {
window.embedPlugin("api-doc", "legacy-api-icon", "API", new StackPane());
HBox header = assertInstanceOf(
HBox.class, stage(window).getScene().getRoot().lookup("#app-header"));
stage(window).getScene().getRoot().applyCss();
stage(window).getScene().getRoot().layout();
assertEquals(6, header.getChildren().size());
Button menu = assertInstanceOf(Button.class, header.getChildren().get(0));
Label brand = assertInstanceOf(Label.class, header.getChildren().get(1));
HBox brand = assertInstanceOf(HBox.class, header.getChildren().get(1));
FontIcon chevron = assertInstanceOf(FontIcon.class, header.getChildren().get(2));
Label title = assertInstanceOf(Label.class, header.getChildren().get(3));
assertInstanceOf(Region.class, header.getChildren().get(4));
@@ -69,8 +72,26 @@ class NavigationRailTest {
assertInstanceOf(FontIcon.class, menu.getGraphic()).getIconCode());
assertEquals("切换导航栏", menu.getAccessibleText());
assertTrue(menu.isFocusTraversable());
assertEquals(2, brand.getChildren().size());
StackPane brandIconContainer = assertInstanceOf(
StackPane.class, brand.getChildren().get(0));
Label brandText = assertInstanceOf(Label.class, brand.getChildren().get(1));
assertEquals("app-brand-icon-container", brandIconContainer.getId());
assertTrue(brandIconContainer.getStyleClass().contains("app-brand-icon"));
assertEquals(32, brandIconContainer.getMinWidth());
assertEquals(32, brandIconContainer.getPrefWidth());
assertEquals(32, brandIconContainer.getMaxWidth());
assertEquals(32, brandIconContainer.getMinHeight());
assertEquals(32, brandIconContainer.getPrefHeight());
assertEquals(32, brandIconContainer.getMaxHeight());
assertEquals(Color.web("#2563eb"),
brandIconContainer.getBackground().getFills().get(0).getFill());
FontIcon brandIcon = assertInstanceOf(
FontIcon.class, brandIconContainer.getChildren().get(0));
assertEquals(AppIcon.APP.glyph(),
assertInstanceOf(FontIcon.class, brand.getGraphic()).getIconCode());
brandIcon.getIconCode());
assertEquals(Color.WHITE, brandIcon.getIconColor());
assertEquals("WorkUtils", brandText.getText());
assertEquals(AppIcon.CHEVRON_RIGHT.glyph(), chevron.getIconCode());
assertEquals("API", title.getText());
assertEquals(AppIcon.SETTINGS.glyph(),
@@ -157,6 +178,39 @@ class NavigationRailTest {
});
}
@Test
void bottomSeparatorIsExactlyOnePixelWithoutPaddingOrBackgroundBand() throws Exception {
fx(() -> {
NavigationRail rail = new NavigationRail(List.of(API_DOC), item -> {
});
VBox root = new VBox(rail);
Scene scene = new Scene(root, DesignTokens.NAV_EXPANDED_WIDTH, 320);
Theme.applyCustomCss(scene);
root.applyCss();
root.layout();
Separator separator = assertInstanceOf(
Separator.class, rail.lookup("#navigation-rail-separator"));
assertEquals(1, separator.getMinHeight());
assertEquals(1, separator.getPrefHeight());
assertEquals(1, separator.getMaxHeight());
assertEquals(1, separator.getHeight());
assertEquals(0, separator.getPadding().getTop());
assertEquals(0, separator.getPadding().getRight());
assertEquals(0, separator.getPadding().getBottom());
assertEquals(0, separator.getPadding().getLeft());
assertTrue(separator.getBackground() == null
|| separator.getBackground().getFills().stream()
.allMatch(fill -> Color.TRANSPARENT.equals(fill.getFill())),
"the Separator control must not paint a gray-blue background band");
Region line = assertInstanceOf(Region.class, separator.lookup(".line"));
assertEquals(1, line.getBorder().getStrokes().get(0).getWidths().getTop());
assertEquals(Color.web("#e2e8f0"),
line.getBorder().getStrokes().get(0).getTopStroke());
});
}
@Test
void headerMenuAndBottomCollapseEntryDriveTheSameExpandedState() throws Exception {
fx(() -> {