Ctrl K
ComunidadGuías…

tests/e2e/edit_tests.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
use std::time::Duration;

use thirtyfour::prelude::*;

use crate::helpers::auth::authenticate_browser;
use crate::helpers::browser::BrowserSession;
use crate::helpers::forms::{fill_input, submit_visible_form};
use crate::helpers::server_helpers::{
    create_default_brew, create_default_cafe, create_default_cup, create_default_gear,
    create_default_roaster, spawn_app_with_auth,
};
use crate::helpers::wait::{
    wait_for_element, wait_for_url_contains, wait_for_url_not_contains, wait_for_visible,
};

#[tokio::test]
async fn edit_cafe_submit_tracks_coordinate_range() {
    let app = spawn_app_with_auth().await;
    let cafe = create_default_cafe(&app).await;
    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();
    session
        .goto(&format!("/cafes/{}/edit", cafe.id))
        .await
        .unwrap();

    let submit = "form[data-validate-submit] button[type='submit']";
    wait_for_element(&session.driver, &format!("{submit}:not(:disabled)"))
        .await
        .unwrap();
    fill_input(&session.driver, "latitude", "91").await.unwrap();
    wait_for_element(&session.driver, &format!("{submit}:disabled"))
        .await
        .unwrap();
    fill_input(&session.driver, "latitude", "51.5")
        .await
        .unwrap();
    wait_for_element(&session.driver, &format!("{submit}:not(:disabled)"))
        .await
        .unwrap();
    session.quit().await;
}

#[tokio::test]
async fn edit_cup_submit_tracks_searchable_selection() {
    let app = spawn_app_with_auth().await;
    let cup = create_default_cup(&app).await;
    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();
    session
        .goto(&format!("/cups/{}/edit", cup.id))
        .await
        .unwrap();

    let submit = "form[data-validate-submit] button[type='submit']";
    wait_for_element(&session.driver, &format!("{submit}:not(:disabled)"))
        .await
        .unwrap();
    session
        .driver
        .find(By::Css(
            "searchable-select[name='cafe_id'] button[aria-label='Clear selection']",
        ))
        .await
        .unwrap()
        .click()
        .await
        .unwrap();
    wait_for_element(&session.driver, &format!("{submit}:disabled"))
        .await
        .unwrap();
    crate::helpers::forms::select_searchable(&session.driver, "cafe_id", "Blue Bottle")
        .await
        .unwrap();
    wait_for_element(&session.driver, &format!("{submit}:not(:disabled)"))
        .await
        .unwrap();
    session.quit().await;
}

// ── Roaster: text fields ──────────────────────────────────────────────

#[tokio::test]
async fn edit_roaster_updates_text_fields() {
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    // Navigate to edit page
    session
        .goto(&format!("/roasters/{}/edit", roaster.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='name']")
        .await
        .unwrap();

    // Verify pre-populated value
    let name_input = session
        .driver
        .find(By::Css("input[name='name']"))
        .await
        .unwrap();
    let current_name = name_input.prop("value").await.unwrap().unwrap_or_default();
    assert_eq!(current_name, "Test Roasters");

    // Clear and update the name
    fill_input(&session.driver, "name", "Updated Roasters")
        .await
        .unwrap();

    submit_visible_form(&session.driver).await.unwrap();

    // Should redirect to the roaster detail page (edit URL also contains /roasters/)
    wait_for_url_not_contains(&session.driver, "/edit")
        .await
        .unwrap();

    // Verify the detail page reflects the change
    let heading = session.driver.find(By::Css("h1")).await.unwrap();
    let heading_text = heading.text().await.unwrap();
    assert_eq!(heading_text, "Updated Roasters");

    session.quit().await;
}

// ── Gear: text fields ─────────────────────────────────────────────────

#[tokio::test]
async fn edit_gear_updates_text_fields() {
    let app = spawn_app_with_auth().await;
    let gear = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/gear/{}/edit", gear.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='model']")
        .await
        .unwrap();

    // Verify pre-populated values
    let make_input = session
        .driver
        .find(By::Css("input[name='make']"))
        .await
        .unwrap();
    assert_eq!(
        make_input.prop("value").await.unwrap().unwrap_or_default(),
        "Comandante"
    );

    let model_input = session
        .driver
        .find(By::Css("input[name='model']"))
        .await
        .unwrap();
    assert_eq!(
        model_input.prop("value").await.unwrap().unwrap_or_default(),
        "C40 MK4"
    );

    // Update model
    fill_input(&session.driver, "model", "C40 MK5")
        .await
        .unwrap();

    submit_visible_form(&session.driver).await.unwrap();

    // Edit page URL contains /gear/ too, so wait for /edit to disappear
    wait_for_url_not_contains(&session.driver, "/edit")
        .await
        .unwrap();

    let body = session.driver.find(By::Css("body")).await.unwrap();
    let body_text = body.text().await.unwrap();
    assert!(
        body_text.contains("C40 MK5"),
        "Detail page should show updated model"
    );

    session.quit().await;
}

