Skip to content

Commit 4cfa8f3

Browse files
authored
test: e2e replay test using Maestro (js) (#4277)
1 parent c10f8da commit 4cfa8f3

13 files changed

+145
-69
lines changed

dev-packages/e2e-tests/maestro/captureException.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
appId: ${APP_ID}
2+
jsEngine: graaljs
23
---
34
- runFlow: utils/launchTestAppClear.yml
45
- tapOn: "Capture Exception"

dev-packages/e2e-tests/maestro/captureMessage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
appId: ${APP_ID}
2+
jsEngine: graaljs
23
---
34
- runFlow: utils/launchTestAppClear.yml
45
- tapOn: "Capture Message"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
appId: ${APP_ID}
2+
jsEngine: graaljs
3+
---
4+
- runFlow:
5+
file: utils/launchTestAppClear.yml
6+
env:
7+
replaysOnErrorSampleRate: 1.0
8+
- tapOn: "Capture Exception"
9+
- runFlow: utils/assertEventIdVisible.yml
10+
- runFlow:
11+
file: utils/assertReplay.yml
12+
when:
13+
platform: iOS

dev-packages/e2e-tests/maestro/captureUnhandledPromiseRejection.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
appId: ${APP_ID}
2+
jsEngine: graaljs
23
---
34
- runFlow: utils/launchTestAppClear.yml
45
- tapOn: "Unhandled Promise Rejection"

dev-packages/e2e-tests/maestro/close.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
appId: ${APP_ID}
2+
jsEngine: graaljs
23
---
34
- runFlow: utils/launchTestAppClear.yml
45
- tapOn: "Close"

dev-packages/e2e-tests/maestro/crash.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
appId: ${APP_ID}
2+
jsEngine: graaljs
23
---
34
- runFlow: utils/launchTestAppClear.yml
45
- tapOn: "Crash"

dev-packages/e2e-tests/maestro/utils/assertEventIdVisible.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
appId: ${APP_ID}
2+
jsEngine: graaljs
23
---
34
- extendedWaitUntil:
45
visible:
@@ -8,3 +9,12 @@ appId: ${APP_ID}
89
- copyTextFrom:
910
id: "eventId"
1011
- assertTrue: ${maestro.copiedText}
12+
13+
- runScript:
14+
file: sentryApi.js
15+
env:
16+
fetch: event
17+
id: ${maestro.copiedText}
18+
sentryAuthToken: ${SENTRY_AUTH_TOKEN}
19+
20+
- assertTrue: ${output.eventId == maestro.copiedText}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
appId: ${APP_ID}
2+
jsEngine: graaljs
3+
---
4+
- extendedWaitUntil:
5+
visible:
6+
id: "eventId"
7+
timeout: 60_000 # 60 seconds
8+
9+
- copyTextFrom:
10+
id: "eventId"
11+
- assertTrue: ${maestro.copiedText}
12+
13+
- runScript:
14+
file: sentryApi.js
15+
env:
16+
fetch: replay
17+
eventId: ${maestro.copiedText}
18+
sentryAuthToken: ${SENTRY_AUTH_TOKEN}
19+
20+
- assertTrue: ${output.replayId}
21+
- assertTrue: ${output.replayDuration}
22+
- assertTrue: ${output.replaySegments}
23+
- assertTrue: ${output.replayCodec == "ftypmp42"}

dev-packages/e2e-tests/maestro/utils/launchTestAppClear.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
appId: ${APP_ID}
2+
jsEngine: graaljs
23
---
4+
# Ensure the app is killed, otherwise we may see "INTERNAL: UiAutomation not connected" errors on Android.
5+
# They seem to be casued by a previous test case running for a long time without UI interactions (e.g. runScript).
6+
- killApp
7+
38
- launchApp:
49
clearState: true
510
arguments:
611
sentryAuthToken: ${SENTRY_AUTH_TOKEN}
12+
replaysOnErrorSampleRate: ${replaysOnErrorSampleRate}
713

814
- extendedWaitUntil:
915
visible: "E2E Tests Ready"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const baseUrl = 'https://sentry.io/api/0/projects/sentry-sdks/sentry-react-native';
2+
3+
const RETRY_COUNT = 600;
4+
const RETRY_INTERVAL = 1000;
5+
const requestHeaders = { 'Authorization': `Bearer ${sentryAuthToken}` }
6+
7+
function sleep(ms) {
8+
// TODO reach out to Maestro & GrallJS via GitHub issues.
9+
// return new Promise(resolve => setTimeout(resolve, ms));
10+
// Instead, we need to do a busy wait.
11+
const until = Date.now() + ms;
12+
while (Date.now() < until) {
13+
// console.log(`Sleeping for ${until - Date.now()} ms`);
14+
try {
15+
http.get('http://127.0.0.1:1');
16+
} catch (e) {
17+
// Ignore
18+
}
19+
}
20+
}
21+
22+
function fetchFromSentry(url) {
23+
console.log(`Fetching ${url}`);
24+
let retries = 0;
25+
const shouldRetry = (response) => {
26+
switch (response.status) {
27+
case 200:
28+
return false;
29+
case 403:
30+
throw new Error(`Could not fetch ${url}: ${response.status} | ${response.body}`);
31+
default:
32+
if (retries++ < RETRY_COUNT) {
33+
console.log(`Request failed (HTTP ${response.status}), retrying: ${retries}/${RETRY_COUNT}`);
34+
return true;
35+
}
36+
throw new Error(`Could not fetch ${url} within retry limit: ${response.status} | ${response.body}`);
37+
}
38+
}
39+
40+
while (true) {
41+
const response = http.get(url, { headers: requestHeaders })
42+
if (!shouldRetry(response)) {
43+
console.log(`Received HTTP ${response.status}: body length ${response.body.length}`);
44+
return response.body;
45+
}
46+
sleep(RETRY_INTERVAL);
47+
}
48+
};
49+
50+
function setOutput(data) {
51+
for (const [key, value] of Object.entries(data)) {
52+
console.log(`Setting output.${key} = '${value}'`);
53+
output[key] = value;
54+
}
55+
}
56+
57+
// Note: "fetch", "id", "eventId", etc. are script inputs, see for example assertEventIdIVisible.yml
58+
switch (fetch) {
59+
case 'event': {
60+
const data = json(fetchFromSentry(`${baseUrl}/events/${id}/json/`));
61+
setOutput({ eventId: data.event_id });
62+
break;
63+
}
64+
case 'replay': {
65+
const event = json(fetchFromSentry(`${baseUrl}/events/${eventId}/json/`));
66+
const replayId = event._dsc.replay_id.replace(/\-/g, '');
67+
const replay = json(fetchFromSentry(`${baseUrl}/replays/${replayId}/`));
68+
const segment = fetchFromSentry(`${baseUrl}/replays/${replayId}/videos/0/`);
69+
70+
setOutput({
71+
replayId: replay.data.id,
72+
replayDuration: replay.data.duration,
73+
replaySegments: replay.data.count_segments,
74+
replayCodec: segment.slice(4, 12)
75+
});
76+
break;
77+
}
78+
default:
79+
throw new Error(`Unknown "fetch" value: '${fetch}'`);
80+
}

0 commit comments

Comments
 (0)