@thalorlabs/pokeapi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +1 -0
- package/CLAUDE.md +36 -0
- package/dist/PokeApiClient.d.ts +20 -0
- package/dist/PokeApiClient.js +69 -0
- package/dist/PokeApiTypes.d.ts +46 -0
- package/dist/PokeApiTypes.js +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/eslint.config.mjs +2 -0
- package/package.json +42 -0
- package/src/PokeApiClient.ts +103 -0
- package/src/PokeApiTypes.ts +48 -0
- package/src/index.ts +6 -0
- package/tests/PokeApiClient.test.ts +195 -0
- package/tsconfig.json +8 -0
package/.prettierrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"@thalorlabs/eslint-plugin-dev-config/prettier"
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# CLAUDE.md — @thalorlabs/pokeapi
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
Provider adapter for pokeapi.co (PokeAPI). Returns `TLPokemon`.
|
|
6
|
+
|
|
7
|
+
## External API
|
|
8
|
+
|
|
9
|
+
- pokeapi.co — free, no auth, no billing, no rate limits
|
|
10
|
+
- Multi-call provider: /pokemon/{id} + /pokemon-species/{id}
|
|
11
|
+
- 1025 Pokemon in the National Dex
|
|
12
|
+
|
|
13
|
+
## Build & publish
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run build # tsc → dist/
|
|
17
|
+
npm test # vitest
|
|
18
|
+
npm version patch # or minor/major
|
|
19
|
+
npm publish --access public
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What this package does
|
|
23
|
+
|
|
24
|
+
- HTTP adapter extending BaseHttpClient
|
|
25
|
+
- Two API calls per getPokemon() — pokemon data + species data
|
|
26
|
+
- Normalises combined response to TLPokemon
|
|
27
|
+
- Maps type names to EPokemonType enum (skips unknown types)
|
|
28
|
+
- Parses generation from roman numeral format
|
|
29
|
+
- Cleans flavor text (removes newlines/form feeds)
|
|
30
|
+
- Sets: isSafe=true
|
|
31
|
+
|
|
32
|
+
## What this package does NOT do
|
|
33
|
+
|
|
34
|
+
- No database, no caching, no logging — orchestrator handles that
|
|
35
|
+
- Raw types never exported
|
|
36
|
+
- No provider selection logic
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
2
|
+
import { TLPokemon } from '@thalorlabs/types';
|
|
3
|
+
export interface PokeApiClientConfig {
|
|
4
|
+
baseURL?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* HTTP adapter for pokeapi.co.
|
|
9
|
+
*
|
|
10
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
11
|
+
* observability. Multi-call provider: fetches pokemon data + species data,
|
|
12
|
+
* then normalises the combined response to TLPokemon.
|
|
13
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
14
|
+
*/
|
|
15
|
+
export declare class PokeApiClient extends BaseHttpClient {
|
|
16
|
+
readonly serviceName = "pokeapi";
|
|
17
|
+
constructor(config?: PokeApiClientConfig);
|
|
18
|
+
getPokemon(): Promise<TLPokemon>;
|
|
19
|
+
private normalise;
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PokeApiClient = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const api_1 = require("@thalorlabs/api");
|
|
6
|
+
const types_1 = require("@thalorlabs/types");
|
|
7
|
+
const ROMAN_NUMERAL_MAP = {
|
|
8
|
+
i: 1,
|
|
9
|
+
ii: 2,
|
|
10
|
+
iii: 3,
|
|
11
|
+
iv: 4,
|
|
12
|
+
v: 5,
|
|
13
|
+
vi: 6,
|
|
14
|
+
vii: 7,
|
|
15
|
+
viii: 8,
|
|
16
|
+
ix: 9,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* HTTP adapter for pokeapi.co.
|
|
20
|
+
*
|
|
21
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
22
|
+
* observability. Multi-call provider: fetches pokemon data + species data,
|
|
23
|
+
* then normalises the combined response to TLPokemon.
|
|
24
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
25
|
+
*/
|
|
26
|
+
class PokeApiClient extends api_1.BaseHttpClient {
|
|
27
|
+
constructor(config = {}) {
|
|
28
|
+
super({
|
|
29
|
+
baseURL: config.baseURL ?? 'https://pokeapi.co/api/v2',
|
|
30
|
+
serviceName: 'pokeapi',
|
|
31
|
+
retries: 3,
|
|
32
|
+
timeout: config.timeout ?? 10000,
|
|
33
|
+
});
|
|
34
|
+
this.serviceName = 'pokeapi';
|
|
35
|
+
}
|
|
36
|
+
async getPokemon() {
|
|
37
|
+
const pokemonId = Math.floor(Math.random() * 1025) + 1;
|
|
38
|
+
const { response: pokemonResponse } = await this.handleRequest({ method: 'GET', url: `/pokemon/${pokemonId}` }, { method: 'GET', url: `/pokemon/${pokemonId}`, requestId: (0, crypto_1.randomUUID)() });
|
|
39
|
+
const { response: speciesResponse } = await this.handleRequest({ method: 'GET', url: `/pokemon-species/${pokemonId}` }, {
|
|
40
|
+
method: 'GET',
|
|
41
|
+
url: `/pokemon-species/${pokemonId}`,
|
|
42
|
+
requestId: (0, crypto_1.randomUUID)(),
|
|
43
|
+
});
|
|
44
|
+
return this.normalise(pokemonResponse.data, speciesResponse.data);
|
|
45
|
+
}
|
|
46
|
+
normalise(pokemon, species) {
|
|
47
|
+
const name = pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1);
|
|
48
|
+
const types = pokemon.types
|
|
49
|
+
.map((t) => t.type.name.toUpperCase())
|
|
50
|
+
.filter((t) => Object.values(types_1.EPokemonType).includes(t));
|
|
51
|
+
const englishEntry = species.flavor_text_entries.find((e) => e.language.name === 'en');
|
|
52
|
+
const description = englishEntry
|
|
53
|
+
? englishEntry.flavor_text.replace(/[\n\f\r]/g, ' ')
|
|
54
|
+
: '';
|
|
55
|
+
const generationRoman = species.generation.name.split('-')[1];
|
|
56
|
+
const generation = ROMAN_NUMERAL_MAP[generationRoman] ?? 0;
|
|
57
|
+
const imageUrl = pokemon.sprites.other['official-artwork'].front_default;
|
|
58
|
+
return types_1.TLPokemon.parse({
|
|
59
|
+
id: (0, crypto_1.createHash)('md5').update(pokemon.name).digest('hex'),
|
|
60
|
+
name,
|
|
61
|
+
types,
|
|
62
|
+
description,
|
|
63
|
+
imageUrl,
|
|
64
|
+
generation,
|
|
65
|
+
isSafe: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.PokeApiClient = PokeApiClient;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response types from pokeapi.co API.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
export interface PokeApiPokemonRaw {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
types: Array<{
|
|
11
|
+
slot: number;
|
|
12
|
+
type: {
|
|
13
|
+
name: string;
|
|
14
|
+
url: string;
|
|
15
|
+
};
|
|
16
|
+
}>;
|
|
17
|
+
sprites: {
|
|
18
|
+
front_default: string;
|
|
19
|
+
other: {
|
|
20
|
+
'official-artwork': {
|
|
21
|
+
front_default: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
species: {
|
|
26
|
+
name: string;
|
|
27
|
+
url: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface PokeApiSpeciesRaw {
|
|
31
|
+
id: number;
|
|
32
|
+
name: string;
|
|
33
|
+
generation: {
|
|
34
|
+
name: string;
|
|
35
|
+
url: string;
|
|
36
|
+
};
|
|
37
|
+
flavor_text_entries: Array<{
|
|
38
|
+
flavor_text: string;
|
|
39
|
+
language: {
|
|
40
|
+
name: string;
|
|
41
|
+
};
|
|
42
|
+
version: {
|
|
43
|
+
name: string;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POKEAPI_CONFIG = exports.PokeApiClient = void 0;
|
|
4
|
+
var PokeApiClient_1 = require("./PokeApiClient");
|
|
5
|
+
Object.defineProperty(exports, "PokeApiClient", { enumerable: true, get: function () { return PokeApiClient_1.PokeApiClient; } });
|
|
6
|
+
exports.POKEAPI_CONFIG = {
|
|
7
|
+
cacheTtlMs: 1000 * 60 * 60 * 24 * 7, // 7 days — Pokemon data never changes
|
|
8
|
+
isBillable: false,
|
|
9
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thalorlabs/pokeapi",
|
|
3
|
+
"author": "ThalorLabs",
|
|
4
|
+
"private": false,
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Provider adapter for pokeapi.co — returns TLPokemon",
|
|
7
|
+
"homepage": "https://github.com/ThalorLabs/pokeapi#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/ThalorLabs/pokeapi/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ThalorLabs/pokeapi.git"
|
|
14
|
+
},
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"clean": "rimraf dist",
|
|
23
|
+
"prebuild": "npm run clean",
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"lint": "eslint src/",
|
|
26
|
+
"lint:fix": "eslint src/ --fix",
|
|
27
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
28
|
+
"format:check": "prettier --check \"src/**/*.ts\""
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@thalorlabs/eslint-plugin-dev-config": "^2.1.1",
|
|
32
|
+
"@types/node": "^25.5.2",
|
|
33
|
+
"prettier": "^3.8.1",
|
|
34
|
+
"rimraf": "^5.0.0",
|
|
35
|
+
"typescript": "^5.0.0",
|
|
36
|
+
"vitest": "^3.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@thalorlabs/api": "^1.0.0",
|
|
40
|
+
"@thalorlabs/types": "^1.9.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
4
|
+
import { EPokemonType, TLPokemon } from '@thalorlabs/types';
|
|
5
|
+
|
|
6
|
+
import { PokeApiPokemonRaw, PokeApiSpeciesRaw } from './PokeApiTypes';
|
|
7
|
+
|
|
8
|
+
export interface PokeApiClientConfig {
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ROMAN_NUMERAL_MAP: Record<string, number> = {
|
|
14
|
+
i: 1,
|
|
15
|
+
ii: 2,
|
|
16
|
+
iii: 3,
|
|
17
|
+
iv: 4,
|
|
18
|
+
v: 5,
|
|
19
|
+
vi: 6,
|
|
20
|
+
vii: 7,
|
|
21
|
+
viii: 8,
|
|
22
|
+
ix: 9,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* HTTP adapter for pokeapi.co.
|
|
27
|
+
*
|
|
28
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
29
|
+
* observability. Multi-call provider: fetches pokemon data + species data,
|
|
30
|
+
* then normalises the combined response to TLPokemon.
|
|
31
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
32
|
+
*/
|
|
33
|
+
export class PokeApiClient extends BaseHttpClient {
|
|
34
|
+
public readonly serviceName = 'pokeapi';
|
|
35
|
+
|
|
36
|
+
constructor(config: PokeApiClientConfig = {}) {
|
|
37
|
+
super({
|
|
38
|
+
baseURL: config.baseURL ?? 'https://pokeapi.co/api/v2',
|
|
39
|
+
serviceName: 'pokeapi',
|
|
40
|
+
retries: 3,
|
|
41
|
+
timeout: config.timeout ?? 10000,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getPokemon(): Promise<TLPokemon> {
|
|
46
|
+
const pokemonId = Math.floor(Math.random() * 1025) + 1;
|
|
47
|
+
|
|
48
|
+
const { response: pokemonResponse } =
|
|
49
|
+
await this.handleRequest<PokeApiPokemonRaw>(
|
|
50
|
+
{ method: 'GET', url: `/pokemon/${pokemonId}` },
|
|
51
|
+
{ method: 'GET', url: `/pokemon/${pokemonId}`, requestId: randomUUID() }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const { response: speciesResponse } =
|
|
55
|
+
await this.handleRequest<PokeApiSpeciesRaw>(
|
|
56
|
+
{ method: 'GET', url: `/pokemon-species/${pokemonId}` },
|
|
57
|
+
{
|
|
58
|
+
method: 'GET',
|
|
59
|
+
url: `/pokemon-species/${pokemonId}`,
|
|
60
|
+
requestId: randomUUID(),
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return this.normalise(pokemonResponse.data, speciesResponse.data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private normalise(
|
|
68
|
+
pokemon: PokeApiPokemonRaw,
|
|
69
|
+
species: PokeApiSpeciesRaw
|
|
70
|
+
): TLPokemon {
|
|
71
|
+
const name =
|
|
72
|
+
pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1);
|
|
73
|
+
|
|
74
|
+
const types = pokemon.types
|
|
75
|
+
.map((t) => t.type.name.toUpperCase())
|
|
76
|
+
.filter((t): t is EPokemonType =>
|
|
77
|
+
Object.values(EPokemonType).includes(t as EPokemonType)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const englishEntry = species.flavor_text_entries.find(
|
|
81
|
+
(e) => e.language.name === 'en'
|
|
82
|
+
);
|
|
83
|
+
const description = englishEntry
|
|
84
|
+
? englishEntry.flavor_text.replace(/[\n\f\r]/g, ' ')
|
|
85
|
+
: '';
|
|
86
|
+
|
|
87
|
+
const generationRoman = species.generation.name.split('-')[1];
|
|
88
|
+
const generation = ROMAN_NUMERAL_MAP[generationRoman] ?? 0;
|
|
89
|
+
|
|
90
|
+
const imageUrl =
|
|
91
|
+
pokemon.sprites.other['official-artwork'].front_default;
|
|
92
|
+
|
|
93
|
+
return TLPokemon.parse({
|
|
94
|
+
id: createHash('md5').update(pokemon.name).digest('hex'),
|
|
95
|
+
name,
|
|
96
|
+
types,
|
|
97
|
+
description,
|
|
98
|
+
imageUrl,
|
|
99
|
+
generation,
|
|
100
|
+
isSafe: true,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response types from pokeapi.co API.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface PokeApiPokemonRaw {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
types: Array<{
|
|
12
|
+
slot: number;
|
|
13
|
+
type: {
|
|
14
|
+
name: string;
|
|
15
|
+
url: string;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
sprites: {
|
|
19
|
+
front_default: string;
|
|
20
|
+
other: {
|
|
21
|
+
'official-artwork': {
|
|
22
|
+
front_default: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
species: {
|
|
27
|
+
name: string;
|
|
28
|
+
url: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PokeApiSpeciesRaw {
|
|
33
|
+
id: number;
|
|
34
|
+
name: string;
|
|
35
|
+
generation: {
|
|
36
|
+
name: string; // e.g. "generation-i"
|
|
37
|
+
url: string;
|
|
38
|
+
};
|
|
39
|
+
flavor_text_entries: Array<{
|
|
40
|
+
flavor_text: string;
|
|
41
|
+
language: {
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
44
|
+
version: {
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
47
|
+
}>;
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { PokeApiClient } from '../src/PokeApiClient';
|
|
3
|
+
import { EPokemonType } from '@thalorlabs/types';
|
|
4
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
5
|
+
|
|
6
|
+
// Mock handleRequest on the prototype — no axios import needed in tests
|
|
7
|
+
const mockHandleRequest = vi.fn();
|
|
8
|
+
vi.spyOn(BaseHttpClient.prototype as any, 'handleRequest').mockImplementation(
|
|
9
|
+
mockHandleRequest
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const MOCK_POKEMON_RAW = {
|
|
13
|
+
id: 25,
|
|
14
|
+
name: 'pikachu',
|
|
15
|
+
types: [{ slot: 1, type: { name: 'electric', url: '' } }],
|
|
16
|
+
sprites: {
|
|
17
|
+
front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png',
|
|
18
|
+
other: { 'official-artwork': { front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png' } },
|
|
19
|
+
},
|
|
20
|
+
species: { name: 'pikachu', url: '' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MOCK_SPECIES_RAW = {
|
|
24
|
+
id: 25,
|
|
25
|
+
name: 'pikachu',
|
|
26
|
+
generation: { name: 'generation-i', url: '' },
|
|
27
|
+
flavor_text_entries: [
|
|
28
|
+
{
|
|
29
|
+
flavor_text: 'An electric\nPokemon.',
|
|
30
|
+
language: { name: 'en' },
|
|
31
|
+
version: { name: 'red' },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function mockBothCalls() {
|
|
37
|
+
mockHandleRequest
|
|
38
|
+
.mockResolvedValueOnce({
|
|
39
|
+
response: { data: MOCK_POKEMON_RAW },
|
|
40
|
+
durationMs: 50,
|
|
41
|
+
})
|
|
42
|
+
.mockResolvedValueOnce({
|
|
43
|
+
response: { data: MOCK_SPECIES_RAW },
|
|
44
|
+
durationMs: 50,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('PokeApiClient', () => {
|
|
49
|
+
let client: PokeApiClient;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
client = new PokeApiClient();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('constructor', () => {
|
|
57
|
+
it('sets serviceName to pokeapi', () => {
|
|
58
|
+
expect(client.serviceName).toBe('pokeapi');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('getPokemon', () => {
|
|
63
|
+
it('returns a valid TLPokemon', async () => {
|
|
64
|
+
mockBothCalls();
|
|
65
|
+
|
|
66
|
+
const pokemon = await client.getPokemon();
|
|
67
|
+
|
|
68
|
+
expect(pokemon.name).toBe('Pikachu');
|
|
69
|
+
expect(pokemon.isSafe).toBe(true);
|
|
70
|
+
expect(pokemon.id).toBeDefined();
|
|
71
|
+
expect(pokemon.imageUrl).toBe('https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('maps known EPokemonType values and skips unknown types', async () => {
|
|
75
|
+
const pokemonWithMixedTypes = {
|
|
76
|
+
...MOCK_POKEMON_RAW,
|
|
77
|
+
types: [
|
|
78
|
+
{ slot: 1, type: { name: 'electric', url: '' } },
|
|
79
|
+
{ slot: 2, type: { name: 'fighting', url: '' } },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
mockHandleRequest
|
|
84
|
+
.mockResolvedValueOnce({
|
|
85
|
+
response: { data: pokemonWithMixedTypes },
|
|
86
|
+
durationMs: 50,
|
|
87
|
+
})
|
|
88
|
+
.mockResolvedValueOnce({
|
|
89
|
+
response: { data: MOCK_SPECIES_RAW },
|
|
90
|
+
durationMs: 50,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const pokemon = await client.getPokemon();
|
|
94
|
+
|
|
95
|
+
expect(pokemon.types).toContain(EPokemonType.ELECTRIC);
|
|
96
|
+
expect(pokemon.types).not.toContain('FIGHTING');
|
|
97
|
+
expect(pokemon.types).toHaveLength(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('cleans flavor text by replacing newlines and form feeds', async () => {
|
|
101
|
+
const speciesWithDirtyText = {
|
|
102
|
+
...MOCK_SPECIES_RAW,
|
|
103
|
+
flavor_text_entries: [
|
|
104
|
+
{
|
|
105
|
+
flavor_text: 'A cute\felectric\npokemon\rthat sparks.',
|
|
106
|
+
language: { name: 'en' },
|
|
107
|
+
version: { name: 'red' },
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
mockHandleRequest
|
|
113
|
+
.mockResolvedValueOnce({
|
|
114
|
+
response: { data: MOCK_POKEMON_RAW },
|
|
115
|
+
durationMs: 50,
|
|
116
|
+
})
|
|
117
|
+
.mockResolvedValueOnce({
|
|
118
|
+
response: { data: speciesWithDirtyText },
|
|
119
|
+
durationMs: 50,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const pokemon = await client.getPokemon();
|
|
123
|
+
|
|
124
|
+
expect(pokemon.description).toBe(
|
|
125
|
+
'A cute electric pokemon that sparks.'
|
|
126
|
+
);
|
|
127
|
+
expect(pokemon.description).not.toMatch(/[\n\f\r]/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('parses generation number from roman numerals', async () => {
|
|
131
|
+
mockBothCalls();
|
|
132
|
+
|
|
133
|
+
const pokemon = await client.getPokemon();
|
|
134
|
+
|
|
135
|
+
expect(pokemon.generation).toBe(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('parses higher generation roman numerals correctly', async () => {
|
|
139
|
+
const speciesGen4 = {
|
|
140
|
+
...MOCK_SPECIES_RAW,
|
|
141
|
+
generation: { name: 'generation-iv', url: '' },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
mockHandleRequest
|
|
145
|
+
.mockResolvedValueOnce({
|
|
146
|
+
response: { data: MOCK_POKEMON_RAW },
|
|
147
|
+
durationMs: 50,
|
|
148
|
+
})
|
|
149
|
+
.mockResolvedValueOnce({
|
|
150
|
+
response: { data: speciesGen4 },
|
|
151
|
+
durationMs: 50,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const pokemon = await client.getPokemon();
|
|
155
|
+
|
|
156
|
+
expect(pokemon.generation).toBe(4);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('capitalizes the pokemon name', async () => {
|
|
160
|
+
mockBothCalls();
|
|
161
|
+
|
|
162
|
+
const pokemon = await client.getPokemon();
|
|
163
|
+
|
|
164
|
+
expect(pokemon.name).toBe('Pikachu');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('generates a deterministic id from pokemon name', async () => {
|
|
168
|
+
mockBothCalls();
|
|
169
|
+
const pokemon1 = await client.getPokemon();
|
|
170
|
+
|
|
171
|
+
mockBothCalls();
|
|
172
|
+
const pokemon2 = await client.getPokemon();
|
|
173
|
+
|
|
174
|
+
expect(pokemon1.id).toBe(pokemon2.id);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('makes two handleRequest calls (pokemon + species)', async () => {
|
|
178
|
+
mockBothCalls();
|
|
179
|
+
|
|
180
|
+
await client.getPokemon();
|
|
181
|
+
|
|
182
|
+
expect(mockHandleRequest).toHaveBeenCalledTimes(2);
|
|
183
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
184
|
+
expect.objectContaining({ method: 'GET' }),
|
|
185
|
+
expect.objectContaining({ method: 'GET' })
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('propagates errors from handleRequest', async () => {
|
|
190
|
+
mockHandleRequest.mockRejectedValueOnce(new Error('Network error'));
|
|
191
|
+
|
|
192
|
+
await expect(client.getPokemon()).rejects.toThrow('Network error');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|