Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Ports/JavaSE/build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,33 @@
<copy file="../../Themes/winTheme.res" tofile="build/classes/winTheme.res" />
<copy file="../../Themes/iOS7Theme.res" tofile="src/iOS7Theme.res" />
<copy file="../../Themes/iOS7Theme.res" tofile="build/classes/iOS7Theme.res" />
<!-- Bundle every shipped native theme so the simulator's
"Native Theme" menu has something to load when the user
picks iOS Modern / Android Material. failonerror is false
so the build still succeeds if scripts/build-native-themes.sh
hasn't generated the modern .res files yet; the maven pom
does the same. The legacy iPhoneTheme.res /
androidTheme.res / android_holo_light.res are also copied
so the menu works without forcing users to download
individual skins. -->
<copy todir="src" failonerror="false">
<fileset dir="../../Themes">
<include name="iOSModernTheme.res"/>
<include name="AndroidMaterialTheme.res"/>
<include name="iPhoneTheme.res"/>
<include name="androidTheme.res"/>
<include name="android_holo_light.res"/>
</fileset>
</copy>
<copy todir="build/classes" failonerror="false">
<fileset dir="../../Themes">
<include name="iOSModernTheme.res"/>
<include name="AndroidMaterialTheme.res"/>
<include name="iPhoneTheme.res"/>
<include name="androidTheme.res"/>
<include name="android_holo_light.res"/>
</fileset>
</copy>
<!--copy file="../../../codenameone-skins/iphone4_os7.skin" tofile="src/iphone4.skin" />
<copy file="../../../codenameone-skins/iphone5_os7.skin" tofile="src/iphone5.skin" />
<copy file="../../../codenameone-skins/xoom.skin" todir="src" / -->
Expand Down
334 changes: 223 additions & 111 deletions Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java

Large diffs are not rendered by default.

138 changes: 136 additions & 2 deletions scripts/javase/lib/SimulatorModeTestApp.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package com.codenameone.examples.javase.tests;

