initial commit

This commit is contained in:
RY4N
2026-03-27 00:33:48 +06:00
commit 3a63c75b3a
63 changed files with 6301 additions and 0 deletions

214
scripts/tests/data/mocks.ts Normal file
View File

@@ -0,0 +1,214 @@
export const mockHtmlData = {
homepage: `
<div class="deslide-wrap">
<div class="swiper-wrapper">
<div class="swiper-slide">
<div class="deslide-cover">
<img class="film-poster-img" data-src="https://example.com/poster.jpg">
</div>
<div class="desi-head-title">Spotlight Anime</div>
<div class="desi-description">Description text</div>
<div class="desi-buttons">
<a href="/watch/spotlight-123"></a>
</div>
<div class="sc-detail">
<span class="scd-item">TV</span>
<span class="scd-item">24m</span>
<span class="scd-item m-hide">Oct 1, 2023</span>
<span class="tick-sub">12</span>
<span class="tick-dub">10</span>
<span class="tick-eps">12</span>
</div>
</div>
</div>
</div>
<div id="trending-home">
<div class="swiper-container">
<div class="swiper-slide">
<div class="item">
<div class="film-title">Trending Anime</div>
<a href="/watch/trending-456" class="film-poster">
<img data-src="https://example.com/trending.jpg">
</a>
</div>
</div>
</div>
</div>
<div id="anime-featured">
<div class="anif-blocks">
<div class="anif-block">
<div class="anif-block-header">Most Popular</div>
<div class="anif-block-ul">
<ul>
<li>
<div class="film-poster">
<img class="film-poster-img" data-src="https://example.com/popular.jpg">
</div>
<div class="film-detail">
<h3 class="film-name"><a href="/watch/popular-789" title="Popular Anime"></a></h3>
<div class="fd-infor">
<span class="fdi-item">TV</span>
<span class="fdi-item">24m</span>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
`,
detail: `
<div id="ani_detail">
<div class="anis-content">
<div class="film-poster">
<img class="film-poster-img" src="https://example.com/detail.jpg">
<div class="tick-rate">18+</div>
</div>
<div class="anisc-detail">
<h2 class="film-name">Detail Anime</h2>
<div class="film-stats">
<div class="tick">
<span class="item">TV</span>
<span class="tick-sub">12</span>
<span class="tick-dub">10</span>
<span class="tick-eps">12</span>
</div>
</div>
<div class="film-buttons">
<a href="/watch/detail-123" class="btn"></a>
</div>
</div>
<div class="anisc-info-wrap">
<div class="anisc-info">
<div class="item">
<span class="item-head">Japanese:</span>
<span class="name">日本語</span>
</div>
<div class="item">
<span class="item-head">Aired:</span>
<span class="name">Oct 1, 2023 to ?</span>
</div>
</div>
</div>
</div>
</div>
`,
search: `
<div class="block_area-content block_area-list film_list">
<div class="film_list-wrap">
<div class="flw-item">
<div class="film-poster">
<img class="film-poster-img" data-src="https://example.com/search.jpg">
<a href="/watch/search-123"></a>
</div>
<div class="film-detail">
<h3 class="film-name"><a class="dynamic-name" href="/watch/search-123">Search Result</a></h3>
<div class="fd-infor">
<span class="fdi-item">TV</span>
</div>
</div>
</div>
</div>
</div>
`,
characters: `
<div class="block_area-content block_area-list film_list">
<div class="film_list-wrap">
<div class="bac-item">
<div class="per-info">
<div class="pi-avatar">
<a href="/character/char-123">
<img data-src="https://example.com/character.jpg">
</a>
</div>
<div class="pi-detail">
<div class="pi-name"><a href="/character/char-123">Character Name</a></div>
<div class="pi-cast">Main</div>
</div>
</div>
</div>
</div>
</div>
`,
news: `
<div class="zr-news-list">
<div class="item">
<a class="zrn-title" href="/news/news-123"></a>
<h3 class="news-title">News Title</h3>
<div class="description">News description</div>
<img class="zrn-image" src="https://example.com/news.jpg">
<div class="time-posted">Oct 1, 2023</div>
</div>
</div>
`,
schedule: `
<div class="block_area-content block_area-list film_list">
<a href="/watch/anime-123">
<div class="time">10:00</div>
<div class="film-name" data-jname="Schedule Alt">Scheduled Anime</div>
<div class="btn-play">Episode 5</div>
</a>
</div>
`,
episodes: `
<div class="block_area-content block_area-list film_list">
<div class="detail-channels">
<a class="ssl-item ep-item" href="/watch/ep-1" title="Episode 1">
<div class="ep-name e-dynamic-name" data-jname="Ep 1 Alt"></div>
</a>
</div>
</div>
`,
characterDetail: `
<div class="actor-page-wrap">
<div class="avatar">
<img src="https://example.com/char-detail.jpg">
</div>
<div class="apw-detail">
<div class="name">Character Full Name</div>
<div class="sub-name">Character Japanese Name</div>
<div class="tab-content">
<div id="bio">
<div class="bio"><p>Character biography</p></div>
</div>
</div>
</div>
</div>
`,
suggestions: `
<div class="nav-item">
<a href="/watch/suggest-1">
<img class="film-poster-img" data-src="https://example.com/s1.jpg">
<div class="film-name">S1</div>
<div class="film-infor"><span>A1</span><span>D1</span></div>
</a>
</div>
<div class="nav-item">
<a href="/watch/suggest-2">
<img class="film-poster-img" data-src="https://example.com/s2.jpg">
<div class="film-name">S2</div>
<div class="film-infor"><span>A2</span><span>D2</span></div>
</a>
</div>
<div class="nav-item">
<a href="/watch/suggest-3">
<img class="film-poster-img" data-src="https://example.com/s3.jpg">
<div class="film-name">Suggest Title</div>
<div class="film-infor"><span>A3</span><span>D3</span></div>
</a>
</div>
`,
topSearch: `
<div class="xhashtag">
<a class="item" href="/watch/top-1">T1</a>
<a class="item" href="/watch/top-2">T2</a>
<a class="item" href="/watch/top-123">Top Title</a>
</div>
`,
scheduleNext: `
<div class="block_area-content">
<div id="schedule-date" data-value="10:00"></div>
</div>
`,
};

