viewpoint_test_macros/lib.rs
1//! # Viewpoint Test Macros - Test Attribute Macros
2//!
3//! Proc macros for the Viewpoint test framework, providing the `#[viewpoint::test]`
4//! attribute macro for convenient test setup. This is an optional convenience layer -
5//! the primary API is `TestHarness` from `viewpoint-test`.
6//!
7//! ## Features
8//!
9//! - **Automatic Setup**: Browser, context, and page are set up before the test
10//! - **Automatic Cleanup**: Resources are cleaned up after the test completes
11//! - **Fixture Injection**: Request fixtures by parameter type (Page, BrowserContext, Browser)
12//! - **Fixture Scoping**: Share browsers/contexts across tests for performance
13//! - **Configuration**: Customize headless mode, timeouts, and more
14//!
15//! ## Quick Start
16//!
17//! ```text
18//! use viewpoint_test_macros::test;
19//! use viewpoint_core::Page;
20//!
21//! #[viewpoint_test_macros::test]
22//! async fn my_test(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
23//! page.goto("https://example.com").goto().await?;
24//! Ok(())
25//! }
26//! ```
27//!
28//! ## Fixture Parameters
29//!
30//! Request different fixtures by changing the parameter type:
31//!
32//! ```text
33//! use viewpoint_test_macros::test;
34//! use viewpoint_core::{Page, BrowserContext, Browser};
35//!
36//! // Get just the page (most common)
37//! #[viewpoint_test_macros::test]
38//! async fn test_with_page(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
39//! page.goto("https://example.com").goto().await?;
40//! Ok(())
41//! }
42//!
43//! // Get page and context
44//! #[viewpoint_test_macros::test]
45//! async fn test_with_context(
46//! page: &Page,
47//! context: &BrowserContext
48//! ) -> Result<(), Box<dyn std::error::Error>> {
49//! // Add cookies to the context
50//! context.add_cookies(vec![/* ... */]).await?;
51//! page.goto("https://example.com").goto().await?;
52//! Ok(())
53//! }
54//!
55//! // Get page, context, and browser
56//! #[viewpoint_test_macros::test]
57//! async fn test_with_browser(
58//! page: &Page,
59//! context: &BrowserContext,
60//! browser: &Browser
61//! ) -> Result<(), Box<dyn std::error::Error>> {
62//! // Create additional contexts
63//! let another_context = browser.new_context().await?;
64//! Ok(())
65//! }
66//! ```
67//!
68//! ## Configuration Options
69//!
70//! Configure the test with attribute arguments:
71//!
72//! ```text
73//! use viewpoint_test_macros::test;
74//! use viewpoint_core::Page;
75//!
76//! // Run in headed mode (visible browser)
77//! #[viewpoint_test_macros::test(headless = false)]
78//! async fn headed_test(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
79//! page.goto("https://example.com").goto().await?;
80//! Ok(())
81//! }
82//!
83//! // Custom timeout (in milliseconds)
84//! #[viewpoint_test_macros::test(timeout = 60000)]
85//! async fn slow_test(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
86//! // This test has a 60 second timeout
87//! page.goto("https://slow-site.com").goto().await?;
88//! Ok(())
89//! }
90//! ```
91//!
92//! ## Fixture Scoping
93//!
94//! Share browsers/contexts across tests for better performance:
95//!
96//! ### Browser Scope
97//!
98//! Share a browser across multiple tests (each test gets a fresh context and page):
99//!
100//! ```text
101//! use viewpoint_test_macros::test;
102//! use viewpoint_core::{Browser, Page};
103//! use std::sync::OnceLock;
104//!
105//! // Define a shared browser
106//! static BROWSER: OnceLock<Browser> = OnceLock::new();
107//!
108//! async fn shared_browser() -> &'static Browser {
109//! // Initialize browser once
110//! BROWSER.get_or_init(|| {
111//! tokio::runtime::Handle::current().block_on(async {
112//! Browser::launch().headless(true).launch().await.unwrap()
113//! })
114//! })
115//! }
116//!
117//! #[viewpoint_test_macros::test(scope = "browser", browser = "shared_browser")]
118//! async fn fast_test_1(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
119//! // Uses shared browser, but fresh context and page
120//! page.goto("https://example.com").goto().await?;
121//! Ok(())
122//! }
123//!
124//! #[viewpoint_test_macros::test(scope = "browser", browser = "shared_browser")]
125//! async fn fast_test_2(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
126//! // Same shared browser, different context and page
127//! page.goto("https://example.org").goto().await?;
128//! Ok(())
129//! }
130//! ```
131//!
132//! ### Context Scope
133//!
134//! Share a context across tests (each test gets a fresh page, but shares cookies/state):
135//!
136//! ```text
137//! use viewpoint_test_macros::test;
138//! use viewpoint_core::{BrowserContext, Page};
139//!
140//! async fn shared_context() -> &'static BrowserContext {
141//! // Return a shared context
142//! todo!()
143//! }
144//!
145//! #[viewpoint_test_macros::test(scope = "context", context = "shared_context")]
146//! async fn test_with_shared_context(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
147//! // Uses shared context (shares cookies, storage, etc.)
148//! page.goto("https://example.com").goto().await?;
149//! Ok(())
150//! }
151//! ```
152//!
153//! ## All Configuration Options
154//!
155//! | Option | Type | Default | Description |
156//! |--------|------|---------|-------------|
157//! | `headless` | bool | `true` | Run browser in headless mode |
158//! | `timeout` | integer | 30000 | Default timeout in milliseconds |
159//! | `scope` | string | - | Fixture scope: `"browser"` or `"context"` |
160//! | `browser` | string | - | Function name returning shared browser (required when scope = "browser") |
161//! | `context` | string | - | Function name returning shared context (required when scope = "context") |
162//!
163//! ## When to Use TestHarness Instead
164//!
165//! The macro is convenient but `TestHarness` offers more control:
166//!
167//! - When you need to configure the browser context with specific options
168//! - When you need to set up network interception before navigation
169//! - When you want more explicit control over setup and teardown
170//! - When you need to handle setup failures differently
171//!
172//! ```ignore
173//! use viewpoint_test::TestHarness;
174//!
175//! #[tokio::test]
176//! async fn explicit_test() -> Result<(), Box<dyn std::error::Error>> {
177//! let harness = TestHarness::new().await?;
178//! let page = harness.page();
179//!
180//! // More explicit setup gives you more control
181//! page.goto("https://example.com").goto().await?;
182//!
183//! Ok(())
184//! }
185//! ```
186
187use proc_macro::TokenStream;
188use syn::{ItemFn, parse_macro_input};
189
190mod test_attr;
191
192/// Attribute macro for Viewpoint tests.
193///
194/// This macro transforms async test functions to include `TestHarness` setup
195/// and cleanup. Fixture parameters (Page, `BrowserContext`, Browser) are
196/// automatically extracted from the harness.
197///
198/// # Basic Usage
199///
200/// ```text
201/// #[viewpoint_test_macros::test]
202/// async fn my_test(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
203/// page.goto("https://example.com").goto().await?;
204/// Ok(())
205/// }
206/// ```
207///
208/// # Configuration Options
209///
210/// - `headless = true|false` - Run browser in headless mode (default: true)
211/// - `timeout = <ms>` - Default timeout in milliseconds (default: 30000)
212/// - `scope = "browser"|"context"` - Fixture scoping level
213/// - `browser = "<fn_name>"` - Function returning shared browser (required when scope = "browser")
214/// - `context = "<fn_name>"` - Function returning shared context (required when scope = "context")
215#[proc_macro_attribute]
216pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
217 let args = parse_macro_input!(attr as test_attr::TestArgs);
218 let input = parse_macro_input!(item as ItemFn);
219
220 match test_attr::expand_test(args, input) {
221 Ok(tokens) => tokens.into(),
222 Err(err) => err.to_compile_error().into(),
223 }
224}