增加真实场景视觉验收截图
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
package com.example.workutils.system.ui;
|
||||
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class NativeUiSceneVisualAcceptanceFixtureTest {
|
||||
|
||||
@Test
|
||||
void capturesSixDistinctRealJavaFxScenesWithoutSynthesizingNativeChrome() throws Exception {
|
||||
Assumptions.assumeTrue(NativeUiVisualAcceptanceFixture.captureEnabled(),
|
||||
() -> "Enable explicitly with -D"
|
||||
+ NativeUiVisualAcceptanceFixture.ENABLED_PROPERTY + "=true");
|
||||
assertNotNull(System.getProperty(NativeUiVisualAcceptanceFixture.OUTPUT_PROPERTY),
|
||||
() -> "Missing -D" + NativeUiVisualAcceptanceFixture.OUTPUT_PROPERTY);
|
||||
|
||||
Method capture = captureMethod();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> captures = (List<Object>) capture.invoke(null);
|
||||
|
||||
assertEquals(6, captures.size());
|
||||
List<String> actualNames = new ArrayList<>();
|
||||
Set<String> imageHashes = new HashSet<>();
|
||||
for (Object captured : captures) {
|
||||
Method pathAccessor = captured.getClass().getDeclaredMethod("path");
|
||||
Method widthAccessor = captured.getClass().getDeclaredMethod("width");
|
||||
Method heightAccessor = captured.getClass().getDeclaredMethod("height");
|
||||
Path path = (Path) pathAccessor.invoke(captured);
|
||||
int width = (int) widthAccessor.invoke(captured);
|
||||
int height = (int) heightAccessor.invoke(captured);
|
||||
BufferedImage image = ImageIO.read(path.toFile());
|
||||
|
||||
actualNames.add(path.getFileName().toString());
|
||||
assertTrue(Files.isRegularFile(path), () -> "Missing scene capture: " + path);
|
||||
assertTrue(Files.size(path) > 0, () -> "Empty scene capture: " + path);
|
||||
assertNotNull(image, () -> "Unreadable scene PNG: " + path);
|
||||
assertEquals(image.getWidth(), width);
|
||||
assertEquals(image.getHeight(), height);
|
||||
assertSceneDimensions(path, width, height);
|
||||
assertTrue(hasVisibleContent(image),
|
||||
() -> "Scene snapshot is blank or black: " + path);
|
||||
imageHashes.add(sha256(path));
|
||||
}
|
||||
|
||||
assertEquals(List.of(
|
||||
"scene-welcome-expanded-1440.png",
|
||||
"scene-welcome-collapsed-1440.png",
|
||||
"scene-running-1440.png",
|
||||
"scene-welcome-expanded-1024.png",
|
||||
"scene-welcome-collapsed-1024.png",
|
||||
"scene-running-1024.png"), actualNames);
|
||||
assertEquals(6, imageHashes.size(),
|
||||
"Every real Scene state and width must produce a distinct image");
|
||||
}
|
||||
|
||||
private static void assertSceneDimensions(Path path, int width, int height) {
|
||||
String name = path.getFileName().toString();
|
||||
int stageWidth = name.contains("1440") ? 1440 : 1024;
|
||||
int stageHeight = name.contains("1440") ? 900 : 768;
|
||||
assertTrue(width <= stageWidth && width >= stageWidth - 32,
|
||||
() -> "Unexpected Scene width for " + path + ": " + width);
|
||||
assertTrue(height <= stageHeight && height >= stageHeight - 80,
|
||||
() -> "Unexpected Scene height for " + path + ": " + height);
|
||||
}
|
||||
|
||||
private static boolean hasVisibleContent(BufferedImage image) {
|
||||
long samples = 0;
|
||||
long visible = 0;
|
||||
for (int y = 0; y < image.getHeight(); y += 16) {
|
||||
for (int x = 0; x < image.getWidth(); x += 16) {
|
||||
int rgb = image.getRGB(x, y);
|
||||
int red = (rgb >>> 16) & 0xff;
|
||||
int green = (rgb >>> 8) & 0xff;
|
||||
int blue = rgb & 0xff;
|
||||
samples++;
|
||||
if (red > 32 || green > 32 || blue > 32) {
|
||||
visible++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return visible * 4 > samples;
|
||||
}
|
||||
|
||||
private static String sha256(Path path) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return HexFormat.of().formatHex(digest.digest(Files.readAllBytes(path)));
|
||||
}
|
||||
|
||||
private static Method captureMethod() {
|
||||
try {
|
||||
return NativeUiVisualAcceptanceFixture.class
|
||||
.getDeclaredMethod("captureConfiguredSceneOutput");
|
||||
} catch (NoSuchMethodException missingCapture) {
|
||||
return fail("Missing real JavaFX Scene snapshot entry point", missingCapture);
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
-13
@@ -46,7 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Explicitly enabled, test-only native screenshot fixture for the real application shell.
|
||||
* Explicitly enabled, test-only visual acceptance fixture for the real application shell.
|
||||
* The fixture deliberately owns no plugin, configuration, network, Git, or export service.
|
||||
*/
|
||||
final class NativeUiVisualAcceptanceFixture {
|
||||
@@ -66,28 +66,24 @@ final class NativeUiVisualAcceptanceFixture {
|
||||
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()
|
||||
.map(spec -> "scene-" + spec.fileName())
|
||||
.toList();
|
||||
|
||||
static List<String> requiredImageNames() {
|
||||
return REQUIRED_IMAGE_NAMES;
|
||||
}
|
||||
|
||||
static List<String> requiredSceneImageNames() {
|
||||
return REQUIRED_SCENE_IMAGE_NAMES;
|
||||
}
|
||||
|
||||
static boolean captureEnabled() {
|
||||
return "true".equals(System.getProperty(ENABLED_PROPERTY));
|
||||
}
|
||||
|
||||
static List<CapturedImage> captureConfiguredOutput() throws Exception {
|
||||
if (!captureEnabled()) {
|
||||
throw new IllegalStateException(
|
||||
"Native screenshot capture requires -D" + ENABLED_PROPERTY + "=true");
|
||||
}
|
||||
String configuredOutput = System.getProperty(OUTPUT_PROPERTY);
|
||||
if (configuredOutput == null || configuredOutput.isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"Native screenshot capture requires -D" + OUTPUT_PROPERTY + "=<directory>");
|
||||
}
|
||||
Path outputDirectory = Path.of(configuredOutput).toAbsolutePath().normalize();
|
||||
Files.createDirectories(outputDirectory);
|
||||
|
||||
Path outputDirectory = configuredOutputDirectory("Native screenshot capture");
|
||||
FixtureWindow fixture = FxTestSupport.fx(() -> FixtureWindow.open(outputDirectory));
|
||||
try {
|
||||
List<CapturedImage> captures = new ArrayList<>();
|
||||
@@ -103,6 +99,41 @@ final class NativeUiVisualAcceptanceFixture {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the real JavaFX Scene tree only. Native window chrome is intentionally excluded
|
||||
* and remains covered by the decorated-Stage assertions and Robot capture path above.
|
||||
*/
|
||||
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) {
|
||||
FxTestSupport.fx(() -> fixture.configure(spec));
|
||||
settleNativeWindow();
|
||||
captures.add(FxTestSupport.fx(() -> fixture.captureScene(spec)));
|
||||
}
|
||||
return List.copyOf(captures);
|
||||
} finally {
|
||||
FxTestSupport.fx(fixture::close);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path configuredOutputDirectory(String captureLabel) throws IOException {
|
||||
if (!captureEnabled()) {
|
||||
throw new IllegalStateException(
|
||||
captureLabel + " requires -D" + ENABLED_PROPERTY + "=true");
|
||||
}
|
||||
String configuredOutput = System.getProperty(OUTPUT_PROPERTY);
|
||||
if (configuredOutput == null || configuredOutput.isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
captureLabel + " requires -D" + OUTPUT_PROPERTY + "=<directory>");
|
||||
}
|
||||
Path outputDirectory = Path.of(configuredOutput).toAbsolutePath().normalize();
|
||||
Files.createDirectories(outputDirectory);
|
||||
return outputDirectory;
|
||||
}
|
||||
|
||||
private static void settleNativeWindow() throws Exception {
|
||||
FxTestSupport.fx(() -> { });
|
||||
Thread.sleep(200);
|
||||
@@ -242,6 +273,29 @@ final class NativeUiVisualAcceptanceFixture {
|
||||
return new CapturedImage(target, actualWidth, actualHeight);
|
||||
}
|
||||
|
||||
CapturedImage captureScene(CaptureSpec spec) throws IOException {
|
||||
assertTrue(javafx.application.Platform.isFxApplicationThread());
|
||||
Scene scene = stage.getScene();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
validateShell(spec);
|
||||
|
||||
WritableImage image = scene.snapshot(null);
|
||||
Path target = outputDirectory.resolve("scene-" + spec.fileName());
|
||||
boolean written = ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png",
|
||||
target.toFile());
|
||||
assertTrue(written, () -> "No PNG writer for " + target);
|
||||
int actualWidth = (int) Math.round(image.getWidth());
|
||||
int actualHeight = (int) Math.round(image.getHeight());
|
||||
assertEquals(scene.getWidth(), actualWidth, 1.0,
|
||||
"Scene snapshot width must match the real Scene");
|
||||
assertEquals(scene.getHeight(), actualHeight, 1.0,
|
||||
"Scene snapshot height must match the real Scene");
|
||||
assertTrue(Files.isRegularFile(target), () -> "Missing Scene capture: " + target);
|
||||
assertTrue(Files.size(target) > 0, () -> "Empty Scene capture: " + target);
|
||||
return new CapturedImage(target, actualWidth, actualHeight);
|
||||
}
|
||||
|
||||
void close() {
|
||||
operationStatusPane.dispose();
|
||||
stage.close();
|
||||
|
||||
+14
@@ -39,6 +39,20 @@ class NativeUiVisualAcceptanceFixtureContractTest {
|
||||
requiredImageNames.invoke(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fixtureDeclaresEveryRequiredRealSceneCapture() throws Exception {
|
||||
Method requiredSceneImageNames = method("requiredSceneImageNames");
|
||||
|
||||
assertEquals(List.of(
|
||||
"scene-welcome-expanded-1440.png",
|
||||
"scene-welcome-collapsed-1440.png",
|
||||
"scene-running-1440.png",
|
||||
"scene-welcome-expanded-1024.png",
|
||||
"scene-welcome-collapsed-1024.png",
|
||||
"scene-running-1024.png"),
|
||||
requiredSceneImageNames.invoke(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fixtureIsDisabledUnlessTheExplicitPropertyIsTrue() throws Exception {
|
||||
Method captureEnabled = method("captureEnabled");
|
||||
|
||||
Reference in New Issue
Block a user