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
6 changes: 6 additions & 0 deletions .changeset/happy-moments-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"react-jitter-runtime": minor
"react-jitter": minor
---

Adds support for detecting mocked hooks when using react-jitter in a testing environment
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,33 @@ Here is an example of the `change` object when `includeArguments` is enabled:

In this example, the `arguments` field shows that the `UserContext` was used, and the `changedKeys` field shows that the `user` property has changed.

### Detecting Unstable Hooks in Unit Tests

React Jitter can also be a powerful tool for improving code quality within your unit tests.

You can leverage this to write tests that fail if a hook becomes unstable, catching performance regressions early in the a testing setup where you might initialize React Jitter in a global setup file, you can easily override the `onHookChange` handler on a per-test basis.

```javascript
// Example of a Vitest/Jest test
it('should not have unstable hooks', () => {
const unstableChanges = [];
// Initialize React Jitter in a global setup file (e.g., setupTests.js)
// Then, override the onHookChange handler for specific tests.
window.reactJitter.onHookChange = (change) => {
// You can ignore mocked hooks or handle them specifically
if (change.unstable && !change.isMocked) {
unstableChanges.push(change);
}
};

render(<MyComponent />);

// Assert that no unstable values were detected during the render
expect(unstableChanges).toHaveLength(0);
});
```

The `onHookChange` callback's `change` object includes an `isMocked` boolean property. This is automatically set to `true` if React Jitter detects that the hook has been mocked (e.g., using `jest.fn()` or `vi.fn()`). This allows you to reliably identify and assert against unstable values in your test environment.

## How It Works

Expand Down
5 changes: 4 additions & 1 deletion __tests__/__snapshots__/wasm.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function UserForm() {
hook: "useFieldValues",
line: 5,
offset: 26,
isMocked: h.m(useFieldValues),
arguments: [
"'name'"
]
Expand All @@ -28,6 +29,7 @@ export default function UserForm() {
hook: "useFieldValues",
line: 6,
offset: 29,
isMocked: h.m(useFieldValues),
arguments: [
"'city'",
"'state'"
Expand All @@ -54,7 +56,8 @@ export default function() {
file: "3_anonymous_function.tsx",
hook: "useFieldValues",
line: 2,
offset: 22
offset: 22,
isMocked: h.m(useFieldValues)
}));
return /*#__PURE__*/ h.re(React.createElement("div", null));
}"
Expand Down
15 changes: 15 additions & 0 deletions plugin-swc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,21 @@ impl VisitMut for JitterTransform {
}))),
}))),
];
hook_meta_props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("isMocked")),
value: Box::new(Expr::Call(CallExpr {
span: DUMMY_SP,
callee: MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(quote_ident!("h").into())),
prop: MemberProp::Ident(quote_ident!("m")),
}
.as_callee(),
args: vec![id.clone().as_arg()],
type_args: None,
ctxt: SyntaxContext::empty(),
})),
}))));

if self.include_arguments {
let mut args_vec = Vec::new();
Expand Down
Binary file modified plugin-swc/swc_plugin_react_jitter.wasm
Binary file not shown.
2 changes: 2 additions & 0 deletions runtime/dist/index.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type HookEndEvent = {
line: number;
offset: number;
arguments?: string[];
isMocked?: boolean;
};
type HookAddress = Pick<HookEndEvent, 'hook' | 'file' | 'line' | 'offset' | 'arguments'>;
type ReactJitterOptions = {
Expand Down Expand Up @@ -82,6 +83,7 @@ declare function useJitterScope(scope: Scope): {
s: (id: string) => void;
e: (hookResult: unknown, hookEndEvent: HookEndEvent) => unknown;
re: <T>(renderResult: T) => T;
m: (value: unknown) => boolean;
};
declare function reactJitter(options: ReactJitterOptions): void;

Expand Down
2 changes: 2 additions & 0 deletions runtime/dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type HookEndEvent = {
line: number;
offset: number;
arguments?: string[];
isMocked?: boolean;
};
type HookAddress = Pick<HookEndEvent, 'hook' | 'file' | 'line' | 'offset' | 'arguments'>;
type ReactJitterOptions = {
Expand Down Expand Up @@ -82,6 +83,7 @@ declare function useJitterScope(scope: Scope): {
s: (id: string) => void;
e: (hookResult: unknown, hookEndEvent: HookEndEvent) => unknown;
re: <T>(renderResult: T) => T;
m: (value: unknown) => boolean;
};
declare function reactJitter(options: ReactJitterOptions): void;

Expand Down
9 changes: 9 additions & 0 deletions runtime/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,9 @@ function useJitterScope(scope) {
if (hookEndEvent.arguments) {
hookCall.arguments = hookEndEvent.arguments;
}
if (hookEndEvent.isMocked) {
hookCall.isMocked = hookEndEvent.isMocked;
}
scopes[scopeId].hookChanges.push(hookCall);
callOnHookChange(hookCall);
}
Expand All @@ -603,6 +606,12 @@ function useJitterScope(scope) {
re: (renderResult) => {
callOnRender(scopes[scopeId]);
return renderResult;
},
m: (value) => {
if (typeof value !== "function") {
return false;
}
return "mockImplementation" in value || "mockReturnValue" in value;
}
};
}
Expand Down
9 changes: 9 additions & 0 deletions runtime/dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,9 @@ function useJitterScope(scope) {
if (hookEndEvent.arguments) {
hookCall.arguments = hookEndEvent.arguments;
}
if (hookEndEvent.isMocked) {
hookCall.isMocked = hookEndEvent.isMocked;
}
scopes[scopeId].hookChanges.push(hookCall);
callOnHookChange(hookCall);
}
Expand All @@ -568,6 +571,12 @@ function useJitterScope(scope) {
re: (renderResult) => {
callOnRender(scopes[scopeId]);
return renderResult;
},
m: (value) => {
if (typeof value !== "function") {
return false;
}
return "mockImplementation" in value || "mockReturnValue" in value;
}
};
}
Expand Down
10 changes: 10 additions & 0 deletions runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function useJitterScope(scope: Scope) {
s: (id: string) => void;
e: (hookResult: unknown, hookEndEvent: HookEndEvent) => unknown;
re: <T>(renderResult: T) => T;
m: (value: unknown) => boolean;
} | null>(null);

if (!hooks.current) {
Expand Down Expand Up @@ -113,6 +114,9 @@ export function useJitterScope(scope: Scope) {
if (hookEndEvent.arguments) {
hookCall.arguments = hookEndEvent.arguments;
}
if (hookEndEvent.isMocked) {
hookCall.isMocked = hookEndEvent.isMocked;
}
scopes[scopeId].hookChanges.push(hookCall);
callOnHookChange(hookCall);
}
Expand All @@ -128,6 +132,12 @@ export function useJitterScope(scope: Scope) {
callOnRender(scopes[scopeId]);
return renderResult;
},
m: (value: unknown): boolean => {
if (typeof value !== 'function') {
return false;
}
return 'mockImplementation' in value || 'mockReturnValue' in value;
},
};
}

Expand Down
1 change: 1 addition & 0 deletions runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type HookEndEvent = {
line: number;
offset: number;
arguments?: string[];
isMocked?: boolean;
};

export type HookAddress = Pick<
Expand Down