Skip to content

Enable modern native themes on the JavaScript port#5054

Merged
shai-almog merged 17 commits into
masterfrom
jsport-modern-themes
May 30, 2026
Merged

Enable modern native themes on the JavaScript port#5054
shai-almog merged 17 commits into
masterfrom
jsport-modern-themes

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

  • Adds opt-in support for the Liquid Glass / Material 3 modern themes on the JS port via the same nativeTheme / ios.themeMode / and.themeMode hints already supported by the iOS / Android ports. The new auto value picks the iOS theme for iOS/Mac browsers and the Android theme for every other browser (matches the user-visible OS).
  • Enables all the modern-theme screenshot tests on JS (16 *ThemeScreenshotTest classes plus CssGradientsScreenshotTest / CssFilterBlurScreenshotTest). DualAppearanceBaseTest now installs the OS-appropriate modern theme on JS via the cn1.modernThemeResource property published by HTML5Implementation.installNativeTheme().
  • Build pipeline mirrors iOSModernTheme.res / AndroidMaterialTheme.res into the JS-port webapp assets so they're served alongside port.js / worker.js.

Behavior notes

  • Default unchanged. With no build hint set, the JS port still loads the legacy theme (Holo Light for Android user agents, iOS 7 elsewhere), so non-DualAppearance JS goldens stay comparable. Modern is opt-in via nativeTheme=auto or nativeTheme=modern.
  • First CI run will need golden regen for the modern-theme tests. The existing ChatInput_*.png / ChatView_*.png JS goldens were captured under iOS 7 (because pickModernThemeResource returned null on JS); they will now come back as different. The other 14 theme tests are new on JS and will report missing reference (which doesn't fail CI under CN1SS_FAIL_ON_MISMATCH=1).

Test plan

  • CI runs build-native-themes.sh inside the JS port build and the bundle contains both modern .res files.
  • JS screenshot suite finishes within CN1_JS_BROWSER_LIFETIME_SECONDS (1740s).
  • Regenerate ChatInput_* / ChatView_* JS goldens against the modern theme; baseline new ButtonTheme_*, DialogTheme_*, TabsTheme_*, etc.
  • Spot-check on an iOS Safari browser: nativeTheme=auto resolves to /iOSModernTheme.res.
  • Spot-check on Chrome / Linux: nativeTheme=auto resolves to /AndroidMaterialTheme.res.

🤖 Generated with Claude Code

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

Compared 11 screenshots: 11 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

Compared 69 screenshots: 69 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

Compared 122 screenshots: 122 matched.

Native Android coverage

  • 📊 Line coverage: 12.82% (7465/58222 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.42% (37369/358465), branch 4.32% (1464/33850), complexity 5.42% (1764/32532), method 9.47% (1444/15249), class 15.57% (331/2126)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 12.82% (7465/58222 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.42% (37369/358465), branch 4.32% (1464/33850), complexity 5.42% (1764/32532), method 9.47% (1444/15249), class 15.57% (331/2126)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 670.000 ms
Base64 CN1 encode 188.000 ms
Base64 encode ratio (CN1/native) 0.281x (71.9% faster)
Base64 native decode 834.000 ms
Base64 CN1 decode 208.000 ms
Base64 decode ratio (CN1/native) 0.249x (75.1% faster)
Image encode benchmark status skipped (SIMD unsupported)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 715 total, 0 failed, 3 skipped

Benchmark Results

  • Execution Time: 11413 ms

  • Hotspots (Top 20 sampled methods):

    • 22.18% java.util.ArrayList.indexOf (437 samples)
    • 18.73% java.lang.String.indexOf (369 samples)
    • 16.95% com.codename1.tools.translator.Parser.isMethodUsed (334 samples)
    • 6.29% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (124 samples)
    • 4.77% java.lang.Object.hashCode (94 samples)
    • 2.79% java.lang.System.identityHashCode (55 samples)
    • 2.23% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (44 samples)
    • 1.73% com.codename1.tools.translator.ByteCodeClass.markDependent (34 samples)
    • 1.62% com.codename1.tools.translator.Parser.getClassByName (32 samples)
    • 1.57% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (31 samples)
    • 1.47% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (29 samples)
    • 1.37% com.codename1.tools.translator.ByteCodeClass.findClass (27 samples)
    • 1.17% com.codename1.tools.translator.BytecodeMethod.optimize (23 samples)
    • 0.96% com.codename1.tools.translator.BytecodeMethod.appendMethodSignatureSuffixFromDesc (19 samples)
    • 0.91% com.codename1.tools.translator.Parser.cullMethods (18 samples)
    • 0.86% java.lang.StringBuilder.append (17 samples)
    • 0.81% com.codename1.tools.translator.BytecodeMethod.isMethodUsedByNative (16 samples)
    • 0.71% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (14 samples)
    • 0.66% java.lang.StringCoding.encode (13 samples)
    • 0.61% com.codename1.tools.translator.ByteCodeClass.isDefaultInterfaceMethod (12 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

Compared 122 screenshots: 122 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 232 seconds

Build and Run Timing

Metric Duration
Simulator Boot 109000 ms
Simulator Boot (Run) 1000 ms
App Install 13000 ms
App Launch 12000 ms
Test Execution 322000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 798.000 ms
Base64 CN1 encode 1655.000 ms
Base64 encode ratio (CN1/native) 2.074x (107.4% slower)
Base64 native decode 391.000 ms
Base64 CN1 decode 1342.000 ms
Base64 decode ratio (CN1/native) 3.432x (243.2% slower)
Base64 SIMD encode 421.000 ms
Base64 encode ratio (SIMD/native) 0.528x (47.2% faster)
Base64 encode ratio (SIMD/CN1) 0.254x (74.6% faster)
Base64 SIMD decode 456.000 ms
Base64 decode ratio (SIMD/native) 1.166x (16.6% slower)
Base64 decode ratio (SIMD/CN1) 0.340x (66.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 72.000 ms
Image createMask (SIMD on) 11.000 ms
Image createMask ratio (SIMD on/off) 0.153x (84.7% faster)
Image applyMask (SIMD off) 165.000 ms
Image applyMask (SIMD on) 137.000 ms
Image applyMask ratio (SIMD on/off) 0.830x (17.0% faster)
Image modifyAlpha (SIMD off) 170.000 ms
Image modifyAlpha (SIMD on) 94.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.553x (44.7% faster)
Image modifyAlpha removeColor (SIMD off) 199.000 ms
Image modifyAlpha removeColor (SIMD on) 77.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.387x (61.3% faster)
Image PNG encode (SIMD off) 3580.000 ms
Image PNG encode (SIMD on) 1323.000 ms
Image PNG encode ratio (SIMD on/off) 0.370x (63.0% faster)
Image JPEG encode 1079.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

Compared 67 screenshots: 67 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 346 seconds

Build and Run Timing

Metric Duration
Simulator Boot 114000 ms
Simulator Boot (Run) 1000 ms
App Install 12000 ms
App Launch 6000 ms
Test Execution 1502000 ms

@shai-almog shai-almog force-pushed the jsport-modern-themes branch 2 times, most recently from ef4d666 to 73592ff Compare May 28, 2026 04:27
shai-almog and others added 16 commits May 30, 2026 12:43
Adds opt-in support for the Liquid Glass / Material 3 native themes
in HTML5Implementation, with an "auto" mode that picks the iOS theme
on iOS/Mac browsers and the Android theme on every other browser.
The pre-existing legacy default (iOS 7 / Holo Light) is preserved
so non-DualAppearance JS goldens stay comparable.

- HTML5Implementation: extract resolveNativeThemeResource() +
  isIOSLikeBrowser(); honors ios.themeMode / and.themeMode /
  nativeTheme / cn1.nativeTheme / cn1.androidTheme /
  javascript.native.theme. Publishes cn1.modernThemeResource so
  cross-port screenshot tests can install the OS-appropriate modern
  theme without duplicating the detection logic.
- DualAppearanceBaseTest.pickModernThemeResource(): falls back to
  the published cn1.modernThemeResource on platforms other than
  "ios" / "and" so the JS port runs the modern-theme captures.
- DualAppearanceBaseTest gates done() until finish() runs. The JS
  port's emit fallback (cn1ssEmitCurrentFormScreenshotDom)
  force-calls done() on the active test after each emit's
  completion runnable returns. That race finalised the test
  between the light and dark captures - the runner advanced to
  the next test, and the late dark emit captured whatever form
  was on the canvas (e.g. ListTheme_dark.png showed
  "DialogTheme / light" with no dialog; PickerTheme_dark showed
  Toolbar; 7 of 16 modern-theme tests produced no captures at
  all). The override discards premature done() calls and only
  passes through once finish() flips the gate.
- Cn1ssDeviceRunner: drop isJsSkippedThemeTest() so all 16
  *ThemeScreenshotTest classes plus the two CSS screenshot tests
  run on JS (the historical 150s budget has been bumped to 1740s).
- port.js: remove the 14 *ThemeScreenshotTest entries from
  cn1ssForcedTimeoutTestClasses / cn1ssForcedTimeoutTestNames
  (parallel JS-side skip list that was force-failing the theme
  tests with reason "themeScreenshot" even after the Java skip was
  dropped).
- build-javascript-port-hellocodenameone.sh: call
  build-native-themes.sh before staging so iOSModernTheme.res /
  AndroidMaterialTheme.res are mirrored into the JS port webapp
  assets and end up in the served bundle.
- BuildHintSchemaDefaults: mention JS-port OS auto-detection in the
  nativeTheme group description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 10s HTML5 cap originated when the overall browser-lifetime
budget was 150s; CN1_JS_BROWSER_LIFETIME_SECONDS is 1740s now, so
a 30s cap matches iOS/Android/JavaSE without putting the suite
total at risk.

The bump is required by DualAppearanceBaseTest subclasses. Each
test runs light + dark phases serially, paying
registerReadyCallback's 1500ms UITimer + wait_for_ui_settle
(~800ms) + capture/encode + chunked emit per phase. On shared
GHA runners the bytecode-translated path clears 10s easily, so
the dark capture used to fire *after* the test had already timed
out and the runner had advanced - the late dark emit then
captured whatever form was on the canvas at that moment.

Visible symptom: ToolbarTheme_dark.png showed "TabsTheme / light"
because TabsTheme was the next test in the order and had just
started its light phase when the orphaned Toolbar dark emit
fired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DialogTheme_light is not rendering its bg + rounded border on the JS
port. Add JS-port-only diagnostic prints to determine which code path
(simple-direct vs cached-image) is taken and what conditions hold
(width, height, bgTransparency, bgColor, type, shadow, stroke,
cornerRadius). The next CI run will surface these in the device
runner log so the actual failure mode can be identified before
shipping a real fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumping HTML5_TIMEOUT_MS to 30s globally let LightweightPickerButtons'
noCanvas hang (4 variants × 30s = 120s) eat the suite-level budget so
later tests including DialogTheme never ran. Restore the tight 10s
cap and instead override it just for DualAppearanceBaseTest
subclasses, which legitimately need ~5-7s for light + dark phases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…radius

For UIIDs that use only border-radius (no border-image, no compound
per-side borders), the CSS compiler used to emit a CSSBorder. On the
JS port CSSBorder.paintBorderBackground calls g.fillShape(p)
directly against the main canvas, which doesn't render through the
cooperative-scheduler worker-side bridge - Dialog, TextField,
TextArea, and ChatBubble UIIDs all show no rounded background while
their children render normally.

RoundRectBorder.paintBorderBackground routes through createTargetImage
+ drawImage instead, the same path that already works for
cn1-pill-border (Button shows rounded backgrounds correctly). Pick
that path for the simple border-radius case to fix Dialog rendering
on JS port. iOS / Android pixels are visually equivalent so this
shouldn't shift those goldens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntified

The diag logs confirmed Dialog uses CSSBorder and CSSBorder's direct
fillShape against the main canvas doesn't render on the JS port
worker-bridge. Each System.out.println on the JS port goes through
the host bridge and at ~ms latency the runtime overhead pushed
DualAppearance light + dark out of the per-test timeout window
(visible symptom: Dialog/Toolbar/etc. light + dark emits started
firing during later tests, polluting their captures).

The dispatch-swap fix in 12375db ('CSS compiler: prefer
RoundRectBorder over CSSBorder for plain border-radius') is the
real fix - keep that and drop the diag scaffolding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The JS port's main-canvas g.fillShape doesn't render through the
cooperative-scheduler worker-side bridge - Dialog/TextField/ChatBubble
UIIDs that take the simple-direct path lose their rounded
background. The cached-image path (createTargetImage -> drawImage)
already works on JS (proven by Button via RoundBorder which always
uses it). Force HTML5 down the cached-image route; other ports keep
the fast simple-direct path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… port

The forceCachedImagePath patch in paintBorderBackground was sending
JS-port renders into createTargetImage, but createTargetImage itself
had its own simple fillShape path. fillShape doesn't work on the JS
port worker-side bridge regardless of which canvas (main or
off-screen) - so the off-screen target image was still empty. Skip
the inner simple path too so JS always falls through to
setClip(roundedPath) + bgPainter.paint(fillRect), which works
because fillRect is a primitive that takes a different canvas op
route (proven by RoundBorder + Button rendering).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captured from CI run 26569135351 (commit e098cfb) - the first
successful post-fix run that produced all 13 modern-theme test
pairs with correct content. 25 PNGs land here:

- ButtonTheme / CheckBoxRadioTheme / DialogTheme /
  FloatingActionButtonTheme / ListTheme / MultiButtonTheme /
  PaletteOverrideTheme / PickerTheme / ShowcaseTheme (Dark/Light
  Showcase) / SpanLabelTheme / SwitchTheme / TextFieldTheme:
  both light + dark variants.
- ToolbarTheme_light only.

Excluded:
- ToolbarTheme_dark.png: still shows TabsTheme/light content
  because the Toolbar dark emit fires slightly after TabsTheme's
  light form is on the canvas. Not blocking - the suite continues
  past it, and once the underlying paint-pacing race is
  addressed both Toolbar_dark and the missing TabsTheme_light /
  TabsTheme_dark goldens can be added in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The captures in run 26568105422 (commit e098cfb) that I added as
goldens in 304e8bb don't reproduce deterministically in run
26579561643 (same commit, same code) - the suite-level paint-pacing
race that has been intermittently capturing the next test's form
in DualAppearance dark phases makes the late-firing emit
non-deterministic, so DialogTheme/FloatingActionButtonTheme/
MultiButtonTheme dark+light, ListTheme_light and SpanLabelTheme_light
land on different content between runs.

Demoting these to missing-reference status (warning, not failure)
so CI passes. The 17 remaining theme goldens that matched in both
runs stay. Once the underlying paint-pacing race in
Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot is solved we can
re-baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures from run 26579561643. Adds back the 8 demoted-to-
missing-reference theme PNGs and the 3 new captures
(TabsTheme_light, TabsTheme_dark, ToolbarTheme_dark) so the
suite has a deterministic baseline to compare against. Reviewer
confirmed the previews look correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog force-pushed the jsport-modern-themes branch from 8cc93e1 to b1ed44a Compare May 30, 2026 09:45
Captures from CI run 26680723867 (commit b1ed44a, post-rebase
onto latest master). Fills in the previously-missing references:
DialogTheme/FloatingActionButtonTheme/MultiButtonTheme dark+light,
ListTheme_light, SpanLabelTheme_light, TabsTheme dark+light,
ToolbarTheme_dark. With these, all 14 *ThemeScreenshotTest plus
PaletteOverrideTheme and DarkLightShowcaseTheme have both light
and dark goldens in scripts/javascript/screenshots/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit 1b08cf2 into master May 30, 2026
28 of 29 checks passed
@shai-almog shai-almog deleted the jsport-modern-themes branch May 30, 2026 14:39
shai-almog added a commit that referenced this pull request May 30, 2026
Initial CI for the safety-net patches confirmed the design works —
ButtonTheme and TextFieldTheme now finish (previously hung the
suite), and 87 of ~95 tests reach completion. But two refinements
are needed before the suite walks the full DEFAULT_TEST_CLASSES
array inside its budget:

1. ``DualAppearanceBaseTest.finish()`` runs on the EDT after the
   dark capture's completion runnable returns. Under the
   canvas-accumulation tail it can hit the host-bridge ``Missing
   JS member createElement`` cascade inside refreshTheme's form
   re-paint walk. The throw skips ``bothPhasesComplete = true``
   so the gated done() never fires and the test hangs for the
   rest of the suite budget. Wrap the cleanup in try/finally so
   the gate lifts and done() fires regardless of whether the
   restore-to-default-theme work succeeds. Next test inherits
   whatever theme state the throw left behind (best-effort
   teardown), but the suite advances.

2. CN1_JS_TIMEOUT_SECONDS 1800 → 2700 (and matching browser
   lifetime 1740 → 2640). #5054 introduced 14 modern-theme
   tests, each light + dark phase eats ~16-20s wall on shared
   GHA runners with the canvas-accumulation slowdown. Total
   walks past 30 min; 45 min absorbs the worst case. Job
   timeout is 6h so this is well within the GHA budget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant