@allwright.dev/core 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,10 +3,37 @@
3
3
  High-level TypeScript client for the allwright automation engine.
4
4
 
5
5
  ```ts
6
- import { chromium } from "@allwright.dev/core";
6
+ import { firefox } from "@allwright.dev/core";
7
7
 
8
- const browser = await chromium.launch();
8
+ const browser = await firefox.launch();
9
9
  const page = browser.page();
10
10
  await page.goto("https://example.com");
11
11
  await browser.close();
12
12
  ```
13
+
14
+ Shared config files are stack-agnostic and can live in `allwright.config.yaml` or `allwright.config.json`.
15
+ The shared schema lives at the repo root in `allwright.schema.json`.
16
+
17
+ ```yaml
18
+ schemaVersion: 1
19
+
20
+ server:
21
+ addr: 127.0.0.1:50051
22
+
23
+ browser:
24
+ name: firefox
25
+ binary: /Applications/Firefox.app/Contents/MacOS/firefox
26
+ launchOptions:
27
+ timeoutMs: 30000
28
+
29
+ expect:
30
+ timeoutMs: 5000
31
+ intervalMs: 100
32
+
33
+ suites:
34
+ smoke:
35
+ browser:
36
+ name: chromium
37
+ ```
38
+
39
+ The TypeScript package exports `findConfigFile()`, `loadConfigFile()`, `resolveConfig()`, and `launchConfiguredBrowser()` so runner packages can consume the same config model without inventing language-specific config files.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import grpc from "@grpc/grpc-js";
2
1
  export interface LaunchOptions {
3
- chromeBinary?: string;
2
+ browserBinary?: string;
4
3
  timeoutMs?: number;
5
4
  }
5
+ export type BrowserKind = "chromium" | "firefox";
6
6
  export interface CommandOptions {
7
7
  timeoutMs?: number;
8
8
  }
@@ -76,314 +76,28 @@ export interface PageInfo {
76
76
  sessionId: string;
77
77
  browserSessionId: string;
78
78
  }
