QA-virtual-testing 1.1.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.
File without changes
@@ -0,0 +1,239 @@
1
+ import sys
2
+ import os
3
+ import time
4
+ import json
5
+ from rich.console import Console
6
+ from rich.prompt import Prompt, Confirm
7
+ from playwright.sync_api import sync_playwright
8
+ from .engine import AIVirtualTester
9
+ from .models import BugList, BugReport
10
+
11
+ console = Console()
12
+
13
+ # --- PROFESSIONAL IN-BROWSER SIDEBAR (HTML & CSS ONLY) ---
14
+ # Notice: No <script> tags here anymore!
15
+ QA_SIDEBAR_UI = """
16
+ <style>
17
+ #qac-wrapper {
18
+ position: fixed; top: 20px; right: 20px; z-index: 2147483647;
19
+ font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
20
+ }
21
+ #qac-panel {
22
+ width: 320px; background: #18181b; border: 1px solid #3f3f46;
23
+ border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5);
24
+ padding: 20px; color: #f4f4f5; display: flex; flex-direction: column; gap: 12px;
25
+ transition: opacity 0.2s ease-in-out;
26
+ }
27
+ .minimized #qac-panel { display: none; }
28
+ .qac-header { display: flex; justify-content: space-between; align-items: center; }
29
+ .qac-header h3 { margin: 0; font-size: 16px; color: #0ea5e9; }
30
+ .qac-label { font-size: 12px; color: #a1a1aa; margin-bottom: 4px; }
31
+ .qac-input, .qac-select, .qac-textarea {
32
+ width: 100%; background: #27272a; border: 1px solid #3f3f46;
33
+ color: white; padding: 8px; border-radius: 6px; font-size: 14px; box-sizing: border-box;
34
+ }
35
+ .qac-btn {
36
+ width: 100%; padding: 10px; border: none; border-radius: 6px;
37
+ font-weight: 600; cursor: pointer; transition: 0.2s;
38
+ }
39
+ .qac-btn-primary { background: #0ea5e9; color: white; }
40
+ .qac-btn-primary:hover { background: #0284c7; }
41
+ .qac-btn-danger { background: #ef4444; color: white; margin-top: 5px; }
42
+ #qac-fab {
43
+ width: 50px; height: 50px; background: #0ea5e9; border-radius: 50%;
44
+ display: none; justify-content: center; align-items: center;
45
+ cursor: pointer; font-size: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
46
+ }
47
+ .minimized #qac-fab { display: flex; }
48
+ </style>
49
+
50
+ <div id="qac-wrapper">
51
+ <div id="qac-fab" onclick="document.getElementById('qac-wrapper').classList.remove('minimized')">🤖</div>
52
+ <div id="qac-panel">
53
+ <div class="qac-header">
54
+ <h3>QA Copilot PRO</h3>
55
+ <button style="background:none; border:none; color:#a1a1aa; cursor:pointer;" onclick="document.getElementById('qac-wrapper').classList.add('minimized')">✖</button>
56
+ </div>
57
+ <div>
58
+ <label class="qac-label">Bug Title</label>
59
+ <input type="text" id="qac-title" class="qac-input" placeholder="What is the issue?">
60
+ </div>
61
+ <div>
62
+ <label class="qac-label">Severity</label>
63
+ <select id="qac-severity" class="qac-select">
64
+ <option value="Low">Low</option>
65
+ <option value="Medium" selected>Medium</option>
66
+ <option value="High">High</option>
67
+ </select>
68
+ </div>
69
+ <div>
70
+ <label class="qac-label">Notes</label>
71
+ <textarea id="qac-desc" class="qac-textarea" placeholder="Steps to reproduce..."></textarea>
72
+ </div>
73
+ <button class="qac-btn qac-btn-primary" onclick="window.submitBug()">📸 Capture & Log Bug</button>
74
+ <button class="qac-btn qac-btn-danger" onclick="window.pythonFinish()">💾 End Session & Save</button>
75
+ <div id="qac-status" style="display:none; font-size:12px; text-align:center; color:#10b981; margin-top:5px;">✅ Bug saved!</div>
76
+ </div>
77
+ </div>
78
+ """
79
+ # --- END UI ---
80
+
81
+ def manual_mode_loop(tester: AIVirtualTester, url: str):
82
+ bugs_caught = []
83
+ pending_bugs = []
84
+ finished = [False]
85
+
86
+ with sync_playwright() as p:
87
+ browser = p.chromium.launch(headless=False)
88
+ context = browser.new_context(record_video_dir=str(tester.video_dir))
89
+
90
+ # Safely package the HTML string
91
+ safe_html = json.dumps(QA_SIDEBAR_UI)
92
+
93
+ # THE FIX: We inject both the HTML AND define the Javascript functions here natively
94
+ context.add_init_script(f"""
95
+ // 1. Inject the HTML when the page loads
96
+ document.addEventListener("DOMContentLoaded", () => {{
97
+ if (!document.getElementById('qac-wrapper')) {{
98
+ document.body.insertAdjacentHTML('beforeend', {safe_html});
99
+ }}
100
+ }});
101
+
102
+ // 2. Define the Javascript functions globally so the buttons can find them
103
+ window.submitBug = () => {{
104
+ const titleInput = document.getElementById('qac-title');
105
+ if (!titleInput) return;
106
+
107
+ const data = {{
108
+ title: titleInput.value,
109
+ severity: document.getElementById('qac-severity').value,
110
+ desc: document.getElementById('qac-desc').value
111
+ }};
112
+
113
+ if(!data.title) {{
114
+ alert("Please enter a Bug Title");
115
+ return;
116
+ }}
117
+
118
+ // Hide UI for a clean screenshot
119
+ document.getElementById('qac-panel').style.opacity = '0';
120
+
121
+ // Send data to Python
122
+ window.pythonLogBug(JSON.stringify(data));
123
+ }};
124
+
125
+ window.onBugSaved = () => {{
126
+ // Bring UI back
127
+ const panel = document.getElementById('qac-panel');
128
+ if (panel) panel.style.opacity = '1';
129
+
130
+ // Clear the forms
131
+ document.getElementById('qac-title').value = '';
132
+ document.getElementById('qac-desc').value = '';
133
+
134
+ // Show success message
135
+ const status = document.getElementById('qac-status');
136
+ if (status) {{
137
+ status.style.display = 'block';
138
+ setTimeout(() => status.style.display = 'none', 3000);
139
+ }}
140
+ }};
141
+ """)
142
+
143
+ page = context.new_page()
144
+
145
+ # Python Bridge: Receives the data from Javascript
146
+ def log_bug_callback(bug_json):
147
+ pending_bugs.append(json.loads(bug_json))
148
+
149
+ def finish_callback():
150
+ finished[0] = True
151
+
152
+ # Expose bridges BEFORE navigating
153
+ page.expose_function("pythonLogBug", log_bug_callback)
154
+ page.expose_function("pythonFinish", finish_callback)
155
+
156
+ page.goto(url)
157
+
158
+ console.print("\n[bold cyan]✨ Manual Copilot Active![/bold cyan]")
159
+ console.print("[white]Use the sidebar in the browser window to log issues.[/white]")
160
+
161
+ # Main Python Loop
162
+ while not finished[0]:
163
+
164
+ # If a bug was submitted, process it
165
+ if pending_bugs:
166
+ data = pending_bugs.pop(0)
167
+ try:
168
+ # Give UI time to hide visually
169
+ page.wait_for_timeout(200)
170
+
171
+ shot_path = tester.img_dir / f"manual_bug_{int(time.time())}.png"
172
+ page.screenshot(path=str(shot_path))
173
+
174
+ bugs_caught.append(BugReport(
175
+ issue=data['title'],
176
+ severity=data['severity'],
177
+ description=data['desc'],
178
+ recommendation="Manual Review",
179
+ suggested_code_fix="N/A",
180
+ screenshot_path=str(shot_path)
181
+ ))
182
+
183
+ # Tell Javascript to show success message and bring UI back
184
+ page.evaluate("if(window.onBugSaved) window.onBugSaved()")
185
+ except Exception as e:
186
+ console.print(f"[red]Failed to capture bug: {e}[/red]")
187
+
188
+ # Watchdog: Ensure UI exists mid-navigation
189
+ try:
190
+ ui_missing = page.evaluate("() => document.body && !document.getElementById('qac-wrapper')")
191
+ if ui_missing:
192
+ page.evaluate(f"() => document.body.insertAdjacentHTML('beforeend', {safe_html})")
193
+ except Exception:
194
+ pass
195
+
196
+ page.wait_for_timeout(100)
197
+ if page.is_closed():
198
+ break
199
+
200
+ context.close()
201
+ browser.close()
202
+
203
+ return BugList(bugs=bugs_caught)
204
+
205
+ def main():
206
+ os.system('cls' if os.name == 'nt' else 'clear')
207
+ console.print("\n[bold cyan]🤖 AI Virtual Tester PRO[/bold cyan]\n", justify="center")
208
+
209
+ api_key = os.environ.get("GEMINI_API_KEY") or Prompt.ask("[yellow]🔑 Enter Gemini API Key[/yellow]", password=True)
210
+ tester = AIVirtualTester(api_key)
211
+
212
+ url = Prompt.ask("\n[bold green]🌐 Target URL[/bold green]", default="http://localhost:8000")
213
+
214
+ mode = Prompt.ask("\n[bold magenta]Testing Mode[/bold magenta]\n1. Autonomous AI Testing\n2. Interactive Manual Copilot", choices=["1", "2"], default="2")
215
+
216
+ output_file = Prompt.ask("\n[bold green]📁 Excel Filename[/bold green]", default="QA_Report.xlsx")
217
+ if not output_file.endswith('.xlsx'): output_file += ".xlsx"
218
+
219
+ report = None
220
+
221
+ if mode == "1":
222
+ stack = Prompt.ask("[bold green]💻 Tech Stack[/bold green]", default="Django, HTML/CSS/JS")
223
+ instr = Prompt.ask("[bold green]📝 Instructions[/bold green]", default="Full UI check")
224
+ autofill = Confirm.ask("Enable Autofill dummy data?")
225
+
226
+ with console.status("[bold magenta]🚀 AI is auditing...[/bold magenta]"):
227
+ report = tester.run_auto_audit(url, instr, stack, autofill)
228
+ else:
229
+ report = manual_mode_loop(tester, url)
230
+
231
+ if report and len(report.bugs) > 0:
232
+ tester.save_rich_excel(report, output_file)
233
+ console.print(f"\n[bold green]✅ Done! Logged {len(report.bugs)} issues.[/bold green]")
234
+ console.print(f"📊 Report: [bold underline]qa_reports/{output_file}[/bold underline]")
235
+ else:
236
+ console.print("\n[yellow]Session ended. No bugs logged.[/yellow]")
237
+
238
+ if __name__ == "__main__":
239
+ main()
@@ -0,0 +1,110 @@
1
+ import os
2
+ import json
3
+ import re
4
+ import time
5
+ from pathlib import Path
6
+ import pandas as pd
7
+ from PIL import Image
8
+ from openpyxl import Workbook
9
+ from openpyxl.drawing.image import Image as XLImage
10
+ from playwright.sync_api import sync_playwright
11
+ import google.generativeai as genai
12
+ from .models import BugList, BugReport
13
+
14
+ class AIVirtualTester:
15
+ def __init__(self, api_key: str):
16
+ genai.configure(api_key=api_key)
17
+ self.model = genai.GenerativeModel('gemini-2.5-flash')
18
+
19
+ # Create directories for advanced features
20
+ self.output_dir = Path("qa_reports")
21
+ self.img_dir = self.output_dir / "screenshots"
22
+ self.video_dir = self.output_dir / "videos"
23
+ self.img_dir.mkdir(parents=True, exist_ok=True)
24
+ self.video_dir.mkdir(parents=True, exist_ok=True)
25
+
26
+ def _inject_autofill(self, page):
27
+ """Advanced feature: Automatically fills forms with dummy data before testing."""
28
+ page.evaluate("""() => {
29
+ document.querySelectorAll('input[type="email"]').forEach(el => el.value = 'qa_tester@example.com');
30
+ document.querySelectorAll('input[type="text"]').forEach(el => el.value = 'QA Automation Test');
31
+ document.querySelectorAll('input[type="number"]').forEach(el => el.value = '42');
32
+ document.querySelectorAll('input[type="password"]').forEach(el => el.value = 'SecureTest123!');
33
+ document.querySelectorAll('textarea').forEach(el => el.value = 'This is an automated test injection.');
34
+ }""")
35
+
36
+ def run_auto_audit(self, url: str, instructions: str, tech_stack: str, autofill: bool) -> BugList:
37
+ """Fully autonomous mode."""
38
+ temp_shot = self.img_dir / f"auto_state_{int(time.time())}.png"
39
+
40
+ with sync_playwright() as p:
41
+ # Launch with video recording enabled
42
+ browser = p.chromium.launch(headless=True)
43
+ context = browser.new_context(record_video_dir=str(self.video_dir))
44
+ page = context.new_page()
45
+
46
+ try:
47
+ page.goto(url, wait_until="networkidle")
48
+ page.wait_for_timeout(2000)
49
+
50
+ if autofill:
51
+ self._inject_autofill(page)
52
+ page.wait_for_timeout(1000) # Wait for UI to update after fill
53
+
54
+ page.screenshot(path=str(temp_shot), full_page=True)
55
+ finally:
56
+ context.close()
57
+ browser.close()
58
+
59
+ # AI Analysis
60
+ with Image.open(temp_shot) as img:
61
+ prompt = f"""
62
+ Act as a Lead QA Engineer. Analyze this UI based on:
63
+ Instructions: {instructions}
64
+ Tech Stack: {tech_stack}
65
+
66
+ Output MUST be a raw JSON object only:
67
+ {{ "bugs": [ {{ "issue": "", "severity": "", "description": "", "recommendation": "", "suggested_code_fix": "" }} ] }}
68
+ """
69
+ response = self.model.generate_content([prompt, img])
70
+
71
+ # Parse JSON
72
+ match = re.search(r'\{.*\}', response.text, re.DOTALL)
73
+ if not match:
74
+ raise ValueError("AI failed to return valid JSON.")
75
+
76
+ bug_data = json.loads(match.group(0))
77
+
78
+ # Attach the screenshot path to all found bugs in auto mode
79
+ for bug in bug_data.get("bugs", []):
80
+ bug["screenshot_path"] = str(temp_shot)
81
+
82
+ return BugList(**bug_data)
83
+
84
+ def save_rich_excel(self, bug_data: BugList, filename: str):
85
+ """Advanced feature: Creates an Excel file with actual images embedded inside it."""
86
+ export_path = self.output_dir / filename
87
+
88
+ # Create base dataframe
89
+ df = pd.DataFrame([b.model_dump(exclude={"screenshot_path"}) for b in bug_data.bugs])
90
+ df.to_excel(str(export_path), index=False, engine='openpyxl')
91
+
92
+ # Re-open with openpyxl to inject images
93
+ import openpyxl
94
+ wb = openpyxl.load_workbook(str(export_path))
95
+ ws = wb.active
96
+
97
+ # Add a column for screenshots
98
+ ws.cell(row=1, column=len(df.columns) + 1, value="Screenshot")
99
+
100
+ for idx, bug in enumerate(bug_data.bugs):
101
+ row_num = idx + 2
102
+ ws.row_dimensions[row_num].height = 150 # Make row tall enough for image
103
+
104
+ if bug.screenshot_path and os.path.exists(bug.screenshot_path):
105
+ img = XLImage(bug.screenshot_path)
106
+ # Resize image to fit nicely in Excel
107
+ img.width, img.height = 200, 150
108
+ ws.add_image(img, f"{openpyxl.utils.get_column_letter(len(df.columns) + 1)}{row_num}")
109
+
110
+ wb.save(str(export_path))
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+
4
+ class BugReport(BaseModel):
5
+ issue: str = Field(description="A short title of the bug")
6
+ severity: str = Field(description="High, Medium, or Low")
7
+ description: str = Field(description="Detailed observation")
8
+ recommendation: str = Field(description="How to fix it")
9
+ suggested_code_fix: str = Field(description="Exact code snippet to fix the issue")
10
+ screenshot_path: Optional[str] = Field(default=None, description="Path to the saved image")
11
+
12
+ class BugList(BaseModel):
13
+ bugs: List[BugReport]
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: QA_virtual_testing
3
+ Version: 1.1.0
4
+ Summary: An AI-powered virtual QA tester with Auto-Healing code generation.
5
+ Author: Mohammad Shahzeb Ali Talha
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: playwright
9
+ Requires-Dist: google-generativeai
10
+ Requires-Dist: pillow
11
+ Requires-Dist: pandas
12
+ Requires-Dist: openpyxl
13
+ Requires-Dist: pydantic
14
+ Requires-Dist: rich
15
+
16
+ # AI Virtual Testing
17
+ An AI-powered QA engineer in your terminal.
18
+
19
+ ## Installation
20
+ ```bash
21
+ pip install ai_virtual_testing
22
+ playwright install chromium
@@ -0,0 +1,9 @@
1
+ ai_virtual_testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ai_virtual_testing/cli.py,sha256=uDn4HxBiBSlVbGL-oJ8BBHWj9baVwnv0keydke2vdjs,10216
3
+ ai_virtual_testing/engine.py,sha256=q6TtT31FQfAxuQPGg2F8NmIO1cXtflAneu3WEoOz_gI,4817
4
+ ai_virtual_testing/models.py,sha256=_o7r91iWUS4aAb5QJnDXuSu00_MrrF5y4dBiC1u7Vh8,599
5
+ qa_virtual_testing-1.1.0.dist-info/METADATA,sha256=Edeu2oHhQZCu4uan3mBdZK17PDjwAjCG8XGXNX-hUh8,581
6
+ qa_virtual_testing-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ qa_virtual_testing-1.1.0.dist-info/entry_points.txt,sha256=YP_PlgnQlAP4Zc6iQxgQmJ5B15BXpX6nyzibyu4FpEw,56
8
+ qa_virtual_testing-1.1.0.dist-info/top_level.txt,sha256=0N-5mRTZh6Q4CWjYeNA-SB-JjTo6pxG1ZJNQ37mkb30,19
9
+ qa_virtual_testing-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ai-test = ai_virtual_testing.cli:main
@@ -0,0 +1 @@
1
+ ai_virtual_testing