Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4548fda
wip
simo6529 Sep 30, 2025
5cb472a
wip
simo6529 Sep 30, 2025
c39541e
feat(nft-picker): complete picker implementation
simo6529 Sep 30, 2025
c442f87
wip
simo6529 Sep 30, 2025
8419dae
wip
simo6529 Sep 30, 2025
11b9e1f
wip
simo6529 Oct 1, 2025
315f55d
wip
simo6529 Oct 1, 2025
3d93cd1
wip
simo6529 Oct 1, 2025
59807f1
wip
simo6529 Oct 1, 2025
cea4849
wip
simo6529 Oct 1, 2025
13e7740
wip
simo6529 Oct 1, 2025
206382e
wip
simo6529 Oct 1, 2025
505fcbf
wip
simo6529 Oct 1, 2025
c3495af
wip
simo6529 Oct 1, 2025
b65f9f5
wip
simo6529 Oct 1, 2025
ec46c76
wip
simo6529 Oct 1, 2025
8c616ce
wip
simo6529 Oct 1, 2025
2274a32
wip
simo6529 Oct 1, 2025
ca0cdd7
wip
simo6529 Oct 1, 2025
5ccf789
wip
simo6529 Oct 1, 2025
5a1dfb6
wip
simo6529 Oct 1, 2025
5291ab4
wip
simo6529 Oct 1, 2025
680ff31
Merge branch 'main' into feature/nft-picker
simo6529 Oct 1, 2025
43d4c21
wip
simo6529 Oct 1, 2025
8e45385
wip
simo6529 Oct 1, 2025
65ecf1d
wip
simo6529 Oct 1, 2025
96ea43f
wip
simo6529 Oct 1, 2025
6174c46
wip
simo6529 Oct 1, 2025
f63ecb2
wip
simo6529 Oct 1, 2025
82c7842
wip
simo6529 Oct 1, 2025
976f6a4
wip
simo6529 Oct 1, 2025
a845855
wip
simo6529 Oct 1, 2025
e572005
wip
simo6529 Oct 1, 2025
da79b29
Merge branch 'main' into feature/nft-picker
simo6529 Oct 1, 2025
d8b4f2f
wip
simo6529 Oct 1, 2025
9b12f70
wip
simo6529 Oct 1, 2025
30b9928
wip
simo6529 Oct 1, 2025
b0674e8
wip
simo6529 Oct 1, 2025
939ca1d
wip
simo6529 Oct 1, 2025
a060c8f
wip
simo6529 Oct 1, 2025
4744511
wip
simo6529 Oct 6, 2025
9b5dee7
wip
simo6529 Oct 6, 2025
20e251e
wip
simo6529 Oct 6, 2025
0539eb0
wip
simo6529 Oct 6, 2025
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
152 changes: 110 additions & 42 deletions __tests__/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,134 @@
# Test Guidelines for Codex
# Codex Testing Guidelines

## Purpose and Structure
## Purpose & Structure

The `__tests__` directory contains Jest test suites for this Next.js project. Tests mirror the source folders such as `components`, `contexts`, `hooks` and `utils` to keep structure familiar. Integration tests for API routes live under `app/api`. Fixtures and helpers used across tests reside in their respective subfolders.
* All tests live in `__tests__`, mirroring source folders (`components`, `contexts`, `hooks`, `utils`).
* API integration tests: `app/api`.
* Shared fixtures & helpers: relevant subfolders.
* Jest automatically picks up mocks from `__mocks__`.

## Testing Frameworks and Tools
## Tools

- **Jest** with the `ts-jest` preset for TypeScript support.
- **@testing-library/react** and **@testing-library/user-event** for React component tests.
- Additional mocks live in `__mocks__` and are automatically picked up by Jest.
* **Jest** + `ts-jest` (TypeScript).
* **@testing-library/react** + **user-event** (React tests).
* Coverage reports in `coverage/`.

## Test Writing Guidelines
## Writing Tests

- Prioritise meaningful coverage that validates business requirements over achieving raw coverage numbers.
- Use the **Arrange – Act – Assert** pattern and keep assertions focused on behaviour, not implementation details.
- Give each test a clear, descriptive name that conveys the scenario and expected outcome.
- Tests should remain independent, deterministic and fast.
* Focus on business value, not raw coverage.
* Follow **Arrange – Act – Assert**.
* One behaviour per test; clear, descriptive names.
* Keep tests independent, deterministic, and fast.
* Use realistic data.

