use tracing::{debug, info, instrument, warn};
use crate::config::TestConfig;
use crate::error::TestError;
use viewpoint_core::{Browser, BrowserContext, Page};
#[derive(Debug)]
pub struct TestHarness {
browser: Option<Browser>,
context: Option<BrowserContext>,
page: Page,
owns_browser: bool,
owns_context: bool,
config: TestConfig,
}
impl TestHarness {
#[instrument(level = "info", name = "TestHarness::new")]
pub async fn new() -> Result<Self, TestError> {
Self::with_config(TestConfig::default()).await
}
#[instrument(level = "info", name = "TestHarness::with_config", skip(config))]
pub async fn with_config(config: TestConfig) -> Result<Self, TestError> {
info!(headless = config.headless, "Creating test harness");
let browser = Browser::launch()
.headless(config.headless)
.launch()
.await
.map_err(|e| TestError::Setup(format!("Failed to launch browser: {e}")))?;
debug!("Browser launched");
let context = browser
.new_context()
.await
.map_err(|e| TestError::Setup(format!("Failed to create context: {e}")))?;
debug!("Context created");
let page = context
.new_page()
.await
.map_err(|e| TestError::Setup(format!("Failed to create page: {e}")))?;
debug!("Page created");
info!("Test harness ready");
Ok(Self {
browser: Some(browser),
context: Some(context),
page,
owns_browser: true,
owns_context: true,
config,
})
}
pub fn builder() -> TestHarnessBuilder {
TestHarnessBuilder::default()
}
#[instrument(level = "info", name = "TestHarness::from_browser", skip(browser))]
pub async fn from_browser(browser: &Browser) -> Result<Self, TestError> {
info!("Creating test harness from existing browser");
let context = browser
.new_context()
.await
.map_err(|e| TestError::Setup(format!("Failed to create context: {e}")))?;
debug!("Context created");
let page = context
.new_page()
.await
.map_err(|e| TestError::Setup(format!("Failed to create page: {e}")))?;
debug!("Page created");
info!("Test harness ready (browser shared)");
Ok(Self {
browser: None, context: Some(context),
page,
owns_browser: false,
owns_context: true,
config: TestConfig::default(),
})
}
#[instrument(level = "info", name = "TestHarness::from_context", skip(context))]
pub async fn from_context(context: &BrowserContext) -> Result<Self, TestError> {
info!("Creating test harness from existing context");
let page = context
.new_page()
.await
.map_err(|e| TestError::Setup(format!("Failed to create page: {e}")))?;
debug!("Page created");
info!("Test harness ready (context shared)");
Ok(Self {
browser: None,
context: None, page,
owns_browser: false,
owns_context: false,
config: TestConfig::default(),
})
}
pub fn page(&self) -> &Page {
&self.page
}
pub fn page_mut(&mut self) -> &mut Page {
&mut self.page
}
pub fn context(&self) -> Option<&BrowserContext> {
self.context.as_ref()
}
pub fn browser(&self) -> Option<&Browser> {
self.browser.as_ref()
}
pub fn config(&self) -> &TestConfig {
&self.config
}
pub async fn new_page(&self) -> Result<Page, TestError> {
let context = self.context.as_ref().ok_or_else(|| {
TestError::Setup("No context available (harness created with from_context)".to_string())
})?;
context
.new_page()
.await
.map_err(|e| TestError::Setup(format!("Failed to create page: {e}")))
}
#[instrument(level = "info", name = "TestHarness::close", skip(self))]
pub async fn close(mut self) -> Result<(), TestError> {
info!(
owns_browser = self.owns_browser,
owns_context = self.owns_context,
"Closing test harness"
);
if let Err(e) = self.page.close().await {
warn!("Failed to close page: {}", e);
}
if self.owns_context {
if let Some(ref mut context) = self.context {
if let Err(e) = context.close().await {
warn!("Failed to close context: {}", e);
}
}
}
if self.owns_browser {
if let Some(ref browser) = self.browser {
if let Err(e) = browser.close().await {
warn!("Failed to close browser: {}", e);
}
}
}
info!("Test harness closed");
Ok(())
}
}
impl Drop for TestHarness {
fn drop(&mut self) {
debug!(
owns_browser = self.owns_browser,
owns_context = self.owns_context,
"TestHarness dropped"
);
}
}
#[derive(Debug, Default)]
pub struct TestHarnessBuilder {
config: TestConfig,
}
impl TestHarnessBuilder {
pub fn headless(mut self, headless: bool) -> Self {
self.config.headless = headless;
self
}
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.config.timeout = timeout;
self
}
pub async fn build(self) -> Result<TestHarness, TestError> {
TestHarness::with_config(self.config).await
}
}