touchpress 0.0.1
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 +111 -0
- package/dist/core/index.d.mts +71 -0
- package/dist/core/index.mjs +2 -0
- package/dist/index.d.mts +76 -0
- package/dist/index.mjs +439 -0
- package/dist/preflight--Z-REtl-.d.mts +853 -0
- package/dist/preflight-C83jCNPs.mjs +1865 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# tangere
|
|
2
|
+
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> tangere is highly experimental. Use at your own risk.
|
|
5
|
+
|
|
6
|
+
tangere 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
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { expect, test } from 'tangere';
|
|
10
|
+
|
|
11
|
+
test('the right credentials land on the profile', async ({ device }) => {
|
|
12
|
+
await device.getByTestId('sign-in-link').tap();
|
|
13
|
+
await device.getByRole('text-field', { name: 'Email' }).fill('rob@example.com');
|
|
14
|
+
await device.getByTestId('password').fill('hunter2', { secret: true });
|
|
15
|
+
await device.getByRole('button', { name: 'Sign in' }).tap();
|
|
16
|
+
|
|
17
|
+
await expect(device.getByTestId('signing-in')).toBeVisible();
|
|
18
|
+
await expect(device.getByTestId('profile-email')).toHaveText('rob@example.com', { exact: true });
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pnpm add -D tangere @playwright/test
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Configure
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// playwright.config.ts
|
|
34
|
+
import { defineConfig } from '@playwright/test';
|
|
35
|
+
import type { TangereOptions } from 'tangere';
|
|
36
|
+
|
|
37
|
+
export default defineConfig<TangereOptions>({
|
|
38
|
+
testDir: 'e2e',
|
|
39
|
+
workers: 1,
|
|
40
|
+
expect: { timeout: 10_000 },
|
|
41
|
+
reporter: [['list'], ['html', { open: 'never' }]],
|
|
42
|
+
use: {
|
|
43
|
+
app: 'com.example.app',
|
|
44
|
+
readyWhen: { testId: 'home' },
|
|
45
|
+
},
|
|
46
|
+
projects: [
|
|
47
|
+
{ name: 'ios', use: { platform: 'ios', deviceName: 'iPhone 17 Pro Max' } },
|
|
48
|
+
{ name: 'android', use: { platform: 'android', deviceName: 'Pixel 7 API 34' } },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every option tangere 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
|
+
|
|
55
|
+
`readyWhen` is required. The driver returns from a launch as soon as the native process starts, before the JavaScript bundle has loaded, so tangere waits for that locator before the first test runs.
|
|
56
|
+
|
|
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
|
+
|
|
59
|
+
One worker gets one device. To run more than one, give `deviceName` an array with an entry per worker.
|
|
60
|
+
|
|
61
|
+
### Run
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx playwright test --project=ios
|
|
65
|
+
```
|
|
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/tangere/blob/main/docs/basics.md) has the spec, and [`apps/e2e/e2e/preflight.setup.mts`](https://github.com/wobsoriano/tangere/blob/main/apps/e2e/e2e/preflight.setup.mts) is a working one.
|
|
68
|
+
|
|
69
|
+
## Run the sample project
|
|
70
|
+
|
|
71
|
+
`apps/e2e` is an Expo app with a home, login, and profile route and a fake sign-in. It is the app tangere 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
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx expo run:ios --device 'iPhone 17 Pro Max' --no-bundler
|
|
75
|
+
npx expo start --port 8081
|
|
76
|
+
npx playwright test --project=ios
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Android works the same way. `expo run:android` takes the AVD's own name with underscores, not the spaced name the test config uses, and the emulator needs `adb reverse` to reach Metro on the host.
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx expo run:android --device Expo_API_36 --no-bundler
|
|
83
|
+
adb reverse tcp:8081 tcp:8081
|
|
84
|
+
npx expo start --port 8081
|
|
85
|
+
npx playwright test --project=android
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Leave Metro running for the whole suite. The first build takes several minutes.
|
|
89
|
+
|
|
90
|
+
## Docs
|
|
91
|
+
|
|
92
|
+
- [Basics](https://github.com/wobsoriano/tangere/blob/main/docs/basics.md)
|
|
93
|
+
- [Configuration](https://github.com/wobsoriano/tangere/blob/main/docs/configuration.md)
|
|
94
|
+
- [Locators](https://github.com/wobsoriano/tangere/blob/main/docs/locators.md)
|
|
95
|
+
- [Assertions](https://github.com/wobsoriano/tangere/blob/main/docs/assertions.md)
|
|
96
|
+
- [Lifecycle](https://github.com/wobsoriano/tangere/blob/main/docs/lifecycle.md)
|
|
97
|
+
- [Continuous integration](https://github.com/wobsoriano/tangere/blob/main/docs/ci.md)
|
|
98
|
+
|
|
99
|
+
## The workspace
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
packages/tangere/ the library, published to npm
|
|
103
|
+
apps/e2e/ tangere-e2e, an Expo SDK 57 app, private
|
|
104
|
+
docs/ the documentation linked above
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`vp check`, `vp test`, and `vp run -r build` at the root cover every package. The end-to-end suite is separate because it needs a device.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
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 TANGERE_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 TangereOptions, l as ScrollTrail, lt as describeQuery, m as FillOptions, mt as RawSnapshot, n as PreflightReport, nt as Verdict, o as TangereError, 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--Z-REtl-.mjs";
|
|
2
|
+
//#region src/core/evidence.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Never throws. A capture that fails records a note and returns, because masking
|
|
5
|
+
* the test's real error with a screenshot error is worse than no screenshot.
|
|
6
|
+
*/
|
|
7
|
+
declare function captureEvidence(session: DeviceSession, sink: ActionSink): Promise<void>;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/core/screenshot.d.ts
|
|
10
|
+
/** A rectangle in image pixels, which is what a snapshot rect becomes once it is scaled. */
|
|
11
|
+
type PixelBox = {
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
readonly width: number;
|
|
15
|
+
readonly height: number;
|
|
16
|
+
};
|
|
17
|
+
type Size = {
|
|
18
|
+
readonly width: number;
|
|
19
|
+
readonly height: number;
|
|
20
|
+
};
|
|
21
|
+
type CompareOptions = {
|
|
22
|
+
/** pixelmatch's per-pixel colour distance, 0 to 1. Smaller is stricter. */
|
|
23
|
+
readonly threshold: number;
|
|
24
|
+
/** The share of the image allowed to differ before the comparison fails. */
|
|
25
|
+
readonly maxDiffPixelRatio: number;
|
|
26
|
+
/**
|
|
27
|
+
* Painted opaque black in both images before anything is compared. A clock, an
|
|
28
|
+
* avatar, or anything else that changes between runs goes here rather than
|
|
29
|
+
* into a looser threshold, which would blind the whole image.
|
|
30
|
+
*/
|
|
31
|
+
readonly mask: readonly PixelBox[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A size mismatch is its own case rather than a ratio of 1, because nothing
|
|
35
|
+
* about a threshold or a mask can rescue it.
|
|
36
|
+
*/
|
|
37
|
+
type Comparison = {
|
|
38
|
+
readonly kind: 'match';
|
|
39
|
+
readonly ratio: number;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: 'mismatch';
|
|
42
|
+
readonly ratio: number;
|
|
43
|
+
readonly diff: Buffer;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: 'size-mismatch';
|
|
46
|
+
readonly expected: Size;
|
|
47
|
+
readonly actual: Size;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The ratio is mismatched pixels over the image's own pixel count, so it means
|
|
51
|
+
* the same whether the images are a whole device or one cropped button.
|
|
52
|
+
*/
|
|
53
|
+
declare function compareScreenshot(expected: Buffer, actual: Buffer, options: CompareOptions): Comparison;
|
|
54
|
+
/**
|
|
55
|
+
* The box is clamped to the image, because a rect comes from a snapshot and a
|
|
56
|
+
* screenshot is a separate capture. A control flush against the bottom edge can
|
|
57
|
+
* round a pixel past it, and that is not a reason to fail an assertion.
|
|
58
|
+
*/
|
|
59
|
+
declare function cropScreenshot(source: Buffer, box: PixelBox): Buffer;
|
|
60
|
+
declare function sizeOf(source: Buffer): Size;
|
|
61
|
+
/** Rounded outward, so a control's own edge is never the thing that gets cut off. */
|
|
62
|
+
declare function toPixelBox(rect: {
|
|
63
|
+
readonly x: number;
|
|
64
|
+
readonly y: number;
|
|
65
|
+
readonly width: number;
|
|
66
|
+
readonly height: number;
|
|
67
|
+
}, scale: number): PixelBox;
|
|
68
|
+
/** Moves a box into the coordinates of a crop taken at `origin`. */
|
|
69
|
+
declare function relativeTo(box: PixelBox, origin: PixelBox): PixelBox;
|
|
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, TANGERE_DEFAULTS, TangereError, type TangereOptions, type TextMatch, type TextOptions, 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 };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as textMatch, C as describeCheck, D as parseDeviceOptions, E as deviceNameForSlot, O as describeQuery, S as resolve, T as TANGERE_DEFAULTS, a as sizeOf, b as parseScreen, d as createScrollSearch, f as directionToward, g as sessionName, h as openSession, i as relativeTo, j as TangereError, 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 renderTitle, w as evaluate, x as renderScreen, y as silentSink } from "../preflight-C83jCNPs.mjs";
|
|
2
|
+
export { TANGERE_DEFAULTS, TangereError, 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
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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 TangereOptions, n as PreflightReport, o as TangereError, ot as Query, p as Device, pt as Platform, r as preflight, st as Role, t as PreflightDevice, vt as ScreenNode } from "./preflight--Z-REtl-.mjs";
|
|
2
|
+
import { ExpectMatcherState } from "@playwright/test";
|
|
3
|
+
//#region src/playwright/fixtures.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Tangere's options and none of its fixtures, for a setup project that reads the
|
|
6
|
+
* configuration before any session exists, such as one calling `preflight`.
|
|
7
|
+
*
|
|
8
|
+
* `platform`, `app`, and `readyWhen` default to `undefined` rather than to a
|
|
9
|
+
* plausible value. A Playwright option fixture needs a default of its declared
|
|
10
|
+
* type, and `parseDeviceOptions` rejects `undefined` by name, so a config that
|
|
11
|
+
* forgot a key and one that never set it fail the same way.
|
|
12
|
+
*/
|
|
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 & TangereOptions>;
|
|
14
|
+
/**
|
|
15
|
+
* `device` is auto so evidence capture runs for every test in a device project,
|
|
16
|
+
* whether or not the body touched it. Its teardown runs before the session's,
|
|
17
|
+
* inside the separate budget Playwright grants after the test finishes, so a
|
|
18
|
+
* timed-out test still gets a screenshot.
|
|
19
|
+
*
|
|
20
|
+
* Importing and extending `test` launches no browser: `browser`, `context`, and
|
|
21
|
+
* `page` are lazy and non-auto, and nothing here names them.
|
|
22
|
+
*/
|
|
23
|
+
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 & TangereOptions & {
|
|
26
|
+
session: DeviceSession;
|
|
27
|
+
}>;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/playwright/screenshot.d.ts
|
|
30
|
+
type ScreenshotOptions = {
|
|
31
|
+
timeout?: number;
|
|
32
|
+
/** The share of the image allowed to differ. @default 0.01 */
|
|
33
|
+
maxDiffPixelRatio?: number;
|
|
34
|
+
/** pixelmatch's per-pixel colour distance, 0 to 1. Smaller is stricter. @default 0.2 */
|
|
35
|
+
threshold?: number;
|
|
36
|
+
/** Painted opaque black in both images, for anything that legitimately changes between runs. */
|
|
37
|
+
mask?: Locator[];
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/playwright/expect.d.ts
|
|
41
|
+
type MatcherOptions = {
|
|
42
|
+
timeout?: number;
|
|
43
|
+
};
|
|
44
|
+
type TextMatcherOptions = MatcherOptions & {
|
|
45
|
+
exact?: boolean;
|
|
46
|
+
};
|
|
47
|
+
type MatcherResult = {
|
|
48
|
+
pass: boolean;
|
|
49
|
+
message: () => string;
|
|
50
|
+
name: string;
|
|
51
|
+
expected: string;
|
|
52
|
+
actual: string | null;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Playwright's own matcher names on this package's `expect` only. Matcher
|
|
56
|
+
* typing is by the first parameter, so these surface on `expect(locator)` and
|
|
57
|
+
* nothing else.
|
|
58
|
+
*/
|
|
59
|
+
declare const expect: import("@playwright/test").Expect<{
|
|
60
|
+
toBeVisible: (this: ExpectMatcherState, locator: Locator, options?: MatcherOptions) => Promise<MatcherResult>;
|
|
61
|
+
toBeEnabled: (this: ExpectMatcherState, locator: Locator, options?: MatcherOptions) => Promise<MatcherResult>;
|
|
62
|
+
toBeSelected: (this: ExpectMatcherState, locator: Locator, options?: MatcherOptions) => Promise<MatcherResult>;
|
|
63
|
+
toBeFocused: (this: ExpectMatcherState, locator: Locator, options?: MatcherOptions) => Promise<MatcherResult>;
|
|
64
|
+
toHaveText(this: ExpectMatcherState, locator: Locator, expected: string | RegExp, options?: TextMatcherOptions): Promise<MatcherResult>;
|
|
65
|
+
toHaveValue(this: ExpectMatcherState, locator: Locator, expected: string | RegExp, options?: MatcherOptions): Promise<MatcherResult>;
|
|
66
|
+
toHaveCount(this: ExpectMatcherState, locator: Locator, expected: number, options?: MatcherOptions): Promise<MatcherResult>;
|
|
67
|
+
toHaveScreenshot(this: ExpectMatcherState, target: Device | Locator, nameOrOptions?: string | ScreenshotOptions, options?: ScreenshotOptions): Promise<{
|
|
68
|
+
pass: boolean;
|
|
69
|
+
message: () => string;
|
|
70
|
+
name: string;
|
|
71
|
+
expected: string;
|
|
72
|
+
actual: string | null;
|
|
73
|
+
}>;
|
|
74
|
+
}>;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { type Device, type ErrorInfo, type ExpectedValue, 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, TangereError, type TangereOptions, type TextMatch, expect, preflight, setupTest, test };
|