Skip to content
Open
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
152 changes: 152 additions & 0 deletions .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- 'scripts/build-ios-app.sh'
- 'scripts/run-ios-ui-tests.sh'
- 'scripts/run-watch-ui-tests.sh'
- 'scripts/run-tv-ui-tests.sh'
- 'scripts/run-ios-native-tests.sh'
- 'scripts/ios/notification-tests/native-tests/**'
- 'scripts/ios/notification-tests/install-native-notification-tests.sh'
Expand All @@ -19,6 +20,7 @@ on:
- 'scripts/ios/screenshots/**'
- 'scripts/ios/screenshots-metal/**'
- 'scripts/ios/screenshots-watch/**'
- 'scripts/ios/screenshots-tv/**'
- 'scripts/templates/**'
- '!scripts/templates/**/*.md'
- 'CodenameOne/src/**'
Expand All @@ -44,6 +46,7 @@ on:
- 'scripts/build-ios-app.sh'
- 'scripts/run-ios-ui-tests.sh'
- 'scripts/run-watch-ui-tests.sh'
- 'scripts/run-tv-ui-tests.sh'
- 'scripts/run-ios-native-tests.sh'
- 'scripts/ios/notification-tests/native-tests/**'
- 'scripts/ios/notification-tests/install-native-notification-tests.sh'
Expand All @@ -53,6 +56,7 @@ on:
- 'scripts/ios/screenshots/**'
- 'scripts/ios/screenshots-metal/**'
- 'scripts/ios/screenshots-watch/**'
- 'scripts/ios/screenshots-tv/**'
- 'scripts/templates/**'
- '!scripts/templates/**/*.md'
- 'CodenameOne/src/**'
Expand Down Expand Up @@ -653,3 +657,151 @@ jobs:
path: artifacts
if-no-files-found: warn
retention-days: 14

build-ios-tv:
# Native Apple TV (tvOS) screenshot pipeline. The tvOS slice auto-enables
# from codename1.tvMain in the sample, so build-ios-app.sh generates the
# <Main>TV target alongside the iOS app. tvOS reuses the iOS UIApplicationMain
# entry and the Metal renderer (no OpenGL ES on tvOS); this job builds the TV
# target for the appletvsimulator, renders the cn1ss suite and streams frames
# to the same Cn1ssScreenshotServer the iOS/watch jobs use, comparing against
# scripts/ios/screenshots-tv.
#
# NON-BLOCKING (continue-on-error) until the tvOS native slice compiles
# end-to-end and the golden set is seeded -- see
# Ports/iOSPort/nativeSources/TVOS_PORT.md. Flip continue-on-error off (and
# drop CN1SS_ALLOWED_MISSING) once the goldens land, matching build-ios-watch.
needs: build-port
continue-on-error: true
permissions:
contents: read
pull-requests: write
issues: write
runs-on: macos-15
timeout-minutes: 60
concurrency:
group: mac-ci-${{ github.workflow }}-tv-${{ github.ref_name }}
cancel-in-progress: true

env:
GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}

steps:
- uses: actions/checkout@v6

- name: Cache CocoaPods and user gems
uses: actions/cache@v5
with:
path: |
~/.gem
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }}
restore-keys: |
${{ runner.os }}-pods-v1-

- name: Ensure CocoaPods tooling
run: |
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
set -euo pipefail
if ! command -v ruby >/dev/null; then
echo "ruby not found"; exit 1
fi
GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')"
export PATH="$GEM_USER_DIR/bin:$PATH"
if ! command -v pod >/dev/null 2>&1; then
gem install cocoapods xcodeproj --no-document --user-install
fi
pod --version

- name: Compute setup-workspace hash
id: setup_hash
run: |
set -euo pipefail
echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT"

- name: Set TMPDIR
run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV

- name: Cache codenameone-tools
uses: actions/cache@v5
with:
path: ${{ runner.temp }}/codenameone-tools
key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
${{ runner.os }}-cn1-tools-

- name: Cache Maven repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-

- name: Restore cn1-binaries cache
uses: actions/cache@v5
with:
path: ../cn1-binaries
key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
cn1-binaries-${{ runner.os }}-

- name: Restore built CN1 + iOS port artifacts
uses: actions/cache/restore@v4
with:
path: |
~/.m2/repository/com/codenameone
Themes
Ports/iOSPort/nativeSources
key: ${{ needs.build-port.outputs.cn1_built_cache_key }}
fail-on-cache-miss: true

