diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 215ee2015..7e6b510a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -220,10 +220,11 @@ jobs: playwright: if: github.event.pull_request.draft == false name: Playwright Tests - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ${{ matrix.os }} timeout-minutes: 45 strategy: matrix: + os: [windows-latest, blacksmith-8vcpu-ubuntu-2404] platform: - { toolchain: beta } # - { toolchain: 1.86.0 } @@ -231,9 +232,10 @@ jobs: # Do our best to cache the toolchain and node install steps - uses: actions/checkout@v5 - name: Free Disk Space - if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }} + if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }} uses: ./.github/actions/free-disk-space - uses: awalsh128/cache-apt-pkgs-action@latest + if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }} with: packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libcups2t64 libxml2 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf fonts-unifont fonts-wqy-zenhei gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-good libavif16 libevent-2.1-7t64 libgstreamer-plugins-bad1.0-0 libharfbuzz-icu0 libhyphen0 libmanette-0.2-0 libsecret-1-0 libwoff1 xfonts-cyrillic xfonts-scalable fonts-ipafont-mincho fonts-tlwg-loma gstreamer1.0-x version: 1.0 @@ -251,6 +253,7 @@ jobs: cache-all-crates: "true" cache-on-failure: "true" - name: Wipe dx cache + if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }} run: | rm -rf ./target/dx - name: Playwright diff --git a/Cargo.lock b/Cargo.lock index 369b2e948..2dcebf972 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19842,6 +19842,13 @@ dependencies = [ "windows-threading 0.2.1", ] +[[package]] +name = "windows-headless" +version = "0.7.1" +dependencies = [ + "dioxus", +] + [[package]] name = "windows-implement" version = "0.58.0" diff --git a/Cargo.toml b/Cargo.toml index 2c99158d5..4b6caf70c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,7 +138,7 @@ members = [ "packages/playwright-tests/cli-optimization", "packages/playwright-tests/wasm-split-harness", "packages/playwright-tests/default-features-disabled", - "packages/playwright-tests/fullstack-error-codes", + "packages/playwright-tests/fullstack-error-codes", "packages/playwright-tests/windows-headless", ] [workspace.package] diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 1892a1e19..aece7f767 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -400,6 +400,7 @@ pub(crate) struct BuildRequest { pub(crate) apple_team_id: Option, pub(crate) session_cache_dir: PathBuf, pub(crate) raw_json_diagnostics: bool, + pub(crate) windows_subsystem: Option, } /// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin @@ -909,27 +910,6 @@ impl BuildRequest { ]); } - // On windows, we pass /SUBSYSTEM:WINDOWS to prevent a console from appearing - if matches!(bundle, BundleFormat::Windows) - && !rustflags - .flags - .iter() - .any(|f| f.starts_with("-Clink-arg=/SUBSYSTEM:")) - { - let subsystem = args - .windows_subsystem - .clone() - .unwrap_or_else(|| "WINDOWS".to_string()); - rustflags - .flags - .push(format!("-Clink-arg=/SUBSYSTEM:{}", subsystem)); - // We also need to set the entry point to mainCRTStartup to avoid windows looking - // for a WinMain function - rustflags - .flags - .push("-Clink-arg=/ENTRY:mainCRTStartup".to_string()); - } - // Make sure we set the sysroot for ios builds in the event the user doesn't have it set if matches!(bundle, BundleFormat::Ios) { let xcode_path = Workspace::get_xcode_path() @@ -1040,6 +1020,7 @@ impl BuildRequest { apple_entitlements: args.apple_entitlements.clone(), apple_team_id: args.apple_team_id.clone(), raw_json_diagnostics: args.raw_json_diagnostics, + windows_subsystem: args.windows_subsystem.clone(), }) } @@ -2673,6 +2654,25 @@ impl BuildRequest { cargo_args.push("--".to_string()); cargo_args.extend(self.extra_rustc_args.clone()); + // On windows, we pass /SUBSYSTEM:WINDOWS to prevent a console from appearing + if matches!(self.bundle, BundleFormat::Windows) + && !self + .rustflags + .flags + .iter() + .any(|f| f.starts_with("-Clink-arg=/SUBSYSTEM:")) + { + let subsystem = self + .windows_subsystem + .clone() + .unwrap_or_else(|| "WINDOWS".to_string()); + + cargo_args.push(format!("-Clink-arg=/SUBSYSTEM:{}", subsystem)); + // We also need to set the entry point to mainCRTStartup to avoid windows looking + // for a WinMain function + cargo_args.push("-Clink-arg=/ENTRY:mainCRTStartup".to_string()); + } + // The bundle splitter needs relocation data to create a call-graph. // This will automatically be erased by wasm-opt during the optimization step. if self.bundle == BundleFormat::Web && self.wasm_split { diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index b09323e3e..fb35bb43c 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -67,6 +67,7 @@ pub struct Config { pub(crate) window_close_behavior: WindowCloseBehaviour, pub(crate) custom_event_handler: Option, pub(crate) disable_file_drop_handler: bool, + pub(crate) additional_windows_args: Option, #[allow(clippy::type_complexity)] pub(crate) on_window: Option, &mut VirtualDom) + 'static>>, @@ -118,6 +119,7 @@ impl Config { custom_event_handler: None, disable_file_drop_handler: false, on_window: None, + additional_windows_args: None, } } @@ -312,6 +314,12 @@ impl Config { self.on_window = Some(Box::new(f)); self } + + /// Add additional windows only launch arguments for webview2 + pub fn with_windows_browser_args(mut self, additional_args: impl ToString) -> Self { + self.additional_windows_args = Some(additional_args.to_string()); + self + } } impl Default for Config { diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 4c9859c95..921f39dc3 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -442,6 +442,14 @@ impl WebviewInstance { None }; + #[cfg(target_os = "windows")] + { + use wry::WebViewBuilderExtWindows; + if let Some(additional_windows_args) = &cfg.additional_windows_args { + webview = webview.with_additional_browser_args(additional_windows_args); + } + } + #[cfg(any( target_os = "windows", target_os = "macos", diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index c90c80df3..b71816ba8 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -8,44 +8,24 @@ const path = require("path"); */ // require('dotenv').config(); -/** - * @see https://playwright.dev/docs/test-configuration - */ -module.exports = defineConfig({ - testDir: ".", - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "retain-on-failure", - // Increase the timeout for navigations to give dx time to build the project - navigationTimeout: 50 * 60 * 1000, - }, - - timeout: 50 * 60 * 1000, - - /* Configure projects for major browsers */ - projects: [ +let webServer = []; +let grep = undefined; +let grepInvert = undefined; +if (process.platform === "win32") { + webServer = [ { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, + cwd: path.join(process.cwd(), "windows-headless"), + command: + "cargo run --package dioxus-cli --release -- run --force-sequential", + port: 8787, + timeout: 50 * 60 * 1000, + reuseExistingServer: !process.env.CI, + stdout: "pipe", }, - ], - - /* Run your local dev server before starting the tests */ - webServer: [ + ]; + grep = /windows/; +} else { + webServer = [ { command: "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test", @@ -224,5 +204,49 @@ module.exports = defineConfig({ reuseExistingServer: !process.env.CI, stdout: "pipe", }, + ]; + + grepInvert = /windows/; +} + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: ".", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "retain-on-failure", + // Increase the timeout for navigations to give dx time to build the project + navigationTimeout: 50 * 60 * 1000, + }, + + timeout: 50 * 60 * 1000, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + grep, + grepInvert, + use: { ...devices["Desktop Chrome"] }, + }, ], + + /* Run your local dev server before starting the tests */ + webServer, }); diff --git a/packages/playwright-tests/windows-headless/Cargo.toml b/packages/playwright-tests/windows-headless/Cargo.toml new file mode 100644 index 000000000..8cb037047 --- /dev/null +++ b/packages/playwright-tests/windows-headless/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "windows-headless" +edition = "2024" +version.workspace = true + +[dependencies] +dioxus = { workspace = true, features = ["desktop"] } diff --git a/packages/playwright-tests/windows-headless/src/main.rs b/packages/playwright-tests/windows-headless/src/main.rs new file mode 100644 index 000000000..f1521a860 --- /dev/null +++ b/packages/playwright-tests/windows-headless/src/main.rs @@ -0,0 +1,23 @@ +use dioxus::LaunchBuilder; +use dioxus::prelude::*; + +fn main() { + LaunchBuilder::new() + .with_cfg( + dioxus::desktop::Config::new() + .with_windows_browser_args("--remote-debugging-port=8787"), + ) + .launch(app); +} + +fn app() -> Element { + let mut count = use_signal(|| 0); + + rsx! { + h1 { "High-five counter: {count}" } + button { + id: "increment-button", + onclick: move |_| count += 1, "Up high!" + } + } +} diff --git a/packages/playwright-tests/windows.spec.js b/packages/playwright-tests/windows.spec.js new file mode 100644 index 000000000..a44d7f51d --- /dev/null +++ b/packages/playwright-tests/windows.spec.js @@ -0,0 +1,37 @@ +const { expect, defineConfig } = require("@playwright/test"); +import { test as base } from '@playwright/test'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import childProcess from 'child_process'; + + +export const test = base.extend({ + browser: async ({ playwright }, use, testInfo) => { + const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:8787`); + await use(browser); + await browser.close(); + }, + context: async ({ browser }, use) => { + const context = browser.contexts()[0]; + await use(context); + }, + page: async ({ context }, use) => { + const page = context.pages()[0]; + await use(page); + }, +}); + +test("button click", async ({ page }) => { + // Expect the page to contain the counter text. + const main = page.locator("#main"); + await expect(main).toContainText("High-five counter: 0"); + + // Click the increment button. + let button = page.locator("button#increment-button"); + await button.click(); + + // Expect the page to contain the updated counter text. + await expect(main).toContainText("High-five counter: 1"); +}); +