diff --git a/packages/frontend/web-ui/src/game/helpers/getGameSlotIndex.spec.ts b/packages/frontend/web-ui/src/game/helpers/getGameSlotIndex.spec.ts new file mode 100644 index 000000000..1b9a78001 --- /dev/null +++ b/packages/frontend/web-ui/src/game/helpers/getGameSlotIndex.spec.ts @@ -0,0 +1,81 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; + +import { models as apiModels } from '@cornie-js/api-models'; + +import { getGameSlotIndex } from './getGameSlotIndex'; + +describe(getGameSlotIndex.name, () => { + describe('having a user and a game without a slot belonging to that user', () => { + let gameFixture: apiModels.GameV1; + + let userFixture: apiModels.UserV1; + + beforeAll(() => { + gameFixture = { + id: 'id-fixture', + isPublic: true, + state: { + slots: [], + status: 'nonStarted', + }, + }; + + userFixture = { + active: true, + id: 'id-fixture', + name: 'name-fixture', + }; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = getGameSlotIndex(gameFixture, userFixture); + }); + + it('should return expected result', () => { + expect(result).toBeNull(); + }); + }); + }); + + describe('having a user and a game with a slot belonging to that user', () => { + let gameFixture: apiModels.GameV1; + + let userFixture: apiModels.UserV1; + + beforeAll(() => { + userFixture = { + active: true, + id: 'id-fixture', + name: 'name-fixture', + }; + + gameFixture = { + id: 'id-fixture', + isPublic: true, + state: { + slots: [ + { + userId: userFixture.id, + }, + ], + status: 'nonStarted', + }, + }; + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = getGameSlotIndex(gameFixture, userFixture); + }); + + it('should return expected result', () => { + expect(result).toBe('0'); + }); + }); + }); +}); diff --git a/packages/frontend/web-ui/src/game/helpers/getGameSlotIndex.ts b/packages/frontend/web-ui/src/game/helpers/getGameSlotIndex.ts new file mode 100644 index 000000000..ca712e888 --- /dev/null +++ b/packages/frontend/web-ui/src/game/helpers/getGameSlotIndex.ts @@ -0,0 +1,23 @@ +import { models as apiModels } from '@cornie-js/api-models'; + +export function getGameSlotIndex( + game: apiModels.GameV1 | undefined, + user: apiModels.UserV1, +): string | null { + if (game === undefined) { + return null; + } + + for (let i: number = 0; i < game.state.slots.length; ++i) { + const gameSlot: apiModels.ActiveGameSlotV1 | apiModels.FinishedGameSlotV1 = + game.state.slots[i] as + | apiModels.ActiveGameSlotV1 + | apiModels.FinishedGameSlotV1; + + if (gameSlot.userId === user.id) { + return i.toString(); + } + } + + return null; +} diff --git a/packages/frontend/web-ui/src/game/hooks/useGame.spec.ts b/packages/frontend/web-ui/src/game/hooks/useGame.spec.ts new file mode 100644 index 000000000..47289e79b --- /dev/null +++ b/packages/frontend/web-ui/src/game/hooks/useGame.spec.ts @@ -0,0 +1,271 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('../../common/hooks/useRedirectUnauthorized'); +jest.mock('../../common/hooks/useUrlLikeLocation'); +jest.mock('../../user/hooks/useGetUserMe'); +jest.mock('../helpers/getGameSlotIndex'); +jest.mock('./useGameCards'); +jest.mock('./useGetGamesV1GameId'); +jest.mock('./useGetGamesV1GameIdSlotsSlotIdCards'); + +import { models as apiModels } from '@cornie-js/api-models'; +import { renderHook, RenderHookResult } from '@testing-library/react'; + +import { useRedirectUnauthorized } from '../../common/hooks/useRedirectUnauthorized'; +import { useUrlLikeLocation } from '../../common/hooks/useUrlLikeLocation'; +import { UrlLikeLocation } from '../../common/models/UrlLikeLocation'; +import { useGetUserMe } from '../../user/hooks/useGetUserMe'; +import { getGameSlotIndex } from '../helpers/getGameSlotIndex'; +import { useGame, UseGameResult } from './useGame'; +import { useGameCards, UseGameCardsResult } from './useGameCards'; +import { useGetGamesV1GameId } from './useGetGamesV1GameId'; +import { useGetGamesV1GameIdSlotsSlotIdCards } from './useGetGamesV1GameIdSlotsSlotIdCards'; + +describe(useGame.name, () => { + describe('when called, and queries return null result', () => { + let gameIdFixture: string; + let urlLikeLocationFixture: UrlLikeLocation; + let useGameCardsResultFixture: UseGameCardsResult; + + let renderResult: RenderHookResult; + + beforeAll(() => { + gameIdFixture = 'game-id-fixture'; + + urlLikeLocationFixture = { + pathname: '/path', + searchParams: new URLSearchParams(`?gameId=${gameIdFixture}`), + } as Partial as UrlLikeLocation; + + useGameCardsResultFixture = { + cards: [], + hasNext: false, + hasPrevious: false, + setNext: jest.fn(), + setPrevious: jest.fn(), + }; + + ( + useUrlLikeLocation as jest.Mock + ).mockReturnValueOnce(urlLikeLocationFixture); + + (useGetUserMe as jest.Mock).mockReturnValueOnce({ + result: null, + }); + + ( + useGetGamesV1GameId as jest.Mock + ).mockReturnValueOnce({ result: null }); + + ( + useGetGamesV1GameIdSlotsSlotIdCards as jest.Mock< + typeof useGetGamesV1GameIdSlotsSlotIdCards + > + ).mockReturnValueOnce({ result: null }); + + (useGameCards as jest.Mock).mockReturnValueOnce( + useGameCardsResultFixture, + ); + + renderResult = renderHook(() => useGame()); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call useRedirectUnauthorized()', () => { + expect(useRedirectUnauthorized).toHaveBeenCalledTimes(1); + expect(useRedirectUnauthorized).toHaveBeenCalledWith(); + }); + + it('should call useGetUserMe()', () => { + expect(useGetUserMe).toHaveBeenCalledTimes(1); + expect(useGetUserMe).toHaveBeenCalledWith(); + }); + + it('should call useGetGamesV1GameId()', () => { + expect(useGetGamesV1GameId).toHaveBeenCalledTimes(1); + expect(useGetGamesV1GameId).toHaveBeenCalledWith(gameIdFixture); + }); + + it('should not call getGameSlotIndex()', () => { + expect(getGameSlotIndex).not.toHaveBeenCalled(); + }); + + it('should call useGetGamesV1GameIdSlotsSlotIdCards()', () => { + expect(useGetGamesV1GameIdSlotsSlotIdCards).toHaveBeenCalledTimes(1); + expect(useGetGamesV1GameIdSlotsSlotIdCards).toHaveBeenCalledWith( + gameIdFixture, + null, + ); + }); + + it('should call useGameCards()', () => { + expect(useGameCards).toHaveBeenCalledTimes(1); + expect(useGameCards).toHaveBeenCalledWith([]); + }); + + it('should retuen expected result', () => { + const expected: UseGameResult = { + currentCard: undefined, + game: undefined, + isPending: true, + useGameCardsResult: useGameCardsResultFixture, + }; + + expect(renderResult.result.current).toStrictEqual(expected); + }); + }); + + describe('when called, and queries return non null results', () => { + let gameCardsFixture: apiModels.CardArrayV1; + let gameFixture: apiModels.ActiveGameV1; + let gameIdFixture: string; + let gameSlotIndexFixture: string; + let urlLikeLocationFixture: UrlLikeLocation; + let userFixture: apiModels.UserV1; + let useGameCardsResultFixture: UseGameCardsResult; + + let renderResult: RenderHookResult; + + beforeAll(() => { + gameCardsFixture = [ + { + kind: 'wildDraw4', + }, + ]; + + gameFixture = { + id: 'game-id-fixture', + isPublic: true, + state: { + currentCard: { + kind: 'wild', + }, + currentColor: 'blue', + currentDirection: 'clockwise', + currentPlayingSlotIndex: 0, + currentTurnCardsDrawn: false, + currentTurnCardsPlayed: false, + drawCount: 0, + lastEventId: 'last-event-id-fixture', + slots: [], + status: 'active', + }, + }; + + gameSlotIndexFixture = '0'; + + userFixture = { + active: true, + id: 'user-id-fixture', + name: 'user-name-fixture', + }; + + gameIdFixture = gameFixture.id; + + urlLikeLocationFixture = { + pathname: '/path', + searchParams: new URLSearchParams(`?gameId=${gameIdFixture}`), + } as Partial as UrlLikeLocation; + + useGameCardsResultFixture = { + cards: [], + hasNext: false, + hasPrevious: false, + setNext: jest.fn(), + setPrevious: jest.fn(), + }; + + ( + useUrlLikeLocation as jest.Mock + ).mockReturnValueOnce(urlLikeLocationFixture); + + (useGetUserMe as jest.Mock).mockReturnValueOnce({ + result: { + isRight: true, + value: userFixture, + }, + }); + + ( + useGetGamesV1GameId as jest.Mock + ).mockReturnValueOnce({ + result: { + isRight: true, + value: gameFixture, + }, + }); + + ( + useGetGamesV1GameIdSlotsSlotIdCards as jest.Mock< + typeof useGetGamesV1GameIdSlotsSlotIdCards + > + ).mockReturnValueOnce({ + result: { + isRight: true, + value: gameCardsFixture, + }, + }); + + ( + getGameSlotIndex as jest.Mock + ).mockReturnValueOnce(gameSlotIndexFixture); + + (useGameCards as jest.Mock).mockReturnValueOnce( + useGameCardsResultFixture, + ); + + renderResult = renderHook(() => useGame()); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call useRedirectUnauthorized()', () => { + expect(useRedirectUnauthorized).toHaveBeenCalledTimes(1); + expect(useRedirectUnauthorized).toHaveBeenCalledWith(); + }); + + it('should call useGetUserMe()', () => { + expect(useGetUserMe).toHaveBeenCalledTimes(1); + expect(useGetUserMe).toHaveBeenCalledWith(); + }); + + it('should call useGetGamesV1GameId()', () => { + expect(useGetGamesV1GameId).toHaveBeenCalledTimes(1); + expect(useGetGamesV1GameId).toHaveBeenCalledWith(gameIdFixture); + }); + + it('should call getGameSlotIndex()', () => { + expect(getGameSlotIndex).toHaveBeenCalledTimes(1); + expect(getGameSlotIndex).toHaveBeenCalledWith(gameFixture, userFixture); + }); + + it('should call useGetGamesV1GameIdSlotsSlotIdCards()', () => { + expect(useGetGamesV1GameIdSlotsSlotIdCards).toHaveBeenCalledTimes(1); + expect(useGetGamesV1GameIdSlotsSlotIdCards).toHaveBeenCalledWith( + gameIdFixture, + gameSlotIndexFixture, + ); + }); + + it('should call useGameCards()', () => { + expect(useGameCards).toHaveBeenCalledTimes(1); + expect(useGameCards).toHaveBeenCalledWith(gameCardsFixture); + }); + + it('should retuen expected result', () => { + const expected: UseGameResult = { + currentCard: gameFixture.state.currentCard, + game: gameFixture, + isPending: false, + useGameCardsResult: useGameCardsResultFixture, + }; + + expect(renderResult.result.current).toStrictEqual(expected); + }); + }); +}); diff --git a/packages/frontend/web-ui/src/game/hooks/useGame.ts b/packages/frontend/web-ui/src/game/hooks/useGame.ts new file mode 100644 index 000000000..6e3dfa022 --- /dev/null +++ b/packages/frontend/web-ui/src/game/hooks/useGame.ts @@ -0,0 +1,67 @@ +import { models as apiModels } from '@cornie-js/api-models'; + +import { useRedirectUnauthorized } from '../../common/hooks/useRedirectUnauthorized'; +import { useUrlLikeLocation } from '../../common/hooks/useUrlLikeLocation'; +import { UrlLikeLocation } from '../../common/models/UrlLikeLocation'; +import { useGetUserMe } from '../../user/hooks/useGetUserMe'; +import { getGameSlotIndex } from '../helpers/getGameSlotIndex'; +import { useGameCards, UseGameCardsResult } from './useGameCards'; +import { useGetGamesV1GameId } from './useGetGamesV1GameId'; +import { useGetGamesV1GameIdSlotsSlotIdCards } from './useGetGamesV1GameIdSlotsSlotIdCards'; + +export interface UseGameResult { + currentCard: apiModels.CardV1 | undefined; + game: apiModels.GameV1 | undefined; + isPending: boolean; + useGameCardsResult: UseGameCardsResult; +} + +function getGameCurrentCard( + game: apiModels.GameV1 | undefined, +): apiModels.CardV1 | undefined { + return game?.state.status === 'active' ? game.state.currentCard : undefined; +} + +export const useGame = (): UseGameResult => { + useRedirectUnauthorized(); + + const url: UrlLikeLocation = useUrlLikeLocation(); + const gameIdParam: string | null = url.searchParams.get('gameId'); + + const { result: usersV1MeResult } = useGetUserMe(); + + const { result: gamesV1GameIdResult } = useGetGamesV1GameId(gameIdParam); + + const game: apiModels.GameV1 | undefined = + gamesV1GameIdResult?.isRight === true + ? gamesV1GameIdResult.value + : undefined; + + const currentCard: apiModels.CardV1 | undefined = getGameCurrentCard(game); + + const gameSlotIndexParam: string | null = + usersV1MeResult?.isRight === true + ? getGameSlotIndex(game, usersV1MeResult.value) + : null; + + const { result: gamesV1GameIdSlotsSlotIdCardsResult } = + useGetGamesV1GameIdSlotsSlotIdCards(gameIdParam, gameSlotIndexParam); + + const isPending = + gamesV1GameIdResult === null || + gamesV1GameIdSlotsSlotIdCardsResult === null; + + const gameCards: apiModels.CardArrayV1 = + gamesV1GameIdSlotsSlotIdCardsResult?.isRight === true + ? gamesV1GameIdSlotsSlotIdCardsResult.value + : []; + + const useGameCardsResult: UseGameCardsResult = useGameCards(gameCards); + + return { + currentCard, + game, + isPending, + useGameCardsResult, + }; +};