@browserflow-ai/core 0.0.6

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 (57) hide show
  1. package/dist/config-schema.d.ts +354 -0
  2. package/dist/config-schema.d.ts.map +1 -0
  3. package/dist/config-schema.js +83 -0
  4. package/dist/config-schema.js.map +1 -0
  5. package/dist/config.d.ts +107 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +5 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/duration.d.ts +39 -0
  10. package/dist/duration.d.ts.map +1 -0
  11. package/dist/duration.js +111 -0
  12. package/dist/duration.js.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +20 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/locator-object.d.ts +556 -0
  18. package/dist/locator-object.d.ts.map +1 -0
  19. package/dist/locator-object.js +114 -0
  20. package/dist/locator-object.js.map +1 -0
  21. package/dist/lockfile.d.ts +1501 -0
  22. package/dist/lockfile.d.ts.map +1 -0
  23. package/dist/lockfile.js +86 -0
  24. package/dist/lockfile.js.map +1 -0
  25. package/dist/run-store.d.ts +81 -0
  26. package/dist/run-store.d.ts.map +1 -0
  27. package/dist/run-store.js +181 -0
  28. package/dist/run-store.js.map +1 -0
  29. package/dist/spec-loader.d.ts +17 -0
  30. package/dist/spec-loader.d.ts.map +1 -0
  31. package/dist/spec-loader.js +37 -0
  32. package/dist/spec-loader.js.map +1 -0
  33. package/dist/spec-schema.d.ts +1411 -0
  34. package/dist/spec-schema.d.ts.map +1 -0
  35. package/dist/spec-schema.js +239 -0
  36. package/dist/spec-schema.js.map +1 -0
  37. package/package.json +45 -0
  38. package/schemas/browserflow.schema.json +220 -0
  39. package/schemas/lockfile.schema.json +568 -0
  40. package/schemas/spec-v2.schema.json +413 -0
  41. package/src/config-schema.test.ts +190 -0
  42. package/src/config-schema.ts +111 -0
  43. package/src/config.ts +112 -0
  44. package/src/duration.test.ts +175 -0
  45. package/src/duration.ts +128 -0
  46. package/src/index.ts +136 -0
  47. package/src/json-schemas.test.ts +374 -0
  48. package/src/locator-object.test.ts +323 -0
  49. package/src/locator-object.ts +250 -0
  50. package/src/lockfile.test.ts +345 -0
  51. package/src/lockfile.ts +209 -0
  52. package/src/run-store.test.ts +232 -0
  53. package/src/run-store.ts +240 -0
  54. package/src/spec-loader.test.ts +181 -0
  55. package/src/spec-loader.ts +47 -0
  56. package/src/spec-schema.test.ts +360 -0
  57. package/src/spec-schema.ts +347 -0