// ── Cafe: text + numeric fields ───────────────────────────────────────

#[tokio::test]
async fn edit_cafe_updates_text_and_numeric_fields() {
    let app = spawn_app_with_auth().await;
    let cafe = create_default_cafe(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/cafes/{}/edit", cafe.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='name']")
        .await
        .unwrap();

    // Verify pre-populated values
    let name_input = session
        .driver
        .find(By::Css("input[name='name']"))
        .await
        .unwrap();
    assert_eq!(
        name_input.prop("value").await.unwrap().unwrap_or_default(),
        "Blue Bottle"
    );

    let lat_input = session
        .driver
        .find(By::Css("input[name='latitude']"))
        .await
        .unwrap();
    let lat_value = lat_input.prop("value").await.unwrap().unwrap_or_default();
    assert!(
        lat_value.starts_with("37.77"),
        "Latitude should be pre-populated, got: {lat_value}"
    );

    // Update name and city
    fill_input(&session.driver, "name", "Verve Coffee")
        .await
        .unwrap();
    fill_input(&session.driver, "city", "Santa Cruz")
        .await
        .unwrap();
    fill_input(&session.driver, "latitude", "36.9741")
        .await
        .unwrap();

    submit_visible_form(&session.driver).await.unwrap();

    // Edit page URL contains /cafes/ too, so wait for /edit to disappear
    wait_for_url_not_contains(&session.driver, "/edit")
        .await
        .unwrap();

    let body = session.driver.find(By::Css("body")).await.unwrap();
    let body_text = body.text().await.unwrap();
    assert!(
        body_text.contains("Verve Coffee"),
        "Detail page should show updated name"
    );
    assert!(
        body_text.contains("Santa Cruz"),
        "Detail page should show updated city"
    );

    session.quit().await;
}

// ── Brew: numeric fields with +/- adjusters ───────────────────────────

#[tokio::test]
async fn edit_brew_updates_numeric_fields() {
    let app = spawn_app_with_auth().await;
    let brew = create_default_brew(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/brews/{}/edit", brew.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='coffee_weight']")
        .await
        .unwrap();

    // Verify pre-populated coffee weight
    let weight_input = session
        .driver
        .find(By::Css("input[name='coffee_weight']"))
        .await
        .unwrap();
    let current_weight = weight_input
        .prop("value")
        .await
        .unwrap()
        .unwrap_or_default();
    assert_eq!(
        current_weight, "15",
        "Coffee weight should be pre-populated"
    );

    // Update coffee weight via direct input
    fill_input(&session.driver, "coffee_weight", "18")
        .await
        .unwrap();

    // Update water volume via direct input
    fill_input(&session.driver, "water_volume", "300")
        .await
        .unwrap();

    // Update grind setting
    fill_input(&session.driver, "grind_setting", "28")
        .await
        .unwrap();

    submit_visible_form(&session.driver).await.unwrap();

    // Edit page URL contains /brews/ too, so wait for /edit to disappear
    wait_for_url_not_contains(&session.driver, "/edit")
        .await
        .unwrap();

    // Verify the brew detail page shows updated values
    let body = session.driver.find(By::Css("body")).await.unwrap();
    let body_text = body.text().await.unwrap();
    assert!(
        body_text.contains("18"),
        "Should show updated coffee weight"
    );
    assert!(
        body_text.contains("300"),
        "Should show updated water volume"
    );

    session.quit().await;
}

// ── Brew: closed bag ──────────────────────────────────────────────────

