@venn-lang/browser 0.1.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.
@@ -0,0 +1,28 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import { type BrowserDriver, BrowserDriverPort } from "../port/index.js";
3
+
4
+ function notImplemented(): VennError {
5
+ return new VennError({
6
+ code: "VN8090",
7
+ message: "browser real driver not implemented in this build",
8
+ });
9
+ }
10
+
11
+ /**
12
+ * The engine-driving `BrowserDriver`. Not wired up in this repo, which ships
13
+ * the language rather than an automation backend.
14
+ *
15
+ * Methods are generated from the port descriptor, so the stub cannot fall
16
+ * behind the interface.
17
+ *
18
+ * @returns a driver whose every method throws, so the gap is a legible failure
19
+ * rather than a test that passes against nothing.
20
+ * @throws {VennError} `VN8090` on any call.
21
+ */
22
+ export function createRealBrowserDriver(): BrowserDriver {
23
+ const fail = (): never => {
24
+ throw notImplemented();
25
+ };
26
+ const entries = BrowserDriverPort.methods.map((method) => [method, fail] as const);
27
+ return Object.fromEntries(entries) as unknown as BrowserDriver;
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@venn-lang/browser`: the verbs that drive a page, the matchers that assert about
3
+ * an element, and the two ports behind them. `BrowserDriver` does the driving;
4
+ * `PreviewProvider` streams frames for a live view of a run.
5
+ */
6
+
7
+ export * from "./drivers/index.js";
8
+ export { browserPlugin, browserPlugin as default } from "./plugin.js";
9
+ export * from "./port/index.js";
10
+ export * from "./preview/index.js";
@@ -0,0 +1,6 @@
1
+ import type { MatcherDefinition } from "@venn-lang/sdk";
2
+ import { text } from "./text.js";
3
+ import { visible } from "./visible.js";
4
+
5
+ /** Every matcher the `browser` namespace contributes to `expect`. */
6
+ export const browserMatchers: MatcherDefinition[] = [visible, text];
@@ -0,0 +1,22 @@
1
+ import { arg, defineMatcher, type MatcherDefinition } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+
4
+ /**
5
+ * `expect element text "Order confirmed"`.
6
+ *
7
+ * Passes when the element's text equals or contains the expected string.
8
+ * Containment is deliberate: assertions about copy should survive a stray
9
+ * space or a wrapping node.
10
+ */
11
+ export const text: MatcherDefinition = defineMatcher({
12
+ name: "text",
13
+ args: [arg("value", t.string, "The text the element should carry.")],
14
+ appliesTo: "Element",
15
+ test: ({ subject, args }) => matchesText(subject, String(args[0])),
16
+ message: ({ args }) => `expected the element text to contain "${String(args[0])}"`,
17
+ });
18
+
19
+ function matchesText(subject: unknown, expected: string): boolean {
20
+ const actual = (subject as { text?: string } | null)?.text ?? "";
21
+ return actual === expected || actual.includes(expected);
22
+ }
@@ -0,0 +1,18 @@
1
+ import { defineMatcher, type MatcherDefinition } from "@venn-lang/sdk";
2
+
3
+ /**
4
+ * `expect element visible`.
5
+ *
6
+ * Passes when the element is on screen. An element that is absent, or whose
7
+ * subject is not an element at all, fails rather than erroring.
8
+ */
9
+ export const visible: MatcherDefinition = defineMatcher({
10
+ name: "visible",
11
+ appliesTo: "Element",
12
+ test: ({ subject }) => isVisible(subject),
13
+ message: () => "expected the element to be visible",
14
+ });
15
+
16
+ function isVisible(subject: unknown): boolean {
17
+ return (subject as { visible?: boolean } | null)?.visible === true;
18
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { browserActions } from "./actions/index.js";
3
+ import { browserMatchers } from "./matchers/index.js";
4
+ import { browserTypes } from "./plugin.types.js";
5
+ import { browserResources } from "./resources/index.js";
6
+ import { browserTypeDefs } from "./types.js";
7
+
8
+ /**
9
+ * The `@venn-lang/browser` plugin. Registers the `browser` namespace: sixteen verbs,
10
+ * the `visible` and `text` matchers, the `Browser` and `Page` resources, and
11
+ * the nominal types those hand around. Requires the `net` capability.
12
+ */
13
+ export const browserPlugin: PluginDefinition = definePlugin({
14
+ name: "venn/browser",
15
+ version: "0.0.0",
16
+ namespace: "browser",
17
+ requires: ["net"],
18
+ actions: browserActions,
19
+ matchers: browserMatchers,
20
+ resources: browserResources,
21
+ types: browserTypes,
22
+ typeDefs: browserTypeDefs,
23
+ });
24
+
25
+ export { browserPlugin as default };
@@ -0,0 +1,13 @@
1
+ import { type ZodType, z } from "@venn-lang/sdk";
2
+
3
+ /** The handle `browser.launch` yields. */
4
+ const Browser = z.object({ id: z.string(), engine: z.string() });
5
+
6
+ /** The handle `browser.newContext` yields; what `page.` completes against. */
7
+ const Page = z.object({ id: z.string(), url: z.string() });
8
+
9
+ /** A resolved DOM element, as `visible` and `text` are handed it. */
10
+ const Element = z.object({ visible: z.boolean(), text: z.string(), value: z.string() });
11
+
12
+ /** Runtime validators for the nominal types the `browser` namespace registers. */
13
+ export const browserTypes: Record<string, ZodType> = { Browser, Page, Element };
@@ -0,0 +1,33 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { BrowserDriver } from "./browser-driver.types.js";
3
+
4
+ /**
5
+ * The port every `browser` verb reaches through. Requires the `net` capability,
6
+ * so a host without it fails to load the plugin rather than failing mid-run.
7
+ *
8
+ * `methods` must list every method a verb calls. An omission is not checked at
9
+ * load time, so it surfaces as a TypeError mid-run instead of a legible VN2011.
10
+ */
11
+ export const BrowserDriverPort: Port<BrowserDriver> = {
12
+ id: "venn.port.browser-driver",
13
+ version: 1,
14
+ requires: ["net"],
15
+ methods: [
16
+ "launch",
17
+ "newContext",
18
+ "visit",
19
+ "click",
20
+ "fill",
21
+ "select",
22
+ "hover",
23
+ "press",
24
+ "upload",
25
+ "download",
26
+ "screenshot",
27
+ "waitFor",
28
+ "waitForUrl",
29
+ "evaluate",
30
+ "frame",
31
+ "clearCookies",
32
+ ],
33
+ };
@@ -0,0 +1,108 @@
1
+ /** How an engine is launched. Both fields fall back to the driver's default. */
2
+ export interface LaunchOptions {
3
+ engine?: string;
4
+ headless?: boolean;
5
+ }
6
+
7
+ /** A viewport size, in CSS pixels. */
8
+ export interface Viewport {
9
+ width: number;
10
+ height: number;
11
+ }
12
+
13
+ /** How an isolated context is opened. */
14
+ export interface ContextOptions {
15
+ locale?: string;
16
+ viewport?: Viewport;
17
+ }
18
+
19
+ /** Where to navigate, and what to send with the request. */
20
+ export interface VisitArgs {
21
+ url: string;
22
+ headers?: Record<string, string>;
23
+ }
24
+
25
+ /** A selector paired with the value to write. Shared by `fill` and `select`. */
26
+ export interface FillArgs {
27
+ selector: string;
28
+ value: string;
29
+ }
30
+
31
+ /** A key press. Without a `selector`, the page holds focus. */
32
+ export interface PressArgs {
33
+ key: string;
34
+ selector?: string;
35
+ }
36
+
37
+ /** A file input, and the path of the file to hand it. */
38
+ export interface UploadArgs {
39
+ selector: string;
40
+ file: string;
41
+ }
42
+
43
+ /** What to click to start a download. */
44
+ export interface DownloadArgs {
45
+ selector: string;
46
+ }
47
+
48
+ /** A page condition to wait on. Every field is optional; `timeout` is in milliseconds. */
49
+ export interface WaitForArgs {
50
+ text?: string;
51
+ selector?: string;
52
+ timeout?: number;
53
+ }
54
+
55
+ /** JavaScript to run inside the page. */
56
+ export interface EvaluateArgs {
57
+ script: string;
58
+ }
59
+
60
+ /** An iframe to enter, named or selected. */
61
+ export interface FrameArgs {
62
+ name: string;
63
+ }
64
+
65
+ /** A live engine. The nominal `browser.Browser`. */
66
+ export interface BrowserHandle {
67
+ id: string;
68
+ engine: string;
69
+ }
70
+
71
+ /** A live isolated context. The nominal `browser.Page`. */
72
+ export interface PageHandle {
73
+ id: string;
74
+ /** Where the context points at the moment the handle is taken. */
75
+ url: string;
76
+ }
77
+
78
+ /** Where a downloaded file landed, and how big it is. */
79
+ export interface DownloadResult {
80
+ path: string;
81
+ bytes: number;
82
+ }
83
+
84
+ /** Where a screenshot landed, under the name it was asked for. */
85
+ export interface ScreenshotResult {
86
+ name: string;
87
+ path: string;
88
+ }
89
+
90
+ /** The contract every `browser` verb goes through. */
91
+ export interface BrowserDriver {
92
+ launch(opts: LaunchOptions): Promise<BrowserHandle>;
93
+ newContext(opts: ContextOptions): Promise<PageHandle>;
94
+ visit(args: VisitArgs): Promise<void>;
95
+ click(selector: string): Promise<void>;
96
+ fill(args: FillArgs): Promise<void>;
97
+ select(args: FillArgs): Promise<void>;
98
+ hover(selector: string): Promise<void>;
99
+ press(args: PressArgs): Promise<void>;
100
+ upload(args: UploadArgs): Promise<void>;
101
+ download(args: DownloadArgs): Promise<DownloadResult>;
102
+ screenshot(name: string): Promise<ScreenshotResult>;
103
+ waitFor(args: WaitForArgs): Promise<void>;
104
+ waitForUrl(url: string): Promise<void>;
105
+ evaluate(args: EvaluateArgs): Promise<unknown>;
106
+ frame(args: FrameArgs): Promise<void>;
107
+ clearCookies(): Promise<void>;
108
+ }
@@ -0,0 +1,21 @@
1
+ export { BrowserDriverPort } from "./browser-driver.port.js";
2
+ export type {
3
+ BrowserDriver,
4
+ BrowserHandle,
5
+ ContextOptions,
6
+ DownloadArgs,
7
+ DownloadResult,
8
+ EvaluateArgs,
9
+ FillArgs,
10
+ FrameArgs,
11
+ LaunchOptions,
12
+ PageHandle,
13
+ PressArgs,
14
+ ScreenshotResult,
15
+ UploadArgs,
16
+ Viewport,
17
+ VisitArgs,
18
+ WaitForArgs,
19
+ } from "./browser-driver.types.js";
20
+ export { PreviewProviderPort } from "./preview-provider.port.js";
21
+ export type { PreviewFrame, PreviewProvider, PreviewTarget } from "./preview-provider.types.js";
@@ -0,0 +1,13 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { PreviewProvider } from "./preview-provider.types.js";
3
+
4
+ /**
5
+ * The port a live view of a run pulls frames from. Requires no capability: both
6
+ * implementations here are in-process, so the port loads anywhere.
7
+ */
8
+ export const PreviewProviderPort: Port<PreviewProvider> = {
9
+ id: "venn.port.preview-provider",
10
+ version: 1,
11
+ requires: [],
12
+ methods: ["start", "stop", "latestFrame"],
13
+ };
@@ -0,0 +1,28 @@
1
+ /** Which worker's frame stream a call targets. Workers stream independently. */
2
+ export interface PreviewTarget {
3
+ worker: number;
4
+ }
5
+
6
+ /** One captured frame, encoded in `data` under the stated `mime`. */
7
+ export interface PreviewFrame {
8
+ /** Rises by one per frame, so a consumer can tell a repeat from a fresh capture. */
9
+ seq: number;
10
+ width: number;
11
+ height: number;
12
+ mime: string;
13
+ data: string;
14
+ }
15
+
16
+ /**
17
+ * What feeds a live view of a running browser.
18
+ *
19
+ * Two strategies exist behind it: a screencast, which only CDP engines offer,
20
+ * and polling for everything else.
21
+ */
22
+ export interface PreviewProvider {
23
+ /** Begins capturing for a worker. Capturing costs, so nothing streams unstarted. */
24
+ start(target: PreviewTarget): Promise<void>;
25
+ stop(target: PreviewTarget): Promise<void>;
26
+ /** The most recent frame, or `undefined` when the worker is not streaming. */
27
+ latestFrame(target: PreviewTarget): PreviewFrame | undefined;
28
+ }
@@ -0,0 +1,37 @@
1
+ import type { PreviewFrame, PreviewProvider } from "../port/index.js";
2
+
3
+ function startStream(streaming: Set<number>, worker: number): Promise<void> {
4
+ streaming.add(worker);
5
+ return Promise.resolve();
6
+ }
7
+
8
+ function stopStream(streaming: Set<number>, worker: number): Promise<void> {
9
+ streaming.delete(worker);
10
+ return Promise.resolve();
11
+ }
12
+
13
+ function cannedFrame(worker: number): PreviewFrame {
14
+ return {
15
+ seq: worker + 1,
16
+ width: 1280,
17
+ height: 800,
18
+ mime: "image/jpeg",
19
+ data: "ZmFrZS1mcmFtZQ==",
20
+ };
21
+ }
22
+
23
+ /**
24
+ * The in-memory `PreviewProvider`. Hands back a fixed JPEG for any worker that
25
+ * has been started, and nothing for one that has not.
26
+ *
27
+ * @returns a fresh provider with no worker streaming.
28
+ */
29
+ export function createFakePreviewProvider(): PreviewProvider {
30
+ const streaming = new Set<number>();
31
+ return {
32
+ start: (target) => startStream(streaming, target.worker),
33
+ stop: (target) => stopStream(streaming, target.worker),
34
+ latestFrame: (target) =>
35
+ streaming.has(target.worker) ? cannedFrame(target.worker) : undefined,
36
+ };
37
+ }
@@ -0,0 +1,2 @@
1
+ export { createFakePreviewProvider } from "./fake-preview.js";
2
+ export { createNonePreviewProvider } from "./none-preview.js";
@@ -0,0 +1,15 @@
1
+ import type { PreviewProvider } from "../port/index.js";
2
+
3
+ /**
4
+ * The `PreviewProvider` that captures nothing. The default on CI, where nobody
5
+ * is watching and a frame stream is pure cost.
6
+ *
7
+ * @returns a provider that accepts every call and never yields a frame.
8
+ */
9
+ export function createNonePreviewProvider(): PreviewProvider {
10
+ return {
11
+ start: () => Promise.resolve(),
12
+ stop: () => Promise.resolve(),
13
+ latestFrame: () => undefined,
14
+ };
15
+ }
@@ -0,0 +1,15 @@
1
+ import { defineResource, type ResourceDefinition } from "@venn-lang/sdk";
2
+
3
+ /**
4
+ * A worker-scoped browser: one engine per worker, shared by every flow that
5
+ * worker runs.
6
+ *
7
+ * `open` hands back a placeholder handle and `close` does nothing, because the
8
+ * runtime does not execute `resource` declarations yet.
9
+ */
10
+ export const browserResource: ResourceDefinition = defineResource({
11
+ name: "Browser",
12
+ scope: "worker",
13
+ open: () => ({ id: "browser-1", engine: "chromium" }),
14
+ close: () => undefined,
15
+ });
@@ -0,0 +1,6 @@
1
+ import type { ResourceDefinition } from "@venn-lang/sdk";
2
+ import { browserResource } from "./browser-resource.js";
3
+ import { pageResource } from "./page-resource.js";
4
+
5
+ /** Every resource the `browser` namespace declares, outer scope first. */
6
+ export const browserResources: ResourceDefinition[] = [browserResource, pageResource];
@@ -0,0 +1,15 @@
1
+ import { defineResource, type ResourceDefinition } from "@venn-lang/sdk";
2
+
3
+ /**
4
+ * A flow-scoped page: one isolated context per flow, so two flows never share
5
+ * a session.
6
+ *
7
+ * `open` hands back a placeholder handle and `close` does nothing, because the
8
+ * runtime does not execute `resource` declarations yet.
9
+ */
10
+ export const pageResource: ResourceDefinition = defineResource({
11
+ name: "Page",
12
+ scope: "flow",
13
+ open: () => ({ id: "page-1", url: "about:blank" }),
14
+ close: () => undefined,
15
+ });
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The types `@venn-lang/browser` publishes to the checker, under the `browser`
5
+ * namespace.
6
+ *
7
+ * `Browser` and `Page` are opaque because they are live things, and their
8
+ * insides belong to the driver rather than to a flow. `Download` and
9
+ * `Screenshot` are plain data, mirroring `DownloadResult` and
10
+ * `ScreenshotResult` in `port/browser-driver.types.ts` field by field. No
11
+ * driver method returns an element, so `Element` mirrors `FakeElement` instead.
12
+ */
13
+ export const browserTypeDefs: Readonly<Record<string, TypeSpec>> = {
14
+ /** A launched engine. Held, passed to the `browser` verbs, closed by scope. */
15
+ Browser: t.opaque("browser.Browser", { id: t.string, engine: t.string }),
16
+ /** An isolated context: which one it is, and where it currently points. */
17
+ Page: t.opaque("browser.Page", { id: t.string, url: t.string }),
18
+ /**
19
+ * A resolved element: the subject `visible` and `text` match against. The
20
+ * selector is how an element is found, not something it carries, so what the
21
+ * matchers are handed is `{ visible, text, value }`, all three always set.
22
+ */
23
+ Element: t.record({ visible: t.bool, text: t.string, value: t.string }, { open: true }),
24
+ /** What `browser.download` captured: where it landed, and how big it was. */
25
+ Download: t.record({ path: t.string, bytes: t.number }),
26
+ /** What `browser.screenshot` captured, under the name it was asked for. */
27
+ Screenshot: t.record({ name: t.string, path: t.string }),
28
+ };