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.
- socialseed_e2e/__init__.py +184 -20
- socialseed_e2e/__version__.py +2 -2
- socialseed_e2e/cli.py +353 -190
- socialseed_e2e/core/base_page.py +368 -49
- socialseed_e2e/core/config_loader.py +15 -3
- socialseed_e2e/core/headers.py +11 -4
- socialseed_e2e/core/loaders.py +6 -4
- socialseed_e2e/core/test_orchestrator.py +2 -0
- socialseed_e2e/core/test_runner.py +487 -0
- socialseed_e2e/templates/agent_docs/AGENT_GUIDE.md.template +412 -0
- socialseed_e2e/templates/agent_docs/EXAMPLE_TEST.md.template +152 -0
- socialseed_e2e/templates/agent_docs/FRAMEWORK_CONTEXT.md.template +55 -0
- socialseed_e2e/templates/agent_docs/WORKFLOW_GENERATION.md.template +182 -0
- socialseed_e2e/templates/data_schema.py.template +111 -70
- socialseed_e2e/templates/e2e.conf.template +19 -0
- socialseed_e2e/templates/service_page.py.template +82 -27
- socialseed_e2e/templates/test_module.py.template +21 -7
- socialseed_e2e/templates/verify_installation.py +192 -0
- socialseed_e2e/utils/__init__.py +29 -0
- socialseed_e2e/utils/ai_generator.py +463 -0
- socialseed_e2e/utils/pydantic_helpers.py +392 -0
- socialseed_e2e/utils/state_management.py +312 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/METADATA +64 -27
- socialseed_e2e-0.1.2.dist-info/RECORD +38 -0
- socialseed_e2e-0.1.0.dist-info/RECORD +0 -29
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/WHEEL +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/entry_points.txt +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {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)
|