#[tokio::test]
async fn edit_brew_with_closed_bag_keeps_bag_fixed_and_submits() {
    let app = spawn_app_with_auth().await;
    let brew = create_default_brew(&app).await;

    let response = reqwest::Client::new()
        .put(app.api_url(&format!("/bags/{}", brew.bag_id)))
        .bearer_auth(app.auth_token.as_ref().unwrap())
        .json(&serde_json::json!({ "closed": true }))
        .send()
        .await
        .unwrap();
    assert!(response.status().is_success());

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/brews/{}/edit", brew.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='coffee_weight']")
        .await
        .unwrap();

    assert!(
        session
            .driver
            .find(By::Css("searchable-select[name='bag_id']"))
            .await
            .is_err(),
        "A closed bag should not be changeable"
    );

    let bag_input = session
        .driver
        .find(By::Css("input[type='hidden'][name='bag_id']"))
        .await
        .expect("The closed bag ID should still be submitted");
    assert_eq!(
        bag_input.prop("value").await.unwrap().unwrap_or_default(),
        brew.bag_id.to_string()
    );

    let bag_display = session
        .driver
        .find(By::Css("[data-closed-bag]"))
        .await
        .expect("The closed bag name should be displayed");
    assert!(bag_display.text().await.unwrap().contains("Test Roast"));

    fill_input(&session.driver, "coffee_weight", "14")
        .await
        .unwrap();
    submit_visible_form(&session.driver).await.unwrap();
    wait_for_url_not_contains(&session.driver, "/edit")
        .await
        .unwrap();

    let body_text = session
        .driver
        .find(By::Css("body"))
        .await
        .unwrap()
        .text()
        .await
        .unwrap();
    assert!(body_text.contains("14"), "The brew update should be saved");

    session.quit().await;
}

// ── Brew: +/- adjuster buttons ────────────────────────────────────────

#[tokio::test]
async fn edit_brew_adjuster_buttons_change_values() {
    let app = spawn_app_with_auth().await;
    let brew = create_default_brew(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/brews/{}/edit", brew.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='coffee_weight']")
        .await
        .unwrap();

    // Find the + button next to coffee weight (it's the sibling after the input)
    let weight_plus = session
        .driver
        .execute(
            r#"
            const input = document.querySelector("input[name='coffee_weight']");
            return input.parentElement.querySelector("button:last-of-type");
            "#,
            vec![],
        )
        .await
        .unwrap();
    let weight_plus_el = weight_plus.element().unwrap();

    // Click + three times to add 1.5g (0.5 per click)
    weight_plus_el.click().await.unwrap();
    weight_plus_el.click().await.unwrap();
    weight_plus_el.click().await.unwrap();

    // Wait for Datastar signal to update the input
    tokio::time::sleep(Duration::from_millis(200)).await;

    let weight_input = session
        .driver
        .find(By::Css("input[name='coffee_weight']"))
        .await
        .unwrap();
    let updated_weight = weight_input
        .prop("value")
        .await
        .unwrap()
        .unwrap_or_default();
    assert_eq!(updated_weight, "16.5", "Weight should be 15 + 3*0.5 = 16.5");

    fill_input(&session.driver, "coffee_weight", "0.5")
        .await
        .unwrap();
    wait_for_element(
        &session.driver,
        "form[data-validate-submit] button[type='submit']:disabled",
    )
    .await
    .unwrap();
    weight_plus_el.click().await.unwrap();
    wait_for_element(
        &session.driver,
        "form[data-validate-submit] button[type='submit']:not(:disabled)",
    )
    .await
    .unwrap();

    session.quit().await;
}

// ── Cup: edit with searchable-select ──────────────────────────────────

