Files
NeitzelFXDesigner/integration-tests/idea/WorkflowProbe.java
T

609 lines
26 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 com.intellij.codeInsight.daemon.impl.HighlightInfo directionHighlight(
Project project, VirtualFile file) {
var document = FileDocumentManager.getInstance().getDocument(file);
var result = new java.util.ArrayList<com.intellij.codeInsight.daemon.impl.HighlightInfo>();
com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx.processHighlights(
document,
project,
com.intellij.lang.annotation.HighlightSeverity.ERROR,
0,
document.getTextLength(),
info -> {
if (info.getDescription() != null
&& info.getDescription().startsWith("Invalid or missing direction:"))
result.add(info);
return true;
});
return result.isEmpty() ? null : result.getFirst();
}
private static void checkNativeFixes(
Project project,
VirtualFile file,
DesignerPane pane,
de.neitzel.fxdesigner.host.DocumentSession document)
throws Exception {
var before = document.snapshot().toCompletableFuture().get();
String bad = before.text().replaceFirst("direction=\"both\"", "direction = 'bad'");
if (bad.equals(before.text())) throw new AssertionError("Fixture has no direction");
document
.apply(
new de.neitzel.fxdesigner.contracts.EditBatch(
before.revision(),
"Native fix fixture",
java.util.List.of(
new de.neitzel.fxdesigner.contracts.EditBatch.Replacement(
0, before.text().length(), bad))))
.toCompletableFuture()
.get();
await(
() ->
fx(
() ->
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
.getText()
.equals(bad)));
int at = bad.indexOf("'bad'") + 1;
edt(
() -> {
new OpenFileDescriptor(project, file, at).navigate(true);
return null;
});
await(() -> edt(() -> directionHighlight(project, file) != null));
var info = edt(() -> directionHighlight(project, file));
if (info.getStartOffset() != at || info.getEndOffset() != at + 3)
throw new AssertionError("Wrong native diagnostic range");
var action =
edt(
() ->
info.findRegisteredQuickFix(
(descriptor, range) ->
"Set binding direction to both".equals(descriptor.getAction().getText())
? descriptor.getAction()
: null));
if (action == null) throw new AssertionError("Native Alt+Enter fix not registered");
edt(
() -> {
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
var psi =
com.intellij.psi.PsiDocumentManager.getInstance(project)
.getPsiFile(editor.getDocument());
if (!action.isAvailable(project, editor, psi))
throw new AssertionError("Native fix unavailable");
action.invoke(project, editor, psi);
return null;
});
String fixed = bad.replace("direction = 'bad'", "direction = 'both'");
await(() -> document.snapshot().toCompletableFuture().get().text().equals(fixed));
await(
() ->
fx(
() ->
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
.getText()
.equals(fixed)));
await(() -> edt(() -> directionHighlight(project, file) == null));
edt(
() -> {
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
var psi =
com.intellij.psi.PsiDocumentManager.getInstance(project)
.getPsiFile(editor.getDocument());
if (action.isAvailable(project, editor, psi))
throw new AssertionError("Old fix survived source change");
action.invoke(project, editor, psi);
return null;
});
if (!document.snapshot().toCompletableFuture().get().text().equals(fixed))
throw new AssertionError("Stale native fix changed source");
edt(
() -> {
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
com.intellij.openapi.command.undo.UndoManager.getInstance(project)
.undo(
com.intellij
.openapi
.fileEditor
.impl
.text
.TextEditorProvider
.getInstance()
.getTextEditor(editor));
return null;
});
await(
() ->
fx(
() ->
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
.getText()
.equals(bad)));
if (!document.snapshot().toCompletableFuture().get().text().equals(bad))
throw new AssertionError("Native fix Undo not exact");
edt(
() -> {
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
com.intellij.openapi.command.undo.UndoManager.getInstance(project)
.redo(
com.intellij
.openapi
.fileEditor
.impl
.text
.TextEditorProvider
.getInstance()
.getTextEditor(editor));
return null;
});
await(
() ->
fx(
() ->
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
.getText()
.equals(fixed)));
if (!document.snapshot().toCompletableFuture().get().text().equals(fixed))
throw new AssertionError("Native fix Redo not exact");
var current = document.snapshot().toCompletableFuture().get();
document
.apply(
new de.neitzel.fxdesigner.contracts.EditBatch(
current.revision(),
"Restore native fixture",
java.util.List.of(
new de.neitzel.fxdesigner.contracts.EditBatch.Replacement(
0, current.text().length(), before.text()))))
.toCompletableFuture()
.get();
await(
() ->
fx(
() ->
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
.getText()
.equals(before.text())));
System.out.println(
"PASS native source highlighting and Alt+Enter fix: exact ranges, source-preserving"
+ " transaction, Undo/Redo, stale action rejection and highlight withdrawal");
}
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()) {
checkNativeFixes(project, file, pane, document);
edt(
() -> {
FileEditorManager.getInstance(project)
.setSelectedEditor(file, "neitzel-fx-designer");
return null;
});
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) {
}
}
});
}
}