Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
244 lines
8.0 KiB
TypeScript
244 lines
8.0 KiB
TypeScript
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { browser } from '@wdio/globals'
|
|
import type * as TauriApi from '@tauri-apps/api'
|
|
|
|
/** The full `@tauri-apps/api` surface, as exposed on `window.__TAURI__`. */
|
|
export type Api = typeof TauriApi
|
|
|
|
/** OS the app under test runs on. */
|
|
export type Platform = NodeJS.Platform | 'android' | 'ios'
|
|
|
|
/**
|
|
* The platform of the app under test. The desktop suite drives an app on the
|
|
* host, so it is `process.platform`; the mobile configs (`wdio.android.conf.ts`,
|
|
* `wdio.ios.conf.ts`) drive an emulator/simulator and set `E2E_PLATFORM` for
|
|
* the spec workers instead.
|
|
*/
|
|
export const platform: Platform =
|
|
(process.env.E2E_PLATFORM as Platform | undefined) ?? process.platform
|
|
|
|
export const isMobile = platform === 'android' || platform === 'ios'
|
|
|
|
type PageOutcome<T> =
|
|
| { ok: true; value: T }
|
|
| { ok: false; error: string; stack?: string }
|
|
|
|
/** Thrown when the function passed to {@link tauri} rejects inside the webview. */
|
|
export class TauriPageError extends Error {
|
|
pageStack?: string
|
|
constructor(message: string, pageStack?: string) {
|
|
super(message)
|
|
this.name = 'TauriPageError'
|
|
this.pageStack = pageStack
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs `fn` inside the app's webview with `window.__TAURI__` as its first argument
|
|
* and resolves with its (JSON-serializable) return value.
|
|
*
|
|
* `fn` is serialized with `Function.prototype.toString`, so it **cannot close over
|
|
* anything in the spec module** — every value it needs must be passed through `args`,
|
|
* and it may only reference `api`, those args, and browser globals (`window`,
|
|
* `document`, `setTimeout`, `Promise`, ...).
|
|
*
|
|
* @example
|
|
* const name = await tauri((api) => api.app.getName())
|
|
* const sum = await tauri((api, a, b) => a + b, 2, 3)
|
|
*/
|
|
export async function tauri<R, A extends unknown[]>(
|
|
fn: (api: Api, ...args: A) => R,
|
|
...args: A
|
|
): Promise<Awaited<R>> {
|
|
// A string body (rather than passing `fn` directly) keeps this working across
|
|
// both the classic and bidi WebDriver protocols and avoids any in-page eval of
|
|
// our own — the driver injects this script itself, which is exempt from the
|
|
// app's CSP. `executeAsync` is used because promise support in `execute` is not
|
|
// uniform across the platform drivers tauri-driver proxies to.
|
|
//
|
|
// The outcome crosses the driver as a JSON string rather than an object so no
|
|
// driver gets to interpret its shape: the Selenium atoms that Appium runs
|
|
// scripts through on iOS turn any object with a numeric `length` property
|
|
// into an array.
|
|
const script = `
|
|
var done = arguments[arguments.length - 1];
|
|
var args = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
|
|
var fn = (${fn.toString()});
|
|
Promise.resolve()
|
|
.then(function () { return fn.apply(null, [window.__TAURI__].concat(args)); })
|
|
.then(
|
|
function (value) { return { ok: true, value: value === undefined ? null : value }; },
|
|
function (error) {
|
|
return {
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined
|
|
};
|
|
}
|
|
)
|
|
.then(function (outcome) {
|
|
try {
|
|
done(JSON.stringify(outcome));
|
|
} catch (error) {
|
|
done(JSON.stringify({ ok: false, error: 'result is not JSON-serializable: ' + error }));
|
|
}
|
|
});
|
|
`
|
|
const raw = await browser.executeAsync(script, ...args)
|
|
const outcome = (
|
|
typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
) as PageOutcome<Awaited<R>> | null
|
|
if (!outcome || typeof outcome !== 'object' || !('ok' in outcome)) {
|
|
throw new Error(
|
|
`tauri() bridge returned an unexpected value: ${JSON.stringify(outcome)}`
|
|
)
|
|
}
|
|
if (!outcome.ok) {
|
|
throw new TauriPageError(outcome.error, outcome.stack)
|
|
}
|
|
return outcome.value
|
|
}
|
|
|
|
/**
|
|
* Asserts that the page-side call rejects and returns the rejection message,
|
|
* so specs can assert on it. Throws if the call unexpectedly resolves.
|
|
*/
|
|
export async function tauriError<A extends unknown[]>(
|
|
fn: (api: Api, ...args: A) => unknown,
|
|
...args: A
|
|
): Promise<string> {
|
|
try {
|
|
await tauri(fn, ...args)
|
|
} catch (error) {
|
|
if (error instanceof TauriPageError) {
|
|
return error.message
|
|
}
|
|
// Some platform drivers (notably the Linux WebKitWebDriver) surface a
|
|
// page-side `invoke` rejection as a WebDriver-level error on the
|
|
// `execute/async` command instead of letting the in-page bridge report it
|
|
// as an `{ ok: false }` outcome. Fall back to that error's message so the
|
|
// backend rejection is still assertable. This is safe for error-path specs:
|
|
// they match the message against an expected pattern, so a genuine driver
|
|
// failure (whose message won't match) still fails the test.
|
|
if (error instanceof Error) {
|
|
return error.message
|
|
}
|
|
throw error
|
|
}
|
|
throw new Error('expected the API call to reject, but it resolved')
|
|
}
|
|
|
|
/**
|
|
* Polls `check` until it returns without throwing or `timeout` elapses.
|
|
* Use for state that a window manager applies asynchronously (focus, minimize, ...).
|
|
*/
|
|
export async function eventually<T>(
|
|
check: () => T | Promise<T>,
|
|
{
|
|
timeout = 10_000,
|
|
interval = 250
|
|
}: { timeout?: number; interval?: number } = {}
|
|
): Promise<T> {
|
|
const deadline = Date.now() + timeout
|
|
let lastError: unknown
|
|
for (;;) {
|
|
try {
|
|
return await check()
|
|
} catch (error) {
|
|
lastError = error
|
|
}
|
|
if (Date.now() > deadline) {
|
|
throw lastError instanceof Error
|
|
? lastError
|
|
: new Error(String(lastError))
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, interval))
|
|
}
|
|
}
|
|
|
|
const skippedModules = (process.env.E2E_SKIP ?? '')
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
|
|
export interface DescribeApiOptions {
|
|
/**
|
|
* The module's plugin is not registered at all on mobile (`menu`, `tray`),
|
|
* so the whole suite is skipped there.
|
|
*/
|
|
desktopOnly?: boolean
|
|
}
|
|
|
|
/**
|
|
* `describe` wrapper keyed by API module name. Any module listed in the
|
|
* comma-separated `E2E_SKIP` env var (e.g. `E2E_SKIP=tray,menu`) is skipped.
|
|
*/
|
|
export function describeApi(module: string, fn: () => void): void
|
|
export function describeApi(
|
|
module: string,
|
|
options: DescribeApiOptions,
|
|
fn: () => void
|
|
): void
|
|
export function describeApi(
|
|
module: string,
|
|
optionsOrFn: DescribeApiOptions | (() => void),
|
|
maybeFn?: () => void
|
|
): void {
|
|
const [options, fn] =
|
|
typeof optionsOrFn === 'function'
|
|
? [{} as DescribeApiOptions, optionsOrFn]
|
|
: [optionsOrFn, maybeFn!]
|
|
const title = `@tauri-apps/api/${module}`
|
|
if (skippedModules.includes(module) || (options.desktopOnly && isMobile)) {
|
|
describe.skip(title, fn)
|
|
} else {
|
|
describe(title, fn)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `it` restricted to the given platform(s); skipped (as pending) elsewhere.
|
|
* Use for behavior that only exists on one OS, e.g. `activityName()` on Android.
|
|
*/
|
|
export function itOn(
|
|
platforms: Platform | Platform[],
|
|
title: string,
|
|
fn: () => void | Promise<void>
|
|
): void {
|
|
const list = Array.isArray(platforms) ? platforms : [platforms]
|
|
if (list.includes(platform)) {
|
|
it(title, fn)
|
|
} else {
|
|
it.skip(title, fn)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `it` for desktop-only APIs. The mobile builds do not register the commands
|
|
* behind them (`#[cfg(desktop)]` in the core plugins) or implement them as
|
|
* no-ops (window title, size, resizability), so they are skipped on mobile.
|
|
*/
|
|
export function itDesktop(title: string, fn: () => void | Promise<void>): void {
|
|
if (isMobile) {
|
|
it.skip(title, fn)
|
|
} else {
|
|
it(title, fn)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `it` for assertions that depend on a real window manager (minimize, maximize,
|
|
* focus, ...). Skipped entirely when `E2E_SKIP_WM` is set (e.g. bare headless CI),
|
|
* and on mobile, which has no window manager (nor those commands).
|
|
*/
|
|
export function itWm(title: string, fn: () => void | Promise<void>): void {
|
|
if (process.env.E2E_SKIP_WM || isMobile) {
|
|
it.skip(title, fn)
|
|
} else {
|
|
it(title, fn)
|
|
}
|
|
}
|