orionis 0.292.0__py3-none-any.whl → 0.294.0__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.
@@ -145,6 +145,15 @@ class Testing:
145
145
  }
146
146
  )
147
147
 
148
+ web_report: bool = field(
149
+ default=False,
150
+ metadata={
151
+ "description": "Whether to generate a web report for the test results. Default is False.",
152
+ "required": True,
153
+ "default": False
154
+ }
155
+ )
156
+
148
157
  def __post_init__(self):
149
158
  """
150
159
  Post-initialization validation for the testing configuration entity.
@@ -245,14 +254,19 @@ class Testing:
245
254
  f"Invalid type for 'persistent': {type(self.persistent).__name__}. It must be a boolean (True or False)."
246
255
  )
247
256
 
248
- if not isinstance(self.persistent_driver, str):
249
- raise OrionisIntegrityException(
250
- f"Invalid type for 'persistent_driver': {type(self.persistent_driver).__name__}. It must be a string."
251
- )
257
+ if self.persistent:
258
+ if not isinstance(self.persistent_driver, str):
259
+ raise OrionisIntegrityException(
260
+ f"Invalid type for 'persistent_driver': {type(self.persistent_driver).__name__}. It must be a string."
261
+ )
262
+ if self.persistent_driver not in ['sqlite', 'json']:
263
+ raise OrionisIntegrityException(
264
+ f"Invalid value for 'persistent_driver': {self.persistent_driver}. It must be one of: ['sqlite', 'json']."
265
+ )
252
266
 
253
- if self.persistent_driver not in ['sqlite', 'json']:
267
+ if not isinstance(self.web_report, bool):
254
268
  raise OrionisIntegrityException(
255
- f"Invalid value for 'persistent_driver': {self.persistent_driver}. It must be one of: ['sqlite', 'json']."
269
+ f"Invalid type for 'web_report': {type(self.web_report).__name__}. It must be a boolean (True or False)."
256
270
  )
257
271
 
258
272
  def toDict(self) -> dict:
@@ -5,7 +5,7 @@
5
5
  NAME = "orionis"
6
6
 
7
7
  # Current version of the framework
8
- VERSION = "0.292.0"
8
+ VERSION = "0.294.0"
9
9
 
10
10
  # Full name of the author or maintainer of the project
11
11
  AUTHOR = "Raul Mauricio Uñate Castro"
@@ -16,15 +16,17 @@ from rich.live import Live
16
16
  from rich.panel import Panel
17
17
  from rich.syntax import Syntax
18
18
  from rich.table import Table
19
+ from rich.text import Text
19
20
  from orionis.console.output.console import Console
20
21
  from orionis.test.entities.test_result import TestResult
21
22
  from orionis.test.enums.test_mode import ExecutionMode
22
23
  from orionis.test.enums.test_status import TestStatus
23
- from orionis.test.exceptions.test_persistence_error import OrionisTestPersistenceError
24
24
  from orionis.test.exceptions.test_failure_exception import OrionisTestFailureException
25
+ from orionis.test.exceptions.test_persistence_error import OrionisTestPersistenceError
25
26
  from orionis.test.exceptions.test_value_error import OrionisTestValueError
26
27
  from orionis.test.logs.history import TestHistory
27
28
  from orionis.test.suites.contracts.test_unit import IUnitTest
29
+ from orionis.test.view.render import TestingResultRender
28
30
 
29
31
  class UnitTest(IUnitTest):
30
32
  """
@@ -129,7 +131,9 @@ class UnitTest(IUnitTest):
129
131
  self.throw_exception: bool = False
130
132
  self.persistent: bool = False
131
133
  self.persistent_driver: str = 'sqlite'
134
+ self.web_report: bool = False
132
135
  self.base_path: str = "tests"
136
+ self.withliveconsole: bool = True
133
137
 
134
138
  def configure(
135
139
  self,
@@ -140,7 +144,8 @@ class UnitTest(IUnitTest):
140
144
  print_result: bool = None,
141
145
  throw_exception: bool = False,
142
146
  persistent: bool = False,
143
- persistent_driver: str = 'sqlite'
147
+ persistent_driver: str = 'sqlite',
148
+ web_report: bool = False
144
149
  ) -> 'UnitTest':
145
150
  """
146
151
  Configures the UnitTest instance with the specified parameters.
@@ -195,6 +200,9 @@ class UnitTest(IUnitTest):
195
200
  if persistent_driver is not None:
196
201
  self.persistent_driver = persistent_driver
197
202
 
203
+ if web_report is not None:
204
+ self.web_report = web_report
205
+
198
206
  return self
199
207
 
200
208
  def discoverTestsInFolder(
@@ -385,17 +393,27 @@ class UnitTest(IUnitTest):
385
393
  OrionisTestFailureException
386
394
  If `throw_exception` is True and there are test failures or errors.
387
395
  """
396
+
397
+ # Check if required print_result and throw_exception
388
398
  if print_result is not None:
389
399
  self.print_result = print_result
390
400
  if throw_exception is not None:
391
401
  self.throw_exception = throw_exception
392
402
 
403
+ # Dynamically determine if live console should be enabled based on test code usage
404
+ self._withLiveConsole()
405
+
406
+ # Start the timer and print the start message
393
407
  self.start_time = time.time()
394
408
  self._startMessage()
395
409
 
396
- # Elegant "running" message using Rich Panel
410
+ # Prepare the running message based on whether live console is enabled
411
+ message = "[bold yellow]⏳ Running tests...[/bold yellow]\n"
412
+ message += "[dim]This may take a few seconds. Please wait...[/dim]" if self.withliveconsole else "[dim]Please wait, results will appear below...[/dim]"
413
+
414
+ # Panel for running message
397
415
  running_panel = Panel(
398
- "[bold yellow]⏳ Running tests...[/bold yellow]\n[dim]This may take a few seconds. Please wait...[/dim]",
416
+ message,
399
417
  border_style="yellow",
400
418
  title="In Progress",
401
419
  title_align="left",
@@ -403,18 +421,18 @@ class UnitTest(IUnitTest):
403
421
  padding=(1, 2)
404
422
  )
