agent

frontend

This document provides instructions for AI agents working on the Reveni codebase. - **NO COMMENTS**: NEVER add comments to the code. Code should be self-documenting and clear without them. - **NO PROP

$ curl -fsSL https://skills.reveni.dev/install.sh | bash

AI Agent Guidelines

This document provides instructions for AI agents working on the Reveni codebase.

General Guidelines

General Rules

// ✅ GOOD
import { Text } from 'reveni-ui'

<Text version="v4" type="titleL">Some text</Text>

// ❌ BAD
export const CustomText = styled.p`
  font-size: ${props => props.theme.v4.texts.bodyMRegular.fontSize};
  color: ${props => props.theme.v4.colors.black};
  margin: 0;
`
// ✅ GOOD
export const Container = styled.div`
  gap: ${props => props.theme.v4.spacing.sp4};
  background-color: ${props => props.theme.v4.colors.grey.tone50};
  padding: ${props => props.theme.v4.spacing.sp4} ${props => props.theme.v4.spacing.sp8}
    ${props => props.theme.v4.spacing.sp8} ${props => props.theme.v4.spacing.sp8};
`

// ❌ BAD
export const Container = styled.div`
  gap: 4px;
  background-color: #f5f5f5;
  padding: 4px 8px 8px 8px;
`
// ✅ GOOD
<Component
  icon={getIcon(item)}
  style={getStyle(item)}
/>

// ❌ BAD
<Component
  {...(item.icon && { icon: item.icon })}
  {...(item.error && { style: errorStyle })}
/>
// ✅ GOOD
const handleClose = () => setRuleToDelete(null)

return <Modal onClose={handleClose} />

// ✅ GOOD — exception: inline inside a .map() to pass the item
{items.map(item => (
  <Item key={item.id} onClick={() => handleSelect(item)} />
))}

// ❌ BAD
return <Modal onClose={() => setRuleToDelete(null)} />
// ✅ GOOD — fits on the same line, implicit return
const double = x => x * 2
const getLabel = item => item.label

// ✅ GOOD — doesn't fit on the same line, braces + explicit return
const getFilteredItems = (items, isEnabled) => {
  return items.filter(item => item.active && isEnabled)
}

// ❌ BAD — implicit return wrapping to the next line
const getFilteredItems = (items, isEnabled) =>
  items.filter(item => item.active && isEnabled)
// ✅ GOOD — ifs grouped together, blank line before return
export const getInitialFilter = (selectedProductTypes, selectedCategories, tags) => {
  if (selectedProductTypes?.length > 0) return returnReasonsFilteringTypes.productType
  if (selectedCategories?.length > 0) return returnReasonsFilteringTypes.category
  if (tags?.length > 0) return returnReasonsFilteringTypes.productTags

  return allProducts
}

// ❌ BAD — everything crammed without separation
export const getInitialFilter = (selectedProductTypes, selectedCategories, tags) => {
  if (selectedProductTypes?.length > 0) return returnReasonsFilteringTypes.productType
  if (selectedCategories?.length > 0) return returnReasonsFilteringTypes.category
  if (tags?.length > 0) return returnReasonsFilteringTypes.productTags
  return allProducts
}

// ✅ GOOD — blank line separating declarations from return
const a = () => {
  const b = '1'
  const c = '2'

  return b + c
}

// ❌ BAD
const a = () => {
  const b = '1'
  const c = '2'
  return b + c
}

// ✅ GOOD — declarations, guards, and main logic clearly separated
const getLabel = (items, isEnabled) => {
  const count = items.length
  const hasItems = count > 0

  if (!isEnabled) return defaultLabel
  if (!hasItems) return emptyLabel

  return `${count} items`
}

// ❌ BAD
const getLabel = (items, isEnabled) => {
  const count = items.length
  const hasItems = count > 0
  if (!isEnabled) return defaultLabel
  if (!hasItems) return emptyLabel
  return `${count} items`
}

Frontend Focus

Component Architecture

// ✅ GOOD
const MyComponent = ({ loading, items }) => {
  if (loading) {
    return <Skeleton />
  }

  if (!items?.length) {
    return <EmptyState />
  }

  return (
    <Styles.Container>
      {items.map(item => (
        <Item key={item.id} item={item} />
      ))}
    </Styles.Container>
  )
}

// ❌ BAD
const MyComponent = ({ loading, items }) => {
  return (
    <Styles.Container>
      {loading ? (
        <Skeleton />
      ) : items?.length > 0 ? (
        items.map(item => <Item key={item.id} item={item} />)
      ) : (
        <EmptyState />
      )}
    </Styles.Container>
  )
}

Data Fetching & Transformers

When a custom hook calls the API (typically through useFetch), the data that goes to the backend and the data that comes from it MUST be mapped through transformer functions. The goal is to have a single, explicit place that documents the shape of the payloads the frontend actually uses.

Rules

Example structure

hooks/
  usePolicyRule/
    usePolicyRule.js
    usePolicyRule.test.js
    transformers.js
    transformers.test.js

Example — response transformer

// transformers.js
const mapPolicyRuleCondition = condition => ({
  id: condition.id,
  type: condition.type,
  value: condition.value,
})

const mapPolicyRule = rule => ({
  id: rule.id,
  name: rule.name,
  conditions: rule.conditions.map(mapPolicyRuleCondition),
})

