touchpress 0.0.1 → 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/README.md +33 -19
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/index.d.mts +51 -7
- package/dist/index.mjs +509 -12
- package/dist/{preflight-C83jCNPs.mjs → preflight-B7KrbqSO.mjs} +146 -112
- package/dist/{preflight--Z-REtl-.d.mts → preflight-Czhj9QFM.d.mts} +40 -8
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# touchpress
|
|
2
2
|
|
|
3
3
|
> [!WARNING]
|
|
4
|
-
>
|
|
4
|
+
> touchpress is highly experimental. Use at your own risk.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
touchpress runs e2e tests for mobile apps on the Playwright test runner. It drives a booted simulator or emulator through [`agent-device`](https://agent-device.dev/).
|
|
7
7
|
|
|
8
8
|
```ts
|
|
9
|
-
import { expect, test } from '
|
|
9
|
+
import { expect, test } from 'touchpress';
|
|
10
10
|
|
|
11
11
|
test('the right credentials land on the profile', async ({ device }) => {
|
|
12
12
|
await device.getByTestId('sign-in-link').tap();
|
|
@@ -24,7 +24,7 @@ test('the right credentials land on the profile', async ({ device }) => {
|
|
|
24
24
|
### Install
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
pnpm add -D
|
|
27
|
+
pnpm add -D touchpress @playwright/test
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
### Configure
|
|
@@ -32,9 +32,9 @@ pnpm add -D tangere @playwright/test
|
|
|
32
32
|
```ts
|
|
33
33
|
// playwright.config.ts
|
|
34
34
|
import { defineConfig } from '@playwright/test';
|
|
35
|
-
import type {
|
|
35
|
+
import type { TouchpressOptions } from 'touchpress';
|
|
36
36
|
|
|
37
|
-
export default defineConfig<
|
|
37
|
+
export default defineConfig<TouchpressOptions>({
|
|
38
38
|
testDir: 'e2e',
|
|
39
39
|
workers: 1,
|
|
40
40
|
expect: { timeout: 10_000 },
|
|
@@ -50,9 +50,9 @@ export default defineConfig<TangereOptions>({
|
|
|
50
50
|
});
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Every option
|
|
53
|
+
Every option touchpress adds is a key of its own in `use`. Playwright merges `use` one key at a time, so what the projects share is written once at the top level and a project sets only what differs.
|
|
54
54
|
|
|
55
|
-
`readyWhen` is required. The driver returns from a launch as soon as the native process starts, before the JavaScript bundle has loaded, so
|
|
55
|
+
`readyWhen` is required. The driver returns from a launch as soon as the native process starts, before the JavaScript bundle has loaded, so touchpress waits for that locator before the first test runs.
|
|
56
56
|
|
|
57
57
|
`deviceName` is what `agent-device devices` prints, which for an Android emulator is the AVD name with its underscores shown as spaces. An AVD created as `Pixel_7_API_34` is `Pixel 7 API 34` here.
|
|
58
58
|
|
|
@@ -64,11 +64,24 @@ One worker gets one device. To run more than one, give `deviceName` an array wit
|
|
|
64
64
|
npx playwright test --project=ios
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`preflight` reads a project's options and reports whether the device they name is booted, so a missing simulator fails once, in about a second, instead of once per test after the launch timeout. Wire it as a setup project per platform that the device projects depend on. [Basics](https://github.com/wobsoriano/
|
|
67
|
+
`preflight` reads a project's options and reports whether the device they name is booted, so a missing simulator fails once, in about a second, instead of once per test after the launch timeout. Wire it as a setup project per platform that the device projects depend on. [Basics](https://github.com/wobsoriano/touchpress/blob/main/docs/basics.md) has the spec, and [`apps/e2e/e2e/preflight.setup.mts`](https://github.com/wobsoriano/touchpress/blob/main/apps/e2e/e2e/preflight.setup.mts) is a working one.
|
|
68
|
+
|
|
69
|
+
### Drive a step with a model
|
|
70
|
+
|
|
71
|
+
`device.act` takes an instruction in English and drives the app until it is satisfied. `device.extract` asks one question about the screen and returns a typed answer.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
test('sign in', async ({ device }) => {
|
|
75
|
+
await device.act('Sign in with the email rob@example.com and the password hunter2');
|
|
76
|
+
await expect(device.getByTestId('greeting')).toHaveText('Hi, Rob');
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Set `use.aiModel` to a gateway model id or a provider model instance, and install `ai`, an optional peer dependency. A loop can run for minutes, so raise the test timeout on any spec that calls `act`. The example uses the sample app's fake account. Keep real credentials in deterministic `fill(text, { secret: true })` calls outside `act`, and assert deterministically afterwards. [AI](https://github.com/wobsoriano/touchpress/blob/main/docs/ai.md) covers `act`, `extract`, the tools the model gets, and the report.
|
|
68
81
|
|
|
69
82
|
## Run the sample project
|
|
70
83
|
|
|
71
|
-
`apps/e2e` is an Expo app with a home, login, and profile route and a fake sign-in. It is the app
|
|
84
|
+
`apps/e2e` is an Expo app with a home, login, and profile route and a fake sign-in. It is the app touchpress is tested against. Build the library first with `vp run -r build` so the app can resolve `dist`, then run these from `apps/e2e`:
|
|
72
85
|
|
|
73
86
|
```sh
|
|
74
87
|
npx expo run:ios --device 'iPhone 17 Pro Max' --no-bundler
|
|
@@ -89,18 +102,19 @@ Leave Metro running for the whole suite. The first build takes several minutes.
|
|
|
89
102
|
|
|
90
103
|
## Docs
|
|
91
104
|
|
|
92
|
-
- [Basics](https://github.com/wobsoriano/
|
|
93
|
-
- [Configuration](https://github.com/wobsoriano/
|
|
94
|
-
- [Locators](https://github.com/wobsoriano/
|
|
95
|
-
- [Assertions](https://github.com/wobsoriano/
|
|
96
|
-
- [
|
|
97
|
-
- [
|
|
105
|
+
- [Basics](https://github.com/wobsoriano/touchpress/blob/main/docs/basics.md)
|
|
106
|
+
- [Configuration](https://github.com/wobsoriano/touchpress/blob/main/docs/configuration.md)
|
|
107
|
+
- [Locators](https://github.com/wobsoriano/touchpress/blob/main/docs/locators.md)
|
|
108
|
+
- [Assertions](https://github.com/wobsoriano/touchpress/blob/main/docs/assertions.md)
|
|
109
|
+
- [AI](https://github.com/wobsoriano/touchpress/blob/main/docs/ai.md)
|
|
110
|
+
- [Lifecycle](https://github.com/wobsoriano/touchpress/blob/main/docs/lifecycle.md)
|
|
111
|
+
- [Continuous integration](https://github.com/wobsoriano/touchpress/blob/main/docs/ci.md)
|
|
98
112
|
|
|
99
113
|
## The workspace
|
|
100
114
|
|
|
101
115
|
```
|
|
102
|
-
packages/
|
|
103
|
-
apps/e2e/
|
|
116
|
+
packages/touchpress/ the library, published to npm
|
|
117
|
+
apps/e2e/ touchpress-e2e, an Expo SDK 57 app, private
|
|
104
118
|
docs/ the documentation linked above
|
|
105
119
|
```
|
|
106
120
|
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as Tree, A as deviceNameForSlot, B as ProbeResult, C as openSession, D as ResolvedOptions, E as ReadyQuery, F as StepOptions, G as CaptureOptions, H as formatFailure, I as Typed, J as DeviceInfo, K as DeviceDriver, L as renderTitle, M as ActionRecord, N as ActionSink, O as
|
|
1
|
+
import { $ as Tree, A as deviceNameForSlot, B as ProbeResult, C as openSession, D as ResolvedOptions, E as ReadyQuery, F as StepOptions, G as CaptureOptions, H as formatFailure, I as Typed, J as DeviceInfo, K as DeviceDriver, L as renderTitle, M as ActionRecord, N as ActionSink, O as TOUCHPRESS_DEFAULTS, P as EvidenceFile, Q as Settled, R as silentSink, S as SessionState, T as DeviceChoice, U as probe, V as ProbeTarget, W as Binding, X as OpenRequest, Y as DeviceSelection, Z as ScrollDirection, _ as RoleOptions, _t as Screen, a as ExpectedValue, at as Filter, b as DeviceSession, bt as renderScreen, c as ScrollSearch, ct as TextMatch, d as directionToward, dt as textMatch, et as Check, f as ActionOptions, ft as PinnedRef, g as Locator, gt as Resolution, h as FilterOptions, ht as Rect, i as ErrorInfo, it as evaluate, j as parseDeviceOptions, k as TouchpressOptions, l as ScrollTrail, lt as describeQuery, m as FillOptions, mt as RawSnapshot, n as PreflightReport, nt as Verdict, o as TouchpressError, ot as Query, p as Device, pt as Platform, q as DeviceFailure, r as preflight, rt as describeCheck, s as ScrollDevice, st as Role, t as PreflightDevice, tt as CheckName, u as createScrollSearch, ut as normalizeText, v as TextOptions, vt as ScreenNode, w as sessionName, x as OpenSessionInput, xt as resolve, y as createDevice, yt as parseScreen, z as ProbeOptions } from "../preflight-Czhj9QFM.mjs";
|
|
2
2
|
//#region src/core/evidence.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Never throws. A capture that fails records a note and returns, because masking
|
|
@@ -68,4 +68,4 @@ declare function toPixelBox(rect: {
|
|
|
68
68
|
/** Moves a box into the coordinates of a crop taken at `origin`. */
|
|
69
69
|
declare function relativeTo(box: PixelBox, origin: PixelBox): PixelBox;
|
|
70
70
|
//#endregion
|
|
71
|
-
export { type ActionOptions, type ActionRecord, type ActionSink, type Binding, type CaptureOptions, type Check, type CheckName, type CompareOptions, type Comparison, type Device, type DeviceChoice, type DeviceDriver, type DeviceFailure, type DeviceInfo, type DeviceSelection, type DeviceSession, type ErrorInfo, type EvidenceFile, type ExpectedValue, type FillOptions, type Filter, type FilterOptions, type Locator, type OpenRequest, type OpenSessionInput, type PinnedRef, type PixelBox, type Platform, type PreflightDevice, type PreflightReport, type ProbeOptions, type ProbeResult, type ProbeTarget, type Query, type RawSnapshot, type ReadyQuery, type Rect, type Resolution, type ResolvedOptions, type Role, type RoleOptions, type Screen, type ScreenNode, type ScrollDevice, type ScrollDirection, type ScrollSearch, type ScrollTrail, type SessionState, type Settled, type Size, type StepOptions,
|
|
71
|
+
export { type ActionOptions, type ActionRecord, type ActionSink, type Binding, type CaptureOptions, type Check, type CheckName, type CompareOptions, type Comparison, type Device, type DeviceChoice, type DeviceDriver, type DeviceFailure, type DeviceInfo, type DeviceSelection, type DeviceSession, type ErrorInfo, type EvidenceFile, type ExpectedValue, type FillOptions, type Filter, type FilterOptions, type Locator, type OpenRequest, type OpenSessionInput, type PinnedRef, type PixelBox, type Platform, type PreflightDevice, type PreflightReport, type ProbeOptions, type ProbeResult, type ProbeTarget, type Query, type RawSnapshot, type ReadyQuery, type Rect, type Resolution, type ResolvedOptions, type Role, type RoleOptions, type Screen, type ScreenNode, type ScrollDevice, type ScrollDirection, type ScrollSearch, type ScrollTrail, type SessionState, type Settled, type Size, type StepOptions, TOUCHPRESS_DEFAULTS, type TextMatch, type TextOptions, TouchpressError, type TouchpressOptions, type Tree, type Typed, type Verdict, captureEvidence, compareScreenshot, createDevice, createScrollSearch, cropScreenshot, describeCheck, describeQuery, deviceNameForSlot, directionToward, evaluate, formatFailure, normalizeText, openSession, parseDeviceOptions, parseScreen, preflight, probe, relativeTo, renderScreen, renderTitle, resolve, sessionName, silentSink, sizeOf, textMatch, toPixelBox };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as textMatch, C as
|
|
2
|
-
export {
|
|
1
|
+
import { A as textMatch, C as renderScreen, D as parseDeviceOptions, E as deviceNameForSlot, O as describeQuery, S as parseScreen, T as TOUCHPRESS_DEFAULTS, a as sizeOf, b as renderTitle, d as createScrollSearch, f as directionToward, g as sessionName, h as openSession, i as relativeTo, j as TouchpressError, k as normalizeText, l as captureEvidence, m as probe, n as compareScreenshot, o as toPixelBox, p as formatFailure, r as cropScreenshot, t as preflight, u as createDevice, v as describeCheck, w as resolve, x as silentSink, y as evaluate } from "../preflight-B7KrbqSO.mjs";
|
|
2
|
+
export { TOUCHPRESS_DEFAULTS, TouchpressError, captureEvidence, compareScreenshot, createDevice, createScrollSearch, cropScreenshot, describeCheck, describeQuery, deviceNameForSlot, directionToward, evaluate, formatFailure, normalizeText, openSession, parseDeviceOptions, parseScreen, preflight, probe, relativeTo, renderScreen, renderTitle, resolve, sessionName, silentSink, sizeOf, textMatch, toPixelBox };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
|
-
import { E as ReadyQuery, _t as Screen, a as ExpectedValue, at as Filter, b as DeviceSession, ct as TextMatch, g as Locator, h as FilterOptions, ht as Rect, i as ErrorInfo, k as
|
|
1
|
+
import { E as ReadyQuery, _t as Screen, a as ExpectedValue, at as Filter, b as DeviceSession, ct as TextMatch, g as Locator, h as FilterOptions, ht as Rect, i as ErrorInfo, k as TouchpressOptions$1, n as PreflightReport, o as TouchpressError, ot as Query, p as Device$1, pt as Platform, r as preflight, st as Role, t as PreflightDevice, vt as ScreenNode } from "./preflight-Czhj9QFM.mjs";
|
|
2
2
|
import { ExpectMatcherState } from "@playwright/test";
|
|
3
|
+
import { FlexibleSchema, LanguageModel } from "ai";
|
|
4
|
+
//#region src/ai/device.d.ts
|
|
5
|
+
/** A loop can run for minutes, so its budget is its own rather than the action timeout a deterministic step takes. */
|
|
6
|
+
type ActOptions = {
|
|
7
|
+
timeout?: number;
|
|
8
|
+
maxSteps?: number;
|
|
9
|
+
};
|
|
10
|
+
type ExtractOptions = {
|
|
11
|
+
timeout?: number;
|
|
12
|
+
};
|
|
13
|
+
type AiDevice = {
|
|
14
|
+
/**
|
|
15
|
+
* Drives the app with a model until the instruction is satisfied. Resolves
|
|
16
|
+
* with the model's summary of what it did, and throws when the model reports
|
|
17
|
+
* it could not proceed or runs out of steps.
|
|
18
|
+
*/
|
|
19
|
+
act(instruction: string, options?: ActOptions): Promise<string>;
|
|
20
|
+
/** Asks a model one question about the current screen and validates the answer against `schema`. */
|
|
21
|
+
extract<T>(question: string, schema: FlexibleSchema<T>, options?: ExtractOptions): Promise<T>;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/ai/options.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* The one key `act` and `extract` add to Playwright's `use`. It is not part of
|
|
27
|
+
* `core/config.ts`, because nothing under `core/` may name an AI SDK type.
|
|
28
|
+
*
|
|
29
|
+
* A gateway model id such as `'anthropic/claude-sonnet-5'` and a provider
|
|
30
|
+
* model instance are both `LanguageModel`, so a config picks either without a
|
|
31
|
+
* second key. Unset is the default, and it fails at the first `act` rather than
|
|
32
|
+
* at worker start, so a project that never calls one needs no model.
|
|
33
|
+
*/
|
|
34
|
+
type AiOptions = {
|
|
35
|
+
aiModel: LanguageModel | undefined;
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
3
38
|
//#region src/playwright/fixtures.d.ts
|
|
4
39
|
/**
|
|
5
|
-
*
|
|
40
|
+
* Touchpress's options and none of its fixtures, for a setup project that reads the
|
|
6
41
|
* configuration before any session exists, such as one calling `preflight`.
|
|
7
42
|
*
|
|
8
43
|
* `platform`, `app`, and `readyWhen` default to `undefined` rather than to a
|
|
@@ -10,7 +45,7 @@ import { ExpectMatcherState } from "@playwright/test";
|
|
|
10
45
|
* type, and `parseDeviceOptions` rejects `undefined` by name, so a config that
|
|
11
46
|
* forgot a key and one that never set it fail the same way.
|
|
12
47
|
*/
|
|
13
|
-
declare const setupTest: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & object, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions &
|
|
48
|
+
declare const setupTest: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & object, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions & TouchpressOptions$1 & AiOptions>;
|
|
14
49
|
/**
|
|
15
50
|
* `device` is auto so evidence capture runs for every test in a device project,
|
|
16
51
|
* whether or not the body touched it. Its teardown runs before the session's,
|
|
@@ -21,8 +56,8 @@ declare const setupTest: import("@playwright/test").TestType<import("@playwright
|
|
|
21
56
|
* `page` are lazy and non-auto, and nothing here names them.
|
|
22
57
|
*/
|
|
23
58
|
declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & object & {
|
|
24
|
-
device: Device;
|
|
25
|
-
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions &
|
|
59
|
+
device: Device$1 & AiDevice;
|
|
60
|
+
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions & TouchpressOptions$1 & AiOptions & {
|
|
26
61
|
session: DeviceSession;
|
|
27
62
|
}>;
|
|
28
63
|
//#endregion
|
|
@@ -64,7 +99,7 @@ declare const expect: import("@playwright/test").Expect<{
|
|
|
64
99
|
toHaveText(this: ExpectMatcherState, locator: Locator, expected: string | RegExp, options?: TextMatcherOptions): Promise<MatcherResult>;
|
|
65
100
|
toHaveValue(this: ExpectMatcherState, locator: Locator, expected: string | RegExp, options?: MatcherOptions): Promise<MatcherResult>;
|
|
66
101
|
toHaveCount(this: ExpectMatcherState, locator: Locator, expected: number, options?: MatcherOptions): Promise<MatcherResult>;
|
|
67
|
-
toHaveScreenshot(this: ExpectMatcherState, target: Device | Locator, nameOrOptions?: string | ScreenshotOptions, options?: ScreenshotOptions): Promise<{
|
|
102
|
+
toHaveScreenshot(this: ExpectMatcherState, target: Device$1 | Locator, nameOrOptions?: string | ScreenshotOptions, options?: ScreenshotOptions): Promise<{
|
|
68
103
|
pass: boolean;
|
|
69
104
|
message: () => string;
|
|
70
105
|
name: string;
|
|
@@ -73,4 +108,13 @@ declare const expect: import("@playwright/test").Expect<{
|
|
|
73
108
|
}>;
|
|
74
109
|
}>;
|
|
75
110
|
//#endregion
|
|
76
|
-
|
|
111
|
+
//#region src/index.d.ts
|
|
112
|
+
/**
|
|
113
|
+
* `Device` and `TouchpressOptions` carry the AI surface here and only here.
|
|
114
|
+
* `touchpress/core` exports the runner-independent pair without it, because
|
|
115
|
+
* nothing under `core/` may name an AI SDK type.
|
|
116
|
+
*/
|
|
117
|
+
type TouchpressOptions = TouchpressOptions$1 & AiOptions;
|
|
118
|
+
type Device = Device$1 & AiDevice;
|
|
119
|
+
//#endregion
|
|
120
|
+
export { type ActOptions, type AiDevice, type AiOptions, Device, type ErrorInfo, type ExpectedValue, type ExtractOptions, type Filter, type FilterOptions, type Locator, type Platform, type PreflightDevice, type PreflightReport, type Query, type ReadyQuery, type Rect, type Role, type Screen, type ScreenNode, type ScreenshotOptions, type TextMatch, TouchpressError, TouchpressOptions, expect, preflight, setupTest, test };
|