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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 299 additions & 0 deletions .claude/skills/api-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
---
name: api-test
description: Create an API type-check test for a component. Use when asked to create, generate, or write an API test for a component. Example - "создай api-test для Checkbox", "api-test Switch".
argument-hint: '[ComponentName]'
allowed-tools: Glob, Grep, Read, Write, Edit, Bash, Agent
---

# Создание API type-check теста для компонента **\$ARGUMENTS**

Тесты проверяют публичный API (типы пропсов) компонента через `expectTypeOf` из `expect-type`.
Исходный файл пишется для `@salutejs/plasma-b2c`, затем `script.mjs` автоматически генерирует варианты для всех библиотек.

## Архитектурный контекст

- Тесты: `utils/api-tests/src/components/{ComponentName}/{ComponentName}.api.test.tsx`
- `utils/api-tests/script.mjs` копирует тесты из `src/` в `tests/{lib}/`, заменяя импорт `@salutejs/plasma-b2c` на каждую целевую библиотеку

---

## Шаг 1 — Найти определение типов компонента

1. Найти экспорт компонента в `packages/plasma-b2c`
2. Найти основные типы в `packages/plasma-new-hope/src/components/$ARGUMENTS/*.types.ts` либо во внутренних папках компонента.
3. Обратить внимание на:
- Дискриминированные юнионы (conditional props)
- Пропсы из `Pick<>` от новых типов
- Наследование от HTML-элементов (`HTMLInputElement`, `HTMLButtonElement`, `HTMLDivElement`)
- Дженерики (если есть)

---

## Шаг 2 — Создать файл теста

Путь: `utils/api-tests/src/components/$ARGUMENTS/$ARGUMENTS.api.test.tsx`

### Структура файла

```tsx
import * as React from 'react';
import type { ComponentProps, ReactNode, CSSProperties, AriaRole /*, другие нужные утилитарные типы */ } from 'react';
// Добавить useState если секция Examples использует его:
// import { useState } from 'react';
import { describe, it } from 'vitest';
import { expectTypeOf } from 'expect-type';
// Импорт иконок если нужны в примерах:
// import { IconDownload } from '@salutejs/plasma-icons';
import { $ARGUMENTS } from '@salutejs/plasma-b2c';

type ${ARGUMENTS}Props = ComponentProps<typeof $ARGUMENTS>;

describe('Basics', () => {
it('Common', () => {
// Проверка каждого собственного пропа компонента
});

it('Variations', () => {
// Проверка вариативных пропсов (view, size, labelPlacement и т.д.)
});

it('HTML...Element', () => {
// Проверка наследуемых HTML-пропсов (назвать по реальному элементу)
});
});

// Только если есть дискриминированные юнионы:
describe('Unions', () => {
it('UnionName', () => {
// Валидные и невалидные комбинации
});
});

// Только если есть дженерики:
describe('Generics', () => {
it('ItemOption', () => {
// Проверка потока типов через JSX
});
});

describe('Complex', () => {
it('Examples', () => {
// Реальные примеры использования через expectTypeOf
});
});

describe('Examples', () => {
it('Basic', () => {
() => {
// JSX-примеры с useState, хуками
return (<$ARGUMENTS /* props */ />);
};
});
});
```

---

## Правила написания тестов

### Секция Common

Каждый собственный проп проверяется отдельной строкой. Группировка: layout → state → content slots → callbacks.

```tsx
// Простые пропсы
expectTypeOf<Props>().toHaveProperty('disabled').toEqualTypeOf<boolean | undefined>();
expectTypeOf<Props>().toHaveProperty('label').toEqualTypeOf<string | undefined>();

// Строковые литералы — точный тип зависит от компонента
expectTypeOf<Props>()
.toHaveProperty('pin')
.toEqualTypeOf<'square-square' | 'square-clear' | 'clear-square' | /* ... */ | undefined>();

// ReactNode / ReactElement — проверить по типам конкретного компонента
expectTypeOf<Props>().toHaveProperty('children').toEqualTypeOf<ReactNode>();
expectTypeOf<Props>().toHaveProperty('contentLeft').toEqualTypeOf<ReactNode>(); // или ReactElement | undefined

// Callback-пропсы — тип handler зависит от базового HTML-элемента
expectTypeOf<Props>()
.toHaveProperty('onChange')
.toEqualTypeOf<React.ChangeEventHandler<HTMLInputElement> | undefined>();
```

**Важно:** optional пропсы (`prop?: Type`) всегда включают `| undefined` в ожидаемом типе.

### Секция Variations

Вариативные пропсы (значения определяются конфигом и отличаются между библиотеками) проверяются паттерном "подмножество string, но не сам string":

```tsx
type View = NonNullable<Props['view']>;
expectTypeOf<View>().toExtend<string>();
expectTypeOf<string>().not.toExtend<View>();
```

