@purchasely/cordova-plugin-purchasely 6.0.0 → 6.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/Package.swift +77 -0
- package/package.json +11 -2
- package/plugin.xml +4 -4
- package/src/ios/CDVPurchasely.m +22 -4
- package/www/Purchasely.js +1 -1
- package/__tests__/Purchasely.test.js +0 -1826
- package/example/IOS_8_1_1_TEST_MATRIX.md +0 -25
- package/example/android.sh +0 -10
- package/example/config.xml +0 -27
- package/example/e2e/README.md +0 -44
- package/example/e2e/helpers/driver.js +0 -187
- package/example/e2e/package.json +0 -19
- package/example/e2e/specs/bridge.e2e.js +0 -154
- package/example/e2e/specs/dismiss.e2e.js +0 -57
- package/example/e2e/tools/ci_run_e2e.sh +0 -63
- package/example/e2e/tools/ci_run_e2e_ios.sh +0 -56
- package/example/e2e/wdio.android.conf.js +0 -24
- package/example/e2e/wdio.ios.conf.js +0 -44
- package/example/e2e/wdio.shared.conf.js +0 -28
- package/example/ios.sh +0 -17
- package/example/package-lock.json +0 -859
- package/example/package.json +0 -37
- package/example/www/css/index.css +0 -110
- package/example/www/img/logo.png +0 -0
- package/example/www/index.html +0 -78
- package/example/www/js/index.js +0 -453
- package/purchasely.iml +0 -11
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# cordova-ios 8.1.1 Manual Test Matrix
|
|
2
|
-
|
|
3
|
-
Run this matrix on a physical iOS device or a StoreKit-configured simulator after:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
cd purchasely/example
|
|
7
|
-
./ios.sh
|
|
8
|
-
cordova run ios --device
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Use a Purchasely project with a presentation, a purchasable product, and a restorable
|
|
12
|
-
purchase. Capture the app logs and the callback payloads for every row.
|
|
13
|
-
|
|
14
|
-
| Scenario | Steps | Expected result |
|
|
15
|
-
| --- | --- | --- |
|
|
16
|
-
| Presentation | Trigger the sample presentation. | The configured presentation is displayed using the requested transition. |
|
|
17
|
-
| Purchase | Select a product and complete the StoreKit purchase. | The StoreKit sheet completes, the purchase callback receives the Purchasely outcome, and the active entitlement is updated. |
|
|
18
|
-
| Restore | Use the sample restore action with a previously purchased account. | The restore callback completes and the restored entitlement is available. |
|
|
19
|
-
| Close | Dismiss the presentation through its close control and, separately, through the system back/dismiss gesture when enabled. | The close callback fires once per dismissal with the matching close reason; the final presentation outcome is delivered. |
|
|
20
|
-
| JavaScript callbacks | Repeat presentation, purchase, restore, and close while logging every success, failure, presented, and close-requested callback. | Each callback is received on the JavaScript bridge with its expected payload and no duplicate terminal callback. |
|
|
21
|
-
| Background and foreground | Show a presentation or StoreKit sheet, send the app to the background, then return it to the foreground. | The app remains responsive, the visible flow resumes or completes according to StoreKit, and no callback is lost or duplicated. |
|
|
22
|
-
|
|
23
|
-
This matrix is runtime validation only. The CI job verifies deterministic installation,
|
|
24
|
-
CocoaPods workspace build, and an unsigned archive; it cannot validate StoreKit or
|
|
25
|
-
backend-configured presentation behavior.
|
package/example/android.sh
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
cordova plugin remove @purchasely/cordova-plugin-purchasely
|
|
4
|
-
cordova plugin remove @purchasely/cordova-plugin-purchasely-google
|
|
5
|
-
cordova plugin remove cordova-plugin-purchasely
|
|
6
|
-
cordova plugin remove cordova-plugin-purchasely-google
|
|
7
|
-
cordova platform remove android
|
|
8
|
-
cordova platform add android@latest
|
|
9
|
-
cordova plugin add ../ --link
|
|
10
|
-
cordova plugin add ../../purchasely-google/ --link
|
package/example/config.xml
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<?xml version='1.0' encoding='utf-8'?>
|
|
2
|
-
<widget id="com.purchasely.demo" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
|
3
|
-
<name>HelloCordova</name>
|
|
4
|
-
<description>
|
|
5
|
-
A sample Apache Cordova application that responds to the deviceready event.
|
|
6
|
-
</description>
|
|
7
|
-
<author email="dev@cordova.apache.org" href="http://cordova.io">
|
|
8
|
-
Apache Cordova Team
|
|
9
|
-
</author>
|
|
10
|
-
<content src="index.html" />
|
|
11
|
-
<access origin="*" />
|
|
12
|
-
<allow-intent href="http://*/*" />
|
|
13
|
-
<allow-intent href="https://*/*" />
|
|
14
|
-
<allow-intent href="tel:*" />
|
|
15
|
-
<allow-intent href="sms:*" />
|
|
16
|
-
<allow-intent href="mailto:*" />
|
|
17
|
-
<allow-intent href="geo:*" />
|
|
18
|
-
<platform name="android">
|
|
19
|
-
<allow-intent href="market:*" />
|
|
20
|
-
<preference name="GradlePluginKotlinEnabled" value="true" />
|
|
21
|
-
<preference name="GradlePluginKotlinVersion" value="2.3.21" />
|
|
22
|
-
</platform>
|
|
23
|
-
<platform name="ios">
|
|
24
|
-
<allow-intent href="itms:*" />
|
|
25
|
-
<allow-intent href="itms-apps:*" />
|
|
26
|
-
</platform>
|
|
27
|
-
</widget>
|
package/example/e2e/README.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Purchasely Cordova — End-to-End tests
|
|
2
|
-
|
|
3
|
-
Device-level E2E tests for the example app, driving the **real** Purchasely 6.0 native
|
|
4
|
-
SDK through the Cordova bridge. Built with **Appium + WebdriverIO**:
|
|
5
|
-
|
|
6
|
-
- **WEBVIEW context** — calls `window.Purchasely.*` and asserts the results directly
|
|
7
|
-
(the deterministic `bridge` suite).
|
|
8
|
-
- **NATIVE_APP context** — injects OS-level touches (interceptor / dismiss suites).
|
|
9
|
-
|
|
10
|
-
These mirror the Flutter `integration_test` suite (`E2E_TEST_INDEX.md`) adapted to the
|
|
11
|
-
Cordova imperative API. They are **not** part of the PR-gating `ci.yml`; they run via the
|
|
12
|
-
`E2E Android` / `E2E iOS` workflows (`workflow_dispatch`, nightly `schedule`, and scoped
|
|
13
|
-
`pull_request`).
|
|
14
|
-
|
|
15
|
-
## Suites & gating
|
|
16
|
-
|
|
17
|
-
| Suite | File | Gate | Notes |
|
|
18
|
-
|-------|------|------|-------|
|
|
19
|
-
| bridge | `specs/bridge.e2e.js` | **hard** | anonymous id, allProducts, fetchPresentationForPlacement, synchronize completion, user-attribute round-trip (string/int/boolean), userSubscriptions |
|
|
20
|
-
| dismiss | `specs/dismiss.e2e.js` | best-effort | present placement or default presentation + programmatic close → dismiss outcome + `closeReason` (needs a paywall to render) |
|
|
21
|
-
|
|
22
|
-
Best-effort suites emit `::warning::` on failure and do not fail the job (native/paywall
|
|
23
|
-
rendering is flaky in CI — same policy as the Flutter suite). Each suite retries up to 3×.
|
|
24
|
-
|
|
25
|
-
## Requirements to go green
|
|
26
|
-
|
|
27
|
-
1. The placement `PURCHASELY_E2E_PLACEMENT` (default `ONBOARDING`) must exist on the
|
|
28
|
-
backend the example API key (`www/js/index.js`) points to, for app id
|
|
29
|
-
`com.purchasely.demo`. Override with the env var if your integration placement differs.
|
|
30
|
-
2. Android: an emulator with Google APIs (the workflow uses API 34, `pixel_6`).
|
|
31
|
-
3. iOS: an iPhone 16/15 simulator (the workflow discovers one).
|
|
32
|
-
|
|
33
|
-
## Run locally
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
# Android (emulator already booted, apk already built via `cordova build android`)
|
|
37
|
-
cd purchasely/example/e2e && npm install
|
|
38
|
-
bash ./tools/ci_run_e2e.sh emulator-5554
|
|
39
|
-
|
|
40
|
-
# iOS (simulator booted, app built via `cordova build ios --emulator`)
|
|
41
|
-
bash ./tools/ci_run_e2e_ios.sh <simulator-udid>
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Logs are written to `purchasely/example/e2e/ci-logs/` (uploaded as CI artifacts).
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// Helpers for driving the Purchasely Cordova example app under Appium/WebdriverIO.
|
|
2
|
-
//
|
|
3
|
-
// Two contexts are used:
|
|
4
|
-
// * WEBVIEW — run JS against `window.Purchasely` (the cordova.exec bridge) and read
|
|
5
|
-
// results directly; used for the deterministic "bridge" assertions.
|
|
6
|
-
// * NATIVE_APP — inject OS-level touches (tap purchase button, press back) for the
|
|
7
|
-
// interceptor / dismiss suites.
|
|
8
|
-
|
|
9
|
-
async function switchToWebview() {
|
|
10
|
-
await browser.waitUntil(async () => {
|
|
11
|
-
const contexts = await browser.getContexts();
|
|
12
|
-
return contexts.some((c) => (typeof c === 'string' ? c : c.id).includes('WEBVIEW'));
|
|
13
|
-
}, { timeout: 60000, timeoutMsg: 'WEBVIEW context never appeared' });
|
|
14
|
-
|
|
15
|
-
const contexts = await browser.getContexts();
|
|
16
|
-
const webview = contexts
|
|
17
|
-
.map((c) => (typeof c === 'string' ? c : c.id))
|
|
18
|
-
.find((id) => id.includes('WEBVIEW'));
|
|
19
|
-
await browser.switchContext(webview);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function switchToNative() {
|
|
23
|
-
await browser.switchContext('NATIVE_APP');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Wait until the Purchasely bridge is available (deviceready fired + plugin clobbered).
|
|
27
|
-
async function waitForPurchaselyReady() {
|
|
28
|
-
await switchToWebview();
|
|
29
|
-
await browser.waitUntil(
|
|
30
|
-
async () => browser.execute(() => typeof window.Purchasely !== 'undefined'),
|
|
31
|
-
{ timeout: 60000, timeoutMsg: 'window.Purchasely never became available' }
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Poll a window global until a native callback has populated it, then return it.
|
|
36
|
-
// Resolves { ok:false, error:'timeout' } if nothing arrives in time. We poll a SYNC
|
|
37
|
-
// execute rather than use executeAsync because the iOS WKWebView aborts async-script
|
|
38
|
-
// results almost immediately ("Timed out waiting for asynchronous script result"),
|
|
39
|
-
// which would fail every bridge call on iOS.
|
|
40
|
-
async function pollGlobal(name, timeoutMs) {
|
|
41
|
-
let value;
|
|
42
|
-
try {
|
|
43
|
-
await browser.waitUntil(
|
|
44
|
-
async () => {
|
|
45
|
-
value = await browser.execute(function (n) { return window[n]; }, name);
|
|
46
|
-
return value !== undefined && value !== null;
|
|
47
|
-
},
|
|
48
|
-
{ timeout: timeoutMs, interval: 250, timeoutMsg: name + ' never settled' }
|
|
49
|
-
);
|
|
50
|
-
} catch (e) {
|
|
51
|
-
return { ok: false, error: 'timeout' };
|
|
52
|
-
}
|
|
53
|
-
return value;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Invoke a Purchasely method that takes trailing (success, error) callbacks and resolve
|
|
57
|
-
// with { ok, value } on success or { ok:false, error } on error. `args` are the leading
|
|
58
|
-
// positional arguments before the callbacks. Fires via a sync execute that stashes the
|
|
59
|
-
// settled result on window.__plyResult, then polls for it (see pollGlobal).
|
|
60
|
-
async function callBridge(method, args = [], timeoutMs = 30000) {
|
|
61
|
-
await browser.execute(
|
|
62
|
-
function (method, args) {
|
|
63
|
-
window.__plyResult = undefined;
|
|
64
|
-
var settled = false;
|
|
65
|
-
var finish = function (payload) { if (!settled) { settled = true; window.__plyResult = payload; } };
|
|
66
|
-
try {
|
|
67
|
-
var fn = window.Purchasely[method];
|
|
68
|
-
if (typeof fn !== 'function') { finish({ ok: false, error: 'no such method: ' + method }); return; }
|
|
69
|
-
fn.apply(
|
|
70
|
-
window.Purchasely,
|
|
71
|
-
args.concat([
|
|
72
|
-
function (value) { finish({ ok: true, value: value }); },
|
|
73
|
-
function (error) { finish({ ok: false, error: String(error) }); },
|
|
74
|
-
])
|
|
75
|
-
);
|
|
76
|
-
} catch (e) { finish({ ok: false, error: String(e) }); }
|
|
77
|
-
},
|
|
78
|
-
method,
|
|
79
|
-
args
|
|
80
|
-
);
|
|
81
|
-
return pollGlobal('__plyResult', timeoutMs);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Fire a void Purchasely method that takes NO success callback — the
|
|
85
|
-
// setUserAttributeWith* setters are fire-and-forget on the Cordova bridge
|
|
86
|
-
// (exec(() => {}, defaultError, ...)). callBridge would hang waiting for a success
|
|
87
|
-
// callback these never invoke, so dispatch the call and resolve immediately.
|
|
88
|
-
async function fireBridge(method, args = []) {
|
|
89
|
-
await browser.execute(
|
|
90
|
-
function (method, args) {
|
|
91
|
-
var fn = window.Purchasely[method];
|
|
92
|
-
if (typeof fn === 'function') fn.apply(window.Purchasely, args);
|
|
93
|
-
},
|
|
94
|
-
method,
|
|
95
|
-
args
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Drive the v6 presentation builder (Purchasely.presentation) from WEBVIEW context and
|
|
100
|
-
// resolve with { ok, value } / { ok:false, error } -- same contract as callBridge, since
|
|
101
|
-
// fetchPresentation*/presentPresentation* (and their (args..., success, error) shape that
|
|
102
|
-
// callBridge drives) were removed in favor of the promise-based builder.
|
|
103
|
-
// `source` is 'placement' | 'screen' | 'defaultSource'; `sourceId` is the placement/screen
|
|
104
|
-
// id (omit for 'defaultSource'); `action` is 'preload' or 'display'; `transition` is passed
|
|
105
|
-
// to display() when action is 'display'. Stashes the built request on
|
|
106
|
-
// `window.__plyLastRequest` so a follow-up closeCurrentPresentation()/backCurrentPresentation()
|
|
107
|
-
// call in the same test can drive it (mirrors closePresentation()/backPresentation() acting
|
|
108
|
-
// on the natively-tracked current presentation pre-builder).
|
|
109
|
-
async function callPresentation(source, sourceId, action, transition, timeoutMs = 90000) {
|
|
110
|
-
await browser.execute(
|
|
111
|
-
function (source, sourceId, action, transition) {
|
|
112
|
-
window.__plyResult = undefined;
|
|
113
|
-
var settled = false;
|
|
114
|
-
var finish = function (payload) { if (!settled) { settled = true; window.__plyResult = payload; } };
|
|
115
|
-
try {
|
|
116
|
-
var builder = sourceId
|
|
117
|
-
? window.Purchasely.presentation[source](sourceId)
|
|
118
|
-
: window.Purchasely.presentation[source]();
|
|
119
|
-
var request = builder.build();
|
|
120
|
-
window.__plyLastRequest = request;
|
|
121
|
-
var promise = action === 'preload' ? request.preload() : request.display(transition);
|
|
122
|
-
promise.then(
|
|
123
|
-
function (value) { finish({ ok: true, value: value }); },
|
|
124
|
-
function (error) { finish({ ok: false, error: String(error) }); }
|
|
125
|
-
);
|
|
126
|
-
} catch (e) { finish({ ok: false, error: String(e) }); }
|
|
127
|
-
},
|
|
128
|
-
source,
|
|
129
|
-
sourceId,
|
|
130
|
-
action,
|
|
131
|
-
transition
|
|
132
|
-
);
|
|
133
|
-
return pollGlobal('__plyResult', timeoutMs);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Display a presentation and stash its dismiss outcome on a window global, WITHOUT
|
|
137
|
-
// blocking the WebDriver session. callPresentation() drives display() inside a single
|
|
138
|
-
// executeAsync that only settles at dismiss — but the session is then busy, so the
|
|
139
|
-
// follow-up close() command can never run (deadlock -> script timeout). Fire display()
|
|
140
|
-
// fire-and-forget instead and poll the outcome via awaitDismissOutcome(); the session
|
|
141
|
-
// stays free to send closeCurrentPresentation() in between.
|
|
142
|
-
async function displayPresentation(source, sourceId, transition) {
|
|
143
|
-
await browser.execute(
|
|
144
|
-
function (source, sourceId, transition) {
|
|
145
|
-
window.__plyOutcome = undefined;
|
|
146
|
-
var builder = sourceId
|
|
147
|
-
? window.Purchasely.presentation[source](sourceId)
|
|
148
|
-
: window.Purchasely.presentation[source]();
|
|
149
|
-
var request = builder.build();
|
|
150
|
-
window.__plyLastRequest = request;
|
|
151
|
-
request.display(transition).then(
|
|
152
|
-
function (v) { window.__plyOutcome = { ok: true, value: v }; },
|
|
153
|
-
function (e) { window.__plyOutcome = { ok: false, error: String(e) }; }
|
|
154
|
-
);
|
|
155
|
-
},
|
|
156
|
-
source,
|
|
157
|
-
sourceId,
|
|
158
|
-
transition
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Poll for the dismiss outcome stashed by displayPresentation(). Resolves { ok, value } /
|
|
163
|
-
// { ok:false, error } once display()'s promise settles (i.e. after close()), or
|
|
164
|
-
// { ok:false, error:'timeout' } if no outcome arrives in time.
|
|
165
|
-
async function awaitDismissOutcome(timeoutMs = 30000) {
|
|
166
|
-
return pollGlobal('__plyOutcome', timeoutMs);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Close the presentation driven by the last callPresentation()/displayPresentation()
|
|
170
|
-
// request (WEBVIEW context).
|
|
171
|
-
async function closeCurrentPresentation() {
|
|
172
|
-
return browser.execute(function () {
|
|
173
|
-
if (window.__plyLastRequest) window.__plyLastRequest.close();
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = {
|
|
178
|
-
switchToWebview,
|
|
179
|
-
switchToNative,
|
|
180
|
-
waitForPurchaselyReady,
|
|
181
|
-
callBridge,
|
|
182
|
-
fireBridge,
|
|
183
|
-
callPresentation,
|
|
184
|
-
displayPresentation,
|
|
185
|
-
awaitDismissOutcome,
|
|
186
|
-
closeCurrentPresentation,
|
|
187
|
-
};
|
package/example/e2e/package.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "purchasely-cordova-e2e",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"description": "End-to-end tests for the Purchasely Cordova example app (Appium + WebdriverIO). Drives the real native Purchasely 6.0 SDK inside the WebView (bridge assertions) and injects OS-level touches (interceptor / dismiss).",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test:android": "wdio run ./wdio.android.conf.js",
|
|
8
|
-
"test:ios": "wdio run ./wdio.ios.conf.js"
|
|
9
|
-
},
|
|
10
|
-
"devDependencies": {
|
|
11
|
-
"@wdio/cli": "^9.0.0",
|
|
12
|
-
"@wdio/local-runner": "^9.0.0",
|
|
13
|
-
"@wdio/mocha-framework": "^9.0.0",
|
|
14
|
-
"@wdio/spec-reporter": "^9.0.0",
|
|
15
|
-
"appium": "^2.11.0",
|
|
16
|
-
"appium-uiautomator2-driver": "^3.9.0",
|
|
17
|
-
"appium-xcuitest-driver": "^7.28.0"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// Deterministic Dart<->native bridge assertions (no native taps). HARD gate — these
|
|
2
|
-
// must pass. Mirrors the Flutter E2E_TEST_INDEX suite T1/T3/T5/T6 adapted to the
|
|
3
|
-
// Cordova imperative API.
|
|
4
|
-
const { waitForPurchaselyReady, callBridge, fireBridge, callPresentation } = require('../helpers/driver');
|
|
5
|
-
|
|
6
|
-
const PLACEMENT = process.env.PURCHASELY_E2E_PLACEMENT || 'ONBOARDING';
|
|
7
|
-
|
|
8
|
-
describe('Purchasely bridge (WEBVIEW context)', () => {
|
|
9
|
-
before(async () => {
|
|
10
|
-
await waitForPurchaselyReady();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
// T1 — anonymous user id
|
|
14
|
-
it('getAnonymousUserId returns a non-empty id', async () => {
|
|
15
|
-
const res = await callBridge('getAnonymousUserId');
|
|
16
|
-
expect(res.ok).toBe(true);
|
|
17
|
-
expect(typeof res.value).toBe('string');
|
|
18
|
-
expect(res.value.length).toBeGreaterThan(0);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// T5 — catalog. Store-dependent: the iOS simulator has no StoreKit products configured,
|
|
22
|
-
// so allProducts settles as a clean error there while the Android emulator (Google
|
|
23
|
-
// Billing) returns a list. Assert the bridge round-trips cleanly either way.
|
|
24
|
-
it('allProducts returns a list', async () => {
|
|
25
|
-
const res = await callBridge('allProducts');
|
|
26
|
-
if (res.ok) {
|
|
27
|
-
expect(Array.isArray(res.value)).toBe(true);
|
|
28
|
-
} else {
|
|
29
|
-
expect(typeof res.error).toBe('string');
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// T3 — preload a presentation for a placement (was fetchPresentationForPlacement;
|
|
34
|
-
// now Purchasely.presentation.placement(id).build().preload())
|
|
35
|
-
it('presentation.placement(...).build().preload() returns a presentation object', async () => {
|
|
36
|
-
const res = await callPresentation('placement', PLACEMENT, 'preload');
|
|
37
|
-
// Store-dependent (preload fetches the paywall + its products): tolerate a clean error
|
|
38
|
-
// on the store-less iOS simulator, assert the shape when it resolves.
|
|
39
|
-
if (res.ok) {
|
|
40
|
-
// v6 presentation normalizes screenId as the authoritative identifier.
|
|
41
|
-
expect(res.value === null || typeof res.value === 'object').toBe(true);
|
|
42
|
-
if (res.value) {
|
|
43
|
-
expect(typeof res.value.screenId).toBe('string');
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
expect(typeof res.error).toBe('string');
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// T6 — synchronize now reports completion (v6 change). On a bare emulator/simulator
|
|
51
|
-
// with no billing, exactly one of success/error must fire (no fire-and-forget).
|
|
52
|
-
it('synchronize resolves exactly one callback', async () => {
|
|
53
|
-
const res = await callBridge('synchronize');
|
|
54
|
-
expect(typeof res.ok).toBe('boolean');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// user-attribute round-trip (set then read back). The setters are fire-and-forget on
|
|
58
|
-
// the Cordova bridge (no success callback), so fire them via fireBridge and read back
|
|
59
|
-
// with callBridge; the small pause lets the native set land before the read.
|
|
60
|
-
it('setUserAttributeWithString then userAttribute round-trips', async () => {
|
|
61
|
-
await fireBridge('setUserAttributeWithString', ['e2e_key', 'e2e_value', 'ESSENTIAL']);
|
|
62
|
-
await browser.pause(300);
|
|
63
|
-
const res = await callBridge('userAttribute', ['e2e_key']);
|
|
64
|
-
expect(res.ok).toBe(true);
|
|
65
|
-
expect(res.value).toBe('e2e_value');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// user-attribute round-trip, int variant
|
|
69
|
-
it('setUserAttributeWithInt then userAttribute round-trips', async () => {
|
|
70
|
-
await fireBridge('setUserAttributeWithInt', ['e2e_key_int', 7, 'ESSENTIAL']);
|
|
71
|
-
await browser.pause(300);
|
|
72
|
-
const res = await callBridge('userAttribute', ['e2e_key_int']);
|
|
73
|
-
expect(res.ok).toBe(true);
|
|
74
|
-
expect(res.value).toBe(7);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// user-attribute round-trip, boolean variant (CDV-W-09 fixed: both platforms now
|
|
78
|
-
// return a real JSON boolean, so this asserts strict equality, not just truthiness).
|
|
79
|
-
it('setUserAttributeWithBoolean then userAttribute round-trips', async () => {
|
|
80
|
-
await fireBridge('setUserAttributeWithBoolean', ['e2e_key_bool', true, 'ESSENTIAL']);
|
|
81
|
-
await browser.pause(300);
|
|
82
|
-
const res = await callBridge('userAttribute', ['e2e_key_bool']);
|
|
83
|
-
expect(res.ok).toBe(true);
|
|
84
|
-
expect(res.value).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// userSubscriptions on a fresh anonymous user. Store-dependent: settles as a list on the
|
|
88
|
-
// Android emulator, can settle as a clean error on the store-less iOS simulator.
|
|
89
|
-
it('userSubscriptions returns a list', async () => {
|
|
90
|
-
const res = await callBridge('userSubscriptions');
|
|
91
|
-
if (res.ok) {
|
|
92
|
-
expect(Array.isArray(res.value)).toBe(true);
|
|
93
|
-
} else {
|
|
94
|
-
expect(typeof res.error).toBe('string');
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// T2 — login/logout cycle: isAnonymous flips true -> false -> true. userLogin/userLogout
|
|
99
|
-
// are fire-and-forget on the Cordova bridge, so drive them via fireBridge + verify via
|
|
100
|
-
// the callback-based isAnonymous getter.
|
|
101
|
-
it('isAnonymous flips around userLogin / userLogout', async () => {
|
|
102
|
-
let res = await callBridge('isAnonymous');
|
|
103
|
-
expect(res.ok).toBe(true);
|
|
104
|
-
expect(res.value).toBe(true);
|
|
105
|
-
|
|
106
|
-
await fireBridge('userLogin', ['cordova_e2e_user']);
|
|
107
|
-
await browser.pause(500);
|
|
108
|
-
res = await callBridge('isAnonymous');
|
|
109
|
-
expect(res.value).toBe(false);
|
|
110
|
-
|
|
111
|
-
await fireBridge('userLogout', []);
|
|
112
|
-
await browser.pause(500);
|
|
113
|
-
res = await callBridge('isAnonymous');
|
|
114
|
-
expect(res.value).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// T4 — dynamic offerings list (may be empty on a bare emulator)
|
|
118
|
-
it('getDynamicOfferings returns a list', async () => {
|
|
119
|
-
const res = await callBridge('getDynamicOfferings');
|
|
120
|
-
expect(res.ok).toBe(true);
|
|
121
|
-
expect(Array.isArray(res.value)).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// T18 — dynamic offerings set / get / remove / clear round-trip (list stays an array,
|
|
125
|
-
// no throw). The mutators are fire-and-forget; getDynamicOfferings reads back.
|
|
126
|
-
it('dynamic offerings set / remove / clear round-trip', async () => {
|
|
127
|
-
await fireBridge('setDynamicOffering', ['e2e_ref', 'e2e_plan', null, 0]);
|
|
128
|
-
await browser.pause(300);
|
|
129
|
-
let res = await callBridge('getDynamicOfferings');
|
|
130
|
-
expect(Array.isArray(res.value)).toBe(true);
|
|
131
|
-
|
|
132
|
-
await fireBridge('removeDynamicOffering', ['e2e_ref']);
|
|
133
|
-
await fireBridge('clearDynamicOfferings', []);
|
|
134
|
-
await browser.pause(300);
|
|
135
|
-
res = await callBridge('getDynamicOfferings');
|
|
136
|
-
expect(res.ok).toBe(true);
|
|
137
|
-
expect(Array.isArray(res.value)).toBe(true);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// T16 — increment / decrement a numeric user attribute
|
|
141
|
-
it('increment / decrement a numeric user attribute', async () => {
|
|
142
|
-
await fireBridge('setUserAttributeWithInt', ['e2e_counter', 5, 'ESSENTIAL']);
|
|
143
|
-
await browser.pause(300);
|
|
144
|
-
await fireBridge('incrementUserAttribute', ['e2e_counter', 3]);
|
|
145
|
-
await browser.pause(300);
|
|
146
|
-
let res = await callBridge('userAttribute', ['e2e_counter']);
|
|
147
|
-
expect(res.value).toBe(8);
|
|
148
|
-
|
|
149
|
-
await fireBridge('decrementUserAttribute', ['e2e_counter', 2]);
|
|
150
|
-
await browser.pause(300);
|
|
151
|
-
res = await callBridge('userAttribute', ['e2e_counter']);
|
|
152
|
-
expect(res.value).toBe(6);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// Presentation display + dismiss outcome. BEST-EFFORT (non-blocking in CI): depends on
|
|
2
|
-
// a paywall actually rendering for the configured placement against the real backend.
|
|
3
|
-
// Mirrors E2E_TEST_INDEX T8/T12 adapted to the Cordova imperative API.
|
|
4
|
-
const {
|
|
5
|
-
waitForPurchaselyReady,
|
|
6
|
-
displayPresentation,
|
|
7
|
-
awaitDismissOutcome,
|
|
8
|
-
closeCurrentPresentation,
|
|
9
|
-
} = require('../helpers/driver');
|
|
10
|
-
|
|
11
|
-
const PLACEMENT = process.env.PURCHASELY_E2E_PLACEMENT || 'ONBOARDING';
|
|
12
|
-
|
|
13
|
-
describe('Presentation dismiss outcome', () => {
|
|
14
|
-
before(async () => {
|
|
15
|
-
await waitForPurchaselyReady();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// T8/T12 — display() resolves with the per-presentation dismiss outcome (was the
|
|
19
|
-
// presentPresentationForPlacement success callback; closePresentation() is now
|
|
20
|
-
// request.close(), driven here via closeCurrentPresentation()).
|
|
21
|
-
// Present a placement, close it programmatically, and assert the outcome fires with a
|
|
22
|
-
// closeReason.
|
|
23
|
-
it('presentation.placement(...).build().display() + request.close() delivers a dismiss outcome', async () => {
|
|
24
|
-
// Fire display() fire-and-forget (its promise settles at dismiss and would otherwise
|
|
25
|
-
// block the session so close() could never run).
|
|
26
|
-
await displayPresentation('placement', PLACEMENT, 'fullScreen');
|
|
27
|
-
|
|
28
|
-
// Give the paywall time to render, then close it programmatically from the bridge.
|
|
29
|
-
await browser.pause(6000);
|
|
30
|
-
await closeCurrentPresentation();
|
|
31
|
-
|
|
32
|
-
const outcome = await awaitDismissOutcome(30000);
|
|
33
|
-
expect(outcome.ok).toBe(true);
|
|
34
|
-
expect(outcome.value).toBeDefined();
|
|
35
|
-
// v6 outcome carries a closeReason; programmatic close => 'programmatic' (Android).
|
|
36
|
-
if (outcome.value && outcome.value.closeReason) {
|
|
37
|
-
expect(typeof outcome.value.closeReason).toBe('string');
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// v6 default (audience-targeted) presentation: same shape as the placement flow above,
|
|
42
|
-
// just with no placement/screen id (was presentPresentationForDefault). Best-effort:
|
|
43
|
-
// depends on a default audience being configured on the backend for this app id.
|
|
44
|
-
it('presentation.defaultSource().build().display() + request.close() delivers a dismiss outcome', async () => {
|
|
45
|
-
await displayPresentation('defaultSource', null, 'fullScreen');
|
|
46
|
-
|
|
47
|
-
await browser.pause(6000);
|
|
48
|
-
await closeCurrentPresentation();
|
|
49
|
-
|
|
50
|
-
const outcome = await awaitDismissOutcome(30000);
|
|
51
|
-
expect(outcome.ok).toBe(true);
|
|
52
|
-
expect(outcome.value).toBeDefined();
|
|
53
|
-
if (outcome.value && outcome.value.closeReason) {
|
|
54
|
-
expect(typeof outcome.value.closeReason).toBe('string');
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Android E2E runner. Assumes:
|
|
3
|
-
# * an emulator is already booted (serial passed as $1, e.g. emulator-5554)
|
|
4
|
-
# * the debug apk has been built (cordova build android)
|
|
5
|
-
# * node deps for purchasely/example/e2e are installed
|
|
6
|
-
#
|
|
7
|
-
# Gating mirrors the Flutter suite: the deterministic `bridge` suite HARD-gates the job;
|
|
8
|
-
# `dismiss` (needs a paywall to render against the real backend) is BEST-EFFORT and only
|
|
9
|
-
# emits ::warning:: on failure.
|
|
10
|
-
set -uo pipefail
|
|
11
|
-
|
|
12
|
-
SERIAL="${1:-emulator-5554}"
|
|
13
|
-
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
|
14
|
-
LOGDIR="$HERE/ci-logs"
|
|
15
|
-
mkdir -p "$LOGDIR"
|
|
16
|
-
export ANDROID_SERIAL="$SERIAL"
|
|
17
|
-
|
|
18
|
-
# Appium 2 loads drivers from APPIUM_HOME (~/.appium), not node_modules, so a fresh CI
|
|
19
|
-
# checkout has none installed even though appium-uiautomator2-driver is a devDependency.
|
|
20
|
-
# Register it (idempotent; a no-op locally where it is already installed).
|
|
21
|
-
echo "== Ensuring uiautomator2 driver is installed =="
|
|
22
|
-
npx appium driver install uiautomator2 2>/dev/null || true
|
|
23
|
-
|
|
24
|
-
echo "== Starting Appium =="
|
|
25
|
-
# --allow-insecure=chromedriver_autodownload lets the uiautomator2 driver fetch the
|
|
26
|
-
# Chromedriver matching the Cordova WebView's Chrome version on demand. Without it,
|
|
27
|
-
# switching to the WEBVIEW context fails ("No Chromedriver found that can automate
|
|
28
|
-
# Chrome 'X'"), which breaks every bridge test in its `before all` hook.
|
|
29
|
-
# Detach Appium's stdout/stderr (it logs to --log anyway): if it keeps the script's
|
|
30
|
-
# output pipe open, the android-emulator-runner action hangs after the tests finish
|
|
31
|
-
# instead of returning.
|
|
32
|
-
npx appium --allow-insecure=uiautomator2:chromedriver_autodownload \
|
|
33
|
-
--log "$LOGDIR/appium-android.log" --log-level info >/dev/null 2>&1 &
|
|
34
|
-
APPIUM_PID=$!
|
|
35
|
-
trap 'kill $APPIUM_PID 2>/dev/null || true' EXIT
|
|
36
|
-
# Wait for Appium to accept connections.
|
|
37
|
-
for i in $(seq 1 30); do
|
|
38
|
-
curl -sf http://127.0.0.1:4723/status >/dev/null 2>&1 && break
|
|
39
|
-
sleep 1
|
|
40
|
-
done
|
|
41
|
-
|
|
42
|
-
run_suite() { # $1 = spec glob, $2 = hard|soft
|
|
43
|
-
local spec="$1" gate="$2" tries=3 n=1
|
|
44
|
-
while [ $n -le $tries ]; do
|
|
45
|
-
echo "== [$gate] $spec (attempt $n/$tries) =="
|
|
46
|
-
if PURCHASELY_E2E_SPEC="$spec" npx wdio run ./wdio.android.conf.js --spec "$spec" 2>&1 | tee "$LOGDIR/wdio-$(basename "$spec").log"; then
|
|
47
|
-
return 0
|
|
48
|
-
fi
|
|
49
|
-
n=$((n+1))
|
|
50
|
-
done
|
|
51
|
-
if [ "$gate" = "hard" ]; then
|
|
52
|
-
echo "::error::E2E suite failed (hard gate): $spec"
|
|
53
|
-
return 1
|
|
54
|
-
fi
|
|
55
|
-
echo "::warning::E2E suite failed (best-effort): $spec"
|
|
56
|
-
return 0
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
cd "$HERE"
|
|
60
|
-
rc=0
|
|
61
|
-
run_suite "./specs/bridge.e2e.js" hard || rc=1
|
|
62
|
-
run_suite "./specs/dismiss.e2e.js" soft || true
|
|
63
|
-
exit $rc
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# iOS E2E runner. Assumes:
|
|
3
|
-
# * a simulator is already booted (udid passed as $1)
|
|
4
|
-
# * the simulator .app has been built (cordova build ios --emulator)
|
|
5
|
-
# * node deps for purchasely/example/e2e are installed
|
|
6
|
-
#
|
|
7
|
-
# Same gating as Android: `bridge` HARD-gates; `dismiss` is BEST-EFFORT.
|
|
8
|
-
set -uo pipefail
|
|
9
|
-
|
|
10
|
-
UDID="${1:-booted}"
|
|
11
|
-
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
|
12
|
-
LOGDIR="$HERE/ci-logs"
|
|
13
|
-
mkdir -p "$LOGDIR"
|
|
14
|
-
export PURCHASELY_E2E_UDID="$UDID"
|
|
15
|
-
|
|
16
|
-
# Appium 2 loads drivers from APPIUM_HOME (~/.appium), not node_modules, so make sure the
|
|
17
|
-
# xcuitest driver is registered (idempotent; a no-op where it is already installed).
|
|
18
|
-
echo "== Ensuring xcuitest driver is installed =="
|
|
19
|
-
npx appium driver install xcuitest 2>/dev/null || true
|
|
20
|
-
|
|
21
|
-
echo "== Starting Appium =="
|
|
22
|
-
# Detach Appium's stdout/stderr (it logs to --log anyway) so it can't hold the runner's
|
|
23
|
-
# output pipe open after the tests finish.
|
|
24
|
-
npx appium --log "$LOGDIR/appium-ios.log" --log-level info >/dev/null 2>&1 &
|
|
25
|
-
APPIUM_PID=$!
|
|
26
|
-
trap 'kill $APPIUM_PID 2>/dev/null || true' EXIT
|
|
27
|
-
for i in $(seq 1 30); do
|
|
28
|
-
curl -sf http://127.0.0.1:4723/status >/dev/null 2>&1 && break
|
|
29
|
-
sleep 1
|
|
30
|
-
done
|
|
31
|
-
|
|
32
|
-
run_suite() { # $1 = spec, $2 = hard|soft
|
|
33
|
-
# More retries than Android: WebDriverAgent's cold first build (~10-13 min) can outlast a
|
|
34
|
-
# few session-creation attempts, and the hard-gate bridge spec runs first — extra tries let
|
|
35
|
-
# WDA finish building (it is then cached via derivedDataPath, so later specs are instant).
|
|
36
|
-
local spec="$1" gate="$2" tries="${E2E_TRIES:-6}" n=1
|
|
37
|
-
while [ $n -le $tries ]; do
|
|
38
|
-
echo "== [$gate] $spec (attempt $n/$tries) =="
|
|
39
|
-
if npx wdio run ./wdio.ios.conf.js --spec "$spec" 2>&1 | tee "$LOGDIR/wdio-$(basename "$spec").log"; then
|
|
40
|
-
return 0
|
|
41
|
-
fi
|
|
42
|
-
n=$((n+1))
|
|
43
|
-
done
|
|
44
|
-
if [ "$gate" = "hard" ]; then
|
|
45
|
-
echo "::error::E2E suite failed (hard gate): $spec"
|
|
46
|
-
return 1
|
|
47
|
-
fi
|
|
48
|
-
echo "::warning::E2E suite failed (best-effort): $spec"
|
|
49
|
-
return 0
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
cd "$HERE"
|
|
53
|
-
rc=0
|
|
54
|
-
run_suite "./specs/bridge.e2e.js" hard || rc=1
|
|
55
|
-
run_suite "./specs/dismiss.e2e.js" soft || true
|
|
56
|
-
exit $rc
|