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);
});
});
});
});
});