layoutlens 1.0.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.
- layoutlens/__init__.py +13 -0
- layoutlens/cli.py +311 -0
- layoutlens/config.py +400 -0
- layoutlens/core.py +570 -0
- layoutlens/test_runner.py +658 -0
- layoutlens-1.0.0.dist-info/METADATA +347 -0
- layoutlens-1.0.0.dist-info/RECORD +20 -0
- layoutlens-1.0.0.dist-info/WHEEL +5 -0
- layoutlens-1.0.0.dist-info/entry_points.txt +2 -0
- layoutlens-1.0.0.dist-info/licenses/LICENSE +21 -0
- layoutlens-1.0.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +3 -0
- scripts/benchmark/__init__.py +6 -0
- scripts/benchmark/benchmark_generator.py +414 -0
- scripts/benchmark/template_engine.py +891 -0
- scripts/testing/__init__.py +13 -0
- scripts/testing/ground_truth_evaluator.py +567 -0
- scripts/testing/page_tester.py +593 -0
- scripts/testing/query_generator.py +604 -0
- scripts/testing/screenshot_manager.py +534 -0
layoutlens/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""LayoutLens: AI-Enabled UI Test System
|
|
2
|
+
|
|
3
|
+
A comprehensive framework for natural language UI testing using AI vision models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .core import LayoutLens, TestSuite, TestCase
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .test_runner import TestRunner
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
__author__ = "LayoutLens Team"
|
|
12
|
+
|
|
13
|
+
__all__ = ["LayoutLens", "TestSuite", "TestCase", "Config", "TestRunner"]
|
layoutlens/cli.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Command-line interface for LayoutLens framework.
|
|
2
|
+
|
|
3
|
+
This module provides a comprehensive CLI for the LayoutLens UI testing system.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from .config import Config, create_default_config
|
|
13
|
+
from .core import LayoutLens
|
|
14
|
+
from .test_runner import TestRunner
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_test(args) -> None:
|
|
18
|
+
"""Execute test command."""
|
|
19
|
+
# Initialize LayoutLens
|
|
20
|
+
config_path = args.config if args.config else None
|
|
21
|
+
tester = LayoutLens(config=config_path, api_key=args.api_key, output_dir=args.output)
|
|
22
|
+
|
|
23
|
+
if args.page:
|
|
24
|
+
# Test single page
|
|
25
|
+
queries = args.queries.split(',') if args.queries else None
|
|
26
|
+
viewports = args.viewports.split(',') if args.viewports else None
|
|
27
|
+
|
|
28
|
+
print(f"Testing page: {args.page}")
|
|
29
|
+
result = tester.test_page(
|
|
30
|
+
html_path=args.page,
|
|
31
|
+
queries=queries,
|
|
32
|
+
viewports=viewports,
|
|
33
|
+
auto_generate_queries=not args.no_auto_queries
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if result:
|
|
37
|
+
print(f"Result: {result.passed_tests}/{result.total_tests} tests passed ({result.success_rate:.2%})")
|
|
38
|
+
else:
|
|
39
|
+
print("Test execution failed")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
elif args.suite:
|
|
43
|
+
# Test suite
|
|
44
|
+
runner = TestRunner(tester.config)
|
|
45
|
+
session = runner.run_test_suite(
|
|
46
|
+
test_suite=args.suite,
|
|
47
|
+
parallel=args.parallel,
|
|
48
|
+
max_workers=args.workers
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
print(f"Session completed: {session.success_rate:.2%} success rate")
|
|
52
|
+
if session.success_rate < 0.8:
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
else:
|
|
56
|
+
print("Error: Either --page or --suite must be specified")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_compare(args) -> None:
|
|
61
|
+
"""Execute compare command."""
|
|
62
|
+
config_path = args.config if args.config else None
|
|
63
|
+
tester = LayoutLens(config=config_path, api_key=args.api_key, output_dir=args.output)
|
|
64
|
+
|
|
65
|
+
print(f"Comparing: {args.page_a} vs {args.page_b}")
|
|
66
|
+
result = tester.compare_pages(
|
|
67
|
+
page_a_path=args.page_a,
|
|
68
|
+
page_b_path=args.page_b,
|
|
69
|
+
viewport=args.viewport,
|
|
70
|
+
query=args.query
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if result:
|
|
74
|
+
print(f"Comparison result: {result['answer']}")
|
|
75
|
+
else:
|
|
76
|
+
print("Comparison failed")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_generate(args) -> None:
|
|
81
|
+
"""Execute generate command."""
|
|
82
|
+
if args.type == "config":
|
|
83
|
+
# Generate config file
|
|
84
|
+
config_path = args.output if args.output else "layoutlens.yaml"
|
|
85
|
+
config = create_default_config(config_path)
|
|
86
|
+
print(f"Default configuration created: {config_path}")
|
|
87
|
+
|
|
88
|
+
elif args.type == "suite":
|
|
89
|
+
# Generate test suite template
|
|
90
|
+
suite_path = args.output if args.output else "test_suite.yaml"
|
|
91
|
+
template = {
|
|
92
|
+
"name": "Sample Test Suite",
|
|
93
|
+
"description": "Template test suite for LayoutLens",
|
|
94
|
+
"test_cases": [
|
|
95
|
+
{
|
|
96
|
+
"name": "Homepage Test",
|
|
97
|
+
"html_path": "pages/homepage.html",
|
|
98
|
+
"queries": [
|
|
99
|
+
"Is the navigation menu visible?",
|
|
100
|
+
"Is the logo centered?",
|
|
101
|
+
"Is the layout responsive?"
|
|
102
|
+
],
|
|
103
|
+
"viewports": ["mobile_portrait", "desktop"],
|
|
104
|
+
"expected_results": {},
|
|
105
|
+
"metadata": {"priority": "high"}
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"metadata": {"version": "1.0"}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
import yaml
|
|
112
|
+
with open(suite_path, 'w') as f:
|
|
113
|
+
yaml.dump(template, f, default_flow_style=False, indent=2)
|
|
114
|
+
|
|
115
|
+
print(f"Test suite template created: {suite_path}")
|
|
116
|
+
|
|
117
|
+
elif args.type == "benchmarks":
|
|
118
|
+
# Generate benchmark data
|
|
119
|
+
config_path = args.config if args.config else None
|
|
120
|
+
tester = LayoutLens(config=config_path, api_key=args.api_key)
|
|
121
|
+
output_dir = args.output if args.output else "benchmarks"
|
|
122
|
+
|
|
123
|
+
print("Generating benchmark data...")
|
|
124
|
+
tester.generate_benchmark_data(output_dir)
|
|
125
|
+
print(f"Benchmark data generated in: {output_dir}")
|
|
126
|
+
|
|
127
|
+
else:
|
|
128
|
+
print(f"Unknown generate type: {args.type}")
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cmd_regression(args) -> None:
|
|
133
|
+
"""Execute regression testing command."""
|
|
134
|
+
config_path = args.config if args.config else None
|
|
135
|
+
runner = TestRunner(Config(config_path) if config_path else None)
|
|
136
|
+
|
|
137
|
+
patterns = args.patterns.split(',') if args.patterns else ["*.html"]
|
|
138
|
+
viewports = args.viewports.split(',') if args.viewports else None
|
|
139
|
+
|
|
140
|
+
print(f"Running regression tests:")
|
|
141
|
+
print(f" Baseline: {args.baseline}")
|
|
142
|
+
print(f" Current: {args.current}")
|
|
143
|
+
print(f" Patterns: {patterns}")
|
|
144
|
+
|
|
145
|
+
session = runner.run_regression_tests(
|
|
146
|
+
baseline_dir=args.baseline,
|
|
147
|
+
current_dir=args.current,
|
|
148
|
+
test_patterns=patterns,
|
|
149
|
+
viewports=viewports
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
print(f"Regression testing completed: {session.success_rate:.2%} success rate")
|
|
153
|
+
if session.success_rate < args.threshold:
|
|
154
|
+
print(f"Regression test failed: success rate {session.success_rate:.2%} below threshold {args.threshold:.2%}")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_validate(args) -> None:
|
|
159
|
+
"""Execute validation command."""
|
|
160
|
+
if args.config:
|
|
161
|
+
try:
|
|
162
|
+
config = Config(args.config)
|
|
163
|
+
errors = config.validate()
|
|
164
|
+
|
|
165
|
+
if errors:
|
|
166
|
+
print("Configuration validation failed:")
|
|
167
|
+
for error in errors:
|
|
168
|
+
print(f" - {error}")
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
else:
|
|
171
|
+
print("Configuration is valid ✓")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"Error loading configuration: {e}")
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
elif args.suite:
|
|
177
|
+
try:
|
|
178
|
+
import yaml
|
|
179
|
+
with open(args.suite, 'r') as f:
|
|
180
|
+
data = yaml.safe_load(f)
|
|
181
|
+
|
|
182
|
+
# Basic validation
|
|
183
|
+
required_fields = ['name', 'test_cases']
|
|
184
|
+
for field in required_fields:
|
|
185
|
+
if field not in data:
|
|
186
|
+
print(f"Missing required field: {field}")
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
|
|
189
|
+
# Validate test cases
|
|
190
|
+
test_cases = data.get('test_cases', [])
|
|
191
|
+
if not test_cases:
|
|
192
|
+
print("No test cases found")
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
for i, case in enumerate(test_cases):
|
|
196
|
+
if 'name' not in case:
|
|
197
|
+
print(f"Test case {i} missing name")
|
|
198
|
+
sys.exit(1)
|
|
199
|
+
if 'html_path' not in case:
|
|
200
|
+
print(f"Test case {i} missing html_path")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
# Check if HTML file exists
|
|
204
|
+
if not Path(case['html_path']).exists():
|
|
205
|
+
print(f"HTML file not found: {case['html_path']}")
|
|
206
|
+
|
|
207
|
+
print(f"Test suite is valid ✓ ({len(test_cases)} test cases)")
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f"Error validating test suite: {e}")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
print("Error: Either --config or --suite must be specified")
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main():
|
|
219
|
+
"""Main CLI entry point."""
|
|
220
|
+
parser = argparse.ArgumentParser(
|
|
221
|
+
description="LayoutLens - AI-Enabled UI Test System",
|
|
222
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
223
|
+
epilog="""
|
|
224
|
+
Examples:
|
|
225
|
+
# Test a single page
|
|
226
|
+
layoutlens test --page homepage.html --queries "Is the logo centered?"
|
|
227
|
+
|
|
228
|
+
# Run a test suite
|
|
229
|
+
layoutlens test --suite regression_tests.yaml --parallel
|
|
230
|
+
|
|
231
|
+
# Compare two pages
|
|
232
|
+
layoutlens compare before.html after.html
|
|
233
|
+
|
|
234
|
+
# Generate configuration
|
|
235
|
+
layoutlens generate config
|
|
236
|
+
|
|
237
|
+
# Run regression tests
|
|
238
|
+
layoutlens regression --baseline v1/ --current v2/ --patterns "*.html,pages/*.html"
|
|
239
|
+
"""
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Global options
|
|
243
|
+
parser.add_argument('--config', '-c', help='Configuration file path')
|
|
244
|
+
parser.add_argument('--api-key', help='OpenAI API key (or set OPENAI_API_KEY)')
|
|
245
|
+
parser.add_argument('--output', '-o', help='Output directory')
|
|
246
|
+
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
|
|
247
|
+
|
|
248
|
+
# Subcommands
|
|
249
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
250
|
+
|
|
251
|
+
# Test command
|
|
252
|
+
test_parser = subparsers.add_parser('test', help='Run UI tests')
|
|
253
|
+
test_group = test_parser.add_mutually_exclusive_group(required=True)
|
|
254
|
+
test_group.add_argument('--page', help='Test single HTML page')
|
|
255
|
+
test_group.add_argument('--suite', help='Test suite YAML file')
|
|
256
|
+
test_parser.add_argument('--queries', help='Comma-separated list of test queries')
|
|
257
|
+
test_parser.add_argument('--viewports', help='Comma-separated list of viewport names')
|
|
258
|
+
test_parser.add_argument('--no-auto-queries', action='store_true', help='Disable automatic query generation')
|
|
259
|
+
test_parser.add_argument('--parallel', action='store_true', help='Run tests in parallel')
|
|
260
|
+
test_parser.add_argument('--workers', type=int, help='Number of parallel workers')
|
|
261
|
+
|
|
262
|
+
# Compare command
|
|
263
|
+
compare_parser = subparsers.add_parser('compare', help='Compare two pages')
|
|
264
|
+
compare_parser.add_argument('page_a', help='First HTML page')
|
|
265
|
+
compare_parser.add_argument('page_b', help='Second HTML page')
|
|
266
|
+
compare_parser.add_argument('--viewport', default='desktop', help='Viewport for comparison')
|
|
267
|
+
compare_parser.add_argument('--query', default='Do these two layouts look the same?', help='Comparison query')
|
|
268
|
+
|
|
269
|
+
# Generate command
|
|
270
|
+
generate_parser = subparsers.add_parser('generate', help='Generate files')
|
|
271
|
+
generate_parser.add_argument('type', choices=['config', 'suite', 'benchmarks'], help='Type of file to generate')
|
|
272
|
+
|
|
273
|
+
# Regression command
|
|
274
|
+
regression_parser = subparsers.add_parser('regression', help='Run regression tests')
|
|
275
|
+
regression_parser.add_argument('--baseline', required=True, help='Baseline directory')
|
|
276
|
+
regression_parser.add_argument('--current', required=True, help='Current version directory')
|
|
277
|
+
regression_parser.add_argument('--patterns', default='*.html', help='Comma-separated file patterns')
|
|
278
|
+
regression_parser.add_argument('--viewports', help='Comma-separated viewport names')
|
|
279
|
+
regression_parser.add_argument('--threshold', type=float, default=0.8, help='Success rate threshold')
|
|
280
|
+
|
|
281
|
+
# Validate command
|
|
282
|
+
validate_parser = subparsers.add_parser('validate', help='Validate configuration or test suite')
|
|
283
|
+
validate_group = validate_parser.add_mutually_exclusive_group(required=True)
|
|
284
|
+
validate_group.add_argument('--config', help='Validate configuration file')
|
|
285
|
+
validate_group.add_argument('--suite', help='Validate test suite file')
|
|
286
|
+
|
|
287
|
+
# Parse arguments
|
|
288
|
+
args = parser.parse_args()
|
|
289
|
+
|
|
290
|
+
# Set up API key from environment if not provided
|
|
291
|
+
if not args.api_key:
|
|
292
|
+
args.api_key = os.getenv('OPENAI_API_KEY')
|
|
293
|
+
|
|
294
|
+
# Handle commands
|
|
295
|
+
if args.command == 'test':
|
|
296
|
+
cmd_test(args)
|
|
297
|
+
elif args.command == 'compare':
|
|
298
|
+
cmd_compare(args)
|
|
299
|
+
elif args.command == 'generate':
|
|
300
|
+
cmd_generate(args)
|
|
301
|
+
elif args.command == 'regression':
|
|
302
|
+
cmd_regression(args)
|
|
303
|
+
elif args.command == 'validate':
|
|
304
|
+
cmd_validate(args)
|
|
305
|
+
else:
|
|
306
|
+
parser.print_help()
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
main()
|