View File

@@ -0,0 +1,140 @@
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { Context } from 'hono';
import homepageController from '../../../../src/controllers/homepage.controller';
import detailpageController from '../../../../src/controllers/detailpage.controller';
import searchController from '../../../../src/controllers/search.controller';
import episodesController from '../../../../src/controllers/episodes.controller';
import charactersController from '../../../../src/controllers/characters.controller';
import characterDetailController from '../../../../src/controllers/characterDetail.controller';
import listpageController from '../../../../src/controllers/listpage.controller';
import topSearchController from '../../../../src/controllers/topSearch.controller';
import schedulesController from '../../../../src/controllers/schedules.controller';
import newsController from '../../../../src/controllers/news.controller';
import suggestionController from '../../../../src/controllers/suggestion.controller';
import nextEpisodeScheduleController from '../../../../src/controllers/nextEpisodeSchedule.controller';
import randomController from '../../../../src/controllers/random.controller';
import filterController from '../../../../src/controllers/filter.controller';
import allGenresController from '../../../../src/controllers/allGenres.controller';
import { mockHtmlData } from '../../data/mocks';
// Mock global fetch since axiosInstance uses it
const mockFetch = jest.fn<typeof global.fetch>();
global.fetch = mockFetch as unknown as typeof global.fetch;
const createMockContext = (
params: Record<string, string> = {},
query: Record<string, string> = {}
) => {
return {
req: {
param: (name?: string) => (name ? params[name] : params),
query: (name?: string) => (name ? query[name] : query),
},
json: jest.fn((data: unknown) => data),
} as unknown as Context;
};
describe('Controllers Comprehensive Suite (Jest)', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const mockSuccess = (data: string) => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
text: () => Promise.resolve(data),
headers: new Map(),
} as unknown as Response);
};
it('homepageController should return homepage data', async () => {
mockSuccess(mockHtmlData.homepage);
const result = (await homepageController()) as unknown as Record<string, unknown>;
expect(result.spotlight).toBeDefined();
});
it('detailpageController should return anime details', async () => {
mockSuccess(mockHtmlData.detail);
const result = await detailpageController(createMockContext({ id: '123' }));
expect(result.title).toBe('Detail Anime');
});
it('searchController should return search results', async () => {
mockSuccess(mockHtmlData.search);
const result = await searchController(createMockContext({}, { keyword: 'one' }));
expect(result.response).toHaveLength(1);
});
it('episodesController should return episodes', async () => {
mockSuccess(mockHtmlData.episodes);
const result = await episodesController(createMockContext({ id: '123' }));
expect(result).toHaveLength(1);
});
it('charactersController should return characters', async () => {
mockSuccess(mockHtmlData.characters);
const result = await charactersController(createMockContext({ id: '123' }));
expect(result.response).toBeDefined();
});
it('characterDetailController should return character details', async () => {
mockSuccess(mockHtmlData.characterDetail);
const result = await characterDetailController(createMockContext({ id: '123' }));
expect(result.name).toBe('Character Full Name');
});
it('listpageController should return anime list', async () => {
mockSuccess(mockHtmlData.search);
const result = await listpageController(createMockContext({ query: 'most-popular' }));
expect(result.response).toBeDefined();
});
it('topSearchController should return top search items', async () => {
mockSuccess(mockHtmlData.topSearch);
const result = await topSearchController(createMockContext());
expect(result).toHaveLength(3);
});
it('schedulesController should return schedules', async () => {
mockSuccess(JSON.stringify({ html: mockHtmlData.schedule }));
const result = await schedulesController(createMockContext());
expect(result.data).toBeDefined();
});
it('newsController should return news items', async () => {
mockSuccess(mockHtmlData.news);
const result = await newsController(createMockContext());
expect(result.news).toHaveLength(1);
});
it('suggestionController should return suggestions', async () => {
mockSuccess(JSON.stringify({ html: mockHtmlData.suggestions }));
const result = await suggestionController(createMockContext({}, { keyword: 'suggest' }));
expect(result).toHaveLength(1);
});
it('nextEpisodeScheduleController should return next episode time', async () => {
mockSuccess(mockHtmlData.scheduleNext);
const result = await nextEpisodeScheduleController(createMockContext({ id: '123' }));
expect(result).toBe('10:00');
});
it('filterController should handle complex queries', async () => {
mockSuccess(mockHtmlData.search);
const result = await filterController(createMockContext({}, { keyword: 'one' }));
expect(result.response).toBeDefined();
});
it('allGenresController should return all genres', async () => {
mockSuccess(mockHtmlData.homepage);
const result = await allGenresController();
expect(result).toBeDefined();
});
it('randomController should return random anime', async () => {
mockSuccess(mockHtmlData.search);
const result = await randomController(createMockContext());
expect(result).toBeDefined();
});
});

