feat: add carousel component with gesture support and animations#1
Conversation
- Implemented performant swipe carousel with react-native-reanimated and gesture-handler - Added Jest setup for testing with mocks for react-native-svg and dimensions - Updated babel configuration to include react-native-reanimated plugin - Created jest configuration and setup files for testing - Enhanced README with installation instructions and usage examples - Added coverage reports for improved test visibility - Updated package.json with necessary dependencies and peer dependencies
|
CodeAnt AI is reviewing your PR. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Reviewer's GuideImplements a new generic, animated swipe Carousel component using react-native-reanimated, gesture-handler, SVG gradient borders, and worklets; adds comprehensive tests, documentation, example usage, Jest config/mocks, ESLint Jest rules, coverage reporting, and updates dependencies/babel/peerDeps accordingly. Sequence diagram for Carousel pan gesture and item change flowsequenceDiagram
actor User
participant GestureHandlerRootView
participant GestureDetector
participant Carousel
participant ReanimatedWorklet
participant ScheduleOnRN
participant ParentComponent
User->>GestureHandlerRootView: Swipe horizontally
GestureHandlerRootView->>GestureDetector: Deliver pan gesture events
GestureDetector->>Carousel: panGesture.onUpdate(event)
activate Carousel
Carousel->>ReanimatedWorklet: Update translateX sharedValue
ReanimatedWorklet-->>Carousel: New translateX clamped to bounds
deactivate Carousel
GestureDetector->>Carousel: panGesture.onEnd(event)
activate Carousel
Carousel->>ReanimatedWorklet: Compute targetIndex and snapToIndex(targetIndex)
ReanimatedWorklet-->>Carousel: Spring animation for translateX and currentIndex
Carousel->>ScheduleOnRN: scheduleOnRN(updateSelectedItem, targetIndex)
deactivate Carousel
ScheduleOnRN->>Carousel: updateSelectedItem(targetIndex)
activate Carousel
Carousel->>ParentComponent: onItemChange(newItem)
deactivate Carousel
ParentComponent-->>ParentComponent: setSelectedItem(newItem)
ParentComponent->>Carousel: Rerender with updated selectedItem
Updated class diagram for Carousel component and related typesclassDiagram
direction LR
class PanGestureEvent {
+translationX: number
+velocityX: number
}
class ImageSource {
<<type alias>>
string
ImageSourcePropType
}
class CarouselItemBase {
+image: ImageSource
}
class CarouselPropsT {
+items: T[]
+selectedItem: T
+onItemChange(item: T) void
+getItemId(item: T) string
+renderItem(item: T, index: number, translateX: SharedValueNumber) ReactNode
+renderDetail(item: T) ReactNode
+itemSize: number
+itemSpacing: number
+minScale: number
+maxScale: number
+showGradientBorder: boolean
+gradientColors: string[]
+gradientId: string
}
class GradientBorder {
+itemSize: number
+gradientColors: string[]
+gradientId: string
+render() ReactNode
}
class CarouselItemT {
+item: T
+index: number
+translateX: SharedValueNumber
+itemSize: number
+itemSpacing: number
+minScale: number
+maxScale: number
+renderItem(item: T, index: number, translateX: SharedValueNumber) ReactNode
-animatedStyle(): AnimatedStyle
+render() ReactNode
}
class CarouselT {
+items: T[]
+selectedItem: T
+onItemChange(item: T) void
+getItemId(item: T) string
+renderItem(item: T, index: number, translateX: SharedValueNumber) ReactNode
+renderDetail(item: T) ReactNode
+itemSize: number
+itemSpacing: number
+minScale: number
+maxScale: number
+showGradientBorder: boolean
+gradientColors: string[]
+gradientId: string
-translateX: SharedValueNumber
-currentIndex: SharedValueNumber
-previousItemId: RefString
-snapToIndex(index: number) void
-updateSelectedItem(index: number) void
-panGesture: PanGesture
+render() ReactNode
}
class SharedValueNumber {
<<Reanimated SharedValue>>
+value: number
}
class PanGesture {
<<gesture handler>>
+withTestId(id: string) PanGesture
+onUpdate(event: PanGestureEvent) PanGesture
+onEnd(event: PanGestureEvent) PanGesture
}
class GestureDetectorComponent {
+gesture: PanGesture
+children: ReactNode
}
class ScheduleOnRN {
<<function>>
+scheduleOnRN(fn: Function, arg: any) void
}
CarouselT ..|> CarouselPropsT
CarouselItemT ..|> CarouselItemBase
CarouselT o-- CarouselItemT : renders
CarouselT ..> GradientBorder : optional
CarouselT ..> SharedValueNumber : uses
CarouselT ..> PanGesture : configures
CarouselT ..> GestureDetectorComponent : wraps content
CarouselT ..> ScheduleOnRN : uses
CarouselItemT ..> SharedValueNumber : animates
GradientBorder ..> ImageSource : via SVG visuals
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughSummary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings. WalkthroughAdds a fully implemented, generic Carousel component (gestures, reanimated animations, SVG gradient borders), extensive tests, example app wiring, Jest/ESLint/Babel test tooling, package dependency updates, expanded README, CI coverage workflow, and .gitignore entries. Changes
Sequence DiagramsequenceDiagram
actor User
participant Gesture as "Pan Gesture Handler"
participant Reanimated as "Reanimated (worklets)"
participant Carousel as "Carousel Component"
participant Callback as "onItemChange"
User->>Gesture: drag (translateX)
Gesture->>Reanimated: update shared translateX
Reanimated->>Carousel: recalc animated styles (position/scale/opacity)
Carousel->>Carousel: compute nearest/snap index from translateX
alt Release with high velocity
User->>Gesture: release (velocity)
Gesture->>Reanimated: compute velocity-based target index
else Release with low velocity
User->>Gesture: release
Gesture->>Reanimated: clamp & snap to nearest index
end
Reanimated->>Carousel: spring animate to snapped index
Carousel->>Callback: invoke onItemChange if index changed
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||||||||
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||||||||||
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
Blocking issues:
- User controlled data in methods like
innerHTML,outerHTMLordocument.writeis an anti-pattern that can lead to XSS vulnerabilities (link) - User controlled data in a
colNode.innerHTMLis an anti-pattern that can lead to XSS vulnerabilities (link)
General comments:
- In
jest-setup.js, the mockedDimensions.getreturns an object with awindowfield, but the component callsDimensions.get('window').width, so the mock should return the window object itself (e.g.{ width, height }) rather than{ window: { width, height } }to match the real API shape. - The
coverageHTML/JSON/LCOV artifacts are committed into the repo; consider removing these from source control and adding the coverage directory to.gitignoreso they are generated only locally/CI.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `jest-setup.js`, the mocked `Dimensions.get` returns an object with a `window` field, but the component calls `Dimensions.get('window').width`, so the mock should return the window object itself (e.g. `{ width, height }`) rather than `{ window: { width, height } }` to match the real API shape.
- The `coverage` HTML/JSON/LCOV artifacts are committed into the repo; consider removing these from source control and adding the coverage directory to `.gitignore` so they are generated only locally/CI.
## Individual Comments
### Comment 1
<location> `src/index.tsx:248-249` </location>
<code_context>
+ const initialIndex = items.findIndex(
+ (item) => getItemId(item) === getItemId(selectedItem)
+ );
+ const translateX = useSharedValue(-initialIndex * itemSpacing);
+ const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0);
+ const previousItemId = useRef(getItemId(selectedItem));
</code_context>
<issue_to_address>
**issue:** Guard `translateX` initialization when the selected item is not found to avoid misaligned initial position.
If `selectedItem` isn’t in `items`, `initialIndex` is `-1`. While `currentIndex` clamps this to `0`, `translateX` becomes `+itemSpacing`, briefly offsetting the first item until the next update. Consider clamping `initialIndex` (e.g. `const safeInitialIndex = Math.max(0, initialIndex)`) and using that for both `translateX` and `currentIndex` so the carousel starts centered on index 0 in this case.
</issue_to_address>
### Comment 2
<location> `src/index.tsx:130-137` </location>
<code_context>
+ x2={GRADIENT_END_X}
+ y2={GRADIENT_END_Y}
+ >
+ {gradientColors.map((color, index) => {
+ const colorCount = gradientColors.length;
+ const offset =
+ colorCount === 1
+ ? '0%'
+ : `${(index / (colorCount - 1)) * 100}%`;
+ return (
+ <Stop
+ key={color}
+ offset={offset}
+ stopColor={color}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using `color` as the React key can lead to unstable keys when gradient colors repeat.
Here `gradientColors.map` uses `key={color}`. If the array contains duplicate colors or the order changes while duplicates remain, keys become non-unique/unstable and can confuse React’s reconciliation. Consider using `key={index}` or a composite like `${color}-${index}` to ensure stable, unique keys.
```suggestion
return (
<Stop
key={`${color}-${index}`}
offset={offset}
stopColor={color}
stopOpacity={GRADIENT_STOP_OPACITY}
/>
);
```
</issue_to_address>
### Comment 3
<location> `jest-setup.js:40-47` </location>
<code_context>
+
+// Mock Dimensions
+const { Dimensions } = require('react-native');
+const mockDimensions = {
+ window: {
+ width: 375,
+ height: 812,
+ },
+};
+
+jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);
+
+// Mock react-native-worklets scheduleOnRN to execute synchronously in tests
</code_context>
<issue_to_address>
**issue (testing):** The Dimensions mock shape does not match `Dimensions.get('window')`, which can break width/height usage in tests.
In this setup `Dimensions.get('window').width` in app code will resolve to `mockDimensions.window.width`, but the mock returns `{ window: { width, height } }` directly. That makes `Dimensions.get('window')` return `{ window: { ... } }`, so `.width` is `undefined` in tests. To match React Native’s API, the mock should return a flat `{ width, height }` object for the requested dimension type, e.g.:
```js
const mockDimensions = { width: 375, height: 812 };
jest.spyOn(Dimensions, 'get').mockImplementation(type =>
type === 'window' ? mockDimensions : /* other types if needed */ mockDimensions,
);
```
</issue_to_address>
### Comment 4
<location> `src/__tests__/index.test.tsx:542-551` </location>
<code_context>
+ jest.useRealTimers();
+ });
+
+ it('should handle pan gesture with positive translationX', () => {
+ const items = createTestItems(3);
+ const selectedItem = items[0]!;
+
+ render(
+ <Carousel
+ items={items}
+ selectedItem={selectedItem}
+ onItemChange={mockOnItemChange}
+ getItemId={mockGetItemId}
+ renderItem={mockRenderItem}
+ />
+ );
+
+ const gesture = getByGestureTestId('carousel-pan-gesture');
+
+ fireGestureHandler(gesture, [
+ { state: State.BEGAN, translationX: 0 },
+ { state: State.ACTIVE, translationX: 100 },
+ { state: State.END, translationX: 100 },
+ ]);
+
+ jest.advanceTimersByTime(1000);
+ });
+
</code_context>
<issue_to_address>
**suggestion (testing):** Pan gesture tests don’t assert any observable behavior, so they won’t catch regressions in gesture handling.
These tests currently only trigger the gesture and advance timers, but never assert outcomes, so they’ll still pass if the gesture logic breaks. Please add behavioral expectations, e.g.:
- `expect(mockOnItemChange).not.toHaveBeenCalled()` when the index should remain unchanged.
- `expect(mockOnItemChange).toHaveBeenCalledWith(expectedItem)` when a swipe should change the item.
- Optionally, verify that `renderDetail` still receives the same `selectedItem` after a no-op gesture.
This will ensure the tests actually validate gesture behavior rather than just execution.
Suggested implementation:
```typescript
it('should handle pan gesture with positive translationX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;
render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);
const gesture = getByGestureTestId('carousel-pan-gesture');
fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: 100 },
{ state: State.END, translationX: 100 },
]);
jest.advanceTimersByTime(1000);
// Swiping right from the first item should not change the selected index
expect(mockOnItemChange).not.toHaveBeenCalled();
// Ensure the rendered item is still the original selected item
expect(mockRenderItem).toHaveBeenCalledWith(
expect.objectContaining({ item: selectedItem })
);
});
```
If `mockRenderItem` has a different call signature (e.g. `(args: { item: TestItem; index: number; isActive: boolean })`), the `expect.objectContaining({ item: selectedItem })` matcher will still be compatible. If instead it is called with positional arguments like `(item, index, isActive)`, you should update the assertion to:
```ts
expect(mockRenderItem).toHaveBeenCalledWith(
selectedItem,
expect.any(Number),
expect.any(Boolean)
);
```
Also ensure that `mockOnItemChange` and `mockRenderItem` are reset between tests (e.g. via `jest.clearAllMocks()` in a global `beforeEach`) so these expectations are not polluted by calls from other tests.
</issue_to_address>
### Comment 5
<location> `src/__tests__/index.test.tsx:784-793` </location>
<code_context>
+ expect(mockOnItemChange).toHaveBeenCalledWith(items[1]);
+ });
+
+ it('should not call onItemChange when item does not change via gesture', () => {
+ const items = createTestItems(3);
+ const selectedItem = items[0]!;
+
+ render(
+ <Carousel
+ items={items}
+ selectedItem={selectedItem}
+ onItemChange={mockOnItemChange}
+ getItemId={mockGetItemId}
+ renderItem={mockRenderItem}
+ />
+ );
+
+ mockOnItemChange.mockClear();
+
+ const gesture = getByGestureTestId('carousel-pan-gesture');
+
+ fireGestureHandler(gesture, [
+ { state: State.BEGAN, translationX: 0 },
+ { state: State.ACTIVE, translationX: 0 },
+ { state: State.END, translationX: 0, velocityX: 0 },
+ ]);
+
+ jest.advanceTimersByTime(1000);
+
+ // Should stay on same item, so onItemChange may not be called
+ // or called with same item
+ });
</code_context>
<issue_to_address>
**issue (testing):** Test for gesture not changing item lacks an assertion and currently doesn’t verify the behavior it describes.
This test name implies `onItemChange` is not called when the item doesn’t change, but the test has no expectations. Please add an explicit assertion (e.g. `expect(mockOnItemChange).not.toHaveBeenCalled();`) so the behavior is actually verified. If calling `onItemChange` with the same item is allowed, assert that it’s only called with `selectedItem` and document that behavior in the test.
</issue_to_address>
### Comment 6
<location> `coverage/lcov-report/sorter.js:84-85` </location>
<code_context>
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-document-method):** User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities
*Source: opengrep*
</issue_to_address>
### Comment 7
<location> `coverage/lcov-report/sorter.js:84-85` </location>
<code_context>
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-innerhtml):** User controlled data in a `colNode.innerHTML` is an anti-pattern that can lead to XSS vulnerabilities
*Source: opengrep*
</issue_to_address>
### Comment 8
<location> `coverage/lcov-report/block-navigation.js:2-86` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 9
<location> `coverage/lcov-report/block-navigation.js:4` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 10
<location> `coverage/lcov-report/block-navigation.js:7` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 11
<location> `coverage/lcov-report/block-navigation.js:10` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 12
<location> `coverage/lcov-report/block-navigation.js:13-17` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 13
<location> `coverage/lcov-report/block-navigation.js:20` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 14
<location> `coverage/lcov-report/block-navigation.js:22` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 15
<location> `coverage/lcov-report/block-navigation.js:24-29` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))
<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>
### Comment 16
<location> `coverage/lcov-report/block-navigation.js:31-39` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))
<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>
### Comment 17
<location> `coverage/lcov-report/block-navigation.js:41-50` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))
<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>
### Comment 18
<location> `coverage/lcov-report/block-navigation.js:42` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 19
<location> `coverage/lcov-report/block-navigation.js:52-63` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))
<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>
### Comment 20
<location> `coverage/lcov-report/block-navigation.js:53` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 21
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 22
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 23
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 24
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 25
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 26
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 27
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 28
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 29
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 30
<location> `coverage/lcov-report/prettify.js:2` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const translateX = useSharedValue(-initialIndex * itemSpacing); | ||
| const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0); |
There was a problem hiding this comment.
issue: Guard translateX initialization when the selected item is not found to avoid misaligned initial position.
If selectedItem isn’t in items, initialIndex is -1. While currentIndex clamps this to 0, translateX becomes +itemSpacing, briefly offsetting the first item until the next update. Consider clamping initialIndex (e.g. const safeInitialIndex = Math.max(0, initialIndex)) and using that for both translateX and currentIndex so the carousel starts centered on index 0 in this case.
| const mockDimensions = { | ||
| window: { | ||
| width: 375, | ||
| height: 812, | ||
| }, | ||
| }; | ||
|
|
||
| jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions); |
There was a problem hiding this comment.
issue (testing): The Dimensions mock shape does not match Dimensions.get('window'), which can break width/height usage in tests.
In this setup Dimensions.get('window').width in app code will resolve to mockDimensions.window.width, but the mock returns { window: { width, height } } directly. That makes Dimensions.get('window') return { window: { ... } }, so .width is undefined in tests. To match React Native’s API, the mock should return a flat { width, height } object for the requested dimension type, e.g.:
const mockDimensions = { width: 375, height: 812 };
jest.spyOn(Dimensions, 'get').mockImplementation(type =>
type === 'window' ? mockDimensions : /* other types if needed */ mockDimensions,
);| it('should handle pan gesture with positive translationX', () => { | ||
| const items = createTestItems(3); | ||
| const selectedItem = items[0]!; | ||
|
|
||
| render( | ||
| <Carousel | ||
| items={items} | ||
| selectedItem={selectedItem} | ||
| onItemChange={mockOnItemChange} | ||
| getItemId={mockGetItemId} |
There was a problem hiding this comment.
suggestion (testing): Pan gesture tests don’t assert any observable behavior, so they won’t catch regressions in gesture handling.
These tests currently only trigger the gesture and advance timers, but never assert outcomes, so they’ll still pass if the gesture logic breaks. Please add behavioral expectations, e.g.:
expect(mockOnItemChange).not.toHaveBeenCalled()when the index should remain unchanged.expect(mockOnItemChange).toHaveBeenCalledWith(expectedItem)when a swipe should change the item.- Optionally, verify that
renderDetailstill receives the sameselectedItemafter a no-op gesture.
This will ensure the tests actually validate gesture behavior rather than just execution.
Suggested implementation:
it('should handle pan gesture with positive translationX', () => {
const items = createTestItems(3);
const selectedItem = items[0]!;
render(
<Carousel
items={items}
selectedItem={selectedItem}
onItemChange={mockOnItemChange}
getItemId={mockGetItemId}
renderItem={mockRenderItem}
/>
);
const gesture = getByGestureTestId('carousel-pan-gesture');
fireGestureHandler(gesture, [
{ state: State.BEGAN, translationX: 0 },
{ state: State.ACTIVE, translationX: 100 },
{ state: State.END, translationX: 100 },
]);
jest.advanceTimersByTime(1000);
// Swiping right from the first item should not change the selected index
expect(mockOnItemChange).not.toHaveBeenCalled();
// Ensure the rendered item is still the original selected item
expect(mockRenderItem).toHaveBeenCalledWith(
expect.objectContaining({ item: selectedItem })
);
});If mockRenderItem has a different call signature (e.g. (args: { item: TestItem; index: number; isActive: boolean })), the expect.objectContaining({ item: selectedItem }) matcher will still be compatible. If instead it is called with positional arguments like (item, index, isActive), you should update the assertion to:
expect(mockRenderItem).toHaveBeenCalledWith(
selectedItem,
expect.any(Number),
expect.any(Boolean)
);Also ensure that mockOnItemChange and mockRenderItem are reset between tests (e.g. via jest.clearAllMocks() in a global beforeEach) so these expectations are not polluted by calls from other tests.
| it('should not call onItemChange when item does not change via gesture', () => { | ||
| const items = createTestItems(3); | ||
| const selectedItem = items[0]!; | ||
|
|
||
| render( | ||
| <Carousel | ||
| items={items} | ||
| selectedItem={selectedItem} | ||
| onItemChange={mockOnItemChange} | ||
| getItemId={mockGetItemId} |
There was a problem hiding this comment.
issue (testing): Test for gesture not changing item lacks an assertion and currently doesn’t verify the behavior it describes.
This test name implies onItemChange is not called when the item doesn’t change, but the test has no expectations. Please add an explicit assertion (e.g. expect(mockOnItemChange).not.toHaveBeenCalled();) so the behavior is actually verified. If calling onItemChange with the same item is allowed, assert that it’s only called with selectedItem and document that behavior in the test.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
coverage/clover.xml (1)
1-88: Coverage artifacts should be excluded from version control.This is another coverage report artifact that should typically be in
.gitignore(see similar comment oncoverage/lcov.info).example/package.json (1)
18-22: Version alignment concern already flagged.The version mismatches between these dependencies and the main package.json have been flagged in the review of package.json. Consider aligning versions for consistency.
coverage/lcov-report/index.html (1)
2-115: Static coverage HTML duplicates earlier coverage-artifact concernThis is another Istanbul-generated file under
coverage/. The earlier suggestion to ignorecoverage/in version control (and tooling) applies here as well to reduce churn.
🧹 Nitpick comments (13)
coverage/lcov.info (1)
1-141: Consider excluding coverage artifacts from version control.Coverage reports are typically generated during CI runs and don't need to be committed to the repository. They can bloat the repository history and become stale.
Consider adding these entries to
.gitignore:+# Coverage reports +coverage/Alternatively, if you want to keep coverage reports for visibility, consider using a separate coverage reporting service or generating them only in CI.
coverage/lcov-report/sorter.js (1)
1-210: Generated coverage UI files should be excluded from version control.This is a generated file from the Istanbul/NYC coverage tool that provides interactive sorting/filtering in the coverage HTML report. These files should typically be in
.gitignorealong with other coverage artifacts.This is covered by the same recommendation to add
coverage/to.gitignore(see comment oncoverage/lcov.info).coverage/lcov-report/prettify.js (1)
1-2: Generated coverage UI files should be excluded from version control.This is a minified syntax highlighting library (Google Code Prettify) used by the coverage HTML reports. Like other coverage artifacts, it should be in
.gitignore.This is covered by the same recommendation to add
coverage/to.gitignore(see comment oncoverage/lcov.info). The static analysis warnings are expected in minified generated code and can be safely ignored.package.json (1)
85-88: Version differences between main and example packages exist but are minor and pose no security risk.The packages in lines 85-88 do have newer versions in the main package.json compared to example/package.json:
react-native-gesture-handler: 2.29.1 (main) vs 2.28.0 (example)react-native-reanimated: 4.1.5 (main) vs 4.1.1 (example)react-native-svg: 15.15.0 (main) vs 15.12.1 (example)react-native-worklets: 0.6.1 (main) vs 0.5.1 (example)All versions are free of security vulnerabilities. Since these are devDependencies in the main package (used for library development) and the example app pulls the library from the monorepo rather than npm, consider whether alignment is necessary or merely a nice-to-have consistency improvement.
coverage/coverage-final.json (1)
1-2: Avoid committing generated coverage JSONThis looks like Jest/Istanbul output; committing
coverage/coverage-final.jsonwill create noisy diffs on every test run. Consider addingcoverage/to.gitignoreand letting CI generate coverage reports instead of versioning them.coverage/lcov-report/base.css (1)
1-224: Vendor-style coverage CSS – consider excluding from lintersThis file matches the standard Istanbul coverage stylesheet, including intentional “duplicate” properties (e.g., color fallbacks and height hacks). Rather than modifying vendor CSS to satisfy Biome/ESLint, I’d suggest excluding
coverage/**from linting/formatting so these generated assets don’t generate noise.coverage/lcov-report/block-navigation.js (1)
1-87: Avoid global/* eslint-disable */on generated coverage JSSince this is Istanbul-generated helper code, the blanket
/* eslint-disable */is understandable but can trip lint rules about unlimited disables. A cleaner approach is to ignorecoverage/**in your ESLint/Biome config (or.eslintignore) so generated scripts like this aren’t linted at all, instead of carrying a global disable comment.README.md (1)
19-30: Consider documenting supported React Native versionsThe install/peer dependency section is clear, but since libraries like
react-native-gesture-handleronly support relatively recent React Native versions (they officially track the last few RN minors), it may help consumers if you explicitly state which RN versions this carousel supports. (docs.swmansion.com)Adding a short “Compatibility” note (e.g., “Tested with RN ≥ X.Y”) would set expectations for users integrating the library.
src/index.tsx (2)
29-29:SCREEN_WIDTHwon't update on orientation or window resize.This value is computed once at module load time. If the device rotates or the window resizes (e.g., split-screen on tablets), the carousel will use stale dimensions.
Consider using
useWindowDimensionshook inside the component:+import { Dimensions, View, type ImageSourcePropType, useWindowDimensions } from 'react-native'; -import { Dimensions } from 'react-native'; -import { View, type ImageSourcePropType } from 'react-native';Then inside the
Carouselcomponent:const { width: screenWidth } = useWindowDimensions();And pass
screenWidthas a prop toCarouselItem.
130-137: Usingcoloraskeymay cause issues with duplicate gradient colors.If
gradientColorscontains duplicates (e.g.,['red', 'blue', 'red']), React will warn about duplicate keys and may incorrectly recycle elements.Use the index as part of the key:
- <Stop - key={color} + <Stop + key={`${color}-${index}`} offset={offset} stopColor={color} stopOpacity={GRADIENT_STOP_OPACITY} />example/src/App.tsx (1)
24-48: Consider adding AbortController for cleanup.If the component unmounts before fetch completes,
setItemsandsetSelectedItemwill be called on an unmounted component. For an example app this is minor, but worth noting.useEffect(() => { + const controller = new AbortController(); const fetchImages = async () => { try { - const response = await fetch('https://picsum.photos/v2/list?limit=5'); + const response = await fetch('https://picsum.photos/v2/list?limit=5', { + signal: controller.signal, + }); // ... rest of implementation } catch (error) { + if (error instanceof Error && error.name === 'AbortError') return; console.error('Error fetching images:', error); } // ... }; fetchImages(); + return () => controller.abort(); }, []);src/__tests__/index.test.tsx (2)
542-565: Test has no assertions - consider adding expectations.This test fires a pan gesture but doesn't verify any resulting state or behavior. Without assertions, it only confirms the gesture doesn't throw an error.
Add assertions to verify expected behavior:
fireGestureHandler(gesture, [ { state: State.BEGAN, translationX: 0 }, { state: State.ACTIVE, translationX: 100 }, { state: State.END, translationX: 100 }, ]); jest.advanceTimersByTime(1000); + + // Positive translation should not change item when at first index + expect(mockOnItemChange).not.toHaveBeenCalled(); });
567-640: Multiple gesture tests lack assertions.Tests at lines 567, 592, and 617 fire gestures without verifying outcomes. These function as smoke tests only.
Add meaningful assertions. For example, for clamping tests:
it('should handle pan gesture clamping to minTranslateX', () => { const items = createTestItems(3); const selectedItem = items[0]!; render(/* ... */); const gesture = getByGestureTestId('carousel-pan-gesture'); fireGestureHandler(gesture, [ { state: State.BEGAN, translationX: 0 }, { state: State.ACTIVE, translationX: -1000 }, { state: State.END, translationX: -1000, velocityX: 0 }, ]); jest.advanceTimersByTime(1000); // Should clamp to last item expect(mockOnItemChange).toHaveBeenCalledWith(items[2]); });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
coverage/lcov-report/favicon.pngis excluded by!**/*.pngcoverage/lcov-report/sort-arrow-sprite.pngis excluded by!**/*.pngyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (20)
README.md(1 hunks)babel.config.js(1 hunks)coverage/clover.xml(1 hunks)coverage/coverage-final.json(1 hunks)coverage/lcov-report/base.css(1 hunks)coverage/lcov-report/block-navigation.js(1 hunks)coverage/lcov-report/index.html(1 hunks)coverage/lcov-report/index.tsx.html(1 hunks)coverage/lcov-report/prettify.css(1 hunks)coverage/lcov-report/prettify.js(1 hunks)coverage/lcov-report/sorter.js(1 hunks)coverage/lcov.info(1 hunks)eslint.config.mjs(2 hunks)example/package.json(1 hunks)example/src/App.tsx(2 hunks)jest-setup.js(1 hunks)jest.config.js(1 hunks)package.json(1 hunks)src/__tests__/index.test.tsx(1 hunks)src/index.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/__tests__/index.test.tsx (1)
src/index.tsx (2)
CarouselItemBase(67-69)Carousel(230-379)
example/src/App.tsx (1)
src/index.tsx (1)
Carousel(230-379)
src/index.tsx (1)
jest-setup.js (5)
React(6-6)Svg(21-21)Defs(23-23)Stop(25-25)Circle(22-22)
🪛 Biome (2.1.2)
coverage/lcov-report/base.css
[error] 6-6: Duplicate font names are redundant and unnecessary: Helvetica
Remove duplicate font names within the property
(lint/suspicious/noDuplicateFontNames)
[error] 54-54: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.
color is already defined here.
Remove or rename the duplicate property to ensure consistent styling.
(lint/suspicious/noDuplicateProperties)
[error] 219-219: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.
height is already defined here.
Remove or rename the duplicate property to ensure consistent styling.
(lint/suspicious/noDuplicateProperties)
coverage/lcov-report/prettify.js
[error] 2-2: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 2-2: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 2-2: Shouldn't redeclare 'ar'. Consider to delete it or rename it.
'ar' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ar'. Consider to delete it or rename it.
'ar' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'at'. Consider to delete it or rename it.
'at' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.
'ak' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.
'ak' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'am'. Consider to delete it or rename it.
'am' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ag'. Consider to delete it or rename it.
'ag' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'af'. Consider to delete it or rename it.
'af' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.
'ak' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'am'. Consider to delete it or rename it.
'am' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ak'. Consider to delete it or rename it.
'ak' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ag'. Consider to delete it or rename it.
'ag' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'V'. Consider to delete it or rename it.
'V' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'U'. Consider to delete it or rename it.
'U' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'ae'. Consider to delete it or rename it.
'ae' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'Y'. Consider to delete it or rename it.
'Y' is defined here:
(lint/suspicious/noRedeclare)
[error] 2-2: Shouldn't redeclare 'W'. Consider to delete it or rename it.
'W' is defined here:
(lint/suspicious/noRedeclare)
🪛 GitHub Check: lint
coverage/lcov-report/block-navigation.js
[warning] 1-1:
Unexpected unlimited 'eslint-disable' comment. Specify some rule names to disable
src/__tests__/index.test.tsx
[warning] 617-617:
Test has no assertions
[warning] 592-592:
Test has no assertions
[warning] 567-567:
Test has no assertions
[warning] 542-542:
Test has no assertions
example/src/App.tsx
[warning] 83-83:
Inline style: { justifyContent: 'center', alignItems: 'center' }
[warning] 60-60:
Inline style: { flex: 1 }
[warning] 52-52:
Inline style: { flex: 1 }
coverage/lcov-report/sorter.js
[warning] 1-1:
Unexpected unlimited 'eslint-disable' comment. Specify some rule names to disable
coverage/lcov-report/prettify.js
[warning] 1-1:
Unexpected unlimited 'eslint-disable' comment. Specify some rule names to disable
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (7)
babel.config.js (1)
6-8: LGTM!The react-native-reanimated plugin is correctly configured and positioned last in the plugins array, as required by the library documentation.
eslint.config.mjs (1)
4-45: LGTM!The Jest plugin integration is correctly configured:
- Properly scoped to test files (
**/__tests__/**/*,**/*.test.{ts,tsx,js,jsx})- Separate configuration for Jest setup files
- Applies recommended Jest rules and globals
coverage/lcov-report/prettify.css (1)
1-1: Generated prettify stylesheet – no action neededThis looks like stock prettify/Istanbul CSS. I’d either keep it as-is or, if you follow the earlier advice, stop versioning
coverage/entirely so these generated assets don’t need maintenance.jest.config.js (1)
1-25: Jest config is consistent with the RN + gesture/reanimated stackPreset,
setupFiles, coverage globs, andtransformIgnorePatternsare coherent for this React Native + gesture-handler + Reanimated + SVG/worklets setup. I don’t see configuration issues here; just ensure you run the full test suite (including coverage) on CI to validate it in your environment.src/index.tsx (1)
230-379: Well-structured carousel implementation.The component correctly:
- Handles controlled
selectedItemchanges viauseEffect- Implements smooth spring animations for snapping
- Clamps gesture translations to valid bounds
- Uses
scheduleOnRNto bridge worklet→JS for state updatesexample/src/App.tsx (1)
65-94: Good example demonstrating Carousel usage.The example properly shows:
- Dynamic data fetching with loading state
- Custom
renderItemwith image styling- Custom
renderDetailfor selected item info- Proper
getItemIdandonItemChangehandlingsrc/__tests__/index.test.tsx (1)
1-33: Comprehensive test suite with good coverage.The test suite thoroughly covers:
- Basic rendering scenarios
- Custom props handling
- Edge cases (empty items, single item, non-existent selectedItem)
- Gesture interactions
- Component integration
The test helpers (
createTestItems, mock functions) are well-structured for reuse.
|
|
||
| <!doctype html> | ||
| <html lang="en"> | ||
|
|
||
| <head> | ||
| <title>Code coverage report for index.tsx</title> | ||
| <meta charset="utf-8" /> | ||
| <link rel="stylesheet" href="prettify.css" /> | ||
| <link rel="stylesheet" href="base.css" /> | ||
| <link rel="shortcut icon" type="image/x-icon" href="favicon.png" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <style type='text/css'> | ||
| .coverage-summary .sorter { | ||
| background-image: url(sort-arrow-sprite.png); | ||
| } | ||
| </style> | ||
| </head> | ||
|
|
||
| <body> | ||
| <div class='wrapper'> | ||
| <div class='pad1'> | ||
| <h1><a href="index.html">All files</a> index.tsx</h1> | ||
| <div class='clearfix'> | ||
|
|
||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">100% </span> | ||
| <span class="quiet">Statements</span> | ||
| <span class='fraction'>79/79</span> | ||
| </div> | ||
|
|
||
|
|
||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">100% </span> | ||
| <span class="quiet">Branches</span> | ||
| <span class='fraction'>27/27</span> | ||
| </div> | ||
|
|
||
|
|
||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">100% </span> | ||
| <span class="quiet">Functions</span> | ||
| <span class='fraction'>13/13</span> | ||
| </div> | ||
|
|
||
|
|
||
| <div class='fl pad1y space-right2'> | ||
| <span class="strong">100% </span> | ||
| <span class="quiet">Lines</span> | ||
| <span class='fraction'>79/79</span> | ||
| </div> | ||
|
|
||
|
|
||
| </div> | ||
| <p class="quiet"> | ||
| Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. | ||
| </p> | ||
| <template id="filterTemplate"> | ||
| <div class="quiet"> | ||
| Filter: | ||
| <input type="search" id="fileSearch"> | ||
| </div> | ||
| </template> | ||
| </div> | ||
| <div class='status-line high'></div> | ||
| <pre><table class="coverage"> | ||
| <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> | ||
| <a name='L2'></a><a href='#L2'>2</a> | ||
| <a name='L3'></a><a href='#L3'>3</a> | ||
| <a name='L4'></a><a href='#L4'>4</a> | ||
| <a name='L5'></a><a href='#L5'>5</a> | ||
| <a name='L6'></a><a href='#L6'>6</a> | ||
| <a name='L7'></a><a href='#L7'>7</a> | ||
| <a name='L8'></a><a href='#L8'>8</a> | ||
| <a name='L9'></a><a href='#L9'>9</a> | ||
| <a name='L10'></a><a href='#L10'>10</a> | ||
| <a name='L11'></a><a href='#L11'>11</a> | ||
| <a name='L12'></a><a href='#L12'>12</a> | ||
| <a name='L13'></a><a href='#L13'>13</a> | ||
| <a name='L14'></a><a href='#L14'>14</a> | ||
| <a name='L15'></a><a href='#L15'>15</a> | ||
| <a name='L16'></a><a href='#L16'>16</a> | ||
| <a name='L17'></a><a href='#L17'>17</a> | ||
| <a name='L18'></a><a href='#L18'>18</a> | ||
| <a name='L19'></a><a href='#L19'>19</a> | ||
| <a name='L20'></a><a href='#L20'>20</a> | ||
| <a name='L21'></a><a href='#L21'>21</a> | ||
| <a name='L22'></a><a href='#L22'>22</a> | ||
| <a name='L23'></a><a href='#L23'>23</a> | ||
| <a name='L24'></a><a href='#L24'>24</a> | ||
| <a name='L25'></a><a href='#L25'>25</a> | ||
| <a name='L26'></a><a href='#L26'>26</a> | ||
| <a name='L27'></a><a href='#L27'>27</a> | ||
| <a name='L28'></a><a href='#L28'>28</a> | ||
| <a name='L29'></a><a href='#L29'>29</a> | ||
| <a name='L30'></a><a href='#L30'>30</a> | ||
| <a name='L31'></a><a href='#L31'>31</a> | ||
| <a name='L32'></a><a href='#L32'>32</a> | ||
| <a name='L33'></a><a href='#L33'>33</a> | ||
| <a name='L34'></a><a href='#L34'>34</a> | ||
| <a name='L35'></a><a href='#L35'>35</a> | ||
| <a name='L36'></a><a href='#L36'>36</a> | ||
| <a name='L37'></a><a href='#L37'>37</a> | ||
| <a name='L38'></a><a href='#L38'>38</a> | ||
| <a name='L39'></a><a href='#L39'>39</a> | ||
| <a name='L40'></a><a href='#L40'>40</a> | ||
| <a name='L41'></a><a href='#L41'>41</a> | ||
| <a name='L42'></a><a href='#L42'>42</a> | ||
| <a name='L43'></a><a href='#L43'>43</a> | ||
| <a name='L44'></a><a href='#L44'>44</a> | ||
| <a name='L45'></a><a href='#L45'>45</a> | ||
| <a name='L46'></a><a href='#L46'>46</a> | ||
| <a name='L47'></a><a href='#L47'>47</a> | ||
| <a name='L48'></a><a href='#L48'>48</a> | ||
| <a name='L49'></a><a href='#L49'>49</a> | ||
| <a name='L50'></a><a href='#L50'>50</a> | ||
| <a name='L51'></a><a href='#L51'>51</a> | ||
| <a name='L52'></a><a href='#L52'>52</a> | ||
| <a name='L53'></a><a href='#L53'>53</a> | ||
| <a name='L54'></a><a href='#L54'>54</a> | ||
| <a name='L55'></a><a href='#L55'>55</a> | ||
| <a name='L56'></a><a href='#L56'>56</a> | ||
| <a name='L57'></a><a href='#L57'>57</a> | ||
| <a name='L58'></a><a href='#L58'>58</a> | ||
| <a name='L59'></a><a href='#L59'>59</a> | ||
| <a name='L60'></a><a href='#L60'>60</a> | ||
| <a name='L61'></a><a href='#L61'>61</a> | ||
| <a name='L62'></a><a href='#L62'>62</a> | ||
| <a name='L63'></a><a href='#L63'>63</a> | ||
| <a name='L64'></a><a href='#L64'>64</a> | ||
| <a name='L65'></a><a href='#L65'>65</a> | ||
| <a name='L66'></a><a href='#L66'>66</a> | ||
| <a name='L67'></a><a href='#L67'>67</a> | ||
| <a name='L68'></a><a href='#L68'>68</a> | ||
| <a name='L69'></a><a href='#L69'>69</a> | ||
| <a name='L70'></a><a href='#L70'>70</a> | ||
| <a name='L71'></a><a href='#L71'>71</a> | ||
| <a name='L72'></a><a href='#L72'>72</a> | ||
| <a name='L73'></a><a href='#L73'>73</a> | ||
| <a name='L74'></a><a href='#L74'>74</a> | ||
| <a name='L75'></a><a href='#L75'>75</a> | ||
| <a name='L76'></a><a href='#L76'>76</a> | ||
| <a name='L77'></a><a href='#L77'>77</a> | ||
| <a name='L78'></a><a href='#L78'>78</a> | ||
| <a name='L79'></a><a href='#L79'>79</a> | ||
| <a name='L80'></a><a href='#L80'>80</a> | ||
| <a name='L81'></a><a href='#L81'>81</a> | ||
| <a name='L82'></a><a href='#L82'>82</a> | ||
| <a name='L83'></a><a href='#L83'>83</a> | ||
| <a name='L84'></a><a href='#L84'>84</a> | ||
| <a name='L85'></a><a href='#L85'>85</a> | ||
| <a name='L86'></a><a href='#L86'>86</a> | ||
| <a name='L87'></a><a href='#L87'>87</a> | ||
| <a name='L88'></a><a href='#L88'>88</a> | ||
| <a name='L89'></a><a href='#L89'>89</a> | ||
| <a name='L90'></a><a href='#L90'>90</a> | ||
| <a name='L91'></a><a href='#L91'>91</a> | ||
| <a name='L92'></a><a href='#L92'>92</a> | ||
| <a name='L93'></a><a href='#L93'>93</a> | ||
| <a name='L94'></a><a href='#L94'>94</a> | ||
| <a name='L95'></a><a href='#L95'>95</a> | ||
| <a name='L96'></a><a href='#L96'>96</a> | ||
| <a name='L97'></a><a href='#L97'>97</a> | ||
| <a name='L98'></a><a href='#L98'>98</a> | ||
| <a name='L99'></a><a href='#L99'>99</a> | ||
| <a name='L100'></a><a href='#L100'>100</a> | ||
| <a name='L101'></a><a href='#L101'>101</a> | ||
| <a name='L102'></a><a href='#L102'>102</a> | ||
| <a name='L103'></a><a href='#L103'>103</a> | ||
| <a name='L104'></a><a href='#L104'>104</a> | ||
| <a name='L105'></a><a href='#L105'>105</a> | ||
| <a name='L106'></a><a href='#L106'>106</a> | ||
| <a name='L107'></a><a href='#L107'>107</a> | ||
| <a name='L108'></a><a href='#L108'>108</a> | ||
| <a name='L109'></a><a href='#L109'>109</a> | ||
| <a name='L110'></a><a href='#L110'>110</a> | ||
| <a name='L111'></a><a href='#L111'>111</a> | ||
| <a name='L112'></a><a href='#L112'>112</a> | ||
| <a name='L113'></a><a href='#L113'>113</a> | ||
| <a name='L114'></a><a href='#L114'>114</a> | ||
| <a name='L115'></a><a href='#L115'>115</a> | ||
| <a name='L116'></a><a href='#L116'>116</a> | ||
| <a name='L117'></a><a href='#L117'>117</a> | ||
| <a name='L118'></a><a href='#L118'>118</a> | ||
| <a name='L119'></a><a href='#L119'>119</a> | ||
| <a name='L120'></a><a href='#L120'>120</a> | ||
| <a name='L121'></a><a href='#L121'>121</a> | ||
| <a name='L122'></a><a href='#L122'>122</a> | ||
| <a name='L123'></a><a href='#L123'>123</a> | ||
| <a name='L124'></a><a href='#L124'>124</a> | ||
| <a name='L125'></a><a href='#L125'>125</a> | ||
| <a name='L126'></a><a href='#L126'>126</a> | ||
| <a name='L127'></a><a href='#L127'>127</a> | ||
| <a name='L128'></a><a href='#L128'>128</a> | ||
| <a name='L129'></a><a href='#L129'>129</a> | ||
| <a name='L130'></a><a href='#L130'>130</a> | ||
| <a name='L131'></a><a href='#L131'>131</a> | ||
| <a name='L132'></a><a href='#L132'>132</a> | ||
| <a name='L133'></a><a href='#L133'>133</a> | ||
| <a name='L134'></a><a href='#L134'>134</a> | ||
| <a name='L135'></a><a href='#L135'>135</a> | ||
| <a name='L136'></a><a href='#L136'>136</a> | ||
| <a name='L137'></a><a href='#L137'>137</a> | ||
| <a name='L138'></a><a href='#L138'>138</a> | ||
| <a name='L139'></a><a href='#L139'>139</a> | ||
| <a name='L140'></a><a href='#L140'>140</a> | ||
| <a name='L141'></a><a href='#L141'>141</a> | ||
| <a name='L142'></a><a href='#L142'>142</a> | ||
| <a name='L143'></a><a href='#L143'>143</a> | ||
| <a name='L144'></a><a href='#L144'>144</a> | ||
| <a name='L145'></a><a href='#L145'>145</a> | ||
| <a name='L146'></a><a href='#L146'>146</a> | ||
| <a name='L147'></a><a href='#L147'>147</a> | ||
| <a name='L148'></a><a href='#L148'>148</a> | ||
| <a name='L149'></a><a href='#L149'>149</a> | ||
| <a name='L150'></a><a href='#L150'>150</a> | ||
| <a name='L151'></a><a href='#L151'>151</a> | ||
| <a name='L152'></a><a href='#L152'>152</a> | ||
| <a name='L153'></a><a href='#L153'>153</a> | ||
| <a name='L154'></a><a href='#L154'>154</a> | ||
| <a name='L155'></a><a href='#L155'>155</a> | ||
| <a name='L156'></a><a href='#L156'>156</a> | ||
| <a name='L157'></a><a href='#L157'>157</a> | ||
| <a name='L158'></a><a href='#L158'>158</a> | ||
| <a name='L159'></a><a href='#L159'>159</a> | ||
| <a name='L160'></a><a href='#L160'>160</a> | ||
| <a name='L161'></a><a href='#L161'>161</a> | ||
| <a name='L162'></a><a href='#L162'>162</a> | ||
| <a name='L163'></a><a href='#L163'>163</a> | ||
| <a name='L164'></a><a href='#L164'>164</a> | ||
| <a name='L165'></a><a href='#L165'>165</a> | ||
| <a name='L166'></a><a href='#L166'>166</a> | ||
| <a name='L167'></a><a href='#L167'>167</a> | ||
| <a name='L168'></a><a href='#L168'>168</a> | ||
| <a name='L169'></a><a href='#L169'>169</a> | ||
| <a name='L170'></a><a href='#L170'>170</a> | ||
| <a name='L171'></a><a href='#L171'>171</a> | ||
| <a name='L172'></a><a href='#L172'>172</a> | ||
| <a name='L173'></a><a href='#L173'>173</a> | ||
| <a name='L174'></a><a href='#L174'>174</a> | ||
| <a name='L175'></a><a href='#L175'>175</a> | ||
| <a name='L176'></a><a href='#L176'>176</a> | ||
| <a name='L177'></a><a href='#L177'>177</a> | ||
| <a name='L178'></a><a href='#L178'>178</a> | ||
| <a name='L179'></a><a href='#L179'>179</a> | ||
| <a name='L180'></a><a href='#L180'>180</a> | ||
| <a name='L181'></a><a href='#L181'>181</a> | ||
| <a name='L182'></a><a href='#L182'>182</a> | ||
| <a name='L183'></a><a href='#L183'>183</a> | ||
| <a name='L184'></a><a href='#L184'>184</a> | ||
| <a name='L185'></a><a href='#L185'>185</a> | ||
| <a name='L186'></a><a href='#L186'>186</a> | ||
| <a name='L187'></a><a href='#L187'>187</a> | ||
| <a name='L188'></a><a href='#L188'>188</a> | ||
| <a name='L189'></a><a href='#L189'>189</a> | ||
| <a name='L190'></a><a href='#L190'>190</a> | ||
| <a name='L191'></a><a href='#L191'>191</a> | ||
| <a name='L192'></a><a href='#L192'>192</a> | ||
| <a name='L193'></a><a href='#L193'>193</a> | ||
| <a name='L194'></a><a href='#L194'>194</a> | ||
| <a name='L195'></a><a href='#L195'>195</a> | ||
| <a name='L196'></a><a href='#L196'>196</a> | ||
| <a name='L197'></a><a href='#L197'>197</a> | ||
| <a name='L198'></a><a href='#L198'>198</a> | ||
| <a name='L199'></a><a href='#L199'>199</a> | ||
| <a name='L200'></a><a href='#L200'>200</a> | ||
| <a name='L201'></a><a href='#L201'>201</a> | ||
| <a name='L202'></a><a href='#L202'>202</a> | ||
| <a name='L203'></a><a href='#L203'>203</a> | ||
| <a name='L204'></a><a href='#L204'>204</a> | ||
| <a name='L205'></a><a href='#L205'>205</a> | ||
| <a name='L206'></a><a href='#L206'>206</a> | ||
| <a name='L207'></a><a href='#L207'>207</a> | ||
| <a name='L208'></a><a href='#L208'>208</a> | ||
| <a name='L209'></a><a href='#L209'>209</a> | ||
| <a name='L210'></a><a href='#L210'>210</a> | ||
| <a name='L211'></a><a href='#L211'>211</a> | ||
| <a name='L212'></a><a href='#L212'>212</a> | ||
| <a name='L213'></a><a href='#L213'>213</a> | ||
| <a name='L214'></a><a href='#L214'>214</a> | ||
| <a name='L215'></a><a href='#L215'>215</a> | ||
| <a name='L216'></a><a href='#L216'>216</a> | ||
| <a name='L217'></a><a href='#L217'>217</a> | ||
| <a name='L218'></a><a href='#L218'>218</a> | ||
| <a name='L219'></a><a href='#L219'>219</a> | ||
| <a name='L220'></a><a href='#L220'>220</a> | ||
| <a name='L221'></a><a href='#L221'>221</a> | ||
| <a name='L222'></a><a href='#L222'>222</a> | ||
| <a name='L223'></a><a href='#L223'>223</a> | ||
| <a name='L224'></a><a href='#L224'>224</a> | ||
| <a name='L225'></a><a href='#L225'>225</a> | ||
| <a name='L226'></a><a href='#L226'>226</a> | ||
| <a name='L227'></a><a href='#L227'>227</a> | ||
| <a name='L228'></a><a href='#L228'>228</a> | ||
| <a name='L229'></a><a href='#L229'>229</a> | ||
| <a name='L230'></a><a href='#L230'>230</a> | ||
| <a name='L231'></a><a href='#L231'>231</a> | ||
| <a name='L232'></a><a href='#L232'>232</a> | ||
| <a name='L233'></a><a href='#L233'>233</a> | ||
| <a name='L234'></a><a href='#L234'>234</a> | ||
| <a name='L235'></a><a href='#L235'>235</a> | ||
| <a name='L236'></a><a href='#L236'>236</a> | ||
| <a name='L237'></a><a href='#L237'>237</a> | ||
| <a name='L238'></a><a href='#L238'>238</a> | ||
| <a name='L239'></a><a href='#L239'>239</a> | ||
| <a name='L240'></a><a href='#L240'>240</a> | ||
| <a name='L241'></a><a href='#L241'>241</a> | ||
| <a name='L242'></a><a href='#L242'>242</a> | ||
| <a name='L243'></a><a href='#L243'>243</a> | ||
| <a name='L244'></a><a href='#L244'>244</a> | ||
| <a name='L245'></a><a href='#L245'>245</a> | ||
| <a name='L246'></a><a href='#L246'>246</a> | ||
| <a name='L247'></a><a href='#L247'>247</a> | ||
| <a name='L248'></a><a href='#L248'>248</a> | ||
| <a name='L249'></a><a href='#L249'>249</a> | ||
| <a name='L250'></a><a href='#L250'>250</a> | ||
| <a name='L251'></a><a href='#L251'>251</a> | ||
| <a name='L252'></a><a href='#L252'>252</a> | ||
| <a name='L253'></a><a href='#L253'>253</a> | ||
| <a name='L254'></a><a href='#L254'>254</a> | ||
| <a name='L255'></a><a href='#L255'>255</a> | ||
| <a name='L256'></a><a href='#L256'>256</a> | ||
| <a name='L257'></a><a href='#L257'>257</a> | ||
| <a name='L258'></a><a href='#L258'>258</a> | ||
| <a name='L259'></a><a href='#L259'>259</a> | ||
| <a name='L260'></a><a href='#L260'>260</a> | ||
| <a name='L261'></a><a href='#L261'>261</a> | ||
| <a name='L262'></a><a href='#L262'>262</a> | ||
| <a name='L263'></a><a href='#L263'>263</a> | ||
| <a name='L264'></a><a href='#L264'>264</a> | ||
| <a name='L265'></a><a href='#L265'>265</a> | ||
| <a name='L266'></a><a href='#L266'>266</a> | ||
| <a name='L267'></a><a href='#L267'>267</a> | ||
| <a name='L268'></a><a href='#L268'>268</a> | ||
| <a name='L269'></a><a href='#L269'>269</a> | ||
| <a name='L270'></a><a href='#L270'>270</a> | ||
| <a name='L271'></a><a href='#L271'>271</a> | ||
| <a name='L272'></a><a href='#L272'>272</a> | ||
| <a name='L273'></a><a href='#L273'>273</a> | ||
| <a name='L274'></a><a href='#L274'>274</a> | ||
| <a name='L275'></a><a href='#L275'>275</a> | ||
| <a name='L276'></a><a href='#L276'>276</a> | ||
| <a name='L277'></a><a href='#L277'>277</a> | ||
| <a name='L278'></a><a href='#L278'>278</a> | ||
| <a name='L279'></a><a href='#L279'>279</a> | ||
| <a name='L280'></a><a href='#L280'>280</a> | ||
| <a name='L281'></a><a href='#L281'>281</a> | ||
| <a name='L282'></a><a href='#L282'>282</a> | ||
| <a name='L283'></a><a href='#L283'>283</a> | ||
| <a name='L284'></a><a href='#L284'>284</a> | ||
| <a name='L285'></a><a href='#L285'>285</a> | ||
| <a name='L286'></a><a href='#L286'>286</a> | ||
| <a name='L287'></a><a href='#L287'>287</a> | ||
| <a name='L288'></a><a href='#L288'>288</a> | ||
| <a name='L289'></a><a href='#L289'>289</a> | ||
| <a name='L290'></a><a href='#L290'>290</a> | ||
| <a name='L291'></a><a href='#L291'>291</a> | ||
| <a name='L292'></a><a href='#L292'>292</a> | ||
| <a name='L293'></a><a href='#L293'>293</a> | ||
| <a name='L294'></a><a href='#L294'>294</a> | ||
| <a name='L295'></a><a href='#L295'>295</a> | ||
| <a name='L296'></a><a href='#L296'>296</a> | ||
| <a name='L297'></a><a href='#L297'>297</a> | ||
| <a name='L298'></a><a href='#L298'>298</a> | ||
| <a name='L299'></a><a href='#L299'>299</a> | ||
| <a name='L300'></a><a href='#L300'>300</a> | ||
| <a name='L301'></a><a href='#L301'>301</a> | ||
| <a name='L302'></a><a href='#L302'>302</a> | ||
| <a name='L303'></a><a href='#L303'>303</a> | ||
| <a name='L304'></a><a href='#L304'>304</a> | ||
| <a name='L305'></a><a href='#L305'>305</a> | ||
| <a name='L306'></a><a href='#L306'>306</a> | ||
| <a name='L307'></a><a href='#L307'>307</a> | ||
| <a name='L308'></a><a href='#L308'>308</a> | ||
| <a name='L309'></a><a href='#L309'>309</a> | ||
| <a name='L310'></a><a href='#L310'>310</a> | ||
| <a name='L311'></a><a href='#L311'>311</a> | ||
| <a name='L312'></a><a href='#L312'>312</a> | ||
| <a name='L313'></a><a href='#L313'>313</a> | ||
| <a name='L314'></a><a href='#L314'>314</a> | ||
| <a name='L315'></a><a href='#L315'>315</a> | ||
| <a name='L316'></a><a href='#L316'>316</a> | ||
| <a name='L317'></a><a href='#L317'>317</a> | ||
| <a name='L318'></a><a href='#L318'>318</a> | ||
| <a name='L319'></a><a href='#L319'>319</a> | ||
| <a name='L320'></a><a href='#L320'>320</a> | ||
| <a name='L321'></a><a href='#L321'>321</a> | ||
| <a name='L322'></a><a href='#L322'>322</a> | ||
| <a name='L323'></a><a href='#L323'>323</a> | ||
| <a name='L324'></a><a href='#L324'>324</a> | ||
| <a name='L325'></a><a href='#L325'>325</a> | ||
| <a name='L326'></a><a href='#L326'>326</a> | ||
| <a name='L327'></a><a href='#L327'>327</a> | ||
| <a name='L328'></a><a href='#L328'>328</a> | ||
| <a name='L329'></a><a href='#L329'>329</a> | ||
| <a name='L330'></a><a href='#L330'>330</a> | ||
| <a name='L331'></a><a href='#L331'>331</a> | ||
| <a name='L332'></a><a href='#L332'>332</a> | ||
| <a name='L333'></a><a href='#L333'>333</a> | ||
| <a name='L334'></a><a href='#L334'>334</a> | ||
| <a name='L335'></a><a href='#L335'>335</a> | ||
| <a name='L336'></a><a href='#L336'>336</a> | ||
| <a name='L337'></a><a href='#L337'>337</a> | ||
| <a name='L338'></a><a href='#L338'>338</a> | ||
| <a name='L339'></a><a href='#L339'>339</a> | ||
| <a name='L340'></a><a href='#L340'>340</a> | ||
| <a name='L341'></a><a href='#L341'>341</a> | ||
| <a name='L342'></a><a href='#L342'>342</a> | ||
| <a name='L343'></a><a href='#L343'>343</a> | ||
| <a name='L344'></a><a href='#L344'>344</a> | ||
| <a name='L345'></a><a href='#L345'>345</a> | ||
| <a name='L346'></a><a href='#L346'>346</a> | ||
| <a name='L347'></a><a href='#L347'>347</a> | ||
| <a name='L348'></a><a href='#L348'>348</a> | ||
| <a name='L349'></a><a href='#L349'>349</a> | ||
| <a name='L350'></a><a href='#L350'>350</a> | ||
| <a name='L351'></a><a href='#L351'>351</a> | ||
| <a name='L352'></a><a href='#L352'>352</a> | ||
| <a name='L353'></a><a href='#L353'>353</a> | ||
| <a name='L354'></a><a href='#L354'>354</a> | ||
| <a name='L355'></a><a href='#L355'>355</a> | ||
| <a name='L356'></a><a href='#L356'>356</a> | ||
| <a name='L357'></a><a href='#L357'>357</a> | ||
| <a name='L358'></a><a href='#L358'>358</a> | ||
| <a name='L359'></a><a href='#L359'>359</a> | ||
| <a name='L360'></a><a href='#L360'>360</a> | ||
| <a name='L361'></a><a href='#L361'>361</a> | ||
| <a name='L362'></a><a href='#L362'>362</a> | ||
| <a name='L363'></a><a href='#L363'>363</a> | ||
| <a name='L364'></a><a href='#L364'>364</a> | ||
| <a name='L365'></a><a href='#L365'>365</a> | ||
| <a name='L366'></a><a href='#L366'>366</a> | ||
| <a name='L367'></a><a href='#L367'>367</a> | ||
| <a name='L368'></a><a href='#L368'>368</a> | ||
| <a name='L369'></a><a href='#L369'>369</a> | ||
| <a name='L370'></a><a href='#L370'>370</a> | ||
| <a name='L371'></a><a href='#L371'>371</a> | ||
| <a name='L372'></a><a href='#L372'>372</a> | ||
| <a name='L373'></a><a href='#L373'>373</a> | ||
| <a name='L374'></a><a href='#L374'>374</a> | ||
| <a name='L375'></a><a href='#L375'>375</a> | ||
| <a name='L376'></a><a href='#L376'>376</a> | ||
| <a name='L377'></a><a href='#L377'>377</a> | ||
| <a name='L378'></a><a href='#L378'>378</a> | ||
| <a name='L379'></a><a href='#L379'>379</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">49x</span> | ||
| <span class="cline-any cline-yes">49x</span> | ||
| <span class="cline-any cline-yes">49x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">49x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">145x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">145x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">145x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">140x</span> | ||
| <span class="cline-any cline-yes">140x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">140x</span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">141x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">140x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-yes">73x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-yes">3x</span> | ||
| <span class="cline-any cline-yes">3x</span> | ||
| <span class="cline-any cline-yes">9x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">3x</span> | ||
| <span class="cline-any cline-yes">2x</span> | ||
| <span class="cline-any cline-yes">2x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-yes">3x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">4x</span> | ||
| <span class="cline-any cline-yes">4x</span> | ||
| <span class="cline-any cline-yes">4x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">4x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-yes">3x</span> | ||
| <span class="cline-any cline-yes">2x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">1x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-yes">6x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">50x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-yes">140x</span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span> | ||
| <span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useRef } from 'react'; | ||
| import { Dimensions } from 'react-native'; | ||
| import { Gesture, GestureDetector } from 'react-native-gesture-handler'; | ||
| import Animated, { | ||
| useAnimatedStyle, | ||
| useSharedValue, | ||
| withSpring, | ||
| interpolate, | ||
| Extrapolation, | ||
| type SharedValue, | ||
| } from 'react-native-reanimated'; | ||
| import Svg, { | ||
| Circle, | ||
| Defs, | ||
| LinearGradient as SvgLinearGradient, | ||
| Stop, | ||
| } from 'react-native-svg'; | ||
| | ||
| import { scheduleOnRN } from 'react-native-worklets'; | ||
| | ||
| import { View, type ImageSourcePropType } from 'react-native'; | ||
| | ||
| // Type for pan gesture event payload | ||
| type PanGestureEvent = { | ||
| translationX: number; | ||
| velocityX: number; | ||
| }; | ||
| | ||
| const SCREEN_WIDTH = Dimensions.get('window').width; | ||
| | ||
| // Default values | ||
| const DEFAULT_ITEM_SIZE = 192; | ||
| const DEFAULT_ITEM_SPACING = 200; | ||
| const DEFAULT_MIN_SCALE = 0.7; | ||
| const DEFAULT_MAX_SCALE = 1.0; | ||
| const DEFAULT_GRADIENT_COLORS = ['orange', 'blue', 'violet']; | ||
| const DEFAULT_GRADIENT_ID = 'carousel-gradient'; | ||
| | ||
| // Animation configuration | ||
| const SPRING_DAMPING = 20; | ||
| const SPRING_STIFFNESS = 90; | ||
| const VELOCITY_THRESHOLD = 500; | ||
| | ||
| // Opacity values for item interpolation | ||
| const MAX_OPACITY = 1; | ||
| const MIN_OPACITY = 0.6; | ||
| | ||
| // Gradient border configuration | ||
| const GRADIENT_BORDER_PADDING = 8; | ||
| const GRADIENT_STROKE_WIDTH = 4; | ||
| const GRADIENT_RADIUS_OFFSET = 2; | ||
| | ||
| // Layout spacing | ||
| const DETAIL_VIEW_MARGIN_TOP = 16; | ||
| | ||
| // Gradient SVG configuration | ||
| const GRADIENT_STOP_OPACITY = '1'; | ||
| const GRADIENT_START_X = '0%'; | ||
| const GRADIENT_START_Y = '0%'; | ||
| const GRADIENT_END_X = '100%'; | ||
| const GRADIENT_END_Y = '100%'; | ||
| | ||
| // Type for image source - can be a URL string or local image source | ||
| export type ImageSource = string | ImageSourcePropType; | ||
| | ||
| // Base type that items must extend - requires an image property | ||
| export type CarouselItemBase = { | ||
| image: ImageSource; | ||
| }; | ||
| | ||
| type CarouselProps<T extends CarouselItemBase> = { | ||
| items: T[]; | ||
| selectedItem: T; | ||
| onItemChange: (item: T) => void; | ||
| getItemId: (item: T) => string; | ||
| renderItem: ( | ||
| item: T, | ||
| index: number, | ||
| translateX: SharedValue<number> | ||
| ) => React.ReactNode; | ||
| renderDetail?: (item: T) => React.ReactNode; | ||
| itemSize?: number; | ||
| itemSpacing?: number; | ||
| minScale?: number; | ||
| maxScale?: number; | ||
| showGradientBorder?: boolean; | ||
| gradientColors?: string[]; | ||
| gradientId?: string; | ||
| }; | ||
| | ||
| type GradientBorderProps = { | ||
| itemSize: number; | ||
| gradientColors: string[]; | ||
| gradientId: string; | ||
| }; | ||
| | ||
| function GradientBorder({ | ||
| itemSize, | ||
| gradientColors, | ||
| gradientId, | ||
| }: GradientBorderProps) { | ||
| const gradientSize = itemSize + GRADIENT_BORDER_PADDING; | ||
| const gradientCenter = gradientSize / 2; | ||
| const gradientRadius = gradientCenter - GRADIENT_RADIUS_OFFSET; | ||
| | ||
| return ( | ||
| <View | ||
| style={{ | ||
| position: 'absolute', | ||
| width: gradientSize, | ||
| height: gradientSize, | ||
| }} | ||
| pointerEvents="none" | ||
| > | ||
| <Svg width={gradientSize} height={gradientSize}> | ||
| <Defs> | ||
| <SvgLinearGradient | ||
| id={gradientId} | ||
| x1={GRADIENT_START_X} | ||
| y1={GRADIENT_START_Y} | ||
| x2={GRADIENT_END_X} | ||
| y2={GRADIENT_END_Y} | ||
| > | ||
| {gradientColors.map((color, index) => { | ||
| const colorCount = gradientColors.length; | ||
| const offset = | ||
| colorCount === 1 | ||
| ? '0%' | ||
| : `${(index / (colorCount - 1)) * 100}%`; | ||
| return ( | ||
| <Stop | ||
| key={color} | ||
| offset={offset} | ||
| stopColor={color} | ||
| stopOpacity={GRADIENT_STOP_OPACITY} | ||
| /> | ||
| ); | ||
| })} | ||
| </SvgLinearGradient> | ||
| </Defs> | ||
| <Circle | ||
| cx={gradientCenter} | ||
| cy={gradientCenter} | ||
| r={gradientRadius} | ||
| stroke={`url(#${gradientId})`} | ||
| strokeWidth={GRADIENT_STROKE_WIDTH} | ||
| fill="none" | ||
| /> | ||
| </Svg> | ||
| </View> | ||
| ); | ||
| } | ||
| | ||
| type CarouselItemProps<T extends CarouselItemBase> = { | ||
| item: T; | ||
| index: number; | ||
| translateX: SharedValue<number>; | ||
| itemSize: number; | ||
| itemSpacing: number; | ||
| minScale: number; | ||
| maxScale: number; | ||
| renderItem: ( | ||
| item: T, | ||
| index: number, | ||
| translateX: SharedValue<number> | ||
| ) => React.ReactNode; | ||
| }; | ||
| | ||
| function CarouselItem<T extends CarouselItemBase>({ | ||
| item, | ||
| index, | ||
| translateX, | ||
| itemSize, | ||
| itemSpacing, | ||
| minScale, | ||
| maxScale, | ||
| renderItem, | ||
| }: CarouselItemProps<T>) { | ||
| const CENTER_X = SCREEN_WIDTH / 2; | ||
| const CENTER_OFFSET = CENTER_X - itemSize / 2; | ||
| | ||
| const animatedStyle = useAnimatedStyle(() => { | ||
| const basePosition = CENTER_OFFSET; | ||
| const offset = index * itemSpacing; | ||
| const position = basePosition + translateX.value + offset; | ||
| | ||
| const distanceFromCenter = Math.abs(position + itemSize / 2 - CENTER_X); | ||
| const maxDistance = itemSpacing; | ||
| | ||
| const scale = interpolate( | ||
| distanceFromCenter, | ||
| [0, maxDistance], | ||
| [maxScale, minScale], | ||
| Extrapolation.CLAMP | ||
| ); | ||
| | ||
| const opacity = interpolate( | ||
| distanceFromCenter, | ||
| [0, maxDistance], | ||
| [MAX_OPACITY, MIN_OPACITY], | ||
| Extrapolation.CLAMP | ||
| ); | ||
| | ||
| return { | ||
| transform: [{ translateX: position }, { scale }], | ||
| opacity, | ||
| }; | ||
| }); | ||
| | ||
| return ( | ||
| <Animated.View | ||
| style={[ | ||
| { | ||
| position: 'absolute', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| width: itemSize, | ||
| height: itemSize, | ||
| left: 0, | ||
| top: 0, | ||
| }, | ||
| animatedStyle, | ||
| ]} | ||
| > | ||
| {renderItem(item, index, translateX)} | ||
| </Animated.View> | ||
| ); | ||
| } | ||
| | ||
| export function Carousel<T extends CarouselItemBase>({ | ||
| items, | ||
| selectedItem, | ||
| onItemChange, | ||
| getItemId, | ||
| renderItem, | ||
| renderDetail, | ||
| itemSize = DEFAULT_ITEM_SIZE, | ||
| itemSpacing = DEFAULT_ITEM_SPACING, | ||
| minScale = DEFAULT_MIN_SCALE, | ||
| maxScale = DEFAULT_MAX_SCALE, | ||
| showGradientBorder = true, | ||
| gradientColors = DEFAULT_GRADIENT_COLORS, | ||
| gradientId = DEFAULT_GRADIENT_ID, | ||
| }: CarouselProps<T>) { | ||
| const initialIndex = items.findIndex( | ||
| (item) => getItemId(item) === getItemId(selectedItem) | ||
| ); | ||
| const translateX = useSharedValue(-initialIndex * itemSpacing); | ||
| const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0); | ||
| const previousItemId = useRef(getItemId(selectedItem)); | ||
| | ||
| useEffect(() => { | ||
| const currentItemId = getItemId(selectedItem); | ||
| if (previousItemId.current !== currentItemId) { | ||
| previousItemId.current = currentItemId; | ||
| const index = items.findIndex( | ||
| (item) => getItemId(item) === currentItemId | ||
| ); | ||
| if (index !== -1) { | ||
| currentIndex.value = index; | ||
| translateX.value = withSpring(-index * itemSpacing, { | ||
| damping: SPRING_DAMPING, | ||
| stiffness: SPRING_STIFFNESS, | ||
| }); | ||
| } | ||
| } | ||
| }, [selectedItem, items, currentIndex, translateX, getItemId, itemSpacing]); | ||
| | ||
| const snapToIndex = (index: number) => { | ||
| 'worklet'; | ||
| const clampedIndex = Math.max(0, Math.min(index, items.length - 1)); | ||
| translateX.value = withSpring(-clampedIndex * itemSpacing, { | ||
| damping: SPRING_DAMPING, | ||
| stiffness: SPRING_STIFFNESS, | ||
| }); | ||
| currentIndex.value = clampedIndex; | ||
| }; | ||
| | ||
| const updateSelectedItem = (index: number) => { | ||
| const clampedIndex = Math.max(0, Math.min(index, items.length - 1)); | ||
| const item = items[clampedIndex]; | ||
| if (item && getItemId(item) !== getItemId(selectedItem)) { | ||
| onItemChange(item); | ||
| } | ||
| }; | ||
| | ||
| const panGesture = Gesture.Pan() | ||
| .onUpdate((event: PanGestureEvent) => { | ||
| const newTranslateX = | ||
| -currentIndex.value * itemSpacing + event.translationX; | ||
| const minTranslateX = -(items.length - 1) * itemSpacing; | ||
| const maxTranslateX = 0; | ||
| | ||
| translateX.value = Math.max( | ||
| minTranslateX, | ||
| Math.min(maxTranslateX, newTranslateX) | ||
| ); | ||
| }) | ||
| .onEnd((event: PanGestureEvent) => { | ||
| const currentTranslateX = translateX.value; | ||
| const velocity = event.velocityX; | ||
| | ||
| let targetIndex = Math.round(-currentTranslateX / itemSpacing); | ||
| | ||
| if (Math.abs(velocity) > VELOCITY_THRESHOLD) { | ||
| if (velocity < 0) { | ||
| targetIndex = Math.min(targetIndex + 1, items.length - 1); | ||
| } else { | ||
| targetIndex = Math.max(targetIndex - 1, 0); | ||
| } | ||
| } | ||
| | ||
| snapToIndex(targetIndex); | ||
| scheduleOnRN(updateSelectedItem, targetIndex); | ||
| }); | ||
| | ||
| return ( | ||
| <View | ||
| style={{ | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| width: '100%', | ||
| }} | ||
| > | ||
| <View | ||
| style={{ | ||
| height: itemSize, | ||
| position: 'relative', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| width: '100%', | ||
| }} | ||
| > | ||
| <GestureDetector gesture={panGesture}> | ||
| <Animated.View | ||
| style={{ | ||
| position: 'absolute', | ||
| width: SCREEN_WIDTH, | ||
| height: itemSize, | ||
| }} | ||
| > | ||
| {items.map((item, index) => ( | ||
| <CarouselItem | ||
| key={getItemId(item)} | ||
| item={item} | ||
| index={index} | ||
| translateX={translateX} | ||
| itemSize={itemSize} | ||
| itemSpacing={itemSpacing} | ||
| minScale={minScale} | ||
| maxScale={maxScale} | ||
| renderItem={renderItem} | ||
| /> | ||
| ))} | ||
| </Animated.View> | ||
| </GestureDetector> | ||
| {showGradientBorder && ( | ||
| <GradientBorder | ||
| itemSize={itemSize} | ||
| gradientColors={gradientColors} | ||
| gradientId={gradientId} | ||
| /> | ||
| )} | ||
| </View> | ||
| {renderDetail && ( | ||
| <View | ||
| style={{ | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| marginTop: DETAIL_VIEW_MARGIN_TOP, | ||
| }} | ||
| > | ||
| {renderDetail(selectedItem)} | ||
| </View> | ||
| )} | ||
| </View> | ||
| ); | ||
| } | ||
| </pre></td></tr></table></pre> | ||
|
|
||
| <div class='push'></div><!-- for sticky footer --> | ||
| </div><!-- /wrapper --> | ||
| <div class='footer quiet pad2 space-top1 center small'> | ||
| Code coverage generated by | ||
| <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> | ||
| at 2025-11-27T14:24:20.221Z | ||
| </div> | ||
| <script src="prettify.js"></script> | ||
| <script> | ||
| window.onload = function () { | ||
| prettyPrint(); | ||
| }; | ||
| </script> | ||
| <script src="sorter.js"></script> | ||
| <script src="block-navigation.js"></script> | ||
| </body> | ||
| </html> | ||
|
No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Coverage reports should not be committed to version control.
The coverage/ directory contains auto-generated test coverage artifacts from Istanbul. These files are regenerated on every test run and should be excluded from version control to:
- Avoid repository bloat
- Prevent merge conflicts
- Keep commits focused on source code changes
Add the coverage directory to .gitignore:
+# Coverage reports
+coverage/Then remove the committed coverage files:
git rm -r --cached coverage/🤖 Prompt for AI Agents
In coverage/lcov-report/index.tsx.html lines 1-1219: the coverage/ artifacts are
committed but should be ignored; update .gitignore to include the coverage/
directory, remove the tracked coverage files from git's index (stop tracking the
directory) and commit the .gitignore change and the removal so the generated
coverage files are no longer stored in the repository.
| const { Dimensions } = require('react-native'); | ||
| const mockDimensions = { | ||
| window: { | ||
| width: 375, | ||
| height: 812, | ||
| }, | ||
| }; | ||
|
|
||
| jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions); | ||
|
|
||
| // Mock react-native-worklets scheduleOnRN to execute synchronously in tests | ||
| jest.mock('react-native-worklets', () => { | ||
| const actualModule = jest.requireActual('react-native-worklets'); | ||
| return { | ||
| ...actualModule, | ||
| scheduleOnRN: jest.fn((fn, ...args) => { | ||
| // Execute synchronously in tests | ||
| if (typeof fn === 'function') { | ||
| fn(...args); | ||
| } | ||
| }), | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
React Native Dimensions.get() API documentation
💡 Result:
Dimensions.get(dim) — returns a ScaledSize for 'window' or 'screen'. Use: const {width, height} = Dimensions.get('window'); Signature: static get(dim: 'window' | 'screen'): ScaledSize. ScaledSize has { width, height, scale, fontScale }. Note: useWindowDimensions() is the preferred hook (it updates on size changes); on Android 'window' may exclude status/navigation bar size. (reactnative.dev)
Citations:
🏁 Script executed:
# Find and read jest-setup.js file
fd jest-setup.js --type f -x cat -n {}Repository: fyndx/react-native-swipe-carousel
Length of output: 2062
🏁 Script executed:
# Search for Dimensions usage in the codebase to see how it's being called
rg -n "Dimensions\.get" --type js --type ts -C 3Repository: fyndx/react-native-swipe-carousel
Length of output: 323
Fix Dimensions.get mock to match React Native API behavior
The current mock is incorrect and will cause runtime failures in tests. According to the React Native documentation, Dimensions.get('window') returns an object where width and height are top-level properties: { width, height, scale, fontScale }.
The codebase uses this API (e.g., src/index.tsx:29):
const SCREEN_WIDTH = Dimensions.get('window').width;However, the current mock returns:
mockDimensions = { window: { width: 375, height: 812 } }
jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);This means Dimensions.get('window') returns { window: { width: 375, height: 812 } }, so Dimensions.get('window').width evaluates to undefined instead of 375, breaking the code in tests.
Fix: Implement the mock to accept the dimension parameter and return the correct shape:
-const mockDimensions = {
- window: {
- width: 375,
- height: 812,
- },
-};
-
-jest.spyOn(Dimensions, 'get').mockReturnValue(mockDimensions);
+jest.spyOn(Dimensions, 'get').mockImplementation((dim) => {
+ const dimensions = {
+ width: 375,
+ height: 812,
+ scale: 1,
+ fontScale: 1,
+ };
+ if (dim === 'screen') {
+ return dimensions;
+ }
+ return dimensions; // window dimensions
+});🤖 Prompt for AI Agents
In jest-setup.js around lines 39 to 60, the Dimensions.get mock returns an
object with a nested window key which makes Dimensions.get('window').width
undefined; change the mock to accept the dimension key parameter and return the
correct shape for that key (i.e., when called with 'window' return {width: 375,
height: 812, scale: 1, fontScale: 1}, and for other keys return an appropriate
object or undefined) by replacing mockReturnValue with a mockImplementation that
inspects the argument and returns the top-level width/height/scale/fontScale
object to match the React Native API.
| const initialIndex = items.findIndex( | ||
| (item) => getItemId(item) === getItemId(selectedItem) | ||
| ); | ||
| const translateX = useSharedValue(-initialIndex * itemSpacing); | ||
| const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0); | ||
| const previousItemId = useRef(getItemId(selectedItem)); |
There was a problem hiding this comment.
Inconsistent handling when selectedItem is not found in items.
When selectedItem is not in the items array:
initialIndex= -1translateX= -(-1) × itemSpacing = +itemSpacing (positions carousel incorrectly)currentIndex= 0 (correctly clamped)
This creates a mismatch between the animation position and the tracked index.
Apply consistent clamping:
const initialIndex = items.findIndex(
(item) => getItemId(item) === getItemId(selectedItem)
);
- const translateX = useSharedValue(-initialIndex * itemSpacing);
- const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0);
+ const validInitialIndex = initialIndex >= 0 ? initialIndex : 0;
+ const translateX = useSharedValue(-validInitialIndex * itemSpacing);
+ const currentIndex = useSharedValue(validInitialIndex);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const initialIndex = items.findIndex( | |
| (item) => getItemId(item) === getItemId(selectedItem) | |
| ); | |
| const translateX = useSharedValue(-initialIndex * itemSpacing); | |
| const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0); | |
| const previousItemId = useRef(getItemId(selectedItem)); | |
| const initialIndex = items.findIndex( | |
| (item) => getItemId(item) === getItemId(selectedItem) | |
| ); | |
| const validInitialIndex = initialIndex >= 0 ? initialIndex : 0; | |
| const translateX = useSharedValue(-validInitialIndex * itemSpacing); | |
| const currentIndex = useSharedValue(validInitialIndex); | |
| const previousItemId = useRef(getItemId(selectedItem)); |
🤖 Prompt for AI Agents
In src/index.tsx around lines 245 to 250, initialIndex can be -1 when
selectedItem isn't found which causes translateX to be computed from
-initialIndex while currentIndex is clamped to 0, so clamp the index once and
reuse it: compute a clampedIndex = initialIndex >= 0 ? initialIndex : 0 (or
Math.max(initialIndex, 0)) and use clampedIndex for translateX and currentIndex
(and any other places that rely on the initial index) so animation position and
tracked index remain consistent.
- Removed coverage reports and related files from the repository to streamline project structure. - Updated .gitignore to include coverage directory for future exclusions.
…ling - Introduced a new GitHub Actions workflow for coverage reporting on pull requests. - Enhanced error handling in the fetchImages function to throw an error for non-OK responses. - Updated key generation in the GradientBorder component for better uniqueness. - Adjusted dependencies in the Carousel component's useEffect to prevent exhaustive-deps warnings. - Added assertions in tests to ensure onItemChange is not called under specific conditions.
|
CodeAnt AI is running Incremental review Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
CodeAnt AI Incremental review completed. |
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
src/index.tsx (1)
245-249: ClampinitialIndexbefore computingtranslateXto prevent misalignment.When
selectedItemis not found initems,initialIndexbecomes-1, causing:
translateX=-(-1) × itemSpacing=+itemSpacing(positions carousel incorrectly to the right)currentIndex=0(correctly clamped)This mismatch causes the carousel to render at the wrong initial position until the next update.
Apply this fix:
const initialIndex = items.findIndex( (item) => getItemId(item) === getItemId(selectedItem) ); - const translateX = useSharedValue(-initialIndex * itemSpacing); - const currentIndex = useSharedValue(initialIndex >= 0 ? initialIndex : 0); + const validInitialIndex = Math.max(0, initialIndex); + const translateX = useSharedValue(-validInitialIndex * itemSpacing); + const currentIndex = useSharedValue(validInitialIndex);
🧹 Nitpick comments (1)
example/src/App.tsx (1)
33-36: Consider simplifying the data transformation.The
imagefield is mapped fromphoto.download_url, butdownload_urlis already available onPicsumPhoto. If the Carousel can acceptdownload_urldirectly, you could eliminate this transformation. If the separateimagefield is intentional for API consistency, consider adding a comment explaining the normalization.If the Carousel's
renderItemcan referenceitem.download_urlinstead ofitem.image, you could simplify:- const transformedItems: PicsumPhoto[] = data.map((photo) => ({ - ...photo, - image: photo.download_url, - })); + const transformedItems: PicsumPhoto[] = data;And update line 75 to use
item.download_urlinstead ofitem.image.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
.github/workflows/coverage.yml(1 hunks).gitignore(1 hunks)example/src/App.tsx(2 hunks)src/__tests__/index.test.tsx(1 hunks)src/index.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/__tests__/index.test.tsx (1)
src/index.tsx (2)
CarouselItemBase(67-69)Carousel(230-380)
src/index.tsx (1)
jest-setup.js (5)
React(6-6)Svg(21-21)Defs(23-23)Stop(25-25)Circle(22-22)
example/src/App.tsx (1)
src/index.tsx (1)
Carousel(230-380)
🪛 actionlint (1.7.9)
.github/workflows/coverage.yml
7-7: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🪛 GitHub Check: lint
src/__tests__/index.test.tsx
[warning] 725-725:
Test has no assertions
[warning] 644-644:
Test has no assertions
[warning] 619-619:
Test has no assertions
[warning] 594-594:
Test has no assertions
[warning] 569-569:
Test has no assertions
src/index.tsx
[warning] 108-108:
Inline style: { position: 'absolute' }
example/src/App.tsx
[warning] 86-86:
Inline style: { justifyContent: 'center', alignItems: 'center' }
[warning] 63-63:
Inline style: { flex: 1 }
[warning] 55-55:
Inline style: { flex: 1 }
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (2)
example/src/App.tsx (1)
28-30: LGTM! Response status check added.The HTTP status check has been added as suggested in the previous review, improving error handling.
src/index.tsx (1)
132-132: LGTM! Gradient key issue resolved.The composite key
${color}-${index}correctly addresses the previous concern about duplicate color keys. This ensures stable, unique keys for React reconciliation.
| coverage: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v3 |
There was a problem hiding this comment.
Update actions/checkout to v4.
The v3 action is too old and will fail on newer GitHub-hosted runners.
Apply this diff:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - uses: actions/checkout@v3 | |
| - uses: actions/checkout@v4 |
🧰 Tools
🪛 actionlint (1.7.9)
7-7: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🤖 Prompt for AI Agents
In .github/workflows/coverage.yml around line 7, update the actions/checkout
step from actions/checkout@v3 to actions/checkout@v4; edit the workflow file to
replace the version tag so the checkout action uses v4 (or the latest stable
major) to ensure compatibility with newer GitHub-hosted runners.
| it('should handle pan gesture with negative translationX', () => { | ||
| const items = createTestItems(3); | ||
| const selectedItem = items[0]!; | ||
|
|
||
| render( | ||
| <Carousel | ||
| items={items} | ||
| selectedItem={selectedItem} | ||
| onItemChange={mockOnItemChange} | ||
| getItemId={mockGetItemId} | ||
| renderItem={mockRenderItem} | ||
| /> | ||
| ); | ||
|
|
||
| const gesture = getByGestureTestId('carousel-pan-gesture'); | ||
|
|
||
| fireGestureHandler(gesture, [ | ||
| { state: State.BEGAN, translationX: 0 }, | ||
| { state: State.ACTIVE, translationX: -100 }, | ||
| { state: State.END, translationX: -100 }, | ||
| ]); | ||
|
|
||
| jest.advanceTimersByTime(1000); | ||
| }); |
There was a problem hiding this comment.
Add assertion to verify gesture behavior.
This test fires a pan gesture with negative translationX but doesn't assert any observable outcome. Without assertions, the test will pass even if the gesture logic breaks.
Add an assertion to verify the expected behavior:
jest.advanceTimersByTime(1000);
+
+ // Swiping left from the first item should move to the next item
+ expect(mockOnItemChange).toHaveBeenCalledWith(items[1]);🧰 Tools
🪛 GitHub Check: lint
[warning] 569-569:
Test has no assertions
🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 569 to 592, the test fires a pan
gesture with negative translationX but contains no assertions; add an assertion
after advancing timers to verify the observable outcome (e.g., expect
mockOnItemChange to have been called once with the next item in the list,
items[1], or otherwise assert the selected item changed appropriately and the
handler was invoked the expected number of times).
| it('should handle pan gesture clamping to minTranslateX', () => { | ||
| const items = createTestItems(3); | ||
| const selectedItem = items[0]!; | ||
|
|
||
| render( | ||
| <Carousel | ||
| items={items} | ||
| selectedItem={selectedItem} | ||
| onItemChange={mockOnItemChange} | ||
| getItemId={mockGetItemId} | ||
| renderItem={mockRenderItem} | ||
| /> | ||
| ); | ||
|
|
||
| const gesture = getByGestureTestId('carousel-pan-gesture'); | ||
|
|
||
| fireGestureHandler(gesture, [ | ||
| { state: State.BEGAN, translationX: 0 }, | ||
| { state: State.ACTIVE, translationX: -1000 }, | ||
| { state: State.END, translationX: -1000 }, | ||
| ]); | ||
|
|
||
| jest.advanceTimersByTime(1000); | ||
| }); |
There was a problem hiding this comment.
Add assertion to verify clamping behavior.
This test fires an extreme left swipe but doesn't verify that the carousel correctly clamps to the last item.
Add an assertion:
jest.advanceTimersByTime(1000);
+
+ // Should clamp to the last item
+ expect(mockOnItemChange).toHaveBeenCalledWith(items[2]);🧰 Tools
🪛 GitHub Check: lint
[warning] 594-594:
Test has no assertions
🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 594 to 617, the test triggers an
extreme left swipe but never asserts that the carousel clamped to the last item;
add an assertion that verifies clamping by expecting the onItemChange handler to
have been called with the last item's id (use mockOnItemChange and mockGetItemId
on the last item), or alternatively assert the carousel's transform/translateX
equals the known minTranslateX value after timers advance, ensuring the test
verifies the carousel ended at the clamped position.
| it('should handle pan gesture clamping to maxTranslateX', () => { | ||
| const items = createTestItems(3); | ||
| const selectedItem = items[0]!; | ||
|
|
||
| render( | ||
| <Carousel | ||
| items={items} | ||
| selectedItem={selectedItem} | ||
| onItemChange={mockOnItemChange} | ||
| getItemId={mockGetItemId} | ||
| renderItem={mockRenderItem} | ||
| /> | ||
| ); | ||
|
|
||
| const gesture = getByGestureTestId('carousel-pan-gesture'); | ||
|
|
||
| fireGestureHandler(gesture, [ | ||
| { state: State.BEGAN, translationX: 0 }, | ||
| { state: State.ACTIVE, translationX: 1000 }, | ||
| { state: State.END, translationX: 1000 }, | ||
| ]); | ||
|
|
||
| jest.advanceTimersByTime(1000); | ||
| }); |
There was a problem hiding this comment.
Add assertion to verify clamping behavior.
This test fires an extreme right swipe from the first item but doesn't verify that the carousel correctly stays at the first item (clamping to maxTranslateX).
Add an assertion:
jest.advanceTimersByTime(1000);
+
+ // Should stay on first item (already at boundary)
+ expect(mockOnItemChange).not.toHaveBeenCalled();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should handle pan gesture clamping to maxTranslateX', () => { | |
| const items = createTestItems(3); | |
| const selectedItem = items[0]!; | |
| render( | |
| <Carousel | |
| items={items} | |
| selectedItem={selectedItem} | |
| onItemChange={mockOnItemChange} | |
| getItemId={mockGetItemId} | |
| renderItem={mockRenderItem} | |
| /> | |
| ); | |
| const gesture = getByGestureTestId('carousel-pan-gesture'); | |
| fireGestureHandler(gesture, [ | |
| { state: State.BEGAN, translationX: 0 }, | |
| { state: State.ACTIVE, translationX: 1000 }, | |
| { state: State.END, translationX: 1000 }, | |
| ]); | |
| jest.advanceTimersByTime(1000); | |
| }); | |
| it('should handle pan gesture clamping to maxTranslateX', () => { | |
| const items = createTestItems(3); | |
| const selectedItem = items[0]!; | |
| render( | |
| <Carousel | |
| items={items} | |
| selectedItem={selectedItem} | |
| onItemChange={mockOnItemChange} | |
| getItemId={mockGetItemId} | |
| renderItem={mockRenderItem} | |
| /> | |
| ); | |
| const gesture = getByGestureTestId('carousel-pan-gesture'); | |
| fireGestureHandler(gesture, [ | |
| { state: State.BEGAN, translationX: 0 }, | |
| { state: State.ACTIVE, translationX: 1000 }, | |
| { state: State.END, translationX: 1000 }, | |
| ]); | |
| jest.advanceTimersByTime(1000); | |
| // Should stay on first item (already at boundary) | |
| expect(mockOnItemChange).not.toHaveBeenCalled(); | |
| }); |
🧰 Tools
🪛 GitHub Check: lint
[warning] 619-619:
Test has no assertions
🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 619 to 642, the test sends an
extreme right swipe but contains no assertion verifying clamping; add a
post-gesture assertion to confirm the carousel remained clamped at the first
item. After advancing timers, assert that the onItemChange callback was not
called (or that the selected item is still the first item), or assert the
carousel's computed translateX equals the expected maxTranslateX value for the
first item so the test verifies clamping behavior.
| it('should handle pan gesture end with low velocity', () => { | ||
| const items = createTestItems(3); | ||
| const selectedItem = items[0]!; | ||
|
|
||
| render( | ||
| <Carousel | ||
| items={items} | ||
| selectedItem={selectedItem} | ||
| onItemChange={mockOnItemChange} | ||
| getItemId={mockGetItemId} | ||
| renderItem={mockRenderItem} | ||
| /> | ||
| ); | ||
|
|
||
| const gesture = getByGestureTestId('carousel-pan-gesture'); | ||
|
|
||
| fireGestureHandler(gesture, [ | ||
| { state: State.BEGAN, translationX: 0 }, | ||
| { state: State.ACTIVE, translationX: -200 }, | ||
| { state: State.END, translationX: -200, velocityX: 100 }, | ||
| ]); | ||
|
|
||
| jest.advanceTimersByTime(1000); | ||
| }); |
There was a problem hiding this comment.
Add assertion to verify low-velocity snap behavior.
This test verifies gesture execution with low velocity but doesn't assert the snap outcome.
Add an assertion to verify which item is selected after the snap:
jest.advanceTimersByTime(1000);
+
+ // With low velocity and -200 translation, should snap to nearest item
+ // Expected behavior should match your snap logic (likely items[1])
+ expect(mockOnItemChange).toHaveBeenCalled();🧰 Tools
🪛 GitHub Check: lint
[warning] 644-644:
Test has no assertions
🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 644 to 667, the test fires a pan
gesture with low velocity but never asserts the resulting snap selection; add an
assertion after jest.advanceTimersByTime(1000) to verify the selected item by
expecting the onItemChange mock to be called with the next item (e.g.
expect(mockOnItemChange).toHaveBeenCalledWith(items[1])) so the test verifies
the carousel snapped to the expected item after the gesture.
| it('should handle pan gesture end with velocity at boundary', () => { | ||
| const items = createTestItems(3); | ||
| const selectedItem = items[0]!; | ||
|
|
||
| render( | ||
| <Carousel | ||
| items={items} | ||
| selectedItem={selectedItem} | ||
| onItemChange={mockOnItemChange} | ||
| getItemId={mockGetItemId} | ||
| renderItem={mockRenderItem} | ||
| /> | ||
| ); | ||
|
|
||
| const gesture = getByGestureTestId('carousel-pan-gesture'); | ||
|
|
||
| fireGestureHandler(gesture, [ | ||
| { state: State.BEGAN, translationX: 0 }, | ||
| { state: State.ACTIVE, translationX: -400 }, | ||
| { state: State.END, translationX: -400, velocityX: -600 }, | ||
| ]); | ||
|
|
||
| jest.advanceTimersByTime(1000); | ||
| }); |
There was a problem hiding this comment.
Add assertion to verify velocity-based navigation at boundary.
This test exercises gesture logic with high velocity at a boundary but doesn't assert the outcome.
Add an assertion:
jest.advanceTimersByTime(1000);
+
+ // High negative velocity should move to next items
+ expect(mockOnItemChange).toHaveBeenCalled();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should handle pan gesture end with velocity at boundary', () => { | |
| const items = createTestItems(3); | |
| const selectedItem = items[0]!; | |
| render( | |
| <Carousel | |
| items={items} | |
| selectedItem={selectedItem} | |
| onItemChange={mockOnItemChange} | |
| getItemId={mockGetItemId} | |
| renderItem={mockRenderItem} | |
| /> | |
| ); | |
| const gesture = getByGestureTestId('carousel-pan-gesture'); | |
| fireGestureHandler(gesture, [ | |
| { state: State.BEGAN, translationX: 0 }, | |
| { state: State.ACTIVE, translationX: -400 }, | |
| { state: State.END, translationX: -400, velocityX: -600 }, | |
| ]); | |
| jest.advanceTimersByTime(1000); | |
| }); | |
| it('should handle pan gesture end with velocity at boundary', () => { | |
| const items = createTestItems(3); | |
| const selectedItem = items[0]!; | |
| render( | |
| <Carousel | |
| items={items} | |
| selectedItem={selectedItem} | |
| onItemChange={mockOnItemChange} | |
| getItemId={mockGetItemId} | |
| renderItem={mockRenderItem} | |
| /> | |
| ); | |
| const gesture = getByGestureTestId('carousel-pan-gesture'); | |
| fireGestureHandler(gesture, [ | |
| { state: State.BEGAN, translationX: 0 }, | |
| { state: State.ACTIVE, translationX: -400 }, | |
| { state: State.END, translationX: -400, velocityX: -600 }, | |
| ]); | |
| jest.advanceTimersByTime(1000); | |
| // High negative velocity should move to next items | |
| expect(mockOnItemChange).toHaveBeenCalled(); | |
| }); |
🧰 Tools
🪛 GitHub Check: lint
[warning] 725-725:
Test has no assertions
🤖 Prompt for AI Agents
In src/__tests__/index.test.tsx around lines 725 to 748, the test simulates a
high-velocity left swipe but lacks an assertion verifying the navigation result;
add an assertion after jest.advanceTimersByTime(1000) that checks the component
invoked the onItemChange handler with the expected next item (e.g., ensure
mockOnItemChange was called once with the id/index of items[1] using
mockGetItemId or the expected value) to confirm velocity-based navigation
occurred at the boundary.
|
Demo Simulator.Screen.Recording.-.iPhone.17.Pro.-.2025-11-30.at.03.08.31.mov |
User description
Summary by Sourcery
Introduce a generic, animated swipe carousel component with gesture support, update its example and documentation, and set up a full Jest testing and coverage reporting pipeline.
New Features:
Enhancements:
Build:
Tests:
CodeAnt-AI Description
Ship interactive carousel component with docs, tests, and workflow
What Changed
onItemChangeonly when the selection really changes.GestureHandlerRootView, and surfaces author/detail info along with error handling and a loading state.Impact
✅ Gesture-driven carousel navigation✅ Test coverage on every pull request✅ Clearer setup guidance for carousel users💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.