#[tokio::test]
async fn edit_cup_updates_searchable_selects() {
    let app = spawn_app_with_auth().await;
    let cup = create_default_cup(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/cups/{}/edit", cup.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "searchable-select[name='roast_id']")
        .await
        .unwrap();

    // The searchable-select should show the current roast name in its display span
    // (when pre-selected, the component hides the search input and shows a span)
    let roast_select = session
        .driver
        .find(By::Css("searchable-select[name='roast_id']"))
        .await
        .unwrap();
    let display_span = roast_select
        .find(By::Css("span.input-field"))
        .await
        .unwrap();
    let display_text = display_span.text().await.unwrap();
    assert!(
        !display_text.is_empty(),
        "Searchable select should show the pre-populated roast name"
    );

    // Submit without changes — should still redirect successfully
    submit_visible_form(&session.driver).await.unwrap();

    // Edit page URL contains /cups/ too, so wait for /edit to disappear
    wait_for_url_not_contains(&session.driver, "/edit")
        .await
        .unwrap();

    session.quit().await;
}

// ── Edit form with image upload ───────────────────────────────────────

#[tokio::test]
async fn edit_roaster_shows_existing_image_and_can_replace() {
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    // First, upload an image via the API
    session.goto("/login").await.unwrap();
    let upload_script = format!(
        r#"
        const callback = arguments[arguments.length - 1];
        const canvas = document.createElement('canvas');
        canvas.width = 4;
        canvas.height = 4;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'blue';
        ctx.fillRect(0, 0, 4, 4);
        const dataUrl = canvas.toDataURL('image/png');
        fetch('/api/v1/roaster/{}/image', {{
            method: 'PUT',
            headers: {{ 'Content-Type': 'application/json' }},
            body: JSON.stringify({{ image: dataUrl }})
        }}).then(r => callback(r.status.toString())).catch(e => callback('error: ' + e));
        "#,
        roaster.id
    );
    let result = session
        .driver
        .execute_async(&upload_script, vec![])
        .await
        .unwrap();
    let status = result.json().as_str().unwrap_or("").to_string();
    assert_eq!(status, "204", "Image upload should return 204");

    // Navigate to the edit page
    session
        .goto(&format!("/roasters/{}/edit", roaster.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='name']")
        .await
        .unwrap();

    // Verify the existing image preview is shown
    let existing_preview = session
        .driver
        .find(By::Css("[id$='-existing']"))
        .await
        .expect("Existing image preview should be present");
    assert!(
        existing_preview.is_displayed().await.unwrap_or(false),
        "Existing image preview should be visible"
    );

    // Verify Replace and Remove buttons are present
    let replace_btn = existing_preview
        .find(By::Css("image-upload[mode='deferred']"))
        .await
        .expect("Replace button should be present");
    assert!(replace_btn.is_displayed().await.unwrap_or(false));

    let remove_btn = existing_preview
        .find(By::Css("button"))
        .await
        .expect("Remove button should be present");
    assert!(remove_btn.is_displayed().await.unwrap_or(false));

    // Simulate uploading a replacement image via JavaScript
    // (we can't trigger the file picker in headless mode, but we can set the hidden input)
    let replace_script = r#"
        const callback = arguments[arguments.length - 1];
        const canvas = document.createElement('canvas');
        canvas.width = 4;
        canvas.height = 4;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'green';
        ctx.fillRect(0, 0, 4, 4);
        const dataUrl = canvas.toDataURL('image/jpeg');
        const hiddenInput = document.getElementById('edit-roaster-image');
        hiddenInput.value = dataUrl;
        // Remove existing preview to simulate what image-upload.js does
        const existing = document.getElementById('edit-roaster-image-existing');
        if (existing) existing.remove();
        callback('done');
    "#;
    session
        .driver
        .execute_async(replace_script, vec![])
        .await
        .unwrap();

    // Existing preview should be removed
    let result = session
        .driver
        .find(By::Css("#edit-roaster-image-existing"))
        .await;
    assert!(
        result.is_err(),
        "Existing image preview should be removed after replacement"
    );

    // Hidden input should have the new data URL
    let hidden_input = session
        .driver
        .find(By::Css("#edit-roaster-image"))
        .await
        .unwrap();
    let value = hidden_input
        .prop("value")
        .await
        .unwrap()
        .unwrap_or_default();
    assert!(
        value.starts_with("data:image/jpeg"),
        "Hidden input should contain the replacement image data URL"
    );

    session.quit().await;
}

#[tokio::test]
async fn edit_form_without_image_shows_upload_area() {
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/roasters/{}/edit", roaster.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='name']")
        .await
        .unwrap();

    // No existing image — should show the dashed upload area
    let result = session.driver.find(By::Css("[id$='-existing']")).await;
    assert!(
        result.is_err(),
        "Existing image preview should not be present when there is no image"
    );

    // The deferred upload component should be visible
    let upload = session
        .driver
        .find(By::Css("image-upload[mode='deferred']"))
        .await
        .expect("Deferred upload area should be present");
    assert!(
        upload.is_displayed().await.unwrap_or(false),
        "Deferred upload area should be visible"
    );

    session.quit().await;
}

// ── Edit form image removal ───────────────────────────────────────────

#[tokio::test]
async fn edit_form_remove_image_calls_delete_api() {
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    // Upload an image first
    session.goto("/login").await.unwrap();
    let upload_script = format!(
        r#"
        const callback = arguments[arguments.length - 1];
        const canvas = document.createElement('canvas');
        canvas.width = 4;
        canvas.height = 4;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'red';
        ctx.fillRect(0, 0, 4, 4);
        const dataUrl = canvas.toDataURL('image/png');
        fetch('/api/v1/roaster/{}/image', {{
            method: 'PUT',
            headers: {{ 'Content-Type': 'application/json' }},
            body: JSON.stringify({{ image: dataUrl }})
        }}).then(r => callback(r.status.toString())).catch(e => callback('error: ' + e));
        "#,
        roaster.id
    );
    let result = session
        .driver
        .execute_async(&upload_script, vec![])
        .await
        .unwrap();
    assert_eq!(
        result.json().as_str().unwrap_or(""),
        "204",
        "Image upload should succeed"
    );

    // Navigate to edit page
    session
        .goto(&format!("/roasters/{}/edit", roaster.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "[id$='-existing']")
        .await
        .unwrap();

    // Click the Remove button (need to accept the confirm dialog)
    let remove_script = format!(
        r#"
        const callback = arguments[arguments.length - 1];
        fetch('/api/v1/roaster/{}/image', {{ method: 'DELETE' }})
            .then(r => {{
                document.querySelector("[id$='-existing']").remove();
                callback(r.status.toString());
            }})
            .catch(e => callback('error: ' + e));
        "#,
        roaster.id
    );
    let result = session
        .driver
        .execute_async(&remove_script, vec![])
        .await
        .unwrap();
    assert_eq!(
        result.json().as_str().unwrap_or(""),
        "204",
        "Image delete should return 204"
    );

    // Preview should be gone
    let result = session.driver.find(By::Css("[id$='-existing']")).await;
    assert!(result.is_err(), "Image preview should be removed from DOM");

    // Verify image is actually deleted from server
    let check_script = format!(
        r#"
        const callback = arguments[arguments.length - 1];
        fetch('/api/v1/roaster/{}/image')
            .then(r => callback(r.status.toString()))
            .catch(e => callback('error: ' + e));
        "#,
        roaster.id
    );
    let result = session
        .driver
        .execute_async(&check_script, vec![])
        .await
        .unwrap();
    assert_eq!(
        result.json().as_str().unwrap_or(""),
        "404",
        "Image should be deleted from server"
    );

    session.quit().await;
}

// ── Cancel button ─────────────────────────────────────────────────────

#[tokio::test]
async fn edit_form_cancel_navigates_back() {
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    // First visit the detail page (so there's a history entry to go back to)
    session
        .goto(&format!("/roasters/{}", roaster.slug))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "h1").await.unwrap();

    // Click the Edit link to go to the edit page
    let edit_link = session
        .driver
        .find(By::Css("a[href*='/edit']"))
        .await
        .unwrap();
    edit_link.click().await.unwrap();

    wait_for_url_contains(&session.driver, "/edit")
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='name']")
        .await
        .unwrap();

    // Click Cancel
    let cancel_btn = session
        .driver
        .find(By::Css("button[onclick='history.back()']"))
        .await
        .unwrap();
    cancel_btn.click().await.unwrap();

    // Should go back to the detail page
    tokio::time::sleep(Duration::from_millis(500)).await;
    let url = session.driver.current_url().await.unwrap();
    assert!(
        !url.as_str().contains("/edit"),
        "Should navigate away from edit page, got: {url}"
    );

    session.quit().await;
}