## Test Categorisation and Prioritisation
### Test Types

Consider the following categories when writing tests:
* **Happy Path** – expected workflows.
* **Errors** – invalid input, unexpected scenarios.
* **Edge Cases** – boundaries, rare conditions.
* **Integration** – components & API interactions.
* **Performance/Security** – when relevant.

- **Happy Path Tests** – standard workflows with valid inputs.
- **Error Handling Tests** – behaviour with invalid inputs or unexpected scenarios.
- **Edge Case Tests** – boundary conditions and uncommon situations.
- **Integration Tests** – interactions between components or with API routes.
- **Performance & Security Tests** – only when relevant.
Prioritise high-risk areas first when time-boxed.

When time‑boxed, focus on high‑risk areas first, then fill coverage gaps and polish.
## Time-Boxed Cycle (20 min)

## Time‑Boxed Testing Approach
1. **5–7 min**: core flows.
2. **5–7 min**: edge cases & branches.
3. **5–6 min**: clean up & refine.

A suggested 20‑minute cycle:
## Quality Checklist

1. **Initial Phase (5‑7 min)** – target high‑impact paths and core functionality.
2. **Middle Phase (5‑7 min)** – add tests for edge cases or missing branches.
3. **Final Phase (5‑6 min)** – clean up, improve readability and verify results.
* [ ] Clear, descriptive names
* [ ] Arrange – Act – Assert used
* [ ] Independent & fast
* [ ] One behaviour per test
* [ ] Production-like data

## Test Quality Checklist
## Running Tests

- [ ] Clear, descriptive test names
- [ ] Proper Arrange – Act – Assert structure
- [ ] Independent and deterministic
- [ ] Execute quickly and focus on one behaviour
- [ ] Use realistic, production‑like data
```bash
npm run test:cov:changed # changed files only
npm run test # full suite
npm run lint
npm run type-check
```

## Execution Instructions
---

Run tests with:
# Coding Standards

```bash
npm run test:cov:changed
### Complexity

* Functions ≤ 15 cognitive complexity.
* Extract deep ternaries (>3 levels).
* Break down complex logic.

### Modern Patterns

**Iteration**

```ts
// ❌ Avoid
items.forEach(item => processItem(item));

// ✅ Prefer
for (const item of items) {
processItem(item);
}
```

This command runs Jest only on files changed since `main`. Use `npm run test` if
you need to execute the entire suite.
* Allows `break/continue`.
* Works with async/await.

This command also checks coverage for modified files. Linting and type‑checking should pass as well:
**Array Access**

```bash
npm run lint
npm run type-check
```ts
// ✅ Prefer
const last = array.at(-1);
const secondLast = array.at(-2);
```

Coverage reports are generated in the `coverage` directory. A summary is printed in the terminal after tests complete.
**Strings**

```ts
// ✅ Prefer
str.replaceAll('old', 'new');
```

**Globals**

```ts
// ✅ Prefer
globalThis.fetch(url);
```

**Imports**

* One import per module.
* Order: external → internal → types.
* No duplicates.

**Accessibility**

* Use semantic HTML (`<label>`, `<output>`) over ARIA when possible.
* Every form control must have a label.
* Test with keyboard + screen reader.

**Modern DOM**

```ts
element.remove(); // instead of parent.removeChild(element)
```

**Error Handling**

* Catch only when meaningful.
* No empty `catch`.
* Log with context.

**Clarity**

* Avoid double negatives.
* Prefer explicit, remove redundant annotations.
* Use optional chaining (`?.`).
163 changes: 163 additions & 0 deletions __tests__/components/nft-picker/NftPicker.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
MAX_ENUMERATION,
MAX_SAFE,
bigintCompare,
expandRangesWindow,
formatCanonical,
fromCanonicalRanges,
isRangeTooLargeError,
parseTokenExpressionToBigints,
parseTokenExpressionToRanges,
sortAndDedupIds,
toCanonicalRanges,
tryToNumberArray,
} from "@/components/nft-picker/NftPicker.utils";

import type { TokenRange } from "@/components/nft-picker/NftPicker.types";