Типичные вариации: `view`, `size`, `labelPlacement`, `chipView`, `hintView`, `hintSize`.

**Внимание:** не все пропсы с типом `string` являются вариациями. Если конфиг компонента не сужает проп до конкретных литералов, `NonNullable` даст `string` и `expectTypeOf<string>().not.toExtend<string>()` сломается. Перед добавлением в Variations — проверить, что конфиг действительно определяет конкретные значения для этого пропа.

### Секция HTML Element

Название `it` по реальному элементу: `'HTMLInputElement'`, `'HTMLButtonElement'` и т.д.

**Общие для всех:**

```tsx
expectTypeOf<Props>().toHaveProperty('id').toEqualTypeOf<string | undefined>();
expectTypeOf<Props>().toHaveProperty('className').toEqualTypeOf<string | undefined>();
expectTypeOf<Props>().toHaveProperty('style').toEqualTypeOf<CSSProperties | undefined>();
expectTypeOf<Props>().toHaveProperty('aria-label').toEqualTypeOf<string | undefined>();
expectTypeOf<Props>().toHaveProperty('role').toEqualTypeOf<AriaRole | undefined>();
```

**Для HTMLInputElement** — добавить:

```tsx
expectTypeOf<Props>().toHaveProperty('value').toEqualTypeOf<string | number | readonly string[] | undefined>();
expectTypeOf<Props>().toHaveProperty('defaultValue').toEqualTypeOf<string | number | readonly string[] | undefined>();
expectTypeOf<Props>()
.toHaveProperty('onChange')
.toEqualTypeOf<React.ChangeEventHandler<HTMLInputElement> | undefined>();
expectTypeOf<Props>().toHaveProperty('onFocus').toEqualTypeOf<React.FocusEventHandler<HTMLInputElement> | undefined>();
expectTypeOf<Props>().toHaveProperty('onBlur').toEqualTypeOf<React.FocusEventHandler<HTMLInputElement> | undefined>();
expectTypeOf<Props>()
.toHaveProperty('onKeyDown')
.toEqualTypeOf<React.KeyboardEventHandler<HTMLInputElement> | undefined>();
```

**Для HTMLButtonElement / HTMLElement** — добавить:

```tsx
expectTypeOf<Props>().toHaveProperty('onClick').toEqualTypeOf<React.MouseEventHandler<HTMLElement> | undefined>();
expectTypeOf<Props>().toHaveProperty('onMouseEnter').toEqualTypeOf<React.MouseEventHandler<HTMLElement> | undefined>();
expectTypeOf<Props>().toHaveProperty('onMouseLeave').toEqualTypeOf<React.MouseEventHandler<HTMLElement> | undefined>();
```

### Секция Unions

Дискриминированные юнионы проверяются через `expectTypeOf<Props>({...})`:

```tsx
it('UnionName', () => {
// Валидные комбинации
expectTypeOf<Props>({ propA: 'value1', propB: true });
expectTypeOf<Props>({ propA: 'value2' });

// Невалидные комбинации
// @ts-expect-error
expectTypeOf<Props>({ propA: 'value1', forbiddenProp: true });

// Если юнион "дырявый" (не ограничивает как должен):
// TODO: Неправильная работа юниона UnionName. Должна быть ошибка.
expectTypeOf<Props>({ propA: 'value1', shouldBeForbidden: true });
});
```

### Секция Generics

Для компонентов с дженериками (например, `items` с произвольными полями) — через JSX:

```tsx
it('ItemType', () => {
const items = [{ value: '', label: '', customProp: '', boolProp: true }];
void (<Component items={items} />);
void (
<Component
items={items}
renderItem={(item) => item.customProp}
filter={(item) => item.boolProp}
onChange={(value, item) => item && item.customProp}
/>
);
});
```

### Секция Complex / Examples

Реалистичные примеры через `expectTypeOf` — комбинации пропсов, отражающие реальное использование:

```tsx
it('Examples', () => {
// Примеры зависят от конкретного компонента
expectTypeOf<Props>({ label: 'Текст', disabled: true });
expectTypeOf<Props>({ text: 'Кнопка', stretching: 'filled' });
});
```

### Секция Examples (JSX)

Примеры с реальным JSX и хуками — обёрнуты в анонимную функцию:

```tsx
describe('Examples', () => {
it('Controlled', () => {
() => {
const [value, setValue] = useState('');
return <Component value={value} onChange={(e) => setValue(e.target.value)} label="Метка" />;
};
});

it('Disabled', () => {
() => {
return <Component label="Неактивный" disabled />;
};
});
});
```

---

## Шаг 3 — Запуск и валидация

Запустить полный цикл тестов (генерация + typecheck):

```bash
cd utils/api-tests && npm test
```