- name: Build sample iOS app (generates the tvOS target)
id: build-ios-app
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30

- name: Ensure tvOS simulator runtime
run: |
set -euo pipefail
if ! xcrun simctl list runtimes available 2>/dev/null | grep -qi tvOS; then
echo "No tvOS runtime found; attempting download"
xcodebuild -downloadPlatform tvOS || true
fi
xcrun simctl list runtimes available 2>/dev/null | grep -i tvOS || true
xcrun simctl list devices available 2>/dev/null | grep -i "Apple TV" || true

- name: Run tvOS UI screenshot tests
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/tv-ui-tests
SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-tv
CN1SS_COMMENT_MARKER: '<!-- CN1SS_IOS_TV_COMMENT -->'
CN1SS_PREVIEW_SUBDIR: ios-tv
CN1SS_REPORT_TITLE: 'Apple TV (tvOS) screenshot updates'
CN1SS_SUCCESS_MESSAGE: '✅ Native Apple TV (tvOS, Metal) screenshot tests passed.'
CN1SS_COMMENT_LOG_PREFIX: '[run-tv-ui-tests]'
CN1SS_FAIL_ON_MISMATCH: '1'
# Tolerate a missing golden set while the tvOS native slice is being
# brought up; tighten to match build-ios-watch once goldens are seeded.
CN1SS_ALLOWED_MISSING: '9999'
run: |
set -euo pipefail
mkdir -p "${ARTIFACTS_DIR}"
echo "workspace='${{ steps.build-ios-app.outputs.workspace }}'"
echo "scheme='${{ steps.build-ios-app.outputs.scheme }}'"
./scripts/run-tv-ui-tests.sh \
"${{ steps.build-ios-app.outputs.workspace }}" \
"${{ steps.build-ios-app.outputs.scheme }}"
timeout-minutes: 45

- name: Upload tv artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: tv-ui-tests
path: artifacts
if-no-files-found: warn
retention-days: 14
11 changes: 11 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -5612,6 +5612,17 @@ public boolean isWatch() {
return false;
}

/// Indicates whether the application is running on a television form factor
/// (Apple TV / Android TV / Google TV). Notice that this is often a guess
/// derived from the device/skin metadata.
///
/// #### Returns
///
/// true if the device is assumed to be a TV
public boolean isTV() {
return false;
}

/// Returns true if the device has dialing capabilities
///
/// #### Returns
Expand Down
11 changes: 11 additions & 0 deletions CodenameOne/src/com/codename1/ui/CN.java
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,17 @@ public static boolean isWatch() {
return Display.impl.isWatch();
}

/// Indicates whether the application is running on a television form factor
/// (Apple TV / Android TV / Google TV). Notice that this is often a guess
/// derived from the device metadata.
///
/// #### Returns
///
/// true if the device is assumed to be a TV
public static boolean isTV() {
return Display.impl.isTV();
}

/// Returns the size of the desktop hosting the application window when running on a desktop platform.
///
/// #### Returns
Expand Down
11 changes: 11 additions & 0 deletions CodenameOne/src/com/codename1/ui/Display.java
Original file line number Diff line number Diff line change
Expand Up @@ -4391,6 +4391,17 @@ public boolean isWatch() {
return impl.isWatch();
}

/// Indicates whether the application is running on a television form factor
/// (Apple TV / Android TV / Google TV). Notice that this is often a guess
/// derived from the device metadata.
///
/// #### Returns
///
/// true if the device is assumed to be a TV
public boolean isTV() {
return impl.isTV();
}

/// Returns true if the device has dialing capabilities
///
/// #### Returns
Expand Down
3 changes: 2 additions & 1 deletion CodenameOne/src/com/codename1/ui/util/Resources.java
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,8 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
if ("HTML5".equals(platformName)) {
platformName = Display.getInstance().getProperty("HTML5.platformName", "mac");
}
String deviceType = Display.getInstance().isDesktop() ? "desktop" : Display.getInstance().isTablet() ? "tablet" : "phone";
Display d = Display.getInstance();
String deviceType = d.isDesktop() ? "desktop" : d.isTablet() ? "tablet" : d.isTV() ? "tv" : d.isWatch() ? "watch" : "phone";
String platformPrefix = "platform-" + platformName + "-";
String densityPrefix = "density-" + densityStr + "-";
String devicePrefix = "device-" + deviceType + "-";
Expand Down
5 changes: 5 additions & 0 deletions CodenameOneDesigner/build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,10 @@
<path path="${run.test.classpath}"/>
</classpath>
</java>
<java classname="com.codename1.designer.css.CSSDeviceFormFactorMediaQueryTest" fork="true" failonerror="true">
<classpath>
<path path="${run.test.classpath}"/>
</classpath>
</java>
</target>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.codename1.designer.css;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Hashtable;

