diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index d4c0795cc5..d16ea2ff2e 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -7,6 +7,9 @@ on: - 'scripts/setup-workspace.sh' - 'scripts/build-ios-port.sh' - 'scripts/build-ios-app.sh' + - 'scripts/run-ios-ui-tests.sh' + - 'scripts/ios/tests/**' + - 'scripts/ios/screenshots/**' - 'scripts/templates/**' - '!scripts/templates/**/*.md' - 'CodenameOne/src/**' @@ -24,6 +27,9 @@ on: - 'scripts/setup-workspace.sh' - 'scripts/build-ios-port.sh' - 'scripts/build-ios-app.sh' + - 'scripts/run-ios-ui-tests.sh' + - 'scripts/ios/tests/**' + - 'scripts/ios/screenshots/**' - 'scripts/templates/**' - '!scripts/templates/**/*.md' - 'CodenameOne/src/**' @@ -37,12 +43,20 @@ on: jobs: build-ios: + permissions: + contents: read + pull-requests: write + issues: write runs-on: macos-15 # pinning macos-15 avoids surprises during the cutover window timeout-minutes: 60 # allow enough time for dependency installs and full build concurrency: # ensure only one mac build runs at once group: mac-ci cancel-in-progress: false # queue new ones instead of canceling in-flight + env: + GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + steps: - uses: actions/checkout@v4 @@ -75,6 +89,24 @@ jobs: timeout-minutes: 25 - name: Build sample iOS app and compile workspace + id: build-ios-app run: ./scripts/build-ios-app.sh -q -DskipTests timeout-minutes: 30 + - name: Run iOS UI screenshot tests + env: + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts + run: | + mkdir -p "${ARTIFACTS_DIR}" + ./scripts/run-ios-ui-tests.sh "${{ steps.build-ios-app.outputs.workspace }}" "${{ steps.build-ios-app.outputs.app_bundle }}" + timeout-minutes: 25 + + - name: Upload iOS artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-ui-tests + path: artifacts + if-no-files-found: warn + retention-days: 14 + diff --git a/scripts/android/tests/PostPrComment.java b/scripts/android/tests/PostPrComment.java index dab82e1d1d..943e50bebb 100644 --- a/scripts/android/tests/PostPrComment.java +++ b/scripts/android/tests/PostPrComment.java @@ -22,8 +22,10 @@ import java.util.regex.Pattern; public class PostPrComment { - private static final String MARKER = ""; - private static final String LOG_PREFIX = "[run-android-instrumentation-tests]"; + private static final String DEFAULT_MARKER = ""; + private static final String DEFAULT_LOG_PREFIX = "[run-android-instrumentation-tests]"; + private static String marker = DEFAULT_MARKER; + private static String logPrefix = DEFAULT_LOG_PREFIX; public static void main(String[] args) throws Exception { int exitCode = execute(args); @@ -35,6 +37,9 @@ private static int execute(String[] args) throws Exception { if (arguments == null) { return 2; } + marker = arguments.marker != null ? arguments.marker : DEFAULT_MARKER; + logPrefix = arguments.logPrefix != null ? arguments.logPrefix : DEFAULT_LOG_PREFIX; + Path bodyPath = arguments.body; if (!Files.isRegularFile(bodyPath)) { return 0; @@ -44,10 +49,10 @@ private static int execute(String[] args) throws Exception { if (body.isEmpty()) { return 0; } - if (!body.contains(MARKER)) { - body = body.stripTrailing() + "\n\n" + MARKER; + if (!body.contains(marker)) { + body = body.stripTrailing() + "\n\n" + marker; } - String bodyWithoutMarker = body.replace(MARKER, "").trim(); + String bodyWithoutMarker = body.replace(marker, "").trim(); if (bodyWithoutMarker.isEmpty()) { return 0; } @@ -154,7 +159,7 @@ private static CommentContext locateExistingComment(HttpClient client, Map commentMap = JsonUtil.asObject(comment); String bodyText = stringValue(commentMap.get("body"), ""); - if (bodyText.contains(MARKER)) { + if (bodyText.contains(marker)) { existingComment = commentMap; Map user = JsonUtil.asObject(commentMap.get("user")); String login = stringValue(user.get("login"), null); @@ -181,7 +186,7 @@ private static CommentContext locateExistingComment(HttpClient client, Map java.util.stream.Stream.of(e.getKey(), e.getValue())).toArray(String[]::new)) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.stringify(Map.of("body", MARKER)))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.stringify(Map.of("body", marker)))) .build(); HttpResponse createResponse = client.send(createRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (createResponse.statusCode() >= 200 && createResponse.statusCode() < 300) { @@ -406,11 +411,11 @@ private static String stringValue(Object value, String fallback) { } private static void log(String message) { - System.out.println(LOG_PREFIX + " " + message); + System.out.println(logPrefix + " " + message); } private static void err(String message) { - System.err.println(LOG_PREFIX + " " + message); + System.err.println(logPrefix + " " + message); } private record CommentContext(long commentId, boolean createdPlaceholder) { @@ -422,15 +427,21 @@ private record AttachmentReplacement(String body, List missing) { private static class Arguments { final Path body; final Path previewDir; + final String marker; + final String logPrefix; - private Arguments(Path body, Path previewDir) { + private Arguments(Path body, Path previewDir, String marker, String logPrefix) { this.body = body; this.previewDir = previewDir; + this.marker = marker; + this.logPrefix = logPrefix; } static Arguments parse(String[] args) { Path body = null; Path previewDir = null; + String marker = null; + String logPrefix = null; for (int i = 0; i < args.length; i++) { String arg = args[i]; switch (arg) { @@ -448,6 +459,20 @@ static Arguments parse(String[] args) { } previewDir = Path.of(args[i]); } + case "--marker" -> { + if (++i >= args.length) { + System.err.println("Missing value for --marker"); + return null; + } + marker = args[i]; + } + case "--log-prefix" -> { + if (++i >= args.length) { + System.err.println("Missing value for --log-prefix"); + return null; + } + logPrefix = args[i]; + } default -> { System.err.println("Unknown argument: " + arg); return null; @@ -458,7 +483,7 @@ static Arguments parse(String[] args) { System.err.println("--body is required"); return null; } - return new Arguments(body, previewDir); + return new Arguments(body, previewDir, marker, logPrefix); } } diff --git a/scripts/android/tests/RenderScreenshotReport.java b/scripts/android/tests/RenderScreenshotReport.java index 791f62451f..0276ec5748 100644 --- a/scripts/android/tests/RenderScreenshotReport.java +++ b/scripts/android/tests/RenderScreenshotReport.java @@ -8,7 +8,9 @@ import java.util.Map; public class RenderScreenshotReport { - private static final String MARKER = ""; + private static final String DEFAULT_MARKER = ""; + private static final String DEFAULT_TITLE = "Android screenshot updates"; + private static final String DEFAULT_SUCCESS_MESSAGE = "✅ Native Android screenshot tests passed."; public static void main(String[] args) throws Exception { Arguments arguments = Arguments.parse(args); @@ -24,7 +26,11 @@ public static void main(String[] args) throws Exception { String text = Files.readString(comparePath, StandardCharsets.UTF_8); Object parsed = JsonUtil.parse(text); Map data = JsonUtil.asObject(parsed); - SummaryAndComment output = buildSummaryAndComment(data); + String marker = arguments.marker != null ? arguments.marker : DEFAULT_MARKER; + String title = arguments.title != null ? arguments.title : DEFAULT_TITLE; + String successMessage = arguments.successMessage != null ? arguments.successMessage : DEFAULT_SUCCESS_MESSAGE; + + SummaryAndComment output = buildSummaryAndComment(data, title, marker, successMessage); writeLines(arguments.summaryOut, output.summaryLines); writeLines(arguments.commentOut, output.commentLines); } @@ -43,7 +49,7 @@ private static void writeLines(Path path, List lines) throws IOException Files.writeString(path, sb.toString(), StandardCharsets.UTF_8); } - private static SummaryAndComment buildSummaryAndComment(Map data) { + private static SummaryAndComment buildSummaryAndComment(Map data, String title, String marker, String successMessage) { List summaryLines = new ArrayList<>(); List commentLines = new ArrayList<>(); Object resultsObj = data.get("results"); @@ -114,8 +120,10 @@ private static SummaryAndComment buildSummaryAndComment(Map data } if (!commentEntries.isEmpty()) { - commentLines.add("### Android screenshot updates"); - commentLines.add(""); + if (title != null && !title.isEmpty()) { + commentLines.add("### " + title); + commentLines.add(""); + } for (Map entry : commentEntries) { String test = stringValue(entry.get("test"), ""); String status = stringValue(entry.get("status"), ""); @@ -127,11 +135,11 @@ private static SummaryAndComment buildSummaryAndComment(Map data if (!commentLines.isEmpty() && !commentLines.get(commentLines.size() - 1).isEmpty()) { commentLines.add(""); } - commentLines.add(MARKER); + commentLines.add(marker); } else { - commentLines.add("✅ Native Android screenshot tests passed."); + commentLines.add(successMessage != null ? successMessage : DEFAULT_SUCCESS_MESSAGE); commentLines.add(""); - commentLines.add(MARKER); + commentLines.add(marker); } return new SummaryAndComment(summaryLines, commentLines); } @@ -258,17 +266,26 @@ private static class Arguments { final Path compareJson; final Path commentOut; final Path summaryOut; + final String marker; + final String title; + final String successMessage; - private Arguments(Path compareJson, Path commentOut, Path summaryOut) { + private Arguments(Path compareJson, Path commentOut, Path summaryOut, String marker, String title, String successMessage) { this.compareJson = compareJson; this.commentOut = commentOut; this.summaryOut = summaryOut; + this.marker = marker; + this.title = title; + this.successMessage = successMessage; } static Arguments parse(String[] args) { Path compare = null; Path comment = null; Path summary = null; + String marker = null; + String title = null; + String successMessage = null; for (int i = 0; i < args.length; i++) { String arg = args[i]; switch (arg) { @@ -293,6 +310,27 @@ static Arguments parse(String[] args) { } summary = Path.of(args[i]); } + case "--marker" -> { + if (++i >= args.length) { + System.err.println("Missing value for --marker"); + return null; + } + marker = args[i]; + } + case "--title" -> { + if (++i >= args.length) { + System.err.println("Missing value for --title"); + return null; + } + title = args[i]; + } + case "--success-message" -> { + if (++i >= args.length) { + System.err.println("Missing value for --success-message"); + return null; + } + successMessage = args[i]; + } default -> { System.err.println("Unknown argument: " + arg); return null; @@ -303,7 +341,7 @@ static Arguments parse(String[] args) { System.err.println("--compare-json, --comment-out, and --summary-out are required"); return null; } - return new Arguments(compare, comment, summary); + return new Arguments(compare, comment, summary, marker, title, successMessage); } } } diff --git a/scripts/build-ios-app.sh b/scripts/build-ios-app.sh index 8c7a55b4d4..dc8d5b2335 100755 --- a/scripts/build-ios-app.sh +++ b/scripts/build-ios-app.sh @@ -174,6 +174,18 @@ fi bia_log "Found generated iOS project at $PROJECT_DIR" +UITEST_TEMPLATE="$SCRIPT_DIR/ios/tests/HelloCodenameOneUITests.swift.tmpl" +if [ -f "$UITEST_TEMPLATE" ]; then + IOS_UITEST_DIR="$(find "$PROJECT_DIR" -maxdepth 1 -type d -name '*UITests' -print -quit 2>/dev/null || true)" + if [ -n "$IOS_UITEST_DIR" ]; then + UI_TEST_DEST="$IOS_UITEST_DIR/templateUITests.swift" + bia_log "Installing UI test template at $UI_TEST_DEST" + cp "$UITEST_TEMPLATE" "$UI_TEST_DEST" + else + bia_log "Warning: Could not locate a *UITests target directory under $PROJECT_DIR; UI tests will be skipped" + fi +fi + if [ -f "$PROJECT_DIR/Podfile" ]; then bia_log "Installing CocoaPods dependencies" ( diff --git a/scripts/ios/screenshots/README.md b/scripts/ios/screenshots/README.md new file mode 100644 index 0000000000..698c4d16c5 --- /dev/null +++ b/scripts/ios/screenshots/README.md @@ -0,0 +1,3 @@ +# iOS screenshot baselines + +This directory stores the reference images that the CI iOS UI tests compare against. Add PNG files named after the test identifiers (e.g. `MainActivity.png`) once the first successful baseline has been captured. diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl new file mode 100644 index 0000000000..48af37e821 --- /dev/null +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -0,0 +1,55 @@ +import XCTest + +final class HelloCodenameOneUITests: XCTestCase { + private var app: XCUIApplication! + private var outputDirectory: URL! + + override func setUpWithError() throws { + continueAfterFailure = false + + app = XCUIApplication() + let environment = ProcessInfo.processInfo.environment + if let outputPath = environment["CN1SS_OUTPUT_DIR"], !outputPath.isEmpty { + outputDirectory = URL(fileURLWithPath: outputPath) + } else { + outputDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) + } + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + + app.launch() + } + + override func tearDownWithError() throws { + app?.terminate() + app = nil + } + + private func captureScreenshot(named name: String) throws { + let screenshot = XCUIScreen.main.screenshot() + let pngURL = outputDirectory.appendingPathComponent("\(name).png") + try screenshot.pngRepresentation.write(to: pngURL) + + let attachment = XCTAttachment(screenshot: screenshot) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + + func testMainScreenScreenshot() throws { + XCTAssertTrue(app.staticTexts["Hello Codename One"].waitForExistence(timeout: 10)) + sleep(1) + try captureScreenshot(named: "MainActivity") + } + + func testBrowserComponentScreenshot() throws { + let button = app.buttons["Open Browser Screen"] + XCTAssertTrue(button.waitForExistence(timeout: 10)) + button.tap() + + let webView = app.webViews.firstMatch + XCTAssertTrue(webView.waitForExistence(timeout: 15)) + sleep(2) + try captureScreenshot(named: "BrowserComponent") + } +} + diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh new file mode 100755 index 0000000000..bb32193649 --- /dev/null +++ b/scripts/run-ios-ui-tests.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# Run Codename One iOS UI tests on the simulator and compare screenshots +set -euo pipefail + +ri_log() { echo "[run-ios-ui-tests] $1"; } + +if [ $# -lt 1 ]; then + ri_log "Usage: $0 [app_bundle] [scheme]" >&2 + exit 2 +fi + +WORKSPACE_PATH="$1" +APP_BUNDLE_PATH="${2:-}" +REQUESTED_SCHEME="${3:-}" + +if [ ! -d "$WORKSPACE_PATH" ]; then + ri_log "Workspace not found at $WORKSPACE_PATH" >&2 + exit 3 +fi + +if [ -n "$APP_BUNDLE_PATH" ]; then + ri_log "Using simulator app bundle at $APP_BUNDLE_PATH" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +TMPDIR="${TMPDIR:-/tmp}"; TMPDIR="${TMPDIR%/}" +DOWNLOAD_DIR="${TMPDIR}/codenameone-tools" +ENV_DIR="$DOWNLOAD_DIR/tools" +ENV_FILE="$ENV_DIR/env.sh" + +ri_log "Loading workspace environment from $ENV_FILE" +[ -f "$ENV_FILE" ] || { ri_log "Missing env file: $ENV_FILE"; exit 3; } +# shellcheck disable=SC1090 +source "$ENV_FILE" + +if [ -z "${JAVA17_HOME:-}" ] || [ ! -x "$JAVA17_HOME/bin/java" ]; then + ri_log "JAVA17_HOME not set correctly" >&2 + exit 3 +fi +if ! command -v xcodebuild >/dev/null 2>&1; then + ri_log "xcodebuild not found" >&2 + exit 3 +fi +if ! command -v xcrun >/dev/null 2>&1; then + ri_log "xcrun not found" >&2 + exit 3 +fi + +JAVA17_BIN="$JAVA17_HOME/bin/java" + +ARTIFACTS_DIR="${ARTIFACTS_DIR:-${GITHUB_WORKSPACE:-$REPO_ROOT}/artifacts}" +mkdir -p "$ARTIFACTS_DIR" +TEST_LOG="$ARTIFACTS_DIR/xcodebuild-test.log" + +if [ ! -d "$ARTIFACTS_DIR" ]; then + ri_log "Failed to create artifacts directory at $ARTIFACTS_DIR" >&2 + exit 3 +fi + +if [ -z "$REQUESTED_SCHEME" ]; then + if [[ "$WORKSPACE_PATH" == *.xcworkspace ]]; then + REQUESTED_SCHEME="$(basename "$WORKSPACE_PATH" .xcworkspace)" + else + REQUESTED_SCHEME="$(basename "$WORKSPACE_PATH")" + fi +fi +SCHEME="$REQUESTED_SCHEME" +ri_log "Using scheme $SCHEME" + +SCREENSHOT_REF_DIR="$SCRIPT_DIR/ios/screenshots" +SCREENSHOT_TMP_DIR="$(mktemp -d "${TMPDIR}/cn1-ios-tests-XXXXXX" 2>/dev/null || echo "${TMPDIR}/cn1-ios-tests")" +SCREENSHOT_RAW_DIR="$SCREENSHOT_TMP_DIR/raw" +SCREENSHOT_PREVIEW_DIR="$SCREENSHOT_TMP_DIR/previews" +RESULT_BUNDLE="$SCREENSHOT_TMP_DIR/test-results.xcresult" +mkdir -p "$SCREENSHOT_RAW_DIR" "$SCREENSHOT_PREVIEW_DIR" + +export CN1SS_OUTPUT_DIR="$SCREENSHOT_RAW_DIR" +export CN1SS_PREVIEW_DIR="$SCREENSHOT_PREVIEW_DIR" + +SIM_DESTINATION="${IOS_SIM_DESTINATION:-platform=iOS Simulator,name=iPhone 15}" +ri_log "Running UI tests on destination '$SIM_DESTINATION'" + +DERIVED_DATA_DIR="$SCREENSHOT_TMP_DIR/derived" +rm -rf "$DERIVED_DATA_DIR" + +set -o pipefail +if ! xcodebuild \ + -workspace "$WORKSPACE_PATH" \ + -scheme "$SCHEME" \ + -sdk iphonesimulator \ + -configuration Debug \ + -destination "$SIM_DESTINATION" \ + -derivedDataPath "$DERIVED_DATA_DIR" \ + -resultBundlePath "$RESULT_BUNDLE" \ + test | tee "$TEST_LOG"; then + ri_log "STAGE:XCODE_TEST_FAILED -> See $TEST_LOG" + exit 10 +fi +set +o pipefail + +PNG_FILES=() +while IFS= read -r png; do + [ -n "$png" ] || continue + PNG_FILES+=("$png") +done < <(find "$SCREENSHOT_RAW_DIR" -type f -name '*.png' -print | sort) + +if [ "${#PNG_FILES[@]}" -eq 0 ]; then + ri_log "No screenshots produced under $SCREENSHOT_RAW_DIR" >&2 + exit 11 +fi + +ri_log "Captured ${#PNG_FILES[@]} screenshot(s)" + +declare -a COMPARE_ARGS=() +for png in "${PNG_FILES[@]}"; do + test_name="$(basename "$png")" + test_name="${test_name%.png}" + COMPARE_ARGS+=("--actual" "${test_name}=${png}") + cp "$png" "$ARTIFACTS_DIR/$(basename "$png")" 2>/dev/null || true + ri_log " -> Saved artifact copy for test '$test_name'" +fi + +COMPARE_JSON="$SCREENSHOT_TMP_DIR/screenshot-compare.json" +SUMMARY_FILE="$SCREENSHOT_TMP_DIR/screenshot-summary.txt" +COMMENT_FILE="$SCREENSHOT_TMP_DIR/screenshot-comment.md" + +ri_log "Running screenshot comparison" +"$JAVA17_BIN" "$SCRIPT_DIR/android/tests/ProcessScreenshots.java" \ + --reference-dir "$SCREENSHOT_REF_DIR" \ + --emit-base64 \ + --preview-dir "$SCREENSHOT_PREVIEW_DIR" \ + "${COMPARE_ARGS[@]}" > "$COMPARE_JSON" + +ri_log "Rendering screenshot summary and PR comment" +"$JAVA17_BIN" "$SCRIPT_DIR/android/tests/RenderScreenshotReport.java" \ + --compare-json "$COMPARE_JSON" \ + --comment-out "$COMMENT_FILE" \ + --summary-out "$SUMMARY_FILE" \ + --title "iOS screenshot updates" \ + --success-message "✅ Native iOS screenshot tests passed." \ + --marker "" + +if [ -s "$COMMENT_FILE" ]; then + ri_log "Prepared comment payload at $COMMENT_FILE" +fi + +cp -f "$COMPARE_JSON" "$ARTIFACTS_DIR/screenshot-compare.json" 2>/dev/null || true +cp -f "$SUMMARY_FILE" "$ARTIFACTS_DIR/screenshot-summary.txt" 2>/dev/null || true +cp -f "$COMMENT_FILE" "$ARTIFACTS_DIR/screenshot-comment-ios.md" 2>/dev/null || true +if [ -d "$SCREENSHOT_PREVIEW_DIR" ]; then + for preview in "$SCREENSHOT_PREVIEW_DIR"/*; do + [ -f "$preview" ] || continue + cp "$preview" "$ARTIFACTS_DIR/$(basename "$preview")" 2>/dev/null || true + done +fi + +COMMENT_RC=0 +if [ -s "$COMMENT_FILE" ]; then + ri_log "Posting PR comment" + if ! "$JAVA17_BIN" "$SCRIPT_DIR/android/tests/PostPrComment.java" \ + --body "$COMMENT_FILE" \ + --preview-dir "$SCREENSHOT_PREVIEW_DIR" \ + --marker "" \ + --log-prefix "[run-ios-ui-tests]"; then + COMMENT_RC=$? + ri_log "PR comment submission failed" + fi +else + ri_log "No PR comment generated" +fi + +if [ -d "$RESULT_BUNDLE" ]; then + rm -f "$ARTIFACTS_DIR/test-results.xcresult.zip" 2>/dev/null || true + zip -qr "$ARTIFACTS_DIR/test-results.xcresult.zip" "$RESULT_BUNDLE" +fi + +exit "$COMMENT_RC" diff --git a/scripts/templates/HelloCodenameOne.java.tmpl b/scripts/templates/HelloCodenameOne.java.tmpl index c1f42a6ee2..32656416ee 100644 --- a/scripts/templates/HelloCodenameOne.java.tmpl +++ b/scripts/templates/HelloCodenameOne.java.tmpl @@ -1,12 +1,18 @@ package @PACKAGE@; +import com.codename1.ui.Button; +import com.codename1.ui.BrowserComponent; +import com.codename1.ui.Container; import com.codename1.ui.Display; +import com.codename1.ui.FontImage; import com.codename1.ui.Form; import com.codename1.ui.Label; import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; public class @MAIN_NAME@ { private Form current; + private Form mainForm; public void init(Object context) { // No special initialization required for this sample @@ -17,9 +23,7 @@ public class @MAIN_NAME@ { current.show(); return; } - Form helloForm = new Form("Hello Codename One", new BorderLayout()); - helloForm.add(BorderLayout.CENTER, new Label("Hello Codename One")); - helloForm.show(); + showMainForm(); } public void stop() { @@ -29,4 +33,59 @@ public class @MAIN_NAME@ { public void destroy() { // Nothing to clean up for this sample } -} \ No newline at end of file + + private void showMainForm() { + if (mainForm == null) { + mainForm = new Form("Main Screen", new BorderLayout()); + + Container content = new Container(BoxLayout.y()); + content.getAllStyles().setBgColor(0x1f2937); + content.getAllStyles().setBgTransparency(255); + content.getAllStyles().setPadding(6, 6, 6, 6); + content.getAllStyles().setFgColor(0xf9fafb); + + Label heading = new Label("Hello Codename One"); + heading.getAllStyles().setFgColor(0x38bdf8); + heading.getAllStyles().setMargin(0, 4, 0, 0); + + Label body = new Label("Instrumentation main activity preview"); + body.getAllStyles().setFgColor(0xf9fafb); + + Button openBrowser = new Button("Open Browser Screen"); + openBrowser.addActionListener(evt -> showBrowserForm()); + + content.add(heading); + content.add(body); + content.add(openBrowser); + + mainForm.add(BorderLayout.CENTER, content); + } + current = mainForm; + mainForm.show(); + } + + private void showBrowserForm() { + Form browserForm = new Form("Browser Screen", new BorderLayout()); + + BrowserComponent browser = new BrowserComponent(); + browser.setPage(buildBrowserHtml(), null); + browserForm.add(BorderLayout.CENTER, browser); + browserForm.getToolbar().addMaterialCommandToLeftBar( + "Back", + FontImage.MATERIAL_ARROW_BACK, + evt -> showMainForm() + ); + + current = browserForm; + browserForm.show(); + } + + private String buildBrowserHtml() { + return "" + + "" + + "

