laxy-verify 1.3.0 → 1.3.2
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/README.md +486 -474
- package/dist/ai-analysis.d.ts +28 -0
- package/dist/ai-analysis.js +32 -0
- package/dist/audit/broken-links.d.ts +25 -25
- package/dist/audit/broken-links.js +97 -97
- package/dist/badge.d.ts +2 -2
- package/dist/badge.js +18 -18
- package/dist/cli.js +1242 -1250
- package/dist/compare-env.d.ts +23 -0
- package/dist/compare-env.js +55 -0
- package/dist/config.d.ts +102 -102
- package/dist/config.js +360 -360
- package/dist/entitlement.d.ts +15 -15
- package/dist/entitlement.js +98 -98
- package/dist/init-analysis.d.ts +6 -0
- package/dist/init-analysis.js +302 -0
- package/dist/init.js +132 -132
- package/dist/lighthouse.d.ts +37 -37
- package/dist/lighthouse.js +231 -231
- package/dist/report-markdown.d.ts +53 -53
- package/dist/report-markdown.js +407 -407
- package/dist/route-discovery.d.ts +7 -0
- package/dist/route-discovery.js +108 -0
- package/dist/security-audit.d.ts +17 -17
- package/dist/security-audit.js +127 -127
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +36 -0
- package/dist/verification-core/report.js +526 -526
- package/dist/verification-core/types.d.ts +164 -164
- package/dist/visual-diff.d.ts +33 -33
- package/dist/visual-diff.js +213 -213
- package/package.json +1 -1
package/dist/entitlement.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
export interface EntitlementFeatures {
|
|
2
|
-
plan: string;
|
|
3
|
-
github_actions: boolean;
|
|
4
|
-
queue_priority: boolean;
|
|
5
|
-
parallel_execution: boolean;
|
|
6
|
-
ai_failure_analysis: boolean;
|
|
7
|
-
compare_env: boolean;
|
|
8
|
-
share_result: boolean;
|
|
9
|
-
history_trend: boolean;
|
|
10
|
-
}
|
|
11
|
-
export type TestablePlan = "free" | "pro" | "team";
|
|
12
|
-
export declare function normalizePlan(plan?: string | null): TestablePlan;
|
|
13
|
-
export declare function getEntitlements(): Promise<EntitlementFeatures>;
|
|
14
|
-
export declare function applyPlanOverride(features: EntitlementFeatures, overridePlan?: TestablePlan): EntitlementFeatures;
|
|
15
|
-
export declare function printPlanBanner(features: EntitlementFeatures): void;
|
|
1
|
+
export interface EntitlementFeatures {
|
|
2
|
+
plan: string;
|
|
3
|
+
github_actions: boolean;
|
|
4
|
+
queue_priority: boolean;
|
|
5
|
+
parallel_execution: boolean;
|
|
6
|
+
ai_failure_analysis: boolean;
|
|
7
|
+
compare_env: boolean;
|
|
8
|
+
share_result: boolean;
|
|
9
|
+
history_trend: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type TestablePlan = "free" | "pro" | "team";
|
|
12
|
+
export declare function normalizePlan(plan?: string | null): TestablePlan;
|
|
13
|
+
export declare function getEntitlements(): Promise<EntitlementFeatures>;
|
|
14
|
+
export declare function applyPlanOverride(features: EntitlementFeatures, overridePlan?: TestablePlan): EntitlementFeatures;
|
|
15
|
+
export declare function printPlanBanner(features: EntitlementFeatures): void;
|
package/dist/entitlement.js
CHANGED
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizePlan = normalizePlan;
|
|
4
|
-
exports.getEntitlements = getEntitlements;
|
|
5
|
-
exports.applyPlanOverride = applyPlanOverride;
|
|
6
|
-
exports.printPlanBanner = printPlanBanner;
|
|
7
|
-
/**
|
|
8
|
-
* Fetches account entitlements for laxy-verify.
|
|
9
|
-
*
|
|
10
|
-
* The CLI asks /api/v1/cli-entitlement for the current plan and enabled automation flags.
|
|
11
|
-
* Responses are cached briefly to avoid repeated network calls during a single run.
|
|
12
|
-
* If the request fails or the user is not logged in, the CLI safely falls back to the unlocked verification defaults.
|
|
13
|
-
*/
|
|
14
|
-
const auth_js_1 = require("./auth.js");
|
|
15
|
-
const FREE_FEATURES = {
|
|
16
|
-
plan: "free",
|
|
17
|
-
// Automation features — not available without login
|
|
18
|
-
github_actions: false,
|
|
19
|
-
queue_priority: false,
|
|
20
|
-
parallel_execution: false,
|
|
21
|
-
ai_failure_analysis: false,
|
|
22
|
-
compare_env: false,
|
|
23
|
-
share_result: false,
|
|
24
|
-
history_trend: false,
|
|
25
|
-
};
|
|
26
|
-
let cache = null;
|
|
27
|
-
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
28
|
-
const PLAN_RANK = {
|
|
29
|
-
free: 0,
|
|
30
|
-
pro: 1,
|
|
31
|
-
team: 2,
|
|
32
|
-
};
|
|
33
|
-
function normalizePlan(plan) {
|
|
34
|
-
if (plan === "pro")
|
|
35
|
-
return "pro";
|
|
36
|
-
if (plan === "team")
|
|
37
|
-
return "team";
|
|
38
|
-
return "free";
|
|
39
|
-
}
|
|
40
|
-
function getPlanRank(plan) {
|
|
41
|
-
return PLAN_RANK[plan ?? "free"] ?? 0;
|
|
42
|
-
}
|
|
43
|
-
async function getEntitlements() {
|
|
44
|
-
if (cache && Date.now() - cache.fetchedAt < CACHE_TTL_MS) {
|
|
45
|
-
return cache.features;
|
|
46
|
-
}
|
|
47
|
-
const token = (0, auth_js_1.loadToken)();
|
|
48
|
-
if (!token)
|
|
49
|
-
return FREE_FEATURES;
|
|
50
|
-
try {
|
|
51
|
-
const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/cli-entitlement`, {
|
|
52
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
53
|
-
});
|
|
54
|
-
if (!res.ok) {
|
|
55
|
-
if (res.status === 401) {
|
|
56
|
-
console.error(" Note: your CLI session is no longer valid. Run laxy-verify login again to refresh your account metadata.");
|
|
57
|
-
}
|
|
58
|
-
return FREE_FEATURES;
|
|
59
|
-
}
|
|
60
|
-
const features = (await res.json());
|
|
61
|
-
cache = { features, fetchedAt: Date.now() };
|
|
62
|
-
return features;
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return FREE_FEATURES;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function applyPlanOverride(features, overridePlan) {
|
|
69
|
-
if (!overridePlan)
|
|
70
|
-
return features;
|
|
71
|
-
const isTeam = overridePlan === "team";
|
|
72
|
-
const isPro = overridePlan === "pro" || overridePlan === "team";
|
|
73
|
-
return {
|
|
74
|
-
...features,
|
|
75
|
-
plan: overridePlan,
|
|
76
|
-
// All verification features run on every plan
|
|
77
|
-
// github_actions, ai_failure_analysis, compare_env
|
|
78
|
-
github_actions: isPro,
|
|
79
|
-
ai_failure_analysis: isPro,
|
|
80
|
-
compare_env: isPro,
|
|
81
|
-
share_result: isPro,
|
|
82
|
-
history_trend: isPro,
|
|
83
|
-
// queue_priority, parallel_execution — Team only
|
|
84
|
-
queue_priority: isTeam,
|
|
85
|
-
parallel_execution: isTeam,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function printPlanBanner(features) {
|
|
89
|
-
const planLabels = {
|
|
90
|
-
free: "Free",
|
|
91
|
-
pro: "Pro",
|
|
92
|
-
team: "Team",
|
|
93
|
-
};
|
|
94
|
-
const label = planLabels[features.plan] ?? features.plan;
|
|
95
|
-
if (features.plan !== "free") {
|
|
96
|
-
console.log(` Plan: ${label}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizePlan = normalizePlan;
|
|
4
|
+
exports.getEntitlements = getEntitlements;
|
|
5
|
+
exports.applyPlanOverride = applyPlanOverride;
|
|
6
|
+
exports.printPlanBanner = printPlanBanner;
|
|
7
|
+
/**
|
|
8
|
+
* Fetches account entitlements for laxy-verify.
|
|
9
|
+
*
|
|
10
|
+
* The CLI asks /api/v1/cli-entitlement for the current plan and enabled automation flags.
|
|
11
|
+
* Responses are cached briefly to avoid repeated network calls during a single run.
|
|
12
|
+
* If the request fails or the user is not logged in, the CLI safely falls back to the unlocked verification defaults.
|
|
13
|
+
*/
|
|
14
|
+
const auth_js_1 = require("./auth.js");
|
|
15
|
+
const FREE_FEATURES = {
|
|
16
|
+
plan: "free",
|
|
17
|
+
// Automation features — not available without login
|
|
18
|
+
github_actions: false,
|
|
19
|
+
queue_priority: false,
|
|
20
|
+
parallel_execution: false,
|
|
21
|
+
ai_failure_analysis: false,
|
|
22
|
+
compare_env: false,
|
|
23
|
+
share_result: false,
|
|
24
|
+
history_trend: false,
|
|
25
|
+
};
|
|
26
|
+
let cache = null;
|
|
27
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
28
|
+
const PLAN_RANK = {
|
|
29
|
+
free: 0,
|
|
30
|
+
pro: 1,
|
|
31
|
+
team: 2,
|
|
32
|
+
};
|
|
33
|
+
function normalizePlan(plan) {
|
|
34
|
+
if (plan === "pro")
|
|
35
|
+
return "pro";
|
|
36
|
+
if (plan === "team")
|
|
37
|
+
return "team";
|
|
38
|
+
return "free";
|
|
39
|
+
}
|
|
40
|
+
function getPlanRank(plan) {
|
|
41
|
+
return PLAN_RANK[plan ?? "free"] ?? 0;
|
|
42
|
+
}
|
|
43
|
+
async function getEntitlements() {
|
|
44
|
+
if (cache && Date.now() - cache.fetchedAt < CACHE_TTL_MS) {
|
|
45
|
+
return cache.features;
|
|
46
|
+
}
|
|
47
|
+
const token = (0, auth_js_1.loadToken)();
|
|
48
|
+
if (!token)
|
|
49
|
+
return FREE_FEATURES;
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/cli-entitlement`, {
|
|
52
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
if (res.status === 401) {
|
|
56
|
+
console.error(" Note: your CLI session is no longer valid. Run laxy-verify login again to refresh your account metadata.");
|
|
57
|
+
}
|
|
58
|
+
return FREE_FEATURES;
|
|
59
|
+
}
|
|
60
|
+
const features = (await res.json());
|
|
61
|
+
cache = { features, fetchedAt: Date.now() };
|
|
62
|
+
return features;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return FREE_FEATURES;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function applyPlanOverride(features, overridePlan) {
|
|
69
|
+
if (!overridePlan)
|
|
70
|
+
return features;
|
|
71
|
+
const isTeam = overridePlan === "team";
|
|
72
|
+
const isPro = overridePlan === "pro" || overridePlan === "team";
|
|
73
|
+
return {
|
|
74
|
+
...features,
|
|
75
|
+
plan: overridePlan,
|
|
76
|
+
// All verification features run on every plan
|
|
77
|
+
// github_actions, ai_failure_analysis, compare_env — Pro and Team
|
|
78
|
+
github_actions: isPro,
|
|
79
|
+
ai_failure_analysis: isPro,
|
|
80
|
+
compare_env: isPro,
|
|
81
|
+
share_result: isPro,
|
|
82
|
+
history_trend: isPro,
|
|
83
|
+
// queue_priority, parallel_execution — Team only
|
|
84
|
+
queue_priority: isTeam,
|
|
85
|
+
parallel_execution: isTeam,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function printPlanBanner(features) {
|
|
89
|
+
const planLabels = {
|
|
90
|
+
free: "Free",
|
|
91
|
+
pro: "Pro",
|
|
92
|
+
team: "Team",
|
|
93
|
+
};
|
|
94
|
+
const label = planLabels[features.plan] ?? features.plan;
|
|
95
|
+
if (features.plan !== "free") {
|
|
96
|
+
console.log(` Plan: ${label}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.analyzeProjectForInit = analyzeProjectForInit;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
40
|
+
const IGNORED_DIRS = new Set([
|
|
41
|
+
".git",
|
|
42
|
+
".next",
|
|
43
|
+
".turbo",
|
|
44
|
+
".vercel",
|
|
45
|
+
".output",
|
|
46
|
+
".laxy-verify",
|
|
47
|
+
"coverage",
|
|
48
|
+
"dist",
|
|
49
|
+
"build",
|
|
50
|
+
"node_modules",
|
|
51
|
+
]);
|
|
52
|
+
function isSourceFile(filePath) {
|
|
53
|
+
return SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
function normalizeRoute(route) {
|
|
56
|
+
const cleaned = route
|
|
57
|
+
.replace(/\\u002F/gi, "/")
|
|
58
|
+
.replace(/\\\//g, "/")
|
|
59
|
+
.trim();
|
|
60
|
+
if (!cleaned.startsWith("/"))
|
|
61
|
+
return null;
|
|
62
|
+
const normalized = cleaned.split("?")[0]?.split("#")[0]?.replace(/\/+/g, "/") ?? "/";
|
|
63
|
+
if (!normalized || normalized === "/")
|
|
64
|
+
return "/";
|
|
65
|
+
if (normalized.startsWith("/_next/") || normalized.startsWith("/api/"))
|
|
66
|
+
return null;
|
|
67
|
+
if (/[.*:[\]]/.test(normalized))
|
|
68
|
+
return null;
|
|
69
|
+
if (/\.[a-z0-9]{2,8}$/i.test(normalized))
|
|
70
|
+
return null;
|
|
71
|
+
if (/\s/.test(normalized))
|
|
72
|
+
return null;
|
|
73
|
+
return normalized.endsWith("/") && normalized !== "/" ? normalized.slice(0, -1) : normalized;
|
|
74
|
+
}
|
|
75
|
+
function collectSourceFiles(rootDir, currentDir = rootDir, acc = []) {
|
|
76
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (IGNORED_DIRS.has(entry.name))
|
|
81
|
+
continue;
|
|
82
|
+
collectSourceFiles(rootDir, fullPath, acc);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entry.isFile() && isSourceFile(fullPath)) {
|
|
86
|
+
acc.push(fullPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return acc;
|
|
90
|
+
}
|
|
91
|
+
function readFileSafe(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function routeFromNextAppFile(filePath, baseDir) {
|
|
100
|
+
const relative = path.relative(baseDir, filePath).replace(/\\/g, "/");
|
|
101
|
+
if (!relative.startsWith("app/"))
|
|
102
|
+
return null;
|
|
103
|
+
if (!/\/page\.(t|j)sx?$/.test(relative) && !relative.endsWith("/page.mdx"))
|
|
104
|
+
return null;
|
|
105
|
+
const segments = relative
|
|
106
|
+
.replace(/^app\//, "")
|
|
107
|
+
.replace(/\/page\.(t|j)sx?$/, "")
|
|
108
|
+
.replace(/\/page\.mdx$/, "")
|
|
109
|
+
.split("/")
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.filter((segment) => !segment.startsWith("("))
|
|
112
|
+
.filter((segment) => !segment.startsWith("@"))
|
|
113
|
+
.filter((segment) => segment !== "page")
|
|
114
|
+
.flatMap((segment) => {
|
|
115
|
+
if (segment === "index")
|
|
116
|
+
return [];
|
|
117
|
+
const cleaned = segment.replace(/\.(t|j)sx?$/, "");
|
|
118
|
+
if (cleaned.startsWith("(") || cleaned.startsWith("[["))
|
|
119
|
+
return [];
|
|
120
|
+
if (cleaned.startsWith("["))
|
|
121
|
+
return [];
|
|
122
|
+
return cleaned ? [cleaned] : [];
|
|
123
|
+
});
|
|
124
|
+
const route = `/${segments.join("/")}`;
|
|
125
|
+
return normalizeRoute(route);
|
|
126
|
+
}
|
|
127
|
+
function routeFromNextPagesFile(filePath, baseDir) {
|
|
128
|
+
const relative = path.relative(baseDir, filePath).replace(/\\/g, "/");
|
|
129
|
+
if (!relative.startsWith("pages/"))
|
|
130
|
+
return null;
|
|
131
|
+
if (!/\.(t|j)sx?$/.test(relative))
|
|
132
|
+
return null;
|
|
133
|
+
if (/^pages\/api\//.test(relative))
|
|
134
|
+
return null;
|
|
135
|
+
const withoutExt = relative.replace(/^pages\//, "").replace(/\.(t|j)sx?$/, "");
|
|
136
|
+
const segments = withoutExt
|
|
137
|
+
.split("/")
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.filter((segment) => segment !== "index")
|
|
140
|
+
.filter((segment) => !segment.startsWith("["));
|
|
141
|
+
const route = `/${segments.join("/")}`;
|
|
142
|
+
return normalizeRoute(route);
|
|
143
|
+
}
|
|
144
|
+
function collectRoutesFromFiles(rootDir, files) {
|
|
145
|
+
const routes = new Map();
|
|
146
|
+
for (const filePath of files) {
|
|
147
|
+
const nextAppRoute = routeFromNextAppFile(filePath, rootDir);
|
|
148
|
+
if (nextAppRoute)
|
|
149
|
+
routes.set(nextAppRoute, filePath);
|
|
150
|
+
const nextPagesRoute = routeFromNextPagesFile(filePath, rootDir);
|
|
151
|
+
if (nextPagesRoute)
|
|
152
|
+
routes.set(nextPagesRoute, filePath);
|
|
153
|
+
const content = readFileSafe(filePath);
|
|
154
|
+
if (!content)
|
|
155
|
+
continue;
|
|
156
|
+
const routeRegexes = [
|
|
157
|
+
/(?:path|to|href|router\.push|navigate)\s*\(?\s*[:=]?\s*["'`]((?:\/(?!\/)[^"'`?#]+))["'`]/g,
|
|
158
|
+
/<Route[^>]*path=["'`]((?:\/(?!\/)[^"'`?#]+))["'`]/g,
|
|
159
|
+
];
|
|
160
|
+
for (const regex of routeRegexes) {
|
|
161
|
+
for (const match of content.matchAll(regex)) {
|
|
162
|
+
const route = normalizeRoute(match[1] ?? "");
|
|
163
|
+
if (route)
|
|
164
|
+
routes.set(route, filePath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return Array.from(routes.entries())
|
|
169
|
+
.map(([route, source]) => ({ route, source }))
|
|
170
|
+
.sort((a, b) => a.route.localeCompare(b.route));
|
|
171
|
+
}
|
|
172
|
+
function buildSelectorHints(files) {
|
|
173
|
+
const contents = files.map(readFileSafe).join("\n");
|
|
174
|
+
const has = (pattern) => pattern.test(contents);
|
|
175
|
+
const dataTestIds = Array.from(contents.matchAll(/data-testid=["'`]([^"'`]+)["'`]/g)).map((match) => match[1]);
|
|
176
|
+
const dashboardDataTestId = dataTestIds.find((id) => /(dashboard|workspace|overview|app)/i.test(id));
|
|
177
|
+
const authDataTestId = dataTestIds.find((id) => /(login|signin|auth|account)/i.test(id));
|
|
178
|
+
return {
|
|
179
|
+
email: has(/name=["'`]email["'`]/i)
|
|
180
|
+
? "input[name=email]"
|
|
181
|
+
: has(/type=["'`]email["'`]/i)
|
|
182
|
+
? "input[type=email]"
|
|
183
|
+
: "input[name=email]",
|
|
184
|
+
password: has(/name=["'`](password|passcode)["'`]/i)
|
|
185
|
+
? "input[name=password]"
|
|
186
|
+
: has(/type=["'`]password["'`]/i)
|
|
187
|
+
? "input[type=password]"
|
|
188
|
+
: "input[name=password]",
|
|
189
|
+
submit: has(/type=["'`]submit["'`]/i)
|
|
190
|
+
? "button[type=submit]"
|
|
191
|
+
: has(/(sign in|login|continue|submit)/i)
|
|
192
|
+
? "button"
|
|
193
|
+
: "button[type=submit]",
|
|
194
|
+
search: has(/name=["'`](q|query|search)["'`]/i)
|
|
195
|
+
? "input[name=search]"
|
|
196
|
+
: has(/placeholder=["'`][^"'`]*(search|검색)[^"'`]*["'`]/i)
|
|
197
|
+
? "input[type=search]"
|
|
198
|
+
: "input[type=search]",
|
|
199
|
+
primaryInput: has(/textarea/i) ? "textarea" : "input",
|
|
200
|
+
dashboardVisible: dashboardDataTestId ? `[data-testid=${dashboardDataTestId}]` : "main",
|
|
201
|
+
authVisible: authDataTestId ? `[data-testid=${authDataTestId}]` : "main",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function pickFirstRoute(routes, patterns) {
|
|
205
|
+
return routes.find((route) => patterns.some((pattern) => pattern.test(route)));
|
|
206
|
+
}
|
|
207
|
+
function uniqueRoutes(routes) {
|
|
208
|
+
const seen = new Set();
|
|
209
|
+
const result = [];
|
|
210
|
+
for (const route of routes) {
|
|
211
|
+
if (!route)
|
|
212
|
+
continue;
|
|
213
|
+
const normalized = normalizeRoute(route);
|
|
214
|
+
if (!normalized || normalized === "/" || seen.has(normalized))
|
|
215
|
+
continue;
|
|
216
|
+
seen.add(normalized);
|
|
217
|
+
result.push(normalized);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
function analyzeProjectForInit(dir) {
|
|
222
|
+
const files = collectSourceFiles(dir);
|
|
223
|
+
const routeEntries = collectRoutesFromFiles(dir, files);
|
|
224
|
+
const routes = uniqueRoutes(routeEntries.map((entry) => entry.route));
|
|
225
|
+
const selectors = buildSelectorHints(files);
|
|
226
|
+
const loginRoute = pickFirstRoute(routes, [/\/login$/, /\/signin$/, /\/sign-in$/, /\/auth/]);
|
|
227
|
+
const dashboardRoute = pickFirstRoute(routes, [/\/dashboard$/, /\/app$/, /\/workspace$/, /\/overview$/, /\/home$/, /\/projects?$/]);
|
|
228
|
+
const signupRoute = pickFirstRoute(routes, [/\/signup$/, /\/register$/, /\/join$/, /\/sign-up$/]);
|
|
229
|
+
const searchRoute = pickFirstRoute(routes, [/\/search$/, /\/discover$/, /\/explore$/, /\/products$/, /\/items$/]);
|
|
230
|
+
const settingsRoute = pickFirstRoute(routes, [/\/settings$/, /\/profile$/, /\/account$/, /\/billing$/]);
|
|
231
|
+
const scenarios = [];
|
|
232
|
+
if (loginRoute) {
|
|
233
|
+
scenarios.push({
|
|
234
|
+
name: "로그인 후 보호 페이지 접근",
|
|
235
|
+
steps: [
|
|
236
|
+
{ goto: loginRoute },
|
|
237
|
+
{ fill: selectors.email, with: "test@example.com" },
|
|
238
|
+
{ fill: selectors.password, with: "testpass123!" },
|
|
239
|
+
{ click: selectors.submit },
|
|
240
|
+
dashboardRoute
|
|
241
|
+
? { goto: dashboardRoute }
|
|
242
|
+
: { wait: 1200 },
|
|
243
|
+
{ expect_visible: dashboardRoute ? selectors.dashboardVisible : "body" },
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (signupRoute && scenarios.length < 3) {
|
|
248
|
+
scenarios.push({
|
|
249
|
+
name: "회원가입 주요 폼 제출",
|
|
250
|
+
steps: [
|
|
251
|
+
{ goto: signupRoute },
|
|
252
|
+
{ fill: selectors.email, with: "test@example.com" },
|
|
253
|
+
{ fill: selectors.password, with: "testpass123!" },
|
|
254
|
+
{ click: selectors.submit },
|
|
255
|
+
{ expect_visible: selectors.authVisible || "body" },
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (searchRoute && scenarios.length < 3) {
|
|
260
|
+
scenarios.push({
|
|
261
|
+
name: "핵심 탐색 흐름 확인",
|
|
262
|
+
steps: [
|
|
263
|
+
{ goto: searchRoute },
|
|
264
|
+
{ fill: selectors.search, with: "test query" },
|
|
265
|
+
{ click: selectors.submit },
|
|
266
|
+
{ expect_visible: "body" },
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (settingsRoute && scenarios.length < 3) {
|
|
271
|
+
scenarios.push({
|
|
272
|
+
name: "설정 화면 렌더 확인",
|
|
273
|
+
steps: [
|
|
274
|
+
{ goto: settingsRoute },
|
|
275
|
+
{ expect_visible: "body" },
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (dashboardRoute && scenarios.length < 3) {
|
|
280
|
+
scenarios.push({
|
|
281
|
+
name: "대시보드 기본 렌더",
|
|
282
|
+
steps: [
|
|
283
|
+
{ goto: dashboardRoute },
|
|
284
|
+
{ expect_visible: selectors.dashboardVisible || "main" },
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (scenarios.length === 0) {
|
|
289
|
+
const firstRoute = routes[0] ?? "/";
|
|
290
|
+
scenarios.push({
|
|
291
|
+
name: "핵심 페이지 기본 렌더",
|
|
292
|
+
steps: [
|
|
293
|
+
{ goto: firstRoute },
|
|
294
|
+
{ expect_visible: "body" },
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
scenarios: scenarios.slice(0, 3),
|
|
300
|
+
extraRoutes: routes.slice(0, 10),
|
|
301
|
+
};
|
|
302
|
+
}
|