/**
* Regression tests for device form-factor media queries compiling into
* {@code device-<type>-} prefixed UIIDs. The {@code tv} and {@code watch}
* form factors reuse the same generic {@code device-*} mechanism that already
* backs {@code device-desktop}/{@code device-tablet}/{@code device-phone};
* at runtime Resources.loadTheme selects the matching prefix via
* Display.isTV()/isWatch().
*/
public class CSSDeviceFormFactorMediaQueryTest {

public static void main(String[] args) throws Exception {
installHeadlessImplementation();
testTvMediaCompilesToDeviceTvUiids();
testWatchMediaCompilesToDeviceWatchUiids();
}

/**
* CSSTheme.load touches Display / Util, which need a CodenameOneImplementation.
* Install the same minimal headless stub the no-cef CLI uses (see
* NoCefCSSCLI#installHeadlessImplementation) so this test runs standalone.
*/
private static void installHeadlessImplementation() throws Exception {
HeadlessCssCompilerImplementation stub = new HeadlessCssCompilerImplementation();
Class<?> displayCls = Class.forName("com.codename1.ui.Display");
java.lang.reflect.Field implField = displayCls.getDeclaredField("impl");
implField.setAccessible(true);
if (implField.get(null) == null) {
implField.set(null, stub);
}
com.codename1.io.Util.setImplementation(stub);
}

private static void testTvMediaCompilesToDeviceTvUiids() throws Exception {
Path cssFile = Files.createTempFile("cn1-tv-media", ".css");
Path resFile = Files.createTempFile("cn1-tv-media", ".res");
try {
String css = "Button { color: #111111; }"
+ "@media device-tv {"
+ " Button { color: #00ff00; background-color: #001100; }"
+ "}";
Files.write(cssFile, css.getBytes(StandardCharsets.UTF_8));

CSSTheme theme = CSSTheme.load(cssFile.toUri().toURL());
theme.resourceFile = resFile.toFile();
theme.res = new com.codename1.ui.util.EditableResourcesForCSS(resFile.toFile());
theme.res.setTheme("Theme", new Hashtable());
theme.updateResources();

Hashtable themeProps = theme.res.getTheme("Theme");
assertEquals("111111", themeProps.get("Button.fgColor"), "Base Button fgColor");
assertEquals("00FF00", themeProps.get("device-tv-Button.fgColor"), "TV Button fgColor");
assertEquals("001100", themeProps.get("device-tv-Button.bgColor"), "TV Button bgColor");
} finally {
deleteIfExists(cssFile);
deleteIfExists(resFile);
}
}

private static void testWatchMediaCompilesToDeviceWatchUiids() throws Exception {
Path cssFile = Files.createTempFile("cn1-watch-media", ".css");
Path resFile = Files.createTempFile("cn1-watch-media", ".res");
try {
String css = "Button { color: #111111; }"
+ "@media device-watch {"
+ " Button { color: #2222ff; }"
+ "}";
Files.write(cssFile, css.getBytes(StandardCharsets.UTF_8));

CSSTheme theme = CSSTheme.load(cssFile.toUri().toURL());
theme.resourceFile = resFile.toFile();
theme.res = new com.codename1.ui.util.EditableResourcesForCSS(resFile.toFile());
theme.res.setTheme("Theme", new Hashtable());
theme.updateResources();

Hashtable themeProps = theme.res.getTheme("Theme");
assertEquals("111111", themeProps.get("Button.fgColor"), "Base Button fgColor");
assertEquals("2222FF", themeProps.get("device-watch-Button.fgColor"), "Watch Button fgColor");
} finally {
deleteIfExists(cssFile);
deleteIfExists(resFile);
}
}

private static void deleteIfExists(Path path) {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
}

private static void assertEquals(Object expected, Object actual, String message) {
if (expected == null ? actual != null : !expected.equals(actual)) {
throw new AssertionError(message + " expected=" + expected + " actual=" + actual);
}
}
}
Loading
Loading