View File

@@ -0,0 +1,101 @@
import { describe, it, expect } from '@jest/globals';
import { extractHomepage } from '../../../../src/extractor/extractHomepage';
import { extractDetailpage } from '../../../../src/extractor/extractDetailpage';
import { extractListPage } from '../../../../src/extractor/extractListpage';
import { extractCharacters } from '../../../../src/extractor/extractCharacters';
import { extractNews } from '../../../../src/extractor/extractNews';
import { extractSchedule } from '../../../../src/extractor/extractSchedule';
import { extractEpisodes } from '../../../../src/extractor/extractEpisodes';
import { extractCharacterDetail } from '../../../../src/extractor/extractCharacterDetail';
import { extractSuggestions } from '../../../../src/extractor/extractSuggestions';
import { extractTopSearch } from '../../../../src/extractor/extractTopSearch';
import { extractNextEpisodeSchedule } from '../../../../src/extractor/extractNextEpisodeSchedule';
import { mockHtmlData } from '../../data/mocks';
describe('Extractors Comprehensive Suite (Jest)', () => {
describe('extractHomepage', () => {
it('should extract spotlight items', () => {
const result = extractHomepage(mockHtmlData.homepage);
expect(result.spotlight).toHaveLength(1);
expect(result.spotlight[0].title).toBe('Spotlight Anime');
});
});
describe('extractDetailpage', () => {
it('should extract detail info', () => {
const result = extractDetailpage(mockHtmlData.detail);
expect(result.title).toBe('Detail Anime');
expect(result.is18Plus).toBe(true);
});
});
describe('extractListPage', () => {
it('should extract results from list page', () => {
const result = extractListPage(mockHtmlData.search);
expect(result.response).toHaveLength(1);
expect(result.response[0].title).toBe('Search Result');
});
});
describe('extractCharacters', () => {
it('should extract characters', () => {
const result = extractCharacters(mockHtmlData.characters);
expect(result.response).toHaveLength(1);
expect(result.response[0].name).toBe('Character Name');
});
});
describe('extractNews', () => {
it('should extract news items', () => {
const result = extractNews(mockHtmlData.news);
expect(result.news).toHaveLength(1);
expect(result.news[0].title).toBe('News Title');
});
});
describe('extractSchedule', () => {
it('should extract schedule', () => {
const result = extractSchedule(mockHtmlData.schedule);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('Scheduled Anime');
});
});
describe('extractEpisodes', () => {
it('should extract episodes', () => {
const result = extractEpisodes(mockHtmlData.episodes);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('Episode 1');
});
});
describe('extractCharacterDetail', () => {
it('should extract character detail', () => {
const result = extractCharacterDetail(mockHtmlData.characterDetail);
expect(result.name).toBe('Character Full Name');
});
});
describe('extractSuggestions', () => {
it('should extract suggestions', () => {
const result = extractSuggestions(mockHtmlData.suggestions);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('S1');
});
});
describe('extractTopSearch', () => {
it('should extract top search', () => {
const result = extractTopSearch(mockHtmlData.topSearch);
expect(result).toHaveLength(3);
expect(result[2].title).toBe('Top Title');
});
});
describe('extractNextEpisodeSchedule', () => {
it('should extract next episode schedule', () => {
const result = extractNextEpisodeSchedule(mockHtmlData.scheduleNext);
expect(result).toBe('10:00');
});
});
});