// ── Edit with deferred image save ─────────────────────────────────────

#[tokio::test]
async fn edit_roaster_with_new_image_saves_image() {
    let app = spawn_app_with_auth().await;
    let roaster = create_default_roaster(&app).await;

    let session = BrowserSession::new(&app.address).await.unwrap();
    authenticate_browser(&session, &app).await.unwrap();

    session
        .goto(&format!("/roasters/{}/edit", roaster.id))
        .await
        .unwrap();
    wait_for_visible(&session.driver, "input[name='name']")
        .await
        .unwrap();

    // Set the hidden image input to a data URL (simulating deferred upload)
    let set_image_script = r#"
        const canvas = document.createElement('canvas');
        canvas.width = 4;
        canvas.height = 4;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'purple';
        ctx.fillRect(0, 0, 4, 4);
        document.getElementById('edit-roaster-image').value = canvas.toDataURL('image/jpeg');
    "#;
    session
        .driver
        .execute(set_image_script, vec![])
        .await
        .unwrap();

    // Update name too
    fill_input(&session.driver, "name", "Roaster With Image")
        .await
        .unwrap();

    submit_visible_form(&session.driver).await.unwrap();

    // Edit page URL contains /roasters/ too, so wait for /edit to disappear
    wait_for_url_not_contains(&session.driver, "/edit")
        .await
        .unwrap();

    // Verify the name was updated
    let heading = session.driver.find(By::Css("h1")).await.unwrap();
    assert_eq!(heading.text().await.unwrap(), "Roaster With Image");

    // Verify the image was saved by checking the API
    let check_script = format!(
        r#"
        const callback = arguments[arguments.length - 1];
        fetch('/api/v1/roaster/{}/image')
            .then(r => callback(r.status.toString()))
            .catch(e => callback('error: ' + e));
        "#,
        roaster.id
    );
    let result = session
        .driver
        .execute_async(&check_script, vec![])
        .await
        .unwrap();
    assert_eq!(
        result.json().as_str().unwrap_or(""),
        "200",
        "Image should be saved to server after edit with deferred upload"
    );

    session.quit().await;
}