From 2071be2b6753327a980909fc58a1c4b81bed911a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 30 Jun 2025 18:23:04 -0500 Subject: [PATCH] Fix hotpatched assets (#4326) * wip: fixes to get docsite working again * Fix asset location with PIC in hot patched builds * Add an hidden cli flag to exit early in hot patch tests on failure * add a test for hot patching with assets * fix clippy * fix looks_like_manganis_symbol * longer timeout for hot patching in CI * log page content * log assets * Log asset after write * record a playwright trace * Explicitly enable cargo incremental for hot patching * fix retry for patch test * reset asset * disable hot patching test for new exports * add nightly playwright * use different toolchain names * use beta instead of nightly for less thrashing * add failing test * workaround hot reloading bug * restore swapping asset tests * add toolchain --------- Co-authored-by: Jonathan Kelley --- .github/workflows/main.yml | 15 +- .gitignore | 1 + packages/cli/src/build/assets.rs | 184 ++++++++++-------- packages/cli/src/build/builder.rs | 7 +- packages/cli/src/build/patch.rs | 19 +- packages/cli/src/build/request.rs | 17 +- packages/cli/src/cli/serve.rs | 5 + packages/cli/src/serve/mod.rs | 34 +++- packages/manganis/manganis-core/src/asset.rs | 18 +- packages/manganis/manganis-macro/src/asset.rs | 2 +- packages/playwright-tests/package-lock.json | 24 +-- packages/playwright-tests/package.json | 2 +- .../playwright-tests/playwright.config.js | 10 +- .../playwright-tests/web-hot-patch/.gitignore | 2 + .../playwright-tests/web-hot-patch/Cargo.toml | 12 ++ .../assets/alternative-style.css | 3 + .../web-hot-patch/assets/style.css | 3 + .../web-hot-patch/assets/toasts.png | Bin 0 -> 20366 bytes .../web-hot-patch/src/main.rs | 30 +++ packages/playwright-tests/web-patch.spec.js | 102 ++++++++++ 20 files changed, 365 insertions(+), 125 deletions(-) create mode 100644 packages/playwright-tests/web-hot-patch/.gitignore create mode 100644 packages/playwright-tests/web-hot-patch/Cargo.toml create mode 100644 packages/playwright-tests/web-hot-patch/assets/alternative-style.css create mode 100644 packages/playwright-tests/web-hot-patch/assets/style.css create mode 100644 packages/playwright-tests/web-hot-patch/assets/toasts.png create mode 100644 packages/playwright-tests/web-hot-patch/src/main.rs create mode 100644 packages/playwright-tests/web-patch.spec.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c0dfad2d..883dc4d3f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -200,6 +200,11 @@ jobs: if: github.event.pull_request.draft == false name: Playwright Tests runs-on: macos-latest + strategy: + matrix: + platform: + - { toolchain: 1.86.0 } + - { toolchain: beta } steps: # Do our best to cache the toolchain and node install steps - uses: actions/checkout@v4 @@ -207,12 +212,13 @@ jobs: with: node-version: 16 - name: Install Rust - uses: dtolnay/rust-toolchain@1.86.0 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: ${{ matrix.platform.toolchain }} targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: + key: "playwright-${{ matrix.platform.toolchain }}-${{ runner.os }}" cache-all-crates: "true" cache-on-failure: "true" - name: Wipe dx cache @@ -220,6 +226,9 @@ jobs: rm -rf ./target/dx - name: Playwright working-directory: ./packages/playwright-tests + env: + # The hot patch test requires incremental compilation + CARGO_INCREMENTAL: 1 run: | npm ci npm install -D @playwright/test @@ -228,7 +237,7 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report + name: playwright-report-${{ matrix.platform.toolchain }}-${{ runner.os }} path: ./packages/playwright-tests/playwright-report/ retention-days: 30 diff --git a/.gitignore b/.gitignore index a1737042a..df4b5192c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /packages/playwright-tests/web/dist /packages/playwright-tests/fullstack/dist /packages/playwright-tests/test-results +/packages/playwright-tests/web-hot-patch-temp /dist .DS_Store /examples/assets/test_video.mp4 diff --git a/packages/cli/src/build/assets.rs b/packages/cli/src/build/assets.rs index b549cda00..96faf4d04 100644 --- a/packages/cli/src/build/assets.rs +++ b/packages/cli/src/build/assets.rs @@ -41,7 +41,6 @@ use manganis::BundledAsset; use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol}; use pdb::FallibleIterator; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; -use wasmparser::BinaryReader; /// Extract all manganis symbols and their sections from the given object file. fn manganis_symbols<'a, 'b, R: ReadRef<'a>>( @@ -50,7 +49,7 @@ fn manganis_symbols<'a, 'b, R: ReadRef<'a>>( file.symbols() .filter(|symbol| { if let Ok(name) = symbol.name() { - name.contains("__MANGANIS__") + looks_like_manganis_symbol(name) } else { false } @@ -62,6 +61,10 @@ fn manganis_symbols<'a, 'b, R: ReadRef<'a>>( }) } +fn looks_like_manganis_symbol(name: &str) -> bool { + name.contains("__MANGANIS__") +} + /// Find the offsets of any manganis symbols in the given file. fn find_symbol_offsets<'a, R: ReadRef<'a>>( path: &Path, @@ -175,105 +178,113 @@ fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result< Ok(offsets) } +fn eval_walrus_global_expr(module: &walrus::Module, expr: &walrus::ConstExpr) -> Option { + match expr { + walrus::ConstExpr::Value(walrus::ir::Value::I32(value)) => Some(*value as u64), + walrus::ConstExpr::Value(walrus::ir::Value::I64(value)) => Some(*value as u64), + walrus::ConstExpr::Global(id) => { + let global = module.globals.get(*id); + if let walrus::GlobalKind::Local(pointer) = &global.kind { + eval_walrus_global_expr(module, pointer) + } else { + None + } + } + _ => None, + } +} + /// Find the offsets of any manganis symbols in the wasm file. fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>( file_contents: &[u8], file: &File<'a, R>, ) -> Result> { - // Parse the wasm file to find the globals - let parser = wasmparser::Parser::new(0); + let Some(section) = file + .sections() + .find(|section| section.name() == Ok("")) + else { + tracing::error!("Failed to find section in WASM file"); + return Ok(Vec::new()); + }; + let Some((_, section_range_end)) = section.file_range() else { + tracing::error!("Failed to find file range for section in WASM file"); + return Ok(Vec::new()); + }; + let section_size = section.data()?.len() as u64; + let section_start = section_range_end - section_size; - // All integer literal global values in the wasm file - let mut global_values = Vec::new(); - for section in parser.parse_all(file_contents) { - let Ok(wasmparser::Payload::GlobalSection(global_section)) = section else { + // Translate the section_relative_address to the file offset + // WASM files have a section address of 0 in object, reparse the data section with wasmparser + // to get the correct address and section start + // Note: We need to reparse just the data section with wasmparser to get the file offset because walrus does + // not expose the file offset information + let reader = wasmparser::DataSectionReader::new(wasmparser::BinaryReader::new( + &file_contents[section_start as usize..section_range_end as usize], + 0, + )) + .context("Failed to create WASM data section reader")?; + let main_memory = reader + .into_iter() + .next() + .context("Failed find main memory from WASM data section")? + .context("Failed to read main memory from WASM data section")?; + // main_memory.data is a slice somewhere in file_contents. Find out the offset in the file + let data_start_offset = (main_memory.data.as_ptr() as u64) + .checked_sub(file_contents.as_ptr() as u64) + .expect("Data section start offset should be within the file contents"); + + // Parse the wasm file to find the globals + let module = walrus::Module::from_buffer(file_contents).unwrap(); + let mut offsets = Vec::new(); + + // Find the main memory offset + let main_memory = module + .data + .iter() + .next() + .context("Failed to find main memory in WASM module")?; + + let walrus::DataKind::Active { + offset: main_memory_offset, + .. + } = main_memory.kind + else { + tracing::error!("Failed to find main memory offset in WASM module"); + return Ok(Vec::new()); + }; + + // In the hot patch build, the main memory offset is a global from the main module and each global + // is it's own global. Use an offset of 0 instead if we can't evaluate the global + let main_memory_offset = + eval_walrus_global_expr(&module, &main_memory_offset).unwrap_or_default(); + + for export in module.exports.iter() { + if !looks_like_manganis_symbol(&export.name) { + continue; + } + + let walrus::ExportItem::Global(global) = export.item else { continue; }; - global_values = global_section - .into_iter() - .map(|global| { - let global = global.ok()?; - match global.init_expr.get_operators_reader().into_iter().next() { - Some(Ok(wasmparser::Operator::I32Const { value })) => Some(value as u64), - Some(Ok(wasmparser::Operator::I64Const { value })) => Some(value as u64), - _ => None, - } - }) - .collect::>(); - } - let mut offsets = Vec::new(); + let walrus::GlobalKind::Local(pointer) = module.globals.get(global).kind else { + continue; + }; - for (symbol, section) in manganis_symbols(file) { - let virtual_address = symbol.address(); - - let Some((_, section_range_end)) = section.file_range() else { + let Some(virtual_address) = eval_walrus_global_expr(&module, &pointer) else { tracing::error!( - "Found __MANGANIS__ symbol {:?} in section {}, but the section has no file range", - symbol.name(), - section.index() + "Found __MANGANIS__ symbol {:?} in WASM file, but the global expression could not be evaluated", + export.name ); continue; }; - let section_size = section.data()?.len() as u64; - let section_start = section_range_end - section_size; - // Translate the section_relative_address to the file offset - // WASM files have a section address of 0 in object, reparse the data section with wasmparser - // to get the correct address and section start - let reader = wasmparser::DataSectionReader::new(BinaryReader::new( - &file_contents[section_start as usize..section_range_end as usize], - 0, - )) - .context("Failed to create WASM data section reader")?; - let main_memory = reader - .into_iter() - .next() - .context("Failed find main memory from WASM data section")? - .context("Failed to read main memory from WASM data section")?; - let main_memory_offset = match main_memory.kind { - wasmparser::DataKind::Active { offset_expr, .. } => { - match offset_expr.get_operators_reader().into_iter().next() { - Some(Ok(wasmparser::Operator::I32Const { value })) => -value as i128, - Some(Ok(wasmparser::Operator::I64Const { value })) => -value as i128, - Some(Ok(wasmparser::Operator::GlobalGet { global_index })) => { - let Some(value) = - global_values.get(global_index as usize).copied().flatten() - else { - tracing::error!( - "Found __MANGANIS__ symbol {:?} in WASM file, but the global index {} is not found", - symbol.name(), - global_index - ); - continue; - }; - value as i128 - } - offset_expr => { - tracing::error!( - "Found __MANGANIS__ symbol {:?} in WASM file, but the offset expression is not a constant is is {:?}", - symbol.name(), - offset_expr - ); - continue; - } - } - } - _ => { - tracing::error!( - "Found __MANGANIS__ symbol {:?} in WASM file, but the data section is not active", - symbol.name() - ); - continue; - } - }; - // main_memory.data is a slice somewhere in file_contents. Find out the offset in the file - let data_start_offset = (main_memory.data.as_ptr() as u64) - .checked_sub(file_contents.as_ptr() as u64) - .expect("Data section start offset should be within the file contents"); - let section_relative_address: u64 = ((virtual_address as i128) + main_memory_offset) + + let section_relative_address: u64 = ((virtual_address as i128) + - main_memory_offset as i128) .try_into() .expect("Virtual address should be greater than or equal to section address"); let file_offset = data_start_offset + section_relative_address; + offsets.push(file_offset); } @@ -304,6 +315,7 @@ pub(crate) fn extract_assets_from_file(path: impl AsRef) -> Result) -> Result) -> Result Result, diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index 7755410ff..ecc22e4ca 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -1,6 +1,7 @@ use crate::{ styles::{GLOW_STYLE, LINK_STYLE}, - AppBuilder, BuildId, BuildMode, BuilderUpdate, Platform, Result, ServeArgs, TraceController, + AppBuilder, BuildId, BuildMode, BuilderUpdate, Error, Platform, Result, ServeArgs, + TraceController, }; mod ansi_buffer; @@ -11,6 +12,7 @@ mod runner; mod server; mod update; +use dioxus_dx_wire_format::BuildStage; pub(crate) use output::*; pub(crate) use runner::*; pub(crate) use server::*; @@ -38,6 +40,7 @@ pub(crate) use update::*; /// to a dynamic one on the fly. pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> { // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc + let exit_on_error = args.exit_on_error; let mut builder = AppServer::start(args).await?; let mut devserver = WebServer::start(&builder)?; let mut screen = Output::start(builder.interactive).await?; @@ -138,12 +141,33 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> // And then open the app if it's ready match update { + BuilderUpdate::Progress { + stage: BuildStage::Failed, + } => { + if exit_on_error { + return Err(Error::Cargo(format!( + "Build failed for platform: {platform}" + ))); + } + } + BuilderUpdate::Progress { + stage: BuildStage::Aborted, + } => { + if exit_on_error { + return Err(Error::Cargo(format!( + "Build aborted for platform: {platform}" + ))); + } + } BuilderUpdate::Progress { .. } => {} BuilderUpdate::CompilerMessage { message } => { screen.push_cargo_log(message); } BuilderUpdate::BuildFailed { err } => { tracing::error!("Build failed: {}", err); + if exit_on_error { + return Err(err); + } } BuilderUpdate::BuildReady { bundle } => match bundle.mode { BuildMode::Thin { ref cache, .. } => { @@ -191,12 +215,20 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> ); } else { tracing::error!("Application [{platform}] exited with error: {status}"); + if exit_on_error { + return Err(Error::Runtime(format!( + "Application [{platform}] exited with error: {status}" + ))); + } } } BuilderUpdate::ProcessWaitFailed { err } => { tracing::warn!( "Failed to wait for process - maybe it's hung or being debugged?: {err}" ); + if exit_on_error { + return Err(err.into()); + } } } } diff --git a/packages/manganis/manganis-core/src/asset.rs b/packages/manganis/manganis-core/src/asset.rs index a613a97e8..531c7b345 100644 --- a/packages/manganis/manganis-core/src/asset.rs +++ b/packages/manganis/manganis-core/src/asset.rs @@ -127,13 +127,15 @@ impl BundledAsset { /// ``` #[derive(PartialEq, Clone, Copy)] pub struct Asset { - /// A pointer to the bundled asset. This will be resolved after the linker has run and - /// put into the lazy asset + /// A function that returns a pointer to the bundled asset. This will be resolved after the linker has run and + /// put into the lazy asset. We use a function instead of using the pointer directly to force the compiler to + /// read the static __REFERENCE_TO_LINK_SECTION at runtime which will be offset by the hot reloading engine instead + /// of at compile time which can't be offset /// /// WARNING: Don't read this directly. Reads can get optimized away at compile time before /// the data for this is filled in by the CLI after the binary is built. Instead, use /// [`std::ptr::read_volatile`] to read the data. - bundled: &'static [u8], + bundled: fn() -> &'static [u8], } impl Debug for Asset { @@ -149,14 +151,15 @@ impl Asset { #[doc(hidden)] /// This should only be called from the macro /// Create a new asset from the bundled form of the asset and the link section - pub const fn new(bundled: &'static [u8]) -> Self { + pub const fn new(bundled: fn() -> &'static [u8]) -> Self { Self { bundled } } /// Get the bundled asset pub fn bundled(&self) -> BundledAsset { - let len = self.bundled.len(); - let ptr = self.bundled as *const [u8] as *const u8; + let bundled = (self.bundled)(); + let len = bundled.len(); + let ptr = bundled as *const [u8] as *const u8; if ptr.is_null() { panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker"); } @@ -164,7 +167,8 @@ impl Asset { for byte in 0..len { // SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and // since we are reading a u8 there are no alignment requirements - bytes = bytes.push(unsafe { std::ptr::read_volatile(ptr.add(byte)) }); + let byte = unsafe { std::ptr::read_volatile(ptr.add(byte)) }; + bytes = bytes.push(byte); } let read = bytes.read(); deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1 diff --git a/packages/manganis/manganis-macro/src/asset.rs b/packages/manganis/manganis-macro/src/asset.rs index c620c0c48..112566352 100644 --- a/packages/manganis/manganis-macro/src/asset.rs +++ b/packages/manganis/manganis-macro/src/asset.rs @@ -120,7 +120,7 @@ impl ToTokens for AssetParser { static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION; manganis::Asset::new( - __REFERENCE_TO_LINK_SECTION + || __REFERENCE_TO_LINK_SECTION ) } }) diff --git a/packages/playwright-tests/package-lock.json b/packages/playwright-tests/package-lock.json index 4d785ba86..69bd204d0 100644 --- a/packages/playwright-tests/package-lock.json +++ b/packages/playwright-tests/package-lock.json @@ -9,17 +9,17 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.52.0" + "@playwright/test": "^1.53.1" } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz", + "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.52.0" + "playwright": "1.53.1" }, "bin": { "playwright": "cli.js" @@ -44,13 +44,13 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz", + "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.53.1" }, "bin": { "playwright": "cli.js" @@ -63,9 +63,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz", + "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/packages/playwright-tests/package.json b/packages/playwright-tests/package.json index 5f2b23a8d..595cfc4c6 100644 --- a/packages/playwright-tests/package.json +++ b/packages/playwright-tests/package.json @@ -12,6 +12,6 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.52.0" + "@playwright/test": "^1.53.1" } } diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 9e445f82f..1c6074261 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -29,7 +29,7 @@ module.exports = defineConfig({ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", + trace: "retain-on-failure", // Increase the timeout for navigations to give dx time to build the project navigationTimeout: 50 * 60 * 1000, }, @@ -202,5 +202,13 @@ module.exports = defineConfig({ reuseExistingServer: !process.env.CI, stdout: "pipe", }, + { + command: + 'rm -rf ./web-hot-patch-temp && cp -r ./web-hot-patch ./web-hot-patch-temp && cd web-hot-patch-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --platform web --addr "127.0.0.1" --port 9980 --hot-patch --exit-on-error', + port: 9980, + timeout: 50 * 60 * 1000, + reuseExistingServer: !process.env.CI, + stdout: "pipe", + }, ], }); diff --git a/packages/playwright-tests/web-hot-patch/.gitignore b/packages/playwright-tests/web-hot-patch/.gitignore new file mode 100644 index 000000000..283c01d61 --- /dev/null +++ b/packages/playwright-tests/web-hot-patch/.gitignore @@ -0,0 +1,2 @@ +dist +target diff --git a/packages/playwright-tests/web-hot-patch/Cargo.toml b/packages/playwright-tests/web-hot-patch/Cargo.toml new file mode 100644 index 000000000..bcfeee4a5 --- /dev/null +++ b/packages/playwright-tests/web-hot-patch/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-playwright-web-patch-test" +version = "0.0.1" +edition = "2021" +description = "Playwright test for Dioxus Web" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +dioxus = { path = "../../dioxus", features = ["web"]} + +[workspace] \ No newline at end of file diff --git a/packages/playwright-tests/web-hot-patch/assets/alternative-style.css b/packages/playwright-tests/web-hot-patch/assets/alternative-style.css new file mode 100644 index 000000000..bd621de2d --- /dev/null +++ b/packages/playwright-tests/web-hot-patch/assets/alternative-style.css @@ -0,0 +1,3 @@ +body { + background-color: rgb(100, 100, 100); +} diff --git a/packages/playwright-tests/web-hot-patch/assets/style.css b/packages/playwright-tests/web-hot-patch/assets/style.css new file mode 100644 index 000000000..8dcca08c7 --- /dev/null +++ b/packages/playwright-tests/web-hot-patch/assets/style.css @@ -0,0 +1,3 @@ +#main { + background-color: red; +} diff --git a/packages/playwright-tests/web-hot-patch/assets/toasts.png b/packages/playwright-tests/web-hot-patch/assets/toasts.png new file mode 100644 index 0000000000000000000000000000000000000000..382298f9c7c658635ad6e2635e98ba265148cace GIT binary patch literal 20366 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 Element { + let mut num = use_signal(|| 0); + + rsx! { + document::Link { + href: CSS, + rel: "stylesheet", + } + img { + id: "toasts", + src: IMAGE, + } + button { + id: "increment-button", + onclick: move |_| { + num += 1; + }, + "Click me! Count: {num}" + } + } +} + +fn main() { + dioxus::launch(app); +} diff --git a/packages/playwright-tests/web-patch.spec.js b/packages/playwright-tests/web-patch.spec.js new file mode 100644 index 000000000..9f91638ca --- /dev/null +++ b/packages/playwright-tests/web-patch.spec.js @@ -0,0 +1,102 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); + +const hotPatchTimeout = { + timeout: 1000 * 60 * 2, // 2 minute +}; + +test("button click", async ({ page }) => { + const fs = require("fs"); + const mainPath = "web-hot-patch-temp/src/main.rs"; + var mainContent = fs.readFileSync(mainPath, "utf8"); + const stylePath = "web-hot-patch-temp/assets/style.css"; + var styleContent = fs.readFileSync(stylePath, "utf8"); + + // Reset any changes made to the main.rs and style.css files. + mainContent = mainContent.replace(/num \+= 2;/g, "num += 1;"); + mainContent = mainContent.replace("Click button! Count:", "Click me! Count:"); + mainContent = mainContent.replace("asset!('/assets/alternative-style.css')", "asset!('/assets/style.css')"); + fs.writeFileSync(mainPath, mainContent); + styleContent = styleContent.replace( + "background-color: blue;", + "background-color: red;" + ); + fs.writeFileSync(stylePath, styleContent); + + await page.goto("http://localhost:9980"); + + // ** First test make sure the initial fat build is working ** + // Expect the page to contain the counter text. + const main = page.locator("#main"); + await expect(main).toContainText("Click me! Count: 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("Click me! Count: 1"); + + // Make sure the css is applied correctly. + await expect(main).toHaveCSS("background-color", "rgb(255, 0, 0)"); + + // Make sure the image is loaded. + const headerImage = page.locator("#toasts"); + // expect the attribute src to start with /assets/toasts- + await expect(headerImage).toHaveAttribute("src", /\/assets\/toasts-/); + + // ** Then make sure the hot patch is working ** + // Then change the file to increment by 2. + const updatedContent = mainContent.replace(/num \+= 1;/g, "num += 2;"); + // Change the click me text to reflect the new increment. + const updatedContentWithText = updatedContent.replace( + "Click me! Count:", + "Click button! Count:" + ); + fs.writeFileSync(mainPath, updatedContentWithText); + + // Wait for the page to update and show the new text. + await expect(main).toContainText("Click button! Count: 1", hotPatchTimeout); + + // Now click the button again. + await button.click(); + + // Expect the count to update by 2. + await expect(main).toContainText("Click button! Count: 3"); + + // Next change just the css file to change the background color to blue. + const updatedStyleContent = styleContent.replace( + "background-color: red;", + "background-color: blue;" + ); + fs.writeFileSync(stylePath, updatedStyleContent); + + // Wait for the page to update the background color. + await expect(main).toHaveCSS( + "background-color", + "rgb(0, 0, 255)", + hotPatchTimeout + ); + + // Make sure the image is still loaded. + // expect the attribute src to start with /assets/toasts- + await expect(headerImage).toHaveAttribute("src", /\/assets\/toasts-/); + + // ** Then add a new asset to the page ** + // Switch from style to alternative-style + const updatedContentWithAlternativeStyle = updatedContentWithText.replace( + 'asset!("/assets/style.css")', + 'asset!("/assets/alternative-style.css")' + ); + fs.writeFileSync(mainPath, updatedContentWithAlternativeStyle); + + // Assert the page has the new alternative style applied. + const body = page.locator("body"); + // Log the page content to debug if needed. + console.log(await page.content()); + await expect(body).toHaveCSS( + "background-color", + "rgb(100, 100, 100)", + hotPatchTimeout + ); +});