Navigate authoritative FXML locations, withdraw stale results and preserve host Undo/Redo. Include editor ownership in native problem identity so equal diagnostics from separate owners survive independent cleanup. Qualify both hosts automatically.
433 lines
18 KiB
Java
433 lines
18 KiB
Java
package qualification;
|
|
|
|
import com.intellij.openapi.application.*;
|
|
import com.intellij.openapi.fileEditor.*;
|
|
import com.intellij.openapi.project.Project;
|
|
import com.intellij.openapi.startup.StartupActivity;
|
|
import com.intellij.openapi.vfs.*;
|
|
import com.oracle.javafx.scenebuilder.kit.fxom.*;
|
|
import de.neitzel.fxdesigner.kit.*;
|
|
import java.nio.file.*;
|
|
import java.util.concurrent.*;
|
|
import javafx.application.Platform;
|
|
import javafx.embed.swing.JFXPanel;
|
|
import javafx.scene.control.Button;
|
|
|
|
/** Disposable companion plugin, never included in the delivered designer plugin. */
|
|
public final class WorkflowProbe implements StartupActivity.DumbAware {
|
|
static final Path output = Path.of(System.getProperty("designer.check.output"));
|
|
static DesignerPane active;
|
|
static final java.util.concurrent.atomic.AtomicBoolean started =
|
|
new java.util.concurrent.atomic.AtomicBoolean();
|
|
|
|
static <T> T edt(Callable<T> action) throws Exception {
|
|
var f = new CompletableFuture<T>();
|
|
ApplicationManager.getApplication()
|
|
.invokeLater(
|
|
() ->
|
|
WriteIntentReadAction.run(
|
|
() -> {
|
|
try {
|
|
f.complete(action.call());
|
|
} catch (Throwable t) {
|
|
f.completeExceptionally(t);
|
|
}
|
|
}),
|
|
ModalityState.nonModal());
|
|
return f.get(20, TimeUnit.SECONDS);
|
|
}
|
|
|
|
static <T> T fx(Callable<T> action) throws Exception {
|
|
var f = new CompletableFuture<T>();
|
|
Platform.runLater(
|
|
() -> {
|
|
try {
|
|
f.complete(action.call());
|
|
} catch (Throwable t) {
|
|
f.completeExceptionally(t);
|
|
}
|
|
});
|
|
return f.get(20, TimeUnit.SECONDS);
|
|
}
|
|
|
|
static JFXPanel bridge(java.awt.Component c) {
|
|
if (c instanceof JFXPanel p) return p;
|
|
if (c instanceof java.awt.Container parent)
|
|
for (var child : parent.getComponents()) {
|
|
var p = bridge(child);
|
|
if (p != null) return p;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static void await(Callable<Boolean> condition) throws Exception {
|
|
long until = System.nanoTime() + TimeUnit.SECONDS.toNanos(120);
|
|
while (System.nanoTime() < until) {
|
|
if (condition.call()) return;
|
|
Thread.sleep(100);
|
|
}
|
|
throw new AssertionError("IDE workflow timed out");
|
|
}
|
|
|
|
static void click(DesignerPane pane, String text) throws Exception {
|
|
fx(
|
|
() -> {
|
|
((Button)
|
|
pane.lookupAll(".button").stream()
|
|
.filter(n -> n instanceof Button b && b.getText().equals(text))
|
|
.findFirst()
|
|
.orElseThrow())
|
|
.fire();
|
|
return null;
|
|
});
|
|
}
|
|
|
|
private static void checkProblems(
|
|
Project project,
|
|
VirtualFile file,
|
|
DesignerPane pane,
|
|
de.neitzel.fxdesigner.host.DocumentSession document)
|
|
throws Exception {
|
|
var original = document.snapshot().toCompletableFuture().get();
|
|
String invalid = original.text().replace("path=\"home\"", "path=\"problemMissing\"");
|
|
if (invalid.equals(original.text())) throw new AssertionError("Missing fixture binding");
|
|
document
|
|
.apply(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch(
|
|
original.revision(),
|
|
"Problem fixture",
|
|
java.util.List.of(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch.Replacement(
|
|
0, original.text().length(), invalid))))
|
|
.toCompletableFuture()
|
|
.get();
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
pane.lookupAll(".text-area").stream()
|
|
.anyMatch(
|
|
n ->
|
|
n instanceof javafx.scene.control.TextArea t
|
|
&& t.getText().equals(invalid))));
|
|
fx(
|
|
() -> {
|
|
pane.lookupAll(".titled-pane").stream()
|
|
.filter(
|
|
n ->
|
|
n instanceof javafx.scene.control.TitledPane t
|
|
&& "Bindings".equals(t.getText()))
|
|
.forEach(n -> ((javafx.scene.control.TitledPane) n).setExpanded(true));
|
|
pane.lookupAll(".tab-pane").stream()
|
|
.filter(n -> n instanceof javafx.scene.control.TabPane)
|
|
.map(n -> (javafx.scene.control.TabPane) n)
|
|
.forEach(
|
|
t ->
|
|
t.getTabs().stream()
|
|
.filter(tab -> "Problems".equals(tab.getText()))
|
|
.findFirst()
|
|
.ifPresent(tab -> t.getSelectionModel().select(tab)));
|
|
return null;
|
|
});
|
|
click(pane, "Validate View");
|
|
var collector = com.intellij.analysis.problemsView.ProblemsCollector.getInstance(project);
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.anyMatch(p -> p.getText().contains("problemMissing"))));
|
|
var problem =
|
|
edt(
|
|
() ->
|
|
(com.intellij.analysis.problemsView.FileProblem)
|
|
collector.getFileProblems(file).stream()
|
|
.filter(p -> p.getText().contains("problemMissing"))
|
|
.findFirst()
|
|
.orElseThrow());
|
|
int expectedOffset =
|
|
edt(
|
|
() -> {
|
|
var d = FileDocumentManager.getInstance().getDocument(file);
|
|
return d.getLineStartOffset(problem.getLine()) + problem.getColumn();
|
|
});
|
|
String declaration =
|
|
invalid.substring(expectedOffset, invalid.indexOf('>', expectedOffset) + 1);
|
|
if (!declaration.startsWith("<DirectBinding")
|
|
|| !declaration.contains("path=\"problemMissing\""))
|
|
throw new AssertionError(
|
|
"Problems origin did not locate an affected binding: " + declaration);
|
|
edt(
|
|
() -> {
|
|
var window =
|
|
com.intellij.openapi.wm.ToolWindowManager.getInstance(project)
|
|
.getToolWindow("Problems View");
|
|
if (window == null) throw new AssertionError("Native Problems tool window missing");
|
|
window.show();
|
|
new OpenFileDescriptor(project, problem.getFile(), problem.getLine(), problem.getColumn())
|
|
.navigate(true);
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
edt(
|
|
() -> {
|
|
var textEditor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
|
return textEditor != null
|
|
&& textEditor.getCaretModel().getOffset() == expectedOffset;
|
|
}));
|
|
if (!document.snapshot().toCompletableFuture().get().text().equals(invalid))
|
|
throw new AssertionError("Problems navigation mutated source");
|
|
document.undo().toCompletableFuture().get();
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.noneMatch(p -> p.getText().contains("problemMissing"))));
|
|
if (!document.snapshot().toCompletableFuture().get().text().equals(original.text()))
|
|
throw new AssertionError("Problems navigation changed undo history");
|
|
// Observe the shared editor's Undo revision before issuing Redo; its previous invalid
|
|
// text must not satisfy the Redo wait while the Undo notification is still queued.
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
pane.lookupAll(".text-area").stream()
|
|
.anyMatch(
|
|
n ->
|
|
n instanceof javafx.scene.control.TextArea t
|
|
&& t.getText().equals(original.text()))));
|
|
document.redo().toCompletableFuture().get();
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
pane.lookupAll(".text-area").stream()
|
|
.anyMatch(
|
|
n ->
|
|
n instanceof javafx.scene.control.TextArea t
|
|
&& t.getText().equals(invalid))));
|
|
click(pane, "Validate View");
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.anyMatch(p -> p.getText().contains("problemMissing"))));
|
|
edt(
|
|
() -> {
|
|
FileEditorManager.getInstance(project).closeFile(file);
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.noneMatch(p -> "Neitzel FX Designer".equals(p.getGroup()))));
|
|
System.out.println(
|
|
"PASS native Problems: current validation, FXML origin/navigation, correction via Undo,"
|
|
+ " redo validation and editor-close cleanup");
|
|
}
|
|
|
|
public void runActivity(Project project) {
|
|
if (!started.compareAndSet(false, true)) return;
|
|
ApplicationManager.getApplication()
|
|
.executeOnPooledThread(
|
|
() -> {
|
|
try {
|
|
Path path =
|
|
Path.of(
|
|
project.getBasePath(),
|
|
"app/src/main/resources/fixture/components/Person.fxml");
|
|
VirtualFile file =
|
|
edt(() -> LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path));
|
|
FileEditor editor =
|
|
edt(
|
|
() -> {
|
|
var manager = FileEditorManager.getInstance(project);
|
|
var all = manager.openFile(file, true);
|
|
manager.setSelectedEditor(file, "neitzel-fx-designer");
|
|
return java.util.Arrays.stream(all)
|
|
.filter(e -> e.getName().equals("Neitzel Designer"))
|
|
.findFirst()
|
|
.orElseThrow();
|
|
});
|
|
await(() -> edt(() -> bridge(editor.getComponent()) != null));
|
|
JFXPanel bridge = edt(() -> bridge(editor.getComponent()));
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
bridge.getScene() != null
|
|
&& bridge.getScene().getRoot() instanceof DesignerPane));
|
|
DesignerPane pane = fx(() -> (DesignerPane) bridge.getScene().getRoot());
|
|
active = pane;
|
|
await(() -> fx(() -> pane.kit().editor().getFxomDocument() != null));
|
|
click(pane, "Build and Refresh");
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
java.util.List.of("card", "second").stream()
|
|
.allMatch(
|
|
id -> {
|
|
var item =
|
|
new FXOMFxIdIndex(
|
|
pane.kit().editor().getFxomDocument())
|
|
.lookup(id);
|
|
return item instanceof FXOMInstance instance
|
|
&& instance.getSceneGraphObject()
|
|
instanceof SnapshotRegion region
|
|
&& region.image() != null
|
|
&& region.statusText().equals("Target runtime");
|
|
})));
|
|
fx(
|
|
() -> {
|
|
var kit = pane.kit().editor();
|
|
var card =
|
|
(FXOMInstance) new FXOMFxIdIndex(kit.getFxomDocument()).lookup("card");
|
|
kit.getSelection().select(card);
|
|
var property =
|
|
(FXOMPropertyT)
|
|
card.getProperties()
|
|
.get(
|
|
new com.oracle.javafx.scenebuilder.kit.metadata.util
|
|
.PropertyName("caption"));
|
|
String old = property.getValue();
|
|
kit.getJobManager()
|
|
.push(
|
|
new com.oracle.javafx.scenebuilder.kit.editor.job.Job(kit) {
|
|
public boolean isExecutable() {
|
|
return true;
|
|
}
|
|
|
|
public String getDescription() {
|
|
return "IDE caption check";
|
|
}
|
|
|
|
public void execute() {
|
|
set("IDE canvas draft");
|
|
}
|
|
|
|
public void undo() {
|
|
set(old);
|
|
}
|
|
|
|
public void redo() {
|
|
execute();
|
|
}
|
|
|
|
void set(String text) {
|
|
var doc = kit.getFxomDocument();
|
|
doc.beginUpdate();
|
|
try {
|
|
property.setValue(text);
|
|
} finally {
|
|
doc.endUpdate();
|
|
}
|
|
}
|
|
});
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
FileDocumentManager.getInstance()
|
|
.getDocument(file)
|
|
.getText()
|
|
.contains("IDE canvas draft")));
|
|
click(pane, "Undo");
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
FileDocumentManager.getInstance()
|
|
.getDocument(file)
|
|
.getText()
|
|
.contains("FXML caption")));
|
|
click(pane, "Redo");
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
FileDocumentManager.getInstance()
|
|
.getDocument(file)
|
|
.getText()
|
|
.contains("IDE canvas draft")));
|
|
click(pane, "Render drafts");
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
java.util.List.of("card", "second").stream()
|
|
.allMatch(
|
|
id -> {
|
|
var item =
|
|
new FXOMFxIdIndex(
|
|
pane.kit().editor().getFxomDocument())
|
|
.lookup(id);
|
|
return item instanceof FXOMInstance instance
|
|
&& instance.getSceneGraphObject()
|
|
instanceof SnapshotRegion region
|
|
&& region.image() != null
|
|
&& region.statusText().equals("Target runtime");
|
|
})));
|
|
fx(
|
|
() -> {
|
|
javax.imageio.ImageIO.write(
|
|
javafx.embed.swing.SwingFXUtils.fromFXImage(
|
|
pane.snapshot(null, null), null),
|
|
"png",
|
|
output.resolve("intellij.png").toFile());
|
|
return null;
|
|
});
|
|
try (var document =
|
|
pane.kit().host().documents().open(path.toUri()).toCompletableFuture().get()) {
|
|
de.neitzel.fxdesigner.kit.ProjectionHostChecks.run(pane, document);
|
|
checkProblems(project, file, pane, document);
|
|
}
|
|
String version = fx(() -> System.getProperty("javafx.runtime.version"));
|
|
if (!version.startsWith("26"))
|
|
throw new AssertionError("Wrong host JavaFX: " + version);
|
|
Files.writeString(
|
|
output.resolve("SUCCESS.txt"),
|
|
"PASS registered IntelliJ editor, shared Kit, real Build and Refresh, target"
|
|
+ " pixels, native document edits, undo/redo, draft render,"
|
|
+ " source-preserving projection and extended Component scenarios; JavaFX "
|
|
+ version
|
|
+ "\n");
|
|
} catch (Throwable e) {
|
|
try {
|
|
if (active != null)
|
|
fx(
|
|
() -> {
|
|
Files.writeString(
|
|
output.resolve("ui-state.txt"),
|
|
active.lookupAll(".label").stream()
|
|
.filter(n -> n instanceof javafx.scene.control.Label)
|
|
.map(
|
|
n ->
|
|
String.valueOf(
|
|
((javafx.scene.control.Label) n).getText()))
|
|
.toList()
|
|
.toString());
|
|
javax.imageio.ImageIO.write(
|
|
javafx.embed.swing.SwingFXUtils.fromFXImage(
|
|
active.snapshot(null, null), null),
|
|
"png",
|
|
output.resolve("failed.png").toFile());
|
|
return null;
|
|
});
|
|
var w = new java.io.StringWriter();
|
|
e.printStackTrace(new java.io.PrintWriter(w));
|
|
Files.writeString(output.resolve("FAILURE.txt"), w.toString());
|
|
} catch (Exception ignored) {
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|