import com.codename1.impl.javase.JavaSEPort;
import com.codename1.ui.Button;
import com.codename1.ui.CN;
import com.codename1.ui.CheckBox;
import com.codename1.ui.Dialog;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.TextField;
import com.codename1.ui.Toolbar;
import com.codename1.io.ConnectionRequest;
import com.codename1.io.NetworkManager;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
* Small simulator app used by JavaSE integration tests.
Expand All @@ -19,11 +30,26 @@ public class SimulatorModeTestApp {
private Form current;

public void init(Object context) {
Toolbar.setGlobalToolbar(true);
boolean appThemeInstalled = false;
try {
Resources theme = Resources.openLayered("/theme");
UIManager.getInstance().setThemeProps(theme.getTheme(theme.getThemeResourceNames()[0]));
appThemeInstalled = true;
} catch (Exception ignored) {
// Fallback to default theme if test resource isn't available.
// No app theme bundled - fall through to native-theme-only
// path below so the screenshot still reflects the user's
// Native Theme menu pick.
}
if (!appThemeInstalled && Display.getInstance().hasNativeTheme()) {
// JavaSEPort.loadSkinFile has already cached the
// simulatorNativeTheme pick as nativeThemeRes, but nothing
// has pushed it into UIManager. Installing it directly
// lights up the iOS Modern / Android Material chrome in
// the captured screenshot; without this the form renders
// with the bare DefaultLookAndFeel and the screenshots
// for every theme look identical.
Display.getInstance().installNativeTheme();
}
}

Expand All @@ -34,14 +60,50 @@ public void start() {
}
String mode = System.getProperty("cn1.test.window.mode", "unknown");
Form form = new Form("JavaSE Simulator Test", new BorderLayout());
// The Toolbar carries most of the visual identity of a native
// theme (title bar color, font, status-bar treatment), so we
// need the global Toolbar in place for the screenshot to look
// different across themes.
Toolbar tb = form.getToolbar();
tb.setTitle("JavaSE Simulator Test");
tb.addMaterialCommandToSideMenu("Settings", com.codename1.ui.FontImage.MATERIAL_SETTINGS,
evt -> { /* placeholder */ });

form.add(BorderLayout.NORTH, new Label("Window mode: " + mode));

com.codename1.ui.Container body = new com.codename1.ui.Container(BoxLayout.y());
body.add(new Label("Robot validation baseline"));
body.add(new Button("Primary Action"));
Button primary = new Button("Primary Action");
primary.setUIID("RaisedButton");
body.add(primary);
Button dialogButton = new Button("Open Dialog");
dialogButton.addActionListener(evt -> Dialog.show("Mode", "Current mode: " + mode, "OK", null));
body.add(dialogButton);
// Add a few more theme-bearing components so the screenshot
// captures more than just two flat buttons. CheckBox + TextField
// pick up theme-specific colors and metrics that diverge
// visibly between iOS Modern, Android Material and the legacy
// pair.
body.add(new CheckBox("Enable feature"));
TextField tf = new TextField("", "Text field hint", 20, TextField.ANY);
body.add(tf);

// Native-theme verification: when the harness sets
// cn1.test.expectedNativeTheme, query JavaSEPort for the
// resource it actually loaded and emit a result line to both
// stdout and an optional sentinel file. The outer verifier reads
// either channel to assert the menu's simulatorNativeTheme
// preference was honored on simulator startup. We surface the
// result in the UI too so the screenshot of a failing case is
// self-explanatory.
String expectedNativeTheme = System.getProperty("cn1.test.expectedNativeTheme");
if (expectedNativeTheme != null && !expectedNativeTheme.isEmpty()) {
String diagnostic = reportNativeThemeResult(expectedNativeTheme);
Label diagLabel = new Label(diagnostic);
diagLabel.setUIID("Label");
body.add(diagLabel);
}

form.add(BorderLayout.CENTER, body);

current = form;
Expand Down Expand Up @@ -76,6 +138,78 @@ public void start() {
});
}

