修复视觉验收夹具与运行态对比度
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
+8
-4
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
+99
-26
@@ -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());
|
||||
|
||||
+20
-1
@@ -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(() -> {
|
||||
|
||||
Reference in New Issue
Block a user