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 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()