Это выполнит `rm -rf tests && node script.mjs && vitest run --config ./vitest.config.ts` — сгенерирует тесты для всех библиотек и запустит typecheck.

Все тесты должны пройти без ошибок типов (`Type Errors: no errors`).
Рантайм ошибки `Cannot find module 'styled-components'` — ожидаемые, игнорировать.

Если есть ошибки типов — внимательно прочитать полный вывод (строки с `TypeCheckError`), исправить ассерты в `src/` файле (не в компоненте) и перезапустить.

---

## Важные правила

1. **Только `@salutejs/plasma-b2c`** в импортах — `script.mjs` заменит на остальные библиотеки
2. **Типы из `plasma-new-hope`** как источник истины — не тестировать b2c-only пропсы (например `status`, `caption`, `animatedHint` для TextField). Проп может быть b2c-only для одного компонента, но общим для другого (например `helperText` — b2c-only в TextField, но общий в Combobox). Всегда проверять по new-hope типам конкретного компонента
3. **Вариативные пропсы** — паттерн `toExtend`, НЕ `toEqualTypeOf` — конфиги сужают по-разному в каждой библиотеке
4. **Не проверять `@deprecated` пропсы** (помеченные `@deprecated`)
5. **Не проверять internal пропсы** (помеченные `@internal` или начинающиеся с `$`)
6. **JSX-примеры должны компилироваться** — это реальные type-check проверки, не документация
7. **Группировать пропсы логически** в Common: layout → state → content slots → callbacks
8. **Юнионы с обеих сторон** — валидные комбинации И `@ts-expect-error` для невалидных, или `// TODO` если юнион "дырявый"

---

## Чеклист

- [ ] Файл создан в `utils/api-tests/src/components/$ARGUMENTS/$ARGUMENTS.api.test.tsx`
- [ ] Импорт компонента из `@salutejs/plasma-b2c`
- [ ] Все собственные пропсы покрыты в `Common`
- [ ] Вариации (`view`, `size` и т.д.) покрыты в `Variations`
- [ ] HTML-атрибуты покрыты в `HTML...Element`
- [ ] Дискриминированные юнионы покрыты в `Unions` с `@ts-expect-error`
- [ ] JSX-примеры в `Examples`
- [ ] Тесты проходят typecheck (`Type Errors: no errors`)

---

## Референсные файлы

Изучить для понимания паттернов:

- `utils/api-tests/src/components/Button/Button.api.test.tsx` — простой компонент, один юнион, без дженериков
- `utils/api-tests/src/components/TextField/TextField.api.test.tsx` — сложный компонент, множественные юнионы, chip-пропсы
- `utils/api-tests/src/components/Combobox/Combobox.api.test.tsx` — дженерики, сложные юнионы, JSX-примеры
1 change: 1 addition & 0 deletions .codex
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ module.exports = {
], // NOTE: If you want a type meaning "empty object", you probably want `Record<string, never>` instead

'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/ban-ts-comment': 'off',
'no-void': ['error', { allowAsStatement: true }],
'wrap-iife': 'off',
'func-call-spacing': 'off',
'no-spaced-func': 'off',
},
settings: {
react: {
Expand Down
72 changes: 72 additions & 0 deletions API-TESTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Тестирование типов компонента

API tests — это контрактные тесты типов, которые гарантируют стабильность публичного API React-компонентов на уровне TypeScript.

### Их цель:

- зафиксировать публичный контракт компонента;
- предотвратить случайные breaking changes;
- задокументировать допустимые и недопустимые комбинации пропсов;
- проверить корректность union, generic, conditional и overload-типов.

**⚠️ ВНИМАНИЕ**
Эти тесты не рендерят компонент и не проверяют поведение — только типы.

## Используемый стек:

- TypeScript
- vitest — как тест-раннер
- expect-type — для проверок типов
- // @ts-expect-error — для негативных кейсов

```tsx
import { describe, it } from 'vitest';
import { expectTypeOf } from 'expect-type';
```

## Общие правила

### 1. Расположение

Все тесты лежат внутри пакета `utils/api-tests`.

### 2. Один компонент — один файл

Имя файла:

```tsx
<ComponentName>.api.test.tsx
```

Пример:

```tsx
Combobox.api.test.tsx;
```

### 3. Всегда тестируем ComponentProps<typeof Component>

```tsx
type ComboboxProps = ComponentProps<typeof Combobox>;
```

❌ Нельзя тестировать внутренние типы напрямую
✅ Только публичный API компонента

### 4. Структура файла

Рекомендуемая структура:

```tsx
describe('Basics', () => {});
describe('Unions', () => {});
describe('Generics', () => {});
describe('Examples', () => {});
```

## Когда добавлять API-тесты

- новый публичный компонент
- новый публичный проп
- изменение union / conditional types
- добавление generic-поведения
Loading
Loading