slapify 0.0.16 → 0.0.18
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 +38 -4
- package/dist/ai/interpreter.js +1 -331
- package/dist/browser/agent.js +1 -485
- package/dist/cli.js +1 -1553
- package/dist/config/loader.js +1 -305
- package/dist/index.js +1 -262
- package/dist/parser/flow.js +1 -117
- package/dist/perf/audit.js +1 -635
- package/dist/report/generator.js +1 -641
- package/dist/runner/index.js +1 -744
- package/dist/task/index.js +1 -4
- package/dist/task/report.js +1 -740
- package/dist/task/runner.js +1 -1362
- package/dist/task/session.js +1 -153
- package/dist/task/tools.d.ts +12 -0
- package/dist/task/tools.js +1 -258
- package/dist/task/types.d.ts +18 -0
- package/dist/task/types.js +1 -2
- package/dist/types.js +1 -2
- package/package.json +6 -3
- package/dist/ai/interpreter.d.ts.map +0 -1
- package/dist/ai/interpreter.js.map +0 -1
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/parser/flow.d.ts.map +0 -1
- package/dist/parser/flow.js.map +0 -1
- package/dist/perf/audit.d.ts.map +0 -1
- package/dist/perf/audit.js.map +0 -1
- package/dist/report/generator.d.ts.map +0 -1
- package/dist/report/generator.js.map +0 -1
- package/dist/runner/index.d.ts.map +0 -1
- package/dist/runner/index.js.map +0 -1
- package/dist/task/index.d.ts.map +0 -1
- package/dist/task/index.js.map +0 -1
- package/dist/task/report.d.ts.map +0 -1
- package/dist/task/report.js.map +0 -1
- package/dist/task/runner.d.ts.map +0 -1
- package/dist/task/runner.js.map +0 -1
- package/dist/task/session.d.ts.map +0 -1
- package/dist/task/session.js.map +0 -1
- package/dist/task/tools.d.ts.map +0 -1
- package/dist/task/tools.js.map +0 -1
- package/dist/task/types.d.ts.map +0 -1
- package/dist/task/types.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/config/loader.js
CHANGED
|
@@ -1,305 +1 @@
|
|
|
1
|
-
import fs from "
|
|
2
|
-
import path from "path";
|
|
3
|
-
import yaml from "yaml";
|
|
4
|
-
const CONFIG_DIR = ".slapify";
|
|
5
|
-
const CONFIG_FILE = "config.yaml";
|
|
6
|
-
const CREDENTIALS_FILE = "credentials.yaml";
|
|
7
|
-
/**
|
|
8
|
-
* Find the config directory by walking up from cwd
|
|
9
|
-
*/
|
|
10
|
-
function findConfigDir(startDir = process.cwd()) {
|
|
11
|
-
let currentDir = startDir;
|
|
12
|
-
while (currentDir !== path.dirname(currentDir)) {
|
|
13
|
-
const configPath = path.join(currentDir, CONFIG_DIR);
|
|
14
|
-
if (fs.existsSync(configPath)) {
|
|
15
|
-
return configPath;
|
|
16
|
-
}
|
|
17
|
-
currentDir = path.dirname(currentDir);
|
|
18
|
-
}
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Replace environment variables in string values
|
|
23
|
-
*/
|
|
24
|
-
function replaceEnvVars(obj) {
|
|
25
|
-
if (typeof obj === "string") {
|
|
26
|
-
return obj.replace(/\$\{(\w+)\}/g, (_, key) => process.env[key] || "");
|
|
27
|
-
}
|
|
28
|
-
if (Array.isArray(obj)) {
|
|
29
|
-
return obj.map(replaceEnvVars);
|
|
30
|
-
}
|
|
31
|
-
if (obj && typeof obj === "object") {
|
|
32
|
-
const result = {};
|
|
33
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
34
|
-
result[key] = replaceEnvVars(value);
|
|
35
|
-
}
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
return obj;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Load and parse the main config file
|
|
42
|
-
*/
|
|
43
|
-
export function loadConfig(configDir) {
|
|
44
|
-
const dir = configDir || findConfigDir();
|
|
45
|
-
if (!dir) {
|
|
46
|
-
throw new Error('No .slapify directory found. Run "slapify init" to create one.');
|
|
47
|
-
}
|
|
48
|
-
const configPath = path.join(dir, CONFIG_FILE);
|
|
49
|
-
if (!fs.existsSync(configPath)) {
|
|
50
|
-
throw new Error(`Config file not found: ${configPath}`);
|
|
51
|
-
}
|
|
52
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
53
|
-
const config = yaml.parse(content);
|
|
54
|
-
// Replace environment variables
|
|
55
|
-
const resolved = replaceEnvVars(config);
|
|
56
|
-
// Apply defaults
|
|
57
|
-
return {
|
|
58
|
-
...resolved,
|
|
59
|
-
browser: {
|
|
60
|
-
headless: true,
|
|
61
|
-
timeout: 30000,
|
|
62
|
-
viewport: { width: 1280, height: 720 },
|
|
63
|
-
...resolved.browser,
|
|
64
|
-
},
|
|
65
|
-
report: {
|
|
66
|
-
format: "markdown",
|
|
67
|
-
screenshots: true,
|
|
68
|
-
output_dir: "./test-reports",
|
|
69
|
-
...resolved.report,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Load and parse the credentials file
|
|
75
|
-
*/
|
|
76
|
-
export function loadCredentials(configDir) {
|
|
77
|
-
const dir = configDir || findConfigDir();
|
|
78
|
-
if (!dir) {
|
|
79
|
-
return { profiles: {} };
|
|
80
|
-
}
|
|
81
|
-
const credentialsPath = path.join(dir, CREDENTIALS_FILE);
|
|
82
|
-
if (!fs.existsSync(credentialsPath)) {
|
|
83
|
-
return { profiles: {} };
|
|
84
|
-
}
|
|
85
|
-
const content = fs.readFileSync(credentialsPath, "utf-8");
|
|
86
|
-
const credentials = yaml.parse(content);
|
|
87
|
-
// Replace environment variables
|
|
88
|
-
return replaceEnvVars(credentials);
|
|
89
|
-
}
|
|
90
|
-
const DEFAULT_MODELS = {
|
|
91
|
-
anthropic: "claude-haiku-4-5-20251001", // Cheap & fast
|
|
92
|
-
openai: "gpt-4o-mini", // Cheap & fast
|
|
93
|
-
google: "gemini-2.0-flash", // Cheapest
|
|
94
|
-
mistral: "mistral-small-latest", // Cheap
|
|
95
|
-
groq: "llama-3.3-70b-versatile", // Free tier
|
|
96
|
-
ollama: "llama3", // Local
|
|
97
|
-
};
|
|
98
|
-
const ENV_VARS = {
|
|
99
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
100
|
-
openai: "OPENAI_API_KEY",
|
|
101
|
-
google: "GOOGLE_API_KEY",
|
|
102
|
-
mistral: "MISTRAL_API_KEY",
|
|
103
|
-
groq: "GROQ_API_KEY",
|
|
104
|
-
ollama: "",
|
|
105
|
-
};
|
|
106
|
-
/**
|
|
107
|
-
* Initialize a new .slapify directory with default config
|
|
108
|
-
*/
|
|
109
|
-
export function initConfig(targetDir = process.cwd(), options = {}) {
|
|
110
|
-
const configDir = path.join(targetDir, CONFIG_DIR);
|
|
111
|
-
if (fs.existsSync(configDir)) {
|
|
112
|
-
throw new Error(".slapify directory already exists");
|
|
113
|
-
}
|
|
114
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
115
|
-
// Determine LLM settings
|
|
116
|
-
const provider = options.provider || "anthropic";
|
|
117
|
-
const model = options.model || DEFAULT_MODELS[provider];
|
|
118
|
-
const envVar = ENV_VARS[provider];
|
|
119
|
-
// Build browser config
|
|
120
|
-
let browserConfig = `browser:
|
|
121
|
-
headless: true
|
|
122
|
-
timeout: 30000
|
|
123
|
-
viewport:
|
|
124
|
-
width: 1280
|
|
125
|
-
height: 720`;
|
|
126
|
-
if (options.browserPath) {
|
|
127
|
-
browserConfig += `
|
|
128
|
-
# Using system browser (saves ~170MB download)
|
|
129
|
-
executablePath: "${options.browserPath}"`;
|
|
130
|
-
}
|
|
131
|
-
else if (options.useSystemBrowser === false) {
|
|
132
|
-
browserConfig += `
|
|
133
|
-
# Will download Chromium on first run (~170MB)`;
|
|
134
|
-
}
|
|
135
|
-
// Build LLM config section
|
|
136
|
-
let llmConfig = `llm:
|
|
137
|
-
provider: ${provider}
|
|
138
|
-
model: ${model}`;
|
|
139
|
-
if (provider === "ollama") {
|
|
140
|
-
llmConfig += `
|
|
141
|
-
# Ollama runs locally - no API key needed
|
|
142
|
-
base_url: http://localhost:11434/v1`;
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
llmConfig += `
|
|
146
|
-
api_key: \${${envVar}}`;
|
|
147
|
-
}
|
|
148
|
-
// Create config.yaml
|
|
149
|
-
const configContent = `# Slapify Configuration
|
|
150
|
-
# Docs: https://slaps.dev/slapify
|
|
151
|
-
|
|
152
|
-
# LLM Settings (required)
|
|
153
|
-
# You can change the provider and model anytime
|
|
154
|
-
${llmConfig}
|
|
155
|
-
|
|
156
|
-
# Browser Settings
|
|
157
|
-
${browserConfig}
|
|
158
|
-
|
|
159
|
-
# Report Settings
|
|
160
|
-
report:
|
|
161
|
-
format: html
|
|
162
|
-
screenshots: true
|
|
163
|
-
output_dir: ./test-reports
|
|
164
|
-
`;
|
|
165
|
-
fs.writeFileSync(path.join(configDir, CONFIG_FILE), configContent);
|
|
166
|
-
// Create default credentials.yaml
|
|
167
|
-
const defaultCredentials = `# Slapify Credentials
|
|
168
|
-
# WARNING: Do not commit this file to version control!
|
|
169
|
-
# This file is gitignored by default
|
|
170
|
-
|
|
171
|
-
profiles:
|
|
172
|
-
# Default login credentials (for form-based login)
|
|
173
|
-
default:
|
|
174
|
-
type: login-form
|
|
175
|
-
username: \${TEST_USERNAME}
|
|
176
|
-
password: \${TEST_PASSWORD}
|
|
177
|
-
|
|
178
|
-
# Example: Admin account with 2FA
|
|
179
|
-
# admin:
|
|
180
|
-
# type: login-form
|
|
181
|
-
# username: admin@example.com
|
|
182
|
-
# password: \${ADMIN_PASSWORD}
|
|
183
|
-
# totp_secret: JBSWY3DPEHPK3PXP
|
|
184
|
-
|
|
185
|
-
# Example: Inject auth token via localStorage
|
|
186
|
-
# token-auth:
|
|
187
|
-
# type: inject
|
|
188
|
-
# localStorage:
|
|
189
|
-
# auth_token: \${AUTH_TOKEN}
|
|
190
|
-
# user_id: "12345"
|
|
191
|
-
|
|
192
|
-
# Example: Inject session via sessionStorage
|
|
193
|
-
# session-auth:
|
|
194
|
-
# type: inject
|
|
195
|
-
# sessionStorage:
|
|
196
|
-
# session_token: \${SESSION_TOKEN}
|
|
197
|
-
|
|
198
|
-
# Example: Inject auth cookies
|
|
199
|
-
# cookie-auth:
|
|
200
|
-
# type: inject
|
|
201
|
-
# cookies:
|
|
202
|
-
# - name: auth_token
|
|
203
|
-
# value: \${AUTH_TOKEN}
|
|
204
|
-
# domain: .example.com
|
|
205
|
-
# - name: refresh_token
|
|
206
|
-
# value: \${REFRESH_TOKEN}
|
|
207
|
-
# domain: .example.com
|
|
208
|
-
|
|
209
|
-
# Example: Combined - cookies + localStorage
|
|
210
|
-
# full-auth:
|
|
211
|
-
# type: inject
|
|
212
|
-
# cookies:
|
|
213
|
-
# - name: session_id
|
|
214
|
-
# value: \${SESSION_ID}
|
|
215
|
-
# domain: .example.com
|
|
216
|
-
# localStorage:
|
|
217
|
-
# user_preferences: '{"theme":"dark","lang":"en"}'
|
|
218
|
-
# feature_flags: '{"beta":true}'
|
|
219
|
-
`;
|
|
220
|
-
fs.writeFileSync(path.join(configDir, CREDENTIALS_FILE), defaultCredentials);
|
|
221
|
-
// Create tests directory with example
|
|
222
|
-
const testsDir = path.join(targetDir, "tests");
|
|
223
|
-
if (!fs.existsSync(testsDir)) {
|
|
224
|
-
fs.mkdirSync(testsDir, { recursive: true });
|
|
225
|
-
}
|
|
226
|
-
// Create example flow file
|
|
227
|
-
const exampleFlow = `# Example Test - Getting Started with Slapify
|
|
228
|
-
# Run with: slapify run tests/example.flow
|
|
229
|
-
|
|
230
|
-
# Navigate to a website
|
|
231
|
-
Go to https://example.com
|
|
232
|
-
|
|
233
|
-
# Verify the page loaded correctly
|
|
234
|
-
Verify page title contains "Example"
|
|
235
|
-
|
|
236
|
-
# Handle potential popups (won't fail if not present)
|
|
237
|
-
[Optional] Close any cookie consent popup
|
|
238
|
-
|
|
239
|
-
# Interact with the page
|
|
240
|
-
Click on "More information" link
|
|
241
|
-
|
|
242
|
-
# Verify navigation worked
|
|
243
|
-
Verify URL contains "iana.org"
|
|
244
|
-
`;
|
|
245
|
-
fs.writeFileSync(path.join(testsDir, "example.flow"), exampleFlow);
|
|
246
|
-
// Create .gitignore for credentials
|
|
247
|
-
const gitignore = `credentials.yaml
|
|
248
|
-
`;
|
|
249
|
-
fs.writeFileSync(path.join(configDir, ".gitignore"), gitignore);
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Check if a browser is available at a given path
|
|
253
|
-
*/
|
|
254
|
-
export function checkBrowserPath(browserPath) {
|
|
255
|
-
return fs.existsSync(browserPath);
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Find common browser paths on the system
|
|
259
|
-
*/
|
|
260
|
-
export function findSystemBrowsers() {
|
|
261
|
-
const browsers = [];
|
|
262
|
-
const commonPaths = [
|
|
263
|
-
{
|
|
264
|
-
name: "Google Chrome",
|
|
265
|
-
path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
266
|
-
},
|
|
267
|
-
{ name: "Chrome (Linux)", path: "/usr/bin/google-chrome" },
|
|
268
|
-
{ name: "Chrome (Linux Alt)", path: "/usr/bin/google-chrome-stable" },
|
|
269
|
-
{ name: "Chromium", path: "/usr/bin/chromium" },
|
|
270
|
-
{ name: "Chromium (Linux)", path: "/usr/bin/chromium-browser" },
|
|
271
|
-
{
|
|
272
|
-
name: "Chrome (Windows)",
|
|
273
|
-
path: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
name: "Chrome (Windows x86)",
|
|
277
|
-
path: "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
name: "Edge",
|
|
281
|
-
path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: "Edge (Windows)",
|
|
285
|
-
path: "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
name: "Brave",
|
|
289
|
-
path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
290
|
-
},
|
|
291
|
-
];
|
|
292
|
-
for (const browser of commonPaths) {
|
|
293
|
-
if (fs.existsSync(browser.path)) {
|
|
294
|
-
browsers.push(browser);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return browsers;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Get the config directory path
|
|
301
|
-
*/
|
|
302
|
-
export function getConfigDir() {
|
|
303
|
-
return findConfigDir();
|
|
304
|
-
}
|
|
305
|
-
//# sourceMappingURL=loader.js.map
|
|
1
|
+
import e from"fs";import n from"path";import o from"yaml";const t=".slapify",r="config.yaml",i="credentials.yaml";function a(o=process.cwd()){let r=o;for(;r!==n.dirname(r);){const o=n.join(r,t);if(e.existsSync(o))return o;r=n.dirname(r)}return null}function s(e){if("string"==typeof e)return e.replace(/\$\{(\w+)\}/g,(e,n)=>process.env[n]||"");if(Array.isArray(e))return e.map(s);if(e&&"object"==typeof e){const n={};for(const[o,t]of Object.entries(e))n[o]=s(t);return n}return e}export function loadConfig(t){const i=t||a();if(!i)throw new Error('No .slapify directory found. Run "slapify init" to create one.');const l=n.join(i,r);if(!e.existsSync(l))throw new Error(`Config file not found: ${l}`);const c=e.readFileSync(l,"utf-8"),p=s(o.parse(c));return{...p,browser:{headless:!0,timeout:3e4,viewport:{width:1280,height:720},...p.browser},report:{format:"markdown",screenshots:!0,output_dir:"./test-reports",...p.report}}}export function loadCredentials(t){const r=t||a();if(!r)return{profiles:{}};const l=n.join(r,i);if(!e.existsSync(l))return{profiles:{}};const c=e.readFileSync(l,"utf-8");return s(o.parse(c))}const l={anthropic:"claude-haiku-4-5-20251001",openai:"gpt-4o-mini",google:"gemini-2.0-flash",mistral:"mistral-small-latest",groq:"llama-3.3-70b-versatile",ollama:"llama3"},c={anthropic:"ANTHROPIC_API_KEY",openai:"OPENAI_API_KEY",google:"GOOGLE_API_KEY",mistral:"MISTRAL_API_KEY",groq:"GROQ_API_KEY",ollama:""};export function initConfig(o=process.cwd(),a={}){const s=n.join(o,t);if(e.existsSync(s))throw new Error(".slapify directory already exists");e.mkdirSync(s,{recursive:!0});const p=a.provider||"anthropic",m=a.model||l[p],u=c[p];let f="browser:\n headless: true\n timeout: 30000\n viewport:\n width: 1280\n height: 720";a.browserPath?f+=`\n # Using system browser (saves ~170MB download)\n executablePath: "${a.browserPath}"`:!1===a.useSystemBrowser&&(f+="\n # Will download Chromium on first run (~170MB)");let h=`llm:\n provider: ${p}\n model: ${m}`;h+="ollama"===p?"\n # Ollama runs locally - no API key needed\n base_url: http://localhost:11434/v1":`\n api_key: \${${u}}`;const d=`# Slapify Configuration\n# Docs: https://slaps.dev/slapify\n\n# LLM Settings (required)\n# You can change the provider and model anytime\n${h}\n\n# Browser Settings\n${f}\n\n# Report Settings\nreport:\n format: html\n screenshots: true\n output_dir: ./test-reports\n`;e.writeFileSync(n.join(s,r),d);e.writeFileSync(n.join(s,i),'# Slapify Credentials\n# WARNING: Do not commit this file to version control!\n# This file is gitignored by default\n\nprofiles:\n # Default login credentials (for form-based login)\n default:\n type: login-form\n username: ${TEST_USERNAME}\n password: ${TEST_PASSWORD}\n\n # Example: Admin account with 2FA\n # admin:\n # type: login-form\n # username: admin@example.com\n # password: ${ADMIN_PASSWORD}\n # totp_secret: JBSWY3DPEHPK3PXP\n\n # Example: Inject auth token via localStorage\n # token-auth:\n # type: inject\n # localStorage:\n # auth_token: ${AUTH_TOKEN}\n # user_id: "12345"\n\n # Example: Inject session via sessionStorage\n # session-auth:\n # type: inject\n # sessionStorage:\n # session_token: ${SESSION_TOKEN}\n\n # Example: Inject auth cookies\n # cookie-auth:\n # type: inject\n # cookies:\n # - name: auth_token\n # value: ${AUTH_TOKEN}\n # domain: .example.com\n # - name: refresh_token\n # value: ${REFRESH_TOKEN}\n # domain: .example.com\n\n # Example: Combined - cookies + localStorage\n # full-auth:\n # type: inject\n # cookies:\n # - name: session_id\n # value: ${SESSION_ID}\n # domain: .example.com\n # localStorage:\n # user_preferences: \'{"theme":"dark","lang":"en"}\'\n # feature_flags: \'{"beta":true}\'\n');const g=n.join(o,"tests");e.existsSync(g)||e.mkdirSync(g,{recursive:!0});e.writeFileSync(n.join(g,"example.flow"),'# Example Test - Getting Started with Slapify\n# Run with: slapify run tests/example.flow\n\n# Navigate to a website\nGo to https://example.com\n\n# Verify the page loaded correctly\nVerify page title contains "Example"\n\n# Handle potential popups (won\'t fail if not present)\n[Optional] Close any cookie consent popup\n\n# Interact with the page\nClick on "More information" link\n\n# Verify navigation worked\nVerify URL contains "iana.org"\n');e.writeFileSync(n.join(s,".gitignore"),"credentials.yaml\n")}export function checkBrowserPath(n){return e.existsSync(n)}export function findSystemBrowsers(){const n=[],o=[{name:"Google Chrome",path:"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"},{name:"Chrome (Linux)",path:"/usr/bin/google-chrome"},{name:"Chrome (Linux Alt)",path:"/usr/bin/google-chrome-stable"},{name:"Chromium",path:"/usr/bin/chromium"},{name:"Chromium (Linux)",path:"/usr/bin/chromium-browser"},{name:"Chrome (Windows)",path:"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"},{name:"Chrome (Windows x86)",path:"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"},{name:"Edge",path:"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"},{name:"Edge (Windows)",path:"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"},{name:"Brave",path:"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"}];for(const t of o)e.existsSync(t.path)&&n.push(t);return n}export function getConfigDir(){return a()}
|
package/dist/index.js
CHANGED
|
@@ -1,262 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// Task mode
|
|
3
|
-
export { runTask, listSessions, loadSession } from "./task/index.js";
|
|
4
|
-
export { loadConfig, loadCredentials, initConfig, getConfigDir, findSystemBrowsers, checkBrowserPath, } from "./config/loader.js";
|
|
5
|
-
export { parseFlowFile, parseFlowContent, findFlowFiles, validateFlowFile, getFlowSummary, } from "./parser/flow.js";
|
|
6
|
-
export { BrowserAgent } from "./browser/agent.js";
|
|
7
|
-
export { AIInterpreter } from "./ai/interpreter.js";
|
|
8
|
-
export { TestRunner } from "./runner/index.js";
|
|
9
|
-
export { ReportGenerator } from "./report/generator.js";
|
|
10
|
-
// Convenience class for programmatic usage
|
|
11
|
-
import { loadConfig, loadCredentials } from "./config/loader.js";
|
|
12
|
-
import { parseFlowFile, parseFlowContent, findFlowFiles, } from "./parser/flow.js";
|
|
13
|
-
import { TestRunner } from "./runner/index.js";
|
|
14
|
-
import { ReportGenerator } from "./report/generator.js";
|
|
15
|
-
/**
|
|
16
|
-
* Main Slapify class for programmatic usage
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* import { Slapify } from 'testpilot';
|
|
21
|
-
*
|
|
22
|
-
* // Using config file
|
|
23
|
-
* const pilot = new Slapify({ configDir: '.testpilot' });
|
|
24
|
-
*
|
|
25
|
-
* // Or with inline config
|
|
26
|
-
* const pilot = new Slapify({
|
|
27
|
-
* config: {
|
|
28
|
-
* llm: { provider: 'anthropic', model: 'claude-sonnet-4-20250514', api_key: process.env.ANTHROPIC_API_KEY },
|
|
29
|
-
* browser: { headless: true }
|
|
30
|
-
* }
|
|
31
|
-
* });
|
|
32
|
-
*
|
|
33
|
-
* // Run inline steps
|
|
34
|
-
* const result = await pilot.run([
|
|
35
|
-
* 'Go to https://example.com',
|
|
36
|
-
* 'Click on "More information" link',
|
|
37
|
-
* 'Verify URL contains "iana.org"'
|
|
38
|
-
* ]);
|
|
39
|
-
*
|
|
40
|
-
* console.log(result.status); // 'passed' or 'failed'
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export class Slapify {
|
|
44
|
-
config;
|
|
45
|
-
credentials;
|
|
46
|
-
reporter;
|
|
47
|
-
constructor(options = {}) {
|
|
48
|
-
// Load config from directory or use provided
|
|
49
|
-
if (options.configDir) {
|
|
50
|
-
this.config = { ...loadConfig(options.configDir), ...options.config };
|
|
51
|
-
this.credentials =
|
|
52
|
-
options.credentials || loadCredentials(options.configDir);
|
|
53
|
-
}
|
|
54
|
-
else if (options.config?.llm) {
|
|
55
|
-
this.config = {
|
|
56
|
-
llm: options.config.llm,
|
|
57
|
-
browser: options.config.browser || { headless: true, timeout: 30000 },
|
|
58
|
-
report: options.config.report || {
|
|
59
|
-
format: "html",
|
|
60
|
-
screenshots: true,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
this.credentials = options.credentials || { profiles: {} };
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
// Try to load from cwd
|
|
67
|
-
this.config = loadConfig();
|
|
68
|
-
this.credentials = loadCredentials();
|
|
69
|
-
}
|
|
70
|
-
this.reporter = new ReportGenerator(this.config.report);
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Create a new runner instance (each test needs its own runner for parallel execution)
|
|
74
|
-
*/
|
|
75
|
-
createRunner() {
|
|
76
|
-
return new TestRunner(this.config, this.credentials);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Run a flow file
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* ```typescript
|
|
83
|
-
* const result = await pilot.runFile('tests/login.flow');
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
async runFile(filePath, options = {}) {
|
|
87
|
-
const flow = parseFlowFile(filePath);
|
|
88
|
-
return this.runFlow(flow, options);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Run steps from an array of strings
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```typescript
|
|
95
|
-
* const result = await pilot.run([
|
|
96
|
-
* 'Go to https://google.com',
|
|
97
|
-
* 'Fill search box with "testpilot"',
|
|
98
|
-
* 'Press Enter',
|
|
99
|
-
* 'Verify results contain "testpilot"'
|
|
100
|
-
* ], 'google-search');
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
async run(steps, name = "inline", options = {}) {
|
|
104
|
-
const content = steps.join("\n");
|
|
105
|
-
const flow = parseFlowContent(content, name);
|
|
106
|
-
return this.runFlow(flow, options);
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Run a flow from string content
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```typescript
|
|
113
|
-
* const flowContent = `
|
|
114
|
-
* Go to https://example.com
|
|
115
|
-
* Click on "More information"
|
|
116
|
-
* Verify page loads
|
|
117
|
-
* `;
|
|
118
|
-
* const result = await pilot.runContent(flowContent, 'example-test');
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
async runContent(content, name = "inline", options = {}) {
|
|
122
|
-
const flow = parseFlowContent(content, name);
|
|
123
|
-
return this.runFlow(flow, options);
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Run a parsed flow
|
|
127
|
-
*/
|
|
128
|
-
async runFlow(flow, options = {}) {
|
|
129
|
-
const runner = this.createRunner();
|
|
130
|
-
if (options.onTestStart) {
|
|
131
|
-
options.onTestStart(flow.name, flow.steps.length);
|
|
132
|
-
}
|
|
133
|
-
const result = await runner.runFlow(flow, (stepResult) => {
|
|
134
|
-
if (options.onStep) {
|
|
135
|
-
options.onStep(stepResult, flow.name);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
if (options.onTestComplete) {
|
|
139
|
-
options.onTestComplete(result);
|
|
140
|
-
}
|
|
141
|
-
return result;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Run multiple flow files
|
|
145
|
-
*
|
|
146
|
-
* @example
|
|
147
|
-
* ```typescript
|
|
148
|
-
* // Sequential
|
|
149
|
-
* const results = await pilot.runMultiple(['tests/login.flow', 'tests/checkout.flow']);
|
|
150
|
-
*
|
|
151
|
-
* // Parallel with 4 workers
|
|
152
|
-
* const results = await pilot.runMultiple(['tests/*.flow'], { parallel: true, workers: 4 });
|
|
153
|
-
* ```
|
|
154
|
-
*/
|
|
155
|
-
async runMultiple(patterns, options = {}) {
|
|
156
|
-
// Expand patterns to file paths
|
|
157
|
-
const files = [];
|
|
158
|
-
for (const pattern of patterns) {
|
|
159
|
-
if (pattern.includes("*")) {
|
|
160
|
-
// It's a glob pattern - find matching files
|
|
161
|
-
const found = await findFlowFiles(pattern.replace(/\/\*.*$/, ""));
|
|
162
|
-
files.push(...found);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const fs = await import("fs");
|
|
166
|
-
if (fs.statSync(pattern).isDirectory()) {
|
|
167
|
-
const found = await findFlowFiles(pattern);
|
|
168
|
-
files.push(...found);
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
files.push(pattern);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (files.length === 0) {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
const results = [];
|
|
179
|
-
if (options.parallel && files.length > 1) {
|
|
180
|
-
// Parallel execution
|
|
181
|
-
const workers = options.workers || 4;
|
|
182
|
-
const pending = [...files];
|
|
183
|
-
const running = new Map();
|
|
184
|
-
while (pending.length > 0 || running.size > 0) {
|
|
185
|
-
// Start new tasks up to worker limit
|
|
186
|
-
while (pending.length > 0 && running.size < workers) {
|
|
187
|
-
const file = pending.shift();
|
|
188
|
-
const promise = this.runFile(file, options)
|
|
189
|
-
.then((result) => {
|
|
190
|
-
results.push(result);
|
|
191
|
-
running.delete(file);
|
|
192
|
-
})
|
|
193
|
-
.catch(() => {
|
|
194
|
-
running.delete(file);
|
|
195
|
-
});
|
|
196
|
-
running.set(file, promise);
|
|
197
|
-
}
|
|
198
|
-
// Wait for at least one to complete
|
|
199
|
-
if (running.size > 0) {
|
|
200
|
-
await Promise.race(running.values());
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
// Sequential execution
|
|
206
|
-
for (const file of files) {
|
|
207
|
-
const result = await this.runFile(file, options);
|
|
208
|
-
results.push(result);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return results;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Run all flow files in a directory
|
|
215
|
-
*
|
|
216
|
-
* @example
|
|
217
|
-
* ```typescript
|
|
218
|
-
* const results = await pilot.runAll('tests/', { parallel: true });
|
|
219
|
-
* ```
|
|
220
|
-
*/
|
|
221
|
-
async runAll(directory = "tests", options = {}) {
|
|
222
|
-
return this.runMultiple([directory], options);
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Generate a report from results
|
|
226
|
-
*/
|
|
227
|
-
generateReport(result) {
|
|
228
|
-
return this.reporter.generate(result);
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Generate a suite report from multiple results
|
|
232
|
-
*/
|
|
233
|
-
generateSuiteReport(results) {
|
|
234
|
-
return this.reporter.generateSuiteReport(results);
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Save a report to file/folder
|
|
238
|
-
*/
|
|
239
|
-
saveReport(result, filename) {
|
|
240
|
-
return this.reporter.saveAsFolder(result);
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Save a suite report to folder
|
|
244
|
-
*/
|
|
245
|
-
saveSuiteReport(results) {
|
|
246
|
-
return this.reporter.saveSuiteAsFolder(results);
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Print summary to console
|
|
250
|
-
*/
|
|
251
|
-
printSummary(result) {
|
|
252
|
-
this.reporter.printSummary(result);
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Get the current configuration
|
|
256
|
-
*/
|
|
257
|
-
getConfig() {
|
|
258
|
-
return this.config;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
export default Slapify;
|
|
262
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export{runTask,listSessions,loadSession}from"./task/index.js";export{loadConfig,loadCredentials,initConfig,getConfigDir,findSystemBrowsers,checkBrowserPath}from"./config/loader.js";export{parseFlowFile,parseFlowContent,findFlowFiles,validateFlowFile,getFlowSummary}from"./parser/flow.js";export{BrowserAgent}from"./browser/agent.js";export{AIInterpreter}from"./ai/interpreter.js";export{TestRunner}from"./runner/index.js";export{ReportGenerator}from"./report/generator.js";import{loadConfig as e,loadCredentials as r}from"./config/loader.js";import{parseFlowFile as t,parseFlowContent as n,findFlowFiles as o}from"./parser/flow.js";import{TestRunner as s}from"./runner/index.js";import{ReportGenerator as i}from"./report/generator.js";export class Slapify{config;credentials;reporter;constructor(t={}){t.configDir?(this.config={...e(t.configDir),...t.config},this.credentials=t.credentials||r(t.configDir)):t.config?.llm?(this.config={llm:t.config.llm,browser:t.config.browser||{headless:!0,timeout:3e4},report:t.config.report||{format:"html",screenshots:!0}},this.credentials=t.credentials||{profiles:{}}):(this.config=e(),this.credentials=r()),this.reporter=new i(this.config.report)}createRunner(){return new s(this.config,this.credentials)}async runFile(e,r={}){const n=t(e);return this.runFlow(n,r)}async run(e,r="inline",t={}){const o=e.join("\n"),s=n(o,r);return this.runFlow(s,t)}async runContent(e,r="inline",t={}){const o=n(e,r);return this.runFlow(o,t)}async runFlow(e,r={}){const t=this.createRunner();r.onTestStart&&r.onTestStart(e.name,e.steps.length);const n=await t.runFlow(e,t=>{r.onStep&&r.onStep(t,e.name)});return r.onTestComplete&&r.onTestComplete(n),n}async runMultiple(e,r={}){const t=[];for(const r of e)if(r.includes("*")){const e=await o(r.replace(/\/\*.*$/,""));t.push(...e)}else{if((await import("fs")).statSync(r).isDirectory()){const e=await o(r);t.push(...e)}else t.push(r)}if(0===t.length)return[];const n=[];if(r.parallel&&t.length>1){const e=r.workers||4,o=[...t],s=new Map;for(;o.length>0||s.size>0;){for(;o.length>0&&s.size<e;){const e=o.shift(),t=this.runFile(e,r).then(r=>{n.push(r),s.delete(e)}).catch(()=>{s.delete(e)});s.set(e,t)}s.size>0&&await Promise.race(s.values())}}else for(const e of t){const t=await this.runFile(e,r);n.push(t)}return n}async runAll(e="tests",r={}){return this.runMultiple([e],r)}generateReport(e){return this.reporter.generate(e)}generateSuiteReport(e){return this.reporter.generateSuiteReport(e)}saveReport(e,r){return this.reporter.saveAsFolder(e)}saveSuiteReport(e){return this.reporter.saveSuiteAsFolder(e)}printSummary(e){this.reporter.printSummary(e)}getConfig(){return this.config}}export default Slapify;
|