405
423
 
406
- # Print the panel and keep a reference to the live display
407
- with Live(running_panel, console=self.rich_console, refresh_per_second=4, transient=True):
408
-
409
- # Setup output capture
410
- output_buffer = io.StringIO()
411
- error_buffer = io.StringIO()
424
+ # Elegant "running" message using Rich Panel
425
+ if self.withliveconsole:
426
+ with Live(running_panel, console=self.rich_console, refresh_per_second=4, transient=True):
427
+ result, output_buffer, error_buffer = self._runSuite()
428
+ else:
429
+ self.rich_console.print(running_panel)
430
+ result, output_buffer, error_buffer = self._runSuite()
412
431
 
413
- # Execute tests based on selected mode
414
- if self.execution_mode == ExecutionMode.PARALLEL.value:
415
- result = self._runTestsInParallel(output_buffer, error_buffer)
416
- else:
417
- result = self._runTestsSequentially(output_buffer, error_buffer)
432
+ # Capture and display the output and error buffers only if not empty
433
+ output_content = output_buffer.getvalue()
434
+ if output_content.strip():
435
+ print(output_buffer.getvalue())
418
436
 
419
437
  # Process results
420
438
  execution_time = time.time() - self.start_time
@@ -431,6 +449,80 @@ class UnitTest(IUnitTest):
431
449
  # Return the summary of the test results
432
450
  return summary
433
451
 
452
+ def _withLiveConsole(self) -> None:
453
+ """
454
+ Determines if the live console should be used based on the presence of debug or dump calls in the test code.
455
+
456
+ Returns
457
+ -------
458
+ bool
459
+ True if the live console should be used, False otherwise.
460
+ """
461
+ if self.withliveconsole:
462
+
463
+ try:
464
+
465
+ # Flatten the test suite to get all test cases
466
+ for test_case in self._flattenTestSuite(self.suite):
467
+
468
+ # Get the source code of the test case class
469
+ source = inspect.getsource(test_case.__class__)
470
+
471
+ # Only match if the keyword is not inside a comment
472
+ for keyword in ('self.dd', 'self.dump'):
473
+
474
+ # Find all lines containing the keyword
475
+ for line in source.splitlines():
476
+ if keyword in line:
477
+
478
+ # Remove leading/trailing whitespace
479
+ stripped = line.strip()
480
+
481
+ # Ignore lines that start with '#' (comments)
482
+ if not stripped.startswith('#') and not re.match(r'^\s*#', line):
483
+ self.withliveconsole = False
484
+ break
485
+
486
+ # If we found a keyword, no need to check further
487
+ if not self.withliveconsole:
488
+ break
489
+
490
+ # If we found a keyword in any test case, no need to check further
491
+ if not self.withliveconsole:
492
+ break
493
+
494
+ except Exception:
495
+ pass
496
+
497
+ def _runSuite(self):
498
+ """
499
+ Run the test suite according to the selected execution mode (parallel or sequential),
500
+ capturing standard output and error streams during execution.
501
+
502
+ Returns
503
+ -------
504
+ tuple
505
+ result : unittest.TestResult
506
+ The result object from the test execution.
507
+ output_buffer : io.StringIO
508
+ Captured standard output during test execution.
509
+ error_buffer : io.StringIO
510
+ Captured standard error during test execution.
511
+ """
512
+
513
+ # Setup output capture
514
+ output_buffer = io.StringIO()
515
+ error_buffer = io.StringIO()
516
+
517
+ # Execute tests based on selected mode
518
+ if self.execution_mode == ExecutionMode.PARALLEL.value:
519
+ result = self._runTestsInParallel(output_buffer, error_buffer)
520
+ else:
521
+ result = self._runTestsSequentially(output_buffer, error_buffer)
522
+
523
+ # Return the result along with captured output and error streams
524
+ return result, output_buffer, error_buffer
525
+
434
526
  def _runTestsSequentially(self, output_buffer: io.StringIO, error_buffer: io.StringIO) -> unittest.TestResult:
435
527
  """
436
528
  Executes the test suite sequentially, capturing the output and error streams.
@@ -757,6 +849,18 @@ class UnitTest(IUnitTest):
757
849
  if self.persistent:
758
850
  self._persistTestResults(report)
759
851
 
852
+ # Handle Web Report Rendering
853
+ if self.web_report:
854
+
855
+ # Generate the web report and get the path
856
+ path = self._webReport(report)
857
+
858
+ # Elegant invitation to view the results, with underlined path
859
+ invite_text = Text("Test results saved. ", style="green")
860
+ invite_text.append("View report: ", style="bold green")
861
+ invite_text.append(str(path), style="underline blue")
862
+ self.rich_console.print(invite_text)
863
+
760
864
  # Return the summary
761
865
  return {
762
866
  "total_tests": result.testsRun,
@@ -769,6 +873,45 @@ class UnitTest(IUnitTest):
769
873
  "test_details": test_details
770
874
  }
771
875
 
876
+ def _webReport(self, summary: Dict[str, Any]) -> None:
877
+ """
878
+ Generates a web report for the test results summary.
879
+
880
+ Parameters
881
+ ----------
882
+ summary : dict
883
+ The summary of test results to generate a web report for.
884
+
885
+ Returns
886
+ -------
887
+ str
888
+ The path to the generated web report.
889
+
890
+ Notes
891
+ -----
892
+ - Determines the storage path based on the current working directory and base_path.
893
+ - Uses TestingResultRender to generate the report.
894
+ - If persistence is enabled and the driver is 'sqlite', the report is marked as persistent.
895
+ - Returns the path to the generated report for further use.
896
+ """
897
+ # Determine the absolute path for storing results
898
+ project = os.path.basename(os.getcwd())
899
+ storage_path = os.path.abspath(os.path.join(os.getcwd(), self.base_path))
900
+
901
+ # Only use storage_path if project is recognized
902
+ if project not in ['framework', 'orionis']:
903
+ storage_path = None
904
+
905
+ # Create the TestingResultRender instance with the storage path and summary
906
+ render = TestingResultRender(
907
+ storage_path=storage_path,
908
+ result=summary,
909
+ persist=self.persistent and self.persistent_driver == 'sqlite'
910
+ )
911
+
912
+ # Render the report and return the path
913
+ return render.render()
914
+
772
915
  def _persistTestResults(self, summary: Dict[str, Any]) -> None:
773
916
  """