export const transformResponsePolicyRules = rules => rules.map(mapPolicyRule)

Example — request transformer

// transformers.js
export const transformRequestCreatePolicyRule = rule => ({
  name: rule.name,
  conditions: rule.conditions.map(condition => ({
    type: condition.type,
    value: condition.value,
  })),
})

Example — wiring into useFetch

import useFetch from 'hooks/useFetch'

import { transformResponsePolicyRules, transformRequestCreatePolicyRule } from './transformers'

const usePolicyRule = id => {
  const { data, fetchData } = useFetch(getPolicyRuleService, {
    doInitialRequest: true,
    params: { id },
    transformResponse: transformResponsePolicyRules,
  })

  const { fetchData: createPolicyRule } = useFetch(createPolicyRuleService, {
    doInitialRequest: false,
    transformRequest: transformRequestCreatePolicyRule,
    transformResponse: transformResponsePolicyRules,
  })

  return { policyRules: data, createPolicyRule }
}

Final Checks

After completing all changes, you MUST run the following checks in this order before committing:

  1. Run linter: Execute yarn linter to check for linting errors. This is mandatory before every commit.
  2. Run formatter (if linter fails): If there are linting errors, execute yarn format to automatically fix formatting issues, then run yarn linter again to verify.
  3. Run tests: Execute yarn test:agent to ensure all tests pass. If you modified specific files, you can run tests for those files only.

These checks ensure code quality and prevent issues before committing.

Pull Requests

Testing Best Practices

When writing unit tests for React components, follow these rules:

1. Explicit Rendering

DO NOT extract component rendering to a renderComponent or similar helper function at the top level of the test file. DO perform an explicit render call inside each test block. DO NOT pass the theme prop to BaseTest. It is already handled internally.

Rationale: It makes it easier to see exactly which props are being passed to the component for each test case without having to look up a helper function.

// ✅ GOOD
test('Should call handleChange when clicked', async () => {
  render(
    <BaseTest>
      <MyComponent onChange={mockOnChange} />
    </BaseTest>
  )
  // ... test logic
})

// ❌ BAD
const renderComponent = props =>
  render(
    <BaseTest>
      <MyComponent {...props} />
    </BaseTest>
  )
test('Should call handleChange when clicked', async () => {
  renderComponent({ onChange: mockOnChange })
  // ... test logic
})

2. Robust Selectors

3. User Interactions

// ✅ GOOD
import userEvent from '@testing-library/user-event'

test('Should call handler when clicked', async () => {
  render(<MyComponent onClick={mockHandler} />)
  await userEvent.click(screen.getByRole('button'))
  expect(mockHandler).toHaveBeenCalled()
})

// ❌ BAD
import userEvent from '@testing-library/user-event'

test('Should call handler when clicked', async () => {
  const user = userEvent.setup()
  render(<MyComponent onClick={mockHandler} />)
  await user.click(screen.getByRole('button'))
  expect(mockHandler).toHaveBeenCalled()
})

4. Test Behavior, Not Implementation

DO NOT write tests that only verify a function was called. DO write tests that verify the observable outcome of a user action: what renders, what values are returned, what the user sees.

A test that only checks "this function was called" covers lines and passes thresholds, but it doesn't prove anything works correctly. If the implementation changes internally but the behavior stays the same, the test should still pass.

Rule of thumb: assertions should answer "what happened from the user's perspective?", not "which internal function was invoked?".

// ❌ BAD — tests implementation (only checks the function was called)
test('Should call handleSubmit when button is clicked', async () => {
  const handleSubmit = jest.fn()
  render(
    <BaseTest>
      <MyForm onSubmit={handleSubmit} />
    </BaseTest>
  )
  await userEvent.click(screen.getByRole('button', { name: /submit/i }))
  expect(handleSubmit).toHaveBeenCalled()
})

// ✅ GOOD — tests behavior (verifies outcome and how the function was called)
test('Should submit the form with the entered values when button is clicked', async () => {
  const handleSubmit = jest.fn()
  render(
    <BaseTest>
      <MyForm onSubmit={handleSubmit} />
    </BaseTest>
  )
  await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Reveni')
  await userEvent.click(screen.getByRole('button', { name: /submit/i }))
  expect(handleSubmit).toHaveBeenCalledTimes(1)
  expect(handleSubmit).toHaveBeenCalledWith({ name: 'Reveni' })
})

When asserting on mock functions, always verify:

5. No Snapshot Tests for Pages or Composed Components

DO NOT write snapshot tests (toMatchSnapshot) for pages or components that are mostly compositions of already-tested child components. Snapshots of large component trees are brittle, break on any minor change in a child, and provide no meaningful coverage beyond what the child tests already guarantee.

Snapshot tests are only acceptable for small, leaf-level components with stable output (e.g., an icon, a badge).

// ❌ BAD — snapshot of an entire page whose children are already tested
test('Should match snapshot', () => {
  const { container } = render(
    <BaseTest>
      <DashboardPage />
    </BaseTest>
  )
  expect(container).toMatchSnapshot()
})

// ✅ GOOD — test the page's own logic: what it renders conditionally, how it reacts to state
test('Should render the empty state when there are no items', () => {
  render(
    <BaseTest>
      <DashboardPage items={[]} />
    </BaseTest>
  )
  expect(screen.getByText(/no items/i)).toBeInTheDocument()
})

6. Mocking