@nhonh/qabot 1.2.1 → 2.0.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.2.1",
3
+ "version": "2.0.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": {
@@ -9,6 +9,7 @@ import { loadConfig } from "../../core/config.js";
9
9
  import { analyzeProject } from "../../analyzers/project-analyzer.js";
10
10
  import { AIEngine } from "../../ai/ai-engine.js";
11
11
  import { UseCaseParser } from "../../ai/usecase-parser.js";
12
+ import { runPreflight } from "../../e2e/preflight.js";
12
13
  import {
13
14
  ensurePlaywright,
14
15
  ensureE2EStructure,
@@ -70,6 +71,39 @@ async function runTest(feature, options) {
70
71
  console.log(DIM(" Browser: ") + W(options.headed ? "Headed" : "Headless"));
71
72
  logger.blank();
72
73
 
74
+ const preflightSpinner = ora({
75
+ text: V(" Analyzing project routes & auth..."),
76
+ spinner: "dots",
77
+ }).start();
78
+ let preflight;
79
+ try {
80
+ preflight = await runPreflight(projectDir, feature, config);
81
+ const authStatus = preflight.featureRequiresAuth
82
+ ? Y("requires auth")
83
+ : G("public");
84
+ preflightSpinner.succeed(
85
+ G(` Route analysis complete — ${feature}: ${authStatus}`),
86
+ );
87
+
88
+ if (preflight.featureRequiresAuth) {
89
+ logger.dim(` Sign-in URL: ${preflight.routes.authUrl}`);
90
+ logger.dim(
91
+ ` Credentials: ${preflight.credentials.email ? G("ready") : Y("will prompt")}`,
92
+ );
93
+ }
94
+ if (preflight.permissions.geolocation)
95
+ logger.dim(` Geolocation: ${G("mock granted (SF, CA)")}`);
96
+ } catch (err) {
97
+ preflightSpinner.succeed(G(" Route analysis done (partial)"));
98
+ preflight = {
99
+ routes: { private: [], public: [] },
100
+ credentials: {},
101
+ permissions: {},
102
+ featureRequiresAuth: false,
103
+ routeIntelligence: "",
104
+ };
105
+ }
106
+
73
107
  const spinner = ora({
74
108
  text: V(" Setting up Playwright..."),
75
109
  spinner: "dots",
@@ -77,7 +111,12 @@ async function runTest(feature, options) {
77
111
  try {
78
112
  await ensurePlaywright(projectDir);
79
113
  await ensureE2EStructure(projectDir);
80
- await writePlaywrightConfig(projectDir, config, baseUrl);
114
+ await writePlaywrightConfig(
115
+ projectDir,
116
+ config,
117
+ baseUrl,
118
+ preflight.permissions,
119
+ );
81
120
  await writeAuthHelper(projectDir, config);
82
121
  spinner.succeed(G(" Playwright ready"));
83
122
  } catch (err) {
@@ -168,8 +207,12 @@ async function runTest(feature, options) {
168
207
  baseUrl,
169
208
  sourceCode,
170
209
  route,
171
- routes: routesContext,
210
+ routeIntelligence: preflight.routeIntelligence,
172
211
  authProvider: config.auth?.provider || "none",
212
+ authUrl: preflight.routes.authUrl,
213
+ signupUrl: preflight.routes.signupUrl,
214
+ featureRequiresAuth: preflight.featureRequiresAuth,
215
+ permissions: preflight.permissions,
173
216
  useCases,
174
217
  });
175
218
  specFile = path.join(specDir, `${feature}.spec.js`);
@@ -1,4 +1,4 @@
1
- export const VERSION = "1.2.1";
1
+ export const VERSION = "2.0.0";
2
2
  export const TOOL_NAME = "qabot";
3
3
 
4
4
  export const PROJECT_TYPES = [
@@ -3,11 +3,7 @@ export function buildE2EPrompt(featureName, context) {
3
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.routes
7
- ? `\n## All Application Routes\n${context.routes}\n`
8
- : context.route
9
- ? `\n## Feature Route: ${context.route}\n`
10
- : "";
6
+ const routeIntelligence = context.routeIntelligence || "";
11
7
 
12
8
  const useCaseSection = context.useCases?.length
13
9
  ? `\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`
@@ -15,29 +11,40 @@ export function buildE2EPrompt(featureName, context) {
15
11
 
16
12
  const baseUrl = context.baseUrl || "http://localhost:3000";
17
13
  const route = context.route || "/" + featureName;
14
+ const requiresAuth = context.featureRequiresAuth !== false;
15
+ const authUrl = context.authUrl || "/signin";
16
+ const signupUrl = context.signupUrl || "/signup";
17
+ const hasGeolocation = context.permissions?.geolocation || false;
18
18
 
19
- return `You are an expert QA automation engineer designing REAL user journey tests.
19
+ return `You are an expert QA automation engineer. You deeply understand web app testing.
20
20
 
21
- ## Feature: ${featureName}
21
+ ## Feature Under Test: ${featureName}
22
22
  ## Base URL: ${baseUrl}
23
- ## Auth: ${context.authProvider || "none"}
24
- ${routeSection}${sourceSection}${useCaseSection}
23
+ ## Auth Provider: ${context.authProvider || "none"}
24
+ ## Feature Route: ${route}
25
+ ## Feature Requires Auth: ${requiresAuth ? "YES" : "NO"}
26
+ ## Sign In URL: ${baseUrl}${authUrl}
27
+ ## Sign Up URL: ${baseUrl}${signupUrl}
28
+ ## Geolocation Required: ${hasGeolocation ? "YES (already granted via Playwright config)" : "NO"}
25
29
 
26
- ## CRITICAL: This is a JOURNEY-BASED test, not a page screenshot test.
30
+ ${routeIntelligence}
31
+ ${sourceSection}${useCaseSection}
27
32
 
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
33
+ ## YOUR JOB: Write a REAL user journey test not a page screenshot test.
35
34
 
36
- ## ARCHITECTURE Single Browser, Serial Tests
35
+ A real QA engineer:
36
+ 1. Opens the app at ${baseUrl}/
37
+ 2. ${requiresAuth ? `Clicks "Sign In", enters email/password from env vars (process.env.E2E_TEST_EMAIL, process.env.E2E_TEST_PASSWORD), submits, waits for redirect` : "Directly navigates to the feature page"}
38
+ 3. Navigates to the feature: ${baseUrl}${route}
39
+ 4. Tests EVERY interaction: clicks buttons, reads data, verifies states
40
+ 5. Tests cross-page navigation (feature links to FAQ, account, etc.)
41
+ 6. Verifies error handling, loading states, empty states
42
+
43
+ ## ARCHITECTURE
37
44
 
38
45
  \`\`\`javascript
39
46
  const { test, expect } = require("@playwright/test");
40
- const { login } = require("../helpers/auth.js");
47
+ ${requiresAuth ? `const { login } = require("../helpers/auth.js");` : ""}
41
48
 
42
49
  test.describe.serial("${featureName} - User Journey", () => {
43
50
  let page;
@@ -50,68 +57,68 @@ test.describe.serial("${featureName} - User Journey", () => {
50
57
  if (page) await page.close();
51
58
  });
52
59
 
53
- // Tests use shared 'page' — same browser, same session, same cookies
54
- // Each test() does NOT take { page } argument
60
+ // ALL tests share this 'page' — single browser session
61
+ // Each test() does NOT take { page } argument — uses shared variable
55
62
  });
56
63
  \`\`\`
57
64
 
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)
65
+ ## TEST CASES Generate 10-15 sequential tests:
76
66
 
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?
86
-
87
- ## PLAYWRIGHT RULES:
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}":
67
+ ### Phase 1: App Entry${requiresAuth ? " & Login" : ""} (TC-01 to TC-0${requiresAuth ? "3" : "2"})
68
+ - TC-01: Navigate to ${baseUrl}/ verify landing page loads screenshot
69
+ ${
70
+ requiresAuth
71
+ ? `- TC-02: Login flow:
102
72
  \`\`\`
103
- const { login } = require("../helpers/auth.js");
104
- // In TC-02:
105
73
  await login(page, "${baseUrl}");
74
+ // login() navigates to ${baseUrl}/, clicks sign in, fills email/password from env, submits
75
+ // After login, verify user is redirected (URL no longer contains "signin")
76
+ await expect(page).not.toHaveURL(/signin/);
106
77
  \`\`\`
78
+ Take screenshot after login.
79
+ - TC-03: Verify authenticated state → look for user avatar, account menu, or logged-in indicator`
80
+ : `- TC-02: Verify page is accessible without login`
81
+ }
107
82
 
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()
83
+ ### Phase 2: Navigate to Feature (TC-0${requiresAuth ? "4" : "3"} to TC-0${requiresAuth ? "5" : "4"})
84
+ - Navigate to ${baseUrl}${route} via page.goto()
85
+ - Wait for networkidle
86
+ - Verify URL contains "${route}"
87
+ - Verify feature-specific content is visible (NOT loading spinner)
88
+ - Screenshot
89
+
90
+ ### Phase 3: Feature Interactions (TC-06 to TC-10)
91
+ READ THE SOURCE CODE CAREFULLY. Generate tests for ACTUAL elements:
92
+ - Buttons: what do they do when clicked? Test each one.
93
+ - Data display: verify numbers, text, images are correct
94
+ - Actions: share link, copy, claim rewards, submit form — test the real action
95
+ - Modals: do any actions open popups? Verify they appear and can be closed
96
+ - State changes: loading→loaded, button disabled→enabled, counter changes
114
97
 
115
- Return ONLY JavaScript code. No markdown fences. No explanation.
116
- Generate a COMPLETE spec file with 10-15 tests.`;
98
+ ### Phase 4: Cross-Page Navigation (TC-11 to TC-12)
99
+ - If feature has links to other pages, CLICK THEM and verify navigation
100
+ - Test going BACK to the feature page after navigating away
101
+ - Verify the feature state after returning
102
+
103
+ ### Phase 5: Responsive & Edge Cases (TC-13 to TC-15)
104
+ - Resize viewport to mobile (375x667) → verify layout adapts
105
+ - Refresh the page → verify data reloads
106
+ - Verify error states if applicable
107
+
108
+ ## RULES:
109
+ 1. \`test.describe.serial\` — tests run IN ORDER, sharing state
110
+ 2. \`let page\` at describe scope — NEVER use \`{ page }\` in test arguments
111
+ 3. \`test.beforeAll\` = \`browser.newPage()\` ONLY — login is a test case, not setup
112
+ 4. Use FULL URLs: \`page.goto("${baseUrl}${route}")\` — not relative paths
113
+ 5. After navigation: \`await page.waitForLoadState("networkidle")\`
114
+ 6. Screenshot every test: \`await page.screenshot({ path: "e2e/screenshots/${featureName}-TC{XX}.png", fullPage: true })\`
115
+ 7. Selectors: getByRole > getByText > getByTestId > locator (CSS last resort)
116
+ 8. Slow elements: \`{ timeout: 15000 }\`
117
+ 9. After clicks that navigate: \`await page.waitForLoadState("networkidle")\`
118
+ 10. Verify URLs after navigation: \`await expect(page).toHaveURL()\`
119
+ 11. Use \`try/catch\` for elements that might not exist (feature flags, A/B tests)
120
+ ${requiresAuth ? `12. Login uses helper: \`await login(page, "${baseUrl}");\` — credentials come from process.env` : ""}
121
+ ${hasGeolocation ? `13. Geolocation is pre-granted in Playwright config — no need to handle permission popup` : ""}
122
+
123
+ Return ONLY JavaScript code. No markdown fences. No explanation.`;
117
124
  }
@@ -47,6 +47,7 @@ export async function writePlaywrightConfig(
47
47
  projectDir,
48
48
  config,
49
49
  overrideBaseUrl,
50
+ permissions = {},
50
51
  ) {
51
52
  const configPath = path.join(projectDir, "e2e", "playwright.config.js");
52
53
 
@@ -83,7 +84,7 @@ module.exports = defineConfig({
83
84
  video: "retain-on-failure",
84
85
  ignoreHTTPSErrors: true,
85
86
  actionTimeout: 15000,
86
- navigationTimeout: 30000,
87
+ navigationTimeout: 30000,${permissions.geolocation ? `\n geolocation: { latitude: 37.7749, longitude: -122.4194 },\n permissions: ["geolocation"],` : ""}${permissions.notifications ? `\n permissions: [...(${permissions.geolocation ? '["geolocation"]' : "[]"}), "notifications"],` : ""}
87
88
  launchOptions: {
88
89
  slowMo: 100,
89
90
  },
@@ -0,0 +1,217 @@
1
+ import path from "node:path";
2
+ import Enquirer from "enquirer";
3
+ import chalk from "chalk";
4
+ import { findFiles, safeReadFile } from "../utils/file-utils.js";
5
+ import { logger } from "../core/logger.js";
6
+
7
+ const V = chalk.hex("#A78BFA");
8
+ const DIM = chalk.hex("#6B7280");
9
+ const W = chalk.hex("#F3F4F6");
10
+ const Y = chalk.hex("#FBBF24");
11
+ const G = chalk.hex("#34D399");
12
+
13
+ export async function runPreflight(projectDir, feature, config) {
14
+ const result = {
15
+ routes: { private: [], public: [], authUrl: null, signupUrl: null },
16
+ credentials: { email: "", password: "" },
17
+ permissions: { geolocation: false, notifications: false },
18
+ featureRequiresAuth: false,
19
+ routeIntelligence: "",
20
+ };
21
+
22
+ result.routes = await analyzeRoutes(projectDir);
23
+
24
+ const featureRoute = guessFeatureRoute(feature);
25
+ result.featureRequiresAuth = result.routes.private.some((r) =>
26
+ matchRoute(r, featureRoute),
27
+ );
28
+
29
+ result.routeIntelligence = buildRouteIntelligence(
30
+ result.routes,
31
+ feature,
32
+ featureRoute,
33
+ );
34
+
35
+ result.permissions = await detectPermissions(projectDir);
36
+
37
+ if (result.featureRequiresAuth) {
38
+ result.credentials = await resolveCredentials(config);
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ async function analyzeRoutes(projectDir) {
45
+ const routes = {
46
+ private: [],
47
+ public: [],
48
+ authUrl: "/signin",
49
+ signupUrl: "/signup",
50
+ };
51
+
52
+ const routeFiles = await findFiles(
53
+ projectDir,
54
+ "src/routes/*.{js,jsx,ts,tsx}",
55
+ );
56
+ for (const file of routeFiles) {
57
+ const content = await safeReadFile(file);
58
+ if (!content) continue;
59
+
60
+ const pathMatches = content.matchAll(/path:\s*["'`]([^"'`]+)["'`]/g);
61
+ const privateMatches = content.matchAll(
62
+ /path:\s*["'`]([^"'`]+)["'`][^}]*isPrivate:\s*true/gs,
63
+ );
64
+ const publicMatches = content.matchAll(
65
+ /path:\s*["'`]([^"'`]+)["'`][^}]*isPrivate:\s*false/gs,
66
+ );
67
+
68
+ for (const m of privateMatches) routes.private.push(m[1]);
69
+ for (const m of publicMatches) routes.public.push(m[1]);
70
+
71
+ const signInMatch = content.match(/pageSignIn:\s*["'`]([^"'`]+)["'`]/);
72
+ if (signInMatch) routes.authUrl = signInMatch[1];
73
+
74
+ const signUpMatch = content.match(/pageSignUp:\s*["'`]([^"'`]+)["'`]/);
75
+ if (signUpMatch) routes.signupUrl = signUpMatch[1];
76
+ }
77
+
78
+ if (routes.private.length === 0 && routes.public.length === 0) {
79
+ const allRouteFiles = await findFiles(
80
+ projectDir,
81
+ "src/**/{routes,router,routing}*.{js,jsx,ts,tsx}",
82
+ );
83
+ for (const file of allRouteFiles) {
84
+ const content = await safeReadFile(file);
85
+ if (!content) continue;
86
+ const matches = content.matchAll(/path:\s*["'`]([^"'`]+)["'`]/g);
87
+ for (const m of matches) routes.public.push(m[1]);
88
+ }
89
+ }
90
+
91
+ return routes;
92
+ }
93
+
94
+ async function detectPermissions(projectDir) {
95
+ const permissions = { geolocation: false, notifications: false };
96
+
97
+ const geoFiles = await findFiles(
98
+ projectDir,
99
+ "src/**/*{geo,location,radar}*.{js,jsx,ts,tsx}",
100
+ );
101
+ if (geoFiles.length > 0) permissions.geolocation = true;
102
+
103
+ const notifFiles = await findFiles(
104
+ projectDir,
105
+ "src/**/*{notification,push}*.{js,jsx,ts,tsx}",
106
+ );
107
+ if (notifFiles.length > 0) permissions.notifications = true;
108
+
109
+ return permissions;
110
+ }
111
+
112
+ async function resolveCredentials(config) {
113
+ const email =
114
+ process.env.E2E_TEST_EMAIL || config.auth?.credentials?.email || "";
115
+ const password =
116
+ process.env.E2E_TEST_PASSWORD || config.auth?.credentials?.password || "";
117
+
118
+ if (
119
+ email &&
120
+ password &&
121
+ !email.includes("ENV_") &&
122
+ !password.includes("ENV_")
123
+ ) {
124
+ return { email, password };
125
+ }
126
+
127
+ if (process.env.E2E_TEST_EMAIL && process.env.E2E_TEST_PASSWORD) {
128
+ return {
129
+ email: process.env.E2E_TEST_EMAIL,
130
+ password: process.env.E2E_TEST_PASSWORD,
131
+ };
132
+ }
133
+
134
+ logger.blank();
135
+ logger.warn("This feature requires authentication.");
136
+ logger.dim(
137
+ "Credentials not found in env vars (E2E_TEST_EMAIL, E2E_TEST_PASSWORD)",
138
+ );
139
+ logger.blank();
140
+
141
+ const enquirer = new Enquirer();
142
+ const answers = await enquirer.prompt([
143
+ {
144
+ type: "input",
145
+ name: "email",
146
+ message: V("\u25C6") + W(" Test account email"),
147
+ validate: (v) => (v.includes("@") ? true : "Enter a valid email"),
148
+ },
149
+ {
150
+ type: "password",
151
+ name: "password",
152
+ message: V("\u25C6") + W(" Test account password"),
153
+ validate: (v) => (v.length >= 1 ? true : "Password required"),
154
+ },
155
+ ]);
156
+
157
+ process.env.E2E_TEST_EMAIL = answers.email;
158
+ process.env.E2E_TEST_PASSWORD = answers.password;
159
+
160
+ logger.success("Credentials set for this session");
161
+ return { email: answers.email, password: answers.password };
162
+ }
163
+
164
+ function buildRouteIntelligence(routes, feature, featureRoute) {
165
+ const lines = [];
166
+
167
+ lines.push("## Route Intelligence (analyzed from source code)");
168
+ lines.push("");
169
+
170
+ if (routes.private.length > 0) {
171
+ lines.push("### Routes requiring authentication (isPrivate: true):");
172
+ routes.private.forEach((r) => lines.push(`- ${r}`));
173
+ lines.push("");
174
+ }
175
+
176
+ if (routes.public.length > 0) {
177
+ lines.push("### Public routes (no auth required):");
178
+ routes.public.slice(0, 15).forEach((r) => lines.push(`- ${r}`));
179
+ lines.push("");
180
+ }
181
+
182
+ lines.push(`### Auth URLs:`);
183
+ lines.push(`- Sign In: ${routes.authUrl || "/signin"}`);
184
+ lines.push(`- Sign Up: ${routes.signupUrl || "/signup"}`);
185
+ lines.push("");
186
+
187
+ const requiresAuth = routes.private.some((r) => matchRoute(r, featureRoute));
188
+ lines.push(`### Feature "${feature}" (${featureRoute}):`);
189
+ lines.push(
190
+ `- Requires auth: ${requiresAuth ? "YES — must login first" : "NO — public page"}`,
191
+ );
192
+ lines.push(
193
+ `- Test strategy: ${requiresAuth ? "Login → Navigate → Test" : "Direct access → Test"}`,
194
+ );
195
+ lines.push("");
196
+
197
+ return lines.join("\n");
198
+ }
199
+
200
+ function guessFeatureRoute(feature) {
201
+ const map = {
202
+ home: "/",
203
+ lobby: "/lobby",
204
+ auth: "/signin",
205
+ account: "/account",
206
+ redemption: "/redemption",
207
+ "refer-a-friend": "/refer-a-friend",
208
+ faq: "/faq",
209
+ promotions: "/promotions",
210
+ };
211
+ return map[feature] || `/${feature}`;
212
+ }
213
+
214
+ function matchRoute(routePattern, target) {
215
+ const normalized = routePattern.replace(/:[^/]+/g, "[^/]+");
216
+ return new RegExp(`^${normalized}$`).test(target) || routePattern === target;
217
+ }