79
- interface PingResponse {
80
- message?: string;
81
- }
82
- interface ChromeLaunchedPayload {
83
- browser?: string;
84
- note?: string;
85
- cdpWebsocketUrl?: string;
86
- userDataDir?: string;
87
- initialTabSessionId?: string;
88
- }
89
- interface BrowserSessionEvent {
90
- sessionId?: string;
91
- event?: string;
92
- chromeLaunched?: ChromeLaunchedPayload;
93
- tabOpened?: {
94
- tabSessionId?: string;
95
- note?: string;
96
- };
97
- pong?: {
98
- message?: string;
99
- };
100
- closed?: {
101
- reason?: string;
102
- };
103
- error?: {
104
- message?: string;
105
- };
106
- }
107
- interface TabSessionEvent {
108
- tabSessionId?: string;
109
- event?: string;
110
- attached?: {
111
- note?: string;
112
- };
113
- pong?: {
114
- message?: string;
115
- };
116
- closed?: {
117
- reason?: string;
118
- };
119
- error?: {
120
- message?: string;
121
- };
122
- navigated?: {
123
- url?: string;
124
- note?: string;
125
- };
126
- chromiumBidiInjection?: {
127
- note?: string;
128
- bidiSessionId?: string;
129
- mapperTargetId?: string;
130
- mapperSessionId?: string;
131
- packageVersion?: string;
132
- };
133
- elementClicked?: {
134
- cssSelector?: string;
135
- note?: string;
136
- bidiSessionId?: string;
137
- };
138
- elementCounted?: {
139
- cssSelector?: string;
140
- count?: number;
141
- note?: string;
142
- };
143
- elementsHighlighted?: {
144
- cssSelector?: string;
145
- count?: number;
146
- note?: string;
147
- };
148
- elementFocused?: {
149
- cssSelector?: string;
150
- note?: string;
151
- };
152
- elementFilled?: {
153
- cssSelector?: string;
154
- value?: string;
155
- note?: string;
156
- };
157
- elementHovered?: {
158
- cssSelector?: string;
159
- note?: string;
160
- };
161
- keyPressed?: {
162
- cssSelector?: string;
163
- key?: string;
164
- note?: string;
165
- };
166
- textContentResolved?: {
167
- cssSelector?: string;
168
- text?: string;
169
- note?: string;
170
- };
171
- innerTextResolved?: {
172
- cssSelector?: string;
173
- text?: string;
174
- note?: string;
175
- };
176
- selectorWaitSatisfied?: {
177
- cssSelector?: string;
178
- visible?: boolean;
179
- note?: string;
180
- };
181
- }
182
- interface LaunchChromeRequest {
183
- launchChrome: {
184
- chromeBinary?: string;
185
- retryOptions?: {
186
- timeoutMs?: number;
187
- };
188
- };
189
- }
190
- interface OpenTabRequest {
191
- openTab: {
192
- retryOptions?: {
193
- timeoutMs?: number;
194
- };
195
- };
196
- }
197
- interface BrowserPingRequest {
198
- ping: {
199
- message: string;
200
- };
201
- }
202
- interface CloseBrowserRequest {
203
- close: Record<string, never>;
204
- }
205
- interface TabPingRequest {
206
- browserSessionId: string;
207
- tabSessionId: string;
208
- ping: {
209
- message: string;
210
- };
211
- }
212
- interface NavigateRequest {
213
- browserSessionId: string;
214
- tabSessionId: string;
215
- navigate: {
216
- url: string;
217
- retryOptions?: {
218
- timeoutMs?: number;
219
- };
220
- };
221
- }
222
- interface ClickRequest {
223
- browserSessionId: string;
224
- tabSessionId: string;
225
- clickElement: {
226
- cssSelector: string;
227
- retryOptions?: {
228
- timeoutMs?: number;
229
- };
230
- };
231
- }
232
- interface CountRequest {
233
- browserSessionId: string;
234
- tabSessionId: string;
235
- countElements: {
236
- cssSelector: string;
237
- retryOptions?: {
238
- timeoutMs?: number;
239
- };
240
- };
241
- }
242
- interface HighlightRequest {
243
- browserSessionId: string;
244
- tabSessionId: string;
245
- highlightElements: {
246
- cssSelector: string;
247
- durationMs?: number;
248
- retryOptions?: {
249
- timeoutMs?: number;
250
- };
251
- };
252
- }
253
- interface FocusRequest {
254
- browserSessionId: string;
255
- tabSessionId: string;
256
- focusElement: {
257
- cssSelector: string;
258
- retryOptions?: {
259
- timeoutMs?: number;
260
- };
261
- };
262
- }
263
- interface FillRequest {
264
- browserSessionId: string;
265
- tabSessionId: string;
266
- fillElement: {
267
- cssSelector: string;
268
- value: string;
269
- retryOptions?: {
270
- timeoutMs?: number;
271
- };
272
- };
273
- }
274
- interface HoverRequest {
275
- browserSessionId: string;
276
- tabSessionId: string;
277
- hoverElement: {
278
- cssSelector: string;
279
- retryOptions?: {
280
- timeoutMs?: number;
281
- };
282
- };
283
- }
284
- interface PressRequest {
285
- browserSessionId: string;
286
- tabSessionId: string;
287
- pressKey: {
288
- cssSelector: string;
289
- key: string;
290
- text?: string;
291
- retryOptions?: {
292
- timeoutMs?: number;
293
- };
294
- };
295
- }
296
- interface TextContentRequest {
297
- browserSessionId: string;
298
- tabSessionId: string;
299
- getTextContent: {
300
- cssSelector: string;
301
- retryOptions?: {
302
- timeoutMs?: number;
303
- };
304
- };
305
- }
306
- interface InnerTextRequest {
307
- browserSessionId: string;
308
- tabSessionId: string;
309
- getInnerText: {
310
- cssSelector: string;
311
- retryOptions?: {
312
- timeoutMs?: number;
313
- };
314
- };
315
- }
316
- interface WaitForSelectorRequest {
317
- browserSessionId: string;
318
- tabSessionId: string;
319
- waitForSelector: {
320
- cssSelector: string;
321
- visible?: boolean;
322
- retryOptions?: {
323
- timeoutMs?: number;
324
- };
325
- };
326
- }
327
- interface CloseTabRequest {
328
- browserSessionId: string;
329
- tabSessionId: string;
330
- close: Record<string, never>;
331
- }
332
- type BrowserSessionRequest = LaunchChromeRequest | OpenTabRequest | BrowserPingRequest | CloseBrowserRequest;
333
- type TabSessionRequest = TabPingRequest | NavigateRequest | ClickRequest | CountRequest | HighlightRequest | FocusRequest | FillRequest | HoverRequest | PressRequest | TextContentRequest | InnerTextRequest | WaitForSelectorRequest | CloseTabRequest;
334
- type BrowserSessionStream = grpc.ClientDuplexStream<BrowserSessionRequest, BrowserSessionEvent>;
335
- type PageSessionStream = grpc.ClientDuplexStream<TabSessionRequest, TabSessionEvent>;
336
- interface EngineServiceClientShape {
337
- Ping(request: Record<string, never>, callback: (error: grpc.ServiceError | null, response: PingResponse) => void): void;
338
- BrowserSession(): BrowserSessionStream;
339
- TabSession(): PageSessionStream;
340
- close(): void;
341
- }
342
- interface RuntimeClient {
343
- client: EngineServiceClientShape;
344
- }
345
- interface BrowserLaunchState {
346
- runtime: RuntimeClient;
347
- stream: BrowserSessionStream;
348
- queue: EventQueue<BrowserSessionEvent>;
349
- sessionId: string;
350
- chromeLaunched: ChromeLaunchedPayload;
351
- }
352
- declare class EventQueue<T> {
353
- #private;
354
- push(item: T): void;
355
- fail(error: Error): void;
356
- next(): Promise<T>;
79
+ export interface LocatorInfo {
80
+ page: Page;
81
+ selector: string;
357
82
  }
358
- export declare class BrowserType {
83
+ export interface BrowserType {
359
84
  launch(options?: LaunchOptions): Promise<Browser>;
360
85
  }
361
- export declare class Browser {
362
- #private;
363
- constructor(state: BrowserLaunchState);
364
- readonly sessionId: string;
365
- readonly browserName: string;
366
- readonly launchNote: string;
367
- readonly cdpWebSocketURL: string;
368
- readonly userDataDir: string;
86
+ export interface Browser extends BrowserInfo {
369
87
  page(): Page;
370
88
  initialPage(): Page;
89
+ initialTab(): Page;
371
90
  pages(): Page[];
372
91
  newPage(options?: CommandOptions): Promise<Page>;
92
+ newTab(options?: CommandOptions): Promise<Page>;
373
93
  close(): Promise<void>;
374
94
  ping(message?: string): Promise<string>;
375
95
  browserInfo(): BrowserInfo;
376
- initialTab(): Page;
377
- newTab(options?: CommandOptions): Promise<Page>;
378
96
  }
379
- export declare class Page {
380
- #private;
381
- constructor(input: PageInfo & {
382
- runtime: RuntimeClient;
383
- });
384
- readonly sessionId: string;
385
- readonly browserSessionId: string;
97
+ export interface Page extends PageInfo {
98
+ locator(selector: string): Locator;
386
99
  goto(url: string, options?: CommandOptions): Promise<NavigateResult>;
100
+ navigate(url: string, options?: CommandOptions): Promise<NavigateResult>;
387
101
  click(selector: string, options?: CommandOptions): Promise<ClickResult>;
388
102
  count(selector: string, options?: CommandOptions): Promise<CountResult>;
389
103
  highlight(selector: string, options?: HighlightOptions): Promise<HighlightResult>;
@@ -397,12 +111,73 @@ export declare class Page {
397
111
  close(): Promise<void>;
398
112
  ping(message?: string): Promise<string>;
399
113
  pageInfo(): PageInfo;
400
- navigate(url: string, options?: CommandOptions): Promise<NavigateResult>;
114
+ }
115
+ export interface Locator {
116
+ readonly page: Page;
117
+ readonly selector: string;
118
+ click(options?: CommandOptions): Promise<ClickResult>;
119
+ count(options?: CommandOptions): Promise<CountResult>;
120
+ highlight(options?: HighlightOptions): Promise<HighlightResult>;
121
+ focus(options?: CommandOptions): Promise<ElementResult>;
122
+ fill(value: string, options?: CommandOptions): Promise<FillResult>;
123
+ hover(options?: CommandOptions): Promise<ElementResult>;
124
+ press(key: string, options?: PressOptions): Promise<PressResult>;
125
+ textContent(options?: CommandOptions): Promise<TextResult>;
126
+ innerText(options?: CommandOptions): Promise<TextResult>;
127
+ waitFor(options?: WaitForSelectorOptions): Promise<WaitForSelectorResult>;
128
+ locator(selector: string): Locator;
129
+ }
130
+ export interface AllwrightConfig {
131
+ schemaVersion?: 1;
132
+ server?: {
133
+ addr?: string;
134
+ };
135
+ browser?: {
136
+ name?: BrowserKind;
137
+ binary?: string;
138
+ launchOptions?: LaunchOptions;
139
+ };
140
+ expect?: RetryConfig;
141
+ suites?: Record<string, AllwrightSuiteConfig>;
142
+ }
143
+ export interface AllwrightSuiteConfig {
144
+ server?: {
145
+ addr?: string;
146
+ };
147
+ browser?: {
148
+ name?: BrowserKind;
149
+ binary?: string;
150
+ launchOptions?: LaunchOptions;
151
+ };
152
+ expect?: RetryConfig;
153
+ }
154
+ export interface RetryConfig {
155
+ timeoutMs?: number;
156
+ intervalMs?: number;
157
+ }
158
+ export interface ResolvedAllwrightConfig {
159
+ configFilePath: string | null;
160
+ suiteName: string | null;
161
+ serverAddr?: string;
162
+ browserName: BrowserKind;
163
+ browserBinary?: string;
164
+ launchOptions: LaunchOptions;
165
+ expect: RetryConfig;
166
+ }
167
+ export interface ResolveConfigOptions {
168
+ cwd?: string;
169
+ configFile?: string;
170
+ suite?: string;
401
171
  }
402
172
  export declare const chromium: BrowserType;
173
+ export declare const firefox: BrowserType;
403
174
  export type Tab = Page;
404
175
  export declare function ping(): Promise<string>;
405
176
  export declare function launchChrome(options?: LaunchOptions): Promise<Browser>;
177
+ export declare function launchConfiguredBrowser(config: ResolvedAllwrightConfig): Promise<Browser>;
178
+ export declare function launchBrowser(browserKind: BrowserKind, options?: LaunchOptions): Promise<Browser>;
406
179
  export declare function setServerAddr(serverAddr: string): void;
407
180
  export declare function shutdown(): Promise<void>;
408
- export {};
181
+ export declare function findConfigFile(startDir?: string): string | null;
182
+ export declare function loadConfigFile(configFile: string): AllwrightConfig;
183
+ export declare function resolveConfig(options?: ResolveConfigOptions): ResolvedAllwrightConfig;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import fs from "node:fs";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import grpc from "@grpc/grpc-js";
4
5
  import protoLoader from "@grpc/proto-loader";
@@ -11,6 +12,14 @@ const PROTO_ROOT = path.join(PACKAGE_ROOT, "proto");
11
12
  const ENGINE_PROTO_PATH = path.join(PROTO_ROOT, "engine", "v1", "engine.proto");
12
13
  let runtimePromise = null;
13
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
+ ];
14
23
  class EventQueue {
15
24
  #items = [];
16
25
  #waiters = [];
@@ -44,12 +53,16 @@ class EventQueue {
44
53
  });
45
54
  }
46
55
  }
47
- export class BrowserType {
56
+ class BrowserTypeImpl {
57
+ #browserKind;
58
+ constructor(browserKind = "chromium") {
59
+ this.#browserKind = browserKind;
60
+ }
48
61
  async launch(options = {}) {
49
- return launchChrome(options);
62
+ return launchBrowser(this.#browserKind, options);
50
63
  }
51
64
  }
52
- export class Browser {
65
+ class BrowserImpl {
53
66
  #closed = false;
54
67
  #runtime;
55
68
  #stream;
@@ -59,10 +72,10 @@ export class Browser {
59
72
  constructor(state) {
60
73
  const browserInfo = {
61
74
  sessionId: state.sessionId,
62
- browserName: state.chromeLaunched.browser ?? "",
63
- launchNote: state.chromeLaunched.note ?? "",
64
- cdpWebSocketURL: state.chromeLaunched.cdpWebsocketUrl ?? "",
65
- userDataDir: state.chromeLaunched.userDataDir ?? "",
75
+ browserName: state.launched.browser ?? "",
76
+ launchNote: state.launched.note ?? "",
77
+ cdpWebSocketURL: "",
78
+ userDataDir: state.launched.userDataDir ?? "",
66
79
  };
67
80
  this.#runtime = state.runtime;
68
81
  this.#stream = state.stream;
@@ -72,7 +85,7 @@ export class Browser {
72
85
  this.launchNote = browserInfo.launchNote;
73
86
  this.cdpWebSocketURL = browserInfo.cdpWebSocketURL;
74
87
  this.userDataDir = browserInfo.userDataDir;
75
- this.#initialPage = this.#createPage(state.chromeLaunched.initialTabSessionId ?? "");
88
+ this.#initialPage = this.#createPage(state.launched.initialTabSessionId ?? "");
76
89
  }
77
90
  sessionId;
78
91
  browserName;
@@ -161,7 +174,7 @@ export class Browser {
161
174
  if (existing) {
162
175
  return existing;
163
176
  }
164
- const page = new Page({
177
+ const page = new PageImpl({
165
178
  runtime: this.#runtime,
166
179
  browserSessionId: this.sessionId,
167
180
  sessionId,
@@ -175,7 +188,7 @@ export class Browser {
175
188
  }
176
189
  }
177
190
  }
178
- export class Page {
191
+ class PageImpl {
179
192
  #runtime;
180
193
  #handlePromise = null;
181
194
  constructor(input) {
@@ -185,6 +198,9 @@ export class Page {
185
198
  }
186
199
  sessionId;
187
200
  browserSessionId;
201
+ locator(selector) {
202
+ return new LocatorImpl({ page: this, selector });
203
+ }
188
204
  async goto(url, options = {}) {
189
205
  const handle = await this.#getHandle();
190
206
  this.#ensureOpen(handle);
@@ -578,7 +594,52 @@ export class Page {
578
594
  return this.#handlePromise;
579
595
  }
580
596
  }
581
- export const chromium = new BrowserType();
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
+ }
641
+ export const chromium = new BrowserTypeImpl("chromium");
642
+ export const firefox = new BrowserTypeImpl("firefox");
582
643
  export async function ping() {
583
644
  const runtime = await getRuntime();
584
645
  return new Promise((resolve, reject) => {
@@ -592,24 +653,34 @@ export async function ping() {
592
653
  });
593
654
  }
594
655
  export async function launchChrome(options = {}) {
656
+ return launchBrowser("chromium", options);
657
+ }
658
+ export async function launchConfiguredBrowser(config) {
659
+ return launchBrowser(config.browserName, {
660
+ ...config.launchOptions,
661
+ browserBinary: config.browserBinary ?? config.launchOptions.browserBinary,
662
+ });
663
+ }
664
+ export async function launchBrowser(browserKind, options = {}) {
595
665
  const runtime = await getRuntime();
596
666
  const stream = runtime.client.BrowserSession();
597
667
  const queue = bindStreamQueue(stream);
598
668
  stream.write({
599
- launchChrome: {
600
- chromeBinary: options.chromeBinary,
669
+ launchBrowser: {
670
+ browserKind: browserKind === "firefox" ? 2 : 1,
671
+ browserBinary: options.browserBinary,
601
672
  retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
602
673
  },
603
674
  });
604
675
  while (true) {
605
676
  const event = await queue.next();
606
- if (event.chromeLaunched) {
607
- return new Browser({
677
+ if (event.browserLaunched) {
678
+ return new BrowserImpl({
608
679
  runtime,
609
680
  stream,
610
681
  queue,
611
682
  sessionId: event.sessionId ?? "",
612
- chromeLaunched: event.chromeLaunched,
683
+ launched: event.browserLaunched,
613
684
  });
614
685
  }
615
686
  if (event.error?.message) {
@@ -629,6 +700,55 @@ export async function shutdown() {
629
700
  runtime.client.close();
630
701
  runtimePromise = null;
631
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
+ }
632
752
  async function getRuntime() {
633
753
  if (!runtimePromise) {
634
754
  runtimePromise = Promise.resolve(createRuntime());
@@ -655,6 +775,124 @@ function configuredServerAddr() {
655
775
  }
656
776
  return normalizeServerAddr(process.env[SERVER_ADDR_ENV_VAR] ?? DEFAULT_SERVER_ADDR);
657
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
+ }
658
896
  function normalizeServerAddr(raw) {
659
897
  const trimmed = raw.trim();
660
898
  if (trimmed.startsWith("dns:") || trimmed.startsWith("unix:")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allwright.dev/core",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "High-level TypeScript client for the allwright automation engine.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,7 @@ message BrowserSessionCommand {
12
12
  OpenTabCommand open_tab = 2;
13
13
  SessionPingCommand ping = 3;
14
14
  CloseBrowserSessionCommand close = 4;
15
+ LaunchBrowserCommand launch_browser = 5;
15
16
  }
16
17
  }
17
18
 
@@ -34,6 +35,7 @@ message BrowserSessionEvent {
34
35
  SessionPongEvent pong = 4;
35
36
  BrowserSessionClosedEvent closed = 5;
36
37
  BrowserSessionErrorEvent error = 6;
38
+ BrowserLaunchedEvent browser_launched = 7;
37
39
  }
38
40
  }
39
41
 
@@ -5,6 +5,26 @@ option go_package = "allwright.dev/gen/allwright/engine/v1;enginev1";
5
5
 
6
6
  import "core/v1/common.proto";
7
7
 
8
+ enum BrowserKind {
9
+ BROWSER_KIND_UNSPECIFIED = 0;
10
+ BROWSER_KIND_CHROMIUM = 1;
11
+ BROWSER_KIND_FIREFOX = 2;
12
+ }
13
+
14
+ message LaunchBrowserCommand {
15
+ BrowserKind browser_kind = 1;
16
+ optional string browser_binary = 2;
17
+ optional CommandRetryOptions retry_options = 3;
18
+ }
19
+
20
+ message BrowserLaunchedEvent {
21
+ BrowserKind browser_kind = 1;
22
+ string browser = 2;
23
+ string note = 3;
24
+ string user_data_dir = 4;
25
+ string initial_tab_session_id = 5;
26
+ }
27
+
8
28
  message LaunchChromeCommand {
9
29
  optional string chrome_binary = 1;
10
30
  optional CommandRetryOptions retry_options = 2;