diff --git a/AGENTS.md b/AGENTS.md index 98fceae39..b2f6177a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,9 +38,9 @@ that matters to the change. tests out of `commonTest`, which Android host tests inherit. Android host and device tests live in `androidHostTest` and `androidDeviceTest` respectively. -Browser tests run real maps in Chrome. Set `CHROME_BIN` if Karma cannot find it. -Do not pass `--tests` to the browser suite; it silently runs no tests and -reports success. +Browser tests run real maps in Playwright Chromium and Firefox. Set `CHROME_BIN` +and `FIREFOX_BIN` if Karma cannot find them. Do not pass `--tests` to the +browser suite; it silently runs no tests and reports success. Android SDK lookup is `local.properties`, then `ANDROID_HOME`, then `ANDROID_SDK_ROOT`. `mise run android-sdk-packages` installs required packages. diff --git a/ci/jobs.json b/ci/jobs.json index 51d222441..70a650b5c 100644 --- a/ci/jobs.json +++ b/ci/jobs.json @@ -1,7 +1,7 @@ [ { "job": "hygiene", "variant": "ubuntu", "tier": "draft" }, { "job": "docs", "variant": "ubuntu", "tier": "draft" }, - { "job": "js", "variant": "chromium", "tier": "draft" }, + { "job": "js", "variant": "chromium-firefox", "tier": "draft" }, { "job": "ios-device", "variant": "arm64", "tier": "draft" }, { "job": "ios", "variant": "arm64", "tier": "ready" }, { diff --git a/ci/plan_test.py b/ci/plan_test.py index d70d30947..2abf70033 100644 --- a/ci/plan_test.py +++ b/ci/plan_test.py @@ -63,7 +63,7 @@ def test_draft_keeps_compile_and_linux_runtime_coverage(self) -> None: { "hygiene / ubuntu", "docs / ubuntu", - "js / chromium", + "js / chromium-firefox", "ios-device / arm64", "android / 36", "desktop / linux-x64", diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index b26ada99b..00ca6dd61 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -1831,6 +1831,11 @@ is-core-module@^2.16.0: dependencies: hasown "^2.0.2" +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-docker@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" @@ -1907,6 +1912,13 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + is-wsl@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" @@ -1988,6 +2000,14 @@ karma-chrome-launcher@3.2.0: dependencies: which "^1.2.1" +karma-firefox-launcher@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz#b278a4cbffa92ab81394b1a398813847b0624a85" + integrity sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw== + dependencies: + is-wsl "^2.2.0" + which "^3.0.0" + karma-mocha@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" @@ -3406,6 +3426,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +which@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1" + integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg== + dependencies: + isexe "^2.0.0" + wildcard@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" diff --git a/lib/location/build.gradle.kts b/lib/location/build.gradle.kts index 742e40f36..27812c1ca 100644 --- a/lib/location/build.gradle.kts +++ b/lib/location/build.gradle.kts @@ -24,7 +24,14 @@ kotlin { js { useEsModules() - browser { testTask { useKarma { useChromeHeadless() } } } + browser { + testTask { + useKarma { + useChromeHeadless() + useFirefoxHeadless() + } + } + } } applyDefaultHierarchyTemplate() diff --git a/lib/location/karma.config.d/ci.js b/lib/location/karma.config.d/ci.js index 7794e1808..6c6d1be67 100644 --- a/lib/location/karma.config.d/ci.js +++ b/lib/location/karma.config.d/ci.js @@ -1,10 +1,12 @@ // The CI runner is an isolated VM, so its launcher disables Chromium's unavailable sandbox. if (process.env.CI && process.platform === "linux") { - config.customLaunchers = { + config.customLaunchers = Object.assign({}, config.customLaunchers, { ChromeHeadlessCI: { base: "ChromeHeadless", flags: ["--no-sandbox"], }, - }; - config.browsers = ["ChromeHeadlessCI"]; + }); + config.browsers = config.browsers.map((browser) => + browser === "ChromeHeadless" ? "ChromeHeadlessCI" : browser, + ); } diff --git a/lib/maplibre-compose/build.gradle.kts b/lib/maplibre-compose/build.gradle.kts index 705d52fc1..f673e6027 100644 --- a/lib/maplibre-compose/build.gradle.kts +++ b/lib/maplibre-compose/build.gradle.kts @@ -39,8 +39,15 @@ kotlin { // (CMP-4906). binaries.executable() // The browser platform composites MapLibre GL JS into the Compose scene, so its tests need a - // real WebGL context; karma.config.d supplies the flags that give one to a headless browser. - browser { testTask { useKarma { useChromeHeadless() } } } + // real WebGL context; karma.config.d supplies the launchers that give one to each browser. + browser { + testTask { + useKarma { + useChromeHeadless() + useFirefoxHeadless() + } + } + } } applyDefaultHierarchyTemplate() diff --git a/lib/maplibre-compose/karma.config.d/webgl.js b/lib/maplibre-compose/karma.config.d/webgl.js index 7d6e754f1..f494a6560 100644 --- a/lib/maplibre-compose/karma.config.d/webgl.js +++ b/lib/maplibre-compose/karma.config.d/webgl.js @@ -1,7 +1,7 @@ // MapLibre GL JS refuses to start without a WebGL context, and a headless browser on a machine // with no GPU has none unless it is told to rasterize in software. -config.customLaunchers = { +config.customLaunchers = Object.assign({}, config.customLaunchers, { ChromeHeadlessWebGL: { base: "ChromeHeadless", flags: [ @@ -11,5 +11,14 @@ config.customLaunchers = { "--no-sandbox", ], }, -}; -config.browsers = ["ChromeHeadlessWebGL"]; + // Headless Firefox on Linux has no WebGL, so Linux runs a headed Firefox on the Xvfb display that + // test:js provides. + FirefoxWebGL: { + base: process.platform === "linux" ? "Firefox" : "FirefoxHeadless", + prefs: { + "webgl.force-enabled": true, + "webgl.disabled": false, + }, + }, +}); +config.browsers = ["ChromeHeadlessWebGL", "FirefoxWebGL"]; diff --git a/lib/maplibre-compose/src/liveMapTest/kotlin/org/maplibre/compose/map/AntimeridianContractTest.kt b/lib/maplibre-compose/src/liveMapTest/kotlin/org/maplibre/compose/map/AntimeridianContractTest.kt index 586e2b084..53e6e701c 100644 --- a/lib/maplibre-compose/src/liveMapTest/kotlin/org/maplibre/compose/map/AntimeridianContractTest.kt +++ b/lib/maplibre-compose/src/liveMapTest/kotlin/org/maplibre/compose/map/AntimeridianContractTest.kt @@ -123,6 +123,8 @@ class AntimeridianContractTest { it.pumpUntil("the style's features to become queryable") { it.state.queryRenderedFeatures(offset = DpOffset(800.dp, 256.dp)).isNotEmpty() } + // The wrapped world copy renders from its own tile, which can land after the first hit. + it.settle() val hits = it.state.queryRenderedFeatures(offset = DpOffset(800.dp, 256.dp)).mapNotNull { hit -> diff --git a/mise.toml b/mise.toml index 13a471ab8..17d3309a1 100644 --- a/mise.toml +++ b/mise.toml @@ -85,6 +85,7 @@ swiftformat = { version = "{{vars.swiftformat_version}}", backend = "github:nick [env] PLAYWRIGHT_BROWSERS_PATH = "{{ config_root }}/.cache/playwright" CHROME_BIN = { value = '''{{ exec(command="node -e \"console.log(require(process.argv[1]).chromium.executablePath())\" \"" ~ tools.playwright.path ~ "/node_modules/playwright\"") }}''', tools = true } +FIREFOX_BIN = { value = '''{{ exec(command="node -e \"console.log(require(process.argv[1]).firefox.executablePath())\" \"" ~ tools.playwright.path ~ "/node_modules/playwright\"") }}''', tools = true } [hooks] postinstall = ["hk install --mise --quiet"] @@ -101,6 +102,15 @@ run = "playwright install chromium --no-shell" [tasks."deps:chromium"] run = "mise deps install chromium" +[deps.firefox] +auto = false +sources = ["mise.lock"] +outputs = [".cache/playwright/firefox-*"] +run = "playwright install firefox --no-shell" + +[tasks."deps:firefox"] +run = "mise deps install firefox" + [tasks.check] description = "Run every formatter and linter in report-only mode." # The type checker reads the types Astro generates. @@ -198,9 +208,20 @@ udid="$(.mise/bin/boot-ios-simulator)" ''' [tasks."test:js"] -depends = ["deps:chromium"] +depends = ["deps:chromium", "deps:firefox"] description = "Run the Kotlin/JS browser test suite." -run = "./gradlew jsBrowserTest" +run = ''' +echo "JS tests launch ${CHROME_BIN:?}" +"$CHROME_BIN" --version +echo "JS tests launch ${FIREFOX_BIN:?}" +"$FIREFOX_BIN" --version +if [[ "$(uname -s)" == Linux ]]; then + xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" \ + env LIBGL_ALWAYS_SOFTWARE=1 ./gradlew jsBrowserTest +else + ./gradlew jsBrowserTest +fi +''' [tasks."test:desktop"] description = "Run the desktop (JVM) test suite against a real Vulkan or Metal device."