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 <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
6cc379718b
commit
2071be2b67
20 files changed
+365
-125
No files matched your search
@@ -200,6 +200,11 @@ jobs:
|
|||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- { toolchain: 1.86.0 }
|
||||||
|
- { toolchain: beta }
|
||||||
steps:
|
steps:
|
||||||
# Do our best to cache the toolchain and node install steps
|
# Do our best to cache the toolchain and node install steps
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -207,12 +212,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.86.0
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: ${{ matrix.platform.toolchain }}
|
||||||
targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
|
targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
|
key: "playwright-${{ matrix.platform.toolchain }}-${{ runner.os }}"
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
cache-on-failure: "true"
|
cache-on-failure: "true"
|
||||||
- name: Wipe dx cache
|
- name: Wipe dx cache
|
||||||
@@ -220,6 +226,9 @@ jobs:
|
|||||||
rm -rf ./target/dx
|
rm -rf ./target/dx
|
||||||
- name: Playwright
|
- name: Playwright
|
||||||
working-directory: ./packages/playwright-tests
|
working-directory: ./packages/playwright-tests
|
||||||
|
env:
|
||||||
|
# The hot patch test requires incremental compilation
|
||||||
|
CARGO_INCREMENTAL: 1
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm install -D @playwright/test
|
npm install -D @playwright/test
|
||||||
@@ -228,7 +237,7 @@ jobs:
|
|||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report-${{ matrix.platform.toolchain }}-${{ runner.os }}
|
||||||
path: ./packages/playwright-tests/playwright-report/
|
path: ./packages/playwright-tests/playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
/packages/playwright-tests/web/dist
|
/packages/playwright-tests/web/dist
|
||||||
/packages/playwright-tests/fullstack/dist
|
/packages/playwright-tests/fullstack/dist
|
||||||
/packages/playwright-tests/test-results
|
/packages/playwright-tests/test-results
|
||||||
|
/packages/playwright-tests/web-hot-patch-temp
|
||||||
/dist
|
/dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/examples/assets/test_video.mp4
|
/examples/assets/test_video.mp4
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ use manganis::BundledAsset;
|
|||||||
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
||||||
use pdb::FallibleIterator;
|
use pdb::FallibleIterator;
|
||||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||||
use wasmparser::BinaryReader;
|
|
||||||
|
|
||||||
/// Extract all manganis symbols and their sections from the given object file.
|
/// Extract all manganis symbols and their sections from the given object file.
|
||||||
fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
|
fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
|
||||||
@@ -50,7 +49,7 @@ fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
|
|||||||
file.symbols()
|
file.symbols()
|
||||||
.filter(|symbol| {
|
.filter(|symbol| {
|
||||||
if let Ok(name) = symbol.name() {
|
if let Ok(name) = symbol.name() {
|
||||||
name.contains("__MANGANIS__")
|
looks_like_manganis_symbol(name)
|
||||||
} else {
|
} else {
|
||||||
false
|
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.
|
/// Find the offsets of any manganis symbols in the given file.
|
||||||
fn find_symbol_offsets<'a, R: ReadRef<'a>>(
|
fn find_symbol_offsets<'a, R: ReadRef<'a>>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
@@ -175,105 +178,113 @@ fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result<
|
|||||||
Ok(offsets)
|
Ok(offsets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn eval_walrus_global_expr(module: &walrus::Module, expr: &walrus::ConstExpr) -> Option<u64> {
|
||||||
|
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.
|
/// Find the offsets of any manganis symbols in the wasm file.
|
||||||
fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
|
fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
|
||||||
file_contents: &[u8],
|
file_contents: &[u8],
|
||||||
file: &File<'a, R>,
|
file: &File<'a, R>,
|
||||||
) -> Result<Vec<u64>> {
|
) -> Result<Vec<u64>> {
|
||||||
// Parse the wasm file to find the globals
|
let Some(section) = file
|
||||||
let parser = wasmparser::Parser::new(0);
|
.sections()
|
||||||
|
.find(|section| section.name() == Ok("<data>"))
|
||||||
|
else {
|
||||||
|
tracing::error!("Failed to find <data> 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 <data> 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
|
// Translate the section_relative_address to the file offset
|
||||||
let mut global_values = Vec::new();
|
// WASM files have a section address of 0 in object, reparse the data section with wasmparser
|
||||||
for section in parser.parse_all(file_contents) {
|
// to get the correct address and section start
|
||||||
let Ok(wasmparser::Payload::GlobalSection(global_section)) = section else {
|
// 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;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
global_values = global_section
|
let walrus::GlobalKind::Local(pointer) = module.globals.get(global).kind else {
|
||||||
.into_iter()
|
continue;
|
||||||
.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::<Vec<_>>();
|
|
||||||
}
|
|
||||||
let mut offsets = Vec::new();
|
|
||||||
|
|
||||||
for (symbol, section) in manganis_symbols(file) {
|
let Some(virtual_address) = eval_walrus_global_expr(&module, &pointer) else {
|
||||||
let virtual_address = symbol.address();
|
|
||||||
|
|
||||||
let Some((_, section_range_end)) = section.file_range() else {
|
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Found __MANGANIS__ symbol {:?} in section {}, but the section has no file range",
|
"Found __MANGANIS__ symbol {:?} in WASM file, but the global expression could not be evaluated",
|
||||||
symbol.name(),
|
export.name
|
||||||
section.index()
|
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let section_size = section.data()?.len() as u64;
|
|
||||||
let section_start = section_range_end - section_size;
|
let section_relative_address: u64 = ((virtual_address as i128)
|
||||||
// Translate the section_relative_address to the file offset
|
- main_memory_offset as i128)
|
||||||
// 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)
|
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("Virtual address should be greater than or equal to section address");
|
.expect("Virtual address should be greater than or equal to section address");
|
||||||
let file_offset = data_start_offset + section_relative_address;
|
let file_offset = data_start_offset + section_relative_address;
|
||||||
|
|
||||||
offsets.push(file_offset);
|
offsets.push(file_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +315,7 @@ pub(crate) fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetMa
|
|||||||
|
|
||||||
if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
|
if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
|
||||||
{
|
{
|
||||||
|
tracing::debug!("Found asset at offset {offset}: {:?}", bundled_asset);
|
||||||
assets.push(bundled_asset);
|
assets.push(bundled_asset);
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions.");
|
tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions.");
|
||||||
@@ -317,6 +329,7 @@ pub(crate) fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetMa
|
|||||||
|
|
||||||
// Write back the assets to the binary file
|
// Write back the assets to the binary file
|
||||||
for (offset, asset) in offsets.into_iter().zip(&assets) {
|
for (offset, asset) in offsets.into_iter().zip(&assets) {
|
||||||
|
tracing::debug!("Writing asset to offset {offset}: {:?}", asset);
|
||||||
let new_data = ConstVec::new();
|
let new_data = ConstVec::new();
|
||||||
let new_data = const_serialize::serialize_const(asset, new_data);
|
let new_data = const_serialize::serialize_const(asset, new_data);
|
||||||
|
|
||||||
@@ -324,6 +337,9 @@ pub(crate) fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetMa
|
|||||||
// Write the modified binary data back to the file
|
// Write the modified binary data back to the file
|
||||||
file.write_all(new_data.as_ref())?;
|
file.write_all(new_data.as_ref())?;
|
||||||
}
|
}
|
||||||
|
// Ensure the file is flushed to disk
|
||||||
|
file.sync_all()
|
||||||
|
.context("Failed to sync file after writing assets")?;
|
||||||
|
|
||||||
// If the file is a macos binary, we need to re-sign the modified binary
|
// If the file is a macos binary, we need to re-sign the modified binary
|
||||||
if object_file.format() == object::BinaryFormat::MachO {
|
if object_file.format() == object::BinaryFormat::MachO {
|
||||||
|
|||||||
@@ -587,13 +587,18 @@ impl AppBuilder {
|
|||||||
let original = self.build.main_exe();
|
let original = self.build.main_exe();
|
||||||
let new = self.build.patch_exe(res.time_start);
|
let new = self.build.patch_exe(res.time_start);
|
||||||
let triple = self.build.triple.clone();
|
let triple = self.build.triple.clone();
|
||||||
let original_artifacts = self.artifacts.as_ref().unwrap();
|
|
||||||
let asset_dir = self.build.asset_dir();
|
let asset_dir = self.build.asset_dir();
|
||||||
|
|
||||||
for bundled in res.assets.assets() {
|
for bundled in res.assets.assets() {
|
||||||
|
let original_artifacts = self.artifacts.as_mut().unwrap();
|
||||||
|
|
||||||
if original_artifacts.assets.contains(bundled) {
|
if original_artifacts.assets.contains(bundled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a new asset, insert it into the artifacts so we can track it when hot reloading
|
||||||
|
original_artifacts.assets.insert_asset(*bundled);
|
||||||
|
|
||||||
let from = dunce::canonicalize(PathBuf::from(bundled.absolute_source_path()))?;
|
let from = dunce::canonicalize(PathBuf::from(bundled.absolute_source_path()))?;
|
||||||
|
|
||||||
let to = asset_dir.join(bundled.bundled_path());
|
let to = asset_dir.join(bundled.bundled_path());
|
||||||
|
|||||||
@@ -597,26 +597,41 @@ fn create_wasm_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Result<J
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let name = import.name.as_str().to_string();
|
||||||
|
|
||||||
if let Some(table_idx) = name_to_ifunc_old.get(import.name.as_str()) {
|
if let Some(table_idx) = name_to_ifunc_old.get(import.name.as_str()) {
|
||||||
let name = import.name.as_str().to_string();
|
|
||||||
new.imports.delete(env_func_import);
|
new.imports.delete(env_func_import);
|
||||||
convert_import_to_ifunc_call(
|
convert_import_to_ifunc_call(
|
||||||
&mut new,
|
&mut new,
|
||||||
ifunc_table_initializer,
|
ifunc_table_initializer,
|
||||||
func_id,
|
func_id,
|
||||||
*table_idx,
|
*table_idx,
|
||||||
name,
|
name.clone(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if name_is_bindgen_symbol(&name) {
|
||||||
|
new.imports.delete(env_func_import);
|
||||||
|
convert_import_to_ifunc_call(&mut new, ifunc_table_initializer, func_id, 0, name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire up the preserved intrinsic functions that we saved before running wasm-bindgen to the expected
|
// Wire up the preserved intrinsic functions that we saved before running wasm-bindgen to the expected
|
||||||
// imports from the patch.
|
// imports from the patch.
|
||||||
for import_id in wbg_funcs {
|
for import_id in wbg_funcs {
|
||||||
let import = new.imports.get_mut(import_id);
|
let import = new.imports.get_mut(import_id);
|
||||||
|
let ImportKind::Function(func_id) = import.kind else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
import.module = "env".into();
|
import.module = "env".into();
|
||||||
import.name = format!("__saved_wbg_{}", import.name);
|
import.name = format!("__saved_wbg_{}", import.name);
|
||||||
|
|
||||||
|
if name_is_bindgen_symbol(&import.name) {
|
||||||
|
let name = import.name.as_str().to_string();
|
||||||
|
new.imports.delete(import_id);
|
||||||
|
convert_import_to_ifunc_call(&mut new, ifunc_table_initializer, func_id, 0, name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wipe away the unnecessary sections
|
// Wipe away the unnecessary sections
|
||||||
|
|||||||
@@ -791,19 +791,12 @@ impl BuildRequest {
|
|||||||
ctx.status_start_bundle();
|
ctx.status_start_bundle();
|
||||||
|
|
||||||
self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
|
self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
|
||||||
.await
|
.await?;
|
||||||
.context("Failed to write main executable")?;
|
self.write_frameworks(ctx, &artifacts.direct_rustc).await?;
|
||||||
self.write_frameworks(ctx, &artifacts.direct_rustc)
|
self.write_assets(ctx, &artifacts.assets).await?;
|
||||||
.await
|
|
||||||
.context("Failed to write frameworks")?;
|
|
||||||
self.write_assets(ctx, &artifacts.assets)
|
|
||||||
.await
|
|
||||||
.context("Failed to write assets")?;
|
|
||||||
self.write_metadata().await?;
|
self.write_metadata().await?;
|
||||||
self.optimize(ctx).await?;
|
self.optimize(ctx).await?;
|
||||||
self.assemble(ctx)
|
self.assemble(ctx).await?;
|
||||||
.await
|
|
||||||
.context("Failed to assemble app bundle")?;
|
|
||||||
|
|
||||||
tracing::debug!("Bundle created at {}", self.root_dir().display());
|
tracing::debug!("Bundle created at {}", self.root_dir().display());
|
||||||
}
|
}
|
||||||
@@ -1420,7 +1413,7 @@ impl BuildRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now extract the assets from the fat binary
|
// Now extract the assets from the fat binary
|
||||||
self.collect_assets(&self.patch_exe(artifacts.time_start), ctx)?;
|
artifacts.assets = self.collect_assets(&self.patch_exe(artifacts.time_start), ctx)?;
|
||||||
|
|
||||||
// If this is a web build, reset the index.html file in case it was modified by SSG
|
// If this is a web build, reset the index.html file in case it was modified by SSG
|
||||||
self.write_index_html(&artifacts.assets)
|
self.write_index_html(&artifacts.assets)
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ pub(crate) struct ServeArgs {
|
|||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub(crate) force_sequential: bool,
|
pub(crate) force_sequential: bool,
|
||||||
|
|
||||||
|
/// Exit the CLI after running into an error. This is mainly used to test hot patching internally
|
||||||
|
#[clap(long)]
|
||||||
|
#[clap(hide = true)]
|
||||||
|
pub(crate) exit_on_error: bool,
|
||||||
|
|
||||||
/// Platform-specific arguments for the build
|
/// Platform-specific arguments for the build
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub(crate) platform_args: CommandWithPlatformOverrides<PlatformServeArgs>,
|
pub(crate) platform_args: CommandWithPlatformOverrides<PlatformServeArgs>,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
styles::{GLOW_STYLE, LINK_STYLE},
|
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;
|
mod ansi_buffer;
|
||||||
@@ -11,6 +12,7 @@ mod runner;
|
|||||||
mod server;
|
mod server;
|
||||||
mod update;
|
mod update;
|
||||||
|
|
||||||
|
use dioxus_dx_wire_format::BuildStage;
|
||||||
pub(crate) use output::*;
|
pub(crate) use output::*;
|
||||||
pub(crate) use runner::*;
|
pub(crate) use runner::*;
|
||||||
pub(crate) use server::*;
|
pub(crate) use server::*;
|
||||||
@@ -38,6 +40,7 @@ pub(crate) use update::*;
|
|||||||
/// to a dynamic one on the fly.
|
/// to a dynamic one on the fly.
|
||||||
pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
|
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
|
// 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 builder = AppServer::start(args).await?;
|
||||||
let mut devserver = WebServer::start(&builder)?;
|
let mut devserver = WebServer::start(&builder)?;
|
||||||
let mut screen = Output::start(builder.interactive).await?;
|
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
|
// And then open the app if it's ready
|
||||||
match update {
|
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::Progress { .. } => {}
|
||||||
BuilderUpdate::CompilerMessage { message } => {
|
BuilderUpdate::CompilerMessage { message } => {
|
||||||
screen.push_cargo_log(message);
|
screen.push_cargo_log(message);
|
||||||
}
|
}
|
||||||
BuilderUpdate::BuildFailed { err } => {
|
BuilderUpdate::BuildFailed { err } => {
|
||||||
tracing::error!("Build failed: {}", err);
|
tracing::error!("Build failed: {}", err);
|
||||||
|
if exit_on_error {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BuilderUpdate::BuildReady { bundle } => match bundle.mode {
|
BuilderUpdate::BuildReady { bundle } => match bundle.mode {
|
||||||
BuildMode::Thin { ref cache, .. } => {
|
BuildMode::Thin { ref cache, .. } => {
|
||||||
@@ -191,12 +215,20 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tracing::error!("Application [{platform}] exited with error: {status}");
|
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 } => {
|
BuilderUpdate::ProcessWaitFailed { err } => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Failed to wait for process - maybe it's hung or being debugged?: {err}"
|
"Failed to wait for process - maybe it's hung or being debugged?: {err}"
|
||||||
);
|
);
|
||||||
|
if exit_on_error {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,13 +127,15 @@ impl BundledAsset {
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
pub struct Asset {
|
pub struct Asset {
|
||||||
/// A pointer to the bundled asset. This will be resolved after the linker has run and
|
/// 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
|
/// 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
|
/// 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
|
/// 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.
|
/// [`std::ptr::read_volatile`] to read the data.
|
||||||
bundled: &'static [u8],
|
bundled: fn() -> &'static [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Asset {
|
impl Debug for Asset {
|
||||||
@@ -149,14 +151,15 @@ impl Asset {
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// This should only be called from the macro
|
/// This should only be called from the macro
|
||||||
/// Create a new asset from the bundled form of the asset and the link section
|
/// 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 }
|
Self { bundled }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the bundled asset
|
/// Get the bundled asset
|
||||||
pub fn bundled(&self) -> BundledAsset {
|
pub fn bundled(&self) -> BundledAsset {
|
||||||
let len = self.bundled.len();
|
let bundled = (self.bundled)();
|
||||||
let ptr = self.bundled as *const [u8] as *const u8;
|
let len = bundled.len();
|
||||||
|
let ptr = bundled as *const [u8] as *const u8;
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker");
|
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 {
|
for byte in 0..len {
|
||||||
// SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and
|
// 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
|
// 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();
|
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
|
deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ impl ToTokens for AssetParser {
|
|||||||
static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION;
|
static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION;
|
||||||
|
|
||||||
manganis::Asset::new(
|
manganis::Asset::new(
|
||||||
__REFERENCE_TO_LINK_SECTION
|
|| __REFERENCE_TO_LINK_SECTION
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+12
-12
@@ -9,17 +9,17 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.52.0"
|
"@playwright/test": "^1.53.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.52.0",
|
"version": "1.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
|
||||||
"integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
|
"integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.52.0"
|
"playwright": "1.53.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.52.0",
|
"version": "1.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
|
||||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
"integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0"
|
"playwright-core": "1.53.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.52.0",
|
"version": "1.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
|
||||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
"integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.52.0"
|
"@playwright/test": "^1.53.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ module.exports = defineConfig({
|
|||||||
// baseURL: 'http://127.0.0.1:3000',
|
// baseURL: 'http://127.0.0.1:3000',
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* 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
|
// Increase the timeout for navigations to give dx time to build the project
|
||||||
navigationTimeout: 50 * 60 * 1000,
|
navigationTimeout: 50 * 60 * 1000,
|
||||||
},
|
},
|
||||||
@@ -202,5 +202,13 @@ module.exports = defineConfig({
|
|||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
stdout: "pipe",
|
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",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
target
|
||||||
@@ -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]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
body {
|
||||||
|
background-color: rgb(100, 100, 100);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#main {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const CSS: Asset = asset!("/assets/style.css");
|
||||||
|
const IMAGE: Asset = asset!("/assets/toasts.png");
|
||||||
|
|
||||||
|
fn app() -> 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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user