diff --git a/scripts/cn1playground/common/src/main/java/bsh/BSHAllocationExpression.java b/scripts/cn1playground/common/src/main/java/bsh/BSHAllocationExpression.java index eecb4c4c52..d8f02c3032 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/BSHAllocationExpression.java +++ b/scripts/cn1playground/common/src/main/java/bsh/BSHAllocationExpression.java @@ -305,6 +305,7 @@ private Object constructObject(Class type, Object[] args, // clean up, prevent memory leak This.registerConstructorContext(null, null); } + com.codenameone.playground.PlaygroundContext.notifyConstructed(obj); String className = type.getName(); // Is it an inner class? if ( className.indexOf("$") == -1 ) diff --git a/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java b/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java index 9dae57c98f..d9cae002f4 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java +++ b/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java @@ -1599,7 +1599,7 @@ private static void fillMethodIndex9(Map index) { index.put("com.codename1.xml.XMLParser", splitMembers("")); index.put("com.codename1.xml.XMLWriter", splitMembers("")); index.put("com.codenameone.playground.CN1Playground", splitMembers("destroy()getTheme()init(Object)runApp()start()stop()")); - index.put("com.codenameone.playground.PlaygroundContext", splitMembers("captureShownForm(Form)clearPreview()clearShownForm()getHostForm()getPreviewRoot()getShownForm()getTheme()log(String)refreshPreview()setTitle(String)debug(String)getCurrent()interceptMethodInvocation(Object, String, Object[])")); + index.put("com.codenameone.playground.PlaygroundContext", splitMembers("captureShownForm(Form)clearCreatedComponents()clearPreview()clearShownForm()getCreatedComponents()getFirstCreatedComponent()getFirstCreatedForm()getHostForm()getPreviewRoot()getShownForm()getTheme()log(String)recordCreatedComponent(Component)refreshPreview()setTitle(String)debug(String)getCurrent()interceptMethodInvocation(Object, String, Object[])notifyConstructed(Object)")); index.put("com.codenameone.playground.PlaygroundContext.Logger", splitMembers("log(String)")); index.put("com.codenameone.playground.PlaygroundLambdaBridge", splitMembers("lambda(Object[], String)lambda(String[], String)")); index.put("com.codenameone.playground.PlaygroundListenerBridge", splitMembers("actionListener(Object)networkListener(Object)onComplete(Object)runnable(Object)")); diff --git a/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java b/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java index 8cf4c71b68..534b28f9a1 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java +++ b/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java @@ -79,6 +79,12 @@ private static Object invokeStatic0(String name, Object[] safeArgs) throws Excep return com.codenameone.playground.PlaygroundContext.interceptMethodInvocation((java.lang.Object) adaptedArgs[0], (java.lang.String) adaptedArgs[1], (java.lang.Object[]) adaptedArgs[2]); } } + if ("notifyConstructed".equals(name)) { + if (matches(safeArgs, new Class[]{java.lang.Object.class}, false)) { + Object[] adaptedArgs = adaptArgs(safeArgs, new Class[]{java.lang.Object.class}, false); + com.codenameone.playground.PlaygroundContext.notifyConstructed((java.lang.Object) adaptedArgs[0]); return null; + } + } throw unsupportedStatic(com.codenameone.playground.PlaygroundContext.class, name, safeArgs); } @@ -175,6 +181,11 @@ private static Object invoke1(com.codenameone.playground.PlaygroundContext typed typedTarget.captureShownForm((com.codename1.ui.Form) adaptedArgs[0]); return null; } } + if ("clearCreatedComponents".equals(name)) { + if (safeArgs.length == 0) { + typedTarget.clearCreatedComponents(); return null; + } + } if ("clearPreview".equals(name)) { if (safeArgs.length == 0) { typedTarget.clearPreview(); return null; @@ -185,6 +196,21 @@ private static Object invoke1(com.codenameone.playground.PlaygroundContext typed typedTarget.clearShownForm(); return null; } } + if ("getCreatedComponents".equals(name)) { + if (safeArgs.length == 0) { + return typedTarget.getCreatedComponents(); + } + } + if ("getFirstCreatedComponent".equals(name)) { + if (safeArgs.length == 0) { + return typedTarget.getFirstCreatedComponent(); + } + } + if ("getFirstCreatedForm".equals(name)) { + if (safeArgs.length == 0) { + return typedTarget.getFirstCreatedForm(); + } + } if ("getHostForm".equals(name)) { if (safeArgs.length == 0) { return typedTarget.getHostForm(); @@ -211,6 +237,12 @@ private static Object invoke1(com.codenameone.playground.PlaygroundContext typed typedTarget.log((java.lang.String) adaptedArgs[0]); return null; } } + if ("recordCreatedComponent".equals(name)) { + if (matches(safeArgs, new Class[]{com.codename1.ui.Component.class}, false)) { + Object[] adaptedArgs = adaptArgs(safeArgs, new Class[]{com.codename1.ui.Component.class}, false); + typedTarget.recordCreatedComponent((com.codename1.ui.Component) adaptedArgs[0]); return null; + } + } if ("refreshPreview".equals(name)) { if (safeArgs.length == 0) { typedTarget.refreshPreview(); return null; diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java index d279d0a7d1..6245c882ba 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java @@ -1,11 +1,15 @@ package com.codenameone.playground; +import com.codename1.ui.CN; +import com.codename1.ui.Component; import com.codename1.ui.Container; import com.codename1.ui.Dialog; import com.codename1.ui.Form; -import com.codename1.ui.CN; import com.codename1.ui.util.Resources; +import java.util.ArrayList; +import java.util.List; + /** * Host objects and helpers exposed to user scripts. */ @@ -21,6 +25,9 @@ public interface Logger { private final Resources theme; private final Logger logger; private Form shownForm; + private final List createdComponents = new ArrayList(); + private Form firstCreatedForm; + private Component firstCreatedComponent; public PlaygroundContext(Form hostForm, Container previewRoot, Resources theme, Logger logger) { this.hostForm = hostForm; @@ -56,6 +63,17 @@ public static PlaygroundContext getCurrent() { public static void debug(String message) { } + public static void notifyConstructed(Object instance) { + if (!(instance instanceof Component)) { + return; + } + PlaygroundContext context = CURRENT.get(); + if (context == null) { + return; + } + context.recordCreatedComponent((Component) instance); + } + public static boolean interceptMethodInvocation(Object target, String methodName, Object[] args) { PlaygroundContext context = CURRENT.get(); if (context == null) { @@ -91,6 +109,37 @@ public void clearShownForm() { shownForm = null; } + public void recordCreatedComponent(Component component) { + if (component == null || component == hostForm || component == previewRoot) { + return; + } + if (firstCreatedComponent == null) { + firstCreatedComponent = component; + } + if (firstCreatedForm == null && component instanceof Form) { + firstCreatedForm = (Form) component; + } + createdComponents.add(component); + } + + public Component getFirstCreatedComponent() { + return firstCreatedComponent; + } + + public Form getFirstCreatedForm() { + return firstCreatedForm; + } + + public List getCreatedComponents() { + return createdComponents; + } + + public void clearCreatedComponents() { + createdComponents.clear(); + firstCreatedForm = null; + firstCreatedComponent = null; + } + public void clearPreview() { previewRoot.removeAll(); previewRoot.revalidate(); diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java index ea13893012..04f70299ee 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java @@ -86,6 +86,10 @@ RunResult run(String script, PlaygroundContext context) { try { Interpreter interpreter = new Interpreter(); bindGlobals(interpreter, context); + if (context != null) { + context.clearCreatedComponents(); + context.clearShownForm(); + } PlaygroundContext.pushCurrent(context); CN1LambdaSupport.pushInterpreter(interpreter); try { @@ -94,7 +98,7 @@ RunResult run(String script, PlaygroundContext context) { interpreter.eval(plan.typeDeclarations.get(i)); } Object result = interpreter.eval(plan.executableScript); - Component component = resolveComponent(interpreter, result, context); + Component component = resolveComponent(interpreter, result, context, plan.wrappedAsBuild); inlineMessages.add(new InlineMessage(0, "Preview updated.", "success")); return new RunResult(component, Collections.emptyList(), inlineMessages); } finally { @@ -158,7 +162,9 @@ private ScriptPlan adaptScript(String script) { List declarations = findTopLevelTypeDeclarations(normalized, importEnd); if (declarations.isEmpty()) { String wrapped = wrapLooseScript(normalized); - return new ScriptPlan(Collections.emptyList(), wrapped == null ? normalized : wrapped); + return new ScriptPlan(Collections.emptyList(), + wrapped == null ? normalized : wrapped, + wrapped != null); } String importSection = normalized.substring(packageEnd, importEnd); @@ -179,7 +185,9 @@ private ScriptPlan adaptScript(String script) { String rewritten = normalized.substring(0, importEnd) + remainingBody.toString(); String wrapped = wrapLooseScript(rewritten); - return new ScriptPlan(declarationScripts, wrapped == null ? rewritten : wrapped); + return new ScriptPlan(declarationScripts, + wrapped == null ? rewritten : wrapped, + wrapped != null); } private String rewriteClassModel(String script) { @@ -1989,8 +1997,15 @@ private String wrapLooseScript(String script) { String prefix = script.substring(0, bodyStart); String body = script.substring(bodyStart); String rewrittenBody = rewriteLooseScriptBody(body); + // Returning Object (rather than Component) lets the trailing + // expression be anything — a Form value, a void method call like + // `f.show();`, a primitive — without bsh complaining about the + // wrapper's declared return type. Whether the resulting value is + // actually previewable is decided in resolveComponent, which can + // also fall back to shownForm or constructed components when the + // trailing expression is non-component. return prefix - + "Component build(PlaygroundContext ctx) {\n" + + "Object build(PlaygroundContext ctx) {\n" + rewrittenBody + "\n}\n" + "build(ctx);"; @@ -2057,7 +2072,10 @@ private String rewriteLooseScriptBody(String body) { if (looksLikeReturnStatement(trimmed)) { return body; } - return "return " + trimmed + ";"; + if (looksLikeBareReference(trimmed)) { + return "return " + trimmed + ";"; + } + return body + "\nreturn null;"; } int statementStart = findStatementStart(body, lastSemi); String leading = body.substring(0, statementStart); @@ -2069,7 +2087,13 @@ private String rewriteLooseScriptBody(String body) { if (looksLikeReturnStatement(statement)) { return body; } - if (looksLikeReturnableExpression(statement)) { + // Only the trailing-bare-identifier convention (`myCmp;`) gets + // promoted to a return. Other trailing expressions — method + // calls like `f.show();`, arithmetic like `1 + 2;`, etc. — are + // left alone so the value-resolution fallback chain can kick in + // (shownForm, first constructed Form/Component) without bsh's + // method-body type check rejecting a void or primitive return. + if (looksLikeBareReference(statement)) { return leading + "return " + statement + ";" + trailing; } return body + "\nreturn null;"; @@ -2135,52 +2159,65 @@ private boolean looksLikeReturnStatement(String statement) { return startsWithWord(statement, skipWhitespace(statement, 0), "return"); } - private boolean looksLikeReturnableExpression(String statement) { - int start = skipWhitespace(statement, 0); - if (start >= statement.length()) { + /** True when the statement is a bare reference to a Component-like + * value: a plain identifier (`form`), a dotted access chain + * (`obj.field.value`), or an indexed access (`arr[0]`, + * `widgets["main"].child`). Method invocations like `f.show()` or + * arithmetic like `1 + 2` deliberately return false — those are + * left as plain statements so the value-fallback chain in + * resolveComponent can supply the preview component instead of + * forcing the build wrapper to return a void or primitive value. */ + private boolean looksLikeBareReference(String statement) { + int len = statement.length(); + int i = skipWhitespace(statement, 0); + if (i >= len) return false; + if (!isIdentifierStart(statement.charAt(i))) { return false; } - if (startsWithAnyWord(statement, start, "if", "for", "while", "switch", "try", "catch", "finally", - "do", "class", "interface", "enum", "throw", "break", "continue", "public", "private", - "protected", "static", "final", "abstract", "synchronized")) { + boolean expectIdentifier = true; + while (i < len) { + char ch = statement.charAt(i); + if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') { + i++; + continue; + } + if (expectIdentifier) { + if (!isIdentifierStart(ch)) return false; + while (i < len && isIdentifierPart(statement.charAt(i))) i++; + expectIdentifier = false; + continue; + } + if (ch == '.') { + i++; + expectIdentifier = true; + continue; + } + if (ch == '[') { + int close = findMatchingBracket(statement, i); + if (close < 0) return false; + i = close + 1; + continue; + } return false; } - return !containsTopLevelAssignment(statement); + return !expectIdentifier; } - private boolean containsTopLevelAssignment(String text) { + private int findMatchingBracket(String text, int openIndex) { int depth = 0; - for (int i = 0; i < text.length(); i++) { + for (int i = openIndex; i < text.length(); i++) { char ch = text.charAt(i); if (ch == '"' || ch == '\'') { i = skipQuoted(text, i); continue; } - if (startsLineComment(text, i)) { - i = skipLineComment(text, i); - continue; - } - if (startsBlockComment(text, i)) { - i = skipBlockComment(text, i); - continue; - } - if (ch == '(' || ch == '[' || ch == '{') { - depth++; - continue; - } - if (ch == ')' || ch == ']' || ch == '}') { + if (ch == '[') depth++; + else if (ch == ']') { depth--; - continue; - } - if (depth == 0 && ch == '=') { - char before = i > 0 ? text.charAt(i - 1) : 0; - char after = i + 1 < text.length() ? text.charAt(i + 1) : 0; - if (before != '=' && before != '!' && before != '<' && before != '>' && after != '=') { - return true; - } + if (depth == 0) return i; } } - return false; + return -1; } private boolean startsWithAnyWord(String text, int index, String... words) { @@ -2891,21 +2928,46 @@ private static final class AnonymousSam { } } - private Component resolveComponent(Interpreter interpreter, Object value, PlaygroundContext context) throws EvalError { - if (context != null && context.getShownForm() != null) { - return context.getShownForm(); - } - if (value != null && value != Primitive.VOID && value != Primitive.NULL) { - return coerceToComponent(value, context); + private Component resolveComponent(Interpreter interpreter, Object value, PlaygroundContext context, + boolean wrappedAsBuild) throws EvalError { + NameSpace namespace = interpreter.getNameSpace(); + + // Lifecycle scripts (init/start) keep their existing flow — they + // own the show()/return contract through runLifecycle(). + if (hasLifecycleMethods(namespace, context)) { + return runLifecycle(namespace, interpreter, context); } - NameSpace namespace = interpreter.getNameSpace(); - if (hasBuildMethod(namespace, context)) { + // Honour an explicit build(ctx) the user wrote. We skip this when + // we wrapped the loose script ourselves, since the wrapper has + // already executed build(ctx) once — calling it again would replay + // the whole body and double up its side effects. + if (!wrappedAsBuild && hasBuildMethod(namespace, context)) { return coerceToComponent(namespace.invokeMethod("build", new Object[]{context}, interpreter), context); } - if (hasLifecycleMethods(namespace, context)) { - return runLifecycle(namespace, interpreter, context); + // Simplified syntax fallback chain: + // 1. Trailing component identifier on the last line (the value + // returned by the implicit build wrapper). + // 2. Form passed to Form.show() during execution. + // 3. First Form constructed with `new` during execution. + // 4. First Component constructed with `new` during execution. + Component fromValue = componentFromValue(value); + if (fromValue != null) { + return fromValue; + } + if (context != null && context.getShownForm() != null) { + return context.getShownForm(); + } + Component fromCreated = firstCreatedComponent(context); + if (fromCreated != null) { + return fromCreated; + } + if (value != null && value != Primitive.VOID && value != Primitive.NULL) { + // Trailing expression evaluated to something that wasn't a + // Component (e.g. `42;`). Surface the existing diagnostic so + // the type mismatch is obvious. + return coerceToComponent(value, context); } throw new EvalError( @@ -2913,6 +2975,27 @@ private Component resolveComponent(Interpreter interpreter, Object value, Playgr null, null); } + private Component componentFromValue(Object value) { + if (value == null || value == Primitive.VOID || value == Primitive.NULL) { + return null; + } + if (value instanceof Component) { + return (Component) value; + } + return null; + } + + private Component firstCreatedComponent(PlaygroundContext context) { + if (context == null) { + return null; + } + Form form = context.getFirstCreatedForm(); + if (form != null) { + return form; + } + return context.getFirstCreatedComponent(); + } + private boolean hasLifecycleMethods(NameSpace namespace, PlaygroundContext context) { return hasMethod(namespace, "start") || hasInitMethod(namespace, context); @@ -3292,10 +3375,12 @@ private static final class TypeDeclarationBlock { private static final class ScriptPlan { final List typeDeclarations; final String executableScript; + final boolean wrappedAsBuild; - ScriptPlan(List typeDeclarations, String executableScript) { + ScriptPlan(List typeDeclarations, String executableScript, boolean wrappedAsBuild) { this.typeDeclarations = typeDeclarations; this.executableScript = executableScript; + this.wrappedAsBuild = wrappedAsBuild; } } } diff --git a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundPreviewResolutionHarness.java b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundPreviewResolutionHarness.java new file mode 100644 index 0000000000..15114d69a9 --- /dev/null +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundPreviewResolutionHarness.java @@ -0,0 +1,466 @@ +package com.codenameone.playground; + +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BorderLayout; + +import java.util.ArrayList; +import java.util.List; + +/** + * Harness covering the preview-component resolution rules for the + * simplified syntax: trailing identifier wins, otherwise a Form passed + * to show(), otherwise the first Form constructed during execution, + * otherwise the first Component constructed during execution. + * + * Also asserts that the lifecycle and explicit-build paths still work + * as before. + */ +public final class PlaygroundPreviewResolutionHarness { + private PlaygroundPreviewResolutionHarness() { + } + + private interface Check { + String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext context); + } + + private static final class Case { + final String name; + final String script; + final boolean expectFailure; + final Check check; + + Case(String name, String script, boolean expectFailure, Check check) { + this.name = name; + this.script = script; + this.expectFailure = expectFailure; + this.check = check; + } + } + + public static void main(String[] args) { + int exitCode = 0; + try { + List cases = new ArrayList(); + addCases(cases); + + List failures = new ArrayList(); + int passed = 0; + for (int i = 0; i < cases.size(); i++) { + Case testCase = cases.get(i); + Run run = runScript(testCase.script); + String failure = evaluate(testCase, run); + if (failure == null) { + passed++; + } else { + failures.add("[" + testCase.name + "] " + failure); + } + } + + System.out.println("Playground preview resolution: " + + passed + "/" + cases.size() + " passed"); + if (!failures.isEmpty()) { + System.out.println("Failures (" + failures.size() + "):"); + for (int i = 0; i < failures.size(); i++) { + System.out.println(" - " + failures.get(i)); + } + throw new IllegalStateException("Preview resolution mismatches: " + + failures.size() + " of " + cases.size()); + } + } catch (Throwable t) { + t.printStackTrace(System.err); + exitCode = 1; + } finally { + System.exit(exitCode); + } + } + + private static String evaluate(Case testCase, Run run) { + boolean failed = run.result.getComponent() == null + || hasErrorDiagnostic(run.result); + if (testCase.expectFailure) { + return failed ? null + : "expected failure but resolved to " + + describe(run.result.getComponent()); + } + if (failed) { + return "expected success but failed; component=" + + describe(run.result.getComponent()) + + ", diagnostic=" + firstDiagnostic(run.result); + } + if (testCase.check != null) { + return testCase.check.evaluate(run.result, run.context); + } + return null; + } + + private static boolean hasErrorDiagnostic(PlaygroundRunner.RunResult result) { + List diags = result.getDiagnostics(); + if (diags == null) return false; + for (int i = 0; i < diags.size(); i++) { + if ("error".equals(diags.get(i).severity)) return true; + } + return false; + } + + private static String firstDiagnostic(PlaygroundRunner.RunResult result) { + List diags = result.getDiagnostics(); + if (diags != null && !diags.isEmpty()) { + return diags.get(0).message; + } + List msgs = result.getMessages(); + if (msgs != null && !msgs.isEmpty()) { + return msgs.get(0).text; + } + return ""; + } + + private static String describe(Component component) { + if (component == null) return "null"; + return component.getClass().getSimpleName() + "@" + System.identityHashCode(component); + } + + private static void addCases(List cases) { + // 1. Trailing identifier (Container) wins. + cases.add(new Case( + "trailing-container-identifier", + "" + + "Container c = new Container();\n" + + "c;\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + return expectKind(result, Container.class); + } + })); + + // 2. Trailing identifier (Form) wins. + cases.add(new Case( + "trailing-form-identifier", + "" + + "Form f = new Form(\"Hi\");\n" + + "f;\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + return expectKind(result, Form.class); + } + })); + + // 3. Trailing identifier wins even when a different Form was created first. + cases.add(new Case( + "trailing-wins-over-earlier-form", + "" + + "Form first = new Form(\"first\");\n" + + "Form second = new Form(\"second\");\n" + + "second;\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Form expected = (Form) ctx.getCreatedComponents().get(1); + return expectIdentity(result, expected, "second form"); + } + })); + + // 4. Trailing Container wins even when a Form was shown via show(). + cases.add(new Case( + "trailing-container-wins-over-show", + "" + + "Form a = new Form(\"a\");\n" + + "a.show();\n" + + "Container c = new Container();\n" + + "c;\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + return expectKind(result, Container.class); + } + })); + + // 5. show() wins when there is no trailing identifier. + cases.add(new Case( + "show-without-trailing", + "" + + "Form f = new Form(\"shown\");\n" + + "f.show();\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Form shown = ctx.getShownForm(); + if (shown == null) { + return "context did not capture shownForm"; + } + return expectIdentity(result, shown, "shown form"); + } + })); + + // 6. show() wins over later-created components when no trailing identifier. + cases.add(new Case( + "show-wins-over-later-component", + "" + + "Form f = new Form(\"shown\");\n" + + "f.show();\n" + + "Container ignored = new Container();\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Form shown = ctx.getShownForm(); + if (shown == null) { + return "context did not capture shownForm"; + } + return expectIdentity(result, shown, "shown form"); + } + })); + + // 7. No show, no trailing identifier: first created Form wins, even + // when a Container was created earlier. + cases.add(new Case( + "first-created-form-wins-over-earlier-container", + "" + + "Container early = new Container();\n" + + "Form mid = new Form(\"mid\");\n" + + "Container late = new Container();\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Form expected = ctx.getFirstCreatedForm(); + if (expected == null) { + return "no form recorded as first-created"; + } + return expectIdentity(result, expected, "first-created form"); + } + })); + + // 8. Two forms, no show, no trailing: the first one wins. + cases.add(new Case( + "first-of-two-forms", + "" + + "Form a = new Form(\"a\");\n" + + "Form b = new Form(\"b\");\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Form a = (Form) ctx.getCreatedComponents().get(0); + return expectIdentity(result, a, "first form"); + } + })); + + // 9. No Form at all — first created Component wins. + cases.add(new Case( + "first-created-component", + "" + + "Container outer = new Container();\n" + + "Container inner = new Container();\n" + + "outer.add(inner);\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Component first = ctx.getCreatedComponents().get(0); + return expectIdentity(result, first, "first-created component"); + } + })); + + // 10. Single component, no trailing: that single component wins. + cases.add(new Case( + "single-component-no-trailing", + "Button b = new Button(\"hi\");\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Component first = ctx.getFirstCreatedComponent(); + return expectIdentity(result, first, "the only created component"); + } + })); + + // 11. Empty body — no resolution possible, must fail. + cases.add(new Case( + "empty-body-fails", + "", + true, + null)); + + // 12. Imports only — no resolution possible. + cases.add(new Case( + "imports-only-fails", + "" + + "import com.codename1.ui.Form;\n" + + "import com.codename1.ui.Container;\n", + true, + null)); + + // 13. Trailing non-component expression with no fallback — must fail. + cases.add(new Case( + "trailing-non-component-no-fallback", + "" + + "int x = 41;\n" + + "x + 1;\n", + true, + null)); + + // 14. Trailing non-component expression but a Form was shown — show wins. + cases.add(new Case( + "trailing-non-component-falls-back-to-show", + "" + + "Form f = new Form(\"shown\");\n" + + "f.show();\n" + + "1 + 2;\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Form shown = ctx.getShownForm(); + if (shown == null) { + return "context did not capture shownForm"; + } + return expectIdentity(result, shown, "shown form"); + } + })); + + // 15. Trailing non-component expression with no show, but a Form created — first form wins. + cases.add(new Case( + "trailing-non-component-falls-back-to-first-form", + "" + + "Form f = new Form(\"f\");\n" + + "1 + 2;\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + Form expected = ctx.getFirstCreatedForm(); + return expectIdentity(result, expected, "first-created form"); + } + })); + + // 16. show() called on a Form that is not the host or preview. + // Make sure the host form does not leak into createdComponents. + cases.add(new Case( + "host-form-not-in-created-components", + "Container c = new Container();\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + List created = ctx.getCreatedComponents(); + for (int i = 0; i < created.size(); i++) { + if (created.get(i) == ctx.getHostForm()) { + return "hostForm leaked into createdComponents"; + } + if (created.get(i) == ctx.getPreviewRoot()) { + return "previewRoot leaked into createdComponents"; + } + } + return null; + } + })); + + // 17. Existing build(ctx) path — user-defined build still drives + // the preview, fallback chain doesn't override it. + cases.add(new Case( + "user-defined-build-method", + "" + + "Component build(PlaygroundContext ctx) {\n" + + " Container c = new Container();\n" + + " Form spurious = new Form(\"ignored\");\n" + + " return c;\n" + + "}\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + return expectKind(result, Container.class); + } + })); + + // 18. Lifecycle path still works (init + start showing a Form). + cases.add(new Case( + "lifecycle-init-start", + "" + + "void init(Object arg) {\n" + + "}\n" + + "void start() {\n" + + " Form f = new Form(\"life\");\n" + + " f.show();\n" + + "}\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + return expectKind(result, Form.class); + } + })); + + // 19. Lifecycle path: start() returning a Component. + cases.add(new Case( + "lifecycle-start-returns-component", + "" + + "Component start() {\n" + + " Container c = new Container();\n" + + " return c;\n" + + "}\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + return expectKind(result, Container.class); + } + })); + + // 20. Trailing identifier referencing a component created inside a helper method. + cases.add(new Case( + "trailing-from-helper-method", + "" + + "Container makeContainer() {\n" + + " return new Container();\n" + + "}\n" + + "Container c = makeContainer();\n" + + "c;\n", + false, + new Check() { + public String evaluate(PlaygroundRunner.RunResult result, PlaygroundContext ctx) { + return expectKind(result, Container.class); + } + })); + } + + private static String expectKind(PlaygroundRunner.RunResult result, Class kind) { + Component c = result.getComponent(); + if (c == null) { + return "expected " + kind.getSimpleName() + " but got null"; + } + if (!kind.isInstance(c)) { + return "expected " + kind.getSimpleName() + " but got " + c.getClass().getSimpleName(); + } + return null; + } + + private static String expectIdentity(PlaygroundRunner.RunResult result, Component expected, String label) { + Component actual = result.getComponent(); + if (actual != expected) { + return "expected " + label + " (" + describe(expected) + + ") but got " + describe(actual); + } + return null; + } + + private static final class Run { + final PlaygroundRunner.RunResult result; + final PlaygroundContext context; + + Run(PlaygroundRunner.RunResult result, PlaygroundContext context) { + this.result = result; + this.context = context; + } + } + + private static Run runScript(String script) { + Display.init(null); + Form host = new Form("Host", new BorderLayout()); + Container preview = new Container(new BorderLayout()); + host.add(BorderLayout.CENTER, preview); + host.show(); + + PlaygroundContext context = new PlaygroundContext(host, preview, null, new PlaygroundContext.Logger() { + public void log(String message) { + } + }); + PlaygroundRunner runner = new PlaygroundRunner(); + PlaygroundRunner.RunResult result = runner.run(script, context); + return new Run(result, context); + } +} diff --git a/scripts/cn1playground/tools/run-playground-smoke-tests.sh b/scripts/cn1playground/tools/run-playground-smoke-tests.sh index 07d8077fa3..c08e5ee5b0 100644 --- a/scripts/cn1playground/tools/run-playground-smoke-tests.sh +++ b/scripts/cn1playground/tools/run-playground-smoke-tests.sh @@ -39,3 +39,6 @@ mvn -f common/pom.xml -DskipTests org.codehaus.mojo:exec-maven-plugin:3.0.0:java mvn -f common/pom.xml -DskipTests org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ -Dexec.classpathScope=test \ -Dexec.mainClass=com.codenameone.playground.PlaygroundLayoutHarness +mvn -f common/pom.xml -DskipTests org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ + -Dexec.classpathScope=test \ + -Dexec.mainClass=com.codenameone.playground.PlaygroundPreviewResolutionHarness