nativeproof 0.10.2 → 0.10.4
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/CHANGELOG.md +42 -0
- package/README.md +7 -1
- package/dist/app.d.ts +46 -20
- package/dist/app.js +2 -1
- package/dist/cli.d.ts +18 -1
- package/dist/cli.js +92 -2
- package/dist/config.d.ts +41 -11
- package/dist/config.js +43 -9
- package/dist/harness.d.ts +17 -9
- package/dist/mock.d.ts +11 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ All notable changes to NativeProof are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.10.4
|
|
8
|
+
|
|
9
|
+
Per-project spec sets and WebdriverIO tuning pass-through, for suites that run a different set of
|
|
10
|
+
specs per platform and need longer timeouts on slow devices.
|
|
11
|
+
|
|
12
|
+
**Added**
|
|
13
|
+
|
|
14
|
+
- `DeviceProject.specs` — per-project spec globs that override the top-level `testDir`/`testMatch`,
|
|
15
|
+
for suites where platforms run different specs (e.g. a shared set plus a platform-specific set:
|
|
16
|
+
`["e2e/shared/**\/*.spec.ts", "e2e/android/**\/*.spec.ts"]`). A `--spec` CLI override still wins.
|
|
17
|
+
Precedence: `--spec` (comma-separated) > `project.specs` > `testDir`/`testMatch`.
|
|
18
|
+
- WebdriverIO tuning pass-throughs on `RunnerConfig`: `connectionRetryTimeout`,
|
|
19
|
+
`connectionRetryCount`, `waitforTimeout`, `bail`, and `logLevel`. Each is forwarded to the
|
|
20
|
+
synthesised WebdriverIO config only when set, so WebdriverIO's own defaults apply otherwise —
|
|
21
|
+
slow software-GPU emulators in particular often need longer connection/wait timeouts. `bail: 0`
|
|
22
|
+
is meaningful (never bail) and is still forwarded.
|
|
23
|
+
|
|
24
|
+
## 0.10.3
|
|
25
|
+
|
|
26
|
+
Out-of-the-box setup: scaffolding, minimal config, a route-optional mock contract, and typed
|
|
27
|
+
contexts across the `createHarness` export boundary.
|
|
28
|
+
|
|
29
|
+
**Added**
|
|
30
|
+
|
|
31
|
+
- `nativeproof init` scaffolds a starter `nativeproof.config.ts` (app + harness + android/ios
|
|
32
|
+
projects) and a sample spec, so a new project is runnable in one command. Idempotent — it
|
|
33
|
+
never overwrites an existing file.
|
|
34
|
+
- `defineApp` now accepts any mock that exposes `frames()` + `stop()` (the new `SessionMock`);
|
|
35
|
+
`route()` is no longer required, since a session never routes (only a spec does). An app whose
|
|
36
|
+
mock only observes traffic can use `defineApp` / `createHarness` / `nativeproof.config` directly.
|
|
37
|
+
- `buildWdioConfig` fills in `platformName` and `appium:automationName` per platform (Android →
|
|
38
|
+
UiAutomator2, iOS → XCUITest), so a project needs only `name` + `platform`. A project's own
|
|
39
|
+
capabilities still win, and `DeviceProject.capabilities` is now optional.
|
|
40
|
+
|
|
41
|
+
**Fixed**
|
|
42
|
+
|
|
43
|
+
- `export const { test } = createHarness(app)` consumed from a spec in another file now keeps a
|
|
44
|
+
fully-typed fixture context. The harness/app/config are parameterised by the *resolved* context
|
|
45
|
+
rather than the screens type `S` (which TS widened to its constraint — screens → `unknown` — when
|
|
46
|
+
computing the exported type), so behaviours get typed `mock` and screens across the import
|
|
47
|
+
boundary. `App<S, M>` is now `App<Ctx>`; `NativeProofConfig` / `defineConfig` follow.
|
|
48
|
+
|
|
7
49
|
## 0.10.2
|
|
8
50
|
|
|
9
51
|
Generic mock typing and built-in evidence-on-failure.
|
package/README.md
CHANGED
|
@@ -100,7 +100,13 @@ npx appium driver doctor uiautomator2
|
|
|
100
100
|
|
|
101
101
|
## Quick start
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Scaffold the starting files with one command, then fill in your app's seam:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx nativeproof init # writes nativeproof.config.ts + a sample spec (never overwrites)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then four steps from zero to a green run on Android — or set it all up by hand:
|
|
104
110
|
|
|
105
111
|
**1. Configure** — one `nativeproof.config.ts` at the project root:
|
|
106
112
|
|
package/dist/app.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Driver } from "./driver.js";
|
|
2
2
|
import type { FailureInfo, ScenarioFixture } from "./fixtures.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SessionMock } from "./mock.js";
|
|
4
4
|
/**
|
|
5
5
|
* `defineApp` — the single seam script.
|
|
6
6
|
*
|
|
@@ -12,22 +12,23 @@ import type { MockBackend } from "./mock.js";
|
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* The device handles every session provides before app screens are layered on.
|
|
15
|
-
* Generic over the mock type `M` (default {@link
|
|
15
|
+
* Generic over the mock type `M` (default {@link SessionMock}) so an app with a richer
|
|
16
16
|
* mock — extra socket/presence controls beyond the base contract — gets `mock` typed as
|
|
17
|
-
* that richer type throughout, with no casts.
|
|
17
|
+
* that richer type throughout, with no casts. The constraint is `SessionMock` (frames + stop),
|
|
18
|
+
* not the full `MockBackend`, so a mock that doesn't implement `route()` still works.
|
|
18
19
|
*/
|
|
19
|
-
export interface DeviceContext<M extends
|
|
20
|
+
export interface DeviceContext<M extends SessionMock = SessionMock> {
|
|
20
21
|
driver: Driver;
|
|
21
22
|
mock: M;
|
|
22
23
|
}
|
|
23
24
|
/** A screen-object factory: given the device context, build that screen's locators/actions. */
|
|
24
|
-
export type ScreenFactory<S, M extends
|
|
25
|
-
export type ScreenFactories<M extends
|
|
25
|
+
export type ScreenFactory<S, M extends SessionMock = SessionMock> = (context: DeviceContext<M>) => S;
|
|
26
|
+
export type ScreenFactories<M extends SessionMock = SessionMock> = Record<string, ScreenFactory<unknown, M>>;
|
|
26
27
|
/** Context passed to the login/join flows. */
|
|
27
|
-
export type FlowContext<M extends
|
|
28
|
+
export type FlowContext<M extends SessionMock = SessionMock> = DeviceContext<M> & {
|
|
28
29
|
role: string;
|
|
29
30
|
};
|
|
30
|
-
export interface AppDefinition<S extends ScreenFactories<M>, M extends
|
|
31
|
+
export interface AppDefinition<S extends ScreenFactories<M>, M extends SessionMock = SessionMock> {
|
|
31
32
|
/** Acquire the device/driver (e.g. wdioDriver()). */
|
|
32
33
|
driver: () => Driver | Promise<Driver>;
|
|
33
34
|
/** Start the app's mock backend (its concrete type `M` flows through the whole session). */
|
|
@@ -37,31 +38,56 @@ export interface AppDefinition<S extends ScreenFactories<M>, M extends MockBacke
|
|
|
37
38
|
/** App-specific evidence-redaction patterns. */
|
|
38
39
|
redact?: readonly RegExp[];
|
|
39
40
|
/** Reach a logged-in state for the role. */
|
|
40
|
-
login?: (context: FlowContext<M
|
|
41
|
+
login?: (context: NoInfer<FlowContext<M>>) => Promise<void>;
|
|
41
42
|
/** Enter the role's main surface. */
|
|
42
|
-
join?: (context: FlowContext<M
|
|
43
|
-
/** Screen-object factories, bound to the device context.
|
|
43
|
+
join?: (context: NoInfer<FlowContext<M>>) => Promise<void>;
|
|
44
|
+
/** Screen-object factories, bound to the device context. `S` (and so the whole context) is
|
|
45
|
+
* inferred from here. */
|
|
44
46
|
screens: S;
|
|
45
47
|
/**
|
|
46
48
|
* Release app-level resources acquired across the session, run on teardown BEFORE the
|
|
47
49
|
* mock stops and before the runner deletes the device session — e.g. force-stop the app
|
|
48
50
|
* so its background sockets are gone before `deleteSession`. The mock is still stopped
|
|
49
51
|
* even if this throws.
|
|
52
|
+
*
|
|
53
|
+
* The context is wrapped in `NoInfer` so passing a `teardown` does NOT drive `S` inference —
|
|
54
|
+
* otherwise this S-dependent parameter co-infers with `screens` and collapses `S` to its
|
|
55
|
+
* `ScreenFactories<M>` constraint (screens → `unknown`) in the exported context type.
|
|
50
56
|
*/
|
|
51
|
-
teardown?: (context: SessionContext<S, M
|
|
57
|
+
teardown?: (context: NoInfer<SessionContext<S, M>>) => Promise<void> | void;
|
|
52
58
|
/**
|
|
53
59
|
* Invoked when a behaviour throws, before the failure propagates — wire on-failure
|
|
54
60
|
* evidence here (e.g. `captureState(...)`) so capture lives in one place, not in every
|
|
55
|
-
* behaviour. Its own errors are swallowed so they never mask the real failure.
|
|
61
|
+
* behaviour. Its own errors are swallowed so they never mask the real failure. `NoInfer` for the
|
|
62
|
+
* same reason as `teardown` — this parameter must not participate in inferring `S`.
|
|
56
63
|
*/
|
|
57
|
-
onFailure?: (context: SessionContext<S, M
|
|
64
|
+
onFailure?: (context: NoInfer<SessionContext<S, M>>, info: FailureInfo) => Promise<void> | void;
|
|
58
65
|
}
|
|
59
|
-
/**
|
|
60
|
-
|
|
66
|
+
/** Eagerly flatten an intersection into one object type, so the resolved context ports cleanly. */
|
|
67
|
+
type Prettify<T> = {
|
|
68
|
+
[K in keyof T]: T[K];
|
|
69
|
+
} & {};
|
|
70
|
+
/**
|
|
71
|
+
* The fixture context a session injects: the device handles plus each app screen's value. Flattened
|
|
72
|
+
* with {@link Prettify} so it is a single object type rather than a lazy intersection — that lets
|
|
73
|
+
* `export const { test } = createHarness(app)` carry fully-typed screens into specs in other files.
|
|
74
|
+
* (Portability still needs the mock type `M` to be nameable — a single exported interface, not an
|
|
75
|
+
* anonymous intersection — so a consumer's mock should be one named type.)
|
|
76
|
+
*/
|
|
77
|
+
export type SessionContext<S extends ScreenFactories<M>, M extends SessionMock = SessionMock> = Prettify<DeviceContext<M> & {
|
|
61
78
|
[K in keyof S]: ReturnType<S[K]>;
|
|
62
|
-
}
|
|
63
|
-
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* Parameterised by the *resolved* session context `Ctx` (`{ driver; mock; …screens }`), not by
|
|
82
|
+
* the screens type `S`. `S` is only ever used inside the mapped `SessionContext<S, M>`, a
|
|
83
|
+
* non-inferable position — so a consumer's `export const { test } = createHarness(app)` would
|
|
84
|
+
* lose the concrete screen return types across the import boundary (TS falls back to the
|
|
85
|
+
* `ScreenFactories<M>` constraint, whose screens are `unknown`). `defineApp` resolves the context
|
|
86
|
+
* here, where `S` is still concrete, so `Ctx` is a plain object type that ports cleanly into specs.
|
|
87
|
+
*/
|
|
88
|
+
export interface App<Ctx> {
|
|
64
89
|
/** A scenario fixture that provisions a logged-in, joined session for `role`. */
|
|
65
|
-
session(role?: string): ScenarioFixture<
|
|
90
|
+
session(role?: string): ScenarioFixture<Ctx>;
|
|
66
91
|
}
|
|
67
|
-
export declare function defineApp<S extends ScreenFactories<M>, M extends
|
|
92
|
+
export declare function defineApp<S extends ScreenFactories<M>, M extends SessionMock = SessionMock>(definition: AppDefinition<S, M>): App<SessionContext<S, M>>;
|
|
93
|
+
export {};
|
package/dist/app.js
CHANGED
|
@@ -12,7 +12,8 @@ export function defineApp(definition) {
|
|
|
12
12
|
if (definition.join)
|
|
13
13
|
await definition.join({ ...device, role });
|
|
14
14
|
const screens = {};
|
|
15
|
-
|
|
15
|
+
const factories = Object.entries(definition.screens);
|
|
16
|
+
for (const [name, factory] of factories) {
|
|
16
17
|
screens[name] = factory(device);
|
|
17
18
|
}
|
|
18
19
|
// Dynamic assembly: the screen factories' precise return types are
|
package/dist/cli.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* the environment (the mobile analogue of needing a display) and is left to the host.
|
|
10
10
|
*/
|
|
11
11
|
export interface CliArgs {
|
|
12
|
-
command: "test" | "help" | "version";
|
|
12
|
+
command: "test" | "init" | "help" | "version";
|
|
13
13
|
config: string | undefined;
|
|
14
14
|
platform: "android" | "ios" | undefined;
|
|
15
15
|
project: string | undefined;
|
|
@@ -22,6 +22,23 @@ export interface CliArgs {
|
|
|
22
22
|
export declare function parseArgs(argv: readonly string[]): CliArgs;
|
|
23
23
|
export declare function version(): string;
|
|
24
24
|
export declare function helpText(): string;
|
|
25
|
+
export interface ScaffoldFile {
|
|
26
|
+
path: string;
|
|
27
|
+
contents: string;
|
|
28
|
+
}
|
|
29
|
+
/** The starter files \`nativeproof init\` writes — pure, so they can be asserted in a test. */
|
|
30
|
+
export declare function scaffoldFiles(): ScaffoldFile[];
|
|
31
|
+
/** Minimal filesystem seam so \`scaffold\` is testable without touching disk. */
|
|
32
|
+
export interface ScaffoldIo {
|
|
33
|
+
exists(file: string): boolean;
|
|
34
|
+
write(file: string, contents: string): void;
|
|
35
|
+
}
|
|
36
|
+
/** Write the starter files under \`cwd\`, never overwriting an existing one. */
|
|
37
|
+
export declare function scaffold(cwd: string, io?: ScaffoldIo): {
|
|
38
|
+
created: string[];
|
|
39
|
+
skipped: string[];
|
|
40
|
+
};
|
|
41
|
+
export declare function init(cwd?: string): number;
|
|
25
42
|
interface ResolvedRunner {
|
|
26
43
|
wdioConfig: string;
|
|
27
44
|
extraEnv: NodeJS.ProcessEnv;
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { findConfigFile } from "./config.js";
|
|
@@ -27,6 +27,10 @@ export function parseArgs(argv) {
|
|
|
27
27
|
const arg = argv[i];
|
|
28
28
|
if (arg === "test")
|
|
29
29
|
continue;
|
|
30
|
+
if (arg === "init") {
|
|
31
|
+
args.command = "init";
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
30
34
|
if (arg === "-h" || arg === "--help")
|
|
31
35
|
return { ...args, command: "help" };
|
|
32
36
|
if (arg === "-v" || arg === "--version")
|
|
@@ -92,7 +96,8 @@ export function helpText() {
|
|
|
92
96
|
"nativeproof — Native Mobile E2E test framework inspired by Playwright",
|
|
93
97
|
"",
|
|
94
98
|
"Usage:",
|
|
95
|
-
" nativeproof [test] [options]",
|
|
99
|
+
" nativeproof [test] [options] run the suite (default)",
|
|
100
|
+
" nativeproof init scaffold nativeproof.config.ts + a sample spec",
|
|
96
101
|
"",
|
|
97
102
|
"Config is auto-discovered: nativeproof.config.ts (preferred), else wdio.conf.ts.",
|
|
98
103
|
"",
|
|
@@ -109,6 +114,88 @@ export function helpText() {
|
|
|
109
114
|
" -v, --version print the version",
|
|
110
115
|
].join("\n");
|
|
111
116
|
}
|
|
117
|
+
const CONFIG_TEMPLATE = `import { createHarness, defineApp, defineConfig, page, startMockServer, wdioDriver } from "nativeproof";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One file declares your app, your devices, and where your specs live — the
|
|
121
|
+
* \`nativeproof\` CLI discovers it and runs the suite. Replace the TODOs with your app.
|
|
122
|
+
*/
|
|
123
|
+
const app = defineApp({
|
|
124
|
+
// Acquire the device/driver (Appium/WebdriverIO under the hood).
|
|
125
|
+
driver: () => wdioDriver(),
|
|
126
|
+
// Start a mock backend; swap startMockServer() for your own if you have one.
|
|
127
|
+
mock: () => startMockServer(),
|
|
128
|
+
// Screen objects: build locators/actions from the device context. Replace these.
|
|
129
|
+
screens: {
|
|
130
|
+
home: ({ driver }) => ({
|
|
131
|
+
heading: page(driver).getByText("Welcome"),
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Specs import these: \`import { test, expect } from "../nativeproof.config";\`
|
|
137
|
+
export const { test, expect } = createHarness(app);
|
|
138
|
+
|
|
139
|
+
export default defineConfig({
|
|
140
|
+
app,
|
|
141
|
+
testDir: "tests",
|
|
142
|
+
// platformName + automationName are filled in from \`platform\`; set \`appium:app\` to your build.
|
|
143
|
+
projects: [
|
|
144
|
+
{ name: "android", platform: "android", capabilities: { /* "appium:app": "/path/to/app.apk" */ } },
|
|
145
|
+
{ name: "ios", platform: "ios", capabilities: { /* "appium:app": "/path/to/app.app" */ } },
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
`;
|
|
149
|
+
const SPEC_TEMPLATE = `import { expect, test } from "../nativeproof.config";
|
|
150
|
+
|
|
151
|
+
test.describe("example", () => {
|
|
152
|
+
test("the home screen shows its heading", async ({ home }) => {
|
|
153
|
+
await expect(home.heading).toBeVisible();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
`;
|
|
157
|
+
/** The starter files \`nativeproof init\` writes — pure, so they can be asserted in a test. */
|
|
158
|
+
export function scaffoldFiles() {
|
|
159
|
+
return [
|
|
160
|
+
{ path: "nativeproof.config.ts", contents: CONFIG_TEMPLATE },
|
|
161
|
+
{ path: "tests/example.spec.ts", contents: SPEC_TEMPLATE },
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
const diskIo = {
|
|
165
|
+
exists: existsSync,
|
|
166
|
+
write(file, contents) {
|
|
167
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
168
|
+
writeFileSync(file, contents);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
/** Write the starter files under \`cwd\`, never overwriting an existing one. */
|
|
172
|
+
export function scaffold(cwd, io = diskIo) {
|
|
173
|
+
const created = [];
|
|
174
|
+
const skipped = [];
|
|
175
|
+
for (const file of scaffoldFiles()) {
|
|
176
|
+
if (io.exists(path.join(cwd, file.path))) {
|
|
177
|
+
skipped.push(file.path);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
io.write(path.join(cwd, file.path), file.contents);
|
|
181
|
+
created.push(file.path);
|
|
182
|
+
}
|
|
183
|
+
return { created, skipped };
|
|
184
|
+
}
|
|
185
|
+
export function init(cwd = process.cwd()) {
|
|
186
|
+
const { created, skipped } = scaffold(cwd);
|
|
187
|
+
for (const file of created)
|
|
188
|
+
console.log(`nativeproof: created ${file}`);
|
|
189
|
+
for (const file of skipped)
|
|
190
|
+
console.log(`nativeproof: ${file} already exists — skipped`);
|
|
191
|
+
if (created.length === 0) {
|
|
192
|
+
console.log("nativeproof: nothing to scaffold (all files already exist)");
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.log("\nNext: set capabilities + screens in nativeproof.config.ts, then run `nativeproof`.");
|
|
196
|
+
}
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
112
199
|
function localBin(name) {
|
|
113
200
|
const bin = path.join(process.cwd(), "node_modules", ".bin", name);
|
|
114
201
|
return existsSync(bin) ? bin : name;
|
|
@@ -208,6 +295,9 @@ export async function main(argv) {
|
|
|
208
295
|
console.log(version());
|
|
209
296
|
return 0;
|
|
210
297
|
}
|
|
298
|
+
if (args.command === "init") {
|
|
299
|
+
return init();
|
|
300
|
+
}
|
|
211
301
|
return runTests(args);
|
|
212
302
|
}
|
|
213
303
|
/** True when this file is the process entry — robust to the symlink npm creates for bins. */
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { App
|
|
2
|
-
import type { MockBackend } from "./mock.js";
|
|
1
|
+
import type { App } from "./app.js";
|
|
3
2
|
/**
|
|
4
3
|
* The Playwright-style config: one `nativeproof.config.ts` declares the app, the device
|
|
5
4
|
* projects, and where the tests live. The `nativeproof` CLI auto-discovers it and
|
|
@@ -30,26 +29,59 @@ export interface DeviceProject {
|
|
|
30
29
|
/** A name to select with `nativeproof --project <name>`. */
|
|
31
30
|
name: string;
|
|
32
31
|
platform: "android" | "ios";
|
|
33
|
-
/**
|
|
34
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Appium capabilities for this device (e.g. `appium:app`, `appium:deviceName`). Optional:
|
|
34
|
+
* `platformName` and `appium:automationName` are filled in from `platform` (see
|
|
35
|
+
* {@link defaultCapabilities}), so a project usually needs only `appium:app` — or nothing
|
|
36
|
+
* for a smoke run against whatever is already installed. Anything you set here wins.
|
|
37
|
+
*/
|
|
38
|
+
capabilities?: Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Spec globs for THIS project, relative to the project root — overriding the top-level
|
|
41
|
+
* `testDir`/`testMatch` when set. For suites where platforms run different specs (e.g. a shared
|
|
42
|
+
* set plus a platform-specific set: `["e2e/shared/**\/*.spec.ts", "e2e/android/**\/*.spec.ts"]`).
|
|
43
|
+
* A `--spec` CLI override still wins over this.
|
|
44
|
+
*/
|
|
45
|
+
specs?: string[];
|
|
35
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The standard capabilities for a platform, so a consumer doesn't restate the same
|
|
49
|
+
* `platformName` / `automationName` in every project. Android → UiAutomator2, iOS → XCUITest
|
|
50
|
+
* (the canonical Appium drivers). A project's own `capabilities` override these.
|
|
51
|
+
*/
|
|
52
|
+
export declare function defaultCapabilities(platform: "android" | "ios"): Record<string, unknown>;
|
|
36
53
|
/** The device/run config the CLI turns into a WebdriverIO run. */
|
|
37
54
|
export interface RunnerConfig {
|
|
38
55
|
/** Directory holding the specs (default "tests"). */
|
|
39
56
|
testDir?: string;
|
|
40
|
-
/** Glob within `testDir` (default "**\/*.spec.ts"). */
|
|
57
|
+
/** Glob within `testDir` (default "**\/*.spec.ts"). A project's own `specs` overrides this. */
|
|
41
58
|
testMatch?: string;
|
|
42
59
|
projects: DeviceProject[];
|
|
43
60
|
appium?: AppiumOptions;
|
|
44
61
|
/** Per-test timeout in ms (default 240000). */
|
|
45
62
|
mochaTimeout?: number;
|
|
63
|
+
/**
|
|
64
|
+
* WebdriverIO pass-throughs for tuning real-device runs. Each is forwarded only when set, so
|
|
65
|
+
* WebdriverIO's own defaults apply otherwise. Slow software-GPU emulators in particular often
|
|
66
|
+
* need a longer `connectionRetryTimeout` / `waitforTimeout` than the defaults.
|
|
67
|
+
*/
|
|
68
|
+
/** Per-command/session connection timeout in ms (wdio default 120000). */
|
|
69
|
+
connectionRetryTimeout?: number;
|
|
70
|
+
/** Connection retry count (wdio default 3). */
|
|
71
|
+
connectionRetryCount?: number;
|
|
72
|
+
/** Default auto-wait timeout in ms for `waitUntil`/`waitFor*` (wdio default 5000). */
|
|
73
|
+
waitforTimeout?: number;
|
|
74
|
+
/** Stop the run after N failures; 0 = never bail (wdio default 0). */
|
|
75
|
+
bail?: number;
|
|
76
|
+
/** WebdriverIO log level (wdio default "info"). */
|
|
77
|
+
logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "silent";
|
|
46
78
|
}
|
|
47
|
-
export interface NativeProofConfig<
|
|
79
|
+
export interface NativeProofConfig<Ctx = unknown> extends RunnerConfig {
|
|
48
80
|
/** The app under test (from `defineApp`). */
|
|
49
|
-
app: App<
|
|
81
|
+
app: App<Ctx>;
|
|
50
82
|
}
|
|
51
83
|
/** Identity helper for typed config + editor autocomplete (mirrors Playwright's `defineConfig`). */
|
|
52
|
-
export declare function defineConfig<
|
|
84
|
+
export declare function defineConfig<Ctx>(config: NativeProofConfig<Ctx>): NativeProofConfig<Ctx>;
|
|
53
85
|
/** Selection inputs (from the CLI / env) used to resolve the active project + connection. */
|
|
54
86
|
export interface RunnerEnv {
|
|
55
87
|
platform?: string;
|
|
@@ -62,9 +94,7 @@ export interface RunnerEnv {
|
|
|
62
94
|
/** Pick the project by explicit name, else by platform, else the first one. */
|
|
63
95
|
export declare function resolveProject(config: RunnerConfig, env?: RunnerEnv): DeviceProject;
|
|
64
96
|
/**
|
|
65
|
-
* Translate an NativeProof config into a WebdriverIO `config` object.
|
|
66
|
-
* absolute against `cwd` (the project root) because the synthesised config is loaded from
|
|
67
|
-
* inside `node_modules`, so a relative glob would resolve against the wrong directory.
|
|
97
|
+
* Translate an NativeProof config into a WebdriverIO `config` object.
|
|
68
98
|
*/
|
|
69
99
|
export declare function buildWdioConfig(config: RunnerConfig, env?: RunnerEnv, cwd?: string): Record<string, unknown>;
|
|
70
100
|
/** Find an `nativeproof.config.*` in `dir`, or null. `exists` is injectable for testing. */
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { captureState, failureEvidenceName } from "./evidence.js";
|
|
4
|
+
/**
|
|
5
|
+
* The standard capabilities for a platform, so a consumer doesn't restate the same
|
|
6
|
+
* `platformName` / `automationName` in every project. Android → UiAutomator2, iOS → XCUITest
|
|
7
|
+
* (the canonical Appium drivers). A project's own `capabilities` override these.
|
|
8
|
+
*/
|
|
9
|
+
export function defaultCapabilities(platform) {
|
|
10
|
+
return platform === "android"
|
|
11
|
+
? { platformName: "Android", "appium:automationName": "UiAutomator2" }
|
|
12
|
+
: { platformName: "iOS", "appium:automationName": "XCUITest" };
|
|
13
|
+
}
|
|
4
14
|
/** Identity helper for typed config + editor autocomplete (mirrors Playwright's `defineConfig`). */
|
|
5
15
|
export function defineConfig(config) {
|
|
6
16
|
return config;
|
|
@@ -24,23 +34,34 @@ export function resolveProject(config, env = {}) {
|
|
|
24
34
|
return first;
|
|
25
35
|
}
|
|
26
36
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
37
|
+
* Resolve the spec globs (absolute) for a run, in precedence order: an explicit `--spec` override
|
|
38
|
+
* (comma-separated allowed), else the active project's own `specs`, else the top-level
|
|
39
|
+
* `testDir`/`testMatch`. Absolute against `cwd` because the synthesised config is loaded from inside
|
|
40
|
+
* `node_modules`, so a relative glob would resolve against the wrong directory.
|
|
30
41
|
*/
|
|
31
|
-
|
|
32
|
-
const
|
|
42
|
+
function resolveSpecs(config, project, env, cwd) {
|
|
43
|
+
const abs = (glob) => path.resolve(cwd, glob);
|
|
44
|
+
if (env.spec)
|
|
45
|
+
return env.spec.split(",").map((glob) => abs(glob.trim()));
|
|
46
|
+
if (project.specs && project.specs.length > 0)
|
|
47
|
+
return project.specs.map(abs);
|
|
33
48
|
const testDir = config.testDir ?? "tests";
|
|
34
49
|
const testMatch = config.testMatch ?? "**/*.spec.ts";
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
return [abs(`${testDir}/${testMatch}`)];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Translate an NativeProof config into a WebdriverIO `config` object.
|
|
54
|
+
*/
|
|
55
|
+
export function buildWdioConfig(config, env = {}, cwd = process.cwd()) {
|
|
56
|
+
const project = resolveProject(config, env);
|
|
57
|
+
const wdio = {
|
|
37
58
|
runner: "local",
|
|
38
59
|
hostname: env.appiumHost ?? config.appium?.host ?? "127.0.0.1",
|
|
39
60
|
port: env.appiumPort ?? config.appium?.port ?? 4723,
|
|
40
61
|
path: env.appiumPath ?? config.appium?.path ?? "/wd/hub",
|
|
41
|
-
specs,
|
|
62
|
+
specs: resolveSpecs(config, project, env, cwd),
|
|
42
63
|
maxInstances: 1,
|
|
43
|
-
capabilities: [project.capabilities],
|
|
64
|
+
capabilities: [{ ...defaultCapabilities(project.platform), ...project.capabilities }],
|
|
44
65
|
framework: "mocha",
|
|
45
66
|
reporters: ["spec"],
|
|
46
67
|
mochaOpts: { ui: "bdd", timeout: config.mochaTimeout ?? 240_000 },
|
|
@@ -54,6 +75,19 @@ export function buildWdioConfig(config, env = {}, cwd = process.cwd()) {
|
|
|
54
75
|
await captureState(failureEvidenceName(test)).catch(() => { });
|
|
55
76
|
},
|
|
56
77
|
};
|
|
78
|
+
// Optional WebdriverIO tuning — forwarded only when the consumer set it, so wdio's defaults apply
|
|
79
|
+
// otherwise (real emulators/simulators often need longer connection/wait timeouts than the defaults).
|
|
80
|
+
if (config.connectionRetryTimeout !== undefined)
|
|
81
|
+
wdio.connectionRetryTimeout = config.connectionRetryTimeout;
|
|
82
|
+
if (config.connectionRetryCount !== undefined)
|
|
83
|
+
wdio.connectionRetryCount = config.connectionRetryCount;
|
|
84
|
+
if (config.waitforTimeout !== undefined)
|
|
85
|
+
wdio.waitforTimeout = config.waitforTimeout;
|
|
86
|
+
if (config.bail !== undefined)
|
|
87
|
+
wdio.bail = config.bail;
|
|
88
|
+
if (config.logLevel !== undefined)
|
|
89
|
+
wdio.logLevel = config.logLevel;
|
|
90
|
+
return wdio;
|
|
57
91
|
}
|
|
58
92
|
const CONFIG_NAMES = [
|
|
59
93
|
"nativeproof.config.ts",
|
package/dist/harness.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { App
|
|
1
|
+
import type { App } from "./app.js";
|
|
2
2
|
import { expect } from "./expect.js";
|
|
3
|
-
import type { MockBackend } from "./mock.js";
|
|
4
3
|
/**
|
|
5
4
|
* `createHarness(app)` — the Playwright `@playwright/test` pattern.
|
|
6
5
|
*
|
|
@@ -19,19 +18,28 @@ import type { MockBackend } from "./mock.js";
|
|
|
19
18
|
* });
|
|
20
19
|
* ```
|
|
21
20
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Parameterised by the *resolved* session context `Ctx`, not by the screens type `S`. When a
|
|
23
|
+
* project does `export const { test } = createHarness(app)` and a spec in another file imports
|
|
24
|
+
* `test`, TS computes the portable exported type — and if the harness were parameterised by
|
|
25
|
+
* `S extends ScreenFactories<M>`, it would write the **constraint** (`ScreenFactories<M>`, whose
|
|
26
|
+
* screens return `unknown`) instead of the concrete `S`, so every imported behaviour's context
|
|
27
|
+
* would be `unknown`. `Ctx` is already the concrete object (`{ driver; mock; …screens }`), so the
|
|
28
|
+
* screen return types survive the boundary and behaviours stay fully typed.
|
|
29
|
+
*/
|
|
30
|
+
export interface HarnessTest<Ctx> {
|
|
31
|
+
(name: string, body: (context: Ctx) => void | Promise<void>): void;
|
|
24
32
|
/** Open a scenario block for the default role. */
|
|
25
33
|
describe(title: string, body: () => void): void;
|
|
26
34
|
/** Open a scenario block for a specific role (e.g. "member" / "guest"). */
|
|
27
35
|
describe(title: string, role: string, body: () => void): void;
|
|
28
36
|
/** Run before each behaviour in the open scenario, with the session context injected. */
|
|
29
|
-
beforeEach(body: (context:
|
|
37
|
+
beforeEach(body: (context: Ctx) => void | Promise<void>): void;
|
|
30
38
|
/** Run after each behaviour in the open scenario, with the session context injected. */
|
|
31
|
-
afterEach(body: (context:
|
|
39
|
+
afterEach(body: (context: Ctx) => void | Promise<void>): void;
|
|
32
40
|
}
|
|
33
|
-
export interface Harness<
|
|
34
|
-
test: HarnessTest<
|
|
41
|
+
export interface Harness<Ctx> {
|
|
42
|
+
test: HarnessTest<Ctx>;
|
|
35
43
|
expect: typeof expect;
|
|
36
44
|
}
|
|
37
|
-
export declare function createHarness<
|
|
45
|
+
export declare function createHarness<Ctx>(app: App<Ctx>): Harness<Ctx>;
|
package/dist/mock.d.ts
CHANGED
|
@@ -43,12 +43,20 @@ export interface FrameLog {
|
|
|
43
43
|
/** Every frame observed so far, in order, both directions. */
|
|
44
44
|
frames(): Promise<readonly MockFrame[]>;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The minimum a mock must provide to drive a {@link defineApp} session: observable frames plus a
|
|
48
|
+
* `stop()` the session calls on teardown. `route()` is intentionally NOT required — a session never
|
|
49
|
+
* routes (only a spec does) — so an app whose mock only observes frames and stops can use
|
|
50
|
+
* `defineApp` without implementing `route`. {@link MockBackend} is the full contract (adds `route`).
|
|
51
|
+
*/
|
|
52
|
+
export interface SessionMock extends FrameLog {
|
|
49
53
|
/** Release the backend (stop the server, close sockets). */
|
|
50
54
|
stop(): Promise<void>;
|
|
51
55
|
}
|
|
56
|
+
export interface MockBackend extends SessionMock {
|
|
57
|
+
/** Intercept a path and control its reply. */
|
|
58
|
+
route(path: string): MockRoute;
|
|
59
|
+
}
|
|
52
60
|
/**
|
|
53
61
|
* True if `frame` satisfies every field of `match`: `path` / `type` at the top level,
|
|
54
62
|
* every other key against the frame's payload.
|
package/package.json
CHANGED