robotframework-testdoc 0.1.8__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of robotframework-testdoc might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-testdoc
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: A CLI Tool to generate a Test Documentation for your RobotFramework Test Scripts.
5
5
  Author-email: Marvin Klerx <marvinklerx20@gmail.com>
6
6
  License: MIT
@@ -15,6 +15,12 @@ Dynamic: license-file
15
15
 
16
16
  # Robot Framework TestDoc
17
17
 
18
+ ## Statistics
19
+
20
+ [![Release Pipeline](https://github.com/MarvKler/robotframework-testdoc/actions/workflows/release.yml/badge.svg)](https://github.com/MarvKler/robotframework-testdoc/actions/workflows/release.yml)
21
+ [![PyPI Downloads - Total](https://static.pepy.tech/badge/robotframework-testdoc)](https://pepy.tech/projects/robotframework-testdoc)
22
+ [![PyPI Downloads - Monthly](https://static.pepy.tech/badge/robotframework-testdoc/month)](https://pepy.tech/projects/robotframework-testdoc)
23
+
18
24
  ## Installation
19
25
 
20
26
  Install the tool using the following command:
@@ -98,6 +104,37 @@ For using this config file, just call the following command:
98
104
  testdoc -c path/to/config.toml tests/ TestDocumentation.html
99
105
  ```
100
106
 
107
+ ## HTML Template Selection
108
+
109
+ You can choose between multiple HTML template for the design of your test documentation.
110
+ These template can be configured via ``cli arguments`` or within a ``.toml configuration file`` with the parameter ``html_template (-ht / --html-template)``.
111
+
112
+ ### Default Design
113
+
114
+ - v2
115
+
116
+ ### Available HTML Templates
117
+
118
+ You can choose one of the following designs:
119
+ - v1
120
+ - v2
121
+
122
+ ### Version 1
123
+
124
+ #### Visit Tests
125
+
126
+ ![alt text](docs/html_v1_common.png)
127
+
128
+ ### Version 2
129
+
130
+ #### Visit Tests on Root Suite Level
131
+
132
+ ![alt text](docs/html_v2_root.png)
133
+
134
+ #### Visit Tests on Suite File Level
135
+
136
+ ![alt text](docs/html_v2_suitefile.png)
137
+
101
138
  ## Theme Selection / Color Configuration
102
139
 
103
140
  You can select between several themes (color configurations) for your HTML document to create!
@@ -0,0 +1,28 @@
1
+ robotframework_testdoc-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
2
+ testdoc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ testdoc/__main__.py,sha256=09c4nsw4Vnp1LaK0CnlofJQFbKmeFexOXyTBDY9NrFk,67
4
+ testdoc/cli.py,sha256=MqY7MAVoFwW-vv1tuJ7pZuzcfqa1aEAbyyQWaFJhWMc,5002
5
+ testdoc/default.toml,sha256=PK7O2gat8326ZYOXBC1mt6-5ceBhdbgs0BL1uo4XLjQ,87
6
+ testdoc/testdoc.py,sha256=cVJguXoFhkCM2nkUlZGB8m-6rBhKwthFFtdtz0T2l4Q,772
7
+ testdoc/helper/cliargs.py,sha256=nsgxXho8QAtcxF5B0OCriLuUEByGQwagDkvR7ISAGWA,2627
8
+ testdoc/helper/datetimeconverter.py,sha256=1IuJ_rZlKKut3pallS9WSdlQ00YNQX2Nhf2oYWt7QDc,159
9
+ testdoc/helper/logger.py,sha256=STPEEdMIGpK004xHDskj8zzW3knBWP05GllYajQMaSY,272
10
+ testdoc/helper/pathconverter.py,sha256=jVFJrBz7-DqHcGYzcwcLhpYGUOmA2xZSoJdsaPtGQJY,1656
11
+ testdoc/helper/toml_reader.py,sha256=JUpCdUQAwS-zImH0fU9leziM8Mc9CXAAHFUs6E0eQRA,323
12
+ testdoc/html/images/robotframework.svg,sha256=w1yNL6XtuHOCCwzjGX3pZQG7ZcJghzllvc7cQ9MKKbQ,1426
13
+ testdoc/html/templates/v1/jinja_template_01.html,sha256=H0CVKV3HljrdQeT_4hWJq3xNw6kc5vqiJtzCXoWBtdY,18389
14
+ testdoc/html/templates/v1/jinja_template_02.html,sha256=0CFAqCHQ035hsHgxZsirHgsdZO5-jdUH9SUQmy3EHkg,5152
15
+ testdoc/html/templates/v2/jinja_template_03.html,sha256=gpQj8Fso-7sdQmC6rL_ym29CVf5fQXL0l0FoT7unS7c,16136
16
+ testdoc/html/themes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ testdoc/html/themes/theme_config.py,sha256=3AFUSoddhAZswcEsshbvqcnmbLOFaBYUFy-lrfNBV3Q,1495
18
+ testdoc/html/themes/themes.py,sha256=6xlHW7O-XO9Z1B33_aRwzffkuWR65jM7CYXsZWUjdmY,1091
19
+ testdoc/html_rendering/render.py,sha256=Ij6Jtut7oUCM-z_FIz04A0showacIAIqbkArBg7I2u8,1969
20
+ testdoc/parser/testcaseparser.py,sha256=CXZRS1mvHx1O6G2FgqF8T_dWp1RcSJbeN7Le0ZInows,6405
21
+ testdoc/parser/testsuiteparser.py,sha256=K7nHa6Kf64kixlGrlsv7leIDl30ct01TE0jxgnXJxl8,2788
22
+ testdoc/parser/modifier/sourceprefixmodifier.py,sha256=Vy_keEKztF7UrjtWjmkU7usGR7E-xLvxJOWocPRu6KI,3950
23
+ testdoc/parser/modifier/suitefilemodifier.py,sha256=OuDuleQj4dRjUcu0AROEPZ-2vR3lWJfWmQVuoWLkXuY,4865
24
+ robotframework_testdoc-0.2.0.dist-info/METADATA,sha256=YNryX3iaQUZFD-dGBJMWsQhdyo6NgxK8vaJrSaHIexM,6090
25
+ robotframework_testdoc-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ robotframework_testdoc-0.2.0.dist-info/entry_points.txt,sha256=BUHy23mdlGCqYOWpsvRhSb1c0tPMzIwyTwr-sHI6xUs,45
27
+ robotframework_testdoc-0.2.0.dist-info/top_level.txt,sha256=p1axpYooAmdwwXQOzFsSXF3u_-88QFKCDxPf67siv3Y,8
28
+ robotframework_testdoc-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
testdoc/cli.py CHANGED
@@ -4,7 +4,8 @@ import os
4
4
  from .testdoc import TestDoc
5
5
  from .helper.cliargs import CommandLineArguments
6
6
 
7
- @click.command()
7
+ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
8
+ @click.command(context_settings=CONTEXT_SETTINGS)
8
9
  @click.option("-t","--title", required=False, help="Modify the title of the test documentation page")
9
10
  @click.option("-n","--name", required=False, help="Modify the name of the root suite element")
10
11
  @click.option("-d","--doc", required=False, help="Modify the documentation of the root suite element")
@@ -20,8 +21,10 @@ from .helper.cliargs import CommandLineArguments
20
21
  @click.option("--hide-source", is_flag=True, required=False, help="If given, test suite/ test case source is hidden")
21
22
  @click.option("--hide-keywords", is_flag=True, required=False, help="If given, keyword calls in test cases are hidden")
22
23
  @click.option("-S", "--style", required=False, help="Choose a predefined default style theme - 'default', 'robot', 'dark' or 'blue' ")
24
+ @click.option("-ht","--html-template", required=False, help="Select the HTML template - possible values: 'v1', 'v2'")
23
25
  @click.option("-c", "--configfile", required=False, help="Optional .toml configuration file (includes all cmd-args)")
24
26
  @click.option("-v", "--verbose", is_flag=True, required=False, help="More precise debugging into shell")
27
+ @click.version_option(package_name='robotframework-testdoc')
25
28
  @click.argument("PATH")
26
29
  @click.argument("OUTPUT")
27
30
  def main(
@@ -38,6 +41,7 @@ def main(
38
41
  hide_source,
39
42
  hide_keywords,
40
43
  style,
44
+ html_template,
41
45
  configfile,
42
46
  verbose,
43
47
  path,
@@ -81,6 +85,7 @@ def main(
81
85
  "hide_keywords": hide_keywords or None,
82
86
  "verbose_mode": verbose or None,
83
87
  "style": style or None,
88
+ "html_template": html_template or None,
84
89
  "config_file": configfile or None,
85
90
  }
86
91
  args.suite_file = path
testdoc/helper/cliargs.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from dataclasses import dataclass, field
2
2
  from typing import Any, List
3
- import tomli
3
+ from .toml_reader import TOMLReader
4
4
  import os
5
5
 
6
6
  @dataclass
@@ -21,6 +21,7 @@ class CommandLineArgumentsData:
21
21
  verbose_mode: bool = False
22
22
  suite_file: str = None
23
23
  style: str = None
24
+ html_template: str = "v2"
24
25
  output_file: str = None
25
26
  colors: dict = None
26
27
 
@@ -37,9 +38,7 @@ class CommandLineArguments:
37
38
  ### Load configuration file
38
39
  ###
39
40
  def load_from_config_file(self, file_path: str):
40
- with open(file_path, "rb") as f:
41
- config = tomli.load(f)
42
-
41
+ config = TOMLReader()._read_toml(file_path)
43
42
  _is_pyproject = self._is_pyproject_config(file_path)
44
43
  if _is_pyproject:
45
44
  self._handle_pyproject_config(config)
@@ -17,7 +17,7 @@ class PathConverter():
17
17
  # Convert path to suite file / directory
18
18
  suite_path = PathConverter().conv_generic_path(path=suite_path)
19
19
  if ".robot" in suite_path:
20
- msg = f"Suite File: '{str(suite_path).split("/")[-1]}'"
20
+ msg = f'Suite File: "{str(suite_path).split("/")[-1]}"'
21
21
  else:
22
22
  msg = f"Suite Directory: '{suite_path}'"
23
23
 
@@ -0,0 +1,11 @@
1
+ import tomli
2
+
3
+ class TOMLReader():
4
+
5
+ def _read_toml(self, file_path:str):
6
+ try:
7
+ with open(file_path, "rb") as f:
8
+ config = tomli.load(f)
9
+ return config
10
+ except Exception as e:
11
+ raise ImportError(f"Cannot read toml file in: {file_path} with error: \n{e}")
@@ -121,7 +121,9 @@
121
121
  <tr>
122
122
  <td style="width: 10%; font-weight: bold;">🏷 Tags:</td>
123
123
  <td style="text-align: left;">
124
- {% if test.tags %}
124
+ {% if test.tags and test.tags is string %}
125
+ {{ test.tags }}
126
+ {% else %}
125
127
  {{ test.tags | join(', ') }}
126
128
  {% endif %}
127
129
  </td>
@@ -132,7 +134,7 @@
132
134
  <td style="width: 10%; font-weight: bold; vertical-align: top;">🔑 Keywords:</td>
133
135
  <td style="text-align: left;">
134
136
  {% if test.keywords %}
135
- - {{ test.keywords | join('<br>- ') }}
137
+ <pre>- {{ test.keywords | join('\n- ') }}</pre>
136
138
  {% endif %}
137
139
  </td>
138
140
  </tr>
@@ -0,0 +1,116 @@
1
+ <!DOCTYPE html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Robot Framework - Test Documentation</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
9
+ <style>
10
+ html, body, .container-fluid {
11
+ background-color: {{ colors.background }};
12
+ color: {{ colors.text_color }};
13
+ font-size: 14px;
14
+ }
15
+ a {
16
+ color: {{ colors.text_color }};
17
+ }
18
+ a:hover {
19
+ color: {{ colors.robot_icon }};
20
+ }
21
+ .sidebar {
22
+ min-width: 250px;
23
+ max-width: 250px;
24
+ background-color: {{ colors.inner_color }};
25
+ border-right: 1px solid {{ colors.border_color }};
26
+ height: 100vh;
27
+ overflow-y: auto;
28
+ }
29
+ .content {
30
+ padding: 20px;
31
+ flex-grow: 1;
32
+ }
33
+ .suite-item {
34
+ padding: 8px 16px;
35
+ cursor: pointer;
36
+ }
37
+ .suite-item:hover {
38
+ background-color: {{ colors.button_hover_color }};
39
+ }
40
+ .accordion-item, .accordion-body {
41
+ background-color: {{ colors.inner_color }};
42
+ border-color: {{ colors.border_color }};
43
+ color: {{ colors.text_color }};
44
+ }
45
+ .accordion-button {
46
+ background-color: {{ colors.inner_color }};
47
+ color: {{ colors.text_color }};
48
+ }
49
+ .accordion-button:hover {
50
+ background-color: {{ colors.button_hover_color }} !important;
51
+ }
52
+ .accordion-button:not(.collapsed),
53
+ .accordion-button:focus,
54
+ .accordion-button:active {
55
+ background-color: {{ colors.button_active_color }} !important;
56
+ box-shadow: none !important;
57
+ color: {{ colors.text_color }};
58
+ }
59
+ .generated_at {
60
+ color: {{ colors.title_color }};
61
+ }
62
+ </style>
63
+ </head>
64
+ <body class="d-flex flex-column min-vh-100">
65
+ <div class="d-flex flex-grow-1">
66
+ <div class="sidebar">
67
+ <h5 class="text-center mt-3">📁 Suites</h5>
68
+ <ul class="list-unstyled">
69
+ {% for suite in suites %}
70
+ <li class="suite-item" onclick="showSuite('{{ loop.index0 }}')">{{ suite.name }}</li>
71
+ {% endfor %}
72
+ </ul>
73
+ </div>
74
+ <div class="content">
75
+ {% for suite in suites %}
76
+ <div id="suite-{{ loop.index0 }}" class="suite-view" style="display: {% if loop.first %}block{% else %}none{% endif %};">
77
+ <h4>{{ suite.name }}</h4>
78
+ {% if suite.doc %}<p><strong>📝 Docs:</strong> {{ suite.doc }}</p>{% endif %}
79
+ {% if suite.source %}<p><strong>🔗 Source:</strong> <a href="{{ suite.source }}" target="_blank">{{ suite.source }}</a></p>{% endif %}
80
+ <p><strong>📊 Number of Tests:</strong> {{ suite.num_tests }}</p>
81
+
82
+ {% for test in suite.tests %}
83
+ <div class="accordion mb-3">
84
+ <div class="accordion-item">
85
+ <h2 class="accordion-header">
86
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#test-{{ loop.index0 }}-{{ loop.index }}">
87
+ 🔍 {{ test.name }}
88
+ </button>
89
+ </h2>
90
+ <div id="test-{{ loop.index0 }}-{{ loop.index }}" class="accordion-collapse collapse">
91
+ <div class="accordion-body">
92
+ {% if test.doc %}<p><strong>📝 Docs:</strong><br>{{ test.doc }}</p>{% endif %}
93
+ {% if test.source %}<p><strong>🔗 Source:</strong> <a href="{{ test.source }}" target="_blank">{{ test.source }}</a></p>{% endif %}
94
+ {% if test.tags %}<p><strong>🏷 Tags:</strong> {{ test.tags | join(', ') }}</p>{% endif %}
95
+ {% if test.keywords %}<p><strong>🔑 Keywords:</strong><br>- {{ test.keywords | join('<br>- ') }}</p>{% endif %}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ {% endfor %}
101
+ </div>
102
+ {% endfor %}
103
+ </div>
104
+ </div>
105
+ <p class="text-center generated_at py-1 border-top">
106
+ Generated at: {{ generated_at }}<br>robotframework-testdoc by Marvin Klerx
107
+ </p>
108
+ <script>
109
+ function showSuite(index) {
110
+ const views = document.querySelectorAll('.suite-view');
111
+ views.forEach(view => view.style.display = 'none');
112
+ document.getElementById('suite-' + index).style.display = 'block';
113
+ }
114
+ </script>
115
+ </body>
116
+ </html>
@@ -0,0 +1,316 @@
1
+ <!-- Jinja Macro for recursive suite tree (sidebar) -->
2
+ {% macro render_suite_tree(suite, parent_id='root') %}
3
+ <li>
4
+ <a href="#" class="suite-link" data-suite-id="{{ suite.id }}">{{ suite.filename }}</a>
5
+ {% if suite.sub_suites %}
6
+ <ul>
7
+ {% for sub_suite in suite.sub_suites %}
8
+ {{ render_suite_tree(sub_suite, sub_suite.id) }}
9
+ {% endfor %}
10
+ </ul>
11
+ {% endif %}
12
+ </li>
13
+ {% endmacro %}
14
+
15
+ <!-- Jinja Macro for test case cards (main area) -->
16
+ {% macro render_test_cases(suite) %}
17
+ {% if suite.tests %}
18
+ <div class="suite-header mb-3 p-3" data-suite-id="{{ suite.id }}" style="border: 1px solid {{ colors.border_color }}; border-radius: .375rem; background: {{ colors.inner_color }};">
19
+ <div><strong>Generic Suite Details:</strong></div>
20
+ <table style="width: 100%; border-collapse: collapse;">
21
+ <tr>
22
+ <td style="width: 10%; font-weight: bold;">📁 Suite Name:</td>
23
+ <td style="text-align: left;">{{ suite.name }}</td>
24
+ </tr>
25
+ {% if not suite.is_folder %}
26
+ <tr>
27
+ <td style="width: 10%; font-weight: bold;">📄 File Name:</td>
28
+ <td style="text-align: left;">{{ suite.filename }}</td>
29
+ </tr>
30
+ {% endif %}
31
+ <tr>
32
+ <td style="width: 10%; font-weight: bold;">📊 Number of Tests:</td>
33
+ <td style="text-align: left;">{{ suite.num_tests }}</td>
34
+ </tr>
35
+ {% if suite.doc is not none %}
36
+ <tr>
37
+ <td style="width: 10%; font-weight: bold; vertical-align: top;">📝 Docs:</td>
38
+ <td style="text-align: left;">{{ suite.doc }}</td>
39
+ </tr>
40
+ {% endif %}
41
+ {% if suite.metadata is not none %}
42
+ <tr>
43
+ <td style="width: 10%; font-weight: bold; vertical-align: top;">⚙️ Metadata:</td>
44
+ <td style="text-align: left;">{{ suite.metadata }}</td>
45
+ </tr>
46
+ {% endif %}
47
+ </table>
48
+ </div>
49
+ {% for test in suite.tests %}
50
+ <div class="card mb-3 test-card" data-suite-id="{{ suite.id }}">
51
+ <div class="card-header" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{ suite.id }}-{{ loop.index }}" aria-expanded="false" aria-controls="collapse-{{ suite.id }}-{{ loop.index }}">
52
+ <svg class="svg-icon" width="20" height="20" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
53
+ <title>Robot Framework</title>
54
+ <path d="M4.9565 10.2246c0-1.8766 1.5257-3.4023 3.4-3.4023 1.8766 0 3.4024 1.5257 3.4024 3.4023 0 .6838-.5526 1.2364-1.2341 1.2364-.6818 0-1.2344-.5526-1.2344-1.2364 0-.513-.4185-.9296-.9338-.9296-.5129 0-.9317.4165-.9317.9296 0 .6838-.5523 1.2364-1.234 1.2364-.6818 0-1.2344-.5526-1.2344-1.2364m14.0868 5.717c0 .6842-.5524 1.2363-1.2341 1.2363H6.3575c-.6818 0-1.2344-.552-1.2344-1.2363 0-.6837.5526-1.2363 1.2344-1.2363h11.4517c.6817 0 1.234 5526 1.234 1.2363m-5.351-5.0244c-.3814-.5657-.2323-1.3328.3334-1.7143l2.8628-1.9334c.5613-.3902 1.3329-.2324 1.7144.3289.3815.5654.2323 1.3329-.3334 1.7144l-2.8628 1.9333c-.5442.3831-1.3348.2379-1.7144-.3289zm7.8393 7.6018a.8815.8815 0 0 1-.258.6227l-2.1277 2.1277a.8822.8822 0 0 1-.623.258H5.4772a.8822.8822 0 0 1-.623-.258l-2.1277-2.1277a.8815.8815 0 0 1-.258-.6227V5.4818a.8797.8797 0 0 1 .258-.6228l2.1277-2.1282a.8816.8816 0 0 1 .623-.2578h13.0456a.8816.8816 0 0 1 .623.2578l2.1277 2.1282a.8797.8797 0 0 1 .258.6228V18.519zm1.811-15.0835L20.5644.6577A2.2454 2.2454 0 0 0 18.9775 0H5.0207A2.2445 2.2445 0 0 0 3.433.658L.657 3.4359A2.2449 2.2449 0 0 0 0 5.0228v13.9547c0 .5953.2366 1.1667.6575 1.5872l2.778 2.7779c.421.421.9918.6573 1.5871.6573h13.9548a2.2448 2.2448 0 0 0 1.5872-.6573l2.7779-2.7779A2.2436 2.2436 0 0 0 24 18.9775V5.023a2.2451 2.2451 0 0 0-.6575-1.5875z"/>
55
+ </svg>
56
+ &nbsp;Test Case: <strong>{{ test.name }}</strong>
57
+ </div>
58
+ <div id="collapse-{{ suite.id }}-{{ loop.index }}" class="collapse">
59
+ <div class="card-body">
60
+ <table style="width: 100%; border-collapse: collapse;">
61
+ {% set has_info = test.doc is not none or test.source is not none or test.tags is not none or test.keywords is not none %}
62
+ {% if test.doc is not none %}
63
+ <tr>
64
+ <td style="width: 10%; font-weight: bold; vertical-align: top;">📝 Docs:</td>
65
+ <td style="text-align: left;">{{ test.doc }}</td>
66
+ </tr>
67
+ {% endif %}
68
+ {% if test.source is not none %}
69
+ <tr>
70
+ <td style="width: 10%; font-weight: bold; vertical-align: top;">🔗 Source:</td>
71
+ <td style="text-align: left;">
72
+ <a href="{{ test.source }}" target="_blank">{{ test.source }}</a>
73
+ </td>
74
+ </tr>
75
+ {% endif %}
76
+ {% if test.tags is not none %}
77
+ <tr>
78
+ <td style="width: 10%; font-weight: bold;">🏷 Tags:</td>
79
+ <td style="text-align: left;">
80
+ {% if test.tags and test.tags is string %}
81
+ {{ test.tags }}
82
+ {% else %}
83
+ {{ test.tags | join(', ') }}
84
+ {% endif %}
85
+ </td>
86
+ </tr>
87
+ {% endif %}
88
+ {% if test.keywords is not none %}
89
+ <tr>
90
+ <td style="width: 10%; font-weight: bold; vertical-align: top;">🔑 Keywords:</td>
91
+ <td style="text-align: left;">
92
+ {% if test.keywords %}
93
+ <pre>- {{ test.keywords | join('\n- ') }}</pre>
94
+ {% endif %}
95
+ </td>
96
+ </tr>
97
+ {% endif %}
98
+ {% if not has_info %}
99
+ <tr>
100
+ <td class="info-msg" style="width: 10%; vertical-align: top; text-align: center;">
101
+ No Details Available / Enabled !
102
+ </td>
103
+ </tr>
104
+ {% endif %}
105
+ </table>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ {% endfor %}
110
+ {% endif %}
111
+ {% if suite.sub_suites %}
112
+ <div class="suite-header mb-3 p-3" data-suite-id="{{ suite.id }}" style="border: 1px solid {{ colors.border_color }}; border-radius: .375rem; background: {{ colors.inner_color }};">
113
+ <div><strong>Generic Parent Suite Details:</strong></div>
114
+ <table style="width: 100%; border-collapse: collapse;">
115
+ <tr>
116
+ <td style="width: 10%; font-weight: bold;">📁 Suite Name:</td>
117
+ <td style="text-align: left;">{{ suite.name }}</td>
118
+ </tr>
119
+ <tr>
120
+ <td style="width: 10%; font-weight: bold;">📊 Number of Tests:</td>
121
+ <td style="text-align: left;">{{ suite.total_tests }}</td>
122
+ </tr>
123
+ {% if suite.doc is not none %}
124
+ <tr>
125
+ <td style="width: 10%; font-weight: bold; vertical-align: top;">📝 Docs:</td>
126
+ <td style="text-align: left;">{{ suite.doc }}</td>
127
+ </tr>
128
+ {% endif %}
129
+ {% if suite.metadata is not none %}
130
+ <tr>
131
+ <td style="width: 10%; font-weight: bold; vertical-align: top;">⚙️ Metadata:</td>
132
+ <td style="text-align: left;">{{ suite.metadata }}</td>
133
+ </tr>
134
+ {% endif %}
135
+ </table>
136
+ </div>
137
+ {% for sub_suite in suite.sub_suites %}
138
+ {{ render_test_cases(sub_suite) }}
139
+ {% endfor %}
140
+ {% endif %}
141
+ {% endmacro %}
142
+
143
+ <!DOCTYPE html>
144
+ <html lang="de">
145
+ <head>
146
+ <meta charset="UTF-8">
147
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
148
+ <title>Robot Framework - Test Documentation</title>
149
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
150
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
151
+ <style>
152
+ html, body, .container-fluid {
153
+ background-color: {{ colors.background }};
154
+ color: {{ colors.text_color }};
155
+ font-size: 14px;
156
+ }
157
+ a { color: {{ colors.text_color }}; }
158
+ a:hover { color: {{ colors.robot_icon }}; }
159
+ .svg-icon { fill: {{ colors.robot_icon }}; }
160
+ h1 { color: {{ colors.title_color }}; font-size: 30px; }
161
+ .info-msg { color: {{ colors.title_color }}; }
162
+ .generated_at { color: {{ colors.title_color }}; }
163
+ .sidebar {
164
+ background: {{ colors.inner_color }};
165
+ border-right: 1px solid {{ colors.border_color }};
166
+ min-width: 260px;
167
+ max-width: 320px;
168
+ height: 100vh;
169
+ overflow-y: auto;
170
+ padding: 20px 10px 10px 10px;
171
+ }
172
+ .suite-tree ul {
173
+ list-style-type: none;
174
+ padding-left: 18px;
175
+ }
176
+ .suite-tree li {
177
+ margin-bottom: 4px;
178
+ }
179
+ .suite-link {
180
+ color: {{ colors.text_color }};
181
+ text-decoration: none;
182
+ cursor: pointer;
183
+ }
184
+ .suite-link.active {
185
+ font-weight: bold;
186
+ color: {{ colors.robot_icon }};
187
+ }
188
+ .main-content {
189
+ padding: 30px 20px 20px 20px;
190
+ background: {{ colors.background }};
191
+ min-height: 100vh;
192
+ }
193
+ .test-card {
194
+ border: 1px solid {{ colors.border_color }};
195
+ background: {{ colors.inner_color }};
196
+ }
197
+ .test-card .card-header {
198
+ background: {{ colors.button_active_color }};
199
+ color: {{ colors.text_color }};
200
+ font-size: 16px;
201
+ }
202
+ .test-card .card-body {
203
+ font-size: 14px;
204
+ }
205
+ table {
206
+ font-size: 13px;
207
+ color: {{ colors.text_color }};
208
+ }
209
+ td {
210
+ padding: 1px 1px;
211
+ }
212
+ @media (max-width: 900px) {
213
+ .sidebar { min-width: 120px; max-width: 180px; }
214
+ .main-content { padding: 10px; }
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <div class="container-fluid">
220
+ <h1 class="text-center mt-4">{{ title }}</h1>
221
+ <hr style="border-top: 1px solid {{ colors.border_color }}; margin: 0 0 20px 0;">
222
+ <div class="row" style="min-height: 80vh;">
223
+ <nav class="col-md-3 sidebar suite-tree">
224
+ <h5 style="color: {{ colors.title_color }};">Directory Structure: </h5>
225
+ <ul>
226
+ {% for suite in suites %}
227
+ {{ render_suite_tree(suite) }}
228
+ {% endfor %}
229
+ </ul>
230
+ </nav>
231
+ <main class="col-md-9 main-content">
232
+ <div id="test-cases-list">
233
+ {% for suite in suites %}
234
+ {{ render_test_cases(suite) }}
235
+ {% endfor %}
236
+ </div>
237
+ </main>
238
+ </div>
239
+ <p class="text-center generated_at py-1 border-top" style="color: {{ colors.text_color }};">
240
+ This test case documentation was generated at: {{ generated_at }}
241
+ <br>
242
+ <a href="mailto:{{ contact_mail }}?subject=Support Request - robotframework-testdoc" target="_blank">Contact the Developer</a> |
243
+ <a href="https://github.com/MarvKler/robotframework-testdoc?tab=readme-ov-file#robot-framework-testdoc" target="_blank">Visit Project on GitHub</a> |
244
+ <a href="https://github.com/MarvKler/robotframework-testdoc/issues/new" target="_blank">Report Issue</a>
245
+ </p>
246
+ </div>
247
+ <script>
248
+ // Assign unique IDs to suites for filtering (should be done in Python, but fallback here)
249
+ document.querySelectorAll('.suite-link').forEach(function(link, idx) {
250
+ if (!link.dataset.suiteId) link.dataset.suiteId = 'suite-' + idx;
251
+ });
252
+ document.querySelectorAll('.test-card').forEach(function(card, idx) {
253
+ if (!card.dataset.suiteId) card.dataset.suiteId = 'suite-' + idx;
254
+ });
255
+
256
+ // Build suite hierarchy for filtering
257
+ function buildSuiteTree() {
258
+ const tree = {};
259
+ document.querySelectorAll('.suite-link').forEach(function(link) {
260
+ const suiteId = link.dataset.suiteId;
261
+ const parentLi = link.closest('li');
262
+ const subLinks = parentLi ? parentLi.querySelectorAll(':scope > ul > li > .suite-link') : [];
263
+ tree[suiteId] = Array.from(subLinks).map(l => l.dataset.suiteId);
264
+ });
265
+ return tree;
266
+ }
267
+
268
+ // Get all descendant suite ids (recursive)
269
+ function getAllDescendantSuites(tree, suiteId) {
270
+ let ids = [suiteId];
271
+ if (tree[suiteId]) {
272
+ tree[suiteId].forEach(function(childId) {
273
+ ids = ids.concat(getAllDescendantSuites(tree, childId));
274
+ });
275
+ }
276
+ return ids;
277
+ }
278
+
279
+ const suiteTree = buildSuiteTree();
280
+
281
+ // Filtering logic: show test cases for selected suite and all its sub-suites
282
+ document.querySelectorAll('.suite-link').forEach(function(link) {
283
+ link.addEventListener('click', function(e) {
284
+ e.preventDefault();
285
+ var suiteId = this.dataset.suiteId;
286
+ document.querySelectorAll('.suite-link').forEach(function(l) { l.classList.remove('active'); });
287
+ this.classList.add('active');
288
+ // Get all descendant suite ids
289
+ var ids = getAllDescendantSuites(suiteTree, suiteId);
290
+
291
+ // Hide all test cards and suite headers
292
+ document.querySelectorAll('.test-card').forEach(function(card) {
293
+ card.style.display = 'none';
294
+ });
295
+ document.querySelectorAll('.suite-header').forEach(function(header) {
296
+ header.style.display = 'none';
297
+ });
298
+
299
+ // Show only test cards and suite headers for this suite and its descendants
300
+ ids.forEach(function(id) {
301
+ document.querySelectorAll('.test-card[data-suite-id="' + id + '"]').forEach(function(card) {
302
+ card.style.display = '';
303
+ });
304
+ document.querySelectorAll('.suite-header[data-suite-id="' + id + '"]').forEach(function(header) {
305
+ header.style.display = '';
306
+ });
307
+ });
308
+ });
309
+ });
310
+
311
+ // Show all test cases and suite headers by default
312
+ document.querySelectorAll('.test-card').forEach(function(card) { card.style.display = ''; });
313
+ document.querySelectorAll('.suite-header').forEach(function(header) { header.style.display = ''; });
314
+ </script>
315
+ </body>
316
+ </html>
@@ -8,23 +8,36 @@ from ..helper.logger import Logger
8
8
 
9
9
  class TestDocHtmlRendering():
10
10
 
11
- TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "html", "templates")
12
-
13
11
  def __init__(self):
14
12
  self.args = CommandLineArguments().data
13
+ self._html_templ_selection()
14
+
15
+ def _html_templ_selection(self):
16
+ """ Check which HTML template should selected - custom specific configuration """
17
+ if self.args.html_template == "v1":
18
+ self.HTML_TEMPLATE_VERSION = self.args.html_template
19
+ self.HTML_TEMPLATE_NAME = "jinja_template_01.html"
20
+ self.TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "html", "templates", self.HTML_TEMPLATE_VERSION)
21
+ elif self.args.html_template == "v2":
22
+ self.HTML_TEMPLATE_VERSION = self.args.html_template
23
+ self.HTML_TEMPLATE_NAME = "jinja_template_03.html"
24
+ self.TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "html", "templates", self.HTML_TEMPLATE_VERSION)
25
+ else:
26
+ raise ValueError(f"CLI Argument 'html_template' got value '{self.args.html_template}' - value not known!")
15
27
 
16
28
  def render_testdoc(self,
17
29
  suites,
18
30
  output_file
19
31
  ):
20
32
  env = Environment(loader=FileSystemLoader(self.TEMPLATE_DIR))
21
- template = env.get_template("jinja_template_01.html")
33
+ template = env.get_template(self.HTML_TEMPLATE_NAME)
22
34
 
23
35
  rendered_html = template.render(
24
36
  suites=suites,
25
37
  generated_at=DateTimeConverter().get_generated_datetime(),
26
38
  title=self.args.title,
27
- colors=ThemeConfig().theme()
39
+ colors=ThemeConfig().theme(),
40
+ contact_mail = "marvinklerx20@gmail.com"
28
41
  )
29
42
  with open(output_file, "w", encoding="utf-8") as f:
30
43
  f.write(rendered_html)
@@ -1,6 +1,8 @@
1
1
  from robot.api import TestSuite
2
+ from robot.running.model import Keyword, Body
2
3
  from robot.errors import DataError
3
4
  from ..helper.cliargs import CommandLineArguments
5
+ import textwrap
4
6
 
5
7
  class TestCaseParser():
6
8
 
@@ -19,7 +21,7 @@ class TestCaseParser():
19
21
  if line.strip()) if test.doc else "No Test Case Documentation Available",
20
22
  "tags": test.tags if test.tags else "No Tags Configured",
21
23
  "source": str(test.source),
22
- "keywords": [kw.name for kw in test.body if hasattr(kw, 'name')] or "No Keyword Calls in Test"
24
+ "keywords": self._keyword_parser(test.body)
23
25
  }
24
26
  suite_info["tests"].append(test_info)
25
27
  return suite_info
@@ -33,4 +35,133 @@ class TestCaseParser():
33
35
  suite.configure(exclude_tags=self.args.exclude)
34
36
  return suite
35
37
  except DataError as e:
36
- raise DataError(e.message)
38
+ raise DataError(e.message)
39
+
40
+ def _keyword_parser(self, test_body: Body):
41
+ """ Parse keywords and their child-items """
42
+ _keyword_object = []
43
+ for kw in test_body:
44
+ _keyword_object.extend(self._handle_keyword_types(kw))
45
+
46
+ _keyword_object = self._kw_post_processing(_keyword_object)
47
+
48
+ # Fallback in case of no keywords
49
+ if len(_keyword_object) == 0:
50
+ return "No Keyword Calls in Test"
51
+ return _keyword_object
52
+
53
+ def _handle_keyword_types(self, kw: Keyword, indent: int = 0):
54
+ """ Handle different keyword types """
55
+ result = []
56
+ kw_type = getattr(kw, 'type', None)
57
+
58
+ _sd = " " # classic rfw delimiter with 4 spaces
59
+ _indent = _sd * indent
60
+
61
+ # Classic keyword
62
+ if kw_type == "KEYWORD" and getattr(kw, 'name', None):
63
+ args = _sd.join(kw.args) if getattr(kw, 'args', None) else ""
64
+ entry = _indent + kw.name
65
+ if args:
66
+ entry += _sd + args
67
+ wrapped = textwrap.wrap(entry, width=150, subsequent_indent=_indent + "..." + _sd)
68
+ result.extend(wrapped)
69
+
70
+ # VAR syntax
71
+ elif kw_type == "VAR" and getattr(kw, 'name', None):
72
+ value = _sd.join(kw.value) if getattr(kw, 'value', None) else ""
73
+ result.append(f"{_indent}VAR {kw.name} = {value}")
74
+
75
+ # IF/ELSE/ELSE IF
76
+ elif kw_type == "IF/ELSE ROOT":
77
+ for branch in getattr(kw, 'body', []):
78
+ branch_type = getattr(branch, 'type', None)
79
+ if branch_type == "IF":
80
+ header = f"{_indent}IF{_sd}{getattr(branch, 'condition', '')}".rstrip()
81
+ elif branch_type == "ELSE IF":
82
+ header = f"{_indent}ELSE IF{_sd}{getattr(branch, 'condition', '')}".rstrip()
83
+ elif branch_type == "ELSE":
84
+ header = f"{_indent}ELSE"
85
+ else:
86
+ header = f"{_indent}{branch_type or ''}"
87
+ if header:
88
+ result.append(header)
89
+ for subkw in getattr(branch, 'body', []):
90
+ result.extend(self._handle_keyword_types(subkw, indent=indent+1))
91
+ result.append(f"{_indent}END")
92
+
93
+ # FOR loop
94
+ elif kw_type == "FOR":
95
+ header = f"{_indent}FOR"
96
+ if hasattr(kw, 'assign') and kw.assign:
97
+ header += f" {' '.join(kw.assign)}"
98
+ if hasattr(kw, 'flavor') and kw.flavor:
99
+ header += f" {kw.flavor}"
100
+ if hasattr(kw, 'values') and kw.values:
101
+ header += f" IN {' '.join(kw.values)}"
102
+ result.append(header)
103
+ if hasattr(kw, 'body'):
104
+ for subkw in kw.body:
105
+ result.extend(self._handle_keyword_types(subkw, indent=indent+1))
106
+ result.append(f"{_indent}END")
107
+
108
+ # WHILE loop
109
+ elif kw_type == "WHILE":
110
+ header = f"{_indent}WHILE"
111
+ if hasattr(kw, 'condition') and kw.condition:
112
+ header += f" {kw.condition}"
113
+ result.append(header)
114
+ if hasattr(kw, 'body'):
115
+ for subkw in kw.body:
116
+ result.extend(self._handle_keyword_types(subkw, indent=indent+1))
117
+ result.append(f"{_indent}END")
118
+
119
+ # TRY/EXCEPT/FINALLY
120
+ elif kw_type in ("TRY", "EXCEPT", "FINALLY"):
121
+ header = f"{_indent}{kw_type}"
122
+ if hasattr(kw, 'patterns') and kw.patterns:
123
+ header += f" {' '.join(kw.patterns)}"
124
+ if hasattr(kw, 'condition') and kw.condition:
125
+ header += f" {kw.condition}"
126
+ result.append(header)
127
+ if hasattr(kw, 'body'):
128
+ for subkw in kw.body:
129
+ result.extend(self._handle_keyword_types(subkw, indent=indent+1))
130
+ if kw_type in ("EXCEPT", "FINALLY"):
131
+ result.append(f"{_indent}END")
132
+
133
+ # BREAK, CONTINUE, RETURN, ERROR
134
+ elif kw_type in ("BREAK", "CONTINUE", "RETURN", "ERROR"):
135
+ entry = f"{_indent}{kw_type}"
136
+ if hasattr(kw, 'values') and kw.values:
137
+ entry += f" {' '.join(kw.values)}"
138
+ result.append(entry)
139
+
140
+ # Other types
141
+ elif kw_type in ("COMMENT", "EMPTY"):
142
+ pass
143
+
144
+ # Unknown types
145
+ elif hasattr(kw, 'body'):
146
+ for subkw in kw.body:
147
+ result.extend(self._handle_keyword_types(subkw))
148
+
149
+ return result
150
+
151
+ def _kw_post_processing(self, kw: list):
152
+ """ Post-processing of generated keyword list to handle special cases """
153
+ # TRY/EXCEPT/FINALLY
154
+ # post-process list for specific handling
155
+ for i in range(len(kw) - 1):
156
+ _cur = str(kw[i]).replace(" ", "")
157
+ _next = str(kw[i + 1]).replace(" ", "")
158
+ if _cur == "END" and _next == "FINALLY":
159
+ kw.pop(i)
160
+ break
161
+ return kw
162
+
163
+
164
+
165
+
166
+
167
+
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from pathlib import Path
2
3
 
3
4
  from robot.api import SuiteVisitor, TestSuite
4
5
  from .testcaseparser import TestCaseParser
@@ -6,6 +7,7 @@ from .modifier.suitefilemodifier import SuiteFileModifier
6
7
 
7
8
  class RobotSuiteParser(SuiteVisitor):
8
9
  def __init__(self):
10
+ self.suite_counter = 0
9
11
  self.suites = []
10
12
  self.tests = []
11
13
 
@@ -16,6 +18,8 @@ class RobotSuiteParser(SuiteVisitor):
16
18
 
17
19
  # Test Suite Parser
18
20
  suite_info = {
21
+ "id": str(suite.longname).lower().replace(".", "_").replace(" ", "_"),
22
+ "filename": str(Path(suite.source).name),
19
23
  "name": suite.name,
20
24
  "doc": "<br>".join(line.replace("\\n","") for line in suite.doc.splitlines() if line.strip()) if suite.doc else None,
21
25
  "is_folder": self._is_directory(suite),
@@ -1,25 +0,0 @@
1
- robotframework_testdoc-0.1.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
2
- testdoc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- testdoc/__main__.py,sha256=09c4nsw4Vnp1LaK0CnlofJQFbKmeFexOXyTBDY9NrFk,67
4
- testdoc/cli.py,sha256=bh_Fh78v5K8ajmSHazI6HGEJ3-pJyZOvcGm1SznU0jA,4659
5
- testdoc/default.toml,sha256=PK7O2gat8326ZYOXBC1mt6-5ceBhdbgs0BL1uo4XLjQ,87
6
- testdoc/testdoc.py,sha256=cVJguXoFhkCM2nkUlZGB8m-6rBhKwthFFtdtz0T2l4Q,772
7
- testdoc/helper/cliargs.py,sha256=EtGqiUDKeMBkGZUSjtyFrPBjeOt5BnHazB5MlvfW8uk,2599
8
- testdoc/helper/datetimeconverter.py,sha256=1IuJ_rZlKKut3pallS9WSdlQ00YNQX2Nhf2oYWt7QDc,159
9
- testdoc/helper/logger.py,sha256=STPEEdMIGpK004xHDskj8zzW3knBWP05GllYajQMaSY,272
10
- testdoc/helper/pathconverter.py,sha256=Il4SX8EdpKPjOiZt97zC4TBLxp2tKUhfyyS2J1qWpyg,1656
11
- testdoc/html/images/robotframework.svg,sha256=w1yNL6XtuHOCCwzjGX3pZQG7ZcJghzllvc7cQ9MKKbQ,1426
12
- testdoc/html/templates/jinja_template_01.html,sha256=gw2hEx2P7zLsQP5-4fA2hErWJuhkUTZkEakXxRzWKtU,18213
13
- testdoc/html/themes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- testdoc/html/themes/theme_config.py,sha256=3AFUSoddhAZswcEsshbvqcnmbLOFaBYUFy-lrfNBV3Q,1495
15
- testdoc/html/themes/themes.py,sha256=6xlHW7O-XO9Z1B33_aRwzffkuWR65jM7CYXsZWUjdmY,1091
16
- testdoc/html_rendering/render.py,sha256=BzPGyePwjCFZAXRHDF-ccM2xRKY-6Cnym1-uDb9i1AQ,1083
17
- testdoc/parser/testcaseparser.py,sha256=tCRj_OH6DkdB_2Np2kvRVSpTHWdFh46IIip2AiFb0HA,1382
18
- testdoc/parser/testsuiteparser.py,sha256=eqFMaEa2aXYCkoAl8wLmA7NsPOx8oxY8KfTKVxyGIPU,2595
19
- testdoc/parser/modifier/sourceprefixmodifier.py,sha256=Vy_keEKztF7UrjtWjmkU7usGR7E-xLvxJOWocPRu6KI,3950
20
- testdoc/parser/modifier/suitefilemodifier.py,sha256=OuDuleQj4dRjUcu0AROEPZ-2vR3lWJfWmQVuoWLkXuY,4865
21
- robotframework_testdoc-0.1.8.dist-info/METADATA,sha256=ktSWzaMXgU0CRYK_M57Gqh--dNOOyP1k47ZwAPWCVVU,4971
22
- robotframework_testdoc-0.1.8.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
23
- robotframework_testdoc-0.1.8.dist-info/entry_points.txt,sha256=BUHy23mdlGCqYOWpsvRhSb1c0tPMzIwyTwr-sHI6xUs,45
24
- robotframework_testdoc-0.1.8.dist-info/top_level.txt,sha256=p1axpYooAmdwwXQOzFsSXF3u_-88QFKCDxPf67siv3Y,8
25
- robotframework_testdoc-0.1.8.dist-info/RECORD,,