@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.
- package/dist/config-schema.d.ts +354 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +83 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +107 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/duration.d.ts +39 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +111 -0
- package/dist/duration.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/locator-object.d.ts +556 -0
- package/dist/locator-object.d.ts.map +1 -0
- package/dist/locator-object.js +114 -0
- package/dist/locator-object.js.map +1 -0
- package/dist/lockfile.d.ts +1501 -0
- package/dist/lockfile.d.ts.map +1 -0
- package/dist/lockfile.js +86 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/run-store.d.ts +81 -0
- package/dist/run-store.d.ts.map +1 -0
- package/dist/run-store.js +181 -0
- package/dist/run-store.js.map +1 -0
- package/dist/spec-loader.d.ts +17 -0
- package/dist/spec-loader.d.ts.map +1 -0
- package/dist/spec-loader.js +37 -0
- package/dist/spec-loader.js.map +1 -0
- package/dist/spec-schema.d.ts +1411 -0
- package/dist/spec-schema.d.ts.map +1 -0
- package/dist/spec-schema.js +239 -0
- package/dist/spec-schema.js.map +1 -0
- package/package.json +45 -0
- package/schemas/browserflow.schema.json +220 -0
- package/schemas/lockfile.schema.json +568 -0
- package/schemas/spec-v2.schema.json +413 -0
- package/src/config-schema.test.ts +190 -0
- package/src/config-schema.ts +111 -0
- package/src/config.ts +112 -0
- package/src/duration.test.ts +175 -0
- package/src/duration.ts +128 -0
- package/src/index.ts +136 -0
- package/src/json-schemas.test.ts +374 -0
- package/src/locator-object.test.ts +323 -0
- package/src/locator-object.ts +250 -0
- package/src/lockfile.test.ts +345 -0
- package/src/lockfile.ts +209 -0
- package/src/run-store.test.ts +232 -0
- package/src/run-store.ts +240 -0
- package/src/spec-loader.test.ts +181 -0
- package/src/spec-loader.ts +47 -0
- package/src/spec-schema.test.ts +360 -0
- package/src/spec-schema.ts +347 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for generated JSON schemas
|
|
3
|
+
*
|
|
4
|
+
* Validates that the generated JSON schemas correctly accept/reject data
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
8
|
+
import Ajv from 'ajv';
|
|
9
|
+
import addFormats from 'ajv-formats';
|
|
10
|
+
import { readFile } from 'fs/promises';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const SCHEMAS_DIR = join(__dirname, '../schemas');
|
|
16
|
+
|
|
17
|
+
interface JsonSchema {
|
|
18
|
+
$schema: string;
|
|
19
|
+
$id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let ajv: Ajv;
|
|
26
|
+
let specSchema: JsonSchema;
|
|
27
|
+
let configSchema: JsonSchema;
|
|
28
|
+
let lockfileSchema: JsonSchema;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
// Initialize Ajv with format support
|
|
32
|
+
ajv = new Ajv({
|
|
33
|
+
allErrors: true,
|
|
34
|
+
strict: false, // Allow draft-07 features
|
|
35
|
+
});
|
|
36
|
+
addFormats(ajv);
|
|
37
|
+
|
|
38
|
+
// Load all schemas
|
|
39
|
+
specSchema = JSON.parse(await readFile(join(SCHEMAS_DIR, 'spec-v2.schema.json'), 'utf-8'));
|
|
40
|
+
configSchema = JSON.parse(await readFile(join(SCHEMAS_DIR, 'browserflow.schema.json'), 'utf-8'));
|
|
41
|
+
lockfileSchema = JSON.parse(await readFile(join(SCHEMAS_DIR, 'lockfile.schema.json'), 'utf-8'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('spec-v2.schema.json', () => {
|
|
45
|
+
test('validates a valid spec', () => {
|
|
46
|
+
const spec = {
|
|
47
|
+
version: 2,
|
|
48
|
+
name: 'test-spec',
|
|
49
|
+
steps: [
|
|
50
|
+
{
|
|
51
|
+
id: 'step-1',
|
|
52
|
+
action: 'navigate',
|
|
53
|
+
url: 'https://example.com',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
const validate = ajv.compile(specSchema);
|
|
58
|
+
const valid = validate(spec);
|
|
59
|
+
expect(valid).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('validates spec with target', () => {
|
|
63
|
+
const spec = {
|
|
64
|
+
version: 2,
|
|
65
|
+
name: 'click-test',
|
|
66
|
+
steps: [
|
|
67
|
+
{
|
|
68
|
+
id: 'click-btn',
|
|
69
|
+
action: 'click',
|
|
70
|
+
target: {
|
|
71
|
+
testid: 'submit-button',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const validate = ajv.compile(specSchema);
|
|
77
|
+
const valid = validate(spec);
|
|
78
|
+
expect(valid).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('rejects spec without version', () => {
|
|
82
|
+
const spec = {
|
|
83
|
+
name: 'test-spec',
|
|
84
|
+
steps: [{ id: 'step-1', action: 'navigate' }],
|
|
85
|
+
};
|
|
86
|
+
const validate = ajv.compile(specSchema);
|
|
87
|
+
const valid = validate(spec);
|
|
88
|
+
expect(valid).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('rejects spec with invalid name format', () => {
|
|
92
|
+
const spec = {
|
|
93
|
+
version: 2,
|
|
94
|
+
name: 'Test Spec', // Should be kebab-case
|
|
95
|
+
steps: [{ id: 'step-1', action: 'navigate' }],
|
|
96
|
+
};
|
|
97
|
+
const validate = ajv.compile(specSchema);
|
|
98
|
+
const valid = validate(spec);
|
|
99
|
+
expect(valid).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('rejects spec without steps', () => {
|
|
103
|
+
const spec = {
|
|
104
|
+
version: 2,
|
|
105
|
+
name: 'test-spec',
|
|
106
|
+
};
|
|
107
|
+
const validate = ajv.compile(specSchema);
|
|
108
|
+
const valid = validate(spec);
|
|
109
|
+
expect(valid).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('rejects spec with empty steps', () => {
|
|
113
|
+
const spec = {
|
|
114
|
+
version: 2,
|
|
115
|
+
name: 'test-spec',
|
|
116
|
+
steps: [],
|
|
117
|
+
};
|
|
118
|
+
const validate = ajv.compile(specSchema);
|
|
119
|
+
const valid = validate(spec);
|
|
120
|
+
expect(valid).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('rejects step without id', () => {
|
|
124
|
+
const spec = {
|
|
125
|
+
version: 2,
|
|
126
|
+
name: 'test-spec',
|
|
127
|
+
steps: [{ action: 'navigate' }],
|
|
128
|
+
};
|
|
129
|
+
const validate = ajv.compile(specSchema);
|
|
130
|
+
const valid = validate(spec);
|
|
131
|
+
expect(valid).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('rejects invalid action type', () => {
|
|
135
|
+
const spec = {
|
|
136
|
+
version: 2,
|
|
137
|
+
name: 'test-spec',
|
|
138
|
+
steps: [{ id: 'step-1', action: 'invalid_action' }],
|
|
139
|
+
};
|
|
140
|
+
const validate = ajv.compile(specSchema);
|
|
141
|
+
const valid = validate(spec);
|
|
142
|
+
expect(valid).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('validates spec with reload action', () => {
|
|
146
|
+
const spec = {
|
|
147
|
+
version: 2,
|
|
148
|
+
name: 'reload-test',
|
|
149
|
+
steps: [
|
|
150
|
+
{
|
|
151
|
+
id: 'reload-page',
|
|
152
|
+
action: 'reload',
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
const validate = ajv.compile(specSchema);
|
|
157
|
+
const valid = validate(spec);
|
|
158
|
+
expect(valid).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('validates spec with scroll action', () => {
|
|
162
|
+
const spec = {
|
|
163
|
+
version: 2,
|
|
164
|
+
name: 'scroll-test',
|
|
165
|
+
steps: [
|
|
166
|
+
{
|
|
167
|
+
id: 'scroll-down',
|
|
168
|
+
action: 'scroll',
|
|
169
|
+
target: { css: '.content' },
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
const validate = ajv.compile(specSchema);
|
|
174
|
+
const valid = validate(spec);
|
|
175
|
+
expect(valid).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('validates spec with press action', () => {
|
|
179
|
+
const spec = {
|
|
180
|
+
version: 2,
|
|
181
|
+
name: 'press-test',
|
|
182
|
+
steps: [
|
|
183
|
+
{
|
|
184
|
+
id: 'press-enter',
|
|
185
|
+
action: 'press',
|
|
186
|
+
value: 'Enter',
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
const validate = ajv.compile(specSchema);
|
|
191
|
+
const valid = validate(spec);
|
|
192
|
+
expect(valid).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('validates spec with upload action', () => {
|
|
196
|
+
const spec = {
|
|
197
|
+
version: 2,
|
|
198
|
+
name: 'upload-test',
|
|
199
|
+
steps: [
|
|
200
|
+
{
|
|
201
|
+
id: 'upload-file',
|
|
202
|
+
action: 'upload',
|
|
203
|
+
target: { testid: 'file-input' },
|
|
204
|
+
value: './test-file.pdf',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
const validate = ajv.compile(specSchema);
|
|
209
|
+
const valid = validate(spec);
|
|
210
|
+
expect(valid).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('validates spec with scroll_into_view action', () => {
|
|
214
|
+
const spec = {
|
|
215
|
+
version: 2,
|
|
216
|
+
name: 'scroll-into-view-test',
|
|
217
|
+
steps: [
|
|
218
|
+
{
|
|
219
|
+
id: 'scroll-element',
|
|
220
|
+
action: 'scroll_into_view',
|
|
221
|
+
target: { css: '#footer' },
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
const validate = ajv.compile(specSchema);
|
|
226
|
+
const valid = validate(spec);
|
|
227
|
+
expect(valid).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('browserflow.schema.json', () => {
|
|
232
|
+
test('validates a valid config', () => {
|
|
233
|
+
const config = {
|
|
234
|
+
project: {
|
|
235
|
+
name: 'my-project',
|
|
236
|
+
base_url: 'http://localhost:3000',
|
|
237
|
+
},
|
|
238
|
+
runtime: {
|
|
239
|
+
browser: 'chromium',
|
|
240
|
+
headless: true,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
const validate = ajv.compile(configSchema);
|
|
244
|
+
const valid = validate(config);
|
|
245
|
+
expect(valid).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('validates minimal config', () => {
|
|
249
|
+
const config = {
|
|
250
|
+
project: { name: 'test' },
|
|
251
|
+
};
|
|
252
|
+
const validate = ajv.compile(configSchema);
|
|
253
|
+
const valid = validate(config);
|
|
254
|
+
expect(valid).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('rejects config without project', () => {
|
|
258
|
+
const config = {
|
|
259
|
+
runtime: { browser: 'chromium' },
|
|
260
|
+
};
|
|
261
|
+
const validate = ajv.compile(configSchema);
|
|
262
|
+
const valid = validate(config);
|
|
263
|
+
expect(valid).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('rejects invalid browser type', () => {
|
|
267
|
+
const config = {
|
|
268
|
+
project: { name: 'test' },
|
|
269
|
+
runtime: { browser: 'chrome' }, // Invalid
|
|
270
|
+
};
|
|
271
|
+
const validate = ajv.compile(configSchema);
|
|
272
|
+
const valid = validate(config);
|
|
273
|
+
expect(valid).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('lockfile.schema.json', () => {
|
|
278
|
+
test('validates a valid lockfile', () => {
|
|
279
|
+
const lockfile = {
|
|
280
|
+
run_id: 'run-123',
|
|
281
|
+
spec_name: 'test-spec',
|
|
282
|
+
spec_hash: 'abc123',
|
|
283
|
+
created_at: '2026-01-15T00:00:00Z',
|
|
284
|
+
locators: {},
|
|
285
|
+
masks: {},
|
|
286
|
+
assertions: [],
|
|
287
|
+
generation: {
|
|
288
|
+
format: 'playwright-ts',
|
|
289
|
+
output_path: 'e2e/tests/test-spec.spec.ts',
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
const validate = ajv.compile(lockfileSchema);
|
|
293
|
+
const valid = validate(lockfile);
|
|
294
|
+
expect(valid).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('validates lockfile with locators', () => {
|
|
298
|
+
const lockfile = {
|
|
299
|
+
run_id: 'run-123',
|
|
300
|
+
spec_name: 'test-spec',
|
|
301
|
+
spec_hash: 'abc123',
|
|
302
|
+
created_at: '2026-01-15T00:00:00Z',
|
|
303
|
+
locators: {
|
|
304
|
+
'step-1': {
|
|
305
|
+
locator_id: 'loc-1',
|
|
306
|
+
preferred: {
|
|
307
|
+
type: 'testid',
|
|
308
|
+
value: 'submit-btn',
|
|
309
|
+
},
|
|
310
|
+
fallbacks: [],
|
|
311
|
+
proof: {},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
masks: {},
|
|
315
|
+
assertions: [],
|
|
316
|
+
generation: {
|
|
317
|
+
format: 'playwright-ts',
|
|
318
|
+
output_path: 'e2e/tests/test-spec.spec.ts',
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
const validate = ajv.compile(lockfileSchema);
|
|
322
|
+
const valid = validate(lockfile);
|
|
323
|
+
expect(valid).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('rejects lockfile missing required fields', () => {
|
|
327
|
+
const lockfile = {
|
|
328
|
+
run_id: 'run-123',
|
|
329
|
+
// Missing spec_name, spec_hash, etc.
|
|
330
|
+
};
|
|
331
|
+
const validate = ajv.compile(lockfileSchema);
|
|
332
|
+
const valid = validate(lockfile);
|
|
333
|
+
expect(valid).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('rejects lockfile with invalid generation format', () => {
|
|
337
|
+
const lockfile = {
|
|
338
|
+
run_id: 'run-123',
|
|
339
|
+
spec_name: 'test-spec',
|
|
340
|
+
spec_hash: 'abc123',
|
|
341
|
+
created_at: '2026-01-15T00:00:00Z',
|
|
342
|
+
locators: {},
|
|
343
|
+
masks: {},
|
|
344
|
+
assertions: [],
|
|
345
|
+
generation: {
|
|
346
|
+
format: 'invalid-format', // Should be playwright-ts
|
|
347
|
+
output_path: 'test.ts',
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
const validate = ajv.compile(lockfileSchema);
|
|
351
|
+
const valid = validate(lockfile);
|
|
352
|
+
expect(valid).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('schema metadata', () => {
|
|
357
|
+
test('spec schema has correct metadata', () => {
|
|
358
|
+
expect(specSchema.$id).toContain('spec-v2.schema.json');
|
|
359
|
+
expect(specSchema.title).toBe('BrowserFlow Spec v2');
|
|
360
|
+
expect(specSchema.description).toContain('test specifications');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('config schema has correct metadata', () => {
|
|
364
|
+
expect(configSchema.$id).toContain('browserflow.schema.json');
|
|
365
|
+
expect(configSchema.title).toBe('BrowserFlow Configuration');
|
|
366
|
+
expect(configSchema.description).toContain('configuration files');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('lockfile schema has correct metadata', () => {
|
|
370
|
+
expect(lockfileSchema.$id).toContain('lockfile.schema.json');
|
|
371
|
+
expect(lockfileSchema.title).toBe('BrowserFlow Lockfile');
|
|
372
|
+
expect(lockfileSchema.description).toContain('lockfile.json');
|
|
373
|
+
});
|
|
374
|
+
});
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for LocatorObject types and resolution
|
|
3
|
+
* @see bf-6ig
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test, mock } from 'bun:test';
|
|
7
|
+
import {
|
|
8
|
+
locatorObjectSchema,
|
|
9
|
+
locatorStrategySchema,
|
|
10
|
+
strategyToLocator,
|
|
11
|
+
resolveLocator,
|
|
12
|
+
type LocatorObject,
|
|
13
|
+
type LocatorStrategy,
|
|
14
|
+
} from './locator-object.js';
|
|
15
|
+
|
|
16
|
+
describe('locatorStrategySchema', () => {
|
|
17
|
+
test('validates testid strategy', () => {
|
|
18
|
+
const result = locatorStrategySchema.safeParse({
|
|
19
|
+
type: 'testid',
|
|
20
|
+
value: 'submit-button',
|
|
21
|
+
});
|
|
22
|
+
expect(result.success).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('validates testid with custom attribute', () => {
|
|
26
|
+
const result = locatorStrategySchema.safeParse({
|
|
27
|
+
type: 'testid',
|
|
28
|
+
value: 'submit-button',
|
|
29
|
+
attribute: 'data-qa',
|
|
30
|
+
});
|
|
31
|
+
expect(result.success).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('validates role strategy', () => {
|
|
35
|
+
const result = locatorStrategySchema.safeParse({
|
|
36
|
+
type: 'role',
|
|
37
|
+
role: 'button',
|
|
38
|
+
name: 'Submit',
|
|
39
|
+
});
|
|
40
|
+
expect(result.success).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('validates role with exact matching', () => {
|
|
44
|
+
const result = locatorStrategySchema.safeParse({
|
|
45
|
+
type: 'role',
|
|
46
|
+
role: 'button',
|
|
47
|
+
name: 'Submit',
|
|
48
|
+
exact: false,
|
|
49
|
+
});
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('validates label strategy', () => {
|
|
54
|
+
const result = locatorStrategySchema.safeParse({
|
|
55
|
+
type: 'label',
|
|
56
|
+
text: 'Email Address',
|
|
57
|
+
});
|
|
58
|
+
expect(result.success).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('validates placeholder strategy', () => {
|
|
62
|
+
const result = locatorStrategySchema.safeParse({
|
|
63
|
+
type: 'placeholder',
|
|
64
|
+
text: 'Enter your email',
|
|
65
|
+
});
|
|
66
|
+
expect(result.success).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('validates text strategy', () => {
|
|
70
|
+
const result = locatorStrategySchema.safeParse({
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: 'Click here',
|
|
73
|
+
});
|
|
74
|
+
expect(result.success).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('validates css strategy', () => {
|
|
78
|
+
const result = locatorStrategySchema.safeParse({
|
|
79
|
+
type: 'css',
|
|
80
|
+
selector: '.submit-btn',
|
|
81
|
+
});
|
|
82
|
+
expect(result.success).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('rejects invalid strategy type', () => {
|
|
86
|
+
const result = locatorStrategySchema.safeParse({
|
|
87
|
+
type: 'invalid',
|
|
88
|
+
value: 'test',
|
|
89
|
+
});
|
|
90
|
+
expect(result.success).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('locatorObjectSchema', () => {
|
|
95
|
+
test('validates complete locator object', () => {
|
|
96
|
+
const locatorObj: LocatorObject = {
|
|
97
|
+
locator_id: 'loc-123',
|
|
98
|
+
preferred: { type: 'testid', value: 'submit-btn' },
|
|
99
|
+
fallbacks: [
|
|
100
|
+
{ type: 'role', role: 'button', name: 'Submit' },
|
|
101
|
+
{ type: 'css', selector: '.submit-btn' },
|
|
102
|
+
],
|
|
103
|
+
proof: {
|
|
104
|
+
a11y_role: 'button',
|
|
105
|
+
a11y_name: 'Submit',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const result = locatorObjectSchema.safeParse(locatorObj);
|
|
109
|
+
expect(result.success).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('validates locator with scoping', () => {
|
|
113
|
+
const locatorObj: LocatorObject = {
|
|
114
|
+
locator_id: 'loc-456',
|
|
115
|
+
preferred: { type: 'role', role: 'button', name: 'Delete' },
|
|
116
|
+
fallbacks: [],
|
|
117
|
+
scoping: {
|
|
118
|
+
within: [{ type: 'testid', value: 'user-list' }],
|
|
119
|
+
nth: 2,
|
|
120
|
+
},
|
|
121
|
+
proof: {},
|
|
122
|
+
};
|
|
123
|
+
const result = locatorObjectSchema.safeParse(locatorObj);
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('validates locator with DOM fingerprint', () => {
|
|
128
|
+
const locatorObj: LocatorObject = {
|
|
129
|
+
locator_id: 'loc-789',
|
|
130
|
+
preferred: { type: 'css', selector: 'button.primary' },
|
|
131
|
+
fallbacks: [],
|
|
132
|
+
proof: {
|
|
133
|
+
dom_fingerprint: {
|
|
134
|
+
tag: 'button',
|
|
135
|
+
classes: ['primary', 'submit'],
|
|
136
|
+
attributes: { type: 'submit' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const result = locatorObjectSchema.safeParse(locatorObj);
|
|
141
|
+
expect(result.success).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('validates locator with bounding box', () => {
|
|
145
|
+
const locatorObj: LocatorObject = {
|
|
146
|
+
locator_id: 'loc-abc',
|
|
147
|
+
preferred: { type: 'testid', value: 'btn' },
|
|
148
|
+
fallbacks: [],
|
|
149
|
+
proof: {
|
|
150
|
+
bounding_box: { x: 100, y: 200, width: 50, height: 30 },
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const result = locatorObjectSchema.safeParse(locatorObj);
|
|
154
|
+
expect(result.success).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('requires locator_id', () => {
|
|
158
|
+
const result = locatorObjectSchema.safeParse({
|
|
159
|
+
preferred: { type: 'testid', value: 'btn' },
|
|
160
|
+
fallbacks: [],
|
|
161
|
+
proof: {},
|
|
162
|
+
});
|
|
163
|
+
expect(result.success).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('requires preferred strategy', () => {
|
|
167
|
+
const result = locatorObjectSchema.safeParse({
|
|
168
|
+
locator_id: 'loc-123',
|
|
169
|
+
fallbacks: [],
|
|
170
|
+
proof: {},
|
|
171
|
+
});
|
|
172
|
+
expect(result.success).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('strategyToLocator', () => {
|
|
177
|
+
// Mock Playwright Page object
|
|
178
|
+
const mockPage = {
|
|
179
|
+
getByTestId: mock((id: string) => ({ _testid: id })),
|
|
180
|
+
getByRole: mock((role: string, options?: { name?: string; exact?: boolean }) => ({
|
|
181
|
+
_role: role,
|
|
182
|
+
_options: options,
|
|
183
|
+
})),
|
|
184
|
+
getByLabel: mock((text: string) => ({ _label: text })),
|
|
185
|
+
getByPlaceholder: mock((text: string) => ({ _placeholder: text })),
|
|
186
|
+
getByText: mock((text: string) => ({ _text: text })),
|
|
187
|
+
locator: mock((selector: string) => ({ _selector: selector })),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
test('converts testid strategy', () => {
|
|
191
|
+
const strategy: LocatorStrategy = { type: 'testid', value: 'submit-btn' };
|
|
192
|
+
const result = strategyToLocator(strategy, mockPage as any);
|
|
193
|
+
expect(mockPage.getByTestId).toHaveBeenCalledWith('submit-btn');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('converts testid with custom attribute', () => {
|
|
197
|
+
const strategy: LocatorStrategy = {
|
|
198
|
+
type: 'testid',
|
|
199
|
+
value: 'submit-btn',
|
|
200
|
+
attribute: 'data-qa',
|
|
201
|
+
};
|
|
202
|
+
const result = strategyToLocator(strategy, mockPage as any);
|
|
203
|
+
expect(mockPage.locator).toHaveBeenCalledWith('[data-qa="submit-btn"]');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('converts role strategy', () => {
|
|
207
|
+
const strategy: LocatorStrategy = {
|
|
208
|
+
type: 'role',
|
|
209
|
+
role: 'button',
|
|
210
|
+
name: 'Submit',
|
|
211
|
+
};
|
|
212
|
+
strategyToLocator(strategy, mockPage as any);
|
|
213
|
+
expect(mockPage.getByRole).toHaveBeenCalledWith('button', {
|
|
214
|
+
name: 'Submit',
|
|
215
|
+
exact: true,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('converts role with exact=false', () => {
|
|
220
|
+
const strategy: LocatorStrategy = {
|
|
221
|
+
type: 'role',
|
|
222
|
+
role: 'button',
|
|
223
|
+
name: 'Submit',
|
|
224
|
+
exact: false,
|
|
225
|
+
};
|
|
226
|
+
strategyToLocator(strategy, mockPage as any);
|
|
227
|
+
expect(mockPage.getByRole).toHaveBeenCalledWith('button', {
|
|
228
|
+
name: 'Submit',
|
|
229
|
+
exact: false,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('converts label strategy', () => {
|
|
234
|
+
const strategy: LocatorStrategy = { type: 'label', text: 'Email' };
|
|
235
|
+
strategyToLocator(strategy, mockPage as any);
|
|
236
|
+
expect(mockPage.getByLabel).toHaveBeenCalledWith('Email');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('converts placeholder strategy', () => {
|
|
240
|
+
const strategy: LocatorStrategy = {
|
|
241
|
+
type: 'placeholder',
|
|
242
|
+
text: 'Enter email',
|
|
243
|
+
};
|
|
244
|
+
strategyToLocator(strategy, mockPage as any);
|
|
245
|
+
expect(mockPage.getByPlaceholder).toHaveBeenCalledWith('Enter email');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('converts text strategy', () => {
|
|
249
|
+
const strategy: LocatorStrategy = { type: 'text', text: 'Click here' };
|
|
250
|
+
strategyToLocator(strategy, mockPage as any);
|
|
251
|
+
expect(mockPage.getByText).toHaveBeenCalledWith('Click here');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('converts css strategy', () => {
|
|
255
|
+
const strategy: LocatorStrategy = { type: 'css', selector: '.submit-btn' };
|
|
256
|
+
strategyToLocator(strategy, mockPage as any);
|
|
257
|
+
expect(mockPage.locator).toHaveBeenCalledWith('.submit-btn');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('resolveLocator', () => {
|
|
262
|
+
// Mock Playwright Page with chainable locator
|
|
263
|
+
const createMockPage = () => {
|
|
264
|
+
const locatorResult = {
|
|
265
|
+
_selector: '',
|
|
266
|
+
nth: mock((n: number) => ({ ...locatorResult, _nth: n })),
|
|
267
|
+
locator: mock((loc: any) => ({ ...locatorResult, _chained: loc })),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
getByTestId: mock((id: string) => ({ ...locatorResult, _testid: id })),
|
|
272
|
+
getByRole: mock((role: string, options?: any) => ({
|
|
273
|
+
...locatorResult,
|
|
274
|
+
_role: role,
|
|
275
|
+
_options: options,
|
|
276
|
+
})),
|
|
277
|
+
getByLabel: mock(() => locatorResult),
|
|
278
|
+
getByPlaceholder: mock(() => locatorResult),
|
|
279
|
+
getByText: mock(() => locatorResult),
|
|
280
|
+
locator: mock((selector: string) => ({ ...locatorResult, _selector: selector })),
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
test('resolves locator using preferred strategy', () => {
|
|
285
|
+
const mockPage = createMockPage();
|
|
286
|
+
const locator: LocatorObject = {
|
|
287
|
+
locator_id: 'loc-1',
|
|
288
|
+
preferred: { type: 'testid', value: 'submit-btn' },
|
|
289
|
+
fallbacks: [],
|
|
290
|
+
proof: {},
|
|
291
|
+
};
|
|
292
|
+
resolveLocator(locator, mockPage as any, { useFallbacks: false });
|
|
293
|
+
expect(mockPage.getByTestId).toHaveBeenCalledWith('submit-btn');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('applies nth scoping', () => {
|
|
297
|
+
const mockPage = createMockPage();
|
|
298
|
+
const locator: LocatorObject = {
|
|
299
|
+
locator_id: 'loc-2',
|
|
300
|
+
preferred: { type: 'role', role: 'listitem' },
|
|
301
|
+
fallbacks: [],
|
|
302
|
+
scoping: { nth: 2 },
|
|
303
|
+
proof: {},
|
|
304
|
+
};
|
|
305
|
+
const result = resolveLocator(locator, mockPage as any, { useFallbacks: false });
|
|
306
|
+
expect(result._nth).toBe(2);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('applies within scoping', () => {
|
|
310
|
+
const mockPage = createMockPage();
|
|
311
|
+
const locator: LocatorObject = {
|
|
312
|
+
locator_id: 'loc-3',
|
|
313
|
+
preferred: { type: 'role', role: 'button', name: 'Delete' },
|
|
314
|
+
fallbacks: [],
|
|
315
|
+
scoping: {
|
|
316
|
+
within: [{ type: 'testid', value: 'user-list' }],
|
|
317
|
+
},
|
|
318
|
+
proof: {},
|
|
319
|
+
};
|
|
320
|
+
resolveLocator(locator, mockPage as any, { useFallbacks: false });
|
|
321
|
+
expect(mockPage.getByTestId).toHaveBeenCalledWith('user-list');
|
|
322
|
+
});
|
|
323
|
+
});
|