@itmetsam/samurai-framework 0.4.0 → 0.6.0

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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/api.d.ts +1 -0
  3. package/dist/api.js +1 -0
  4. package/dist/browser/browser-finder.d.ts +1 -0
  5. package/dist/browser/browser-finder.js +1 -0
  6. package/dist/browser/browser.d.ts +8 -0
  7. package/dist/browser/browser.js +33 -12
  8. package/dist/browser/warm-up.d.ts +14 -0
  9. package/dist/browser/warm-up.js +37 -0
  10. package/dist/browsers/cache-dir.d.ts +2 -0
  11. package/dist/browsers/cache-dir.js +15 -0
  12. package/dist/browsers/downloader.d.ts +23 -0
  13. package/dist/browsers/downloader.js +52 -0
  14. package/dist/browsers/index.d.ts +6 -0
  15. package/dist/browsers/index.js +5 -0
  16. package/dist/browsers/overrides.d.ts +17 -0
  17. package/dist/browsers/overrides.js +15 -0
  18. package/dist/browsers/pins.d.ts +16 -0
  19. package/dist/browsers/pins.js +14 -0
  20. package/dist/browsers/resolve.d.ts +26 -0
  21. package/dist/browsers/resolve.js +56 -0
  22. package/dist/browsers/store.d.ts +52 -0
  23. package/dist/browsers/store.js +170 -0
  24. package/dist/cli/args.d.ts +2 -0
  25. package/dist/cli/args.js +2 -0
  26. package/dist/cli/browsers.d.ts +6 -0
  27. package/dist/cli/browsers.js +119 -0
  28. package/dist/cli/flow-reporter.d.ts +5 -2
  29. package/dist/cli/flow-reporter.js +3 -2
  30. package/dist/cli/flow.d.ts +1 -1
  31. package/dist/cli/flow.js +4 -2
  32. package/dist/cli/main.d.ts +1 -1
  33. package/dist/cli/main.js +11 -1
  34. package/dist/config/config.d.ts +5 -0
  35. package/dist/config/config.js +13 -0
  36. package/dist/flows/core/steps.js +14 -3
  37. package/dist/flows/run/index.d.ts +1 -0
  38. package/dist/flows/run/index.js +1 -0
  39. package/dist/flows/run/project.js +18 -21
  40. package/dist/flows/steps/api.js +28 -5
  41. package/dist/flows/steps/catalog.d.ts +10 -4
  42. package/dist/flows/steps/catalog.js +23 -16
  43. package/dist/flows/steps/define.d.ts +2 -0
  44. package/dist/flows/steps/define.js +15 -4
  45. package/dist/flows/steps/handler.js +16 -4
  46. package/dist/flows/steps/index.d.ts +3 -2
  47. package/dist/flows/steps/index.js +2 -1
  48. package/dist/flows/steps/set-variable.js +14 -3
  49. package/dist/flows/steps/set.d.ts +26 -0
  50. package/dist/flows/steps/set.js +43 -0
  51. package/dist/flows/steps/wait.js +8 -1
  52. package/dist/recorder/session.js +4 -0
  53. package/dist/runner/groups.d.ts +5 -0
  54. package/dist/runner/groups.js +13 -0
  55. package/dist/runner/test-runner.js +9 -3
  56. package/dist/transport/bidi-connection.d.ts +2 -0
  57. package/dist/transport/bidi-connection.js +1 -1
  58. package/dist/types/config.d.ts +12 -0
  59. package/package.json +6 -1
