diff --git a/CMakeLists.txt b/CMakeLists.txt
index c9c9cf4..65307b7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -139,3 +139,33 @@ add_executable(test_history
src/history.cpp
tests/test_history.cpp
)
+
+# 异步图片下载测试
+add_executable(test_async_images
+ src/http_client.cpp
+ tests/test_async_images.cpp
+)
+
+target_link_libraries(test_async_images
+ CURL::libcurl
+)
+
+# 简单图片测试
+add_executable(test_simple_image
+ src/http_client.cpp
+ tests/test_simple_image.cpp
+)
+
+target_link_libraries(test_simple_image
+ CURL::libcurl
+)
+
+# 最小图片测试
+add_executable(test_image_minimal
+ src/http_client.cpp
+ tests/test_image_minimal.cpp
+)
+
+target_link_libraries(test_image_minimal
+ CURL::libcurl
+)
diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md
index bc8a0b2..396dedb 100644
--- a/NEXT_STEPS.md
+++ b/NEXT_STEPS.md
@@ -1,9 +1,9 @@
# TUT 2.0 - 下次继续从这里开始
## 当前位置
-- **阶段**: Phase 9 - 性能优化和测试工具 (已完成!)
-- **进度**: 图片缓存、测试工具、文档完善
-- **最后提交**: `feat: Add comprehensive testing tools and improve help`
+- **阶段**: Phase 10 - 异步图片加载 (已完成!)
+- **进度**: 多并发图片下载、渐进式渲染、非阻塞UI
+- **最后提交**: `feat: Add async image loading with progressive rendering`
## 立即可做的事
@@ -34,6 +34,16 @@
## 已完成的功能清单
+### Phase 10 - 异步图片加载
+- [x] 异步二进制下载接口 (HttpClient)
+- [x] 图片下载队列管理
+- [x] 多并发下载 (最多3张图片同时下载)
+- [x] 渐进式渲染 (图片下载完立即显示)
+- [x] 非阻塞UI (下载时可正常浏览)
+- [x] 实时进度显示
+- [x] Esc取消图片加载
+- [x] 保留图片缓存系统兼容
+
### Phase 9 - 性能优化和测试工具
- [x] 图片 LRU 缓存 (100张,10分钟过期)
- [x] 缓存命中统计显示
@@ -201,10 +211,10 @@ cmake --build build
## 下一步功能优先级
-1. **异步图片加载** - 图片也使用异步加载
-2. **Cookie 支持** - 保存和发送 Cookie
-3. **表单提交** - 实现 POST 表单提交
-4. **更多HTML5支持** - 更完善的HTML渲染
+1. **Cookie 持久化** - 保存和自动发送 Cookie (已有内存Cookie支持)
+2. **表单提交改进** - 文件上传、multipart/form-data
+3. **更多HTML5支持** -
表格渲染、代码块
+4. **性能优化** - DNS缓存、连接复用、HTTP/2
## 恢复对话时说
@@ -240,8 +250,17 @@ cmake --build build
✓ **表单** - 文本输入、复选框、下拉选择
✓ **书签** - 持久化书签管理
✓ **历史** - 浏览历史记录
-✓ **图片** - ASCII艺术渲染、智能缓存
-✓ **性能** - LRU缓存、差分渲染、异步加载
+✓ **图片** - ASCII艺术渲染、智能缓存、异步加载
+✓ **性能** - LRU缓存、差分渲染、异步加载、多并发下载
+
+## 技术亮点
+
+- **完全异步**: 页面和图片都使用异步加载,UI永不阻塞
+- **渐进式渲染**: 图片下载完立即显示,无需等待全部完成
+- **多并发下载**: 最多3张图片同时下载,显著提升加载速度
+- **智能缓存**: 页面5分钟缓存、图片10分钟缓存,LRU策略
+- **差分渲染**: 只更新变化的屏幕区域,减少闪烁
+- **真彩色支持**: 24位True Color图片渲染
---
更新时间: 2025-12-28
diff --git a/src/browser.cpp b/src/browser.cpp
index 4702fc8..d238adf 100644
--- a/src/browser.cpp
+++ b/src/browser.cpp
@@ -97,6 +97,11 @@ public:
static constexpr int CACHE_MAX_AGE = 300; // 5分钟缓存
static constexpr size_t CACHE_MAX_SIZE = 20; // 最多缓存20个页面
+ // 图片下载状态
+ int images_total = 0;
+ int images_loaded = 0;
+ int images_cached = 0;
+
// 图片缓存
std::map image_cache;
static constexpr int IMAGE_CACHE_MAX_AGE = 600; // 10分钟缓存
@@ -184,8 +189,8 @@ public:
status_message = current_tree.title.empty() ? url : current_tree.title;
}
- // 下载图片
- load_images(current_tree);
+ // 下载图片(异步)
+ queue_images(current_tree);
// 布局计算
current_layout = layout_engine->layout(current_tree);
@@ -242,8 +247,8 @@ public:
history_manager.add(url, current_tree.title);
}
- // 加载图片(仍然同步,可以后续优化)
- load_images(current_tree);
+ // 加载图片(异步)
+ queue_images(current_tree);
current_layout = layout_engine->layout(current_tree);
return;
}
@@ -301,6 +306,69 @@ public:
default:
return false;
}
+ } else if (loading_state == LoadingState::LOADING_IMAGES) {
+ // 轮询图片下载
+ http_client.poll_image_downloads();
+
+ // 处理已完成的图片
+ auto completed = http_client.get_completed_images();
+ bool need_relayout = false;
+
+ for (auto& task : completed) {
+ images_loaded++;
+
+ if (!task.is_success() || task.data.empty()) {
+ continue; // 跳过失败的图片
+ }
+
+ // 解码图片
+ tut::ImageData img_data = tut::ImageRenderer::load_from_memory(task.data);
+ if (img_data.is_valid()) {
+ // 设置到对应的DomNode
+ DomNode* img_node = static_cast(task.user_data);
+ if (img_node) {
+ img_node->image_data = img_data;
+ need_relayout = true;
+
+ // 添加到缓存
+ if (image_cache.size() >= IMAGE_CACHE_MAX_SIZE) {
+ // 移除最老的缓存条目
+ auto oldest = image_cache.begin();
+ for (auto it = image_cache.begin(); it != image_cache.end(); ++it) {
+ if (it->second.timestamp < oldest->second.timestamp) {
+ oldest = it;
+ }
+ }
+ image_cache.erase(oldest);
+ }
+
+ ImageCacheEntry entry;
+ entry.image_data = std::move(img_data);
+ entry.timestamp = std::chrono::steady_clock::now();
+ image_cache[task.url] = std::move(entry);
+ }
+ }
+ }
+
+ // 如果有图片完成,重新布局
+ if (need_relayout) {
+ current_layout = layout_engine->layout(current_tree);
+ }
+
+ // 检查是否所有图片都已完成
+ if (http_client.get_pending_image_count() == 0 &&
+ http_client.get_loading_image_count() == 0) {
+ if (images_total > 0) {
+ status_message = "✓ Loaded " + std::to_string(images_total) + " images";
+ if (images_cached > 0) {
+ status_message += " (" + std::to_string(images_cached) + " from cache)";
+ }
+ }
+ loading_state = LoadingState::IDLE;
+ return false;
+ }
+
+ return true;
}
return loading_state != LoadingState::IDLE;
@@ -312,7 +380,11 @@ public:
if (loading_state == LoadingState::LOADING_PAGE) {
status_message = spinner + " Loading " + extract_host(pending_url) + "...";
} else if (loading_state == LoadingState::LOADING_IMAGES) {
- status_message = spinner + " Loading images...";
+ status_message = spinner + " Loading images " + std::to_string(images_loaded) +
+ "/" + std::to_string(images_total);
+ if (images_cached > 0) {
+ status_message += " (cached: " + std::to_string(images_cached) + ")";
+ }
}
}
@@ -355,17 +427,22 @@ public:
status_message = current_tree.title.empty() ? pending_url : current_tree.title;
- // 加载图片(目前仍同步,可后续优化为异步)
- load_images(current_tree);
+ // 加载图片(异步)
+ queue_images(current_tree);
current_layout = layout_engine->layout(current_tree);
- loading_state = LoadingState::IDLE;
+ // 不设置为IDLE,等待图片加载完成
+ // loading_state will be set by poll_loading when images finish
}
// 取消加载
void cancel_loading() {
if (loading_state != LoadingState::IDLE) {
- http_client.cancel_async();
+ if (loading_state == LoadingState::LOADING_PAGE) {
+ http_client.cancel_async();
+ } else if (loading_state == LoadingState::LOADING_IMAGES) {
+ http_client.cancel_all_images();
+ }
loading_state = LoadingState::IDLE;
status_message = "⚠ Cancelled";
}
@@ -391,72 +468,49 @@ public:
}
// 下载并解码页面中的图片
- void load_images(DocumentTree& tree) {
+ // 将图片加入异步下载队列
+ void queue_images(DocumentTree& tree) {
if (tree.images.empty()) {
+ loading_state = LoadingState::IDLE;
return;
}
- int loaded = 0;
- int cached = 0;
- int total = static_cast(tree.images.size());
+ images_cached = 0;
+ images_total = 0;
+ images_loaded = 0;
for (DomNode* img_node : tree.images) {
if (img_node->img_src.empty()) {
continue;
}
- loaded++;
+ images_total++;
// 检查缓存
auto cache_it = image_cache.find(img_node->img_src);
if (cache_it != image_cache.end() && !cache_it->second.is_expired(IMAGE_CACHE_MAX_AGE)) {
// 使用缓存的图片
img_node->image_data = cache_it->second.image_data;
- cached++;
- status_message = "🖼 Loading image " + std::to_string(loaded) + "/" + std::to_string(total) +
- " (cached: " + std::to_string(cached) + ")";
- draw_screen();
+ images_cached++;
+ images_loaded++;
continue;
}
- // 更新状态
- status_message = "🖼 Downloading image " + std::to_string(loaded) + "/" + std::to_string(total) + "...";
- draw_screen();
-
- // 下载图片
- auto response = http_client.fetch_binary(img_node->img_src);
- if (!response.is_success() || response.data.empty()) {
- continue; // 跳过失败的图片
- }
-
- // 解码图片
- tut::ImageData img_data = tut::ImageRenderer::load_from_memory(response.data);
- if (img_data.is_valid()) {
- img_node->image_data = img_data;
-
- // 添加到缓存
- // 限制缓存大小
- if (image_cache.size() >= IMAGE_CACHE_MAX_SIZE) {
- // 移除最老的缓存条目
- auto oldest = image_cache.begin();
- for (auto it = image_cache.begin(); it != image_cache.end(); ++it) {
- if (it->second.timestamp < oldest->second.timestamp) {
- oldest = it;
- }
- }
- image_cache.erase(oldest);
- }
-
- ImageCacheEntry entry;
- entry.image_data = std::move(img_data);
- entry.timestamp = std::chrono::steady_clock::now();
- image_cache[img_node->img_src] = std::move(entry);
- }
+ // 添加到下载队列
+ http_client.add_image_download(img_node->img_src, img_node);
}
- if (cached > 0) {
- status_message = "✓ Loaded " + std::to_string(total) + " images (" +
- std::to_string(cached) + " from cache)";
+ // 如果所有图片都在缓存中,直接完成
+ if (http_client.get_pending_image_count() == 0 &&
+ http_client.get_loading_image_count() == 0) {
+ if (images_cached > 0) {
+ status_message = "✓ Loaded " + std::to_string(images_total) + " images (" +
+ std::to_string(images_cached) + " from cache)";
+ }
+ loading_state = LoadingState::IDLE;
+ } else {
+ loading_state = LoadingState::LOADING_IMAGES;
+ update_loading_status();
}
}
diff --git a/src/http_client.cpp b/src/http_client.cpp
index 8ae2db4..a56c8e2 100644
--- a/src/http_client.cpp
+++ b/src/http_client.cpp
@@ -17,6 +17,15 @@ static size_t binary_write_callback(void* contents, size_t size, size_t nmemb, s
return total_size;
}
+// 内部图片下载任务
+struct InternalImageTask {
+ CURL* easy_handle = nullptr;
+ std::string url;
+ void* user_data = nullptr;
+ std::vector data;
+ bool is_loading = false;
+};
+
class HttpClient::Impl {
public:
CURL* curl;
@@ -25,13 +34,20 @@ public:
bool follow_redirects;
std::string cookie_file;
- // 异步请求相关
+ // 异步请求相关 (页面)
CURLM* multi_handle = nullptr;
CURL* async_easy = nullptr;
AsyncState async_state = AsyncState::IDLE;
std::string async_response_body;
HttpResponse async_result;
+ // 异步图片下载相关
+ CURLM* image_multi = nullptr; // 专用于图片的multi handle
+ std::vector pending_images; // 待下载队列
+ std::vector loading_images; // 正在下载
+ std::vector completed_images; // 已完成
+ int max_concurrent_images = 3;
+
Impl() : timeout(30),
user_agent("TUT-Browser/2.0 (Terminal User Interface Browser)"),
follow_redirects(true) {
@@ -49,12 +65,22 @@ public:
if (!multi_handle) {
throw std::runtime_error("Failed to initialize CURL multi handle");
}
+
+ // 初始化image multi handle用于图片下载
+ image_multi = curl_multi_init();
+ if (!image_multi) {
+ throw std::runtime_error("Failed to initialize image CURL multi handle");
+ }
}
~Impl() {
// 清理异步请求
cleanup_async();
+ cleanup_all_images();
+ if (image_multi) {
+ curl_multi_cleanup(image_multi);
+ }
if (multi_handle) {
curl_multi_cleanup(multi_handle);
}
@@ -96,6 +122,46 @@ public:
curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, "");
}
+
+ void cleanup_all_images() {
+ // 清理所有正在加载的图片
+ for (auto& task : loading_images) {
+ if (task.easy_handle) {
+ curl_multi_remove_handle(image_multi, task.easy_handle);
+ curl_easy_cleanup(task.easy_handle);
+ }
+ }
+ loading_images.clear();
+
+ // 清理待下载的图片
+ for (auto& task : pending_images) {
+ if (task.easy_handle) {
+ curl_easy_cleanup(task.easy_handle);
+ }
+ }
+ pending_images.clear();
+ completed_images.clear();
+ }
+
+ // 启动一个图片下载任务
+ void start_image_download(InternalImageTask& task) {
+ task.easy_handle = curl_easy_init();
+ if (!task.easy_handle) {
+ return; // 跳过失败的任务
+ }
+
+ // 配置请求
+ setup_easy_handle(task.easy_handle, task.url);
+
+ // 设置写回调
+ task.data.clear();
+ curl_easy_setopt(task.easy_handle, CURLOPT_WRITEFUNCTION, binary_write_callback);
+ curl_easy_setopt(task.easy_handle, CURLOPT_WRITEDATA, &task.data);
+
+ // 添加到multi handle
+ curl_multi_add_handle(image_multi, task.easy_handle);
+ task.is_loading = true;
+ }
};
HttpClient::HttpClient() : pImpl(std::make_unique()) {}
@@ -452,4 +518,114 @@ void HttpClient::cancel_async() {
bool HttpClient::is_async_active() const {
return pImpl->async_state == AsyncState::LOADING;
+}
+
+// ========== 异步图片下载接口 ==========
+
+void HttpClient::add_image_download(const std::string& url, void* user_data) {
+ InternalImageTask task;
+ task.url = url;
+ task.user_data = user_data;
+ pImpl->pending_images.push_back(std::move(task));
+}
+
+void HttpClient::poll_image_downloads() {
+ // 启动新的下载任务,直到达到最大并发数
+ while (!pImpl->pending_images.empty() &&
+ static_cast(pImpl->loading_images.size()) < pImpl->max_concurrent_images) {
+ InternalImageTask task = std::move(pImpl->pending_images.front());
+ pImpl->pending_images.erase(pImpl->pending_images.begin());
+
+ pImpl->start_image_download(task);
+ pImpl->loading_images.push_back(std::move(task));
+ }
+
+ if (pImpl->loading_images.empty()) {
+ return; // 没有正在下载的任务
+ }
+
+ // 执行非阻塞的multi perform
+ int still_running = 0;
+ CURLMcode mc = curl_multi_perform(pImpl->image_multi, &still_running);
+
+ if (mc != CURLM_OK) {
+ // 发生错误,放弃所有正在下载的任务
+ pImpl->cleanup_all_images();
+ return;
+ }
+
+ // 检查是否有完成的请求
+ int msgs_left = 0;
+ CURLMsg* msg;
+ std::vector> to_remove; // 记录需要移除的handles和结果
+
+ while ((msg = curl_multi_info_read(pImpl->image_multi, &msgs_left))) {
+ if (msg->msg == CURLMSG_DONE) {
+ to_remove.push_back({msg->easy_handle, msg->data.result});
+ }
+ }
+
+ // 处理完成的任务
+ for (const auto& [easy, curl_result] : to_remove) {
+ // 找到对应的任务
+ for (auto it = pImpl->loading_images.begin(); it != pImpl->loading_images.end(); ++it) {
+ if (it->easy_handle == easy) {
+ ImageDownloadTask completed;
+ completed.url = it->url;
+ completed.user_data = it->user_data;
+
+ if (curl_result == CURLE_OK) {
+ // 获取响应信息
+ long http_code = 0;
+ curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &http_code);
+ completed.status_code = static_cast(http_code);
+
+ char* content_type = nullptr;
+ curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &content_type);
+ if (content_type) {
+ completed.content_type = content_type;
+ }
+
+ completed.data = std::move(it->data);
+ } else {
+ completed.error_message = curl_easy_strerror(curl_result);
+ completed.status_code = 0;
+ }
+
+ pImpl->completed_images.push_back(std::move(completed));
+
+ // 清理easy handle
+ curl_multi_remove_handle(pImpl->image_multi, easy);
+ curl_easy_cleanup(easy);
+
+ // 从loading列表中移除
+ pImpl->loading_images.erase(it);
+ break;
+ }
+ }
+ }
+}
+
+std::vector HttpClient::get_completed_images() {
+ std::vector result = std::move(pImpl->completed_images);
+ pImpl->completed_images.clear();
+ return result;
+}
+
+void HttpClient::cancel_all_images() {
+ pImpl->cleanup_all_images();
+}
+
+int HttpClient::get_pending_image_count() const {
+ return static_cast(pImpl->pending_images.size());
+}
+
+int HttpClient::get_loading_image_count() const {
+ return static_cast(pImpl->loading_images.size());
+}
+
+void HttpClient::set_max_concurrent_images(int max) {
+ if (max > 0 && max <= 10) { // 限制在1-10之间
+ pImpl->max_concurrent_images = max;
+ }
}
\ No newline at end of file
diff --git a/src/http_client.h b/src/http_client.h
index c374140..26bb82b 100644
--- a/src/http_client.h
+++ b/src/http_client.h
@@ -40,6 +40,20 @@ struct BinaryResponse {
}
};
+// 异步图片下载任务
+struct ImageDownloadTask {
+ std::string url;
+ void* user_data; // 用户自定义数据 (例如 DomNode*)
+ std::vector data;
+ std::string content_type;
+ int status_code = 0;
+ std::string error_message;
+
+ bool is_success() const {
+ return status_code >= 200 && status_code < 300;
+ }
+};
+
class HttpClient {
public:
HttpClient();
@@ -51,13 +65,22 @@ public:
HttpResponse post(const std::string& url, const std::string& data,
const std::string& content_type = "application/x-www-form-urlencoded");
- // 异步请求接口
+ // 异步请求接口 (页面)
void start_async_fetch(const std::string& url);
AsyncState poll_async(); // 非阻塞轮询,返回当前状态
HttpResponse get_async_result(); // 获取结果并重置状态
void cancel_async(); // 取消当前异步请求
bool is_async_active() const; // 是否有活跃的异步请求
+ // 异步图片下载接口 (支持多并发)
+ void add_image_download(const std::string& url, void* user_data = nullptr);
+ void poll_image_downloads(); // 非阻塞轮询所有图片下载
+ std::vector get_completed_images(); // 获取并移除已完成的图片
+ void cancel_all_images(); // 取消所有图片下载
+ int get_pending_image_count() const; // 获取待下载图片数量
+ int get_loading_image_count() const; // 获取正在下载的图片数量
+ void set_max_concurrent_images(int max); // 设置最大并发数 (默认3)
+
// 配置
void set_timeout(long timeout_seconds);
void set_user_agent(const std::string& user_agent);
diff --git a/tests/test_async_images.cpp b/tests/test_async_images.cpp
new file mode 100644
index 0000000..213cdec
--- /dev/null
+++ b/tests/test_async_images.cpp
@@ -0,0 +1,122 @@
+#include "../src/http_client.h"
+#include
+#include
+#include
+#include
+
+void test_async_image_downloads() {
+ std::cout << "Testing async image downloads...\n";
+
+ HttpClient client;
+
+ // Add multiple image downloads
+ // Using small test images from httpbin.org
+ const char* test_images[] = {
+ "https://httpbin.org/image/png",
+ "https://httpbin.org/image/jpeg",
+ "https://httpbin.org/image/webp"
+ };
+
+ // Add images to queue
+ for (int i = 0; i < 3; i++) {
+ client.add_image_download(test_images[i], (void*)(intptr_t)i);
+ std::cout << " Queued: " << test_images[i] << "\n";
+ }
+
+ std::cout << " Pending: " << client.get_pending_image_count() << "\n";
+ assert(client.get_pending_image_count() == 3);
+
+ // Poll until all images are downloaded
+ int iterations = 0;
+ int max_iterations = 200; // 10 seconds max (50ms * 200)
+
+ while ((client.get_pending_image_count() > 0 ||
+ client.get_loading_image_count() > 0) &&
+ iterations < max_iterations) {
+ client.poll_image_downloads();
+
+ // Check for completed images
+ auto completed = client.get_completed_images();
+ for (const auto& img : completed) {
+ std::cout << " Downloaded: " << img.url
+ << " (status: " << img.status_code
+ << ", size: " << img.data.size() << " bytes)\n";
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
+ iterations++;
+ }
+
+ std::cout << " Completed after " << iterations << " iterations\n";
+ std::cout << " Final state - Pending: " << client.get_pending_image_count()
+ << ", Loading: " << client.get_loading_image_count() << "\n";
+
+ std::cout << "✓ Async image download test passed!\n\n";
+}
+
+void test_image_cancellation() {
+ std::cout << "Testing image download cancellation...\n";
+
+ HttpClient client;
+
+ // Add images
+ client.add_image_download("https://httpbin.org/image/png", nullptr);
+ client.add_image_download("https://httpbin.org/image/jpeg", nullptr);
+
+ std::cout << " Queued 2 images\n";
+
+ // Start downloads
+ client.poll_image_downloads();
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ // Cancel all
+ std::cout << " Cancelling all downloads\n";
+ client.cancel_all_images();
+
+ assert(client.get_pending_image_count() == 0);
+ assert(client.get_loading_image_count() == 0);
+
+ std::cout << "✓ Image cancellation test passed!\n\n";
+}
+
+void test_concurrent_limit() {
+ std::cout << "Testing concurrent download limit...\n";
+
+ HttpClient client;
+ client.set_max_concurrent_images(2);
+
+ // Add 5 images
+ for (int i = 0; i < 5; i++) {
+ client.add_image_download("https://httpbin.org/delay/1", nullptr);
+ }
+
+ std::cout << " Queued 5 images with max_concurrent=2\n";
+ assert(client.get_pending_image_count() == 5);
+
+ // Start downloads
+ client.poll_image_downloads();
+
+ // Should have max 2 loading at a time
+ int loading = client.get_loading_image_count();
+ std::cout << " Loading: " << loading << " (should be <= 2)\n";
+ assert(loading <= 2);
+
+ client.cancel_all_images();
+ std::cout << "✓ Concurrent limit test passed!\n\n";
+}
+
+int main() {
+ std::cout << "=== Async Image Download Tests ===\n\n";
+
+ try {
+ test_async_image_downloads();
+ test_image_cancellation();
+ test_concurrent_limit();
+
+ std::cout << "=== All tests passed! ===\n";
+ return 0;
+ } catch (const std::exception& e) {
+ std::cerr << "Test failed: " << e.what() << "\n";
+ return 1;
+ }
+}
diff --git a/tests/test_image_minimal.cpp b/tests/test_image_minimal.cpp
new file mode 100644
index 0000000..1a09552
--- /dev/null
+++ b/tests/test_image_minimal.cpp
@@ -0,0 +1,42 @@
+#include "../src/http_client.h"
+#include
+#include
+#include
+
+int main() {
+ std::cout << "=== Minimal Async Image Test ===\n";
+
+ try {
+ HttpClient client;
+ std::cout << "1. Client created\n";
+
+ client.add_image_download("https://httpbin.org/image/png", nullptr);
+ std::cout << "2. Image queued\n";
+ std::cout << " Pending: " << client.get_pending_image_count() << "\n";
+
+ std::cout << "3. First poll...\n";
+ client.poll_image_downloads();
+ std::cout << " After poll - Pending: " << client.get_pending_image_count()
+ << ", Loading: " << client.get_loading_image_count() << "\n";
+
+ std::cout << "4. Wait a bit...\n";
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+
+ std::cout << "5. Second poll...\n";
+ client.poll_image_downloads();
+ std::cout << " After poll - Pending: " << client.get_pending_image_count()
+ << ", Loading: " << client.get_loading_image_count() << "\n";
+
+ std::cout << "6. Get completed...\n";
+ auto completed = client.get_completed_images();
+ std::cout << " Completed: " << completed.size() << "\n";
+
+ std::cout << "7. Destroying client...\n";
+ } catch (const std::exception& e) {
+ std::cerr << "ERROR: " << e.what() << "\n";
+ return 1;
+ }
+
+ std::cout << "8. Test completed successfully!\n";
+ return 0;
+}
diff --git a/tests/test_simple_image.cpp b/tests/test_simple_image.cpp
new file mode 100644
index 0000000..08b8391
--- /dev/null
+++ b/tests/test_simple_image.cpp
@@ -0,0 +1,23 @@
+#include "../src/http_client.h"
+#include
+
+int main() {
+ std::cout << "Testing basic image download...\n";
+
+ try {
+ HttpClient client;
+ std::cout << "Client created\n";
+
+ client.add_image_download("https://httpbin.org/image/png", nullptr);
+ std::cout << "Image queued\n";
+ std::cout << "Pending: " << client.get_pending_image_count() << "\n";
+
+ std::cout << "Client will be destroyed\n";
+ } catch (const std::exception& e) {
+ std::cerr << "Error: " << e.what() << "\n";
+ return 1;
+ }
+
+ std::cout << "Test completed successfully\n";
+ return 0;
+}