View File

@@ -0,0 +1,137 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { Context } from 'hono';
import homepageController from '../../../../src/controllers/homepage.controller';
import detailpageController from '../../../../src/controllers/detailpage.controller';
import searchController from '../../../../src/controllers/search.controller';
import episodesController from '../../../../src/controllers/episodes.controller';
import charactersController from '../../../../src/controllers/characters.controller';
import characterDetailController from '../../../../src/controllers/characterDetail.controller';
import listpageController from '../../../../src/controllers/listpage.controller';
import topSearchController from '../../../../src/controllers/topSearch.controller';
import schedulesController from '../../../../src/controllers/schedules.controller';
import newsController from '../../../../src/controllers/news.controller';
import suggestionController from '../../../../src/controllers/suggestion.controller';
import nextEpisodeScheduleController from '../../../../src/controllers/nextEpisodeSchedule.controller';
import randomController from '../../../../src/controllers/random.controller';
import filterController from '../../../../src/controllers/filter.controller';
import allGenresController from '../../../../src/controllers/allGenres.controller';
import { mockHtmlData } from '../../data/mocks';
// Mock axiosInstance globally
vi.mock('../../../../src/services/axiosInstance', () => ({
axiosInstance: vi.fn(),
}));
import { axiosInstance } from '../../../../src/services/axiosInstance';
const createMockContext = (
params: Record<string, string> = {},
query: Record<string, string> = {}
) => {
return {
req: {
param: (name?: string) => (name ? params[name] : params),
query: (name?: string) => (name ? query[name] : query),
},
json: vi.fn(data => data),
} as unknown as Context;
};
describe('Controllers Comprehensive Suite', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockSuccess = (data: string) =>
(axiosInstance as Mock).mockResolvedValue({ success: true, data });
it('homepageController should return homepage data', async () => {
mockSuccess(mockHtmlData.homepage);
const result = (await homepageController()) as unknown as Record<string, unknown>;
expect(result.spotlight).toBeDefined();
});
it('detailpageController should return anime details', async () => {
mockSuccess(mockHtmlData.detail);
const result = await detailpageController(createMockContext({ id: '123' }));
expect(result.title).toBe('Detail Anime');
});
it('searchController should return search results', async () => {
mockSuccess(mockHtmlData.search);
const result = await searchController(createMockContext({}, { keyword: 'one' }));
expect(result.response).toHaveLength(1);
});
it('episodesController should return episodes', async () => {
mockSuccess(mockHtmlData.episodes);
const result = await episodesController(createMockContext({ id: '123' }));
expect(result).toHaveLength(1);
});
it('charactersController should return characters', async () => {
mockSuccess(mockHtmlData.characters);
const result = await charactersController(createMockContext({ id: '123' }));
expect(result.response).toBeDefined();
});
it('characterDetailController should return character details', async () => {
mockSuccess(mockHtmlData.characterDetail);
const result = await characterDetailController(createMockContext({ id: '123' }));
expect(result.name).toBe('Character Full Name');
});
it('listpageController should return anime list', async () => {
mockSuccess(mockHtmlData.search);
const result = await listpageController(createMockContext({ query: 'most-popular' }));
expect(result.response).toBeDefined();
});
it('topSearchController should return top search items', async () => {
mockSuccess(mockHtmlData.topSearch);
const result = await topSearchController(createMockContext());
expect(result).toHaveLength(3);
});
it('schedulesController should return schedules', async () => {
mockSuccess(JSON.stringify({ html: mockHtmlData.schedule }));
const result = await schedulesController(createMockContext());
expect(result.data).toBeDefined();
});
it('newsController should return news items', async () => {
mockSuccess(mockHtmlData.news);
const result = await newsController(createMockContext());
expect(result.news).toHaveLength(1);
});
it('suggestionController should return suggestions', async () => {
mockSuccess(JSON.stringify({ html: mockHtmlData.suggestions }));
const result = await suggestionController(createMockContext({}, { keyword: 'suggest' }));
expect(result).toHaveLength(1);
});
it('nextEpisodeScheduleController should return next episode time', async () => {
mockSuccess(mockHtmlData.scheduleNext);
const result = await nextEpisodeScheduleController(createMockContext({ id: '123' }));
expect(result).toBe('10:00');
});
it('filterController should handle complex queries', async () => {
mockSuccess(mockHtmlData.search);
const result = await filterController(createMockContext({}, { keyword: 'one' }));
expect(result.response).toBeDefined();
});
it('allGenresController should return all genres', async () => {
mockSuccess(mockHtmlData.homepage);
const result = await allGenresController();
expect(result).toBeDefined();
});
it('randomController should return random anime', async () => {
mockSuccess(mockHtmlData.search);
const result = await randomController(createMockContext());
expect(result).toBeDefined();
});
});

