@allwright.dev/core 0.0.31 → 0.0.33

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/index.js CHANGED
@@ -1,673 +1,31 @@
1
- import path from "node:path";
2
- import fs from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import grpc from "@grpc/grpc-js";
5
- import protoLoader from "@grpc/proto-loader";
6
- const DEFAULT_SERVER_ADDR = "127.0.0.1:50051";
7
- const SERVER_ADDR_ENV_VAR = "ALLWRIGHT_SERVER_ADDR";
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
- const PACKAGE_ROOT = path.resolve(__dirname, "..");
11
- const PROTO_ROOT = path.join(PACKAGE_ROOT, "proto");
12
- const ENGINE_PROTO_PATH = path.join(PROTO_ROOT, "engine", "v1", "engine.proto");
13
- let runtimePromise = null;
14
- let serverAddrOverride = null;
15
- const CONFIG_FILENAMES = [
16
- "allwright.config.yaml",
17
- "allwright.config.yml",
18
- "allwright.config.json",
19
- ".allwright/config.yaml",
20
- ".allwright/config.yml",
21
- ".allwright/config.json",
22
- ];
23
- class EventQueue {
24
- #items = [];
25
- #waiters = [];
26
- #endedError = null;
27
- push(item) {
28
- const waiter = this.#waiters.shift();
29
- if (waiter) {
30
- waiter.resolve(item);
31
- return;
32
- }
33
- this.#items.push(item);
34
- }
35
- fail(error) {
36
- if (this.#endedError) {
37
- return;
38
- }
39
- this.#endedError = error;
40
- while (this.#waiters.length > 0) {
41
- this.#waiters.shift().reject(error);
42
- }
43
- }
44
- async next() {
45
- if (this.#items.length > 0) {
46
- return this.#items.shift();
47
- }
48
- if (this.#endedError) {
49
- throw this.#endedError;
50
- }
51
- return new Promise((resolve, reject) => {
52
- this.#waiters.push({ resolve, reject });
53
- });
54
- }
55
- }
56
- class BrowserTypeImpl {
57
- #browserKind;
58
- constructor(browserKind = "chromium") {
59
- this.#browserKind = browserKind;
60
- }
61
- async launch(options = {}) {
62
- return launchBrowser(this.#browserKind, options);
63
- }
64
- }
65
- class BrowserImpl {
66
- #closed = false;
67
- #runtime;
68
- #stream;
69
- #queue;
70
- #pages = new Map();
71
- #initialPage;
72
- constructor(state) {
73
- const browserInfo = {
74
- sessionId: state.sessionId,
75
- browserName: state.launched.browser ?? "",
76
- launchNote: state.launched.note ?? "",
77
- cdpWebSocketURL: "",
78
- userDataDir: state.launched.userDataDir ?? "",
79
- };
80
- this.#runtime = state.runtime;
81
- this.#stream = state.stream;
82
- this.#queue = state.queue;
83
- this.sessionId = browserInfo.sessionId;
84
- this.browserName = browserInfo.browserName;
85
- this.launchNote = browserInfo.launchNote;
86
- this.cdpWebSocketURL = browserInfo.cdpWebSocketURL;
87
- this.userDataDir = browserInfo.userDataDir;
88
- this.#initialPage = this.#createPage(state.launched.initialTabSessionId ?? "");
89
- }
90
- sessionId;
91
- browserName;
92
- launchNote;
93
- cdpWebSocketURL;
94
- userDataDir;
95
- page() {
96
- return this.#initialPage;
97
- }
98
- initialPage() {
99
- return this.#initialPage;
100
- }
101
- pages() {
102
- return [...this.#pages.values()];
103
- }
104
- async newPage(options = {}) {
105
- this.#ensureOpen();
106
- this.#stream.write({
107
- openTab: {
108
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
109
- },
110
- });
111
- while (true) {
112
- const event = await this.#queue.next();
113
- if (event.tabOpened?.tabSessionId) {
114
- return this.#createPage(event.tabOpened.tabSessionId);
115
- }
116
- if (event.error?.message) {
117
- throw new Error(`browser session error while opening tab: ${event.error.message}`);
118
- }
119
- }
120
- }
121
- async close() {
122
- if (this.#closed) {
123
- return;
124
- }
125
- this.#stream.write({
126
- close: {},
127
- });
128
- while (true) {
129
- const event = await this.#queue.next();
130
- if (event.closed) {
131
- this.#closed = true;
132
- this.#stream.end();
133
- return;
134
- }
135
- if (event.error?.message) {
136
- throw new Error(`browser session error while closing: ${event.error.message}`);
137
- }
138
- }
139
- }
140
- async ping(message = "ping") {
141
- this.#ensureOpen();
142
- this.#stream.write({
143
- ping: {
144
- message,
145
- },
146
- });
147
- while (true) {
148
- const event = await this.#queue.next();
149
- if (event.pong?.message) {
150
- return event.pong.message;
151
- }
152
- if (event.error?.message) {
153
- throw new Error(`browser session error while pinging: ${event.error.message}`);
154
- }
155
- }
156
- }
157
- browserInfo() {
158
- return {
159
- sessionId: this.sessionId,
160
- browserName: this.browserName,
161
- launchNote: this.launchNote,
162
- cdpWebSocketURL: this.cdpWebSocketURL,
163
- userDataDir: this.userDataDir,
164
- };
165
- }
166
- initialTab() {
167
- return this.initialPage();
168
- }
169
- async newTab(options = {}) {
170
- return this.newPage(options);
171
- }
172
- #createPage(sessionId) {
173
- const existing = this.#pages.get(sessionId);
174
- if (existing) {
175
- return existing;
176
- }
177
- const page = new PageImpl({
178
- runtime: this.#runtime,
179
- browserSessionId: this.sessionId,
180
- sessionId,
181
- });
182
- this.#pages.set(sessionId, page);
183
- return page;
184
- }
185
- #ensureOpen() {
186
- if (this.#closed) {
187
- throw new Error(`browser session ${this.sessionId} is closed`);
188
- }
189
- }
190
- }
191
- class PageImpl {
192
- #runtime;
193
- #handlePromise = null;
194
- constructor(input) {
195
- this.#runtime = input.runtime;
196
- this.sessionId = input.sessionId;
197
- this.browserSessionId = input.browserSessionId;
198
- }
199
- sessionId;
200
- browserSessionId;
201
- locator(selector) {
202
- return new LocatorImpl({ page: this, selector });
203
- }
204
- async goto(url, options = {}) {
205
- const handle = await this.#getHandle();
206
- this.#ensureOpen(handle);
207
- handle.stream.write({
208
- browserSessionId: this.browserSessionId,
209
- tabSessionId: this.sessionId,
210
- navigate: {
211
- url,
212
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
213
- },
214
- });
215
- let navigated = null;
216
- let injection = null;
217
- while (true) {
218
- const event = await handle.queue.next();
219
- if (event.navigated) {
220
- navigated = event.navigated;
221
- }
222
- if (event.chromiumBidiInjection) {
223
- injection = event.chromiumBidiInjection;
224
- }
225
- if (event.error?.message) {
226
- throw new Error(`page session error while navigating: ${event.error.message}`);
227
- }
228
- if (event.closed) {
229
- handle.closed = true;
230
- throw new Error(`page session ${this.sessionId} closed while navigating`);
231
- }
232
- if (navigated && injection) {
233
- return {
234
- url: navigated.url ?? "",
235
- note: navigated.note ?? "",
236
- bidiSessionId: injection.bidiSessionId ?? "",
237
- mapperTargetId: injection.mapperTargetId ?? "",
238
- mapperSessionId: injection.mapperSessionId ?? "",
239
- packageVersion: injection.packageVersion ?? "",
240
- };
241
- }
242
- }
243
- }
244
- async click(selector, options = {}) {
245
- const handle = await this.#getHandle();
246
- this.#ensureOpen(handle);
247
- handle.stream.write({
248
- browserSessionId: this.browserSessionId,
249
- tabSessionId: this.sessionId,
250
- clickElement: {
251
- cssSelector: selector,
252
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
253
- },
254
- });
255
- while (true) {
256
- const event = await handle.queue.next();
257
- if (event.elementClicked) {
258
- return {
259
- selector: event.elementClicked.cssSelector ?? "",
260
- note: event.elementClicked.note ?? "",
261
- bidiSessionId: event.elementClicked.bidiSessionId ?? "",
262
- };
263
- }
264
- if (event.error?.message) {
265
- throw new Error(`page session error while clicking: ${event.error.message}`);
266
- }
267
- if (event.closed) {
268
- handle.closed = true;
269
- throw new Error(`page session ${this.sessionId} closed while waiting for click result`);
270
- }
271
- }
272
- }
273
- async count(selector, options = {}) {
274
- const handle = await this.#getHandle();
275
- this.#ensureOpen(handle);
276
- handle.stream.write({
277
- browserSessionId: this.browserSessionId,
278
- tabSessionId: this.sessionId,
279
- countElements: {
280
- cssSelector: selector,
281
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
282
- },
283
- });
284
- while (true) {
285
- const event = await handle.queue.next();
286
- if (event.elementCounted) {
287
- return {
288
- selector: event.elementCounted.cssSelector ?? "",
289
- count: event.elementCounted.count ?? 0,
290
- note: event.elementCounted.note ?? "",
291
- };
292
- }
293
- if (event.error?.message) {
294
- throw new Error(`page session error while counting elements: ${event.error.message}`);
295
- }
296
- if (event.closed) {
297
- handle.closed = true;
298
- throw new Error(`page session ${this.sessionId} closed while waiting for count result`);
299
- }
300
- }
301
- }
302
- async highlight(selector, options = {}) {
303
- const handle = await this.#getHandle();
304
- this.#ensureOpen(handle);
305
- handle.stream.write({
306
- browserSessionId: this.browserSessionId,
307
- tabSessionId: this.sessionId,
308
- highlightElements: {
309
- cssSelector: selector,
310
- durationMs: options.durationMs,
311
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
312
- },
313
- });
314
- while (true) {
315
- const event = await handle.queue.next();
316
- if (event.elementsHighlighted) {
317
- return {
318
- selector: event.elementsHighlighted.cssSelector ?? "",
319
- count: event.elementsHighlighted.count ?? 0,
320
- note: event.elementsHighlighted.note ?? "",
321
- };
322
- }
323
- if (event.error?.message) {
324
- throw new Error(`page session error while highlighting elements: ${event.error.message}`);
325
- }
326
- if (event.closed) {
327
- handle.closed = true;
328
- throw new Error(`page session ${this.sessionId} closed while waiting for highlight result`);
329
- }
330
- }
331
- }
332
- async focus(selector, options = {}) {
333
- const handle = await this.#getHandle();
334
- this.#ensureOpen(handle);
335
- handle.stream.write({
336
- browserSessionId: this.browserSessionId,
337
- tabSessionId: this.sessionId,
338
- focusElement: {
339
- cssSelector: selector,
340
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
341
- },
342
- });
343
- while (true) {
344
- const event = await handle.queue.next();
345
- if (event.elementFocused) {
346
- return {
347
- selector: event.elementFocused.cssSelector ?? "",
348
- note: event.elementFocused.note ?? "",
349
- };
350
- }
351
- if (event.error?.message) {
352
- throw new Error(`page session error while focusing: ${event.error.message}`);
353
- }
354
- if (event.closed) {
355
- handle.closed = true;
356
- throw new Error(`page session ${this.sessionId} closed while waiting for focus result`);
357
- }
358
- }
359
- }
360
- async fill(selector, value, options = {}) {
361
- const handle = await this.#getHandle();
362
- this.#ensureOpen(handle);
363
- handle.stream.write({
364
- browserSessionId: this.browserSessionId,
365
- tabSessionId: this.sessionId,
366
- fillElement: {
367
- cssSelector: selector,
368
- value,
369
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
370
- },
371
- });
372
- while (true) {
373
- const event = await handle.queue.next();
374
- if (event.elementFilled) {
375
- return {
376
- selector: event.elementFilled.cssSelector ?? "",
377
- value: event.elementFilled.value ?? "",
378
- note: event.elementFilled.note ?? "",
379
- };
380
- }
381
- if (event.error?.message) {
382
- throw new Error(`page session error while filling: ${event.error.message}`);
383
- }
384
- if (event.closed) {
385
- handle.closed = true;
386
- throw new Error(`page session ${this.sessionId} closed while waiting for fill result`);
387
- }
388
- }
389
- }
390
- async hover(selector, options = {}) {
391
- const handle = await this.#getHandle();
392
- this.#ensureOpen(handle);
393
- handle.stream.write({
394
- browserSessionId: this.browserSessionId,
395
- tabSessionId: this.sessionId,
396
- hoverElement: {
397
- cssSelector: selector,
398
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
399
- },
400
- });
401
- while (true) {
402
- const event = await handle.queue.next();
403
- if (event.elementHovered) {
404
- return {
405
- selector: event.elementHovered.cssSelector ?? "",
406
- note: event.elementHovered.note ?? "",
407
- };
408
- }
409
- if (event.error?.message) {
410
- throw new Error(`page session error while hovering: ${event.error.message}`);
411
- }
412
- if (event.closed) {
413
- handle.closed = true;
414
- throw new Error(`page session ${this.sessionId} closed while waiting for hover result`);
415
- }
416
- }
417
- }
418
- async press(selector, key, options = {}) {
419
- const handle = await this.#getHandle();
420
- this.#ensureOpen(handle);
421
- handle.stream.write({
422
- browserSessionId: this.browserSessionId,
423
- tabSessionId: this.sessionId,
424
- pressKey: {
425
- cssSelector: selector,
426
- key,
427
- text: options.text,
428
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
429
- },
430
- });
431
- while (true) {
432
- const event = await handle.queue.next();
433
- if (event.keyPressed) {
434
- return {
435
- selector: event.keyPressed.cssSelector ?? "",
436
- key: event.keyPressed.key ?? "",
437
- note: event.keyPressed.note ?? "",
438
- };
439
- }
440
- if (event.error?.message) {
441
- throw new Error(`page session error while pressing key: ${event.error.message}`);
442
- }
443
- if (event.closed) {
444
- handle.closed = true;
445
- throw new Error(`page session ${this.sessionId} closed while waiting for press result`);
446
- }
447
- }
448
- }
449
- async textContent(selector, options = {}) {
450
- return this.#readText(selector, options, true);
451
- }
452
- async innerText(selector, options = {}) {
453
- return this.#readText(selector, options, false);
454
- }
455
- async waitForSelector(selector, options = {}) {
456
- const handle = await this.#getHandle();
457
- this.#ensureOpen(handle);
458
- handle.stream.write({
459
- browserSessionId: this.browserSessionId,
460
- tabSessionId: this.sessionId,
461
- waitForSelector: {
462
- cssSelector: selector,
463
- visible: options.visible,
464
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
465
- },
466
- });
467
- while (true) {
468
- const event = await handle.queue.next();
469
- if (event.selectorWaitSatisfied) {
470
- return {
471
- selector: event.selectorWaitSatisfied.cssSelector ?? "",
472
- visible: event.selectorWaitSatisfied.visible ?? false,
473
- note: event.selectorWaitSatisfied.note ?? "",
474
- };
475
- }
476
- if (event.error?.message) {
477
- throw new Error(`page session error while waiting for selector: ${event.error.message}`);
478
- }
479
- if (event.closed) {
480
- handle.closed = true;
481
- throw new Error(`page session ${this.sessionId} closed while waiting for selector result`);
482
- }
483
- }
484
- }
485
- async close() {
486
- const handle = await this.#getHandle();
487
- if (handle.closed) {
488
- return;
489
- }
490
- handle.stream.write({
491
- browserSessionId: this.browserSessionId,
492
- tabSessionId: this.sessionId,
493
- close: {},
494
- });
495
- while (true) {
496
- const event = await handle.queue.next();
497
- if (event.closed) {
498
- handle.closed = true;
499
- handle.stream.end();
500
- return;
501
- }
502
- if (event.error?.message) {
503
- throw new Error(`page session error while closing: ${event.error.message}`);
504
- }
505
- }
506
- }
507
- async ping(message = "ping") {
508
- const handle = await this.#getHandle();
509
- this.#ensureOpen(handle);
510
- handle.stream.write({
511
- browserSessionId: this.browserSessionId,
512
- tabSessionId: this.sessionId,
513
- ping: {
514
- message,
515
- },
516
- });
517
- while (true) {
518
- const event = await handle.queue.next();
519
- if (event.pong?.message) {
520
- return event.pong.message;
521
- }
522
- if (event.error?.message) {
523
- throw new Error(`page session error while pinging: ${event.error.message}`);
524
- }
525
- if (event.closed) {
526
- handle.closed = true;
527
- throw new Error(`page session ${this.sessionId} closed while waiting for pong`);
528
- }
529
- }
530
- }
531
- pageInfo() {
532
- return {
533
- sessionId: this.sessionId,
534
- browserSessionId: this.browserSessionId,
535
- };
536
- }
537
- async navigate(url, options = {}) {
538
- return this.goto(url, options);
539
- }
540
- async #readText(selector, options, textContent) {
541
- const handle = await this.#getHandle();
542
- this.#ensureOpen(handle);
543
- handle.stream.write(textContent
544
- ? {
545
- browserSessionId: this.browserSessionId,
546
- tabSessionId: this.sessionId,
547
- getTextContent: {
548
- cssSelector: selector,
549
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
550
- },
551
- }
552
- : {
553
- browserSessionId: this.browserSessionId,
554
- tabSessionId: this.sessionId,
555
- getInnerText: {
556
- cssSelector: selector,
557
- retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
558
- },
559
- });
560
- while (true) {
561
- const event = await handle.queue.next();
562
- if (event.textContentResolved) {
563
- return {
564
- selector: event.textContentResolved.cssSelector ?? "",
565
- text: event.textContentResolved.text ?? "",
566
- note: event.textContentResolved.note ?? "",
567
- };
568
- }
569
- if (event.innerTextResolved) {
570
- return {
571
- selector: event.innerTextResolved.cssSelector ?? "",
572
- text: event.innerTextResolved.text ?? "",
573
- note: event.innerTextResolved.note ?? "",
574
- };
575
- }
576
- if (event.error?.message) {
577
- throw new Error(`page session error while reading text: ${event.error.message}`);
578
- }
579
- if (event.closed) {
580
- handle.closed = true;
581
- throw new Error(`page session ${this.sessionId} closed while waiting for text result`);
582
- }
583
- }
584
- }
585
- #ensureOpen(handle) {
586
- if (handle.closed) {
587
- throw new Error(`page session ${this.sessionId} is closed`);
588
- }
589
- }
590
- async #getHandle() {
591
- if (!this.#handlePromise) {
592
- this.#handlePromise = createPageHandle(this.#runtime);
593
- }
594
- return this.#handlePromise;
595
- }
596
- }
597
- class LocatorImpl {
598
- page;
599
- selector;
600
- constructor(input) {
601
- this.page = input.page;
602
- this.selector = input.selector;
603
- }
604
- async click(options = {}) {
605
- return this.page.click(this.selector, options);
606
- }
607
- async count(options = {}) {
608
- return this.page.count(this.selector, options);
609
- }
610
- async highlight(options = {}) {
611
- return this.page.highlight(this.selector, options);
612
- }
613
- async focus(options = {}) {
614
- return this.page.focus(this.selector, options);
615
- }
616
- async fill(value, options = {}) {
617
- return this.page.fill(this.selector, value, options);
618
- }
619
- async hover(options = {}) {
620
- return this.page.hover(this.selector, options);
621
- }
622
- async press(key, options = {}) {
623
- return this.page.press(this.selector, key, options);
624
- }
625
- async textContent(options = {}) {
626
- return this.page.textContent(this.selector, options);
627
- }
628
- async innerText(options = {}) {
629
- return this.page.innerText(this.selector, options);
630
- }
631
- async waitFor(options = {}) {
632
- return this.page.waitForSelector(this.selector, options);
633
- }
634
- locator(selector) {
635
- return new LocatorImpl({
636
- page: this.page,
637
- selector: `${this.selector} ${selector}`,
638
- });
639
- }
640
- }
1
+ import { BrowserImpl, BrowserTypeImpl } from "./browser.js";
2
+ import { findConfigFile, loadConfigFile, resolveConfig } from "./config.js";
3
+ import { PageImpl } from "./page.js";
4
+ import { createBrowserSessionHandle, getRuntime, launchConfiguredBrowser as launchConfiguredBrowserWithResolver, ping as runtimePing, resolveLaunchBrowserArgs, setServerAddr, shutdown, } from "./runtime.js";
5
+ export { findConfigFile, loadConfigFile, resolveConfig, setServerAddr, shutdown };
641
6
  export const chromium = new BrowserTypeImpl("chromium");
