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.
Files changed (52) hide show
  1. package/README.md +38 -4
  2. package/dist/ai/interpreter.js +1 -331
  3. package/dist/browser/agent.js +1 -485
  4. package/dist/cli.js +1 -1553
  5. package/dist/config/loader.js +1 -305
  6. package/dist/index.js +1 -262
  7. package/dist/parser/flow.js +1 -117
  8. package/dist/perf/audit.js +1 -635
  9. package/dist/report/generator.js +1 -641
  10. package/dist/runner/index.js +1 -744
  11. package/dist/task/index.js +1 -4
  12. package/dist/task/report.js +1 -740
  13. package/dist/task/runner.js +1 -1362
  14. package/dist/task/session.js +1 -153
  15. package/dist/task/tools.d.ts +12 -0
  16. package/dist/task/tools.js +1 -258
  17. package/dist/task/types.d.ts +18 -0
  18. package/dist/task/types.js +1 -2
  19. package/dist/types.js +1 -2
  20. package/package.json +6 -3
  21. package/dist/ai/interpreter.d.ts.map +0 -1
  22. package/dist/ai/interpreter.js.map +0 -1
  23. package/dist/browser/agent.d.ts.map +0 -1
  24. package/dist/browser/agent.js.map +0 -1
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js.map +0 -1
  27. package/dist/config/loader.d.ts.map +0 -1
  28. package/dist/config/loader.js.map +0 -1
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/index.js.map +0 -1
  31. package/dist/parser/flow.d.ts.map +0 -1
  32. package/dist/parser/flow.js.map +0 -1
  33. package/dist/perf/audit.d.ts.map +0 -1
  34. package/dist/perf/audit.js.map +0 -1
  35. package/dist/report/generator.d.ts.map +0 -1
  36. package/dist/report/generator.js.map +0 -1
  37. package/dist/runner/index.d.ts.map +0 -1
  38. package/dist/runner/index.js.map +0 -1
  39. package/dist/task/index.d.ts.map +0 -1
  40. package/dist/task/index.js.map +0 -1
  41. package/dist/task/report.d.ts.map +0 -1
  42. package/dist/task/report.js.map +0 -1
  43. package/dist/task/runner.d.ts.map +0 -1
  44. package/dist/task/runner.js.map +0 -1
  45. package/dist/task/session.d.ts.map +0 -1
  46. package/dist/task/session.js.map +0 -1
  47. package/dist/task/tools.d.ts.map +0 -1
  48. package/dist/task/tools.js.map +0 -1
  49. package/dist/task/types.d.ts.map +0 -1
  50. package/dist/task/types.js.map +0 -1
  51. package/dist/types.d.ts.map +0 -1
  52. package/dist/types.js.map +0 -1
@@ -1,305 +1 @@
1
- import fs from "fs";
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
- // Main exports for programmatic usage
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;