View File

@@ -0,0 +1,101 @@
import { describe, it, expect } from 'vitest';
import { extractHomepage } from '../../../../src/extractor/extractHomepage';
import { extractDetailpage } from '../../../../src/extractor/extractDetailpage';
import { extractListPage } from '../../../../src/extractor/extractListpage';
import { extractCharacters } from '../../../../src/extractor/extractCharacters';
import { extractNews } from '../../../../src/extractor/extractNews';
import { extractSchedule } from '../../../../src/extractor/extractSchedule';
import { extractEpisodes } from '../../../../src/extractor/extractEpisodes';
import { extractCharacterDetail } from '../../../../src/extractor/extractCharacterDetail';
import { extractSuggestions } from '../../../../src/extractor/extractSuggestions';
import { extractTopSearch } from '../../../../src/extractor/extractTopSearch';
import { extractNextEpisodeSchedule } from '../../../../src/extractor/extractNextEpisodeSchedule';
import { mockHtmlData } from '../../data/mocks';
describe('Extractors Comprehensive Suite', () => {
describe('extractHomepage', () => {
it('should extract spotlight items', () => {
const result = extractHomepage(mockHtmlData.homepage);
expect(result.spotlight).toHaveLength(1);
expect(result.spotlight[0].title).toBe('Spotlight Anime');
});
});
describe('extractDetailpage', () => {
it('should extract detail info', () => {
const result = extractDetailpage(mockHtmlData.detail);
expect(result.title).toBe('Detail Anime');
expect(result.is18Plus).toBe(true);
});
});
describe('extractListPage', () => {
it('should extract results from list page', () => {
const result = extractListPage(mockHtmlData.search);
expect(result.response).toHaveLength(1);
expect(result.response[0].title).toBe('Search Result');
});
});
describe('extractCharacters', () => {
it('should extract characters', () => {
const result = extractCharacters(mockHtmlData.characters);
expect(result.response).toHaveLength(1);
expect(result.response[0].name).toBe('Character Name');
});
});
describe('extractNews', () => {
it('should extract news items', () => {
const result = extractNews(mockHtmlData.news);
expect(result.news).toHaveLength(1);
expect(result.news[0].title).toBe('News Title');
});
});
describe('extractSchedule', () => {
it('should extract schedule', () => {
const result = extractSchedule(mockHtmlData.schedule);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('Scheduled Anime');
});
});
describe('extractEpisodes', () => {
it('should extract episodes', () => {
const result = extractEpisodes(mockHtmlData.episodes);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('Episode 1');
});
});
describe('extractCharacterDetail', () => {
it('should extract character detail', () => {
const result = extractCharacterDetail(mockHtmlData.characterDetail);
expect(result.name).toBe('Character Full Name');
});
});
describe('extractSuggestions', () => {
it('should extract suggestions', () => {
const result = extractSuggestions(mockHtmlData.suggestions);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('S1');
});
});
describe('extractTopSearch', () => {
it('should extract top search', () => {
const result = extractTopSearch(mockHtmlData.topSearch);
expect(result).toHaveLength(3);
expect(result[2].title).toBe('Top Title');
});
});
describe('extractNextEpisodeSchedule', () => {
it('should extract next episode schedule', () => {
const result = extractNextEpisodeSchedule(mockHtmlData.scheduleNext);
expect(result).toBe('10:00');
});
});
});