修复视觉验收夹具与运行态对比度
This commit is contained in:
@@ -121,9 +121,20 @@ public class MainWindow {
|
|||||||
menuButton.setFocusTraversable(true);
|
menuButton.setFocusTraversable(true);
|
||||||
menuButton.setOnAction(event -> navigationRail.toggle());
|
menuButton.setOnAction(event -> navigationRail.toggle());
|
||||||
|
|
||||||
Label brand = new Label("WorkUtils", IconFactory.create(AppIcon.APP, 20));
|
FontIcon brandIcon = IconFactory.create(AppIcon.APP, 18);
|
||||||
brand.setContentDisplay(ContentDisplay.LEFT);
|
StackPane brandIconContainer = new StackPane(brandIcon);
|
||||||
brand.setFont(Theme.sectionTitle());
|
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 = IconFactory.create(AppIcon.CHEVRON_RIGHT, 14);
|
||||||
toolbarChevron.setVisible(false);
|
toolbarChevron.setVisible(false);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import javafx.application.Platform;
|
|||||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ContentDisplay;
|
import javafx.scene.control.ContentDisplay;
|
||||||
@@ -54,7 +55,13 @@ public final class NavigationRail extends VBox {
|
|||||||
buildBottomToggle();
|
buildBottomToggle();
|
||||||
|
|
||||||
Separator separator = new Separator();
|
Separator separator = new Separator();
|
||||||
|
separator.setId("navigation-rail-separator");
|
||||||
separator.getStyleClass().add(Theme.CSS_NAV_SEP);
|
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);
|
getChildren().addAll(itemBox, separator, collapseButton);
|
||||||
|
|
||||||
expanded.addListener((observable, oldValue, newValue) -> updateAppearance());
|
expanded.addListener((observable, oldValue, newValue) -> updateAppearance());
|
||||||
|
|||||||
@@ -30,7 +30,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-separator {
|
.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 导航 ============ */
|
/* ============ ToggleButton 导航 ============ */
|
||||||
@@ -99,6 +110,22 @@
|
|||||||
-fx-max-height: 48px;
|
-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 {
|
.content-area {
|
||||||
-fx-background-color: -workutils-bg;
|
-fx-background-color: -workutils-bg;
|
||||||
@@ -153,6 +180,17 @@
|
|||||||
-fx-background-color: derive(-workutils-primary, -12%);
|
-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 {
|
.btn-danger {
|
||||||
-fx-background-color: -workutils-danger;
|
-fx-background-color: -workutils-danger;
|
||||||
-fx-text-fill: white;
|
-fx-text-fill: white;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public final class FxTestSupport {
|
|||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new AssertionError(interrupted);
|
throw new AssertionError(interrupted);
|
||||||
}
|
}
|
||||||
|
Platform.setImplicitExit(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T fx(Callable<T> action) throws Exception {
|
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;
|
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.beans.property.SimpleStringProperty;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.css.CssParser;
|
import javafx.css.CssParser;
|
||||||
|
import javafx.css.CssMetaData;
|
||||||
|
import javafx.css.PseudoClass;
|
||||||
|
import javafx.css.Styleable;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
@@ -20,8 +27,10 @@ import javafx.scene.control.TextField;
|
|||||||
import javafx.scene.control.TreeCell;
|
import javafx.scene.control.TreeCell;
|
||||||
import javafx.scene.control.TreeItem;
|
import javafx.scene.control.TreeItem;
|
||||||
import javafx.scene.control.TreeView;
|
import javafx.scene.control.TreeView;
|
||||||
|
import javafx.scene.control.ProgressIndicator;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
@@ -32,6 +41,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static com.example.workutils.system.ui.FxTestSupport.fx;
|
import static com.example.workutils.system.ui.FxTestSupport.fx;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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
|
@Test
|
||||||
void repeatedUniqueLoadsPreserveAndRestoreParserErrorState() throws Exception {
|
void repeatedUniqueLoadsPreserveAndRestoreParserErrorState() throws Exception {
|
||||||
fx(() -> {
|
fx(() -> {
|
||||||
@@ -267,6 +317,15 @@ class LightThemeCssRuntimeTest {
|
|||||||
.orElseThrow();
|
.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(
|
private record Fixture(
|
||||||
Label pageTitle,
|
Label pageTitle,
|
||||||
Label sectionTitle,
|
Label sectionTitle,
|
||||||
|
|||||||
+8
-4
@@ -23,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.fail;
|
|||||||
class NativeUiSceneVisualAcceptanceFixtureTest {
|
class NativeUiSceneVisualAcceptanceFixtureTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void capturesSixDistinctRealJavaFxScenesWithoutSynthesizingNativeChrome() throws Exception {
|
void capturesTenDistinctRealJavaFxScenesWithoutSynthesizingNativeChrome() throws Exception {
|
||||||
Assumptions.assumeTrue(NativeUiVisualAcceptanceFixture.captureEnabled(),
|
Assumptions.assumeTrue(NativeUiVisualAcceptanceFixture.captureEnabled(),
|
||||||
() -> "Enable explicitly with -D"
|
() -> "Enable explicitly with -D"
|
||||||
+ NativeUiVisualAcceptanceFixture.ENABLED_PROPERTY + "=true");
|
+ NativeUiVisualAcceptanceFixture.ENABLED_PROPERTY + "=true");
|
||||||
@@ -34,7 +34,7 @@ class NativeUiSceneVisualAcceptanceFixtureTest {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<Object> captures = (List<Object>) capture.invoke(null);
|
List<Object> captures = (List<Object>) capture.invoke(null);
|
||||||
|
|
||||||
assertEquals(6, captures.size());
|
assertEquals(10, captures.size());
|
||||||
List<String> actualNames = new ArrayList<>();
|
List<String> actualNames = new ArrayList<>();
|
||||||
Set<String> imageHashes = new HashSet<>();
|
Set<String> imageHashes = new HashSet<>();
|
||||||
for (Object captured : captures) {
|
for (Object captured : captures) {
|
||||||
@@ -64,8 +64,12 @@ class NativeUiSceneVisualAcceptanceFixtureTest {
|
|||||||
"scene-running-1440.png",
|
"scene-running-1440.png",
|
||||||
"scene-welcome-expanded-1024.png",
|
"scene-welcome-expanded-1024.png",
|
||||||
"scene-welcome-collapsed-1024.png",
|
"scene-welcome-collapsed-1024.png",
|
||||||
"scene-running-1024.png"), actualNames);
|
"scene-running-1024.png",
|
||||||
assertEquals(6, imageHashes.size(),
|
"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");
|
"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.Scene;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ButtonBase;
|
import javafx.scene.control.ButtonBase;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Labeled;
|
import javafx.scene.control.Labeled;
|
||||||
import javafx.scene.control.ProgressIndicator;
|
import javafx.scene.control.ProgressIndicator;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
import javafx.scene.image.WritableImage;
|
import javafx.scene.image.WritableImage;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.robot.Robot;
|
import javafx.scene.robot.Robot;
|
||||||
import javafx.stage.Screen;
|
import javafx.stage.Screen;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
@@ -39,6 +42,8 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Executor;
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
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-expanded-1024.png", View.WELCOME, true, 1024, 768),
|
||||||
new CaptureSpec("welcome-collapsed-1024.png", View.WELCOME, false, 1024, 768),
|
new CaptureSpec("welcome-collapsed-1024.png", View.WELCOME, false, 1024, 768),
|
||||||
new CaptureSpec("running-1024.png", View.RUNNING, true, 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()
|
private static final List<String> REQUIRED_IMAGE_NAMES = CAPTURE_SPECS.stream()
|
||||||
.map(CaptureSpec::fileName)
|
.map(CaptureSpec::fileName)
|
||||||
.toList();
|
.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())
|
.map(spec -> "scene-" + spec.fileName())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -78,6 +92,10 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
return REQUIRED_SCENE_IMAGE_NAMES;
|
return REQUIRED_SCENE_IMAGE_NAMES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<String> navigationLabels() {
|
||||||
|
return NAVIGATION_LABELS;
|
||||||
|
}
|
||||||
|
|
||||||
static boolean captureEnabled() {
|
static boolean captureEnabled() {
|
||||||
return "true".equals(System.getProperty(ENABLED_PROPERTY));
|
return "true".equals(System.getProperty(ENABLED_PROPERTY));
|
||||||
}
|
}
|
||||||
@@ -105,18 +123,18 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
*/
|
*/
|
||||||
static List<CapturedImage> captureConfiguredSceneOutput() throws Exception {
|
static List<CapturedImage> captureConfiguredSceneOutput() throws Exception {
|
||||||
Path outputDirectory = configuredOutputDirectory("JavaFX Scene snapshot capture");
|
Path outputDirectory = configuredOutputDirectory("JavaFX Scene snapshot capture");
|
||||||
FixtureWindow fixture = FxTestSupport.fx(() -> FixtureWindow.open(outputDirectory));
|
List<CapturedImage> captures = new ArrayList<>();
|
||||||
try {
|
for (CaptureSpec spec : SCENE_CAPTURE_SPECS) {
|
||||||
List<CapturedImage> captures = new ArrayList<>();
|
FixtureWindow fixture = FxTestSupport.fx(() -> FixtureWindow.open(outputDirectory));
|
||||||
for (CaptureSpec spec : CAPTURE_SPECS) {
|
try {
|
||||||
FxTestSupport.fx(() -> fixture.configure(spec));
|
FxTestSupport.fx(() -> fixture.configure(spec));
|
||||||
settleNativeWindow();
|
settleNativeWindow();
|
||||||
captures.add(FxTestSupport.fx(() -> fixture.captureScene(spec)));
|
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 {
|
private static Path configuredOutputDirectory(String captureLabel) throws IOException {
|
||||||
@@ -175,6 +193,7 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
|
|
||||||
private enum View {
|
private enum View {
|
||||||
WELCOME,
|
WELCOME,
|
||||||
|
SELECTED,
|
||||||
RUNNING
|
RUNNING
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +208,7 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
private final MainWindow mainWindow;
|
private final MainWindow mainWindow;
|
||||||
private final Stage stage;
|
private final Stage stage;
|
||||||
private final NavigationRail navigationRail;
|
private final NavigationRail navigationRail;
|
||||||
|
private final Label toolbarTitle;
|
||||||
private final ManualExecutor manualExecutor = new ManualExecutor();
|
private final ManualExecutor manualExecutor = new ManualExecutor();
|
||||||
private final AsyncOperationController<Void> operationController =
|
private final AsyncOperationController<Void> operationController =
|
||||||
new AsyncOperationController<>(manualExecutor, Runnable::run);
|
new AsyncOperationController<>(manualExecutor, Runnable::run);
|
||||||
@@ -197,14 +217,10 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
private final OperationStatusPane operationStatusPane =
|
private final OperationStatusPane operationStatusPane =
|
||||||
new OperationStatusPane(operationController, false);
|
new OperationStatusPane(operationController, false);
|
||||||
private final VBox runningPane;
|
private final VBox runningPane;
|
||||||
|
private int apiDocOpens;
|
||||||
|
|
||||||
private FixtureWindow(Path outputDirectory) throws ReflectiveOperationException {
|
private FixtureWindow(Path outputDirectory) throws ReflectiveOperationException {
|
||||||
this.outputDirectory = outputDirectory;
|
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.setId("visual-fixture-operation");
|
||||||
operationButton.getStyleClass().add(Theme.CSS_BTN_PRIMARY);
|
operationButton.getStyleClass().add(Theme.CSS_BTN_PRIMARY);
|
||||||
operationButton.setAccessibleText("开始生成视觉验收预览");
|
operationButton.setAccessibleText("开始生成视觉验收预览");
|
||||||
@@ -217,6 +233,12 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
operationStatusPane);
|
operationStatusPane);
|
||||||
runningPane.setPadding(new Insets(Theme.S6));
|
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.setMinWidth(0);
|
||||||
stage.setMinHeight(0);
|
stage.setMinHeight(0);
|
||||||
assertSame(StageStyle.DECORATED, stage.getStyle(),
|
assertSame(StageStyle.DECORATED, stage.getStyle(),
|
||||||
@@ -240,7 +262,7 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
if (spec.view() == View.WELCOME) {
|
if (spec.view() == View.WELCOME) {
|
||||||
mainWindow.refreshNavItems(navItems);
|
mainWindow.refreshNavItems(navItems);
|
||||||
} else {
|
} else {
|
||||||
showRunningState();
|
showApiDocState(spec.view() == View.RUNNING);
|
||||||
}
|
}
|
||||||
navigationRail.setExpanded(spec.expanded());
|
navigationRail.setExpanded(spec.expanded());
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
@@ -301,14 +323,28 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
stage.close();
|
stage.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showRunningState() {
|
private void showApiDocState(boolean running) {
|
||||||
mainWindow.embedRaw(null, "任务执行", runningPane);
|
int opensBefore = apiDocOpens;
|
||||||
if (operationController.snapshot().state() != OperationState.RUNNING) {
|
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 -> { });
|
operationController.start("正在生成预览", context -> null, ignored -> { });
|
||||||
}
|
}
|
||||||
assertEquals(OperationState.RUNNING, operationController.snapshot().state());
|
if (running) {
|
||||||
assertTrue(manualExecutor.hasPendingTask(),
|
assertEquals(OperationState.RUNNING, operationController.snapshot().state());
|
||||||
"The executor must hold the operation without executing it");
|
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) {
|
private void validateShell(CaptureSpec spec) {
|
||||||
@@ -327,6 +363,10 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
assertEquals(DesignTokens.HEADER_HEIGHT, header.getBoundsInParent().getHeight(), 0.5);
|
assertEquals(DesignTokens.HEADER_HEIGHT, header.getBoundsInParent().getHeight(), 0.5);
|
||||||
validateSettingsControl(scene, header);
|
validateSettingsControl(scene, header);
|
||||||
|
|
||||||
|
if (spec.view() != View.WELCOME) {
|
||||||
|
validateSelectedApiDocState(scene);
|
||||||
|
}
|
||||||
|
|
||||||
if (spec.view() == View.RUNNING) {
|
if (spec.view() == View.RUNNING) {
|
||||||
assertEquals(OperationState.RUNNING, operationController.snapshot().state());
|
assertEquals(OperationState.RUNNING, operationController.snapshot().state());
|
||||||
assertTrue(operationButton.isVisible());
|
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) {
|
private void validateSettingsControl(Scene scene, Node header) {
|
||||||
List<ButtonBase> settingsControls = descendants(scene.getRoot()).stream()
|
List<ButtonBase> settingsControls = descendants(scene.getRoot()).stream()
|
||||||
.filter(ButtonBase.class::isInstance)
|
.filter(ButtonBase.class::isInstance)
|
||||||
@@ -377,14 +434,17 @@ final class NativeUiVisualAcceptanceFixture {
|
|||||||
stage.setY(desktop.getMinY() + (desktop.getHeight() - spec.stageHeight()) / 2);
|
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(
|
return List.of(
|
||||||
|
nav(AppIcon.GIT_SYNC, "Git仓库同步", "同步本地仓库", "git-sync"),
|
||||||
|
nav(AppIcon.GIT_REVIEW, "Git分支评审", "评审分支变更", "git-review"),
|
||||||
nav(AppIcon.DIFF, "文本对比", "比较文本差异", "diff"),
|
nav(AppIcon.DIFF, "文本对比", "比较文本差异", "diff"),
|
||||||
nav(AppIcon.MYBATIS_GENERATOR, "代码生成", "生成 MyBatis 代码", "generator"),
|
nav(AppIcon.SQL_EXPORT, "MyBatis SQL导出", "导出查询结果", "mybatisexport"),
|
||||||
nav(AppIcon.GIT_SYNC, "Git 同步", "同步本地仓库", "git-sync"),
|
nav(AppIcon.MYBATIS_GENERATOR, "MyBatis逆向工程", "生成 MyBatis 代码",
|
||||||
nav(AppIcon.API_DOC, "API 文档", "导出接口文档", "api-doc"),
|
"mybatisgenerator"),
|
||||||
nav(AppIcon.SQL_EXPORT, "SQL 导出", "导出查询结果", "sql-export"),
|
nav(AppIcon.SCANNER, "扫描功能", "扫描代码关键词", "scan"),
|
||||||
nav(AppIcon.SCANNER, "关键词扫描", "扫描代码关键词", "scan"));
|
new MainWindow.NavItem(AppIcon.API_DOC, "API 接口文档导出",
|
||||||
|
"导出接口文档", openApiDoc, "api-doc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MainWindow.NavItem nav(AppIcon icon, String name,
|
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) {
|
private static List<Node> descendants(Parent parent) {
|
||||||
List<Node> descendants = new ArrayList<>();
|
List<Node> descendants = new ArrayList<>();
|
||||||
Deque<Node> pending = new ArrayDeque<>(parent.getChildrenUnmodifiable());
|
Deque<Node> pending = new ArrayDeque<>(parent.getChildrenUnmodifiable());
|
||||||
|
|||||||
+20
-1
@@ -49,10 +49,29 @@ class NativeUiVisualAcceptanceFixtureContractTest {
|
|||||||
"scene-running-1440.png",
|
"scene-running-1440.png",
|
||||||
"scene-welcome-expanded-1024.png",
|
"scene-welcome-expanded-1024.png",
|
||||||
"scene-welcome-collapsed-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));
|
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
|
@Test
|
||||||
void fixtureIsDisabledUnlessTheExplicitPropertyIsTrue() throws Exception {
|
void fixtureIsDisabledUnlessTheExplicitPropertyIsTrue() throws Exception {
|
||||||
Method captureEnabled = method("captureEnabled");
|
Method captureEnabled = method("captureEnabled");
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import javafx.scene.Scene;
|
|||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ContentDisplay;
|
import javafx.scene.control.ContentDisplay;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Separator;
|
||||||
import javafx.scene.control.ToggleButton;
|
import javafx.scene.control.ToggleButton;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
@@ -56,10 +57,12 @@ class NavigationRailTest {
|
|||||||
window.embedPlugin("api-doc", "legacy-api-icon", "API", new StackPane());
|
window.embedPlugin("api-doc", "legacy-api-icon", "API", new StackPane());
|
||||||
HBox header = assertInstanceOf(
|
HBox header = assertInstanceOf(
|
||||||
HBox.class, stage(window).getScene().getRoot().lookup("#app-header"));
|
HBox.class, stage(window).getScene().getRoot().lookup("#app-header"));
|
||||||
|
stage(window).getScene().getRoot().applyCss();
|
||||||
|
stage(window).getScene().getRoot().layout();
|
||||||
|
|
||||||
assertEquals(6, header.getChildren().size());
|
assertEquals(6, header.getChildren().size());
|
||||||
Button menu = assertInstanceOf(Button.class, header.getChildren().get(0));
|
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));
|
FontIcon chevron = assertInstanceOf(FontIcon.class, header.getChildren().get(2));
|
||||||
Label title = assertInstanceOf(Label.class, header.getChildren().get(3));
|
Label title = assertInstanceOf(Label.class, header.getChildren().get(3));
|
||||||
assertInstanceOf(Region.class, header.getChildren().get(4));
|
assertInstanceOf(Region.class, header.getChildren().get(4));
|
||||||
@@ -69,8 +72,26 @@ class NavigationRailTest {
|
|||||||
assertInstanceOf(FontIcon.class, menu.getGraphic()).getIconCode());
|
assertInstanceOf(FontIcon.class, menu.getGraphic()).getIconCode());
|
||||||
assertEquals("切换导航栏", menu.getAccessibleText());
|
assertEquals("切换导航栏", menu.getAccessibleText());
|
||||||
assertTrue(menu.isFocusTraversable());
|
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(),
|
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(AppIcon.CHEVRON_RIGHT.glyph(), chevron.getIconCode());
|
||||||
assertEquals("API", title.getText());
|
assertEquals("API", title.getText());
|
||||||
assertEquals(AppIcon.SETTINGS.glyph(),
|
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
|
@Test
|
||||||
void headerMenuAndBottomCollapseEntryDriveTheSameExpandedState() throws Exception {
|
void headerMenuAndBottomCollapseEntryDriveTheSameExpandedState() throws Exception {
|
||||||
fx(() -> {
|
fx(() -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user