@nhonh/qabot 1.0.2 → 1.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nhonh/qabot",
3
- "version": "1.0.2",
3
+ "version": "1.2.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
  }
@@ -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) {
@@ -153,11 +153,22 @@ async function runTest(feature, options) {
153
153
 
154
154
  const route = featureConfig?.route || guessRoute(feature);
155
155
 
156
+ let routesContext = "";
157
+ const routeFiles = await findFiles(
158
+ projectDir,
159
+ "src/routes/*.{js,jsx,ts,tsx}",
160
+ );
161
+ if (routeFiles.length > 0) {
162
+ const routeContent = await safeReadFile(routeFiles[0]);
163
+ if (routeContent) routesContext = routeContent.slice(0, 3000);
164
+ }
165
+
156
166
  try {
157
167
  const spec = await generateE2ESpec(ai, feature, {
158
168
  baseUrl,
159
169
  sourceCode,
160
170
  route,
171
+ routes: routesContext,
161
172
  authProvider: config.auth?.provider || "none",
162
173
  useCases,
163
174
  });
@@ -277,6 +288,7 @@ function runPlaywrightStreaming(projectDir, specFile, options, baseUrl) {
277
288
  `--config=${configPath}`,
278
289
  "--project=chromium",
279
290
  "--reporter=line",
291
+ "--timeout=60000",
280
292
  );
281
293
  if (options.headed) args.push("--headed");
282
294
 
@@ -1,4 +1,4 @@
1
- export const VERSION = "1.0.2";
1
+ export const VERSION = "1.2.0";
2
2
  export const TOOL_NAME = "qabot";
3
3
 
4
4
  export const PROJECT_TYPES = [
@@ -30,12 +30,24 @@ ${code}
30
30
  - Base URL: ${context.baseUrl || "http://localhost:3000"}
31
31
  - Auth: ${context.authProvider || "none"}
32
32
 
33
- ## Common Playwright fixes
34
- 1. Wrong selectoruse getByRole, getByText, getByTestId instead of CSS selectors
35
- 2. Timingadd waitForLoadState("networkidle") or waitForSelector
36
- 3. Element not visible scroll into view or wait for animation
37
- 4. Navigation ensure page.goto uses correct path
38
- 5. Auth ensure login completed before testing
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 via \`browser.newPage()\` ONLY
37
+ 4. \`test.afterAll\` closes page ONCE
38
+ 5. Each \`test()\` does NOT take \`{ page }\` argument — uses shared \`page\`
39
+ 6. Tests are SEQUENTIAL JOURNEY — TC-01 opens landing, TC-02 logs in, TC-03+ navigate and test
40
+ 7. Login happens inside a test (TC-02), NOT in beforeAll
41
+
42
+ ## Common fixes
43
+ 1. Wrong selector — use getByRole, getByText, getByTestId, or .first() for multiple matches
44
+ 2. Timing — add waitForLoadState("networkidle") or waitForTimeout(2000) after navigation
45
+ 3. Element not visible — scroll into view, wait for animation, check if inside modal/iframe
46
+ 4. Navigation — use FULL URL (e.g. "${context.baseUrl}/refer-a-friend") not relative paths
47
+ 5. Auth — if login fails, wrap in try/catch and skip dependent tests gracefully
48
+ 6. Timeout — increase to { timeout: 15000 } or { timeout: 30000 } for slow pages
49
+ 7. Multiple matches — use .first() or more specific parent: page.locator(".section").getByText()
50
+ 8. Page changed — after clicking navigation links, await page.waitForURL() before assertions
39
51
 
40
52
  Return the COMPLETE fixed test file. No markdown fences.`;
41
53
 
@@ -1,67 +1,117 @@
1
1
  export function buildE2EPrompt(featureName, context) {
2
2
  const sourceSection = context.sourceCode
3
- ? `\n## Source Code Analysis\nUse this to understand the page structure, components, routes, buttons, forms, modals, and data flow:\n\`\`\`\n${context.sourceCode.slice(0, 8000)}\n\`\`\`\n`
3
+ ? `\n## Source Code (analyze navigation, hooks, API calls, user actions, conditions)\n\`\`\`\n${context.sourceCode.slice(0, 10000)}\n\`\`\`\n`
4
4
  : "";
5
5
 
6
- const routeSection = context.route
7
- ? `\n## Page Route: ${context.route}\n`
8
- : "";
6
+ const routeSection = context.routes
7
+ ? `\n## All Application Routes\n${context.routes}\n`
8
+ : context.route
9
+ ? `\n## Feature Route: ${context.route}\n`
10
+ : "";
9
11
 
10
12
  const useCaseSection = context.useCases?.length
11
- ? `\n## QA Use Cases (from QA team)\n${context.useCases.map((uc) => `### ${uc.scenario}\n${uc.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}`).join("\n\n")}\n`
13
+ ? `\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
14
  : "";
13
15
 
14
- return `You are an expert QA automation engineer. Write comprehensive Playwright E2E tests.
16
+ const baseUrl = context.baseUrl || "http://localhost:3000";
17
+ const route = context.route || "/" + featureName;
18
+
19
+ return `You are an expert QA automation engineer designing REAL user journey tests.
15
20
 
16
21
  ## Feature: ${featureName}
17
- ## Base URL: ${context.baseUrl || "http://localhost:3000"}
18
- ## Auth: ${context.authProvider || "none"} (use auth helper if needed)
22
+ ## Base URL: ${baseUrl}
23
+ ## Auth: ${context.authProvider || "none"}
19
24
  ${routeSection}${sourceSection}${useCaseSection}
20
25
 
21
- ## REQUIREMENTS Generate AT LEAST 8 test cases covering:
26
+ ## CRITICAL: This is a JOURNEY-BASED test, not a page screenshot test.
27
+
28
+ You MUST test REAL USER FLOWS across multiple pages. A user does NOT just open one page — they:
29
+ 1. Land on the app → see landing/home page
30
+ 2. Sign in or are already signed in
31
+ 3. Navigate to the feature via menu/link/URL
32
+ 4. Interact with the feature (click buttons, fill forms, read data)
33
+ 5. Feature may redirect them to other pages (FAQ, account, modals)
34
+ 6. Come back, verify state changed
35
+
36
+ ## ARCHITECTURE — Single Browser, Serial Tests
37
+
38
+ \`\`\`javascript
39
+ const { test, expect } = require("@playwright/test");
40
+ const { login } = require("../helpers/auth.js");
41
+
42
+ test.describe.serial("${featureName} - User Journey", () => {
43
+ let page;
44
+
45
+ test.beforeAll(async ({ browser }) => {
46
+ page = await browser.newPage();
47
+ });
22
48
 
23
- ### Must Include (P0 Critical):
24
- 1. Page Load & Layout — verify page loads, key sections visible, no console errors
25
- 2. Navigation — verify URL is correct, breadcrumbs/tabs work
26
- 3. Primary User Action — the main thing a user does on this page (click button, submit form, etc)
27
- 4. Data Display — verify data renders correctly (lists, cards, tables, amounts)
49
+ test.afterAll(async () => {
50
+ if (page) await page.close();
51
+ });
28
52
 
29
- ### Should Include (P1Important):
30
- 5. Secondary Actions other clickable elements, links, toggles
31
- 6. Error States — what happens when something goes wrong (empty data, network error)
32
- 7. Loading States — skeleton/spinner shows while data loads
33
- 8. Responsive/Mobile — if applicable, test viewport changes
53
+ // Tests use shared 'page' same browser, same session, same cookies
54
+ // Each test() does NOT take { page } argument
55
+ });
56
+ \`\`\`
34
57
 
35
- ### Nice to Have (P2 Edge Cases):
36
- 9. Edge Cases — empty states, boundary values, special characters
37
- 10. Authentication Guards verify redirects when not logged in
58
+ ## GENERATE 10-15 TEST CASES as a SEQUENTIAL USER JOURNEY:
59
+
60
+ ### Phase 1: Entry & Authentication (TC-01 to TC-03)
61
+ - TC-01: Open app landing page → verify it loads → screenshot
62
+ - TC-02: Sign in flow → click sign in → fill credentials → submit → verify redirect to lobby/home
63
+ - TC-03: Verify authenticated state → user menu visible, correct username/avatar
64
+
65
+ ### Phase 2: Navigation to Feature (TC-04 to TC-05)
66
+ - TC-04: Navigate to "${featureName}" → via menu, link, or direct URL "${route}" → verify page loads
67
+ - TC-05: Verify feature page layout → key sections, headers, data loaded (not empty/loading)
68
+
69
+ ### Phase 3: Core Feature Interactions (TC-06 to TC-10)
70
+ READ THE SOURCE CODE and generate tests for the ACTUAL user interactions:
71
+ - What buttons does the page have? Test clicking them.
72
+ - What data does the page display? Verify it shows correctly.
73
+ - What actions can the user take? (share, copy, claim, submit, toggle, select)
74
+ - What modals/popups appear? Verify they open and close.
75
+ - What state changes happen? (loading → loaded, button text changes, counters update)
76
+
77
+ ### Phase 4: Cross-Page Navigation (TC-11 to TC-12)
78
+ - Does the feature link to other pages? (FAQ, account, game details, etc.)
79
+ - Test navigating AWAY and coming BACK — verify state is preserved or reset correctly
80
+ - Test browser back button behavior
81
+
82
+ ### Phase 5: Edge Cases & Cleanup (TC-13 to TC-15)
83
+ - Test with different viewport (mobile: 375x667)
84
+ - Test error states if possible (disconnect, slow network)
85
+ - Verify page after refresh (F5) — does state persist?
38
86
 
39
87
  ## PLAYWRIGHT RULES:
40
- 1. Use \`const { test, expect } = require("@playwright/test");\`
41
- 2. ALWAYS start with \`test.describe("${featureName}", () => { ... })\`
42
- 3. Use \`test.beforeEach\` for common setup (login + navigate)
43
- 4. Use accessible selectors IN THIS ORDER of preference:
44
- - \`page.getByRole("button", { name: /text/i })\`
45
- - \`page.getByText(/text/i)\`
46
- - \`page.getByTestId("id")\`
47
- - \`page.locator("css-selector")\` (LAST resort)
48
- 5. After every navigation: \`await page.waitForLoadState("networkidle")\`
49
- 6. Take screenshot at EVERY test: \`await page.screenshot({ path: "e2e/screenshots/${featureName}-{testname}.png", fullPage: true })\`
50
- 7. Use \`expect(locator).toBeVisible()\` not \`toBeTruthy()\`
51
- 8. Use \`{ timeout: 15000 }\` for slow-loading elements
52
- 9. Handle auth:
53
- \`\`\`
54
- const { login } = require("../helpers/auth.js");
55
- test.beforeEach(async ({ page, baseURL }) => {
56
- await login(page, baseURL);
57
- await page.goto("${context.route || "/" + featureName}");
58
- await page.waitForLoadState("networkidle");
59
- });
60
- \`\`\`
61
- 10. Each test MUST have a clear assertion with \`expect()\`
62
- 11. Add \`test.slow()\` for tests that need extra time
63
-
64
- ## OUTPUT FORMAT:
88
+ 1. Use \`test.describe.serial\` all tests share ONE page
89
+ 2. \`let page\` at describe scope shared across ALL tests
90
+ 3. \`test.beforeAll\` \`browser.newPage()\` ONLY (login happens in TC-02)
91
+ 4. Each \`test()\` does NOT take \`{ page }\` argument
92
+ 5. Use \`page.goto("${baseUrl}${route}")\` with FULL URL for navigation
93
+ 6. After navigation: \`await page.waitForLoadState("networkidle")\`
94
+ 7. Screenshot EVERY test: \`await page.screenshot({ path: "e2e/screenshots/${featureName}-TC{XX}.png", fullPage: true })\`
95
+ 8. Selectors priority: \`getByRole()\` > \`getByText()\` > \`getByTestId()\` > \`locator()\`
96
+ 9. For slow elements: \`{ timeout: 15000 }\`
97
+ 10. For navigation between pages, always verify URL changed: \`expect(page).toHaveURL()\`
98
+ 11. After clicking links/buttons that navigate: \`await page.waitForLoadState("networkidle")\`
99
+ 12. For modals: \`await expect(page.getByRole("dialog")).toBeVisible()\`
100
+ 13. Use \`try/catch\` for optional elements that may not exist (feature flags, A/B tests)
101
+ 14. If auth is "${context.authProvider}":
102
+ \`\`\`
103
+ const { login } = require("../helpers/auth.js");
104
+ // In TC-02:
105
+ await login(page, "${baseUrl}");
106
+ \`\`\`
107
+
108
+ ## IMPORTANT:
109
+ - Tests are SEQUENTIAL TC-02 login result carries to TC-03, TC-04, etc.
110
+ - Do NOT navigate to "${route}" in beforeAll — let TC-01 to TC-04 handle the journey
111
+ - TC-01 starts at "${baseUrl}/" (landing page)
112
+ - Generate MEANINGFUL assertions, not just "page exists"
113
+ - Each test must VERIFY something specific with expect()
114
+
65
115
  Return ONLY JavaScript code. No markdown fences. No explanation.
66
- Generate a COMPLETE runnable spec file with 8-12 tests.`;
116
+ Generate a COMPLETE spec file with 10-15 tests.`;
67
117
  }
@@ -43,9 +43,12 @@ export async function ensureE2EStructure(projectDir) {
43
43
  }
44
44
  }
45
45
 
46
- export async function writePlaywrightConfig(projectDir, config) {
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: 1,
71
+ retries: 0,
65
72
  workers: 1,
73
+ timeout: 90000,
74
+ expect: { timeout: 15000 },
66
75
  reporter: [
67
- ["html", { open: "never", outputFolder: "../qabot-reports/playwright" }],
76
+ ["line"],
68
77
  ["json", { outputFile: "../qabot-reports/playwright/results.json" }],
69
78
  ],
70
79
  use: {
71
- baseURL: ENV_URLS[process.env.E2E_ENV || "default"] || "http://localhost:3000",
80
+ baseURL: ${baseUrlExpr},
72
81
  trace: "on-first-retry",
73
82
  screenshot: "on",
83
+ video: "retain-on-failure",
74
84
  ignoreHTTPSErrors: true,
75
- actionTimeout: 10000,
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> &middot;
@@ -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>