642
7
  export const firefox = new BrowserTypeImpl("firefox");
643
8
  export async function ping() {
644
- const runtime = await getRuntime();
645
- return new Promise((resolve, reject) => {
646
- runtime.client.Ping({}, (error, response) => {
647
- if (error) {
648
- reject(new Error(`ping engine server: ${error.message}`));
649
- return;
650
- }
651
- resolve(response.message ?? "");
652
- });
653
- });
9
+ return runtimePing();
654
10
  }
655
11
  export async function launchChrome(options = {}) {
656
12
  return launchBrowser("chromium", options);
657
13
  }
14
+ export async function launchFirefox(options = {}) {
15
+ return launchBrowser("firefox", options);
16
+ }
658
17
  export async function launchConfiguredBrowser(config) {
659
- return launchBrowser(config.browserName, {
660
- ...config.launchOptions,
661
- browserBinary: config.browserBinary ?? config.launchOptions.browserBinary,
662
- });
18
+ return launchConfiguredBrowserWithResolver(config, (browserKind, launchOptions) => launchBrowser(browserKind, launchOptions));
663
19
  }
664
- export async function launchBrowser(browserKind, options = {}) {
20
+ export async function launchBrowser(browserKindOrOptions, options = {}) {
21
+ if (resolveLaunchBrowserArgs(browserKindOrOptions)) {
22
+ return launchConfiguredBrowser(resolveConfig(browserKindOrOptions ?? {}));
23
+ }
665
24
  const runtime = await getRuntime();
666
- const stream = runtime.client.BrowserSession();
667
- const queue = bindStreamQueue(stream);
25
+ const { stream, queue } = await createBrowserSessionHandle(runtime);
668
26
  stream.write({
669
27
  launchBrowser: {
670
- browserKind: browserKind === "firefox" ? 2 : 1,
28
+ browserKind: browserKindOrOptions === "firefox" ? 2 : 1,
671
29
  browserBinary: options.browserBinary,
672
30
  retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
673
31
  },
@@ -688,241 +46,4 @@ export async function launchBrowser(browserKind, options = {}) {
688
46
  }
689
47
  }
690
48
  }
691
- export function setServerAddr(serverAddr) {
692
- serverAddrOverride = normalizeServerAddr(serverAddr);
693
- runtimePromise = null;
694
- }
695
- export async function shutdown() {
696
- if (!runtimePromise) {
697
- return;
698
- }
699
- const runtime = await runtimePromise;
700
- runtime.client.close();
701
- runtimePromise = null;
702
- }
703
- export function findConfigFile(startDir = process.cwd()) {
704
- let currentDir = path.resolve(startDir);
705
- while (true) {
706
- for (const filename of CONFIG_FILENAMES) {
707
- const candidate = path.join(currentDir, filename);
708
- if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
709
- return candidate;
710
- }
711
- }
712
- const parentDir = path.dirname(currentDir);
713
- if (parentDir === currentDir) {
714
- return null;
715
- }
716
- currentDir = parentDir;
717
- }
718
- }
719
- export function loadConfigFile(configFile) {
720
- const resolved = path.resolve(configFile);
721
- const raw = fs.readFileSync(resolved, "utf8");
722
- const parsed = parseConfigContents(raw, resolved);
723
- validateConfigShape(parsed, resolved);
724
- return parsed;
725
- }
726
- export function resolveConfig(options = {}) {
727
- const configFilePath = options.configFile ? path.resolve(options.configFile) : findConfigFile(options.cwd);
728
- const fileConfig = configFilePath ? loadConfigFile(configFilePath) : {};
729
- const suiteName = options.suite?.trim() || null;
730
- const suiteConfig = suiteName ? fileConfig.suites?.[suiteName] : undefined;
731
- if (suiteName && !suiteConfig) {
732
- throw new Error(`allwright config suite "${suiteName}" was not found in ${configFilePath ?? "the resolved config file"}`);
733
- }
734
- const serverAddr = suiteConfig?.server?.addr ?? fileConfig.server?.addr;
735
- const browserName = suiteConfig?.browser?.name ?? fileConfig.browser?.name ?? "chromium";
736
- const browserBinary = suiteConfig?.browser?.binary ?? fileConfig.browser?.binary;
737
- const launchOptions = mergeLaunchOptions(fileConfig.browser?.launchOptions, suiteConfig?.browser?.launchOptions);
738
- const expect = {
739
- ...(fileConfig.expect ?? {}),
740
- ...(suiteConfig?.expect ?? {}),
741
- };
742
- return {
743
- configFilePath,
744
- suiteName,
745
- serverAddr,
746
- browserName,
747
- browserBinary,
748
- launchOptions: browserBinary ? { ...launchOptions, browserBinary } : launchOptions,
749
- expect,
750
- };
751
- }
752
- async function getRuntime() {
753
- if (!runtimePromise) {
754
- runtimePromise = Promise.resolve(createRuntime());
755
- }
756
- return runtimePromise;
757
- }
758
- function createRuntime() {
759
- const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
760
- includeDirs: [PROTO_ROOT],
761
- keepCase: false,
762
- longs: String,
763
- enums: String,
764
- defaults: true,
765
- oneofs: true,
766
- });
767
- const proto = grpc.loadPackageDefinition(loaded);
768
- const ClientCtor = proto.allwright.engine.v1.EngineService;
769
- const client = new ClientCtor(configuredServerAddr(), grpc.credentials.createInsecure());
770
- return { client };
771
- }
772
- function configuredServerAddr() {
773
- if (serverAddrOverride) {
774
- return serverAddrOverride;
775
- }
776
- return normalizeServerAddr(process.env[SERVER_ADDR_ENV_VAR] ?? DEFAULT_SERVER_ADDR);
777
- }
778
- function mergeLaunchOptions(base, override) {
779
- return {
780
- ...(base ?? {}),
781
- ...(override ?? {}),
782
- };
783
- }
784
- function validateConfigShape(value, source) {
785
- if (!value || typeof value !== "object" || Array.isArray(value)) {
786
- throw new Error(`allwright config ${source} must contain a top-level object`);
787
- }
788
- const config = value;
789
- if (config.schemaVersion !== undefined && config.schemaVersion !== 1) {
790
- throw new Error(`allwright config ${source} has unsupported schemaVersion ${String(config.schemaVersion)}; expected 1`);
791
- }
792
- const browserName = config.browser?.name;
793
- if (browserName !== undefined && browserName !== "chromium" && browserName !== "firefox") {
794
- throw new Error(`allwright config ${source} has unsupported browser.name ${String(browserName)}; use "chromium" or "firefox"`);
795
- }
796
- }
797
- function parseConfigContents(raw, source) {
798
- const extension = path.extname(source).toLowerCase();
799
- if (extension === ".json") {
800
- return JSON.parse(raw);
801
- }
802
- if (extension === ".yaml" || extension === ".yml") {
803
- return parseSimpleYaml(raw, source);
804
- }
805
- throw new Error(`unsupported allwright config file extension ${extension || "<none>"} for ${source}`);
806
- }
807
- function parseSimpleYaml(raw, source) {
808
- const root = {};
809
- const stack = [
810
- { indent: -1, value: root },
811
- ];
812
- for (const [index, originalLine] of raw.split(/\r?\n/).entries()) {
813
- const lineNumber = index + 1;
814
- const line = stripYamlComment(originalLine);
815
- if (!line.trim()) {
816
- continue;
817
- }
818
- const indent = countLeadingSpaces(line);
819
- if (indent % 2 !== 0) {
820
- throw new Error(`invalid YAML indentation in ${source}:${lineNumber}; use multiples of 2 spaces`);
821
- }
822
- while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
823
- stack.pop();
824
- }
825
- const current = stack[stack.length - 1];
826
- const trimmed = line.trim();
827
- const separatorIndex = trimmed.indexOf(":");
828
- if (separatorIndex <= 0) {
829
- throw new Error(`invalid YAML mapping in ${source}:${lineNumber}`);
830
- }
831
- const key = trimmed.slice(0, separatorIndex).trim();
832
- const rawValue = trimmed.slice(separatorIndex + 1).trim();
833
- if (!key) {
834
- throw new Error(`empty YAML key in ${source}:${lineNumber}`);
835
- }
836
- if (!rawValue) {
837
- const child = {};
838
- current.value[key] = child;
839
- stack.push({ indent, value: child });
840
- continue;
841
- }
842
- current.value[key] = parseYamlScalar(rawValue, source, lineNumber);
843
- }
844
- return root;
845
- }
846
- function stripYamlComment(line) {
847
- let inSingleQuote = false;
848
- let inDoubleQuote = false;
849
- for (let index = 0; index < line.length; index += 1) {
850
- const char = line[index];
851
- if (char === "'" && !inDoubleQuote) {
852
- inSingleQuote = !inSingleQuote;
853
- continue;
854
- }
855
- if (char === "\"" && !inSingleQuote) {
856
- inDoubleQuote = !inDoubleQuote;
857
- continue;
858
- }
859
- if (char === "#" && !inSingleQuote && !inDoubleQuote) {
860
- return line.slice(0, index);
861
- }
862
- }
863
- return line;
864
- }
865
- function countLeadingSpaces(line) {
866
- let count = 0;
867
- while (count < line.length && line[count] === " ") {
868
- count += 1;
869
- }
870
- return count;
871
- }
872
- function parseYamlScalar(value, source, lineNumber) {
873
- if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
874
- return value.slice(1, -1);
875
- }
876
- if (value === "true") {
877
- return true;
878
- }
879
- if (value === "false") {
880
- return false;
881
- }
882
- if (value === "null") {
883
- return null;
884
- }
885
- if (/^-?\d+$/.test(value)) {
886
- return Number.parseInt(value, 10);
887
- }
888
- if (/^-?\d+\.\d+$/.test(value)) {
889
- return Number.parseFloat(value);
890
- }
891
- if (value.startsWith("[") || value.startsWith("{")) {
892
- throw new Error(`unsupported YAML collection syntax in ${source}:${lineNumber}; use nested mappings instead`);
893
- }
894
- return value;
895
- }
896
- function normalizeServerAddr(raw) {
897
- const trimmed = raw.trim();
898
- if (trimmed.startsWith("dns:") || trimmed.startsWith("unix:")) {
899
- return trimmed;
900
- }
901
- if (trimmed.includes("://")) {
902
- const parsed = new URL(trimmed);
903
- return parsed.host;
904
- }
905
- return trimmed;
906
- }
907
- function bindStreamQueue(stream) {
908
- const queue = new EventQueue();
909
- stream.on("data", (event) => {
910
- queue.push(event);
911
- });
912
- stream.on("error", (error) => {
913
- queue.fail(new Error(`grpc stream error: ${error.message}`));
914
- });
915
- stream.on("end", () => {
916
- queue.fail(new Error("grpc stream ended"));
917
- });
918
- return queue;
919
- }
920
- async function createPageHandle(runtime) {
921
- const stream = runtime.client.TabSession();
922
- const queue = bindStreamQueue(stream);
923
- return {
924
- stream,
925
- queue,
926
- closed: false,
927
- };
928
- }
49
+ export { BrowserImpl, BrowserTypeImpl, PageImpl };