socialseed-e2e 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. socialseed_e2e/__init__.py +184 -20
  2. socialseed_e2e/__version__.py +2 -2
  3. socialseed_e2e/cli.py +353 -190
  4. socialseed_e2e/core/base_page.py +368 -49
  5. socialseed_e2e/core/config_loader.py +15 -3
  6. socialseed_e2e/core/headers.py +11 -4
  7. socialseed_e2e/core/loaders.py +6 -4
  8. socialseed_e2e/core/test_orchestrator.py +2 -0
  9. socialseed_e2e/core/test_runner.py +487 -0
  10. socialseed_e2e/templates/agent_docs/AGENT_GUIDE.md.template +412 -0
  11. socialseed_e2e/templates/agent_docs/EXAMPLE_TEST.md.template +152 -0
  12. socialseed_e2e/templates/agent_docs/FRAMEWORK_CONTEXT.md.template +55 -0
  13. socialseed_e2e/templates/agent_docs/WORKFLOW_GENERATION.md.template +182 -0
  14. socialseed_e2e/templates/data_schema.py.template +111 -70
  15. socialseed_e2e/templates/e2e.conf.template +19 -0
  16. socialseed_e2e/templates/service_page.py.template +82 -27
  17. socialseed_e2e/templates/test_module.py.template +21 -7
  18. socialseed_e2e/templates/verify_installation.py +192 -0
  19. socialseed_e2e/utils/__init__.py +29 -0
  20. socialseed_e2e/utils/ai_generator.py +463 -0
  21. socialseed_e2e/utils/pydantic_helpers.py +392 -0
  22. socialseed_e2e/utils/state_management.py +312 -0
  23. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/METADATA +64 -27
  24. socialseed_e2e-0.1.2.dist-info/RECORD +38 -0
  25. socialseed_e2e-0.1.0.dist-info/RECORD +0 -29
  26. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/WHEEL +0 -0
  27. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/entry_points.txt +0 -0
  28. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/licenses/LICENSE +0 -0
  29. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,392 @@