Codename One

" + + "

BrowserComponent instrumentation test content.

"; + } +} diff --git a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj index 5de7bc3890..3681b58433 100644 --- a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj +++ b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj @@ -16,16 +16,25 @@ 0F634EA518E9ABBC002F3D1D /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F634E7C18E9ABBC002F3D1D /* UIKit.framework */; }; 0F634EA528E9ABBC002F3D1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F634E7C18EAABBC002F3D1D /* Images.xcassets */; }; + 0F634EC818E9ABBC002F3D1D /* templateUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F634EC718E9ABBC002F3D1D /* templateUITests.swift */; }; + 0F634ECA18E9ABBC002F3D1D /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F634EA218E9ABBC002F3D1D /* XCTest.framework */; }; **FILE_LIST**/* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 0F634EA618E9ABBC002F3D1D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0F634E6D18E9ABBC002F3D1D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 0F634E7418E9ABBC002F3D1D; - remoteInfo = template; - }; + 0F634EA618E9ABBC002F3D1D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0F634E6D18E9ABBC002F3D1D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0F634E7418E9ABBC002F3D1D; + remoteInfo = template; + }; + 0F634ED718E9ABBC002F3D1D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0F634E6D18E9ABBC002F3D1D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0F634E7418E9ABBC002F3D1D; + remoteInfo = template; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -38,6 +47,9 @@ 0F634E7E18E9ABBC002F3D1D /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = System/Library/Frameworks/GLKit.framework; sourceTree = SDKROOT; }; 0F634E8018E9ABBC002F3D1D /* OpenGLES.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGLES.framework; path = System/Library/Frameworks/OpenGLES.framework; sourceTree = SDKROOT; }; 0F634E7C18EAABBC002F3D1D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Images.xcassets"; sourceTree = ""; }; + 0F634EC518E9ABBC002F3D1D /* templateUITests.xctest */ = {isa = PBXFileReference; explicitFileType = com.apple.product-type.bundle.ui-testing; includeInIndex = 0; path = templateUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 0F634EC618E9ABBC002F3D1D /* templateUITests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "templateUITests-Info.plist"; sourceTree = ""; }; + 0F634EC718E9ABBC002F3D1D /* templateUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = templateUITests.swift; sourceTree = ""; }; **ACTUAL_FILES**/* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -53,38 +65,48 @@ ***FRAMEWORKS*** ); runOnlyForDeploymentPostprocessing = 0; }; - 0F634E9E18E9ABBC002F3D1D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0F634EA318E9ABBC002F3D1D /* XCTest.framework in Frameworks */, - 0F634EA518E9ABBC002F3D1D /* UIKit.framework in Frameworks */, - 0F634EA418E9ABBC002F3D1D /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 0F634E9E18E9ABBC002F3D1D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0F634EA318E9ABBC002F3D1D /* XCTest.framework in Frameworks */, + 0F634EA518E9ABBC002F3D1D /* UIKit.framework in Frameworks */, + 0F634EA418E9ABBC002F3D1D /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0F634ECF18E9ABBC002F3D1D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0F634ECA18E9ABBC002F3D1D /* XCTest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0F634E6C18E9ABBC002F3D1D = { - isa = PBXGroup; - children = ( - 0F634E8218E9ABBC002F3D1D /* template */, - 0F634EA818E9ABBC002F3D1D /* templateTests */, - 0F634E7718E9ABBC002F3D1D /* Frameworks */, - 0F634E7618E9ABBC002F3D1D /* Products */, - ); - sourceTree = ""; - }; - 0F634E7618E9ABBC002F3D1D /* Products */ = { - isa = PBXGroup; - children = ( - 0F634E7518E9ABBC002F3D1D /* template.app */, - 0F634EA118E9ABBC002F3D1D /* templateTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; + 0F634E6C18E9ABBC002F3D1D = { + isa = PBXGroup; + children = ( + 0F634E8218E9ABBC002F3D1D /* template */, + 0F634EA818E9ABBC002F3D1D /* templateTests */, + 0F634ED518E9ABBC002F3D1D /* templateUITests */, + 0F634E7718E9ABBC002F3D1D /* Frameworks */, + 0F634E7618E9ABBC002F3D1D /* Products */, + ); + sourceTree = ""; + }; + 0F634E7618E9ABBC002F3D1D /* Products */ = { + isa = PBXGroup; + children = ( + 0F634E7518E9ABBC002F3D1D /* template.app */, + 0F634EA118E9ABBC002F3D1D /* templateTests.xctest */, + 0F634EC518E9ABBC002F3D1D /* templateUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; 0F634E7718E9ABBC002F3D1D /* Frameworks */ = { isa = PBXGroup; children = ( @@ -115,13 +137,22 @@ name = "Supporting Files"; sourceTree = ""; }; - 0F634EA818E9ABBC002F3D1D /* templateTests */ = { - isa = PBXGroup; - children = ( - ); - path = templateTests; - sourceTree = ""; - }; + 0F634EA818E9ABBC002F3D1D /* templateTests */ = { + isa = PBXGroup; + children = ( + ); + path = templateTests; + sourceTree = ""; + }; + 0F634ED518E9ABBC002F3D1D /* templateUITests */ = { + isa = PBXGroup; + children = ( + 0F634EC718E9ABBC002F3D1D /* templateUITests.swift */, + 0F634EC618E9ABBC002F3D1D /* templateUITests-Info.plist */, + ); + path = templateUITests; + sourceTree = ""; + }; 0F634EA918E9ABBC002F3D1D /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -149,24 +180,42 @@ productReference = 0F634E7518E9ABBC002F3D1D /* template.app */; productType = "com.apple.product-type.application"; }; - 0F634EA018E9ABBC002F3D1D /* templateTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 0F634EB518E9ABBC002F3D1D /* Build configuration list for PBXNativeTarget "templateTests" */; - buildPhases = ( - 0F634E9D18E9ABBC002F3D1D /* Sources */, - 0F634E9E18E9ABBC002F3D1D /* Frameworks */, - 0F634E9F18E9ABBC002F3D1D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 0F634EA718E9ABBC002F3D1D /* PBXTargetDependency */, - ); - name = templateTests; - productName = templateTests; - productReference = 0F634EA118E9ABBC002F3D1D /* templateTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; + 0F634EA018E9ABBC002F3D1D /* templateTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0F634EB518E9ABBC002F3D1D /* Build configuration list for PBXNativeTarget "templateTests" */; + buildPhases = ( + 0F634E9D18E9ABBC002F3D1D /* Sources */, + 0F634E9E18E9ABBC002F3D1D /* Frameworks */, + 0F634E9F18E9ABBC002F3D1D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0F634EA718E9ABBC002F3D1D /* PBXTargetDependency */, + ); + name = templateTests; + productName = templateTests; + productReference = 0F634EA118E9ABBC002F3D1D /* templateTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 0F634EC418E9ABBC002F3D1D /* templateUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0F634ED218E9ABBC002F3D1D /* Build configuration list for PBXNativeTarget "templateUITests" */; + buildPhases = ( + 0F634ED018E9ABBC002F3D1D /* Sources */, + 0F634ECF18E9ABBC002F3D1D /* Frameworks */, + 0F634ED118E9ABBC002F3D1D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0F634ED618E9ABBC002F3D1D /* PBXTargetDependency */, + ); + name = templateUITests; + productName = templateUITests; + productReference = 0F634EC518E9ABBC002F3D1D /* templateUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -176,11 +225,14 @@ CLASSPREFIX = Ho; LastUpgradeCheck = 0500; ORGANIZATIONNAME = CodenameOne; - TargetAttributes = { - 0F634EA018E9ABBC002F3D1D = { - TestTargetID = 0F634E7418E9ABBC002F3D1D; - }; - }; + TargetAttributes = { + 0F634EA018E9ABBC002F3D1D = { + TestTargetID = 0F634E7418E9ABBC002F3D1D; + }; + 0F634EC418E9ABBC002F3D1D = { + TestTargetID = 0F634E7418E9ABBC002F3D1D; + }; + }; }; buildConfigurationList = 0F634E7018E9ABBC002F3D1D /* Build configuration list for PBXProject "template" */; compatibilityVersion = "Xcode 3.2"; @@ -194,10 +246,11 @@ productRefGroup = 0F634E7618E9ABBC002F3D1D /* Products */; projectDirPath = ""; projectRoot = ""; - targets = ( - 0F634E7418E9ABBC002F3D1D /* template */, - 0F634EA018E9ABBC002F3D1D /* templateTests */, - ); + targets = ( + 0F634E7418E9ABBC002F3D1D /* template */, + 0F634EA018E9ABBC002F3D1D /* templateTests */, + 0F634EC418E9ABBC002F3D1D /* templateUITests */, + ); }; /* End PBXProject section */ @@ -210,14 +263,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 0F634E9F18E9ABBC002F3D1D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 0F634EAD18E9ABBC002F3D1D /* InfoPlist.strings in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 0F634E9F18E9ABBC002F3D1D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0F634EAD18E9ABBC002F3D1D /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0F634ED118E9ABBC002F3D1D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -229,21 +289,34 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 0F634E9D18E9ABBC002F3D1D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; + 0F634E9D18E9ABBC002F3D1D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0F634ED018E9ABBC002F3D1D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0F634EC818E9ABBC002F3D1D /* templateUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 0F634EA718E9ABBC002F3D1D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 0F634E7418E9ABBC002F3D1D /* template */; - targetProxy = 0F634EA618E9ABBC002F3D1D /* PBXContainerItemProxy */; - }; + 0F634EA718E9ABBC002F3D1D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0F634E7418E9ABBC002F3D1D /* template */; + targetProxy = 0F634EA618E9ABBC002F3D1D /* PBXContainerItemProxy */; + }; + 0F634ED618E9ABBC002F3D1D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0F634E7418E9ABBC002F3D1D /* template */; + targetProxy = 0F634ED718E9ABBC002F3D1D /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -405,25 +478,59 @@ }; name = Debug; }; - 0F634EB718E9ABBC002F3D1D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ARCHS = armv7; - BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/template.app/template-src"; - FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(inherited)", - "$(DEVELOPER_FRAMEWORKS_DIR)", - ); - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "template-src/template-Prefix.pch"; - INFOPLIST_FILE = "templateTests/templateTests-Info.plist"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUNDLE_LOADER)"; - WRAPPER_EXTENSION = xctest; - }; - name = Release; - }; + 0F634EB718E9ABBC002F3D1D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = armv7; + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/template.app/template-src"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "template-src/template-Prefix.pch"; + INFOPLIST_FILE = "templateTests/templateTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 0F634ED318E9ABBC002F3D1D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = "templateUITests/templateUITests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codenameone.templateUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = template; + }; + name = Debug; + }; + 0F634ED418E9ABBC002F3D1D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = "templateUITests/templateUITests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.codenameone.templateUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = template; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -444,14 +551,23 @@ ); defaultConfigurationIsVisible = 0; }; - 0F634EB518E9ABBC002F3D1D /* Build configuration list for PBXNativeTarget "templateTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 0F634EB618E9ABBC002F3D1D /* Debug */, - 0F634EB718E9ABBC002F3D1D /* Release */, - ); - defaultConfigurationIsVisible = 0; - }; + 0F634EB518E9ABBC002F3D1D /* Build configuration list for PBXNativeTarget "templateTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0F634EB618E9ABBC002F3D1D /* Debug */, + 0F634EB718E9ABBC002F3D1D /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; + 0F634ED218E9ABBC002F3D1D /* Build configuration list for PBXNativeTarget "templateUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0F634ED318E9ABBC002F3D1D /* Debug */, + 0F634ED418E9ABBC002F3D1D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 0F634E6D18E9ABBC002F3D1D /* Project object */; diff --git a/vm/ByteCodeTranslator/src/template/template.xcodeproj/xcuserdata/user2.xcuserdatad/xcschemes/template.xcscheme b/vm/ByteCodeTranslator/src/template/template.xcodeproj/xcuserdata/user2.xcuserdatad/xcschemes/template.xcscheme index 92280a23a4..bb3521aed4 100644 --- a/vm/ByteCodeTranslator/src/template/template.xcodeproj/xcuserdata/user2.xcuserdatad/xcschemes/template.xcscheme +++ b/vm/ByteCodeTranslator/src/template/template.xcodeproj/xcuserdata/user2.xcuserdatad/xcschemes/template.xcscheme @@ -38,6 +38,16 @@ ReferencedContainer = "container:template.xcodeproj"> + + + + + + + +CFBundleDevelopmentRegion +en +CFBundleExecutable +$(EXECUTABLE_NAME) +CFBundleIdentifier +$(PRODUCT_BUNDLE_IDENTIFIER) +CFBundleInfoDictionaryVersion +6.0 +CFBundleName +$(PRODUCT_NAME) +CFBundlePackageType +BNDL +CFBundleShortVersionString +1.0 +CFBundleVersion +1 + + diff --git a/vm/ByteCodeTranslator/src/template/templateUITests/templateUITests.swift b/vm/ByteCodeTranslator/src/template/templateUITests/templateUITests.swift new file mode 100644 index 0000000000..2adbb1aacd --- /dev/null +++ b/vm/ByteCodeTranslator/src/template/templateUITests/templateUITests.swift @@ -0,0 +1,9 @@ +import XCTest + +final class templateUITests: XCTestCase { + func testExample() { + let app = XCUIApplication() + app.launch() + XCTAssertTrue(app.exists) + } +}