774
917
  Persist the test results summary using the configured persistent driver.
@@ -793,13 +936,13 @@ class UnitTest(IUnitTest):
793
936
  """
794
937
 
795
938
  try:
796
- if self.persistent_driver == 'sqlite':
939
+ # Determine the absolute path for storing results
940
+ project = os.getcwd().split(os.sep)[-1]
941
+ storage_path = None
942
+ if project in ['framework', 'orionis']:
943
+ storage_path = os.path.abspath(os.path.join(os.getcwd(), self.base_path))
797
944
 
798
- # Determine the absolute path for storing results
799
- project = os.getcwd().split(os.sep)[-1]
800
- storage_path = None
801
- if project in ['framework', 'orionis']:
802
- storage_path = os.path.abspath(os.path.join(os.getcwd(), self.base_path))
945
+ if self.persistent_driver == 'sqlite':
803
946
 
804
947
  # Initialize the TestHistory class for database operations
805
948
  history = TestHistory(
@@ -813,14 +956,14 @@ class UnitTest(IUnitTest):
813
956
 
814
957
  elif self.persistent_driver == 'json':
815
958
 
959
+ # Ensure the base path exists and write the summary to a JSON file
960
+ os.makedirs(storage_path, exist_ok=True)
961
+
816
962
  # Get the current timestamp for the log file name
817
963
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
818
964
 
819
- # Ensure the base path exists and write the summary to a JSON file
820
- os.makedirs(self.base_path, exist_ok=True)
821
-
822
965
  # Create the log file path with the timestamp
823
- log_path = os.path.join(self.base_path, f'test_{timestamp}.json')
966
+ log_path = os.path.abspath(os.path.join(storage_path, f'test_{timestamp}.json'))
824
967
 
825
968
  # Write the summary to the JSON file
826
969
  with open(log_path, 'w', encoding='utf-8') as log:
@@ -0,0 +1,128 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from orionis.services.environment.env import Env
5
+ from orionis.test.logs.history import TestHistory
6
+
7
+ class TestingResultRender:
8
+
9
+ def __init__(
10
+ self,
11
+ result,
12
+ storage_path:str = None,
13
+ persist=False
14
+ ):
15
+ """
16
+ Initialize the TestingResultRender object.
17
+
18
+ Parameters
19
+ ----------
20
+ result : Any
21
+ The test result data to be processed or stored.
22
+ storage_path : str, optional
23
+ Custom path to store the test report. If not provided, uses the environment variable
24
+ 'TEST_REPORT_PATH' or defaults to a local storage path.
25
+ persist : bool, optional
26
+ Whether to persist the report. Defaults to False.
27
+
28
+ Notes
29
+ -----
30
+ - Determines the file path for the test report based on the provided storage_path, environment variable,
31
+ or a default location.
32
+ - Ensures the parent directory for the report exists.
33
+ - Stores the resolved report path in the environment variable 'TEST_REPORT_PATH'.
34
+ """
35
+
36
+ # Initialize instance variables
37
+ self.__filename = 'test-results.html'
38
+ self.__result = result
39
+ self.__persist = persist
40
+
41
+ # Determine file path
42
+ db_path = None
43
+ if storage_path:
44
+ db_path = Path(storage_path).expanduser().resolve()
45
+ if db_path.is_dir():
46
+ db_path = db_path / self.__filename
47
+ else:
48
+ env_path = Env.get("TEST_REPORT_PATH", None)
49
+ if env_path:
50
+ db_path = Path(env_path).expanduser().resolve()
51
+ if db_path.is_dir():
52
+ db_path = db_path / self.__filename
53
+ else:
54
+ db_path = Path.cwd() / 'storage/framework/testing' / self.__filename
55
+
56
+ # Ensure parent directory exists
57
+ db_path.parent.mkdir(parents=True, exist_ok=True)
58
+
59
+ # Store path in environment
60
+ Env.set("TEST_REPORT_PATH", str(db_path), 'path')
61
+ self.__report_path = db_path
62
+
63
+ def render(self):
64
+ """
65
+ Otherwise, uses the current test result stored in memory. The method replaces placeholders in a
66
+ template file with the test results and the persistence mode, then writes the rendered content
67
+ to a report file.
68
+
69
+ Parameters
70
+ ----------
71
+ None
72
+
73
+ Returns
74
+ -------
75
+ str
76
+ The full path to the generated report file.
77
+
78
+ Notes
79
+ -----
80
+ - If persistence is enabled, the last 10 reports are fetched from the SQLite database.
81
+ - If persistence is not enabled, only the current test result in memory is used.
82
+ - The method reads a template file, replaces placeholders with the test results and persistence mode,
83
+ and writes the final content to the report file.
84
+ """
85
+
86
+ # Determine the source of test results based on persistence mode
87
+ if self.__persist:
88
+ # If persistence is enabled, fetch the last 10 reports from SQLite
89
+ logs = TestHistory()
90
+ reports = logs.get(last=10)
91
+ # Parse each report's JSON data into a list
92
+ results_list = [json.loads(report[1]) for report in reports]
93
+ else:
94
+ # If not persistent, use only the current in-memory result
95
+ results_list = [self.__result]
96
+
97
+ # Set placeholder values for the template
98
+ persistence_mode = 'SQLite' if self.__persist else 'Static'
99
+ test_results_json = json.dumps(results_list, ensure_ascii=False, indent=None)
100
+
101
+ # Locate the HTML template file
102
+ template_path = Path(__file__).parent / 'report.stub'
103
+
104
+ # Read the template content
105
+ with open(template_path, 'r', encoding='utf-8') as template_file:
106
+ template_content = template_file.read()
107
+
108
+ # Replace placeholders with actual values
109
+ rendered_content = template_content.replace(
110
+ '{{orionis-testing-result}}',
111
+ test_results_json
112
+ ).replace(
113
+ '{{orionis-testing-persistent}}',
114
+ persistence_mode
115
+ )
116
+
117
+ # Write the rendered HTML report to the specified path
118
+ with open(self.__report_path, 'w', encoding='utf-8') as report_file:
119
+ report_file.write(rendered_content)
120
+
121
+ # Open the generated report in the default web browser if running on Windows or macOS.
122
+ # This provides immediate feedback to the user after report generation.
123
+ if os.name == 'nt' or os.name == 'posix' and sys.platform == 'darwin':
124
+ import webbrowser
125
+ webbrowser.open(self.__report_path.as_uri())
126
+
127
+ # Return the absolute path to the generated report
128
+ return str(self.__report_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orionis
3
- Version: 0.292.0
3
+ Version: 0.294.0
4
4
  Summary: Orionis Framework – Elegant, Fast, and Powerful.
5
5
  Home-page: https://github.com/orionis-framework/framework
6
6
  Author: Raul Mauricio Uñate Castro
@@ -222,11 +222,11 @@ orionis/foundation/config/session/enums/same_site_policy.py,sha256=Oo05CJ-5keJWz
222
222
  orionis/foundation/config/session/helpers/secret_key.py,sha256=yafjzQ9KVQdXzCQCMthpgizlNCo5F5UTLtAnInipUMk,447
223
223
  orionis/foundation/config/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
224
224
  orionis/foundation/config/testing/entities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
225
- orionis/foundation/config/testing/entities/testing.py,sha256=m_i9jZlOXs_AzNKNNf0a3ByS7jDGaRz_2FO1ScV577Q,11293
225
+ orionis/foundation/config/testing/entities/testing.py,sha256=AuhPU9O15Aeqs8jQVHWJwamgrrcvmC4ThsJ31jyrWic,11849
226
226
  orionis/foundation/config/testing/enums/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
227
  orionis/foundation/config/testing/enums/test_mode.py,sha256=IbFpauu7J-iSAfmC8jDbmTEYl8eZr-AexL-lyOh8_74,337
228
228
  orionis/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
229
- orionis/metadata/framework.py,sha256=7Ko8j0LyRbf2F4ayJKqLau4RUwwdelswaW4ISf8MnFQ,4960
229
+ orionis/metadata/framework.py,sha256=aV2GKSisyRjv08CYG9hLpyxUodGeHnrEUyzvebA8xGw,4960
230
230
  orionis/metadata/package.py,sha256=tqLfBRo-w1j_GN4xvzUNFyweWYFS-qhSgAEc-AmCH1M,5452
231
231
  orionis/patterns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
232
232
  orionis/patterns/singleton/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -345,16 +345,16 @@ orionis/test/output/contracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
345
345
  orionis/test/output/contracts/dumper.py,sha256=5OqGc4GEXCXX76sCX185giQMyKwwZvlOv3I7tTwV2fQ,1324
346
346
  orionis/test/suites/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
347
347
  orionis/test/suites/test_suite.py,sha256=fdVmC56PJfWDuYeekY7oN-04AEHMqxwLI5mZNJAuOZI,5261
348
- orionis/test/suites/test_unit.py,sha256=ZkpRyUEHsxnbto3FBmvPfFHHRyOgv4NBIcS2LF1-Y_s,49374
348
+ orionis/test/suites/test_unit.py,sha256=dnLEEeBnGkE7DRM2XXJPtxHw25JLzP9ZtcGImmBNBM4,54916
349
349
  orionis/test/suites/contracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
350
350
  orionis/test/suites/contracts/test_suite.py,sha256=eluzYwkNBbKjxYStj_tHN_Fm3YDPpGQdqMu5eiluh-E,1059
351
351
  orionis/test/suites/contracts/test_unit.py,sha256=l1LQllODyvcSByXMl1lGrUkoLsXbBHZZLWZI4A-mlQg,5881
352
352
  orionis/test/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
353
- orionis/test/view/index.html,sha256=1qamNWmUjyOKcMnNiVfjvYMT_CHsmz7WDhBzp0dQkV8,25731
354
- orionis-0.292.0.dist-info/licenses/LICENCE,sha256=-_4cF2EBKuYVS_SQpy1uapq0oJPUU1vl_RUWSy2jJTo,1111
353
+ orionis/test/view/render.py,sha256=jXZkbITBknbUwm_mD8bcTiwLDvsFkrO9qrf0ZgPwqxc,4903
354
+ orionis-0.294.0.dist-info/licenses/LICENCE,sha256=-_4cF2EBKuYVS_SQpy1uapq0oJPUU1vl_RUWSy2jJTo,1111
355
355
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
356
356
  tests/example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
357
- tests/example/test_example.py,sha256=pGj3mn6HslmqVNEc_2cbMwbIEcCjT08fLsZ47sz5bvk,601
357
+ tests/example/test_example.py,sha256=byd_lI6tVDgGPEIrr7PLZbBu0UoZOymmdmyA_4u-QUw,601
358
358
  tests/foundation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
359
359
  tests/foundation/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
360
360
  tests/foundation/config/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -455,8 +455,8 @@ tests/support/inspection/fakes/fake_reflection_instance_with_abstract.py,sha256=
455
455
  tests/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
456
456
  tests/testing/test_testing_result.py,sha256=MrGK3ZimedL0b5Ydu69Dg8Iul017AzLTm7VPxpXlpfU,4315
457
457
  tests/testing/test_testing_unit.py,sha256=A6QkiOkP7GPC1Szh_GqsrV7GxjWjK8cIwFez6YfrzmM,7683
458
- orionis-0.292.0.dist-info/METADATA,sha256=4zAZQi8NiTjCdg0efqEfXRXRP_RTRfnScor13qqt_jY,4772
459
- orionis-0.292.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
460
- orionis-0.292.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
461
- orionis-0.292.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
462
- orionis-0.292.0.dist-info/RECORD,,
458
+ orionis-0.294.0.dist-info/METADATA,sha256=7lTo0mbUupOIdMFbWCqfYzOOrNiVyGigY9kSGs94hIA,4772
459
+ orionis-0.294.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
460
+ orionis-0.294.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
461
+ orionis-0.294.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
462
+ orionis-0.294.0.dist-info/RECORD,,
@@ -17,7 +17,7 @@ class TestExample(TestCase):
17
17
  Ensures that the integer 2 is equal to itself.
18
18
  """