/**
* Reports whether the active native theme matches {@code expected}.
*
* <p>The current Simulator path stores the user's "Native Theme" menu
* choice in the {@code simulatorNativeTheme} preference, then reloads
* the simulator. On reload, {@code JavaSEPort.loadSkinFile} reads the
* preference, loads {@code /&lt;name&gt;.res} from the classpath, and
* caches the resolved key. We check three things:
*
* <ol>
* <li>{@code JavaSEPort.getCurrentSimulatorNativeTheme()} matches
* the expected key - this is what the simulator's loadSkinFile
* captured from the preference / build hints. A mismatch here
* means the preference wasn't honored or auto-resolution
* picked something else.</li>
* <li>{@code JavaSEPort.getNativeTheme()} returns non-null - the
* cached {@code Resources} is what {@code installNativeTheme}
* actually layers under the app theme. Null here means the
* .res lookup failed even though the override was set, which
* is what you'd hit if the modern themes weren't bundled.</li>
* <li>The expected {@code .res} is still present in the classpath
* so the test is exercising a real load path.</li>
* </ol>
*
* <p>The string returned is what we render on the form for the
* screenshot.
*/
private String reportNativeThemeResult(String expected) {
String resolvedKey = null;
boolean nativeResLoaded = false;
boolean expectedResPresent = false;
try {
resolvedKey = JavaSEPort.getCurrentSimulatorNativeTheme();
Resources nativeRes = JavaSEPort.getNativeTheme();
nativeResLoaded = nativeRes != null;
InputStream is = JavaSEPort.class.getResourceAsStream("/" + expected + ".res");
if (is != null) {
expectedResPresent = true;
try { is.close(); } catch (Exception ignored) { }
}
} catch (Exception ex) {
ex.printStackTrace();
}
boolean pass = expected.equals(resolvedKey) && nativeResLoaded && expectedResPresent;
String result = pass ? "PASS" : "FAIL";
String line = "[native-theme-test] result=" + result
+ " expected=" + expected
+ " resolvedKey=" + resolvedKey
+ " nativeResLoaded=" + nativeResLoaded
+ " expectedResPresent=" + expectedResPresent;
System.out.println(line);
// Optional sentinel file so harnesses that can't tail stdout in
// realtime can still read the result. The path is configured by
// the verifier; absence is fine.
String sentinel = System.getProperty("cn1.test.nativeThemeResultFile");
if (sentinel != null && !sentinel.isEmpty()) {
try {
Path p = Paths.get(sentinel);
Path parent = p.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
Files.write(p, (line + System.lineSeparator()).getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (Exception ex) {
ex.printStackTrace();
}
}
return "Native theme " + result + ": expected=" + expected
+ " loaded=" + (resolvedKey != null ? resolvedKey : "(none)");
}

public void stop() {
current = CN.getCurrentForm();
}
Expand Down
84 changes: 80 additions & 4 deletions scripts/javase/lib/SimulatorWindowModeVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public static void main(String[] args) {
Path projectDir = prepareCodenameOneSettings();
Path prefsRoot = configureSimulatorPreferences(parsed, projectDir);

// Native-theme scenarios write the result line to this
// sentinel so the verifier can read it after capturing the
// screenshot. Path lives in the temp project so different
// scenario runs don't trample each other.
Path nativeThemeSentinel = parsed.nativeTheme != null
? projectDir.resolve("native-theme-result.txt")
: null;

List<String> cmd = new ArrayList<String>();
String javaExec = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
cmd.add(javaExec);
Expand All @@ -55,6 +63,10 @@ public static void main(String[] args) {
cmd.add("-Dcn1.simulator.autoTestRecorder=true");
cmd.add("-Dcn1.simulator.autoTestRecorderRecord=true");
}
if (parsed.nativeTheme != null) {
cmd.add("-Dcn1.test.expectedNativeTheme=" + parsed.nativeTheme);
cmd.add("-Dcn1.test.nativeThemeResultFile=" + nativeThemeSentinel.toAbsolutePath());
}
if (parsed.skinPath != null && parsed.skinPath.length() > 0) {
cmd.add("-Dskin=" + parsed.skinPath);
cmd.add("-Ddskin=" + parsed.skinPath);
Expand All @@ -67,7 +79,17 @@ public static void main(String[] args) {
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.directory(projectDir.toFile());
pb.redirectErrorStream(true);
pb.inheritIO();
if (parsed.nativeTheme != null) {
// Capture output to a log so we can also confirm the
// result line on stdout if the sentinel file isn't
// written for some reason. Without the redirect the
// inherited stdout would be swallowed by the JVM and
// unavailable to the assertion below.
Path logPath = projectDir.resolve("simulator-output.log");
pb.redirectOutput(logPath.toFile());
} else {
pb.inheritIO();
}
child = pb.start();

waitForSimulatorWarmup(Duration.ofSeconds("network-monitor".equals(parsed.scenario) ? 12 : 8));
Expand All @@ -80,7 +102,13 @@ public static void main(String[] args) {
if (!ImageIO.write(image, "png", screenshotPath.toFile())) {
throw new AssertionError("No PNG writer available; screenshot was not written");
}
System.out.println("[javase-verifier] screenshot=" + screenshotPath + " mode=" + parsed.mode + " scenario=" + parsed.scenario);
System.out.println("[javase-verifier] screenshot=" + screenshotPath
+ " mode=" + parsed.mode + " scenario=" + parsed.scenario
+ (parsed.nativeTheme != null ? " nativeTheme=" + parsed.nativeTheme : ""));

if (parsed.nativeTheme != null) {
assertNativeThemeApplied(parsed, nativeThemeSentinel, projectDir);
}
exitCode = 0;
} catch (Throwable t) {
t.printStackTrace(System.err);
Expand Down Expand Up @@ -129,6 +157,40 @@ private static void validateScreenshotContent(BufferedImage image) {
}
}

/**
* Reads the result line written by {@code SimulatorModeTestApp}
* during simulator startup and verifies it reports a PASS. The
* sentinel file is preferred since it lands the line atomically;
* we fall back to the captured stdout log if the sentinel is
* missing (e.g. the app's init threw before the report ran).
*/
private static void assertNativeThemeApplied(Args args, Path sentinel, Path projectDir) throws Exception {
String line = null;
if (sentinel != null && Files.exists(sentinel)) {
line = new String(Files.readAllBytes(sentinel), StandardCharsets.UTF_8).trim();
}
if (line == null || line.isEmpty()) {
Path log = projectDir.resolve("simulator-output.log");
if (Files.exists(log)) {
for (String l : Files.readAllLines(log, StandardCharsets.UTF_8)) {
if (l.startsWith("[native-theme-test]")) {
line = l.trim();
break;
}
}
}
}
if (line == null || line.isEmpty()) {
throw new AssertionError("Native theme test produced no result line for "
+ args.nativeTheme + " (sentinel=" + sentinel + ")");
}
System.out.println("[javase-verifier] native-theme assertion: " + line);
if (!line.contains("result=PASS")) {
throw new AssertionError("Native theme " + args.nativeTheme
+ " was not loaded by the simulator: " + line);
}
}

private static Path prepareCodenameOneSettings() throws Exception {
Path tempProject = Files.createTempDirectory("cn1-javase-sim-project");
Path settings = tempProject.resolve("codenameone_settings.properties");
Expand All @@ -147,6 +209,14 @@ private static Path configureSimulatorPreferences(Args args, Path projectDir) th
System.setProperty("java.util.prefs.userRoot", prefsRoot.toAbsolutePath().toString());
Preferences prefs = Preferences.userNodeForPackage(com.codename1.impl.javase.JavaSEPort.class);
prefs.putBoolean("Portrait", !"landscape".equals(args.scenario));
if (args.nativeTheme != null) {
// Mirrors exactly what the "Native Theme" menu writes when
// the user picks an explicit theme - this is the lever the
// simulator menu acts on, so testing the lever directly
// covers the menu's reload path without driving the menu
// via AWT events.
prefs.put("simulatorNativeTheme", args.nativeTheme);
}
prefs.flush();
return prefsRoot;
}
Expand All @@ -157,13 +227,16 @@ private static final class Args {
final String simClasspath;
final String skinPath;
final String scenario;
final String nativeTheme;

private Args(String mode, String screenshotPath, String simClasspath, String skinPath, String scenario) {
private Args(String mode, String screenshotPath, String simClasspath, String skinPath, String scenario,
String nativeTheme) {
this.mode = mode;
this.screenshotPath = screenshotPath;
this.simClasspath = simClasspath;
this.skinPath = skinPath;
this.scenario = scenario;
this.nativeTheme = nativeTheme;
}

static Args parse(String[] args) {
Expand All @@ -172,6 +245,7 @@ static Args parse(String[] args) {
String simClasspath = null;
String skinPath = null;
String scenario = "default";
String nativeTheme = null;
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("--mode".equals(arg) && i + 1 < args.length) {
Expand All @@ -184,6 +258,8 @@ static Args parse(String[] args) {
skinPath = args[++i];
} else if ("--scenario".equals(arg) && i + 1 < args.length) {
scenario = args[++i];
} else if ("--native-theme".equals(arg) && i + 1 < args.length) {
nativeTheme = args[++i];
}
}
if (!"single".equals(mode) && !"multi".equals(mode)) {
Expand All @@ -195,7 +271,7 @@ static Args parse(String[] args) {
if (simClasspath == null || simClasspath.trim().isEmpty()) {
throw new IllegalArgumentException("--sim-classpath is required");
}
return new Args(mode, screenshot, simClasspath, skinPath, scenario);
return new Args(mode, screenshot, simClasspath, skinPath, scenario, nativeTheme);
}
}
}
Loading
Loading