Drive a real site and play its video
You can put another website inside your own layout, act on it through locators, and play its video after asking the user once. This page takes that from an empty <iframe> to a currentTime that advances, on either backend, and names the three choices that only show up once the calls are composed.
The end state is a real site rendered inside your app’s layout, playing, with one consent prompt behind it. What the recipe costs:
- Uses
attachFrame({ iframe, domains, cookies }),Frame.backend,Frame.goto,Frame.addStyleTag,Frame.locator(...).exists(),.ensure(),.videoElement(),RemoteVideoElement,permissions.request - Needs the extension for the video surface, the cloud backend otherwise
- Proven by four integrations
A backend is where a call runs: the FKN cloud, or the user’s own browser through the extension, the FKN browser extension. Every step carries the [Page] badge, defined on recipes, because attachFrame() needs a window realm, a JavaScript execution context with a window. The two live demos under frames and locators and actions run this shape end to end, so you can watch it before you write it.
[Page] Choose the layout before mounting
Section titled “[Page] Choose the layout before mounting”An attached frame, the Frame that attachFrame() returns, is bound to one <iframe> element in one place in your document. Moving that element to another parent removes it from the document and inserts it again, and the browser reloads it on the way. On the cloud backend the attachment ends the moment the iframe leaves the document, and every later call throws cloud.attachFrame: the attached iframe left the document or was reloaded; attach a fresh iframe.
So the layout is decided first. Choose where the site sits, a modal or a panel beside your own controls, from what the app knows before the attach, mount the iframe there, and attach it in that place. Read frame.backend() after the attach to decide what the layout shows around it, since the video surface below is an extension backend feature.
[Page] Attach and declare the domains
Section titled “[Page] Attach and declare the domains”attachFrame() takes the iframe once it is in the document, domains names the hosts the frame will hold, and cookies picks the jar and with it the backend:
const const iframe: HTMLIFrameElement
iframe = var document: Document
window.document returns a reference to the document contained in the window.
document.Document.createElement<"iframe">(tagName: "iframe", options?: ElementCreationOptions): HTMLIFrameElement (+2 overloads)
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
createElement('iframe')var document: Document
window.document returns a reference to the document contained in the window.
document.ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors.
querySelector('#player-slot')!.ParentNode.append(...nodes: (Node | string)[]): void
Inserts nodes after the last child of node, while replacing strings in nodes with equivalent Text nodes.
Throws a "HierarchyRequestError" DOMException if the constraints of the node tree are violated.
append(const iframe: HTMLIFrameElement
iframe) // in the document, in its final place, before the attach
const const frame: Frame
frame = await function attachFrame(options: AttachFrameOptions): Promise<Frame> (+1 overload)
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({ iframe: HTMLIFrameElement
iframe, domains?: string[] | undefined
domains: ['example.org'], cookies?: AttachCookies | undefined
Which jar, and with it which backend: 'persistent' when absent. See AttachCookies.
cookies: function isExtensionExposed(): boolean
Unchanged on purpose: a page half built before versioning calls exactly this, so an extension that
announces an ABI keeps answering it the same way. An outdated extension also answers true.
isExtensionExposed() ? 'native' : 'persistent', // the extension and the person's own cookies when it is there, the cloud otherwise})const const onExtension: boolean
onExtension = const frame: Frame
frame.function backend(): "cloud" | "extension" | undefined
Which backend serves this attachment, fixed for its life. The same call can draw a card on one
backend and not the other, and spends a different identity on each, so an app must be able to
tell. Undefined only for a Frame an app built itself with createFrame from a backend that
names none.
backend() === 'extension' // fixed for the attachment's lifeThe call resolves once a backend has taken the iframe, and the Frame is the same object on either one. 'native' runs on the extension, and 'persistent', the default, on the render proxy, the cloud frame backend, whatever is installed, see which jar, which backend.
domains does two different jobs. On the extension it lifts the framing headers for those hosts and copies the person’s cookies for them into the frame. On the cloud it is the list of hosts a frame.fetch() may reach, see options.
On the cloud backend the frame holds none of the person’s browser cookies. It reads your app’s cloud jar, so a sign-in, when a site wants one, runs inside the frame, and frame.clearCookies() is how your app ends it, see clearing cookies.
[Page] Ask for the category once
Section titled “[Page] Ask for the category once”The consent sheet is the prompt the extension shows before an action above severity 0, and click() sits at severity 3. What the user answers is a category of access on one website, so one Interaction row covers every click, fill and hover on that site, and ensure() raises it without touching an element:
try { await const frame: Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('.controls').ensure: (operation: "click" | "fill" | "hover" | "textContent" | "getAttribute" | "isVisible" | "count" | "exists" | "videoElement", options?: EnsureOptions) => Promise<void>
ensure('click', { reason: string
reason: 'Control the player from this app' }) // one Interaction row} catch (var error: unknown
error) { if ((var error: unknown
error as interface Error
Error).Error.name: string
name !== 'PermissionDeniedError') throw var error: unknown
error // the user said no, so leave the site's own controls in view}
await const frame: Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('.controls').locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#play').click: (options?: PositionOptions | undefined) => Promise<void>
click() // covered, no sheetawait const frame: Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('.controls').locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('#mute').click: (options?: PositionOptions | undefined) => Promise<void>
click() // covered too, one 'covered' row in the activity logThe two clicks raise no sheet. Each writes one row to the activity log, the on-device record of what an app did, see ensure() and categories. The refusal arrives under the name PermissionDeniedError, which neither exported guard matches, so test error.name, see a refusal neither guard matches.
Run the ensure() from the click that turns the feature on, with a reason that names what the user gets, see asking well. The cloud backend asks for the same category through the broker’s card, see consent on the cloud backend.
[Page] Navigate with the right wait
Section titled “[Page] Navigate with the right wait”goto() navigates the frame, and waitUntil decides when it resolves. A player page that keeps loading long after its <video> exists is the case for 'commit':
await const frame: Frame
frame.function goto(url: string, options?: GotoOptions): Promise<void>
Navigates the frame to url, which must pass the rules an iframe src does, and adds its host
to the attachment. Resolves at options.waitUntil, and rejects with TimeoutError past
options.timeout (GotoOptions).
goto('https://example.org/player', { waitUntil?: "commit" | "load" | undefined
When goto resolves: 'load', the default, once the document it brings fired load; 'commit',
Playwright's, once that document holds the frame, possibly before the page's own scripts ran and
whether or not it ever fires load. Either way a call made after it runs on the document the goto
brought, never on the one it replaced, and the goto rejects with TimeoutError when that
document has not come by its deadline.
A goto that changes only the fragment of the url the page is at keeps its document. On the cloud
backend it resolves, either way, once the page reports the new fragment. Two pages are the
exception there, where such a goto brings a new document like any other: one reached through a
redirect, and one that followed a link onto another origin. On the extension (measured on
Chromium) one awaited to 'load' resolves, since the frame fires load for it, and one awaited to
'commit' runs out at its timeout, since no new document starts.
waitUntil: 'commit' }) // once the player's document holds the frame, before it finished loadingconst frame: Frame
frame.function url(): string
The url last given to attachFrame or goto, never the frame's live location: a page that moves itself does not change it.
url() // 'https://example.org/player', the url you asked for'commit' resolves once the goto’s own document holds the frame, the same moment on both backends, and a locator call from then on runs on that document and is retried until its element appears. 'load', the default, waits for the page’s load event as well. Since @fkn/lib 0.9.42 neither leaves a call on the cloud racing a document that has not committed, so the choice no longer depends on the backend.
Both waits are bounded by timeout: 30,000 ms by default on the extension, and in the render proxy plus 5,000 ms in the library on the cloud, after which the goto rejects with a TimeoutError. A relative URL resolves against your page rather than the frame, so pass absolute ones, see goto() and url().
[Page] Read and act
Section titled “[Page] Read and act”A locator chain is a Locator built by chaining selectors, and nothing touches the page until you call an action on it. Probe first, then read: exists() answers at severity 0 with no sheet, and textContent() reads at severity 1, after one sheet on the extension and none on the cloud:
const const title: Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
title = const frame: Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('h1') // nothing has happened yetif (await const title: Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
title.exists: (_options?: LocatorOptions | undefined) => Promise<boolean>
exists()) { // true, a probe at severity 0, no sheet var document: Document
window.document returns a reference to the document contained in the window.
document.ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)
Returns the first element that is a descendant of node that matches selectors.
querySelector('#heading')!.Element.textContent: string | null
textContent = await const title: Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
title.textContent: (_options?: LocatorOptions | undefined) => Promise<string>
textContent({ reason?: string | undefined
reason: 'Show the title in this app' }) // the heading text, after one sheet for read.text on the extension}
await const frame: Frame
frame.addStyleTag: (options: AddStyleTagOptions) => Promise<void>
addStyleTag({ content: string
content: '#sidebar { display: none }' }) // media.appear, severity 0, with url() stripped from the css firstThe heading now sits in your own page, which is the first half of the check below. exists() and count() accept any number of matches and never prompt, where a read or an action needs exactly one match, see reading versus acting. addStyleTag() filters the CSS before it lands, see styling the page.
[Page] Take the video surface
Section titled “[Page] Take the video surface”videoElement() hands you a RemoteVideoElement, a handle backed by the real <video> inside the frame. It runs at severity 0, so no sheet:
const const video: RemoteVideoElement
video: type RemoteVideoElement = EventTarget & { currentTime: number; volume: number; muted: boolean; playbackRate: number; src: string; loop: boolean; autoplay: boolean; preload: string; poster: string; disableRemotePlayback: boolean; readonly duration: number; readonly paused: boolean; readonly ended: boolean; readonly seeking: boolean; readonly readyState: number; readonly currentSrc: string; readonly error: MediaError | null; readonly buffered: TimeRanges; readonly seekable: TimeRanges; play(): Promise<void>; pause(): void; load(): void; requestPictureInPicture(): Promise<void>; exitPictureInPicture(): Promise<void>; readonly HAVE_FUTURE_DATA: 3; readonly HAVE_ENOUGH_DATA: 4;}
RemoteVideoElement = await const frame: Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('video').videoElement: (_options?: LocatorOptions | undefined) => Promise<RemoteVideoElement>
videoElement() // media.video, severity 0, no sheet
const video: RemoteVideoElement
video.EventTarget.addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
addEventListener('timeupdate', () => { const video: RemoteVideoElement
video.currentTime: number
currentTime // advancing, refreshed by the event that just arrived})const video: RemoteVideoElement
video.muted: boolean
muted = true // written locally at once, then reconciled from the frameawait const video: RemoteVideoElement
video.function play(): Promise<void>
play() // the frame's own play() promise, so its rejection reaches youplay() returns the frame’s own promise, and the timeupdate listener keeps currentTime and duration current. Without a listener the read-only fields stay at the snapshot taken when the handle was made, see video. requestPictureInPicture() rejects with requestPictureInPicture is not supported in this environment where the frame’s browser lacks it, so watch the handle’s enterpictureinpicture event instead.
The extension backend serves the handle. On the cloud backend it is not measured, so gate your own control strip on onExtension and leave the site’s player in view otherwise, see limitations.
[Page] Tell terminal from retryable
Section titled “[Page] Tell terminal from retryable”Every locator action retries a failed attempt every 50 ms until its deadline, then rejects with a TimeoutError carrying the last attempt’s message. Three names stop the loop early, and isLocatorUnsupported() picks out the one no retry can fix:
const const readTitle: () => Promise<string | null>
readTitle = async (): interface Promise<T>
Represents the completion of an asynchronous operation
Promise<string | null> => { try { return await const frame: Frame
frame.locator: (selector: string) => Locator$1<Extended<{ readonly element: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly getByRole: { readonly to: "element"; readonly resolve: (context: LocatorContext, role: string) => Element[]; readonly render: (role: unknown) => { fragment: string; }; }; readonly getByText: { readonly to: "element"; readonly resolve: (context: LocatorContext, text: string) => Element[]; readonly render: (text: unknown) => { fragment: string; }; }; readonly getByTestId: { readonly to: "element"; readonly resolve: (context: LocatorContext, testId: string) => Element[]; readonly render: (testId: unknown) => { fragment: string; }; }; readonly first: { readonly to: "element"; readonly resolve: (context: LocatorContext) => Element[]; readonly render: () => { fragment: string; }; }; readonly nth: { readonly to: "element"; readonly resolve: (context: LocatorContext, index: number) => Element[]; readonly render: (index: unknown) => { fragment: string; }; }; }; readonly operations: { readonly click: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly fill: { readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void; }; readonly hover: { readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void; }; readonly textContent: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string; }; readonly getAttribute: { readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null; }; readonly isVisible: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; readonly count: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number; }; readonly exists: { readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean; }; }; }; readonly frame: { readonly selectors: { readonly locator: { readonly to: "element"; readonly css: true; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "descend"; }; }; readonly frameLocator: { readonly to: "frame"; readonly css: true; readonly barrier: "down"; readonly resolve: (context: LocatorContext, selector: string) => Element[]; readonly render: (selector: unknown) => { fragment: string; separator: "down"; }; }; readonly owner: { readonly to: "frame"; readonly barrier: "up"; readonly resolve: (_context: LocatorContext) => Element[]; readonly render: () => { fragment: string; separator: "up"; }; }; }; readonly operations: { readonly addStyleTag: { readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void; }; readonly fetch: { readonly kind: ChainKind; readonly ...
locator('h1').textContent: (_options?: LocatorOptions | undefined) => Promise<string>
textContent({ timeout?: number | undefined
timeout: 5_000 }) } catch (function (local var) error: unknown
error) { if (function isLocatorUnsupported(error: unknown): boolean
isLocatorUnsupported(function (local var) error: unknown
error)) throw function (local var) error: unknown
error // this backend cannot run it, and asking again changes nothing if (function isTerminalError(error: unknown): boolean
isTerminalError(function (local var) error: unknown
error)) throw function (local var) error: unknown
error // a gate said no, or the argument was malformed, never retried if (function (local var) error: unknown
error instanceof class TimeoutError
A call that ran out of time: its deadline passed before it settled. One class for every call that
has a deadline, as Playwright's TimeoutError, named 'TimeoutError' so a check by name works as
well as instanceof. It is minted where the call was made, never in the realm that ran it, so it
is an instance of this class in the caller's realm.
message is what the call reported at its deadline, unchanged from the error it replaces. cause
is the last retryable error an attempt met before the deadline, so a locator call that never found
its element says why; it is absent when no attempt failed (one never settled, or the call is not
retried). Not terminal: nothing retries a call past its own deadline.
TimeoutError) return null // 'No elements found' after 5 s, so try once more when the page settles throw function (local var) error: unknown
error }}The first branch is LocatorUnsupportedError: Locator operation not supported on the <backend> backend: <operation> when a backend does not serve the operation. On the cloud path the detached-iframe error from the first step carries the same name, so that the loop stops instead of retrying a dead channel. The second branch covers LocatorDeniedError and LocatorInvalidError, both thrown once, see errors.
The third is the deadline. A missing element ends as a TimeoutError whose message is No elements found once the 5 seconds passed to timeout are spent, with the last attempt’s LocatorError as cause, and its message is Locator timeout (5000ms): textContent only when no attempt failed before the deadline, see what a timeout says. A retry of the whole call is yours to schedule, once the page had time to settle.
Check it worked
Section titled “Check it worked”The heading from the site is in your own page, and video.currentTime reads higher on each timeupdate than on the one before. When it does not, take these in order:
- A
TimeoutErrorreadingNo elements foundafter 30 seconds means the chain matched nothing on its last attempt. Check the selector against the site’s markup. - A frame that went stale means the iframe moved or reloaded. On the cloud the message is
cloud.attachFrame: the attached iframe left the document or was reloaded; attach a fresh iframe, and the fix is a fresh iframe in its final place. On the extension a document that navigated off the declared origins refuses withframe: this frame no longer holds the document the app attached it to, and agoto()to a declared target brings it back. Locator operation not supported on the <backend> backend: <operation>means the frame landed on a backend without that operation. Readframe.backend()and hide the feature rather than retrying.- A
TimeoutErrorreadingLocator timeout (30000ms): videoElementmeans no attempt failed and a single attempt hung. Raisetimeout, or find out what the attempt is waiting on.
Optional: Ask for everything at once
Section titled “Optional: Ask for everything at once”frame.requestPermissions() puts Site data and Interaction on one prompt before the first call, the shape the guided demo uses, so the heading read above costs no sheet of its own. attachFrame({ permissions }) runs the same ask as the attach’s last step, see several at once.
Optional: Pin a backend
Section titled “Optional: Pin a backend”extension.attachFrame({ cookies: 'native' }) and cloud.attachFrame() call one backend each, and the extension one opens the install card when nothing answers, see pinning a backend.