@nhonh/qabot 1.0.2 → 1.1.0
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.
- package/package.json +2 -3
- package/src/cli/commands/test.js +2 -1
- package/src/core/constants.js +1 -1
- package/src/e2e/e2e-generator.js +14 -6
- package/src/e2e/e2e-prompts.js +80 -53
- package/src/e2e/playwright-setup.js +24 -10
- package/src/reporter/html-builder.js +0 -2
- package/assets/favicon.svg +0 -15
- package/assets/logo.svg +0 -56
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nhonh/qabot",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI-powered universal QA automation tool. Import any project, AI analyzes and runs tests across all layers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
"files": [
|
|
60
60
|
"bin/",
|
|
61
61
|
"src/",
|
|
62
|
-
"templates/"
|
|
63
|
-
"assets/"
|
|
62
|
+
"templates/"
|
|
64
63
|
]
|
|
65
64
|
}
|
package/src/cli/commands/test.js
CHANGED
|
@@ -77,7 +77,7 @@ async function runTest(feature, options) {
|
|
|
77
77
|
try {
|
|
78
78
|
await ensurePlaywright(projectDir);
|
|
79
79
|
await ensureE2EStructure(projectDir);
|
|
80
|
-
await writePlaywrightConfig(projectDir, config);
|
|
80
|
+
await writePlaywrightConfig(projectDir, config, baseUrl);
|
|
81
81
|
await writeAuthHelper(projectDir, config);
|
|
82
82
|
spinner.succeed(G(" Playwright ready"));
|
|
83
83
|
} catch (err) {
|
|
@@ -277,6 +277,7 @@ function runPlaywrightStreaming(projectDir, specFile, options, baseUrl) {
|
|
|
277
277
|
`--config=${configPath}`,
|
|
278
278
|
"--project=chromium",
|
|
279
279
|
"--reporter=line",
|
|
280
|
+
"--timeout=60000",
|
|
280
281
|
);
|
|
281
282
|
if (options.headed) args.push("--headed");
|
|
282
283
|
|
package/src/core/constants.js
CHANGED
package/src/e2e/e2e-generator.js
CHANGED
|
@@ -30,12 +30,20 @@ ${code}
|
|
|
30
30
|
- Base URL: ${context.baseUrl || "http://localhost:3000"}
|
|
31
31
|
- Auth: ${context.authProvider || "none"}
|
|
32
32
|
|
|
33
|
-
##
|
|
34
|
-
1.
|
|
35
|
-
2.
|
|
36
|
-
3.
|
|
37
|
-
4.
|
|
38
|
-
5.
|
|
33
|
+
## CRITICAL: Architecture rules (DO NOT CHANGE)
|
|
34
|
+
1. MUST use \`test.describe.serial\` — all tests share ONE browser session
|
|
35
|
+
2. MUST use \`let page\` at describe scope — shared across all tests
|
|
36
|
+
3. \`test.beforeAll\` creates page + login + navigate ONCE
|
|
37
|
+
4. \`test.afterAll\` closes page ONCE
|
|
38
|
+
5. Each \`test()\` does NOT take \`{ page }\` argument — uses shared \`page\`
|
|
39
|
+
|
|
40
|
+
## Common fixes
|
|
41
|
+
1. Wrong selector — use getByRole, getByText, getByTestId
|
|
42
|
+
2. Timing — add waitForLoadState("networkidle") or waitForTimeout(2000)
|
|
43
|
+
3. Element not visible — add .first() for multiple matches, or more specific selector
|
|
44
|
+
4. Navigation — use full URL with baseURL prefix
|
|
45
|
+
5. Auth — ensure login completes before assertions
|
|
46
|
+
6. Timeout — increase timeout for slow elements: { timeout: 15000 }
|
|
39
47
|
|
|
40
48
|
Return the COMPLETE fixed test file. No markdown fences.`;
|
|
41
49
|
|
package/src/e2e/e2e-prompts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function buildE2EPrompt(featureName, context) {
|
|
2
2
|
const sourceSection = context.sourceCode
|
|
3
|
-
? `\n## Source Code Analysis\
|
|
3
|
+
? `\n## Source Code Analysis\n\`\`\`\n${context.sourceCode.slice(0, 8000)}\n\`\`\`\n`
|
|
4
4
|
: "";
|
|
5
5
|
|
|
6
6
|
const routeSection = context.route
|
|
@@ -8,60 +8,87 @@ export function buildE2EPrompt(featureName, context) {
|
|
|
8
8
|
: "";
|
|
9
9
|
|
|
10
10
|
const useCaseSection = context.useCases?.length
|
|
11
|
-
? `\n## QA Use Cases
|
|
11
|
+
? `\n## QA Use Cases\n${context.useCases.map((uc) => `### ${uc.scenario}\n${uc.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}`).join("\n\n")}\n`
|
|
12
12
|
: "";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const baseUrl = context.baseUrl || "http://localhost:3000";
|
|
15
|
+
const route = context.route || "/" + featureName;
|
|
16
|
+
|
|
17
|
+
return `You are an expert Playwright E2E automation engineer.
|
|
15
18
|
|
|
16
19
|
## Feature: ${featureName}
|
|
17
|
-
## Base URL: ${
|
|
18
|
-
##
|
|
19
|
-
${
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
20
|
+
## Base URL: ${baseUrl}
|
|
21
|
+
## Page Route: ${route}
|
|
22
|
+
## Auth: ${context.authProvider || "none"}
|
|
23
|
+
${sourceSection}${useCaseSection}
|
|
24
|
+
|
|
25
|
+
## CRITICAL ARCHITECTURE — ONE BROWSER SESSION
|
|
26
|
+
|
|
27
|
+
All tests run in a SINGLE browser session. The browser opens ONCE, logs in ONCE, then navigates between tests WITHOUT closing.
|
|
28
|
+
|
|
29
|
+
Use this EXACT structure:
|
|
30
|
+
|
|
31
|
+
\`\`\`
|
|
32
|
+
const { test, expect } = require("@playwright/test");
|
|
33
|
+
const { login } = require("../helpers/auth.js");
|
|
34
|
+
|
|
35
|
+
test.describe.serial("${featureName}", () => {
|
|
36
|
+
let page;
|
|
37
|
+
|
|
38
|
+
test.beforeAll(async ({ browser }) => {
|
|
39
|
+
page = await browser.newPage();
|
|
40
|
+
await login(page, "${baseUrl}");
|
|
41
|
+
await page.goto("${baseUrl}${route}");
|
|
42
|
+
await page.waitForLoadState("networkidle");
|
|
43
|
+
await page.screenshot({ path: "e2e/screenshots/${featureName}-00-initial.png", fullPage: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test.afterAll(async () => {
|
|
47
|
+
await page.close();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("TC-01: page loads and displays main content", async () => {
|
|
51
|
+
// use the shared 'page' variable — NOT the argument
|
|
52
|
+
await expect(page).toHaveURL(/${featureName.replace(/-/g, "[-/]")}/i);
|
|
53
|
+
// verify main content visible
|
|
54
|
+
await page.screenshot({ path: "e2e/screenshots/${featureName}-01-loaded.png", fullPage: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("TC-02: next test case...", async () => {
|
|
58
|
+
// continue using shared 'page' — same browser, same session
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
## IMPORTANT RULES:
|
|
64
|
+
1. Use \`test.describe.serial\` — tests run IN ORDER on the SAME page
|
|
65
|
+
2. Use \`let page\` declared at describe level — shared across ALL tests
|
|
66
|
+
3. \`test.beforeAll\` creates page + login + navigate ONCE
|
|
67
|
+
4. \`test.afterAll\` closes page ONCE
|
|
68
|
+
5. Each \`test()\` does NOT take \`{ page }\` argument — uses the shared \`page\` variable
|
|
69
|
+
6. Take screenshot in EVERY test with path: \`e2e/screenshots/${featureName}-{number}-{name}.png\`
|
|
70
|
+
7. Use \`page.getByRole()\`, \`page.getByText()\`, \`page.getByTestId()\` selectors
|
|
71
|
+
8. After navigation within tests: \`await page.waitForLoadState("networkidle")\`
|
|
72
|
+
9. Use \`{ timeout: 15000 }\` for slow-loading elements
|
|
73
|
+
10. Prefix each test name with TC-XX: for tracking
|
|
74
|
+
|
|
75
|
+
## GENERATE 8-12 TEST CASES:
|
|
76
|
+
|
|
77
|
+
### P0 Critical (must have):
|
|
78
|
+
- TC-01: Page loads, main content visible, URL correct
|
|
79
|
+
- TC-02: Key data displays correctly (lists, cards, amounts)
|
|
80
|
+
- TC-03: Primary user action (main button/form/interaction)
|
|
81
|
+
- TC-04: Navigation within the feature works
|
|
82
|
+
|
|
83
|
+
### P1 Important:
|
|
84
|
+
- TC-05: Secondary actions (links, toggles, modals)
|
|
85
|
+
- TC-06: Different UI states (loading, empty, error)
|
|
86
|
+
- TC-07: Form validation if applicable
|
|
87
|
+
- TC-08: Responsive check (viewport resize)
|
|
88
|
+
|
|
89
|
+
### P2 Edge cases:
|
|
90
|
+
- TC-09: Edge cases relevant to the feature
|
|
91
|
+
- TC-10: Back navigation / breadcrumbs
|
|
92
|
+
|
|
93
|
+
Return ONLY JavaScript code. No markdown fences. No explanation.`;
|
|
67
94
|
}
|
|
@@ -43,9 +43,12 @@ export async function ensureE2EStructure(projectDir) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export async function writePlaywrightConfig(
|
|
46
|
+
export async function writePlaywrightConfig(
|
|
47
|
+
projectDir,
|
|
48
|
+
config,
|
|
49
|
+
overrideBaseUrl,
|
|
50
|
+
) {
|
|
47
51
|
const configPath = path.join(projectDir, "e2e", "playwright.config.js");
|
|
48
|
-
if (await fileExists(configPath)) return configPath;
|
|
49
52
|
|
|
50
53
|
const envUrls = {};
|
|
51
54
|
if (config.environments) {
|
|
@@ -54,6 +57,10 @@ export async function writePlaywrightConfig(projectDir, config) {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
const baseUrlExpr = overrideBaseUrl
|
|
61
|
+
? `"${overrideBaseUrl}"`
|
|
62
|
+
: `ENV_URLS[process.env.E2E_ENV || "default"] || "http://localhost:3000"`;
|
|
63
|
+
|
|
57
64
|
const content = `const { defineConfig, devices } = require("@playwright/test");
|
|
58
65
|
|
|
59
66
|
const ENV_URLS = ${JSON.stringify(envUrls, null, 2)};
|
|
@@ -61,19 +68,25 @@ const ENV_URLS = ${JSON.stringify(envUrls, null, 2)};
|
|
|
61
68
|
module.exports = defineConfig({
|
|
62
69
|
testDir: "./tests",
|
|
63
70
|
fullyParallel: false,
|
|
64
|
-
retries:
|
|
71
|
+
retries: 0,
|
|
65
72
|
workers: 1,
|
|
73
|
+
timeout: 60000,
|
|
74
|
+
expect: { timeout: 10000 },
|
|
66
75
|
reporter: [
|
|
67
|
-
["
|
|
76
|
+
["line"],
|
|
68
77
|
["json", { outputFile: "../qabot-reports/playwright/results.json" }],
|
|
69
78
|
],
|
|
70
79
|
use: {
|
|
71
|
-
baseURL:
|
|
80
|
+
baseURL: ${baseUrlExpr},
|
|
72
81
|
trace: "on-first-retry",
|
|
73
82
|
screenshot: "on",
|
|
83
|
+
video: "retain-on-failure",
|
|
74
84
|
ignoreHTTPSErrors: true,
|
|
75
|
-
actionTimeout:
|
|
85
|
+
actionTimeout: 15000,
|
|
76
86
|
navigationTimeout: 30000,
|
|
87
|
+
launchOptions: {
|
|
88
|
+
slowMo: 100,
|
|
89
|
+
},
|
|
77
90
|
},
|
|
78
91
|
projects: [
|
|
79
92
|
{
|
|
@@ -90,7 +103,6 @@ module.exports = defineConfig({
|
|
|
90
103
|
|
|
91
104
|
export async function writeAuthHelper(projectDir, config) {
|
|
92
105
|
const helperPath = path.join(projectDir, "e2e", "helpers", "auth.js");
|
|
93
|
-
if (await fileExists(helperPath)) return;
|
|
94
106
|
|
|
95
107
|
const authProvider = config.auth?.provider || "none";
|
|
96
108
|
const content = `const { expect } = require("@playwright/test");
|
|
@@ -100,8 +112,8 @@ async function login(page, baseURL) {
|
|
|
100
112
|
const password = process.env.E2E_TEST_PASSWORD || "";
|
|
101
113
|
|
|
102
114
|
if (!email || !password) {
|
|
103
|
-
console.warn("E2E_TEST_EMAIL or E2E_TEST_PASSWORD not set — skipping login");
|
|
104
|
-
return;
|
|
115
|
+
console.warn("[QABot] E2E_TEST_EMAIL or E2E_TEST_PASSWORD not set — skipping login");
|
|
116
|
+
return false;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
await page.goto(baseURL || "/");
|
|
@@ -127,14 +139,16 @@ async function login(page, baseURL) {
|
|
|
127
139
|
const submitBtn = page.getByRole("button", { name: /continue|log in|sign in|submit/i });
|
|
128
140
|
await submitBtn.click();
|
|
129
141
|
await page.waitForLoadState("networkidle");
|
|
142
|
+
await page.waitForTimeout(2000);
|
|
130
143
|
}`
|
|
131
144
|
: `
|
|
132
|
-
// Generic login — customize for your auth provider
|
|
133
145
|
await page.getByLabel(/email/i).fill(email);
|
|
134
146
|
await page.getByLabel(/password/i).fill(password);
|
|
135
147
|
await page.getByRole("button", { name: /sign in|log in|submit/i }).click();
|
|
136
148
|
await page.waitForLoadState("networkidle");`
|
|
137
149
|
}
|
|
150
|
+
|
|
151
|
+
return true;
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
module.exports = { login };
|
|
@@ -15,7 +15,6 @@ export class HtmlBuilder {
|
|
|
15
15
|
return `<!DOCTYPE html>
|
|
16
16
|
<html lang="en"><head>
|
|
17
17
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
18
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none'%3E%3Cdefs%3E%3ClinearGradient id='sg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0%25' stop-color='%23A78BFA'/%3E%3Cstop offset='100%25' stop-color='%237C3AED'/%3E%3C/linearGradient%3E%3ClinearGradient id='cg' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%2334D399'/%3E%3Cstop offset='100%25' stop-color='%2310B981'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M16 2L28 7.5C28 7.5 29 18 16 29.5 3 18 4 7.5 4 7.5Z' fill='url(%23sg)'/%3E%3Cpath d='M16 5L25.5 9.5C25.5 9.5 26.2 17.5 16 27 5.8 17.5 6.5 9.5 6.5 9.5Z' fill='%230C0A1A'/%3E%3Cpath d='M11.5 15.5L14.5 18.5 20.5 12.5' stroke='url(%23cg)' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E">
|
|
19
18
|
<title>QABot Report \u2014 ${esc(meta.feature || "all")} | ${esc(meta.projectName || "")}</title>
|
|
20
19
|
<style>
|
|
21
20
|
:root{--bg:#0C0A1A;--bg2:#13102A;--card:#1A1635;--card2:#221E3D;--border:#2D2852;--text:#E8E4F0;--dim:#8B85A0;--v:#A78BFA;--v2:#7C3AED;--v3:#C4B5FD;--g:#34D399;--r:#F87171;--y:#FBBF24;--cyan:#22D3EE;--font:system-ui,-apple-system,BlinkMacSystemFont,sans-serif;--mono:'SF Mono',SFMono-Regular,'JetBrains Mono',Menlo,monospace}
|
|
@@ -104,7 +103,6 @@ footer{text-align:center;padding:2rem 0 1rem;color:var(--dim);font-size:.8rem;bo
|
|
|
104
103
|
<div class="wrap">
|
|
105
104
|
|
|
106
105
|
<div class="hero">
|
|
107
|
-
<div style="margin-bottom:1rem"><svg width="48" height="48" viewBox="0 0 32 32" fill="none"><defs><linearGradient id="hsg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#A78BFA"/><stop offset="100%" stop-color="#7C3AED"/></linearGradient><linearGradient id="hcg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#34D399"/><stop offset="100%" stop-color="#10B981"/></linearGradient></defs><path d="M16 2L28 7.5C28 7.5 29 18 16 29.5 3 18 4 7.5 4 7.5Z" fill="url(#hsg)"/><path d="M16 5L25.5 9.5C25.5 9.5 26.2 17.5 16 27 5.8 17.5 6.5 9.5 6.5 9.5Z" fill="#0C0A1A"/><path d="M11.5 15.5L14.5 18.5 20.5 12.5" stroke="url(#hcg)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></div>
|
|
108
106
|
<h1>QABot Test Report</h1>
|
|
109
107
|
<div class="meta">
|
|
110
108
|
<span>${esc(meta.projectName || "")}</span> ·
|
package/assets/favicon.svg
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="sg" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
-
<stop offset="0%" stop-color="#A78BFA"/>
|
|
5
|
-
<stop offset="100%" stop-color="#7C3AED"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="cg" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
-
<stop offset="0%" stop-color="#34D399"/>
|
|
9
|
-
<stop offset="100%" stop-color="#10B981"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
</defs>
|
|
12
|
-
<path d="M16 2 L28 7.5 C28 7.5 29 18 16 29.5 C3 18 4 7.5 4 7.5 Z" fill="url(#sg)"/>
|
|
13
|
-
<path d="M16 5 L25.5 9.5 C25.5 9.5 26.2 17.5 16 27 C5.8 17.5 6.5 9.5 6.5 9.5 Z" fill="#0C0A1A"/>
|
|
14
|
-
<path d="M11.5 15.5 L14.5 18.5 L20.5 12.5" stroke="url(#cg)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
15
|
-
</svg>
|
package/assets/logo.svg
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="shieldGrad" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
-
<stop offset="0%" stop-color="#A78BFA"/>
|
|
5
|
-
<stop offset="100%" stop-color="#7C3AED"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="innerGrad" x1="0" y1="0" x2="0" y2="1">
|
|
8
|
-
<stop offset="0%" stop-color="#1A1635"/>
|
|
9
|
-
<stop offset="100%" stop-color="#0C0A1A"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
<linearGradient id="checkGrad" x1="0" y1="0" x2="1" y2="1">
|
|
12
|
-
<stop offset="0%" stop-color="#34D399"/>
|
|
13
|
-
<stop offset="100%" stop-color="#10B981"/>
|
|
14
|
-
</linearGradient>
|
|
15
|
-
<filter id="glow">
|
|
16
|
-
<feGaussianBlur stdDeviation="8" result="blur"/>
|
|
17
|
-
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
18
|
-
</filter>
|
|
19
|
-
<filter id="innerShadow">
|
|
20
|
-
<feGaussianBlur stdDeviation="4" result="blur"/>
|
|
21
|
-
<feOffset dx="0" dy="2" result="offset"/>
|
|
22
|
-
<feComposite in="SourceGraphic" in2="offset" operator="over"/>
|
|
23
|
-
</filter>
|
|
24
|
-
</defs>
|
|
25
|
-
|
|
26
|
-
<!-- Outer glow -->
|
|
27
|
-
<path d="M256 28 L462 120 C462 120 474 300 256 484 C38 300 50 120 50 120 Z" fill="#A78BFA" opacity="0.15" filter="url(#glow)"/>
|
|
28
|
-
|
|
29
|
-
<!-- Shield body -->
|
|
30
|
-
<path d="M256 42 L448 128 C448 128 458 290 256 468 C54 290 64 128 64 128 Z" fill="url(#shieldGrad)" stroke="#C4B5FD" stroke-width="3"/>
|
|
31
|
-
|
|
32
|
-
<!-- Inner shield -->
|
|
33
|
-
<path d="M256 72 L420 146 C420 146 428 280 256 438 C84 280 92 146 92 146 Z" fill="url(#innerGrad)" stroke="#2D2852" stroke-width="2"/>
|
|
34
|
-
|
|
35
|
-
<!-- Checkmark circle background -->
|
|
36
|
-
<circle cx="256" cy="245" r="105" fill="#7C3AED" opacity="0.3"/>
|
|
37
|
-
<circle cx="256" cy="245" r="90" fill="none" stroke="#A78BFA" stroke-width="3" stroke-dasharray="6 4" opacity="0.5"/>
|
|
38
|
-
|
|
39
|
-
<!-- Main checkmark -->
|
|
40
|
-
<path d="M196 245 L236 285 L316 205" fill="none" stroke="url(#checkGrad)" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" filter="url(#innerShadow)"/>
|
|
41
|
-
|
|
42
|
-
<!-- Small decorative checkmarks (verified layers) -->
|
|
43
|
-
<path d="M152 175 L162 185 L182 165" fill="none" stroke="#34D399" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
|
|
44
|
-
<path d="M340 175 L350 185 L370 165" fill="none" stroke="#34D399" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
|
|
45
|
-
<path d="M152 315 L162 325 L182 305" fill="none" stroke="#34D399" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
|
|
46
|
-
<path d="M340 315 L350 325 L370 305" fill="none" stroke="#34D399" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
|
|
47
|
-
|
|
48
|
-
<!-- QA text -->
|
|
49
|
-
<text x="256" y="400" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="52" font-weight="800" fill="#C4B5FD" letter-spacing="8">QA</text>
|
|
50
|
-
|
|
51
|
-
<!-- Horizontal scan lines (inspection feel) -->
|
|
52
|
-
<line x1="130" y1="210" x2="170" y2="210" stroke="#A78BFA" stroke-width="2" opacity="0.3"/>
|
|
53
|
-
<line x1="342" y1="210" x2="382" y2="210" stroke="#A78BFA" stroke-width="2" opacity="0.3"/>
|
|
54
|
-
<line x1="130" y1="280" x2="170" y2="280" stroke="#A78BFA" stroke-width="2" opacity="0.3"/>
|
|
55
|
-
<line x1="342" y1="280" x2="382" y2="280" stroke="#A78BFA" stroke-width="2" opacity="0.3"/>
|
|
56
|
-
</svg>
|