brewlog/tests/e2e/image_tests.rs
Jon Seager cf56ac28f7
test: add integration and e2e tests for image API
Cover all image entry points: upload, get, thumbnail, delete, auth
guards, validation, datastar responses, deferred image saves via
roaster create, checkin (cafe/cup), and scan flows including the
matched-roast guard that prevents overwriting existing images.
2026-02-10 17:50:30 +00:00

125 lines
3.8 KiB
Rust

use thirtyfour::prelude::*;
use crate::helpers::auth::authenticate_browser;
use crate::helpers::browser::BrowserSession;
use crate::helpers::server_helpers::{create_default_roaster, spawn_app_with_auth};
use crate::helpers::wait::{wait_for_element, wait_for_visible};
#[tokio::test]
async fn detail_page_shows_upload_placeholder_when_no_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/{}", roaster.slug))
.await
.unwrap();
wait_for_visible(&session.driver, "h1").await.unwrap();
// Authenticated users should see the image-upload component
let upload = session
.driver
.find(By::Css("image-upload"))
.await
.expect("image-upload component should be present for authenticated user");
assert!(
upload.is_displayed().await.unwrap_or(false),
"image-upload should be visible"
);
session.quit().await;
}
#[tokio::test]
async fn detail_page_shows_image_after_upload() {
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 via the API using JavaScript fetch
session
.goto(&format!("/roasters/{}", roaster.slug))
.await
.unwrap();
wait_for_visible(&session.driver, "h1").await.unwrap();
// Use fetch() to upload a tiny PNG via the API
let upload_script = format!(
r#"
const callback = arguments[arguments.length - 1];
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 1, 1);
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", "Upload should return 204, got {status}");
// Reload the page to see the image
session
.goto(&format!("/roasters/{}", roaster.slug))
.await
.unwrap();
wait_for_visible(&session.driver, "h1").await.unwrap();
// Should now have an <img> with the thumbnail URL
let img = wait_for_element(
&session.driver,
&format!("img[src='/api/v1/roaster/{}/image']", roaster.id),
)
.await
.expect("image element should be present after upload");
assert!(
img.is_displayed().await.unwrap_or(false),
"uploaded image should be visible"
);
session.quit().await;
}
#[tokio::test]
async fn detail_page_hides_upload_when_unauthenticated() {
let app = spawn_app_with_auth().await;
let roaster = create_default_roaster(&app).await;
let session = BrowserSession::new(&app.address).await.unwrap();
// Don't authenticate
session
.goto(&format!("/roasters/{}", roaster.slug))
.await
.unwrap();
wait_for_visible(&session.driver, "h1").await.unwrap();
// image-upload should NOT be present
let result = session.driver.find(By::Css("image-upload")).await;
assert!(
result.is_err(),
"image-upload component should not be present for unauthenticated user"
);
session.quit().await;
}