package/src/config.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Configuration types for BrowserFlow
3
+ */
4
+
5
+ /**
6
+ * Generator output format options
7
+ */
8
+ export type GeneratorOutputFormat = 'playwright-ts' | 'playwright-js' | 'bash';
9
+
10
+ /**
11
+ * Browser types supported by Playwright
12
+ */
13
+ export type BrowserType = 'chromium' | 'firefox' | 'webkit';
14
+
15
+ /**
16
+ * Playwright project configuration
17
+ */
18
+ export interface PlaywrightProject {
19
+ name: string;
20
+ use: {
21
+ browserName?: BrowserType;
22
+ viewport?: { width: number; height: number };
23
+ headless?: boolean;
24
+ [key: string]: unknown;
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Playwright configuration options for generated tests
30
+ */
31
+ export interface PlaywrightConfigOptions {
32
+ /** Test timeout in ms */
33
+ timeout?: number;
34
+ /** Number of retries */
35
+ retries?: number;
36
+ /** Number of workers */
37
+ workers?: number;
38
+ /** Reporter to use */
39
+ reporter?: 'html' | 'list' | 'dot' | 'json';
40
+ /** Projects configuration */
41
+ projects?: PlaywrightProject[];
42
+ /** Web server configuration */
43
+ webServer?: {
44
+ command: string;
45
+ url: string;
46
+ reuseExistingServer?: boolean;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Generator configuration
52
+ */
53
+ export interface GeneratorConfig {
54
+ /** Output format for generated tests */
55
+ outputFormat: GeneratorOutputFormat;
56
+ /** Include baseline screenshot comparisons */
57
+ includeBaselineChecks: boolean;
58
+ /** Directory for baseline screenshots */
59
+ baselinesDir?: string;
60
+ /** Playwright config options */
61
+ playwrightConfig?: PlaywrightConfigOptions;
62
+ }
63
+
64
+ /**
65
+ * Review step data
66
+ */
67
+ export interface ReviewStepData {
68
+ step_index: number;
69
+ status: 'approved' | 'rejected' | 'pending';
70
+ comment?: string;
71
+ tags?: string[];
72
+ }
73
+
74
+ /**
75
+ * Review data for an exploration
76
+ */
77
+ export interface ReviewData {
78
+ exploration_id: string;
79
+ reviewer?: string;
80
+ started_at: string;
81
+ updated_at: string;
82
+ steps: ReviewStepData[];
83
+ overall_notes?: string;
84
+ verdict: 'approved' | 'rejected' | 'pending';
85
+ submitted_at?: string;
86
+ }
87
+
88
+ /**
89
+ * Generated test output
90
+ */
91
+ export interface GeneratedTest {
92
+ /** Test file path (relative) */
93
+ path: string;
94
+ /** Test file content */
95
+ content: string;
96
+ /** Spec name this was generated from */
97
+ specName: string;
98
+ /** Exploration ID used */
99
+ explorationId: string;
100
+ /** Generation timestamp */
101
+ generatedAt: string;
102
+ }
103
+
104
+ /**
105
+ * Generated config output
106
+ */
107
+ export interface GeneratedConfig {
108
+ /** Config file path */
109
+ path: string;
110
+ /** Config file content */
111
+ content: string;
112
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Tests for duration string parser
3
+ * @see bf-x1q
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { parseDuration, formatDuration, isValidDuration } from './duration.js';
8
+
9
+ describe('parseDuration', () => {
10
+ describe('basic units', () => {
11
+ test('parses milliseconds', () => {
12
+ expect(parseDuration('500ms')).toBe(500);
13
+ expect(parseDuration('0ms')).toBe(0);
14
+ expect(parseDuration('1ms')).toBe(1);
15
+ });
16
+
17
+ test('parses seconds', () => {
18
+ expect(parseDuration('3s')).toBe(3000);
19
+ expect(parseDuration('1s')).toBe(1000);
20
+ expect(parseDuration('30s')).toBe(30000);
21
+ });
22
+
23
+ test('parses minutes', () => {
24
+ expect(parseDuration('2m')).toBe(120000);
25
+ expect(parseDuration('1m')).toBe(60000);
26
+ expect(parseDuration('5m')).toBe(300000);
27
+ });
28
+
29
+ test('parses hours', () => {
30
+ expect(parseDuration('1h')).toBe(3600000);
31
+ expect(parseDuration('2h')).toBe(7200000);
32
+ });
33
+ });
34
+
35
+ describe('combined units', () => {
36
+ test('parses minute and seconds', () => {
37
+ expect(parseDuration('1m30s')).toBe(90000);
38
+ expect(parseDuration('2m15s')).toBe(135000);
39
+ });
40
+
41
+ test('parses hours and minutes', () => {
42
+ expect(parseDuration('1h30m')).toBe(5400000);
43
+ expect(parseDuration('2h45m')).toBe(9900000);
44
+ });
45
+
46
+ test('parses complex combinations', () => {
47
+ expect(parseDuration('1h30m45s')).toBe(5445000);
48
+ expect(parseDuration('1m30s500ms')).toBe(90500);
49
+ });
50
+ });
51
+
52
+ describe('numeric input', () => {
53
+ test('treats numeric input as milliseconds', () => {
54
+ expect(parseDuration(500)).toBe(500);
55
+ expect(parseDuration(3000)).toBe(3000);
56
+ expect(parseDuration(0)).toBe(0);
57
+ });
58
+
59
+ test('parses plain number strings as milliseconds', () => {
60
+ expect(parseDuration('500')).toBe(500);
61
+ expect(parseDuration('3000')).toBe(3000);
62
+ });
63
+ });
64
+
65
+ describe('whitespace handling', () => {
66
+ test('trims whitespace', () => {
67
+ expect(parseDuration(' 3s ')).toBe(3000);
68
+ expect(parseDuration('\t2m\n')).toBe(120000);
69
+ });
70
+
71
+ test('handles lowercase conversion', () => {
72
+ expect(parseDuration('3S')).toBe(3000);
73
+ expect(parseDuration('2M')).toBe(120000);
74
+ expect(parseDuration('1H')).toBe(3600000);
75
+ });
76
+ });
77
+
78
+ describe('error handling', () => {
79
+ test('throws for empty string', () => {
80
+ expect(() => parseDuration('')).toThrow('Duration must be a non-empty string');
81
+ });
82
+
83
+ test('throws for whitespace-only string', () => {
84
+ expect(() => parseDuration(' ')).toThrow('Duration must be a non-empty string');
85
+ });
86
+
87
+ test('throws for invalid format', () => {
88
+ expect(() => parseDuration('3 seconds')).toThrow(/Invalid duration/);
89
+ expect(() => parseDuration('abc')).toThrow(/Invalid duration/);
90
+ expect(() => parseDuration('3x')).toThrow(/Invalid duration/);
91
+ });
92
+
93
+ test('provides helpful error message', () => {
94
+ try {
95
+ parseDuration('invalid');
96
+ } catch (e) {
97
+ expect((e as Error).message).toContain('3s');
98
+ expect((e as Error).message).toContain('2m');
99
+ expect((e as Error).message).toContain('500ms');
100
+ expect((e as Error).message).toContain('1m30s');
101
+ }
102
+ });
103
+ });
104
+
105
+ describe('edge cases', () => {
106
+ test('handles fractional values', () => {
107
+ expect(parseDuration('1.5s')).toBe(1500);
108
+ expect(parseDuration('0.5m')).toBe(30000);
109
+ });
110
+
111
+ test('handles zero values', () => {
112
+ expect(parseDuration('0s')).toBe(0);
113
+ expect(parseDuration('0m')).toBe(0);
114
+ });
115
+ });
116
+ });
117
+
118
+ describe('formatDuration', () => {
119
+ test('formats milliseconds', () => {
120
+ expect(formatDuration(500)).toBe('500ms');
121
+ expect(formatDuration(0)).toBe('0ms');
122
+ expect(formatDuration(999)).toBe('999ms');
123
+ });
124
+
125
+ test('formats seconds', () => {
126
+ expect(formatDuration(1000)).toBe('1s');
127
+ expect(formatDuration(3000)).toBe('3s');
128
+ expect(formatDuration(30000)).toBe('30s');
129
+ });
130
+
131
+ test('formats minutes', () => {
132
+ expect(formatDuration(60000)).toBe('1m');
133
+ expect(formatDuration(120000)).toBe('2m');
134
+ });
135
+
136
+ test('formats hours', () => {
137
+ expect(formatDuration(3600000)).toBe('1h');
138
+ expect(formatDuration(7200000)).toBe('2h');
139
+ });
140
+
141
+ test('formats combinations', () => {
142
+ expect(formatDuration(90000)).toBe('1m30s');
143
+ expect(formatDuration(5400000)).toBe('1h30m');
144
+ expect(formatDuration(5445000)).toBe('1h30m45s');
145
+ });
146
+
147
+ test('formats sub-second remainders to seconds', () => {
148
+ // formatDuration uses floor, so sub-second parts are dropped
149
+ expect(formatDuration(1500)).toBe('1s');
150
+ expect(formatDuration(90500)).toBe('1m30s');
151
+ });
152
+ });
153
+
154
+ describe('isValidDuration', () => {
155
+ test('returns true for valid durations', () => {
156
+ expect(isValidDuration('3s')).toBe(true);
157
+ expect(isValidDuration('2m')).toBe(true);
158
+ expect(isValidDuration('1h')).toBe(true);
159
+ expect(isValidDuration('500ms')).toBe(true);
160
+ expect(isValidDuration('1m30s')).toBe(true);
161
+ expect(isValidDuration('1h30m')).toBe(true);
162
+ });
163
+
164
+ test('returns false for invalid durations', () => {
165
+ expect(isValidDuration('')).toBe(false);
166
+ expect(isValidDuration('abc')).toBe(false);
167
+ expect(isValidDuration('3 seconds')).toBe(false);
168
+ expect(isValidDuration('3x')).toBe(false);
169
+ });
170
+
171
+ test('returns true for plain number strings', () => {
172
+ expect(isValidDuration('500')).toBe(true);
173
+ expect(isValidDuration('3000')).toBe(true);
174
+ });
175
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Duration string parser
3
+ *
4
+ * Parses human-readable duration strings like "30s", "5m", "1h30m"
5
+ * into milliseconds.
6
+ *
7
+ * @see bf-x1q for implementation task
8
+ */
9
+
10
+ const UNITS: Record<string, number> = {
11
+ ms: 1,
12
+ s: 1000,
13
+ m: 60 * 1000,
14
+ h: 60 * 60 * 1000,
15
+ };
16
+
17
+ /**
18
+ * Validates a duration string without parsing it.
19
+ *
20
+ * @param input - Duration string to validate
21
+ * @returns true if valid, false otherwise
22
+ */
23
+ export function isValidDuration(input: string): boolean {
24
+ if (typeof input !== 'string') {
25
+ return false;
26
+ }
27
+
28
+ const trimmed = input.trim().toLowerCase();
29
+ if (!trimmed) {
30
+ return false;
31
+ }
32
+
33
+ // Plain number (milliseconds)
34
+ if (/^\d+$/.test(trimmed)) {
35
+ return true;
36
+ }
37
+
38
+ // Duration with units
39
+ const regex = /(\d+(?:\.\d+)?)(ms|s|m|h)/g;
40
+ let hasMatch = false;
41
+
42
+ // Check if entire string matches expected pattern
43
+ let lastIndex = 0;
44
+ let match;
45
+ while ((match = regex.exec(trimmed)) !== null) {
46
+ if (match.index !== lastIndex) {
47
+ return false; // Gap in matching
48
+ }
49
+ hasMatch = true;
50
+ lastIndex = regex.lastIndex;
51
+ }
52
+
53
+ return hasMatch && lastIndex === trimmed.length;
54
+ }
55
+
56
+ /**
57
+ * Parses a duration string into milliseconds.
58
+ *
59
+ * Supported formats:
60
+ * - "30s" -> 30000
61
+ * - "5m" -> 300000
62
+ * - "1h" -> 3600000
63
+ * - "1h30m" -> 5400000
64
+ * - "500ms" -> 500
65
+ * - "500" -> 500 (plain numbers treated as ms)
66
+ *
67
+ * @param input - Duration string to parse
68
+ * @returns Duration in milliseconds
69
+ * @throws Error if format is invalid
70
+ */
71
+ export function parseDuration(input: string | number): number {
72
+ if (typeof input === 'number') {
73
+ return input;
74
+ }
75
+
76
+ if (typeof input !== 'string' || !input.trim()) {
77
+ throw new Error('Duration must be a non-empty string');
78
+ }
79
+
80
+ const trimmed = input.trim().toLowerCase();
81
+
82
+ // Plain number (milliseconds)
83
+ if (/^\d+$/.test(trimmed)) {
84
+ return parseInt(trimmed, 10);
85
+ }
86
+
87
+ let total = 0;
88
+ const regex = /(\d+(?:\.\d+)?)(ms|s|m|h)/g;
89
+ let match;
90
+ let hasMatch = false;
91
+
92
+ while ((match = regex.exec(trimmed)) !== null) {
93
+ hasMatch = true;
94
+ const [, value, unit] = match;
95
+ total += parseFloat(value) * UNITS[unit];
96
+ }
97
+
98
+ if (!hasMatch) {
99
+ throw new Error(
100
+ `Invalid duration "${input}". Use format like "3s", "2m", "500ms", or "1m30s"`
101
+ );
102
+ }
103
+
104
+ return Math.round(total);
105
+ }
106
+
107
+ /**
108
+ * Formats milliseconds into a human-readable duration string.
109
+ *
110
+ * @param ms - Duration in milliseconds
111
+ * @returns Human-readable duration string
112
+ */
113
+ export function formatDuration(ms: number): string {
114
+ if (ms < 1000) {
115
+ return `${ms}ms`;
116
+ }
117
+
118
+ const hours = Math.floor(ms / 3600000);
119
+ const minutes = Math.floor((ms % 3600000) / 60000);
120
+ const seconds = Math.floor((ms % 60000) / 1000);
121
+
122
+ const parts: string[] = [];
123
+ if (hours > 0) parts.push(`${hours}h`);
124
+ if (minutes > 0) parts.push(`${minutes}m`);
125
+ if (seconds > 0) parts.push(`${seconds}s`);
126
+
127
+ return parts.join('') || '0s';
128
+ }
package/src/index.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @browserflow-ai/core - Shared types, schemas, and utilities
3
+ */
4
+
5
+ // Spec schema and types
6
+ export {
7
+ specSchema,
8
+ specStepSchema,
9
+ stepSchema,
10
+ actionTypeSchema,
11
+ verifyCheckSchema,
12
+ expectedOutcomeSchema,
13
+ highlightRegionSchema,
14
+ maskRegionSchema,
15
+ durationSchema,
16
+ targetSchema,
17
+ preconditionsSchema,
18
+ stateSchema,
19
+ type ActionType,
20
+ type VerifyCheck,
21
+ type HighlightRegion,
22
+ type MaskRegion,
23
+ type SpecStep,
24
+ type LegacySpecStep,
25
+ type ExpectedOutcome,
26
+ type BrowserFlowSpec,
27
+ type Target,
28
+ type State,
29
+ type Preconditions,
30
+ } from './spec-schema.js';
31
+
32
+ // Spec loading and normalization
33
+ export { normalizePreconditions, loadSpec } from './spec-loader.js';
34
+
35
+ // Locator types and resolution
36
+ export {
37
+ type LocatorObject,
38
+ type LocatorStrategy,
39
+ type LocatorStrategyType,
40
+ type LocatorScoping,
41
+ type LocatorProof,
42
+ type DOMFingerprint,
43
+ type BoundingBox,
44
+ type ResolveOptions,
45
+ type LocatorMethod,
46
+ type LocatorArgs,
47
+ type LegacyLocatorObject,
48
+ locatorObjectSchema,
49
+ locatorStrategySchema,
50
+ locatorStrategyTypeSchema,
51
+ locatorScopingSchema,
52
+ locatorProofSchema,
53
+ domFingerprintSchema,
54
+ boundingBoxSchema,
55
+ resolveLocator,
56
+ strategyToLocator,
57
+ resolveLegacyLocator,
58
+ } from './locator-object.js';
59
+
60
+ // Lockfile types
61
+ export {
62
+ type Lockfile,
63
+ type Mask,
64
+ type Assertion,
65
+ type AssertionType,
66
+ type GenerationMetadata,
67
+ type ExplorationReport,
68
+ type ExplorationLockfile,
69
+ type ExplorationStep,
70
+ type StepExecution,
71
+ type OutcomeCheck,
72
+ type ExplorationError,
73
+ lockfileSchema,
74
+ maskSchema,
75
+ assertionSchema,
76
+ assertionTypeSchema,
77
+ generationMetadataSchema,
78
+ validateLockfile,
79
+ computeSpecHash,
80
+ readLockfile,
81
+ writeLockfile,
82
+ } from './lockfile.js';
83
+
84
+ // Duration utilities
85
+ export { parseDuration, formatDuration, isValidDuration } from './duration.js';
86
+
87
+ // Run store utilities
88
+ export {
89
+ type RunDirectoryPaths,
90
+ type RunStore,
91
+ createRunStore,
92
+ createRunId,
93
+ getRunPaths,
94
+ generateExplorationId,
95
+ getScreenshotPath,
96
+ } from './run-store.js';
97
+
98
+ // Configuration types
99
+ export {
100
+ type GeneratorOutputFormat,
101
+ type BrowserType,
102
+ type PlaywrightProject,
103
+ type PlaywrightConfigOptions,
104
+ type GeneratorConfig,
105
+ type ReviewStepData,
106
+ type ReviewData,
107
+ type GeneratedTest,
108
+ type GeneratedConfig,
109
+ } from './config.js';
110
+
111
+ // Browserflow config schema (for browserflow.yaml validation)
112
+ export {
113
+ browserflowConfigSchema,
114
+ browserTypeSchema,
115
+ viewportSchema,
116
+ projectConfigSchema,
117
+ runtimeConfigSchema,
118
+ locatorsConfigSchema,
119
+ explorationConfigSchema,
120
+ reviewConfigSchema,
121
+ outputConfigSchema,
122
+ ciConfigSchema,
123
+ validateBrowserflowConfig,
124
+ parseBrowserflowConfig,
125
+ type BrowserTypeConfig,
126
+ type ViewportConfig,
127
+ type ProjectConfig,
128
+ type RuntimeConfig,
129
+ type LocatorsConfig,
130
+ type ExplorationConfig,
131
+ type ReviewConfig,
132
+ type OutputConfig,
133
+ type CiConfig,
134
+ type BrowserflowConfig,
135
+ } from './config-schema.js';
136
+