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
57 changes: 56 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ The GraphQL server listens on port `3001`. When running locally, the endpoint is

## GraphQL API

The API currently supports retrieving lists of Pokémon and individual Pokémon by ID.
The API currently supports retrieving lists of Pokémon, individual Pokémon by ID, base stats for a set of Pokémon, the available moves in a format, and which Pokémon can learn a set of moves.

### Retrieve Pokémon

Expand Down Expand Up @@ -100,6 +100,57 @@ query {

This query retrieves a single Pokémon. If no Pokémon matches the supplied ID, the resolver returns `null`.

### Retrieve base stats for several Pokémon

```graphql
query {
pokemonStats(ids: ["pikachu", "charizard"]) {
id
name
hp
atk
def
spa
spd
spe
total
}
}
```

This query returns base stats and the Base Stat Total (`total`) for up to 100 Pokémon at once. Unknown IDs are ignored. The home page uses this query to show its team stats overview.

### Retrieve the moves available in a format

```graphql
query {
moves(format: "gen9") {
id
name
type
}
}
```

This query returns every move introduced up to the selected format's generation, sorted by name. `type` is the move's type and is used by the frontend to colour the move slots.

### Retrieve which Pokémon can learn selected moves

```graphql
query {
moveLearners(
format: "gen9"
pokemonIds: ["garchomp", "pikachu", "skarmory"]
moveIds: ["stealthrock", "aeroblast"]
) {
moveId
pokemonIds
}
}
```

For each requested move, this query returns the subset of the requested Pokémon that can learn it in the selected format. Up to 100 Pokémon IDs and 100 move IDs can be requested at once, and unknown IDs are ignored. The home page runs this query once per move line (up to 16 lines) to show the team members that learn the searched move.

## Frontend Integration

The React frontend uses Apollo Client to communicate with the GraphQL API. The frontend must be configured to use an endpoint that is reachable from the browser, and the backend must be running for API requests to succeed.
Expand Down Expand Up @@ -153,7 +204,11 @@ The tests use `http://localhost:3001/` by default. To test another endpoint, set
- Retrieving the Pokémon list
- Retrieving a Pokémon by ID
- Retrieving a Pokémon's sprite file name
- Retrieving base stats and Base Stat Total for several Pokémon
- Retrieving the moves available in a format
- Retrieving which team members can learn each selected move
- Returning `null` for an unknown Pokémon ID
- Ignoring unknown IDs when requesting base stats
- Rejecting a query containing an invalid GraphQL field

The tests are read-only and use the existing database. They do not create or modify database records.
203 changes: 203 additions & 0 deletions backend/src/graphql/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ type SearchPokemon = {
num: number;
};

type PokemonStats = {
id: string;
name: string;
hp: number;
atk: number;
def: number;
spa: number;
spd: number;
spe: number;
total: number;
};

type GraphQLResponse<T> = {
data?: T;
errors?: { message: string }[];
Expand Down Expand Up @@ -158,6 +170,197 @@ describe("GraphQL API integration", () => {
expect(body.errors!.length).toBeGreaterThan(0);
});

it("returns base stats and the base stat total for the requested Pokémon", async () => {
const { response, body } = await sendQuery<{
pokemonStats: PokemonStats[];
}>(
`
query PokemonStats($ids: [ID!]!) {
pokemonStats(ids: $ids) {
id
name
hp
atk
def
spa
spd
spe
total
}
}
`,
{ ids: ["pikachu", "charizard", "blissey"] },
);

expect(response.status).toBe(200);
expect(body.errors).toBeUndefined();

const statsById = new Map(
body.data!.pokemonStats.map((stats) => [stats.id, stats]),
);

expect(statsById.get("pikachu")).toEqual({
id: "pikachu",
name: "Pikachu",
hp: 35,
atk: 55,
def: 40,
spa: 50,
spd: 50,
spe: 90,
total: 320,
});

const blissey = statsById.get("blissey")!;

expect(blissey.total).toBe(
blissey.hp +
blissey.atk +
blissey.def +
blissey.spa +
blissey.spd +
blissey.spe,
);
});

it("ignores unknown IDs when requesting base stats", async () => {
const { body } = await sendQuery<{
pokemonStats: { id: string }[];
}>(
`
query PokemonStats($ids: [ID!]!) {
pokemonStats(ids: $ids) {
id
}
}
`,
{ ids: ["pikachu", "__integration_test_missing_pokemon__"] },
);

expect(body.errors).toBeUndefined();
expect(body.data!.pokemonStats).toEqual([{ id: "pikachu" }]);
});

it("returns moves available in a format with their types", async () => {
const { response, body } = await sendQuery<{
moves: { id: string; name: string; type: string }[];
}>(`
query {
moves(format: "gen9") {
id
name
type
}
}
`);

expect(response.status).toBe(200);
expect(body.errors).toBeUndefined();

const moves = body.data!.moves;
const stealthRock = moves.find((move) => move.id === "stealthrock");

expect(stealthRock).toEqual({
id: "stealthrock",
name: "Stealth Rock",
type: "Rock",
});

const names = moves.map((move) => move.name);
const sortedNames = [...names].sort((a, b) => a.localeCompare(b));

expect(names).toEqual(sortedNames);
});

it("leaves out moves introduced after the selected format", async () => {
const { response, body } = await sendQuery<{
moves: { id: string }[];
}>(`
query {
moves(format: "gen1") {
id
}
}
`);

expect(response.status).toBe(200);
expect(body.errors).toBeUndefined();

const ids = body.data!.moves.map((move) => move.id);

expect(ids).toContain("thunderwave");
expect(ids).not.toContain("stealthrock");
});

it("returns which team members can learn each requested move", async () => {
const { response, body } = await sendQuery<{
moveLearners: { moveId: string; pokemonIds: string[] }[];
}>(
`
query MoveLearners(
$format: String!
$pokemonIds: [ID!]!
$moveIds: [ID!]!
) {
moveLearners(
format: $format
pokemonIds: $pokemonIds
moveIds: $moveIds
) {
moveId
pokemonIds
}
}
`,
{
format: "gen9",
pokemonIds: ["garchomp", "pikachu", "skarmory"],
moveIds: ["stealthrock", "earthquake", "aeroblast"],
},
);

expect(response.status).toBe(200);
expect(body.errors).toBeUndefined();
expect(body.data!.moveLearners).toEqual([
{ moveId: "stealthrock", pokemonIds: ["garchomp", "skarmory"] },
{ moveId: "earthquake", pokemonIds: ["garchomp"] },
{ moveId: "aeroblast", pokemonIds: [] },
]);
});

it("ignores unknown move IDs when finding team members that learn a move", async () => {
const { body } = await sendQuery<{
moveLearners: { moveId: string; pokemonIds: string[] }[];
}>(
`
query MoveLearners(
$format: String!
$pokemonIds: [ID!]!
$moveIds: [ID!]!
) {
moveLearners(
format: $format
pokemonIds: $pokemonIds
moveIds: $moveIds
) {
moveId
pokemonIds
}
}
`,
{
format: "gen9",
pokemonIds: ["pikachu"],
moveIds: ["__integration_test_missing_move__"],
},
);

expect(body.errors).toBeUndefined();
expect(body.data!.moveLearners).toEqual([
{ moveId: "__integration_test_missing_move__", pokemonIds: [] },
]);
});

it("respects the requested result limit", async () => {
const { response, body } = await sendQuery<{
searchPokemons: SearchPokemon[];
Expand Down
Loading