19
19
  # Check if 1 equals 1
20
- self.assertEqual(1, 2)
20
+ self.assertEqual(1, 1)
21
21
 
22
22
  # Check if 2 equals 2
23
23
  self.assertEqual(2, 2)
@@ -1,533 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Orionis Test Dashboard</title>
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <link rel="icon" href="https://orionis-framework.com/svg/logo.svg" />
10
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
11
- <link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />
12
- <link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap" rel="stylesheet">
13
- </head>
14
-
15
- <style>
16
- /* Use a monospaced font for the table */
17
- #test-table,
18
- #test-table .gridjs-td,
19
- #test-table .gridjs-th {
20
- font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'Liberation Mono', monospace !important;
21
- }
22
-
23
- #test-table .gridjs-td,
24
- #test-table .gridjs-th {
25
- max-width: 220px;
26
- white-space: normal;
27
- overflow-wrap: anywhere;
28
- text-overflow: initial;
29
- vertical-align: middle;
30
- font-size: 0.85rem; /* más pequeño */
31
- line-height: 1.28;
32
- }
33
-
34
- #test-table .gridjs-th {
35
- font-size: 0.82rem !important; /* aún más pequeño para los títulos */
36
- font-weight: 600;
37
- letter-spacing: 0.01em;
38
- }
39
-
40
- #test-table .gridjs-td.status-cell {
41
- max-width: 90px;
42
- text-align: center;
43
- font-weight: 600;
44
- letter-spacing: 0.02em;
45
- white-space: nowrap;
46
- font-size: 0.75em;
47
- padding-top: 0.2em;
48
- padding-bottom: 0.2em;
49
- }
50
-
51
- #test-table .gridjs-td.doc-cell {
52
- max-width: 60px;
53
- text-align: center;
54
- white-space: nowrap;
55
- }
56
-
57
- #test-table {
58
- overflow-x: auto;
59
- }
60
-
61
- .badge-status {
62
- display: inline-flex;
63
- align-items: center;
64
- gap: 0.3em;
65
- padding: 0.13em 0.6em;
66
- border-radius: 9999px;
67
- font-size: 0.78em;
68
- font-weight: 600;
69
- letter-spacing: 0.01em;
70
- }
71
-
72
- .badge-passed {
73
- background: #d1fae5;
74
- color: #059669;
75
- }
76
-
77
- .badge-failed {
78
- background: #fee2e2;
79
- color: #dc2626;
80
- }
81
-
82
- .badge-errors {
83
- background: #fef9c3;
84
- color: #ca8a04;
85
- }
86
-
87
- .badge-skipped {
88
- background: #e0e7ff;
89
- color: #3730a3;
90
- }
91
-
92
- .doc-btn {
93
- background: #eef2ff;
94
- color: #3730a3;
95
- border: none;
96
- border-radius: 0.5em;
97
- padding: 0.2em 0.8em;
98
- font-size: 0.92em;
99
- font-weight: 500;
100
- cursor: pointer;
101
- transition: background 0.15s;
102
- font-family: inherit;
103
- }
104
-
105
- .doc-btn:hover {
106
- background: #c7d2fe;
107
- }
108
-
109
- /* Modal styles */
110
- .orionis-modal {
111
- position: fixed;
112
- left: 0;
113
- top: 0;
114
- width: 100vw;
115
- height: 100vh;
116
- background: rgba(30, 41, 59, 0.45);
117
- display: flex !important;
118
- align-items: center;
119
- justify-content: center;
120
- z-index: 10000;
121
- }
122
-
123
- .orionis-modal-content {
124
- background: white;
125
- max-width: 70vw;
126
- width: 70vw;
127
- padding: 2em 1.5em 1.2em 1.5em;
128
- border-radius: 1.2em;
129
- box-shadow: 0 8px 32px 0 rgba(31, 41, 55, 0.18);
130
- position: relative;
131
- }
132
-
133
- .orionis-modal-close {
134
- position: absolute;
135
- top: 0.7em;
136
- right: 1em;
137
- font-size: 1.3em;
138
- color: #64748b;
139
- background: none;
140
- border: none;
141
- cursor: pointer;
142
- }
143
-
144
- .orionis-modal-title {
145
- font-size: 0.98em; /* reducido de 1.08em */
146
- font-weight: 600;
147
- margin-bottom: 0.7em;
148
- color: #3730a3;
149
- display: flex;
150
- align-items: center;
151
- gap: 0.5em;
152
- font-family: inherit;
153
- }
154
-
155
- .orionis-modal-pre {
156
- white-space: pre-wrap;
157
- font-size: 0.92em;
158
- color: #334155;
159
- background: #f1f5f9;
160
- padding: 1em;
161
- border-radius: 0.7em;
162
- max-height: 320px;
163
- overflow: auto;
164
- font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'Liberation Mono', monospace;
165
- }
166
- </style>
167
-
168
- <body class="bg-gray-100 text-gray-800 font-sans">
169
- <div class="max-w-7xl mx-auto px-4 sm:px-6 py-8">
170
-
171
- <!-- Header -->
172
- <header class="bg-gradient-to-r from-blue-900 to-cyan-400 text-white rounded-2xl shadow-xl p-6 mb-10">
173
- <div class="flex flex-col md:flex-row justify-between items-center gap-4">
174
- <div class="flex items-center gap-4">
175
- <img src="https://orionis-framework.com/svg/logo.svg" alt="Orionis Logo"
176
- class="h-10 brightness-0 invert" />
177
- <h1 class="text-2xl font-light tracking-wider">Orionis Testing Results Dashboard</h1>
178
- </div>
179
- <div id="timestamp" class="text-sm text-white/90"></div>
180
- </header>
181
-
182
- <!-- Execution Summary Card -->
183
- <div class="w-full mb-10">
184
- <div class="bg-white rounded-2xl shadow-lg p-6 border-t-4 border-indigo-500 flex flex-col sm:flex-row items-center justify-between gap-4">
185
- <div>
186
- <div class="text-xs font-semibold text-gray-500 uppercase">Resumen de Ejecución</div>
187
- <div class="text-lg font-bold text-gray-800 mt-2" id="execution-summary-title">Select an execution</div>
188
- <div class="text-sm text-gray-600 mt-1" id="execution-summary-desc">Select an execution to view the summary.</div>
189
- </div>
190
- <div class="flex items-center gap-4">
191
- <div class="flex items-center gap-1 text-gray-700">
192
- <span id="execution-time">Duration: --:--:--</span>
193
- </div>
194
- </div>
195
- </div>
196
- </div>
197
-
198
- <!-- Summary Cards -->
199
- <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 mb-10">
200
-
201
- <div class="bg-white rounded-2xl shadow-lg p-6 border-t-4 border-green-500">
202
- <div class="text-xs font-semibold text-gray-500 uppercase">Passed</div>
203
- <div class="text-4xl font-bold text-gray-800 mt-2" id="passed">0</div>
204
- <div class="mt-4 bg-gray-200 rounded-full h-2">
205
- <div class="bg-green-500 h-2 rounded-full" id="passed-progress" style="width: 0%"></div>
206
- </div>
207
- </div>
208
-
209
- <div class="bg-white rounded-2xl shadow-lg p-6 border-t-4 border-red-500">
210
- <div class="text-xs font-semibold text-gray-500 uppercase">Failed</div>
211
- <div class="text-4xl font-bold text-gray-800 mt-2" id="failed">0</div>
212
- </div>
213
-
214
- <div class="bg-white rounded-2xl shadow-lg p-6 border-t-4 border-yellow-500">
215
- <div class="text-xs font-semibold text-gray-500 uppercase">Errors</div>
216
- <div class="text-4xl font-bold text-gray-800 mt-2" id="errors">0</div>
217
- </div>
218
-
219
- <div class="bg-white rounded-2xl shadow-lg p-6 border-t-4 border-blue-500">
220
- <div class="text-xs font-semibold text-gray-500 uppercase">Skipped</div>
221
- <div class="text-4xl font-bold text-gray-800 mt-2" id="skipped">0</div>
222
- </div>
223
-
224
- </div>
225
-
226
- <!-- Download Buttons & Select -->
227
- <div class="flex flex-wrap justify-between items-center mb-10 gap-4">
228
- <!-- Buttons to the left -->
229
- <div class="flex flex-wrap gap-4">
230
- <button disabled id="download-json" class="flex items-center gap-2 bg-blue-300 text-white px-4 py-2 rounded shadow cursor-not-allowed opacity-60">
231
- <i class="bi bi-file-earmark-code-fill text-lg"></i>
232
- <span>Download JSON</span>
233
- </button>
234
- </div>
235
- <!-- Elegant Select to the right -->
236
- <div>
237
- <select class="appearance-none bg-white border border-gray-300 text-gray-700 py-2 px-4 pr-10 rounded-lg shadow focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-blue-400 transition text-base font-medium" id="test-select">
238
- </select>
239
- </div>
240
- </div>
241
-
242
- <!-- Table Summary View with Grid.js -->
243
- <div class="bg-white rounded-xl shadow-md p-5 border border-indigo-100">
244
- <div class="flex items-center gap-2 mb-3">
245
- <i class="bi bi-table text-lg text-indigo-500"></i>
246
- <h2 class="text-lg font-semibold text-indigo-900">Execution Details</h2>
247
- </div>
248
- <div class="text-gray-500 mb-4 text-sm">
249
- See all test cases for the selected execution.<br>Click
250
- <span class="inline-flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 text-blue-700 rounded text-xs font-mono">
251
- <i class="bi bi-journal-text"></i> View
252
- </span>
253
- to show the docstring.
254
- </div>
255
- <div id="test-table"></div>
256
- </div>
257
-
258
- <!-- Footer -->
259
- <footer class="mt-12 text-center text-gray-500 text-sm py-6">
260
- Developed with the power of
261
- <a href="https://orionis-framework.com/" target="_blank" rel="noopener"
262
- class="font-semibold text-blue-700 hover:underline">
263
- Orionis Framework
264
- </a>
265
- <i class="bi bi-stars text-yellow-400 align-middle ml-1"></i>
266
- </footer>
267
-
268
- <script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
269
- <script>
270
-
271
- const data = $data;
272
-
273
- // Live Clock (Header Timestamp)
274
- function updateClock() {
275
- const now = new Date();
276
- const formatted = now.toLocaleString('en-US', {
277
- year: 'numeric',
278
- month: '2-digit',
279
- day: '2-digit',
280
- hour: '2-digit',
281
- minute: '2-digit',
282
- second: '2-digit',
283
- hour12: false
284
- });
285
- document.getElementById("timestamp").textContent = `Current time: ${formatted}`;
286
- }
287
- updateClock();
288
- setInterval(updateClock, 1000);
289
-
290
- // Populate Dropdown in English
291
- let html = '<option selected disabled>Select an execution</option>';
292
- data.forEach((item, i) => {
293
- const date = new Date(item.timestamp);
294
- const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1)
295
- .toString()
296
- .padStart(2, '0')}-${date.getDate()
297
- .toString()
298
- .padStart(2, '0')} ${date.getHours()
299
- .toString()
300
- .padStart(2, '0')}:${date.getMinutes()
301
- .toString()
302
- .padStart(2, '0')}:${date.getSeconds()
303
- .toString()
304
- .padStart(2, '0')}`;
305
- html += `<option value="${i}">Execution ${i + 1} - ${formattedDate}</option>`;
306
- });
307
- document.getElementById("test-select").innerHTML = html;
308
-
309
- // Event Listener for the dropdown
310
- document.getElementById("test-select").addEventListener("change", function () {
311
- const selectedIndex = this.value;
312
- const selectedData = data[selectedIndex];
313
-
314
- // Animate Counters
315
- function animateValue(id, start, end, duration) {
316
- const obj = document.getElementById(id);
317
- let startTimestamp = null;
318
- const step = (timestamp) => {
319
- if (!startTimestamp) startTimestamp = timestamp;
320
- const progress = Math.min((timestamp - startTimestamp) / duration, 1);
321
- obj.textContent = Math.floor(progress * (end - start) + start);
322
- if (progress < 1) {
323
- window.requestAnimationFrame(step);
324
- } else {
325
- obj.textContent = end;
326
- }
327
- };
328
- window.requestAnimationFrame(step);
329
- }
330
- animateValue("passed", Number(document.getElementById("passed").textContent), selectedData.passed, 500);
331
- animateValue("failed", Number(document.getElementById("failed").textContent), selectedData.failed, 500);
332
- animateValue("errors", Number(document.getElementById("errors").textContent), selectedData.errors, 500);
333
- animateValue("skipped", Number(document.getElementById("skipped").textContent), selectedData.skipped, 500);
334
-
335
- // Animate Progress Bar
336
- const passedBar = document.getElementById("passed-progress");
337
- passedBar.style.transition = "width 0.6s cubic-bezier(0.4,0,0.2,1)";
338
- const passedPercentage = (selectedData.passed / selectedData.total_tests) * 100;
339
- passedBar.style.width = `${passedPercentage}%`;
340
-
341
- // Update Execution Summary
342
- const date = new Date(selectedData.timestamp);
343
- const formattedDate = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
344
- document.getElementById("execution-summary-title").innerHTML = `<span class="inline-flex items-center gap-2"><i class="bi bi-activity text-indigo-500"></i> Execution - <span class="font-mono">${formattedDate}</span></span>`;
345
- document.getElementById("execution-summary-desc").innerHTML =
346
- `<span class="inline-flex gap-2 flex-wrap">
347
- <span class="bg-green-100 text-green-800 px-2 py-1 rounded text-xs font-semibold"><i class="bi bi-check-circle-fill"></i> Passed: ${selectedData.passed}</span>
348
- <span class="bg-red-100 text-red-800 px-2 py-1 rounded text-xs font-semibold"><i class="bi bi-x-circle-fill"></i> Failed: ${selectedData.failed}</span>
349
- <span class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs font-semibold"><i class="bi bi-exclamation-triangle-fill"></i> Errors: ${selectedData.errors}</span>
350
- <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-semibold"><i class="bi bi-skip-forward-fill"></i> Skipped: ${selectedData.skipped}</span>
351
- <span class="bg-gray-100 text-gray-800 px-2 py-1 rounded text-xs font-semibold"><i class="bi bi-list-ol"></i> Total: ${selectedData.total_tests}</span>
352
- <span class="bg-indigo-100 text-indigo-800 px-2 py-1 rounded text-xs font-semibold"><i class="bi bi-bar-chart-fill"></i> Success: ${selectedData.success_rate.toFixed(2)}%</span>
353
- </span>`;
354
-
355
- // Update Duration
356
- function formatTime(seconds) {
357
- const h = Math.floor(seconds / 3600).toString().padStart(2, '0');
358
- const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
359
- const s = Math.floor(seconds % 60).toString().padStart(2, '0');
360
- return `${h}:${m}:${s}`;
361
- }
362
- document.getElementById("execution-time").innerHTML = `<i class="bi bi-clock-history text-indigo-500"></i> Duration: <span class="font-mono">${formatTime(selectedData.total_time)}</span>`;
363
-
364
- // Enable Download Button
365
- document.getElementById("download-json").disabled = false;
366
- document.getElementById("download-json").classList.remove("bg-blue-300", "cursor-not-allowed", "opacity-60");
367
- document.getElementById("download-json").classList.add("bg-blue-600", "hover:bg-blue-700", "cursor-pointer", "opacity-100");
368
- });
369
-
370
- // Download JSON report with execution date and time in filename
371
- document.getElementById("download-json").addEventListener("click", function () {
372
- const select = document.getElementById("test-select");
373
- const selectedIndex = select.value;
374
- if (selectedIndex === "" || selectedIndex === null || isNaN(selectedIndex)) return;
375
- const selectedData = data[selectedIndex];
376
- const jsonString = JSON.stringify(selectedData, null, 2);
377
- const date = new Date(selectedData.timestamp);
378
- // Format: YYYYMMDD_HHMMSS
379
- const formattedDate = `${date.getFullYear()}${(date.getMonth()+1).toString().padStart(2,'0')}${date.getDate().toString().padStart(2,'0')}_${date.getHours().toString().padStart(2,'0')}${date.getMinutes().toString().padStart(2,'0')}${date.getSeconds().toString().padStart(2,'0')}`;
380
- const filename = `test_report_${formattedDate}.json`;
381
- const blob = new Blob([jsonString], { type: "application/json" });
382
- const url = URL.createObjectURL(blob);
383
- const a = document.createElement("a");
384
- a.href = url;
385
- a.download = filename;
386
- document.body.appendChild(a);
387
- a.click();
388
- document.body.removeChild(a);
389
- URL.revokeObjectURL(url);
390
- });
391
-
392
- // Show doc string in a modal dialog (event delegation)
393
- function showDocString(doc) {
394
- // Remove any existing modal
395
- const oldModal = document.querySelector('.orionis-modal');
396
- if (oldModal) oldModal.remove();
397
-
398
- // Create modal element
399
- const modal = document.createElement('div');
400
- modal.className = 'orionis-modal';
401
- modal.style.display = 'flex'; // Ensure modal is visible
402
-
403
- modal.innerHTML = `
404
- <div class="orionis-modal-content">
405
- <button class="orionis-modal-close" aria-label="Cerrar" type="button">&times;</button>
406
- <div class="orionis-modal-title">
407
- <i class="bi bi-journal-text"></i> Docstring
408
- </div>
409
- <pre class="orionis-modal-pre">${doc ? String(doc).replace(/</g, "&lt;").replace(/>/g, "&gt;") : 'No docstring.'}</pre>
410
- </div>
411
- `;
412
- // Close modal when clicking outside content
413
- modal.addEventListener('mousedown', function (e) {
414
- if (e.target === modal) modal.remove();
415
- });
416
- // Close modal when clicking close button
417
- modal.querySelector('.orionis-modal-close').onclick = () => modal.remove();
418
- document.body.appendChild(modal);
419
- }
420
-
421
- function renderTestTable(testDetails) {
422
- if (window.testGrid) {
423
- try { window.testGrid.destroy(); } catch (e) { }
424
- }
425
- const tableDiv = document.getElementById("test-table");
426
- tableDiv.innerHTML = "";
427
-
428
- try {
429
- window.testGrid = new gridjs.Grid({
430
- columns: [
431
- { name: "ID", width: "22%" },
432
- { name: "Class", width: "12%" },
433
- { name: "Method", width: "14%" },
434
- { name: "Status", width: "11%" },
435
- { name: "Time (s)", width: "9%" },
436
- { name: "File", width: "22%" },
437
- { name: "Doc", width: "10%" }
438
- ],
439
- data: testDetails.map((t, idx) => [
440
- gridjs.html(`<div title="${t.id}">${t.id}</div>`),
441
- gridjs.html(`<div title="${t.class}">${t.class}</div>`),
442
- gridjs.html(`<div title="${t.method}">${t.method}</div>`),
443
- gridjs.html(`
444
- <span class="badge-status ${
445
- t.status === 'PASSED' ? 'badge-passed' :
446
- t.status === 'FAILED' ? 'badge-failed' :
447
- t.status === 'ERRORS' ? 'badge-errors' : 'badge-skipped'
448
- } status-cell">
449
- ${
450
- t.status === 'PASSED' ? '<i class="bi bi-check-circle-fill"></i>' :
451
- t.status === 'FAILED' ? '<i class="bi bi-x-circle-fill"></i>' :
452
- t.status === 'ERRORS' ? '<i class="bi bi-exclamation-triangle-fill"></i>' :
453
- '<i class="bi bi-skip-forward-fill"></i>'
454
- }
455
- ${t.status}
456
- </span>
457
- `),
458
- t.execution_time,
459
- gridjs.html(`<div title="${t.file_path}">${t.file_path}</div>`),
460
- gridjs.html(`<button class="doc-btn doc-cell" data-doc-idx="${idx}" type="button"><i class="bi bi-journal-text"></i> Ver</button>`)
461
- ]),
462
- search: {
463
- enabled: true,
464
- placeholder: 'Buscar solo por clase...'
465
- },
466
- pagination: { limit: 8, summary: false },
467
- sort: false,
468
- resizable: true,
469
- style: {
470
- th: { 'text-align': 'left', 'background': '#f1f5f9', 'font-size': '1em' }
471
- }
472
- }).render(tableDiv);
473
-
474
- // Delegated event for doc buttons (always works, even after pagination)
475
- tableDiv.addEventListener('click', function (e) {
476
- const btn = e.target.closest('.doc-btn');
477
- if (!btn) return;
478
- const idx = Number(btn.getAttribute('data-doc-idx'));
479
- const t = testDetails[idx];
480
- if (!t) return;
481
-
482
- let html = '';
483
- html += `<div class="orionis-modal-title"><i class="bi bi-journal-text"></i> Docstring</div>`;
484
- html += `<pre class="orionis-modal-pre">${t.doc_string ? String(t.doc_string).replace(/</g, "&lt;").replace(/>/g, "&gt;") : 'No docstring.'}</pre>`;
485
- if (t.status === 'FAILED' || t.status === 'ERRORS') {
486
- html += `<div class="orionis-modal-title mt-4"><i class="bi bi-bug-fill"></i> Traceback</div>`;
487
- html += `<pre class="orionis-modal-pre" style="background:#fee2e2;color:#991b1b">${t.traceback ? String(t.traceback).replace(/</g, "&lt;").replace(/>/g, "&gt;") : 'No traceback.'}</pre>`;
488
- }
489
-
490
- // Remove any existing modal
491
- const oldModal = document.querySelector('.orionis-modal');
492
- if (oldModal) oldModal.remove();
493
-
494
- // Create modal element
495
- const modal = document.createElement('div');
496
- modal.className = 'orionis-modal';
497
- modal.style.display = 'flex';
498
-
499
- modal.innerHTML = `
500
- <div class="orionis-modal-content">
501
- <button class="orionis-modal-close" aria-label="Cerrar" type="button">&times;</button>
502
- ${html}
503
- </div>
504
- `;
505
-
506
- // Close modal when clicking outside content
507
- modal.addEventListener('mousedown', function (e) {
508
- if (e.target === modal) modal.remove();
509
- });
510
-
511
- // Close modal when clicking close button
512
- modal.querySelector('.orionis-modal-close').onclick = () => modal.remove();
513
- document.body.appendChild(modal);
514
- });
515
- } catch (err) {
516
- tableDiv.innerHTML = `<div class="text-red-600 font-mono text-sm py-4">Ocurrió un error al mostrar la tabla.<br>${err.message}</div>`;
517
- }
518
- }
519
-
520
- document.getElementById("test-select").addEventListener("change", function () {
521
- const selectedIndex = this.value;
522
- if (!data[selectedIndex]) return;
523
- renderTestTable(data[selectedIndex].test_details);
524
- });
525
-
526
- document.addEventListener("DOMContentLoaded", function () {
527
- renderTestTable([]);
528
- });
529
-
530
- </script>
531
- </body>
532
-
533
- </html>