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.
- ai_virtual_testing/__init__.py +0 -0
- ai_virtual_testing/cli.py +239 -0
- ai_virtual_testing/engine.py +110 -0
- ai_virtual_testing/models.py +13 -0
- qa_virtual_testing-1.1.0.dist-info/METADATA +22 -0
- qa_virtual_testing-1.1.0.dist-info/RECORD +9 -0
- qa_virtual_testing-1.1.0.dist-info/WHEEL +5 -0
- qa_virtual_testing-1.1.0.dist-info/entry_points.txt +2 -0
- qa_virtual_testing-1.1.0.dist-info/top_level.txt +1 -0
|
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 @@
|
|
|
1
|
+
ai_virtual_testing
|