package/README.md CHANGED
@@ -22,14 +22,14 @@ test("Visitor can sign in", async ({ page, env, secrets }) => {
22
22
  - **Network control.** Mock, block or modify requests; wait for requests, responses and network idle; read response bodies.
23
23
  - **Recorder building blocks.** Specs are parsed into steps and edited back with minimal diffs, and an engine records what a person does in the browser. The UI that ties them together is not built yet (see the [roadmap](docs/roadmap.md)).
24
24
 
25
- > **Status:** early. Firefox only, run from a checkout of this repository (not published to npm yet). See [what is still open](docs/roadmap.md).
25
+ > **Status:** early. Firefox only. See [what is still open](docs/roadmap.md).
26
26
 
27
27
  ## Getting started
28
28
 
29
29
  ### 1. Requirements
30
30
 
31
31
  - [Node.js](https://nodejs.org) 26 or newer and [Bun](https://bun.sh) 1.3 or newer (CI uses these versions)
32
- - Firefox installed on the machine. It is found in the usual install locations on Linux, macOS and Windows.
32
+ - Firefox, downloaded by the framework: run `npx samurai browsers install firefox` once in a project (`bun run dev browsers install firefox` in this checkout; see [Which browser runs](docs/pages-and-browsers.md#which-browser-runs)). The Firefox installed on the machine is not used.
33
33
 
34
34
  ### 2. Install
35
35
 
package/dist/api.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { test, describe } from "./runner/registry.js";
2
2
  export { expect } from "./assert/expect.js";
3
3
  export { defineConfig } from "./config/config.js";
4
+ export { defineStep, type StepDefinition, type StepResult, type StepRunInput, } from "./flows/steps/index.js";
4
5
  export type { TestFixtures } from "./types/test.js";
package/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { test, describe } from "./runner/registry.js";
2
2
  export { expect } from "./assert/expect.js";
3
3
  export { defineConfig } from "./config/config.js";
4
+ export { defineStep, } from "./flows/steps/index.js";
@@ -1,2 +1,3 @@
1
1
  import type { SupportedBrowser } from "../types/browser.js";
2
+ /** @deprecated Launches use the builds SAMURAI downloads (`samurai browsers install`); removed in a later major */
2
3
  export declare function findBrowser(browserName: SupportedBrowser): string;
@@ -7,6 +7,7 @@ const macBrowserPaths = {
7
7
  // chrome: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
8
8
  firefox: "/Applications/Firefox.app/Contents/MacOS/Firefox",
9
9
  };
10
+ /** @deprecated Launches use the builds SAMURAI downloads (`samurai browsers install`); removed in a later major */
10
11
  export function findBrowser(browserName) {
11
12
  logger.verbose("Checking install location for %s", browserName);
12
13
  if (!supportedBrowsers.includes(browserName)) {
@@ -18,6 +18,12 @@ export declare const SESSION_CAPABILITIES: {
18
18
  };
19
19
  };
20
20
  };
21
+ /** session.new got no reply in time */
22
+ export declare class BrowserStartTimeoutError extends Error {
23
+ constructor(label: string, timeout: number);
24
+ }
25
+ /** Sends session.new with its own, longer timeout; `label` names the browser in the timeout error */
26
+ export declare function startSession(connector: Pick<BiDiConnector, "send">, label: string, commandTimeout: number | undefined): Promise<void>;
21
27
  export declare const browserLaunchFlag: (browserName: SupportedBrowser, launchOptions: Partial<BrowserLaunchOptions>) => string[];
22
28
  /**
23
29
  * Stops what a failed launch started. A browser left running holds the profile (the next launch
@@ -28,6 +34,8 @@ export declare function abandonLaunch(browserProc: {
28
34
  }, biDiConnector: {
29
35
  kill(): void;
30
36
  } | undefined): void;
37
+ /** Env added to a browser's process */
38
+ export declare const browserLaunchEnv: (browserName: SupportedBrowser, appDataPath?: string) => Record<string, string>;
31
39
  export interface BrowserLaunchOptions {
32
40
  /** Which port to use for the WebDeveloper tools */
33
41
  port: number;
@@ -1,9 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { setTimeout as sleep } from "node:timers/promises";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { findBrowser } from "./browser-finder.js";
4
+ import { projectBrowser } from "../browsers/resolve.js";
5
5
  import path from "node:path";
6
- import { BiDiConnector } from "../transport/bidi-connection.js";
6
+ import { BiDiConnector, DEFAULT_COMMAND_TIMEOUT, } from "../transport/bidi-connection.js";
7
+ import { BiDiError } from "../transport/bidi-error.js";
7
8
  import Page from "./page.js";
8
9
  import { BrowserContext } from "./browser-context.js";
9
10
  import { validateEmulation } from "./emulation.js";
@@ -17,6 +18,27 @@ import { browsersDir, projectDir, readConfig } from "../config/config.js";
17
18
  export const SESSION_CAPABILITIES = {
18
19
  alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
19
20
  };
21
+ /** session.new waits at least this long (ms): the first start after an install can take a while (macOS checks the new app) */
22
+ const SESSION_START_TIMEOUT = 120_000;
23
+ /** session.new got no reply in time */
24
+ export class BrowserStartTimeoutError extends Error {
25
+ constructor(label, timeout) {
26
+ super(`${label} did not finish starting within ${Math.round(timeout / 1000)}s. The first start after an install can be slow; run again.`);
27
+ this.name = "BrowserStartTimeoutError";
28
+ }
29
+ }
30
+ /** Sends session.new with its own, longer timeout; `label` names the browser in the timeout error */
31
+ export async function startSession(connector, label, commandTimeout) {
32
+ const timeout = Math.max(commandTimeout ?? DEFAULT_COMMAND_TIMEOUT, SESSION_START_TIMEOUT);
33
+ try {
34
+ await connector.send("session.new", { capabilities: SESSION_CAPABILITIES }, { timeout });
35
+ }
36
+ catch (err) {
37
+ if (err instanceof BiDiError && err.code === "timeout")
38
+ throw new BrowserStartTimeoutError(label, timeout);
39
+ throw err;
40
+ }
41
+ }
20
42
  /** Time (ms) to wait for Firefox to answer browser.close before killing the process */
21
43
  const BROWSER_CLOSE_GRACE = 2000;
22
44
  const browserProfilePath = (browserName) => path.join(browsersDir(), "profiles", browserName, "user.js");
@@ -70,13 +92,15 @@ export function abandonLaunch(browserProc, biDiConnector) {
70
92
  // On macOS a spawned browser is attributed to the terminal, which is denied access to
71
93
  // ~/Library/Application Support/Firefox (profiles.ini). Point Firefox at a local app data dir instead.
72
94
  const browserAppDataPath = (browserName) => path.join(browsersDir(), "app-data", browserName);
73
- const browserLaunchEnv = (browserName) => {
74
- const appDataPath = browserAppDataPath(browserName);
95
+ /** Env added to a browser's process */
96
+ export const browserLaunchEnv = (browserName, appDataPath = browserAppDataPath(browserName)) => {
75
97
  mkdirSync(appDataPath, { recursive: true });
76
98
  logger.debug("Using app data dir for %s: %s", browserName, appDataPath);
77
99
  switch (browserName) {
78
100
  case "firefox":
79
- return { MOZ_APP_DATA: appDataPath };
101
+ // The project profile may have been opened by a newer Firefox (the system one, before 0.6);
102
+ // without this Firefox refuses to start on it
103
+ return { MOZ_APP_DATA: appDataPath, MOZ_ALLOW_DOWNGRADE: "1" };
80
104
  }
81
105
  };
82
106
  const browserProfiles = {
@@ -127,7 +151,7 @@ export class Browser {
127
151
  signal) {
128
152
  signal?.throwIfAborted();
129
153
  logger.verbose("Trying to launch browser %s", browserName);
130
- const browserLocation = findBrowser(browserName);
154
+ const { executablePath: browserLocation, label } = await projectBrowser(browserName);
131
155
  logger.debug("Found browser install location at: %s", browserLocation);
132
156
  ensureBrowserProfile(browserName);
133
157
  logger.verbose("Trying to spawn %s process", browserName);
@@ -163,15 +187,12 @@ export class Browser {
163
187
  try {
164
188
  let url = urlMatch[0] + browserWsPath[browserName];
165
189
  logger.verbose("Websocket URL match: %s", url);
166
- biDiConnector = await BiDiConnector.connect(url, {
167
- commandTimeout: await readCommandTimeout(),
168
- });
190
+ const commandTimeout = await readCommandTimeout();
191
+ biDiConnector = await BiDiConnector.connect(url, { commandTimeout });
169
192
  logger.verbose("Starting new session");
170
193
  const status = await biDiConnector.send("session.status", {});
171
194
  logger.verbose("Session status", status);
172
- await biDiConnector.send("session.new", {
173
- capabilities: SESSION_CAPABILITIES,
174
- });
195
+ await startSession(biDiConnector, label, commandTimeout);
175
196
  logger.verbose("Session started successfully");
176
197
  const downloadsDir = await prepareDownloadsDir(biDiConnector, path.resolve(projectDir(), (await readOptionalConfig("downloadsDir")) ?? "result/downloads"));
177
198
  const browser = new Browser({
@@ -0,0 +1,14 @@
1
+ import { type ResolvedBrowser } from "../browsers/resolve.js";
2
+ import type { SupportedBrowser } from "../types/browser.js";
3
+ import { type BrowserLaunchOptions } from "./browser.js";
4
+ /** Written into a managed build's folder once it started there; its first start can be slow (macOS checks the new app) */
5
+ export declare const LAUNCHED_STAMP = ".samurai-launched";
6
+ /**
7
+ * Starts a managed build once, before its first real use, so that slow first start happens outside a test's
8
+ * timeout. A browser set by env or config, a stamped build, and an aborted run are skipped.
9
+ */
10
+ export declare function warmUp(target: ResolvedBrowser, launch: () => Promise<{
11
+ close(): Promise<void>;
12
+ }>, signal?: AbortSignal): Promise<void>;
13
+ /** Resolves the project's browser (throws when it is missing) and warms a managed build up, headless */
14
+ export declare function ensureWarm(name: SupportedBrowser, launchOptions?: Partial<BrowserLaunchOptions>, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { projectBrowser } from "../browsers/resolve.js";
4
+ import logger from "../logger/index.js";
5
+ import { Browser, BrowserStartTimeoutError, } from "./browser.js";
6
+ /** Written into a managed build's folder once it started there; its first start can be slow (macOS checks the new app) */
7
+ export const LAUNCHED_STAMP = ".samurai-launched";
8
+ /**
9
+ * Starts a managed build once, before its first real use, so that slow first start happens outside a test's
10
+ * timeout. A browser set by env or config, a stamped build, and an aborted run are skipped.
11
+ */
12
+ export async function warmUp(target, launch, signal) {
13
+ if (!target.buildDir)
14
+ return;
15
+ const stamp = path.join(target.buildDir, LAUNCHED_STAMP);
16
+ if (existsSync(stamp) || signal?.aborted)
17
+ return;
18
+ logger.verbose("First start of %s", target.label);
19
+ let browser;
20
+ try {
21
+ browser = await launch();
22
+ }
23
+ catch (err) {
24
+ if (signal?.aborted)
25
+ return;
26
+ if (err instanceof BrowserStartTimeoutError)
27
+ throw err;
28
+ throw new Error(`Could not start ${target.label} for the first time: ${err instanceof Error ? err.message : String(err)}`);
29
+ }
30
+ await browser.close();
31
+ writeFileSync(stamp, new Date().toISOString());
32
+ }
33
+ /** Resolves the project's browser (throws when it is missing) and warms a managed build up, headless */
34
+ export async function ensureWarm(name, launchOptions = {}, signal) {
35
+ await warmUp(await projectBrowser(name), async () => (await Browser.launch(name, { ...launchOptions, headless: true }, signal))
36
+ .browser, signal);
37
+ }
@@ -0,0 +1,2 @@
1
+ /** Where downloaded browsers live, shared by every project of this user: `SAMURAI_BROWSERS_PATH`, else the OS cache folder */
2
+ export declare function browsersCacheDir(env?: Partial<NodeJS.ProcessEnv>, platform?: NodeJS.Platform, home?: string): string;
@@ -0,0 +1,15 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ /** Where downloaded browsers live, shared by every project of this user: `SAMURAI_BROWSERS_PATH`, else the OS cache folder */
4
+ export function browsersCacheDir(env = process.env, platform = process.platform, home = homedir()) {
5
+ if (env.SAMURAI_BROWSERS_PATH)
6
+ return path.resolve(env.SAMURAI_BROWSERS_PATH);
7
+ switch (platform) {
8
+ case "darwin":
9
+ return path.join(home, "Library", "Caches", "samurai", "browsers");
10
+ case "win32":
11
+ return path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "samurai", "browsers");
12
+ default:
13
+ return path.join(env.XDG_CACHE_HOME || path.join(home, ".cache"), "samurai", "browsers");
14
+ }
15
+ }
@@ -0,0 +1,23 @@
1
+ /** Written into a build folder once its download and unpack finished; a folder without it is unfinished */
2
+ export declare const COMPLETE_MARKER = ".samurai-complete";
3
+ /** One build folder in the cache, as the downloader names it ("firefox", "chrome", "chromedriver") */
4
+ export interface CachedBuild {
5
+ browser: string;
6
+ buildId: string;
7
+ dir: string;
8
+ }
9
+ /** What the store needs to fetch and find builds; tests pass a fake */
10
+ export interface Downloader {
11
+ /** undefined: no builds exist for this OS and architecture */
12
+ platform(): string | undefined;
13
+ /** The folder a build is, or would be, unpacked into */
14
+ installDir(cacheDir: string, browser: string, buildId: string): string;
15
+ executablePath(cacheDir: string, browser: string, buildId: string): string;
16
+ install(cacheDir: string, browser: string, buildId: string, onProgress?: (done: number, total: number) => void): Promise<void>;
17
+ uninstall(cacheDir: string, browser: string, buildId: string): void;
18
+ /** Removes a build's folder (its marker first) and any archive a killed download left behind */
19
+ clean(cacheDir: string, browser: string, buildId: string): void;
20
+ list(cacheDir: string): CachedBuild[];
21
+ }
22
+ /** Downloads from Mozilla and Google with @puppeteer/browsers; `baseUrl` (tests only) points it elsewhere */
23
+ export declare function puppeteerDownloader(baseUrl?: string | undefined): Downloader;
@@ -0,0 +1,52 @@
1
+ import { existsSync, readdirSync, rmSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { Browser as PuppeteerBrowser, Cache, detectBrowserPlatform, install, } from "@puppeteer/browsers";
4
+ /** Written into a build folder once its download and unpack finished; a folder without it is unfinished */
5
+ export const COMPLETE_MARKER = ".samurai-complete";
6
+ /** Downloads from Mozilla and Google with @puppeteer/browsers; `baseUrl` (tests only) points it elsewhere */
7
+ export function puppeteerDownloader(baseUrl = process.env.SAMURAI_BROWSERS_BASE_URL) {
8
+ const platform = () => {
9
+ const found = detectBrowserPlatform();
10
+ if (!found)
11
+ throw new Error(`No browser downloads for ${process.platform} ${process.arch}`);
12
+ return found;
13
+ };
14
+ const as = (browser) => browser;
15
+ return {
16
+ platform: () => detectBrowserPlatform(),
17
+ installDir: (cacheDir, browser, buildId) => new Cache(cacheDir).installationDir(as(browser), platform(), buildId),
18
+ executablePath: (cacheDir, browser, buildId) => new Cache(cacheDir).computeExecutablePath({
19
+ browser: as(browser),
20
+ platform: platform(),
21
+ buildId,
22
+ }),
23
+ async install(cacheDir, browser, buildId, onProgress) {
24
+ await install({
25
+ cacheDir,
26
+ browser: as(browser),
27
+ buildId,
28
+ platform: platform(),
29
+ ...(onProgress && { downloadProgressCallback: onProgress }),
30
+ ...(baseUrl && { baseUrl }),
31
+ });
32
+ },
33
+ uninstall: (cacheDir, browser, buildId) => new Cache(cacheDir).uninstall(as(browser), platform(), buildId),
34
+ clean(cacheDir, browser, buildId) {
35
+ const cache = new Cache(cacheDir);
36
+ const dir = cache.installationDir(as(browser), platform(), buildId);
37
+ // The marker goes first: a removal that stops halfway leaves a folder that reads as missing
38
+ rmSync(path.join(dir, COMPLETE_MARKER), { force: true });
39
+ rmSync(dir, { recursive: true, force: true });
40
+ // The archive is downloaded next to the build folders, as <buildId>-<file>
41
+ const root = cache.browserRoot(as(browser));
42
+ if (!existsSync(root))
43
+ return;
44
+ for (const entry of readdirSync(root))
45
+ if (entry.startsWith(`${buildId}-`))
46
+ rmSync(path.join(root, entry), { recursive: true, force: true });
47
+ },
48
+ list: (cacheDir) => new Cache(cacheDir)
49
+ .getInstalledBrowsers()
50
+ .map((b) => ({ browser: b.browser, buildId: b.buildId, dir: b.path })),
51
+ };
52
+ }
@@ -0,0 +1,6 @@
1
+ export { browsersCacheDir } from "./cache-dir.js";
2
+ export type { CachedBuild, Downloader } from "./downloader.js";
3
+ export { ENV_PATH, type Override } from "./overrides.js";
4
+ export { LAUNCHABLE, MANAGED_BROWSERS, PINNED, isManagedBrowser, type ManagedBrowser, } from "./pins.js";
5
+ export { browserStatus, projectBrowser, projectExecutable, resolveBrowser, resolveExecutable, type ResolvedBrowser, } from "./resolve.js";
6
+ export { BrowserNotInstalledError, installBrowser, listInstalled, managedBuildDir, managedExecutable, uninstallBrowser, type BrowserStatus, type InstalledBuild, type StoreOptions, } from "./store.js";
@@ -0,0 +1,5 @@
1
+ export { browsersCacheDir } from "./cache-dir.js";
2
+ export { ENV_PATH } from "./overrides.js";
3
+ export { LAUNCHABLE, MANAGED_BROWSERS, PINNED, isManagedBrowser, } from "./pins.js";
4
+ export { browserStatus, projectBrowser, projectExecutable, resolveBrowser, resolveExecutable, } from "./resolve.js";
5
+ export { BrowserNotInstalledError, installBrowser, listInstalled, managedBuildDir, managedExecutable, uninstallBrowser, } from "./store.js";
@@ -0,0 +1,17 @@
1
+ import type { ManagedBrowser } from "./pins.js";
2
+ /** A browser binary set on purpose, and where */
3
+ export interface Override {
4
+ path: string;
5
+ from: "env" | "config";
6
+ }
7
+ /** Where the overrides come from; each defaults to the running process */
8
+ export interface OverrideSources {
9
+ env?: Partial<NodeJS.ProcessEnv>;
10
+ /** `browserPaths` of samurai.config.ts */
11
+ browserPaths?: Partial<Record<ManagedBrowser, string>> | undefined;
12
+ /** What relative `browserPaths` resolve from */
13
+ projectDir?: string;
14
+ }
15
+ export declare const ENV_PATH: Record<ManagedBrowser, string>;
16
+ /** The env variable, else the config's `browserPaths`; undefined: use the managed build */
17
+ export declare function overrideOf(name: ManagedBrowser, { env, browserPaths, projectDir, }?: OverrideSources): Override | undefined;
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+ export const ENV_PATH = {
3
+ firefox: "SAMURAI_FIREFOX_PATH",
4
+ chrome: "SAMURAI_CHROME_PATH",
5
+ };
6
+ /** The env variable, else the config's `browserPaths`; undefined: use the managed build */
7
+ export function overrideOf(name, { env = process.env, browserPaths, projectDir = process.cwd(), } = {}) {
8
+ const fromEnv = env[ENV_PATH[name]];
9
+ if (fromEnv)
10
+ return { path: path.resolve(fromEnv), from: "env" };
11
+ const fromConfig = browserPaths?.[name];
12
+ if (fromConfig)
13
+ return { path: path.resolve(projectDir, fromConfig), from: "config" };
14
+ return undefined;
15
+ }
@@ -0,0 +1,16 @@
1
+ /** The browser builds this framework version downloads and launches. Bumped by hand in a release */
2
+ export declare const PINNED: {
3
+ readonly firefox: {
4
+ readonly buildId: "stable_158.0";
5
+ readonly label: "Firefox 158.0";
6
+ };
7
+ readonly chrome: {
8
+ readonly buildId: "155.0.8059.79";
9
+ readonly label: "Chrome for Testing 155.0.8059.79";
10
+ };
11
+ };
12
+ export type ManagedBrowser = keyof typeof PINNED;
13
+ export declare const MANAGED_BROWSERS: ManagedBrowser[];
14
+ /** The managed browsers the framework can launch; Chrome joins when it can drive it */
15
+ export declare const LAUNCHABLE: readonly ManagedBrowser[];
16
+ export declare function isManagedBrowser(name: string): name is ManagedBrowser;
@@ -0,0 +1,14 @@
1
+ /** The browser builds this framework version downloads and launches. Bumped by hand in a release */
2
+ export const PINNED = {
3
+ firefox: { buildId: "stable_158.0", label: "Firefox 158.0" },
4
+ chrome: {
5
+ buildId: "155.0.8059.79",
6
+ label: "Chrome for Testing 155.0.8059.79",
7
+ },
8
+ };
9
+ export const MANAGED_BROWSERS = Object.keys(PINNED);
10
+ /** The managed browsers the framework can launch; Chrome joins when it can drive it */
11
+ export const LAUNCHABLE = ["firefox"];
12
+ export function isManagedBrowser(name) {
13
+ return Object.prototype.hasOwnProperty.call(PINNED, name);
14
+ }
@@ -0,0 +1,26 @@
1
+ import type { SamuraiTestConfig } from "../types/config.js";
2
+ import { type OverrideSources } from "./overrides.js";
3
+ import { type ManagedBrowser } from "./pins.js";
4
+ import { type BrowserStatus, type StoreOptions } from "./store.js";
5
+ /** The browser a launch uses, and how to name it */
6
+ export interface ResolvedBrowser {
7
+ executablePath: string;
8
+ /** "Firefox 158.0" for the managed build, else "Firefox" */
9
+ label: string;
10
+ /** The managed build's folder; undefined when an env or config path is used instead */
11
+ buildDir?: string;
12
+ }
13
+ /** The browser a launch uses: the env path, else the config's `browserPaths`, else the pinned build. Never downloads */
14
+ export declare function resolveBrowser(name: ManagedBrowser, sources?: OverrideSources & StoreOptions): ResolvedBrowser;
15
+ /** The binary a launch uses; see `resolveBrowser` */
16
+ export declare function resolveExecutable(name: ManagedBrowser, sources?: OverrideSources & StoreOptions): string;
17
+ /** `resolveExecutable` for the active project */
18
+ export declare function projectExecutable(name: ManagedBrowser): Promise<string>;
19
+ /** `resolveBrowser` for the active project, with its config's `browserPaths` (none when the config can't be read) */
20
+ export declare function projectBrowser(name: ManagedBrowser): Promise<ResolvedBrowser>;
21
+ /** Every pinned browser's status for a project (default: the working directory); a config that can't be read gives no config overrides */
22
+ export declare function browserStatus(options?: StoreOptions & {
23
+ projectDir?: string;
24
+ config?: SamuraiTestConfig;
25
+ env?: Partial<NodeJS.ProcessEnv>;
26
+ }): Promise<BrowserStatus[]>;
@@ -0,0 +1,56 @@
1
+ import { existsSync } from "node:fs";
2
+ import { projectConfig, projectDir, readConfig } from "../config/config.js";
3
+ import logger from "../logger/index.js";
4
+ import { ENV_PATH, overrideOf } from "./overrides.js";
5
+ import { MANAGED_BROWSERS, PINNED } from "./pins.js";
6
+ import { managedBuildDir, managedExecutable, statusOf, } from "./store.js";
7
+ /** The browser a launch uses: the env path, else the config's `browserPaths`, else the pinned build. Never downloads */
8
+ export function resolveBrowser(name, sources = {}) {
9
+ const override = overrideOf(name, sources);
10
+ if (!override)
11
+ return {
12
+ executablePath: managedExecutable(name, sources),
13
+ label: PINNED[name].label,
14
+ buildDir: managedBuildDir(name, sources),
15
+ };
16
+ if (!existsSync(override.path))
17
+ throw new Error(override.from === "env"
18
+ ? `${ENV_PATH[name]} points to a missing file: ${override.path}`
19
+ : `browserPaths.${name} in samurai.config.ts points to a missing file: ${override.path}`);
20
+ return {
21
+ executablePath: override.path,
22
+ label: name.charAt(0).toUpperCase() + name.slice(1),
23
+ };
24
+ }
25
+ /** The binary a launch uses; see `resolveBrowser` */
26
+ export function resolveExecutable(name, sources = {}) {
27
+ return resolveBrowser(name, sources).executablePath;
28
+ }
29
+ /** `resolveExecutable` for the active project */
30
+ export async function projectExecutable(name) {
31
+ return (await projectBrowser(name)).executablePath;
32
+ }
33
+ /** `resolveBrowser` for the active project, with its config's `browserPaths` (none when the config can't be read) */
34
+ export async function projectBrowser(name) {
35
+ let browserPaths;
36
+ try {
37
+ browserPaths = await readConfig("browserPaths");
38
+ }
39
+ catch (err) {
40
+ logger.debug("Could not read browserPaths from config", { err });
41
+ }
42
+ return resolveBrowser(name, { browserPaths, projectDir: projectDir() });
43
+ }
44
+ /** Every pinned browser's status for a project (default: the working directory); a config that can't be read gives no config overrides */
45
+ export async function browserStatus(options = {}) {
46
+ const dir = options.projectDir ?? process.cwd();
47
+ let browserPaths;
48
+ try {
49
+ browserPaths = (await projectConfig({ ...options, projectDir: dir }))
50
+ .browserPaths;
51
+ }
52
+ catch (err) {
53
+ logger.debug("Could not read browserPaths from config", { err });
54
+ }
55
+ return MANAGED_BROWSERS.map((name) => statusOf(name, { ...options, browserPaths, projectDir: dir }));
56
+ }
@@ -0,0 +1,52 @@
1
+ import { COMPLETE_MARKER, type Downloader } from "./downloader.js";
2
+ import { type Override, type OverrideSources } from "./overrides.js";
3
+ import { type ManagedBrowser } from "./pins.js";
4
+ export { COMPLETE_MARKER };
5
+ export declare class BrowserNotInstalledError extends Error {
6
+ readonly browser: ManagedBrowser;
7
+ readonly label: string;
8
+ constructor(browser: ManagedBrowser, label: string);
9
+ }
10
+ export interface StoreOptions {
11
+ /** Default: `browsersCacheDir()` */
12
+ cacheDir?: string;
13
+ /** Default: @puppeteer/browsers */
14
+ downloader?: Downloader;
15
+ }
16
+ export interface BrowserStatus {
17
+ name: ManagedBrowser;
18
+ label: string;
19
+ buildId: string;
20
+ /** unavailable: no download exists for this OS and architecture */
21
+ state: "installed" | "missing" | "unavailable";
22
+ executablePath?: string;
23
+ /** Whether the framework can launch it yet */
24
+ launchable: boolean;
25
+ /** A path set on purpose, which launches use instead of the managed build */
26
+ override?: Override;
27
+ }
28
+ export interface InstalledBuild {
29
+ name: ManagedBrowser;
30
+ buildId: string;
31
+ dir: string;
32
+ sizeBytes: number;
33
+ /** Pinned by this framework version */
34
+ pinned: boolean;
35
+ }
36
+ /**
37
+ * Downloads the pinned build unless it is complete already; returns its executable. The download goes into a
38
+ * private staging folder and is moved into place once complete, so two installs at once do not break each other.
39
+ */
40
+ export declare function installBrowser(name: ManagedBrowser, options?: StoreOptions & {
41
+ onProgress?: (done: number, total: number) => void;
42
+ }): Promise<string>;
43
+ /** The pinned build of `name`: whether it is there, and any path set instead of it */
44
+ export declare function statusOf(name: ManagedBrowser, options?: StoreOptions & OverrideSources): BrowserStatus;
45
+ /** The pinned build's executable; throws BrowserNotInstalledError when it isn't complete */
46
+ export declare function managedExecutable(name: ManagedBrowser, options?: StoreOptions): string;
47
+ /** The folder the pinned build of `name` is, or would be, installed in */
48
+ export declare function managedBuildDir(name: ManagedBrowser, options?: StoreOptions): string;
49
+ /** Removes a build (default: the pinned one); Chrome takes its chromedriver along */
50
+ export declare function uninstallBrowser(name: ManagedBrowser, buildId?: string, options?: StoreOptions): void;
51
+ /** Every Firefox and Chrome build in the cache, also those other framework versions pinned */
52
+ export declare function listInstalled(options?: StoreOptions): InstalledBuild[];