import type { IFetcher } from '~/models/fetcher'; import type { Immutable } from '~/models/immutable'; import type { Logger } from '~/models/logger'; import mockFrontPageSets from '~/testing/front-page.json'; import mockSearchResults from '~/testing/search-results.json'; import path from 'path'; import { readFile } from 'fs/promises'; import { RT_SEARCH_URL } from '~/globals/rt-urls'; import { RTScraperGeneric } from '~/models/rt-scraper-generic'; import type { MediaSet, Movie, TVSeries } from '@rt_lite/common/models'; const pageWithoutBody = ` Page `.trim(); // eslint-disable-next-line max-lines-per-function describe('RTLite Generic class', () => { /* eslint-disable @typescript-eslint/init-declarations */ let mockFetchErrorToThrow: Error | null; let mockFetchEmptyBody: boolean; let shouldLog: boolean; /* eslint-enable @typescript-eslint/init-declarations */ const mockFetch = jest.fn(async (url: string) => { if (mockFetchErrorToThrow) throw mockFetchErrorToThrow; if (url.includes(RT_SEARCH_URL.href)) return { // eslint-disable-next-line @typescript-eslint/require-await json: async (): Promise => mockSearchResults }; const frontPageText = mockFetchEmptyBody ? pageWithoutBody : await readFile(path.resolve(__dirname, '../testing/front-page.html'), 'utf-8'); // eslint-disable-next-line @typescript-eslint/require-await return { text: async (): Promise => frontPageText }; }); const mockBodyProducer = jest.fn((html: string): HTMLBodyElement => { const body = document.createElement('body'); body.innerHTML = html; return body; }); const mockLogger = { // eslint-disable-next-line promise/prefer-await-to-callbacks error: jest.fn((err: Immutable): void => { if (err !== mockFetchErrorToThrow) console.error(err); }) }; const rtLiteGenericFactory = (): RTScraperGeneric => new RTScraperGeneric( mockFetch as unknown as IFetcher, mockBodyProducer, shouldLog, (mockLogger as unknown) as Logger ); beforeEach(() => { mockFetchEmptyBody = false; mockFetchErrorToThrow = null; shouldLog = true; }); afterEach(() => { jest.clearAllMocks(); }); describe('findMedia', () => { test('Returns empty results if no terms specified', async () => { expect.assertions(5); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMedia(); expect(results).not.toBeNull(); expect(Array.isArray(results.movies)).toStrictEqual(true); expect(Array.isArray(results.tvSeries)).toStrictEqual(true); expect(results.movies).toHaveLength(0); expect(results.tvSeries).toHaveLength(0); }); test('Returns empty results and logs error if error encountered and logging enabled', async () => { expect.assertions(7); mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMedia('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results.movies)).toStrictEqual(true); expect(Array.isArray(results.tvSeries)).toStrictEqual(true); expect(results.movies).toHaveLength(0); expect(results.tvSeries).toHaveLength(0); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledWith(mockFetchErrorToThrow); }); test('Returns empty results if error encountered and logging disabled', async () => { expect.assertions(6); shouldLog = false; mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMedia('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results.movies)).toStrictEqual(true); expect(Array.isArray(results.tvSeries)).toStrictEqual(true); expect(results.movies).toHaveLength(0); expect(results.tvSeries).toHaveLength(0); expect(mockLogger.error).not.toHaveBeenCalled(); }); test('Returns full results mapped to rtlite models', async () => { // eslint-disable-next-line @typescript-eslint/no-extra-parens,jest/prefer-expect-assertions expect.assertions(5 + (mockSearchResults.movies.length * 6) + (mockSearchResults.tvSeries.length * 6)); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMedia('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results.movies)).toStrictEqual(true); expect(Array.isArray(results.tvSeries)).toStrictEqual(true); expect(results.movies).toHaveLength(mockSearchResults.movies.length); expect(results.tvSeries).toHaveLength(mockSearchResults.tvSeries.length); results.movies.forEach((movie: Immutable, index: number) => { const { movies: { [index]: rtMovie } } = mockSearchResults; expect(movie.title).toStrictEqual(rtMovie.name); expect(movie.url).not.toBeNull(); expect(movie.url).toBeInstanceOf(URL); expect(movie.url.pathname).toStrictEqual(rtMovie.url); /* eslint-disable jest/no-conditional-expect */ // eslint-disable-next-line max-len if (rtMovie.meterScore !== null && !isNaN(rtMovie.meterScore)) expect(movie.score).toStrictEqual(rtMovie.meterScore); else expect(movie.score).toBeUndefined(); if (isNaN(rtMovie.year)) expect(movie.year).toBeUndefined(); else expect(movie.year).toStrictEqual(rtMovie.year); /* eslint-enable jest/no-conditional-expect */ }); results.tvSeries.forEach((tvSeries: Immutable, index: number) => { const { tvSeries: { [index]: rtTvSeries } } = mockSearchResults; expect(tvSeries.title).toStrictEqual(rtTvSeries.title); expect(tvSeries.url).not.toBeNull(); expect(tvSeries.url).toBeInstanceOf(URL); expect(tvSeries.url.pathname).toStrictEqual(rtTvSeries.url); /* eslint-disable jest/no-conditional-expect,@typescript-eslint/no-unnecessary-condition */ if (rtTvSeries.meterScore === null || isNaN(rtTvSeries.meterScore)) expect(tvSeries.score).toBeUndefined(); else expect(tvSeries.score).toStrictEqual(rtTvSeries.meterScore); if (rtTvSeries.startYear === null || isNaN(rtTvSeries.startYear)) expect(tvSeries.year).toBeUndefined(); else expect(tvSeries.year).toStrictEqual(rtTvSeries.startYear); /* eslint-enable jest/no-conditional-expect,@typescript-eslint/no-unnecessary-condition */ }); }); }); describe('findMovies', () => { test('Returns empty results if no terms specified', async () => { expect.assertions(3); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMovies(); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); }); test('Returns empty results and logs error if error encountered and logging enabled', async () => { expect.assertions(5); mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMovies('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledWith(mockFetchErrorToThrow); }); test('Returns empty results if error encountered and logging disabled', async () => { expect.assertions(4); shouldLog = false; mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMovies('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); expect(mockLogger.error).not.toHaveBeenCalled(); }); test('Returns full results mapped to rtlite movie model', async () => { // eslint-disable-next-line @typescript-eslint/no-extra-parens,jest/prefer-expect-assertions expect.assertions(3 + (mockSearchResults.movies.length * 6)); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findMovies('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(mockSearchResults.movies.length); results.forEach((movie: Immutable, index: number) => { const { movies: { [index]: rtMovie } } = mockSearchResults; expect(movie.title).toStrictEqual(rtMovie.name); expect(movie.url).not.toBeNull(); expect(movie.url).toBeInstanceOf(URL); expect(movie.url.pathname).toStrictEqual(rtMovie.url); /* eslint-disable jest/no-conditional-expect */ // eslint-disable-next-line max-len if (rtMovie.meterScore !== null && !isNaN(rtMovie.meterScore)) expect(movie.score).toStrictEqual(rtMovie.meterScore); else expect(movie.score).toBeUndefined(); if (isNaN(rtMovie.year)) expect(movie.year).toBeUndefined(); else expect(movie.year).toStrictEqual(rtMovie.year); /* eslint-enable jest/no-conditional-expect */ }); }); }); describe('findTVSeries', () => { test('Returns empty results if no terms specified', async () => { expect.assertions(3); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findTVSeries(); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); }); test('Returns empty results and logs error if error encountered and logging enabled', async () => { expect.assertions(5); mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findTVSeries('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledWith(mockFetchErrorToThrow); }); test('Returns empty results if error encountered and logging disabled', async () => { expect.assertions(4); shouldLog = false; mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findTVSeries('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); expect(mockLogger.error).not.toHaveBeenCalled(); }); test('Returns full results mapped to rtlite tvseries model', async () => { // eslint-disable-next-line @typescript-eslint/no-extra-parens,jest/prefer-expect-assertions expect.assertions(3 + (mockSearchResults.tvSeries.length * 6)); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.findTVSeries('planet of the apes'); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(mockSearchResults.tvSeries.length); results.forEach((series: Immutable, index: number) => { const { tvSeries: { [index]: rtSeries } } = mockSearchResults; expect(series.title).toStrictEqual(rtSeries.title); expect(series.url).not.toBeNull(); expect(series.url).toBeInstanceOf(URL); expect(series.url.pathname).toStrictEqual(rtSeries.url); /* eslint-disable jest/no-conditional-expect,@typescript-eslint/no-unnecessary-condition */ // eslint-disable-next-line max-len if (rtSeries.meterScore !== null && !isNaN(rtSeries.meterScore)) expect(series.score).toStrictEqual(rtSeries.meterScore); else expect(series.score).toBeUndefined(); if (isNaN(rtSeries.startYear)) expect(series.year).toBeUndefined(); else expect(series.year).toStrictEqual(rtSeries.startYear); /* eslint-enable jest/no-conditional-expect,@typescript-eslint/no-unnecessary-condition */ }); }); }); describe('getFrontPageSets', () => { test('Returns empty results and logs error if error encountered and logging enabled', async () => { expect.assertions(5); mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.getFrontPageSets(); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledWith(mockFetchErrorToThrow); }); test('Returns empty results if error encountered and logging disabled', async () => { expect.assertions(4); shouldLog = false; mockFetchErrorToThrow = new Error('uh-oh'); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.getFrontPageSets(); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); expect(mockLogger.error).not.toHaveBeenCalled(); }); test('Returns empty results if body not found on page', async () => { expect.assertions(4); mockFetchEmptyBody = true; const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.getFrontPageSets(); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(0); expect(mockLogger.error).not.toHaveBeenCalled(); }); // eslint-disable-next-line jest/prefer-expect-assertions test('Returns full results if no problems encountered', async () => { const { length: totalMovieCount } = mockFrontPageSets .map(({ media }: Immutable) => media) .flat(Infinity); // eslint-disable-next-line @typescript-eslint/no-extra-parens expect.assertions(3 + (mockFrontPageSets.length * 2) + totalMovieCount); const rtLiteGeneric = rtLiteGenericFactory(); const results = await rtLiteGeneric.getFrontPageSets(); expect(results).not.toBeNull(); expect(Array.isArray(results)).toStrictEqual(true); expect(results).toHaveLength(mockFrontPageSets.length); results.forEach((frontPageSet: Immutable, index) => { const { [index]: mockSet } = mockFrontPageSets; expect(frontPageSet.title).toStrictEqual(mockSet.title); expect(frontPageSet.media).toHaveLength(mockSet.media.length); frontPageSet.media .map(media => ({ ...media, url: media.url.href })) // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types .forEach((media, jndex) => { const { media: { [jndex]: mockMedia } } = mockSet; expect(media).toStrictEqual(mockMedia); }); }); }); }); });