Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/E2ETestApp/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<!-- gwpAsanMode: Android 14 samples allocations with GWP-ASan in every app, and on the x86_64
emulator its stack recorder (android_unsafe_frame_pointer_chase) segfaults on a Hermes
free right after launch, which Detox reports as "The app has unexpectedly disconnected". -->
<application
android:name=".MainApplication"
android:label="@string/app_name"
Expand All @@ -15,6 +18,7 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="${usesCleartextTraffic}"
android:networkSecurityConfig="@xml/network_security_config"
android:gwpAsanMode="never"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
Expand Down
50 changes: 43 additions & 7 deletions examples/E2ETestApp/e2e/mock-server/MockServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import { createServer } from 'net';

declare const process: { env: Record<string, string | undefined> };

/** A port free on the host's IPv4 and IPv6 loopback: `::` listens on both (dual stack). */
const freePort = () =>
new Promise<number>((resolve, reject) => {
const server = createServer();
server.once('error', reject);
server.listen({ host: '::', port: 0 }, () => {
const address = server.address();
server.close(() =>
address && typeof address === 'object'
? resolve(address.port)
: reject(new Error('No free port')),
);
});
});

/**
* Client for `stream-chat-test-mock-server`. The driver (`driver.rb`) spawns one mock server per
* test on its own port; `start` asks it for one and `stop` shuts it down.
Expand Down Expand Up @@ -27,13 +44,32 @@ export class MockServer {
return this.url.replace('localhost', host);
}

/** Never retried: every call to the driver's `/start` spawns a new server on a new port. */
async start(testName: string): Promise<void> {
const response = await this.execute(
`${MockServer.driverUrl}/start/${encodeURIComponent(testName)}?api_version=v2&timestamps=ns`,
);
this.port = (await response.text()).trim();
await this.waitUntilReady();
/**
* Every call to the driver's `/start` spawns a new server, so a start is only retried after the
* server failed to answer, and always on a new port.
*
* The port is picked here rather than by the driver: on CI the driver's probe twice handed out a
* port the server then never answered on (41215, an hour apart, nightly 43138494994). This probe
* also checks IPv6, which `localhost` may resolve to.
*/
async start(testName: string, attempts = 3): Promise<void> {
for (let attempt = 1; ; attempt++) {
const port = await freePort();
const response = await this.execute(
`${MockServer.driverUrl}/start/${encodeURIComponent(testName)}` +
`?api_version=v2&timestamps=ns&port=${port}`,
);
this.port = (await response.text()).trim();
try {
await this.waitUntilReady();
return;
} catch (error) {
await this.stop();
if (attempt >= attempts) {
throw error;
}
}
}
}

async stop(): Promise<void> {
Expand Down
6 changes: 6 additions & 0 deletions examples/E2ETestApp/e2e/robots/UserRobot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,12 @@ export class UserRobot extends Robot {
.perform(async () => {
await (await waitToAppearAndClick(CreatePoll.fields, { index: 0 })).replaceText(question);
for (const [index, option] of options.entries()) {
if (device.getPlatform() === 'android') {
// The keyboard, often already open from the composer, covers the option field that
// appears below the last filled one, so it never counts as visible. A tap outside
// the fields hides it (see below).
await element(CreatePoll.questionLabel).tap();
}
await (
await waitToAppearAndClick(CreatePoll.fields, { index: index + 1 })
).replaceText(option);
Expand Down
26 changes: 26 additions & 0 deletions examples/E2ETestApp/e2e/robots/UserRobotMessageListAsserts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ declare module './UserRobot' {
}
}

/**
* Waits until `selector` has been hidden for `stableMs` in a row. A single check can land before an
* element briefly shows: on iOS 17 the scroll-to-bottom button flashes while the list catches up
* with a message that just arrived, so "hidden" read once was a coin toss (nightly 43138494994).
*/
const waitUntilStaysHidden = async (
selector: Detox.NativeMatcher,
timeout: number,
stableMs = 1000,
) => {
const deadline = Date.now() + timeout;
let hiddenSince: number | undefined;
while (Date.now() < deadline) {
if (await isDisplayed(selector, 0)) {
hiddenSince = undefined;
} else {
hiddenSince ??= Date.now();
if (Date.now() - hiddenSince >= stableMs) {
return;
}
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
};

export const assertVisibility = async (
selector: Detox.NativeMatcher,
shouldBeDisplayed: boolean,
Expand All @@ -123,6 +148,7 @@ export const assertVisibility = async (
}
} else {
await waitToDisappear(selector, { index: 0, timeout });
await waitUntilStaysHidden(selector, timeout);
jestExpect(await isDisplayed(selector, 0)).toBe(false);
}
};
Expand Down
Loading