describe("NftPicker.utils", () => {
describe("parseTokenExpressionToBigints", () => {
it("parses decimal and range inputs", () => {
const result = parseTokenExpressionToBigints("1,2,5-7");
expect(result.map((value) => value.toString())).toEqual([
"1",
"2",
"5",
"6",
"7",
]);
});

it("parses hexadecimal values", () => {
const result = parseTokenExpressionToBigints("0x1A,0x1B-0x1D");
expect(result.map((value) => value.toString(16))).toEqual([
"1a",
"1b",
"1c",
"1d",
]);
});

it("trims whitespace and normalises ranges", () => {
const result = parseTokenExpressionToBigints(" 1 - 3 , 5 \n");
expect(result.map((value) => value.toString())).toEqual([
"1",
"2",
"3",
"5",
]);
});

it("swaps reversed ranges", () => {
const result = parseTokenExpressionToBigints("5-2");
expect(result.map((value) => value.toString())).toEqual([
"2",
"3",
"4",
"5",
]);
});

it("throws parse errors for invalid segments", () => {
expect(() => parseTokenExpressionToBigints("1,foo"))
.toThrow();
try {
parseTokenExpressionToBigints("1,foo");
} catch (error) {
expect(Array.isArray(error)).toBe(true);
const errors = error as { message: string }[];
expect(errors[0].message).toBe("Invalid token format");
}
});

it("throws a parse error when a single range exceeds the enumeration ceiling", () => {
expect(() => parseTokenExpressionToRanges("0-10000"))
.toThrow();
try {
parseTokenExpressionToRanges("0-10000");
} catch (error) {
expect(Array.isArray(error)).toBe(true);
const errors = error as { code?: string; message: string }[];
expect(errors[0]?.code).toBe("range-too-large");
expect(errors[0]?.message).toContain("exceeding the limit");
}
});

it("throws a parse error when combined ranges exceed the enumeration ceiling", () => {
const input = "0-5000,6000-11000";
expect(() => parseTokenExpressionToBigints(input)).toThrow();
try {
parseTokenExpressionToBigints(input);
} catch (error) {
expect(Array.isArray(error)).toBe(true);
const errors = error as { code?: string; message: string }[];
expect(errors[0]?.code).toBe("range-too-large");
expect(errors[0]?.message).toContain(MAX_ENUMERATION.toString());
}
});
});

it("sorts and deduplicates token ids", () => {
const result = sortAndDedupIds([
5n,
3n,
3n,
4n,
1n,
]);
expect(result).toEqual([1n, 3n, 4n, 5n]);
});

it("converts to and from canonical ranges", () => {
const ranges = toCanonicalRanges([1n, 2n, 3n, 5n, 6n]);
expect(ranges).toEqual([
{ start: 1n, end: 3n },
{ start: 5n, end: 6n },
]);
const expanded = fromCanonicalRanges(ranges);
expect(expanded).toEqual([1n, 2n, 3n, 5n, 6n]);
});

it("throws a structured error when expanding canonical ranges beyond the ceiling", () => {
const oversized = [{ start: 0n, end: 10_000n }];
expect(() => fromCanonicalRanges(oversized)).toThrow();
try {
fromCanonicalRanges(oversized);
} catch (error) {
expect(isRangeTooLargeError(error)).toBe(true);
if (isRangeTooLargeError(error)) {
expect(error.limit).toBe(MAX_ENUMERATION);
expect(error.size).toBe(10_001n);
}
}
});

it("formats canonical ranges", () => {
const formatted = formatCanonical([
{ start: 1n, end: 3n },
{ start: 5n, end: 5n },
]);
expect(formatted).toBe("1-3,5");
});

it("converts to numbers and counts unsafe values", () => {
const { numbers, unsafeCount } = tryToNumberArray([1n, MAX_SAFE + 1n]);
expect(numbers).toEqual([1]);
expect(unsafeCount).toBe(1);
});

it("compares bigints", () => {
expect(bigintCompare(1n, 2n)).toBe(-1);
expect(bigintCompare(2n, 1n)).toBe(1);
expect(bigintCompare(3n, 3n)).toBe(0);
});

it("expands ranges within a window", () => {
const ranges: TokenRange[] = [
{ start: 1n, end: 3n },
{ start: 10n, end: 12n },
];
const window = expandRangesWindow(ranges, 2, 4);
expect(window).toEqual([3n, 10n, 11n, 12n]);
});
});
Loading