1
+ """Universal Pydantic helpers for multi-language backend compatibility.
2
+
3
+ This module provides utilities for handling serialization between Python and
4
+ different backend naming conventions (Java/camelCase, C#/PascalCase, Python/snake_case, etc.).
5
+
6
+ The framework automatically detects and handles the naming convention of the backend API,
7
+ making it easy to create tests for APIs written in any language.
8
+
9
+ Usage:
10
+ from socialseed_e2e.utils.pydantic_helpers import (
11
+ APIModel, api_field, NamingConvention, get_naming_convention
12
+ )
13
+
14
+ # For Java/Spring Boot backend (camelCase)
15
+ class LoginRequest(APIModel):
16
+ model_config = {"naming_convention": NamingConvention.CAMEL_CASE}
17
+ refresh_token: str = api_field("refreshToken")
18
+ user_name: str = api_field("userName")
19
+
20
+ # For C#/ASP.NET backend (PascalCase)
21
+ class UserRequest(APIModel):
22
+ model_config = {"naming_convention": NamingConvention.PASCAL_CASE}
23
+ user_id: str = api_field("UserId")
24
+ email_address: str = api_field("EmailAddress")
25
+
26
+ # For Python/Flask backend (snake_case)
27
+ class DataRequest(APIModel):
28
+ model_config = {"naming_convention": NamingConvention.SNAKE_CASE}
29
+ user_id: str = api_field("user_id")
30
+ created_at: str = api_field("created_at")
31
+
32
+ # Automatic serialization with correct convention
33
+ request = LoginRequest(refresh_token="abc", user_name="john")
34
+ data = request.to_dict() # {'refreshToken': 'abc', 'userName': 'john'}
35
+ """
36
+
37
+ import re
38
+ from enum import Enum
39
+ from typing import Any, Dict, Optional, TypeVar, Union, get_args, get_origin
40
+
41
+ from pydantic import BaseModel, EmailStr, Field
42
+
43
+ T = TypeVar("T", bound=BaseModel)
44
+
45
+
46
+ class NamingConvention(Enum):
47
+ """Supported naming conventions for different backend languages."""
48
+
49
+ CAMEL_CASE = "camelCase" # Java, JavaScript, TypeScript, Go
50
+ PASCAL_CASE = "PascalCase" # C#, Pascal
51
+ SNAKE_CASE = "snake_case" # Python, Rust, Ruby
52
+ KEBAB_CASE = "kebab-case" # Some APIs, URLs
53
+ UPPER_SNAKE = "UPPER_SNAKE" # Environment variables, constants
54
+
55
+
56
+ def to_camel_case(snake_str: str) -> str:
57
+ """Convert snake_case to camelCase.
58
+
59
+ Examples:
60
+ >>> to_camel_case("refresh_token")
61
+ 'refreshToken'
62
+ >>> to_camel_case("user_id")
63
+ 'userId'
64
+ """
65
+ components = snake_str.split("_")
66
+ return components[0] + "".join(x.capitalize() for x in components[1:])
67
+
68
+
69
+ def to_pascal_case(snake_str: str) -> str:
70
+ """Convert snake_case to PascalCase.
71
+
72
+ Examples:
73
+ >>> to_pascal_case("user_id")
74
+ 'UserId'
75
+ >>> to_pascal_case("refresh_token")
76
+ 'RefreshToken'
77
+ """
78
+ components = snake_str.split("_")
79
+ return "".join(x.capitalize() for x in components)
80
+
81
+
82
+ def to_snake_case(camel_str: str) -> str:
83
+ """Convert camelCase/PascalCase to snake_case.
84
+
85
+ Examples:
86
+ >>> to_snake_case("refreshToken")
87
+ 'refresh_token'
88
+ >>> to_snake_case("UserId")
89
+ 'user_id'
90
+ """
91
+ # Handle PascalCase
92
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_str)
93
+ # Handle camelCase
94
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
95
+
96
+
97
+ def to_kebab_case(snake_str: str) -> str:
98
+ """Convert snake_case to kebab-case."""
99
+ return snake_str.replace("_", "-")
100
+
101
+
102
+ def api_field(json_name: str, convention: Optional[NamingConvention] = None, **kwargs) -> Any:
103
+ """Create a Pydantic Field with proper alias configuration.
104
+
105
+ This helper creates fields that serialize correctly for any backend
106
+ naming convention. The framework automatically handles the conversion.
107
+
108
+ Args:
109
+ json_name: The field name as it appears in JSON/API
110
+ convention: Naming convention hint (optional, defaults to camelCase)
111
+ **kwargs: Additional Field arguments (default, description, etc.)
112
+
113
+ Returns:
114
+ A Pydantic Field configured for the specified naming convention
115
+
116
+ Examples:
117
+ # For Java backend (camelCase)
118
+ refresh_token: str = api_field("refreshToken")
119
+
120
+ # For C# backend (PascalCase)
121
+ user_id: str = api_field("UserId", NamingConvention.PASCAL_CASE)
122
+
123
+ # With default value
124
+ page_size: int = api_field("pageSize", default=20)
125
+
126
+ # With description
127
+ email: str = api_field("email", description="User email address")
128
+ """
129
+ return Field(..., alias=json_name, serialization_alias=json_name, **kwargs)
130
+
131
+
132
+ class APIModel(BaseModel):
133
+ """Universal base model for any backend API compatibility.
134
+
135
+ This model automatically handles serialization for different backend
136
+ naming conventions (camelCase, PascalCase, snake_case, etc.).
137
+
138
+ Usage:
139
+ # Java/Spring Boot (camelCase - default)
140
+ class LoginRequest(APIModel):
141
+ refresh_token: str = api_field("refreshToken")
142
+
143
+ # C#/ASP.NET (PascalCase)
144
+ class UserRequest(APIModel):
145
+ model_config = {"naming_convention": NamingConvention.PASCAL_CASE}
146
+ user_id: str = api_field("UserId")
147
+
148
+ # Python/Flask (snake_case)
149
+ class DataRequest(APIModel):
150
+ model_config = {"naming_convention": NamingConvention.SNAKE_CASE}
151
+ created_at: str = api_field("created_at")
152
+
153
+ Configuration:
154
+ Set naming_convention in model_config to match your backend:
155
+ - NamingConvention.CAMEL_CASE (default) - Java, JS, TS, Go
156
+ - NamingConvention.PASCAL_CASE - C#, Pascal
157
+ - NamingConvention.SNAKE_CASE - Python, Rust
158
+ - NamingConvention.KEBAB_CASE - Some APIs
159
+ """
160
+
161
+ model_config = {
162
+ "populate_by_name": True,
163
+ "naming_convention": NamingConvention.CAMEL_CASE,
164
+ "validate_assignment": True,
165
+ "extra": "ignore", # Ignore extra fields from API responses
166
+ }
167
+
168
+ def to_dict(self, **kwargs) -> Dict[str, Any]:
169
+ """Serialize to dictionary with proper naming convention.
170
+
171
+ Automatically uses by_alias=True to convert field names
172
+ to the backend's expected format.
173
+
174
+ Args:
175
+ **kwargs: Additional arguments passed to model_dump()
176
+
177
+ Returns:
178
+ Dictionary with correctly named keys
179
+
180
+ Examples:
181
+ # Java backend (camelCase)
182
+ request = LoginRequest(refresh_token="abc")
183
+ data = request.to_dict() # {'refreshToken': 'abc'}
184
+
185
+ # With exclusions
186
+ data = request.to_dict(exclude={'password'})
187
+ """
188
+ return self.model_dump(by_alias=True, **kwargs)
189
+
190
+ def to_json(self, **kwargs) -> str:
191
+ """Serialize to JSON string with proper naming convention.
192
+
193
+ Args:
194
+ **kwargs: Additional arguments for model_dump_json()
195
+
196
+ Returns:
197
+ JSON string with correctly named keys
198
+ """
199
+ return self.model_dump_json(by_alias=True, **kwargs)
200
+
201
+ @classmethod
202
+ def from_dict(cls: type[T], data: Dict[str, Any]) -> T:
203
+ """Create instance from dictionary with flexible field matching.
204
+
205
+ Thanks to populate_by_name=True, this accepts both:
206
+ - API format: {"refreshToken": "abc", "userName": "john"}
207
+ - Python format: {"refresh_token": "abc", "user_name": "john"}
208
+
209
+ Args:
210
+ data: Dictionary with field values
211
+
212
+ Returns:
213
+ Instance of the model class
214
+
215
+ Examples:
216
+ # From API response (camelCase)
217
+ data = {"refreshToken": "abc"}
218
+ request = RefreshTokenRequest.from_dict(data)
219
+
220
+ # From Python code (snake_case)
221
+ data = {"refresh_token": "abc"}
222
+ request = RefreshTokenRequest.from_dict(data)
223
+ """
224
+ return cls.model_validate(data)
225
+
226
+ @classmethod
227
+ def get_naming_convention(cls) -> NamingConvention:
228
+ """Get the naming convention for this model.
229
+
230
+ Returns:
231
+ The NamingConvention enum value
232
+ """
233
+ config = cls.model_config
234
+ convention = config.get("naming_convention", NamingConvention.CAMEL_CASE)
235
+ if isinstance(convention, str):
236
+ return NamingConvention(convention)
237
+ return convention
238
+
239
+
240
+ def to_api_dict(model: BaseModel, **kwargs) -> Dict[str, Any]:
241
+ """Convert any Pydantic model to API-compatible dictionary.
242
+
243
+ This is a standalone function that works with any model,
244
+ including those that don't inherit from APIModel.
245
+
246
+ Args:
247
+ model: Any Pydantic model instance
248
+ **kwargs: Additional arguments for model_dump()
249
+
250
+ Returns:
251
+ Dictionary with aliased keys
252
+
253
+ Example:
254
+ class MyModel(BaseModel):
255
+ model_config = {"populate_by_name": True}
256
+ refresh_token: str = Field(alias="refreshToken")
257
+
258
+ m = MyModel(refresh_token="abc")
259
+ data = to_api_dict(m) # {'refreshToken': 'abc'}
260
+ """
261
+ defaults = {"by_alias": True}
262
+ defaults.update(kwargs)
263
+ return model.model_dump(**defaults)
264
+
265
+
266
+ def validate_api_model(model_class: type[T], data: Dict[str, Any]) -> T:
267
+ """Validate data and create model instance with helpful error messages.
268
+
269
+ Args:
270
+ model_class: The Pydantic model class to validate against
271
+ data: Dictionary with field values
272
+
273
+ Returns:
274
+ Validated model instance
275
+
276
+ Raises:
277
+ ValueError: If validation fails, with detailed error message
278
+
279
+ Example:
280
+ try:
281
+ request = validate_api_model(LoginRequest, data)
282
+ except ValueError as e:
283
+ print(f"Validation error: {e}")
284
+ """
285
+ try:
286
+ return model_class.model_validate(data)
287
+ except Exception as e:
288
+ # Provide helpful error message
289
+ class_name = model_class.__name__
290
+ fields = list(model_class.model_fields.keys())
291
+ raise ValueError(
292
+ f"Failed to validate {class_name}: {e}\n"
293
+ f"Expected fields: {fields}\n"
294
+ f"Input data: {data}"
295
+ ) from e
296
+
297
+
298
+ def detect_naming_convention(sample_data: Dict[str, Any]) -> NamingConvention:
299
+ """Detect the naming convention used in sample data.
300
+
301
+ Analyzes the keys of a dictionary to determine the likely
302
+ naming convention being used.
303
+
304
+ Args:
305
+ sample_data: Dictionary with sample field names
306
+
307
+ Returns:
308
+ Detected NamingConvention
309
+
310
+ Example:
311
+ >>> data = {"refreshToken": "abc", "userName": "john"}
312
+ >>> detect_naming_convention(data)
313
+ NamingConvention.CAMEL_CASE
314
+ """
315
+ if not sample_data:
316
+ return NamingConvention.CAMEL_CASE # Default
317
+
318
+ keys = list(sample_data.keys())
319
+ if not keys:
320
+ return NamingConvention.CAMEL_CASE
321
+
322
+ # Check first few keys
323
+ sample_keys = keys[:5]
324
+
325
+ camel_count = 0
326
+ pascal_count = 0
327
+ snake_count = 0
328
+ kebab_count = 0
329
+
330
+ for key in sample_keys:
331
+ if "-" in key:
332
+ kebab_count += 1
333
+ elif "_" in key:
334
+ snake_count += 1
335
+ elif key[0].isupper():
336
+ pascal_count += 1
337
+ elif any(c.isupper() for c in key[1:]):
338
+ camel_count += 1
339
+
340
+ # Determine most likely convention
341
+ counts = [
342
+ (camel_count, NamingConvention.CAMEL_CASE),
343
+ (pascal_count, NamingConvention.PASCAL_CASE),
344
+ (snake_count, NamingConvention.SNAKE_CASE),
345
+ (kebab_count, NamingConvention.KEBAB_CASE),
346
+ ]
347
+
348
+ # Return the one with highest count, or default to camelCase
349
+ return (
350
+ max(counts, key=lambda x: x[0])[1]
351
+ if max(c[0] for c in counts) > 0
352
+ else NamingConvention.CAMEL_CASE
353
+ )
354
+
355
+
356
+ # Convenience field creators for common patterns
357
+ def camel_field(name: str, **kwargs) -> Any:
358
+ """Create field for camelCase (Java, JS, TS, Go)."""
359
+ if "_" in name:
360
+ # If name is snake_case, convert to camelCase
361
+ name = to_camel_case(name)
362
+ return api_field(name, NamingConvention.CAMEL_CASE, **kwargs)
363
+
364
+
365
+ def pascal_field(name: str, **kwargs) -> Any:
366
+ """Create field for PascalCase (C#)."""
367
+ if "_" in name:
368
+ # If name is snake_case, convert to PascalCase
369
+ name = to_pascal_case(name)
370
+ return api_field(name, NamingConvention.PASCAL_CASE, **kwargs)
371
+
372
+
373
+ def snake_field(name: str, **kwargs) -> Any:
374
+ """Create field for snake_case (Python, Rust)."""
375
+ return api_field(name, NamingConvention.SNAKE_CASE, **kwargs)
376
+
377
+
378
+ # Pre-defined fields for common patterns
379
+ refresh_token_field = lambda **kwargs: camel_field("refreshToken", **kwargs)
380
+ access_token_field = lambda **kwargs: camel_field("accessToken", **kwargs)
381
+ user_name_field = lambda **kwargs: camel_field("userName", **kwargs)
382
+ user_id_field = lambda **kwargs: camel_field("userId", **kwargs)
383
+ created_at_field = lambda **kwargs: camel_field("createdAt", **kwargs)
384
+ updated_at_field = lambda **kwargs: camel_field("updatedAt", **kwargs)
385
+ new_password_field = lambda **kwargs: camel_field("newPassword", **kwargs)
386
+ current_password_field = lambda **kwargs: camel_field("currentPassword", **kwargs)
387
+
388
+
389
+ # Backwards compatibility aliases
390
+ JavaCompatibleModel = APIModel # For existing code
391
+ to_camel_dict = to_api_dict # For existing code
392
+ validate_camelcase_model = validate_api_model # For existing code
@@ -0,0 +1,312 @@
1
+ """State management mixins for Page Objects.
2
+
3
+ This module provides mixins for managing dynamic state in Page Objects,
4
+ solving the common problem of LSP errors when assigning dynamic attributes
5
+ to share state between tests.
6
+
7
+ Problem:
8
+ When tests need to share state (tokens, IDs), they often assign dynamic
9
+ attributes to the Page object. This causes LSP errors:
10
+
11
+ ERROR: Cannot assign to attribute "current_user_email" for class "AuthServicePage"
12
+ Attribute "current_user_email" is unknown
13
+
14
+ Solution:
15
+ Use DynamicStateMixin which provides a type-safe way to store and retrieve
16
+ shared state between test modules.
17
+
18
+ Usage:
19
+ class AuthServicePage(BasePage, DynamicStateMixin):
20
+ def __init__(self, base_url: str, **kwargs):
21
+ super().__init__(base_url=base_url, **kwargs)
22
+ self.init_dynamic_state() # Initialize the state container
23
+
24
+ # In test module 01:
25
+ page.set_state("user_id", "123")
26
+ page.set_state("auth_token", "abc")
27
+
28
+ # In test module 02:
29
+ user_id = page.get_state("user_id") # Returns "123"
30
+ auth_token = page.get_state("auth_token") # Returns "abc"
31
+ """
32
+
33
+ from typing import Any, Dict, Generic, Optional, TypeVar
34
+
35
+ T = TypeVar("T")
36
+
37
+
38
+ class DynamicStateMixin:
39
+ """Mixin to add dynamic state management to Page Objects.
40
+
41
+ This mixin provides a type-safe, dictionary-based approach to storing
42
+ and retrieving shared state between test modules.
43
+
44
+ Attributes:
45
+ _dynamic_state: Internal dictionary storing all dynamic state
46
+
47
+ Example:
48
+ class MyPage(BasePage, DynamicStateMixin):
49
+ def __init__(self, base_url: str, **kwargs):
50
+ super().__init__(base_url=base_url, **kwargs)
51
+ self.init_dynamic_state()
52
+
53
+ # Store values
54
+ page.set_state("created_user_id", "uuid-123")
55
+ page.set_state("auth_token", "token-abc")
56
+
57
+ # Retrieve values
58
+ user_id = page.get_state("created_user_id") # "uuid-123"
59
+ token = page.get_state("auth_token", default="") # "token-abc"
60
+
61
+ # Type-safe retrieval with casting
62
+ user_id = page.get_state_as(str, "created_user_id")
63
+ count = page.get_state_as(int, "item_count", default=0)
64
+ """
65
+
66
+ _dynamic_state: Dict[str, Any]
67
+
68
+ def init_dynamic_state(self) -> None:
69
+ """Initialize the dynamic state container.
70
+
71
+ Must be called in the __init__ method of your Page class.
72
+
73
+ Example:
74
+ class MyPage(BasePage, DynamicStateMixin):
75
+ def __init__(self, base_url: str, **kwargs):
76
+ super().__init__(base_url=base_url, **kwargs)
77
+ self.init_dynamic_state() # Initialize here
78
+ """
79
+ self._dynamic_state = {}
80
+
81
+ def set_state(self, key: str, value: Any) -> None:
82
+ """Store a value in the dynamic state.
83
+
84
+ Args:
85
+ key: Identifier for the value (e.g., "user_id", "auth_token")
86
+ value: Any value to store
87
+
88
+ Example:
89
+ page.set_state("user_id", "123")
90
+ page.set_state("user_data", {"name": "John", "email": "john@example.com"})
91
+ """
92
+ self._dynamic_state[key] = value
93
+
94
+ def get_state(self, key: str, default: Any = None) -> Any:
95
+ """Retrieve a value from the dynamic state.
96
+
97
+ Args:
98
+ key: Identifier for the value
99
+ default: Value to return if key doesn't exist (default: None)
100
+
101
+ Returns:
102
+ The stored value or the default
103
+
104
+ Example:
105
+ user_id = page.get_state("user_id") # Returns value or None
106
+ user_id = page.get_state("user_id", default="") # Returns value or ""
107
+ """
108
+ return self._dynamic_state.get(key, default)
109
+
110
+ def get_state_as(self, type_: type[T], key: str, default: T = None) -> T:
111
+ """Type-safe retrieval of state values.
112
+
113
+ Args:
114
+ type_: Expected type of the value (str, int, etc.)
115
+ key: Identifier for the value
116
+ default: Default value if key doesn't exist
117
+
118
+ Returns:
119
+ The stored value cast to the specified type
120
+
121
+ Raises:
122
+ TypeError: If the stored value is not of the expected type
123
+
124
+ Example:
125
+ user_id = page.get_state_as(str, "user_id")
126
+ count = page.get_state_as(int, "item_count", default=0)
127
+ """
128
+ value = self._dynamic_state.get(key, default)
129
+ if value is not None and not isinstance(value, type_):
130
+ raise TypeError(
131
+ f"State value for '{key}' is {type(value).__name__}, " f"expected {type_.__name__}"
132
+ )
133
+ return value
134
+
135
+ def has_state(self, key: str) -> bool:
136
+ """Check if a key exists in the dynamic state.
137
+
138
+ Args:
139
+ key: Identifier to check
140
+
141
+ Returns:
142
+ True if the key exists, False otherwise
143
+
144
+ Example:
145
+ if page.has_state("user_id"):
146
+ user_id = page.get_state("user_id")
147
+ """
148
+ return key in self._dynamic_state
149
+
150
+ def remove_state(self, key: str) -> Optional[Any]:
151
+ """Remove a value from the dynamic state.
152
+
153
+ Args:
154
+ key: Identifier to remove
155
+
156
+ Returns:
157
+ The removed value, or None if key didn't exist
158
+
159
+ Example:
160
+ old_value = page.remove_state("temporary_token")
161
+ """
162
+ return self._dynamic_state.pop(key, None)
163
+
164
+ def clear_state(self) -> None:
165
+ """Clear all dynamic state.
166
+
167
+ Useful for cleanup between test suites or when switching users.
168
+
169
+ Example:
170
+ page.clear_state() # Removes all stored values
171
+ """
172
+ self._dynamic_state.clear()
173
+
174
+ def get_all_state(self) -> Dict[str, Any]:
175
+ """Get a copy of all dynamic state.
176
+
177
+ Returns:
178
+ Dictionary containing all stored state
179
+
180
+ Example:
181
+ all_state = page.get_all_state()
182
+ print(f"Stored keys: {list(all_state.keys())}")
183
+ """
184
+ return self._dynamic_state.copy()
185
+
186
+ def require_state(self, key: str, error_msg: Optional[str] = None) -> Any:
187
+ """Get a required state value, raising an error if not found.
188
+
189
+ This is useful for dependencies between test modules where a value
190
+ MUST exist from a previous step.
191
+
192
+ Args:
193
+ key: Identifier for the value
194
+ error_msg: Custom error message (optional)
195
+
196
+ Returns:
197
+ The stored value
198
+
199
+ Raises:
200
+ RuntimeError: If the key doesn't exist
201
+
202
+ Example:
203
+ # Will raise RuntimeError if "user_id" wasn't set by previous test
204
+ user_id = page.require_state("user_id")
205
+
206
+ # With custom error message
207
+ user_id = page.require_state(
208
+ "user_id",
209
+ error_msg="User must be created in registration test first"
210
+ )
211
+ """
212
+ if key not in self._dynamic_state:
213
+ msg = (
214
+ error_msg
215
+ or f"Required state '{key}' not found. "
216
+ f"Ensure previous test modules ran successfully."
217
+ )
218
+ raise RuntimeError(msg)
219
+ return self._dynamic_state[key]
220
+
221
+
222
+ class AuthStateMixin(DynamicStateMixin):
223
+ """Specialized mixin for authentication-related state.
224
+
225
+ Provides convenient properties for common auth values like tokens,
226
+ user IDs, and credentials.
227
+
228
+ Example:
229
+ class AuthPage(BasePage, AuthStateMixin):
230
+ def __init__(self, base_url: str, **kwargs):
231
+ super().__init__(base_url=base_url, **kwargs)
232
+ self.init_auth_state() # Initializes both auth and dynamic state
233
+
234
+ # Store auth values
235
+ page.access_token = "token-abc"
236
+ page.user_id = "user-123"
237
+
238
+ # Retrieve auth values
239
+ token = page.access_token
240
+ user_id = page.user_id
241
+ """
242
+
243
+ def init_auth_state(self) -> None:
244
+ """Initialize both dynamic state and auth-specific properties."""
245
+ self.init_dynamic_state()
246
+ # Initialize standard auth properties
247
+ self.set_state("access_token", None)
248
+ self.set_state("refresh_token", None)
249
+ self.set_state("user_id", None)
250
+ self.set_state("user_email", None)
251
+ self.set_state("user_name", None)
252
+
253
+ @property
254
+ def access_token(self) -> Optional[str]:
255
+ """Get the current access token."""
256
+ return self.get_state("access_token")
257
+
258
+ @access_token.setter
259
+ def access_token(self, value: Optional[str]) -> None:
260
+ """Set the access token."""
261
+ self.set_state("access_token", value)
262
+
263
+ @property
264
+ def refresh_token(self) -> Optional[str]:
265
+ """Get the current refresh token."""
266
+ return self.get_state("refresh_token")
267
+
268
+ @refresh_token.setter
269
+ def refresh_token(self, value: Optional[str]) -> None:
270
+ """Set the refresh token."""
271
+ self.set_state("refresh_token", value)
272
+
273
+ @property
274
+ def user_id(self) -> Optional[str]:
275
+ """Get the current user ID."""
276
+ return self.get_state("user_id")
277
+
278
+ @user_id.setter
279
+ def user_id(self, value: Optional[str]) -> None:
280
+ """Set the user ID."""
281
+ self.set_state("user_id", value)
282
+
283
+ @property
284
+ def user_email(self) -> Optional[str]:
285
+ """Get the current user email."""
286
+ return self.get_state("user_email")
287
+
288
+ @user_email.setter
289
+ def user_email(self, value: Optional[str]) -> None:
290
+ """Set the user email."""
291
+ self.set_state("user_email", value)
292
+
293
+ @property
294
+ def user_name(self) -> Optional[str]:
295
+ """Get the current user name."""
296
+ return self.get_state("user_name")
297
+
298
+ @user_name.setter
299
+ def user_name(self, value: Optional[str]) -> None:
300
+ """Set the user name."""
301
+ self.set_state("user_name", value)
302
+
303
+ @property
304
+ def is_authenticated(self) -> bool:
305
+ """Check if currently authenticated (has access token)."""
306
+ return self.access_token is not None
307
+
308
+ def clear_auth(self) -> None:
309
+ """Clear all authentication state."""
310
+ self.set_state("access_token", None)
311
+ self.set_state("refresh_token", None)
312
+ self.set_state("user_id", None)