@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.
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +641 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/actions/capture.ts +50 -0
- package/src/actions/frame.ts +18 -0
- package/src/actions/index.ts +26 -0
- package/src/actions/input.ts +105 -0
- package/src/actions/navigation.ts +89 -0
- package/src/actions/support.ts +17 -0
- package/src/actions/wait.ts +24 -0
- package/src/drivers/fake-driver.ts +101 -0
- package/src/drivers/fake-driver.types.ts +64 -0
- package/src/drivers/index.ts +9 -0
- package/src/drivers/real-driver.ts +28 -0
- package/src/index.ts +10 -0
- package/src/matchers/index.ts +6 -0
- package/src/matchers/text.ts +22 -0
- package/src/matchers/visible.ts +18 -0
- package/src/plugin.ts +25 -0
- package/src/plugin.types.ts +13 -0
- package/src/port/browser-driver.port.ts +33 -0
- package/src/port/browser-driver.types.ts +108 -0
- package/src/port/index.ts +21 -0
- package/src/port/preview-provider.port.ts +13 -0
- package/src/port/preview-provider.types.ts +28 -0
- package/src/preview/fake-preview.ts +37 -0
- package/src/preview/index.ts +2 -0
- package/src/preview/none-preview.ts +15 -0
- package/src/resources/browser-resource.ts +15 -0
- package/src/resources/index.ts +6 -0
- package/src/resources/page-resource.ts +15 -0
- package/src/types.ts +28 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/drivers/fake-driver.ts","../src/port/browser-driver.port.ts","../src/port/preview-provider.port.ts","../src/drivers/real-driver.ts","../src/actions/support.ts","../src/actions/capture.ts","../src/actions/frame.ts","../src/actions/input.ts","../src/actions/navigation.ts","../src/actions/wait.ts","../src/actions/index.ts","../src/matchers/text.ts","../src/matchers/visible.ts","../src/matchers/index.ts","../src/plugin.types.ts","../src/resources/browser-resource.ts","../src/resources/page-resource.ts","../src/resources/index.ts","../src/types.ts","../src/plugin.ts","../src/preview/fake-preview.ts","../src/preview/none-preview.ts"],"sourcesContent":["import type { FillArgs } from \"../port/index.js\";\nimport type {\n FakeBrowserDriver,\n FakeBrowserState,\n FakeCore,\n FakeDriverOptions,\n FakeElement,\n FakeExtra,\n} from \"./fake-driver.types.js\";\n\n/**\n * Discards an already-evaluated side effect and resolves void. Lets a method\n * body stay a single expression whatever the mutation returned.\n */\nfunction after(_effect: unknown): Promise<void> {\n return Promise.resolve();\n}\n\nfunction navigate(state: FakeBrowserState, url: string): void {\n state.url = url;\n state.history.push(url);\n}\n\nfunction recordFill(state: FakeBrowserState, args: FillArgs): void {\n state.fills.push({ selector: args.selector, value: args.value });\n const element = state.elements.get(args.selector);\n if (element) element.value = args.value;\n}\n\nfunction enterFrame(state: FakeBrowserState, name: string): void {\n state.frame = name;\n}\n\nfunction bumpCookies(state: FakeBrowserState): void {\n state.cookiesCleared += 1;\n}\n\nfunction toElements(input: Record<string, Partial<FakeElement>>): Map<string, FakeElement> {\n const elements = new Map<string, FakeElement>();\n for (const [selector, partial] of Object.entries(input)) {\n elements.set(selector, { visible: true, text: \"\", value: \"\", ...partial });\n }\n return elements;\n}\n\nfunction initState(options: FakeDriverOptions): FakeBrowserState {\n const url = options.url ?? \"about:blank\";\n return {\n url,\n history: options.url ? [url] : [],\n clicks: [],\n fills: [],\n frame: undefined,\n cookiesCleared: 0,\n evalResult: { ok: true },\n elements: toElements(options.elements ?? {}),\n };\n}\n\nfunction coreMethods(state: FakeBrowserState): FakeCore {\n return {\n launch: (opts) => Promise.resolve({ id: \"browser-1\", engine: opts.engine ?? \"chromium\" }),\n newContext: () => Promise.resolve({ id: \"page-1\", url: state.url }),\n visit: (args) => after(navigate(state, args.url)),\n click: (selector) => after(state.clicks.push(selector)),\n fill: (args) => after(recordFill(state, args)),\n select: (args) => after(recordFill(state, args)),\n hover: () => Promise.resolve(),\n press: () => Promise.resolve(),\n };\n}\n\nfunction extraMethods(state: FakeBrowserState): FakeExtra {\n return {\n upload: () => Promise.resolve(),\n download: () => Promise.resolve({ path: \"/artifacts/download.bin\", bytes: 0 }),\n screenshot: (name) => Promise.resolve({ name, path: `/artifacts/${name}.png` }),\n waitFor: () => Promise.resolve(),\n waitForUrl: (url) => after(navigate(state, url)),\n evaluate: () => Promise.resolve(state.evalResult),\n frame: (args) => after(enterFrame(state, args.name)),\n clearCookies: () => after(bumpCookies(state)),\n };\n}\n\n/**\n * The offline `BrowserDriver`. Every method resolves at once against an\n * in-memory page, so a test never waits on a real browser.\n *\n * @param options elements to preload and the URL to start at.\n * @returns a fresh driver whose `state` records what the flow did.\n */\nexport function createFakeBrowserDriver(options: FakeDriverOptions = {}): FakeBrowserDriver {\n const state = initState(options);\n return {\n ...coreMethods(state),\n ...extraMethods(state),\n state,\n element: (selector) => state.elements.get(selector),\n };\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { BrowserDriver } from \"./browser-driver.types.js\";\n\n/**\n * The port every `browser` verb reaches through. Requires the `net` capability,\n * so a host without it fails to load the plugin rather than failing mid-run.\n *\n * `methods` must list every method a verb calls. An omission is not checked at\n * load time, so it surfaces as a TypeError mid-run instead of a legible VN2011.\n */\nexport const BrowserDriverPort: Port<BrowserDriver> = {\n id: \"venn.port.browser-driver\",\n version: 1,\n requires: [\"net\"],\n methods: [\n \"launch\",\n \"newContext\",\n \"visit\",\n \"click\",\n \"fill\",\n \"select\",\n \"hover\",\n \"press\",\n \"upload\",\n \"download\",\n \"screenshot\",\n \"waitFor\",\n \"waitForUrl\",\n \"evaluate\",\n \"frame\",\n \"clearCookies\",\n ],\n};\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { PreviewProvider } from \"./preview-provider.types.js\";\n\n/**\n * The port a live view of a run pulls frames from. Requires no capability: both\n * implementations here are in-process, so the port loads anywhere.\n */\nexport const PreviewProviderPort: Port<PreviewProvider> = {\n id: \"venn.port.preview-provider\",\n version: 1,\n requires: [],\n methods: [\"start\", \"stop\", \"latestFrame\"],\n};\n","import { VennError } from \"@venn-lang/contracts\";\nimport { type BrowserDriver, BrowserDriverPort } from \"../port/index.js\";\n\nfunction notImplemented(): VennError {\n return new VennError({\n code: \"VN8090\",\n message: \"browser real driver not implemented in this build\",\n });\n}\n\n/**\n * The engine-driving `BrowserDriver`. Not wired up in this repo, which ships\n * the language rather than an automation backend.\n *\n * Methods are generated from the port descriptor, so the stub cannot fall\n * behind the interface.\n *\n * @returns a driver whose every method throws, so the gap is a legible failure\n * rather than a test that passes against nothing.\n * @throws {VennError} `VN8090` on any call.\n */\nexport function createRealBrowserDriver(): BrowserDriver {\n const fail = (): never => {\n throw notImplemented();\n };\n const entries = BrowserDriverPort.methods.map((method) => [method, fail] as const);\n return Object.fromEntries(entries) as unknown as BrowserDriver;\n}\n","import type { ActionContext, ActionInput } from \"@venn-lang/sdk\";\nimport { type BrowserDriver, BrowserDriverPort } from \"../port/index.js\";\n\n/** The browser driver bound to this run. */\nexport function browserDriver(ctx: ActionContext): BrowserDriver {\n return ctx.port(BrowserDriverPort);\n}\n\n/** The first positional argument as a string. A missing one reads as `\"\"`. */\nexport function arg0(input: ActionInput<unknown>): string {\n return String(input.args[0] ?? \"\");\n}\n\n/** The second positional argument as a string. A missing one reads as `\"\"`. */\nexport function arg1(input: ActionInput<unknown>): string {\n return String(input.args[1] ?? \"\");\n}\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { arg0, browserDriver } from \"./support.js\";\n\n/**\n * `browser.screenshot \"checkout-empty\"`.\n *\n * Captures the page as it stands and files it under the given name.\n *\n * @returns a `browser.Screenshot` saying where the image landed.\n */\nexport const screenshot: ActionDefinition = defineAction({\n name: \"screenshot\",\n doc: \"Capture a screenshot by name.\",\n args: [arg(\"name\", t.string, \"What to file it under.\")],\n result: t.ref(\"browser.Screenshot\"),\n run: (ctx, input) => browserDriver(ctx).screenshot(arg0(input)),\n});\n\n/**\n * `browser.download \"#invoice-pdf\"`.\n *\n * Clicks something that starts a download and waits for the file to land.\n *\n * @returns a `browser.Download` saying where the file landed and how big it is.\n */\nexport const download: ActionDefinition = defineAction({\n name: \"download\",\n doc: \"Trigger and capture a download from a selector.\",\n args: [arg(\"selector\", t.string, \"What to act on: a CSS selector, or visible text.\")],\n result: t.ref(\"browser.Download\"),\n run: (ctx, input) => browserDriver(ctx).download({ selector: arg0(input) }),\n});\n\n/**\n * `browser.evaluate \"document.title\"`.\n *\n * Runs JavaScript inside the page and hands back what it evaluated to. The\n * escape hatch for whatever the other verbs cannot reach.\n *\n * The result is `dynamic`: narrowing it would be a guess about someone else's\n * script.\n */\nexport const evaluate: ActionDefinition = defineAction({\n name: \"evaluate\",\n doc: \"Evaluate a script in the page and return its value.\",\n args: [arg(\"script\", t.string, \"JavaScript to run in the page.\")],\n result: t.dynamic,\n run: (ctx, input) => browserDriver(ctx).evaluate({ script: arg0(input) }),\n});\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { arg0, browserDriver } from \"./support.js\";\n\n/**\n * `browser.frame \"payment-iframe\"`.\n *\n * Points the following steps at an iframe, by its name or a selector for it.\n * Selectors do not cross a frame boundary, so anything inside one needs this\n * first.\n */\nexport const frame: ActionDefinition = defineAction({\n name: \"frame\",\n doc: \"Enter an iframe by name or selector for nested steps.\",\n args: [arg(\"name\", t.string, \"The iframe's name, or a selector for it.\")],\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).frame({ name: arg0(input) }),\n});\n","import { type ActionDefinition, type ActionInput, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport type { PressArgs } from \"../port/index.js\";\nimport { arg0, arg1, browserDriver } from \"./support.js\";\n\nconst uploadParams = z.object({ file: z.string() });\n\nfunction pressArgs(input: ActionInput<unknown>): PressArgs {\n return input.args.length > 1 ? { selector: arg0(input), key: arg1(input) } : { key: arg0(input) };\n}\n\n/**\n * `browser.click \"#submit\"`.\n *\n * Clicks the matching element, waiting for it to be ready first.\n */\nexport const click: ActionDefinition = defineAction({\n name: \"click\",\n doc: \"Click the element matching a selector.\",\n args: [arg(\"selector\", t.string, \"What to act on: a CSS selector, or visible text.\")],\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).click(arg0(input)),\n});\n\n/**\n * `browser.fill \"#email\" \"ada@example.test\"`.\n *\n * Replaces whatever an input holds with the given value.\n */\nexport const fill: ActionDefinition = defineAction({\n name: \"fill\",\n doc: \"Fill an input with a value.\",\n args: [\n arg(\"selector\", t.string, \"What to act on: a CSS selector, or visible text.\"),\n arg(\"value\", t.string, \"What to type into it.\"),\n ],\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).fill({ selector: arg0(input), value: arg1(input) }),\n});\n\n/**\n * `browser.select \"#country\" \"BR\"`.\n *\n * Picks an option in a dropdown. The second argument is the option's value\n * attribute, not the label the user sees.\n */\nexport const select: ActionDefinition = defineAction({\n name: \"select\",\n doc: \"Select an option by value.\",\n args: [\n arg(\"selector\", t.string, \"What to act on: a CSS selector, or visible text.\"),\n arg(\"value\", t.string, \"The option's value, not its label.\"),\n ],\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).select({ selector: arg0(input), value: arg1(input) }),\n});\n\n/**\n * `browser.hover \".menu-trigger\"`.\n *\n * Moves the pointer over an element, which is how a hover menu or tooltip is\n * made to appear.\n */\nexport const hover: ActionDefinition = defineAction({\n name: \"hover\",\n doc: \"Hover over an element.\",\n args: [arg(\"selector\", t.string, \"What to act on: a CSS selector, or visible text.\")],\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).hover(arg0(input)),\n});\n\n/**\n * `browser.press \"Enter\"`, or `browser.press \"#search\" \"Enter\"`.\n *\n * Sends a keystroke. Called with one argument, the page has focus and the\n * argument is the key. Called with two, the first is a selector to focus and\n * the second is the key.\n *\n * Both shapes are strings, so a single signature of two strings covers them.\n */\nexport const press: ActionDefinition = defineAction({\n name: \"press\",\n doc: \"Press a key, optionally focused on a selector.\",\n args: [\n arg(\"key\", t.string, \"The key to press: `Enter`, `Escape`, `Control+A`.\"),\n arg(\"selector\", t.string, \"What to focus first. Omit it and the page has focus.\"),\n ],\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).press(pressArgs(input)),\n});\n\n/**\n * `browser.upload \"#avatar\" { file: \"./fixtures/face.png\" }`.\n *\n * Hands a file to a file input, as picking one in the OS dialog would.\n */\nexport const upload: ActionDefinition = defineAction({\n name: \"upload\",\n doc: \"Upload a file to a file input.\",\n params: uploadParams,\n args: [arg(\"selector\", t.string, \"What to act on: a CSS selector, or visible text.\")],\n result: t.void,\n run: (ctx, input) =>\n browserDriver(ctx).upload({ selector: arg0(input), file: input.params.file }),\n});\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { arg0, browserDriver } from \"./support.js\";\n\nconst launchParams = z.object({\n engine: z.string().optional(),\n headless: z.boolean().optional(),\n});\n\nconst contextParams = z.object({\n locale: z.string().optional(),\n viewport: z.object({ width: z.number(), height: z.number() }).optional(),\n});\n\nconst visitParams = z.object({ headers: z.record(z.string(), z.string()).optional() });\n\n/**\n * `browser.launch { engine: \"chromium\", headless: true }`.\n *\n * Starts a browser engine. Everything else in the namespace needs one running.\n *\n * @returns a `browser.Browser` handle.\n */\nexport const launch: ActionDefinition = defineAction({\n name: \"launch\",\n doc: \"Launch a browser engine.\",\n params: launchParams.optional(),\n result: t.ref(\"browser.Browser\"),\n run: (ctx, input) => browserDriver(ctx).launch(input.params ?? {}),\n});\n\n/**\n * `browser.visit \"/checkout\" { headers: { \"X-Debug\": \"1\" } }`.\n *\n * Navigates the page and waits for it to load. A relative path joins the base\n * URL; an absolute one is used as given.\n */\nexport const visit: ActionDefinition = defineAction({\n name: \"visit\",\n doc: \"Navigate the page to a URL.\",\n params: visitParams.optional(),\n args: [arg(\"url\", t.string, \"Where to go. Relative paths join the base URL.\")],\n result: t.void,\n run: (ctx, input) =>\n browserDriver(ctx).visit({ url: arg0(input), headers: input.params?.headers }),\n});\n\n/**\n * `browser.waitForUrl \"/orders/*\"`.\n *\n * Blocks until the page lands on a matching URL. Use it after an action that\n * navigates on its own, such as a form submit or a redirect.\n */\nexport const waitForUrl: ActionDefinition = defineAction({\n name: \"waitForUrl\",\n doc: \"Wait until the page URL matches.\",\n args: [arg(\"url\", t.string, \"The URL to wait for. A pattern is allowed.\")],\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).waitForUrl(arg0(input)),\n});\n\n/**\n * `browser.newContext { locale: \"pt-BR\", viewport: { width: 1280, height: 800 } }`.\n *\n * Opens an isolated context: its own cookies, storage and viewport. Two\n * contexts in one browser cannot see each other's session.\n *\n * @returns a `browser.Page` handle.\n */\nexport const newContext: ActionDefinition = defineAction({\n name: \"newContext\",\n doc: \"Open an isolated browser context (a Page).\",\n params: contextParams.optional(),\n result: t.ref(\"browser.Page\"),\n run: (ctx, input) => browserDriver(ctx).newContext(input.params ?? {}),\n});\n\n/**\n * `browser.clearCookies`.\n *\n * Drops every cookie in the current context, logging the session out. Storage\n * is left alone.\n */\nexport const clearCookies: ActionDefinition = defineAction({\n name: \"clearCookies\",\n doc: \"Clear all cookies in the current context.\",\n result: t.void,\n run: (ctx) => browserDriver(ctx).clearCookies(),\n});\n","import { type ActionDefinition, Duration, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { browserDriver } from \"./support.js\";\n\nconst waitForParams = z.object({\n text: z.string().optional(),\n selector: z.string().optional(),\n timeout: Duration.optional(),\n});\n\n/**\n * `browser.waitFor { selector: \"#cart\", timeout: \"10s\" }`.\n *\n * Blocks until the page shows the given `text` or matches the given\n * `selector`. Use it in place of a sleep, so a slow page waits and a fast one\n * does not. `timeout` bounds the wait.\n */\nexport const waitFor: ActionDefinition = defineAction({\n name: \"waitFor\",\n doc: \"Wait for a condition (text or selector) on the page.\",\n params: waitForParams.optional(),\n result: t.void,\n run: (ctx, input) => browserDriver(ctx).waitFor(input.params ?? {}),\n});\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { download, evaluate, screenshot } from \"./capture.js\";\nimport { frame } from \"./frame.js\";\nimport { click, fill, hover, press, select, upload } from \"./input.js\";\nimport { clearCookies, launch, newContext, visit, waitForUrl } from \"./navigation.js\";\nimport { waitFor } from \"./wait.js\";\n\n/** Every verb in the `browser` namespace, in the order the plugin registers them. */\nexport const browserActions: ActionDefinition[] = [\n launch,\n visit,\n click,\n fill,\n select,\n hover,\n press,\n upload,\n download,\n screenshot,\n waitFor,\n waitForUrl,\n evaluate,\n frame,\n newContext,\n clearCookies,\n];\n","import { arg, defineMatcher, type MatcherDefinition } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\n\n/**\n * `expect element text \"Order confirmed\"`.\n *\n * Passes when the element's text equals or contains the expected string.\n * Containment is deliberate: assertions about copy should survive a stray\n * space or a wrapping node.\n */\nexport const text: MatcherDefinition = defineMatcher({\n name: \"text\",\n args: [arg(\"value\", t.string, \"The text the element should carry.\")],\n appliesTo: \"Element\",\n test: ({ subject, args }) => matchesText(subject, String(args[0])),\n message: ({ args }) => `expected the element text to contain \"${String(args[0])}\"`,\n});\n\nfunction matchesText(subject: unknown, expected: string): boolean {\n const actual = (subject as { text?: string } | null)?.text ?? \"\";\n return actual === expected || actual.includes(expected);\n}\n","import { defineMatcher, type MatcherDefinition } from \"@venn-lang/sdk\";\n\n/**\n * `expect element visible`.\n *\n * Passes when the element is on screen. An element that is absent, or whose\n * subject is not an element at all, fails rather than erroring.\n */\nexport const visible: MatcherDefinition = defineMatcher({\n name: \"visible\",\n appliesTo: \"Element\",\n test: ({ subject }) => isVisible(subject),\n message: () => \"expected the element to be visible\",\n});\n\nfunction isVisible(subject: unknown): boolean {\n return (subject as { visible?: boolean } | null)?.visible === true;\n}\n","import type { MatcherDefinition } from \"@venn-lang/sdk\";\nimport { text } from \"./text.js\";\nimport { visible } from \"./visible.js\";\n\n/** Every matcher the `browser` namespace contributes to `expect`. */\nexport const browserMatchers: MatcherDefinition[] = [visible, text];\n","import { type ZodType, z } from \"@venn-lang/sdk\";\n\n/** The handle `browser.launch` yields. */\nconst Browser = z.object({ id: z.string(), engine: z.string() });\n\n/** The handle `browser.newContext` yields; what `page.` completes against. */\nconst Page = z.object({ id: z.string(), url: z.string() });\n\n/** A resolved DOM element, as `visible` and `text` are handed it. */\nconst Element = z.object({ visible: z.boolean(), text: z.string(), value: z.string() });\n\n/** Runtime validators for the nominal types the `browser` namespace registers. */\nexport const browserTypes: Record<string, ZodType> = { Browser, Page, Element };\n","import { defineResource, type ResourceDefinition } from \"@venn-lang/sdk\";\n\n/**\n * A worker-scoped browser: one engine per worker, shared by every flow that\n * worker runs.\n *\n * `open` hands back a placeholder handle and `close` does nothing, because the\n * runtime does not execute `resource` declarations yet.\n */\nexport const browserResource: ResourceDefinition = defineResource({\n name: \"Browser\",\n scope: \"worker\",\n open: () => ({ id: \"browser-1\", engine: \"chromium\" }),\n close: () => undefined,\n});\n","import { defineResource, type ResourceDefinition } from \"@venn-lang/sdk\";\n\n/**\n * A flow-scoped page: one isolated context per flow, so two flows never share\n * a session.\n *\n * `open` hands back a placeholder handle and `close` does nothing, because the\n * runtime does not execute `resource` declarations yet.\n */\nexport const pageResource: ResourceDefinition = defineResource({\n name: \"Page\",\n scope: \"flow\",\n open: () => ({ id: \"page-1\", url: \"about:blank\" }),\n close: () => undefined,\n});\n","import type { ResourceDefinition } from \"@venn-lang/sdk\";\nimport { browserResource } from \"./browser-resource.js\";\nimport { pageResource } from \"./page-resource.js\";\n\n/** Every resource the `browser` namespace declares, outer scope first. */\nexport const browserResources: ResourceDefinition[] = [browserResource, pageResource];\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types `@venn-lang/browser` publishes to the checker, under the `browser`\n * namespace.\n *\n * `Browser` and `Page` are opaque because they are live things, and their\n * insides belong to the driver rather than to a flow. `Download` and\n * `Screenshot` are plain data, mirroring `DownloadResult` and\n * `ScreenshotResult` in `port/browser-driver.types.ts` field by field. No\n * driver method returns an element, so `Element` mirrors `FakeElement` instead.\n */\nexport const browserTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /** A launched engine. Held, passed to the `browser` verbs, closed by scope. */\n Browser: t.opaque(\"browser.Browser\", { id: t.string, engine: t.string }),\n /** An isolated context: which one it is, and where it currently points. */\n Page: t.opaque(\"browser.Page\", { id: t.string, url: t.string }),\n /**\n * A resolved element: the subject `visible` and `text` match against. The\n * selector is how an element is found, not something it carries, so what the\n * matchers are handed is `{ visible, text, value }`, all three always set.\n */\n Element: t.record({ visible: t.bool, text: t.string, value: t.string }, { open: true }),\n /** What `browser.download` captured: where it landed, and how big it was. */\n Download: t.record({ path: t.string, bytes: t.number }),\n /** What `browser.screenshot` captured, under the name it was asked for. */\n Screenshot: t.record({ name: t.string, path: t.string }),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { browserActions } from \"./actions/index.js\";\nimport { browserMatchers } from \"./matchers/index.js\";\nimport { browserTypes } from \"./plugin.types.js\";\nimport { browserResources } from \"./resources/index.js\";\nimport { browserTypeDefs } from \"./types.js\";\n\n/**\n * The `@venn-lang/browser` plugin. Registers the `browser` namespace: sixteen verbs,\n * the `visible` and `text` matchers, the `Browser` and `Page` resources, and\n * the nominal types those hand around. Requires the `net` capability.\n */\nexport const browserPlugin: PluginDefinition = definePlugin({\n name: \"venn/browser\",\n version: \"0.0.0\",\n namespace: \"browser\",\n requires: [\"net\"],\n actions: browserActions,\n matchers: browserMatchers,\n resources: browserResources,\n types: browserTypes,\n typeDefs: browserTypeDefs,\n});\n\nexport { browserPlugin as default };\n","import type { PreviewFrame, PreviewProvider } from \"../port/index.js\";\n\nfunction startStream(streaming: Set<number>, worker: number): Promise<void> {\n streaming.add(worker);\n return Promise.resolve();\n}\n\nfunction stopStream(streaming: Set<number>, worker: number): Promise<void> {\n streaming.delete(worker);\n return Promise.resolve();\n}\n\nfunction cannedFrame(worker: number): PreviewFrame {\n return {\n seq: worker + 1,\n width: 1280,\n height: 800,\n mime: \"image/jpeg\",\n data: \"ZmFrZS1mcmFtZQ==\",\n };\n}\n\n/**\n * The in-memory `PreviewProvider`. Hands back a fixed JPEG for any worker that\n * has been started, and nothing for one that has not.\n *\n * @returns a fresh provider with no worker streaming.\n */\nexport function createFakePreviewProvider(): PreviewProvider {\n const streaming = new Set<number>();\n return {\n start: (target) => startStream(streaming, target.worker),\n stop: (target) => stopStream(streaming, target.worker),\n latestFrame: (target) =>\n streaming.has(target.worker) ? cannedFrame(target.worker) : undefined,\n };\n}\n","import type { PreviewProvider } from \"../port/index.js\";\n\n/**\n * The `PreviewProvider` that captures nothing. The default on CI, where nobody\n * is watching and a frame stream is pure cost.\n *\n * @returns a provider that accepts every call and never yields a frame.\n */\nexport function createNonePreviewProvider(): PreviewProvider {\n return {\n start: () => Promise.resolve(),\n stop: () => Promise.resolve(),\n latestFrame: () => undefined,\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@venn-lang/browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The browser namespace: sixteen verbs that drive a page, plus two element matchers and the ports the driving actually goes through.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"venn",
|
|
7
|
+
"testing",
|
|
8
|
+
"e2e",
|
|
9
|
+
"browser"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-browser#readme",
|
|
12
|
+
"bugs": "https://github.com/venn-lang/venn/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/venn-lang/venn.git",
|
|
16
|
+
"directory": "packages/std-browser"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Vinicius Borges",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"development": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"!src/**/*.test.ts",
|
|
34
|
+
"!src/**/*.suite.ts"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@venn-lang/contracts": "0.1.0",
|
|
41
|
+
"@venn-lang/sdk": "0.1.0",
|
|
42
|
+
"@venn-lang/types": "0.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsdown": "^0.22.14",
|
|
46
|
+
"typescript": "^7.0.2",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { arg0, browserDriver } from "./support.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `browser.screenshot "checkout-empty"`.
|
|
7
|
+
*
|
|
8
|
+
* Captures the page as it stands and files it under the given name.
|
|
9
|
+
*
|
|
10
|
+
* @returns a `browser.Screenshot` saying where the image landed.
|
|
11
|
+
*/
|
|
12
|
+
export const screenshot: ActionDefinition = defineAction({
|
|
13
|
+
name: "screenshot",
|
|
14
|
+
doc: "Capture a screenshot by name.",
|
|
15
|
+
args: [arg("name", t.string, "What to file it under.")],
|
|
16
|
+
result: t.ref("browser.Screenshot"),
|
|
17
|
+
run: (ctx, input) => browserDriver(ctx).screenshot(arg0(input)),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `browser.download "#invoice-pdf"`.
|
|
22
|
+
*
|
|
23
|
+
* Clicks something that starts a download and waits for the file to land.
|
|
24
|
+
*
|
|
25
|
+
* @returns a `browser.Download` saying where the file landed and how big it is.
|
|
26
|
+
*/
|
|
27
|
+
export const download: ActionDefinition = defineAction({
|
|
28
|
+
name: "download",
|
|
29
|
+
doc: "Trigger and capture a download from a selector.",
|
|
30
|
+
args: [arg("selector", t.string, "What to act on: a CSS selector, or visible text.")],
|
|
31
|
+
result: t.ref("browser.Download"),
|
|
32
|
+
run: (ctx, input) => browserDriver(ctx).download({ selector: arg0(input) }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `browser.evaluate "document.title"`.
|
|
37
|
+
*
|
|
38
|
+
* Runs JavaScript inside the page and hands back what it evaluated to. The
|
|
39
|
+
* escape hatch for whatever the other verbs cannot reach.
|
|
40
|
+
*
|
|
41
|
+
* The result is `dynamic`: narrowing it would be a guess about someone else's
|
|
42
|
+
* script.
|
|
43
|
+
*/
|
|
44
|
+
export const evaluate: ActionDefinition = defineAction({
|
|
45
|
+
name: "evaluate",
|
|
46
|
+
doc: "Evaluate a script in the page and return its value.",
|
|
47
|
+
args: [arg("script", t.string, "JavaScript to run in the page.")],
|
|
48
|
+
result: t.dynamic,
|
|
49
|
+
run: (ctx, input) => browserDriver(ctx).evaluate({ script: arg0(input) }),
|
|
50
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { arg0, browserDriver } from "./support.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `browser.frame "payment-iframe"`.
|
|
7
|
+
*
|
|
8
|
+
* Points the following steps at an iframe, by its name or a selector for it.
|
|
9
|
+
* Selectors do not cross a frame boundary, so anything inside one needs this
|
|
10
|
+
* first.
|
|
11
|
+
*/
|
|
12
|
+
export const frame: ActionDefinition = defineAction({
|
|
13
|
+
name: "frame",
|
|
14
|
+
doc: "Enter an iframe by name or selector for nested steps.",
|
|
15
|
+
args: [arg("name", t.string, "The iframe's name, or a selector for it.")],
|
|
16
|
+
result: t.void,
|
|
17
|
+
run: (ctx, input) => browserDriver(ctx).frame({ name: arg0(input) }),
|
|
18
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ActionDefinition } from "@venn-lang/sdk";
|
|
2
|
+
import { download, evaluate, screenshot } from "./capture.js";
|
|
3
|
+
import { frame } from "./frame.js";
|
|
4
|
+
import { click, fill, hover, press, select, upload } from "./input.js";
|
|
5
|
+
import { clearCookies, launch, newContext, visit, waitForUrl } from "./navigation.js";
|
|
6
|
+
import { waitFor } from "./wait.js";
|
|
7
|
+
|
|
8
|
+
/** Every verb in the `browser` namespace, in the order the plugin registers them. */
|
|
9
|
+
export const browserActions: ActionDefinition[] = [
|
|
10
|
+
launch,
|
|
11
|
+
visit,
|
|
12
|
+
click,
|
|
13
|
+
fill,
|
|
14
|
+
select,
|
|
15
|
+
hover,
|
|
16
|
+
press,
|
|
17
|
+
upload,
|
|
18
|
+
download,
|
|
19
|
+
screenshot,
|
|
20
|
+
waitFor,
|
|
21
|
+
waitForUrl,
|
|
22
|
+
evaluate,
|
|
23
|
+
frame,
|
|
24
|
+
newContext,
|
|
25
|
+
clearCookies,
|
|
26
|
+
];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type ActionDefinition, type ActionInput, arg, defineAction, z } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import type { PressArgs } from "../port/index.js";
|
|
4
|
+
import { arg0, arg1, browserDriver } from "./support.js";
|
|
5
|
+
|
|
6
|
+
const uploadParams = z.object({ file: z.string() });
|
|
7
|
+
|
|
8
|
+
function pressArgs(input: ActionInput<unknown>): PressArgs {
|
|
9
|
+
return input.args.length > 1 ? { selector: arg0(input), key: arg1(input) } : { key: arg0(input) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `browser.click "#submit"`.
|
|
14
|
+
*
|
|
15
|
+
* Clicks the matching element, waiting for it to be ready first.
|
|
16
|
+
*/
|
|
17
|
+
export const click: ActionDefinition = defineAction({
|
|
18
|
+
name: "click",
|
|
19
|
+
doc: "Click the element matching a selector.",
|
|
20
|
+
args: [arg("selector", t.string, "What to act on: a CSS selector, or visible text.")],
|
|
21
|
+
result: t.void,
|
|
22
|
+
run: (ctx, input) => browserDriver(ctx).click(arg0(input)),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `browser.fill "#email" "ada@example.test"`.
|
|
27
|
+
*
|
|
28
|
+
* Replaces whatever an input holds with the given value.
|
|
29
|
+
*/
|
|
30
|
+
export const fill: ActionDefinition = defineAction({
|
|
31
|
+
name: "fill",
|
|
32
|
+
doc: "Fill an input with a value.",
|
|
33
|
+
args: [
|
|
34
|
+
arg("selector", t.string, "What to act on: a CSS selector, or visible text."),
|
|
35
|
+
arg("value", t.string, "What to type into it."),
|
|
36
|
+
],
|
|
37
|
+
result: t.void,
|
|
38
|
+
run: (ctx, input) => browserDriver(ctx).fill({ selector: arg0(input), value: arg1(input) }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `browser.select "#country" "BR"`.
|
|
43
|
+
*
|
|
44
|
+
* Picks an option in a dropdown. The second argument is the option's value
|
|
45
|
+
* attribute, not the label the user sees.
|
|
46
|
+
*/
|
|
47
|
+
export const select: ActionDefinition = defineAction({
|
|
48
|
+
name: "select",
|
|
49
|
+
doc: "Select an option by value.",
|
|
50
|
+
args: [
|
|
51
|
+
arg("selector", t.string, "What to act on: a CSS selector, or visible text."),
|
|
52
|
+
arg("value", t.string, "The option's value, not its label."),
|
|
53
|
+
],
|
|
54
|
+
result: t.void,
|
|
55
|
+
run: (ctx, input) => browserDriver(ctx).select({ selector: arg0(input), value: arg1(input) }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `browser.hover ".menu-trigger"`.
|
|
60
|
+
*
|
|
61
|
+
* Moves the pointer over an element, which is how a hover menu or tooltip is
|
|
62
|
+
* made to appear.
|
|
63
|
+
*/
|
|
64
|
+
export const hover: ActionDefinition = defineAction({
|
|
65
|
+
name: "hover",
|
|
66
|
+
doc: "Hover over an element.",
|
|
67
|
+
args: [arg("selector", t.string, "What to act on: a CSS selector, or visible text.")],
|
|
68
|
+
result: t.void,
|
|
69
|
+
run: (ctx, input) => browserDriver(ctx).hover(arg0(input)),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `browser.press "Enter"`, or `browser.press "#search" "Enter"`.
|
|
74
|
+
*
|
|
75
|
+
* Sends a keystroke. Called with one argument, the page has focus and the
|
|
76
|
+
* argument is the key. Called with two, the first is a selector to focus and
|
|
77
|
+
* the second is the key.
|
|
78
|
+
*
|
|
79
|
+
* Both shapes are strings, so a single signature of two strings covers them.
|
|
80
|
+
*/
|
|
81
|
+
export const press: ActionDefinition = defineAction({
|
|
82
|
+
name: "press",
|
|
83
|
+
doc: "Press a key, optionally focused on a selector.",
|
|
84
|
+
args: [
|
|
85
|
+
arg("key", t.string, "The key to press: `Enter`, `Escape`, `Control+A`."),
|
|
86
|
+
arg("selector", t.string, "What to focus first. Omit it and the page has focus."),
|
|
87
|
+
],
|
|
88
|
+
result: t.void,
|
|
89
|
+
run: (ctx, input) => browserDriver(ctx).press(pressArgs(input)),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `browser.upload "#avatar" { file: "./fixtures/face.png" }`.
|
|
94
|
+
*
|
|
95
|
+
* Hands a file to a file input, as picking one in the OS dialog would.
|
|
96
|
+
*/
|
|
97
|
+
export const upload: ActionDefinition = defineAction({
|
|
98
|
+
name: "upload",
|
|
99
|
+
doc: "Upload a file to a file input.",
|
|
100
|
+
params: uploadParams,
|
|
101
|
+
args: [arg("selector", t.string, "What to act on: a CSS selector, or visible text.")],
|
|
102
|
+
result: t.void,
|
|
103
|
+
run: (ctx, input) =>
|
|
104
|
+
browserDriver(ctx).upload({ selector: arg0(input), file: input.params.file }),
|
|
105
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { arg0, browserDriver } from "./support.js";
|
|
4
|
+
|
|
5
|
+
const launchParams = z.object({
|
|
6
|
+
engine: z.string().optional(),
|
|
7
|
+
headless: z.boolean().optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const contextParams = z.object({
|
|
11
|
+
locale: z.string().optional(),
|
|
12
|
+
viewport: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const visitParams = z.object({ headers: z.record(z.string(), z.string()).optional() });
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `browser.launch { engine: "chromium", headless: true }`.
|
|
19
|
+
*
|
|
20
|
+
* Starts a browser engine. Everything else in the namespace needs one running.
|
|
21
|
+
*
|
|
22
|
+
* @returns a `browser.Browser` handle.
|
|
23
|
+
*/
|
|
24
|
+
export const launch: ActionDefinition = defineAction({
|
|
25
|
+
name: "launch",
|
|
26
|
+
doc: "Launch a browser engine.",
|
|
27
|
+
params: launchParams.optional(),
|
|
28
|
+
result: t.ref("browser.Browser"),
|
|
29
|
+
run: (ctx, input) => browserDriver(ctx).launch(input.params ?? {}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `browser.visit "/checkout" { headers: { "X-Debug": "1" } }`.
|
|
34
|
+
*
|
|
35
|
+
* Navigates the page and waits for it to load. A relative path joins the base
|
|
36
|
+
* URL; an absolute one is used as given.
|
|
37
|
+
*/
|
|
38
|
+
export const visit: ActionDefinition = defineAction({
|
|
39
|
+
name: "visit",
|
|
40
|
+
doc: "Navigate the page to a URL.",
|
|
41
|
+
params: visitParams.optional(),
|
|
42
|
+
args: [arg("url", t.string, "Where to go. Relative paths join the base URL.")],
|
|
43
|
+
result: t.void,
|
|
44
|
+
run: (ctx, input) =>
|
|
45
|
+
browserDriver(ctx).visit({ url: arg0(input), headers: input.params?.headers }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `browser.waitForUrl "/orders/*"`.
|
|
50
|
+
*
|
|
51
|
+
* Blocks until the page lands on a matching URL. Use it after an action that
|
|
52
|
+
* navigates on its own, such as a form submit or a redirect.
|
|
53
|
+
*/
|
|
54
|
+
export const waitForUrl: ActionDefinition = defineAction({
|
|
55
|
+
name: "waitForUrl",
|
|
56
|
+
doc: "Wait until the page URL matches.",
|
|
57
|
+
args: [arg("url", t.string, "The URL to wait for. A pattern is allowed.")],
|
|
58
|
+
result: t.void,
|
|
59
|
+
run: (ctx, input) => browserDriver(ctx).waitForUrl(arg0(input)),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `browser.newContext { locale: "pt-BR", viewport: { width: 1280, height: 800 } }`.
|
|
64
|
+
*
|
|
65
|
+
* Opens an isolated context: its own cookies, storage and viewport. Two
|
|
66
|
+
* contexts in one browser cannot see each other's session.
|
|
67
|
+
*
|
|
68
|
+
* @returns a `browser.Page` handle.
|
|
69
|
+
*/
|
|
70
|
+
export const newContext: ActionDefinition = defineAction({
|
|
71
|
+
name: "newContext",
|
|
72
|
+
doc: "Open an isolated browser context (a Page).",
|
|
73
|
+
params: contextParams.optional(),
|
|
74
|
+
result: t.ref("browser.Page"),
|
|
75
|
+
run: (ctx, input) => browserDriver(ctx).newContext(input.params ?? {}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `browser.clearCookies`.
|
|
80
|
+
*
|
|
81
|
+
* Drops every cookie in the current context, logging the session out. Storage
|
|
82
|
+
* is left alone.
|
|
83
|
+
*/
|
|
84
|
+
export const clearCookies: ActionDefinition = defineAction({
|
|
85
|
+
name: "clearCookies",
|
|
86
|
+
doc: "Clear all cookies in the current context.",
|
|
87
|
+
result: t.void,
|
|
88
|
+
run: (ctx) => browserDriver(ctx).clearCookies(),
|
|
89
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ActionContext, ActionInput } from "@venn-lang/sdk";
|
|
2
|
+
import { type BrowserDriver, BrowserDriverPort } from "../port/index.js";
|
|
3
|
+
|
|
4
|
+
/** The browser driver bound to this run. */
|
|
5
|
+
export function browserDriver(ctx: ActionContext): BrowserDriver {
|
|
6
|
+
return ctx.port(BrowserDriverPort);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The first positional argument as a string. A missing one reads as `""`. */
|
|
10
|
+
export function arg0(input: ActionInput<unknown>): string {
|
|
11
|
+
return String(input.args[0] ?? "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The second positional argument as a string. A missing one reads as `""`. */
|
|
15
|
+
export function arg1(input: ActionInput<unknown>): string {
|
|
16
|
+
return String(input.args[1] ?? "");
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ActionDefinition, Duration, defineAction, z } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { browserDriver } from "./support.js";
|
|
4
|
+
|
|
5
|
+
const waitForParams = z.object({
|
|
6
|
+
text: z.string().optional(),
|
|
7
|
+
selector: z.string().optional(),
|
|
8
|
+
timeout: Duration.optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `browser.waitFor { selector: "#cart", timeout: "10s" }`.
|
|
13
|
+
*
|
|
14
|
+
* Blocks until the page shows the given `text` or matches the given
|
|
15
|
+
* `selector`. Use it in place of a sleep, so a slow page waits and a fast one
|
|
16
|
+
* does not. `timeout` bounds the wait.
|
|
17
|
+
*/
|
|
18
|
+
export const waitFor: ActionDefinition = defineAction({
|
|
19
|
+
name: "waitFor",
|
|
20
|
+
doc: "Wait for a condition (text or selector) on the page.",
|
|
21
|
+
params: waitForParams.optional(),
|
|
22
|
+
result: t.void,
|
|
23
|
+
run: (ctx, input) => browserDriver(ctx).waitFor(input.params ?? {}),
|
|
24
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { FillArgs } from "../port/index.js";
|
|
2
|
+
import type {
|
|
3
|
+
FakeBrowserDriver,
|
|
4
|
+
FakeBrowserState,
|
|
5
|
+
FakeCore,
|
|
6
|
+
FakeDriverOptions,
|
|
7
|
+
FakeElement,
|
|
8
|
+
FakeExtra,
|
|
9
|
+
} from "./fake-driver.types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Discards an already-evaluated side effect and resolves void. Lets a method
|
|
13
|
+
* body stay a single expression whatever the mutation returned.
|
|
14
|
+
*/
|
|
15
|
+
function after(_effect: unknown): Promise<void> {
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function navigate(state: FakeBrowserState, url: string): void {
|
|
20
|
+
state.url = url;
|
|
21
|
+
state.history.push(url);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function recordFill(state: FakeBrowserState, args: FillArgs): void {
|
|
25
|
+
state.fills.push({ selector: args.selector, value: args.value });
|
|
26
|
+
const element = state.elements.get(args.selector);
|
|
27
|
+
if (element) element.value = args.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function enterFrame(state: FakeBrowserState, name: string): void {
|
|
31
|
+
state.frame = name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function bumpCookies(state: FakeBrowserState): void {
|
|
35
|
+
state.cookiesCleared += 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toElements(input: Record<string, Partial<FakeElement>>): Map<string, FakeElement> {
|
|
39
|
+
const elements = new Map<string, FakeElement>();
|
|
40
|
+
for (const [selector, partial] of Object.entries(input)) {
|
|
41
|
+
elements.set(selector, { visible: true, text: "", value: "", ...partial });
|
|
42
|
+
}
|
|
43
|
+
return elements;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function initState(options: FakeDriverOptions): FakeBrowserState {
|
|
47
|
+
const url = options.url ?? "about:blank";
|
|
48
|
+
return {
|
|
49
|
+
url,
|
|
50
|
+
history: options.url ? [url] : [],
|
|
51
|
+
clicks: [],
|
|
52
|
+
fills: [],
|
|
53
|
+
frame: undefined,
|
|
54
|
+
cookiesCleared: 0,
|
|
55
|
+
evalResult: { ok: true },
|
|
56
|
+
elements: toElements(options.elements ?? {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function coreMethods(state: FakeBrowserState): FakeCore {
|
|
61
|
+
return {
|
|
62
|
+
launch: (opts) => Promise.resolve({ id: "browser-1", engine: opts.engine ?? "chromium" }),
|
|
63
|
+
newContext: () => Promise.resolve({ id: "page-1", url: state.url }),
|
|
64
|
+
visit: (args) => after(navigate(state, args.url)),
|
|
65
|
+
click: (selector) => after(state.clicks.push(selector)),
|
|
66
|
+
fill: (args) => after(recordFill(state, args)),
|
|
67
|
+
select: (args) => after(recordFill(state, args)),
|
|
68
|
+
hover: () => Promise.resolve(),
|
|
69
|
+
press: () => Promise.resolve(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extraMethods(state: FakeBrowserState): FakeExtra {
|
|
74
|
+
return {
|
|
75
|
+
upload: () => Promise.resolve(),
|
|
76
|
+
download: () => Promise.resolve({ path: "/artifacts/download.bin", bytes: 0 }),
|
|
77
|
+
screenshot: (name) => Promise.resolve({ name, path: `/artifacts/${name}.png` }),
|
|
78
|
+
waitFor: () => Promise.resolve(),
|
|
79
|
+
waitForUrl: (url) => after(navigate(state, url)),
|
|
80
|
+
evaluate: () => Promise.resolve(state.evalResult),
|
|
81
|
+
frame: (args) => after(enterFrame(state, args.name)),
|
|
82
|
+
clearCookies: () => after(bumpCookies(state)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The offline `BrowserDriver`. Every method resolves at once against an
|
|
88
|
+
* in-memory page, so a test never waits on a real browser.
|
|
89
|
+
*
|
|
90
|
+
* @param options elements to preload and the URL to start at.
|
|
91
|
+
* @returns a fresh driver whose `state` records what the flow did.
|
|
92
|
+
*/
|
|
93
|
+
export function createFakeBrowserDriver(options: FakeDriverOptions = {}): FakeBrowserDriver {
|
|
94
|
+
const state = initState(options);
|
|
95
|
+
return {
|
|
96
|
+
...coreMethods(state),
|
|
97
|
+
...extraMethods(state),
|
|
98
|
+
state,
|
|
99
|
+
element: (selector) => state.elements.get(selector),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { BrowserDriver } from "../port/index.js";
|
|
2
|
+
|
|
3
|
+
/** The in-memory model of one DOM element, as the matchers see it. */
|
|
4
|
+
export interface FakeElement {
|
|
5
|
+
visible: boolean;
|
|
6
|
+
text: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** One recorded `fill` or `select` call. */
|
|
11
|
+
export interface FillRecord {
|
|
12
|
+
selector: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Everything the fake driver records, so a test can assert on what a flow did. */
|
|
17
|
+
export interface FakeBrowserState {
|
|
18
|
+
url: string;
|
|
19
|
+
/** Every URL visited, in order. `visit` and `waitForUrl` both append. */
|
|
20
|
+
history: string[];
|
|
21
|
+
clicks: string[];
|
|
22
|
+
fills: FillRecord[];
|
|
23
|
+
/** The iframe currently entered, or `undefined` at the top level. */
|
|
24
|
+
frame: string | undefined;
|
|
25
|
+
cookiesCleared: number;
|
|
26
|
+
/** What `evaluate` resolves to, whatever the script says. */
|
|
27
|
+
evalResult: unknown;
|
|
28
|
+
elements: Map<string, FakeElement>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What to preload the fake driver's page with. */
|
|
32
|
+
export interface FakeDriverOptions {
|
|
33
|
+
/** Elements by selector. Missing fields default to visible with empty text. */
|
|
34
|
+
elements?: Record<string, Partial<FakeElement>>;
|
|
35
|
+
url?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The fake driver, plus the state a test reads back. */
|
|
39
|
+
export interface FakeBrowserDriver extends BrowserDriver {
|
|
40
|
+
readonly state: FakeBrowserState;
|
|
41
|
+
element(selector: string): FakeElement | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The driver is built in two halves so neither builder function outgrows the
|
|
45
|
+
// line budget. The split is arbitrary: together they are the whole interface.
|
|
46
|
+
|
|
47
|
+
/** Navigation and input methods. */
|
|
48
|
+
export type FakeCore = Pick<
|
|
49
|
+
BrowserDriver,
|
|
50
|
+
"launch" | "newContext" | "visit" | "click" | "fill" | "select" | "hover" | "press"
|
|
51
|
+
>;
|
|
52
|
+
|
|
53
|
+
/** Capture, wait and frame methods. */
|
|
54
|
+
export type FakeExtra = Pick<
|
|
55
|
+
BrowserDriver,
|
|
56
|
+
| "upload"
|
|
57
|
+
| "download"
|
|
58
|
+
| "screenshot"
|
|
59
|
+
| "waitFor"
|
|
60
|
+
| "waitForUrl"
|
|
61
|
+
| "evaluate"
|
|
62
|
+
| "frame"
|
|
63
|
+
| "clearCookies"
|
|
64
|
+
>;
|