Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use react-native Devtools lib to symbolicate error stacks #111

Merged
merged 1 commit into from
Mar 20, 2024
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
55 changes: 47 additions & 8 deletions packages/client/src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ export type ClientConfig = {
* @default "bdd"
*/
ui: InterfaceConfig;
/** A funcion called to load tests */
/**
* Called when a test fails error occurs to allow transformations of the stacktrace
*/
transformFailure?: (test: Mocha.Test, error: Error) => Promise<Error>;
/**
* A funcion called to load tests
*/
tests(context: CustomContext): void | Promise<void>,
} & MochaConfig;

Expand Down Expand Up @@ -295,11 +301,30 @@ export class Client extends ClientEventEmitter {
});

// Setup listeners for all events emitted by the runner
for (const name in Runner.constants) {
if (name.startsWith("EVENT")) {
const eventName = Runner.constants[name as keyof typeof Runner.constants];
runner.on(eventName, this.sendEvent.bind(this, eventName));
}
const mappedEventKeys = Object.keys(Runner.constants).filter(k => k.startsWith("EVENT")) as (keyof typeof Runner.constants)[];
const mappedEventNames = new Set(mappedEventKeys.map(k => Runner.constants[k]));

const { transformFailure } = this.config;
if (transformFailure) {
// Don't automatically map the "fail" event
mappedEventNames.delete(Runner.constants.EVENT_TEST_FAIL);
// Register a listener which allows the user to transform the failure
runner.on(Runner.constants.EVENT_TEST_FAIL, (test, error) => {
this.queueEvent(Runner.constants.EVENT_TEST_FAIL,
transformFailure(test, error).then((transformedError) => {
return [test, transformedError];
}, cause => {
const err = new Error(`Failed to transform failure: ${cause.message}`, { cause });
return [test, err];
})
);
});
}

for (const eventName of mappedEventNames) {
runner.on(eventName, (...args) => {
this.queueEvent(eventName, args);
});
}

this.debug("Running test suite");
Expand All @@ -312,8 +337,10 @@ export class Client extends ClientEventEmitter {
runner.once(Runner.constants.EVENT_RUN_END, () => {
this.emit("end", runner.failures);
if (this.config.autoDisconnect) {
this.debug("Disconnecting automatically after ended run");
this.disconnect();
this.debug("Disconnecting automatically after ended run and pending events");
this.pendingEvent.then(() => {
this.disconnect();
});
}
});

Expand Down Expand Up @@ -478,6 +505,18 @@ export class Client extends ClientEventEmitter {
}
}

private pendingEvent: Promise<void> = Promise.resolve();

/**
* Queue an event to be sent, use this to prevent out of order delivery
*/
private queueEvent(name: string, promisedArgs: Promise<unknown[]> | unknown[]) {
this.pendingEvent = this.pendingEvent.then(async () => {
const args = await promisedArgs;
this.sendEvent(name, ...args);
});
}

private sendEvent(name: string, ...args: unknown[]) {
try {
this.send({ action: "event", name, args });
Expand Down
31 changes: 26 additions & 5 deletions packages/react-native/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useEffect, useState, createContext, useContext } from "react";
import { Text, Platform, TextProps } from 'react-native';
import parseErrorStack, { StackFrame } from 'react-native/Libraries/Core/Devtools/parseErrorStack';
import symbolicateStackTrace from 'react-native/Libraries/Core/Devtools/symbolicateStackTrace';

import { Client, CustomContext } from "mocha-remote-client";

Expand Down Expand Up @@ -39,21 +41,40 @@ export const MochaRemoteContext = createContext<MochaRemoteContextValue>({
context: {},
});

function isExternalFrame({ file }: StackFrame) {
return !file.includes("/mocha-remote/packages/client/dist/") && !file.includes("/mocha-remote-client/dist/")
}

function framesToStack(error: Error, frames: StackFrame[]) {
const lines = frames.filter(isExternalFrame).map(({ methodName, column, file, lineNumber }) => {
return ` at ${methodName} (${file}:${lineNumber}:${column})`
});
return `${error.name}: ${error.message}\n${lines.join("\n")}`;
}

export function MochaRemoteProvider({ children, tests, title = `React Native on ${Platform.OS}` }: MochaRemoteProviderProps) {
const [connected, setConnected] = useState(false);
const [status, setStatus] = useState<Status>({ kind: "waiting" });
const [context, setContext] = useState<CustomContext>({});
useEffect(() => {
const client = new Client({
title,
async transformFailure(_, err) {
// TODO: Remove the two `as any` once https://github.com/facebook/react-native/pull/43566 gets released
const stack = parseErrorStack(err.stack as any);
const symbolicated = await symbolicateStackTrace(stack) as any;
err.stack = framesToStack(err, symbolicated.stack);
return err;
},
tests(context) {
setContext(context);
// Adding an async hook before each test to allow the UI to update
beforeEach("async-pause", () => {
return new Promise<void>((resolve) => setImmediate(resolve));
});
// Require in the tests
tests(context);
// Make the context available to context consumers
setContext(context);
},
})
.on("connection", () => {
Expand Down Expand Up @@ -98,7 +119,7 @@ export function MochaRemoteProvider({ children, tests, title = `React Native on
}, [setStatus, setContext]);

return (
<MochaRemoteContext.Provider value={{status, connected, context}}>
<MochaRemoteContext.Provider value={{ status, connected, context }}>
{children}
</MochaRemoteContext.Provider>
);
Expand All @@ -125,7 +146,7 @@ function getStatusEmoji(status: Status) {
}

export function StatusEmoji(props: TextProps) {
const {status} = useMochaRemoteContext();
const { status } = useMochaRemoteContext();
return <Text {...props}>{getStatusEmoji(status)}</Text>
}

Expand All @@ -144,7 +165,7 @@ function getStatusMessage(status: Status) {
}

export function StatusText(props: TextProps) {
const {status} = useMochaRemoteContext();
const { status } = useMochaRemoteContext();
return <Text {...props}>{getStatusMessage(status)}</Text>
}

Expand All @@ -157,6 +178,6 @@ function getConnectionMessage(connected: boolean) {
}

export function ConnectionText(props: TextProps) {
const {connected} = useMochaRemoteContext();
const { connected } = useMochaRemoteContext();
return <Text {...props}>{getConnectionMessage(connected)}</Text>
}
Loading