fix(tests): fix all cypress tests

This commit is contained in:
TOomaAh
2025-04-16 03:10:37 +02:00
committed by TOomaAh
parent 1618eb954c
commit 227533a691
5 changed files with 216 additions and 125 deletions

View File

@@ -1,92 +0,0 @@
describe('TVDB Integration', () => {
// Constants for routes and selectors
const ROUTES = {
home: '/',
tvdbSettings: '/settings/tvdb',
tomorrowIsOursTvShow: '/tv/72879',
monsterTvShow: '/tv/225634',
};
const SELECTORS = {
sidebarToggle: '[data-testid=sidebar-toggle]',
sidebarSettingsMobile: '[data-testid=sidebar-menu-settings-mobile]',
settingsNavDesktop: 'nav[data-testid="settings-nav-desktop"]',
tvdbEnable: 'input[data-testid="tvdb-enable"]',
tvdbSaveButton: '[data-testid=tvbd-save-button]',
heading: '.heading',
season1: 'Season 1',
season2: 'Season 2',
};
// Reusable commands
const toggleTVDBSetting = () => {
cy.intercept('/api/v1/settings/tvdb').as('tvdbRequest');
cy.get(SELECTORS.tvdbSaveButton).click();
return cy.wait('@tvdbRequest');
};
const verifyTVDBResponse = (response, expectedUseValue) => {
expect(response.statusCode).to.equal(200);
expect(response.body.tvdb).to.equal(expectedUseValue);
};
beforeEach(() => {
// Perform login
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
// Navigate to TVDB settings
cy.visit(ROUTES.home);
cy.get(SELECTORS.sidebarToggle).click();
cy.get(SELECTORS.sidebarSettingsMobile).click();
cy.get(
`${SELECTORS.settingsNavDesktop} a[href="${ROUTES.tvdbSettings}"]`
).click();
// Verify heading
cy.get(SELECTORS.heading).should('contain', 'Tvdb');
// Configure TVDB settings
cy.get(SELECTORS.tvdbEnable).then(($checkbox) => {
const isChecked = $checkbox.is(':checked');
if (!isChecked) {
// If disabled, enable TVDB
cy.wrap($checkbox).click();
toggleTVDBSetting().then(({ response }) => {
verifyTVDBResponse(response, true);
});
} else {
// If enabled, disable then re-enable TVDB
cy.wrap($checkbox).click();
toggleTVDBSetting().then(({ response }) => {
verifyTVDBResponse(response, false);
});
cy.wrap($checkbox).click();
toggleTVDBSetting().then(({ response }) => {
verifyTVDBResponse(response, true);
});
}
});
});
it('should display "Tomorrow is Ours" show information correctly (1 season on TMDB >1 seasons on TVDB)', () => {
cy.visit(ROUTES.tomorrowIsOursTvShow);
cy.contains(SELECTORS.season2)
.should('be.visible')
.scrollIntoView()
.click();
});
it('Should display "Monster" show information correctly (Not existing on TVDB)', () => {
cy.visit(ROUTES.monsterTvShow);
cy.intercept('/api/v1/tv/225634/season/1').as('season1');
cy.contains(SELECTORS.season1)
.should('be.visible')
.scrollIntoView()
.click();
cy.wait('@season1');
cy.contains('9 - Hang Men').should('be.visible');
});
});

View File

@@ -0,0 +1,127 @@
describe('TVDB Integration', () => {
// Constants for routes and selectors
const ROUTES = {
home: '/',
metadataSettings: '/settings/metadata',
tomorrowIsOursTvShow: '/tv/72879',
monsterTvShow: '/tv/225634',
};
const SELECTORS = {
sidebarToggle: '[data-testid=sidebar-toggle]',
sidebarSettingsMobile: '[data-testid=sidebar-menu-settings-mobile]',
settingsNavDesktop: 'nav[data-testid="settings-nav-desktop"]',
metadataTestButton: 'button[type="button"]:contains("Test")',
metadataSaveButton: '[data-testid="metadata-save-button"]',
tmdbStatus: '[data-testid="tmdb-status"]',
tvdbStatus: '[data-testid="tvdb-status"]',
tvIndexerSelector: '[data-testid="tv-indexer-selector"]',
animeIndexerSelector: '[data-testid="anime-indexer-selector"]',
seasonSelector: '[data-testid="season-selector"]',
season1: 'Season 1',
season2: 'Season 2',
episodeList: '[data-testid="episode-list"]',
episode9: '9 - Hang Men',
};
// Reusable commands
const navigateToMetadataSettings = () => {
cy.visit(ROUTES.home);
cy.get(SELECTORS.sidebarToggle).click();
cy.get(SELECTORS.sidebarSettingsMobile).click();
cy.get(
`${SELECTORS.settingsNavDesktop} a[href="${ROUTES.metadataSettings}"]`
).click();
};
const testAndVerifyMetadataConnection = () => {
cy.intercept('POST', '/api/v1/settings/metadatas/test').as(
'testConnection'
);
cy.get(SELECTORS.metadataTestButton).click();
return cy.wait('@testConnection');
};
const saveMetadataSettings = (customBody = null) => {
// Si un corps personnalisé est fourni, utilisez-le pour modifier la requête
if (customBody) {
cy.intercept('PUT', '/api/v1/settings/metadatas', (req) => {
req.body = customBody;
}).as('saveMetadata');
} else {
// Sinon, juste intercepter sans modifier
cy.intercept('PUT', '/api/v1/settings/metadatas').as('saveMetadata');
}
cy.get(SELECTORS.metadataSaveButton).click();
return cy.wait('@saveMetadata');
};
beforeEach(() => {
// Perform login
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
// Navigate to Metadata settings
navigateToMetadataSettings();
// Verify we're on the correct settings page
cy.contains('h3', 'Metadata').should('be.visible');
// Configure TVDB as TV provider and test connection
// Supposons que vous avez ajouté data-testid au div parent du Select
cy.get('[data-testid="tv-indexer-selector"]').click();
// Test the connection
testAndVerifyMetadataConnection().then(({ response }) => {
expect(response.statusCode).to.equal(200);
// Check TVDB connection status
cy.get(SELECTORS.tvdbStatus).should('contain', 'Operational');
});
// Save settings
saveMetadataSettings({
anime: 'tvdb',
tv: 'tvdb',
}).then(({ response }) => {
expect(response.statusCode).to.equal(200);
expect(response.body.tv).to.equal(false);
});
});
it('should display "Tomorrow is Ours" show information with multiple seasons from TVDB', () => {
// Navigate to the TV show
cy.visit(ROUTES.tomorrowIsOursTvShow);
// Verify that multiple seasons are displayed (TMDB has only 1 season, TVDB has multiple)
//cy.get(SELECTORS.seasonSelector).should('exist');
cy.intercept('/api/v1/tv/225634/season/1').as('season1');
// Select Season 2 and verify it loads
cy.contains(SELECTORS.season2)
.should('be.visible')
.scrollIntoView()
.click();
// Verify that episodes are displayed for Season 2
cy.contains('260 - Episode 506').should('be.visible');
});
it('Should display "Monster" show information correctly when not existing on TVDB', () => {
// Navigate to the TV show
cy.visit(ROUTES.monsterTvShow);
// Intercept season 1 request
cy.intercept('/api/v1/tv/225634/season/1').as('season1');
// Select Season 1
cy.contains(SELECTORS.season1)
.should('be.visible')
.scrollIntoView()
.click();
// Wait for the season data to load
cy.wait('@season1');
// Verify specific episode exists
cy.contains(SELECTORS.episode9).should('be.visible');
});
});

View File

@@ -20,12 +20,14 @@ import logger from '@server/logger';
interface TvdbConfig {
baseUrl: string;
maxRequestsPerSecond: number;
maxRequests: number;
cachePrefix: AvailableCacheIds;
}
const DEFAULT_CONFIG: TvdbConfig = {
baseUrl: 'https://api4.thetvdb.com/v4',
maxRequestsPerSecond: 50,
maxRequests: 20,
cachePrefix: 'tvdb' as const,
};
@@ -52,8 +54,8 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
{
nodeCache: cacheManager.getCache(finalConfig.cachePrefix).data,
rateLimit: {
maxRequests: finalConfig.maxRequests,
maxRPS: finalConfig.maxRequestsPerSecond,
id: finalConfig.cachePrefix,
},
}
);
@@ -112,14 +114,25 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
}
async login(): Promise<TvdbLoginResponse> {
let body: { apiKey: string; pin?: string } = {
apiKey:
process.env.TVDB_API_KEY || 'e4428e99-1c35-4500-9534-e13c1193b428',
};
if (this.pin) {
body = {
...body,
pin: this.pin,
};
}
const response = await this.post<TvdbBaseResponse<TvdbLoginResponse>>(
'/login',
{
apiKey: process.env.TVDB_API_KEY,
...body,
}
);
this.defaultHeaders.Authorization = `Bearer ${response.data.token}`;
this.token = response.data.token;
return response.data;
@@ -250,9 +263,11 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
private async fetchTvdbShowData(tvdbId: number): Promise<TvdbTvDetails> {
const resp = await this.get<TvdbBaseResponse<TvdbTvDetails>>(
`/series/${tvdbId}/extended?meta=episodes`,
`/series/${tvdbId}/extended?meta=episodes&short=true`,
{
short: 'true',
headers: {
Authorization: `Bearer ${this.token}`,
},
},
Tvdb.DEFAULT_CACHE_TTL
);
@@ -265,12 +280,15 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
return [];
}
return tvdbData.seasons
const seasons = tvdbData.seasons
.filter(
(season) =>
season.number > 0 && season.type && season.type.type === 'official'
)
.sort((a, b) => a.number - b.number)
.map((season) => this.createSeasonData(season, tvdbData));
return seasons;
}
private createSeasonData(
@@ -307,18 +325,38 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
private async getTvdbSeasonData(
tvdbId: number,
seasonNumber: number,
tvId: number,
language: string = Tvdb.DEFAULT_LANGUAGE
tvId: number
//language: string = Tvdb.DEFAULT_LANGUAGE
): Promise<TmdbSeasonWithEpisodes> {
const tvdbData = await this.fetchTvdbShowData(tvdbId);
if (!tvdbData) {
logger.error(`Failed to fetch TVDB data for ID: ${tvdbId}`);
return this.createEmptySeasonResponse(tvId);
}
// get season id
const season = tvdbData.seasons.find(
(season) =>
season.number === seasonNumber &&
season.type.type &&
season.type.type === 'official'
);
if (!season) {
logger.error(
`Failed to find season ${seasonNumber} for TVDB ID: ${tvdbId}`
);
return this.createEmptySeasonResponse(tvId);
}
const resp = await this.get<TvdbBaseResponse<TvdbSeasonDetails>>(
`/series/${tvdbId}/episodes/official/${language}`,
{}
`/seasons/${season.id}/extended`,
{
headers: {
Authorization: `Bearer ${this.token}`,
},
}
);
const seasons = resp.data;
@@ -342,6 +380,7 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
tvId: number
): TmdbTvEpisodeResult[] {
if (!tvdbSeason || !tvdbSeason.episodes) {
logger.error('No episodes found in TVDB season data');
return [];
}
@@ -355,6 +394,10 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
index: number,
tvId: number
): TmdbTvEpisodeResult {
logger.info(
`Creating episode data for episode: ${episode.name} with index: ${index}`
);
return {
id: episode.id,
air_date: episode.aired,
@@ -364,9 +407,7 @@ class Tvdb extends ExternalAPI implements TvShowIndexer {
season_number: episode.seasonNumber,
production_code: '',
show_id: tvId,
still_path: episode.image
? 'https://artworks.thetvdb.com' + episode.image
: '',
still_path: episode.image ? episode.image : '',
vote_average: 1,
vote_count: 1,
};

View File

@@ -11,6 +11,7 @@ enum IndexerType {
}
type IndexerOptionType = {
testId?: string;
value: IndexerType;
label: string;
icon: React.ReactNode;
@@ -23,12 +24,14 @@ const messages = defineMessages('components.MetadataSelector', {
});
interface MetadataSelectorProps {
testId: string;
value: IndexerType;
onChange: (value: IndexerType) => void;
isDisabled?: boolean;
}
const MetadataSelector = ({
testId = 'indexer-selector',
value,
onChange,
isDisabled = false,
@@ -37,11 +40,13 @@ const MetadataSelector = ({
const indexerOptions: IndexerOptionType[] = [
{
testId: 'tmdb-option',
value: IndexerType.TMDB,
label: intl.formatMessage(messages.tmdbLabel),
icon: <TmdbLogo />,
},
{
testId: 'tvdb-option',
value: IndexerType.TVDB,
label: intl.formatMessage(messages.tvdbLabel),
icon: <TvdbLogo />,
@@ -64,26 +69,28 @@ const MetadataSelector = ({
const formatOptionLabel = (option: IndexerOptionType) => (
<div className="flex items-center">
{option.icon}
<span>{option.label}</span>
<span data-testid={option.testId}>{option.label}</span>
</div>
);
return (
<Select
options={indexerOptions}
isDisabled={isDisabled}
className="react-select-container"
classNamePrefix="react-select"
value={indexerOptions.find((option) => option.value === value)}
onChange={(selectedOption) => {
if (selectedOption) {
onChange(selectedOption.value);
}
}}
placeholder={intl.formatMessage(messages.selectIndexer)}
styles={customStyles}
formatOptionLabel={formatOptionLabel}
/>
<div data-testid={testId}>
<Select
options={indexerOptions}
isDisabled={isDisabled}
className="react-select-container"
classNamePrefix="react-select"
value={indexerOptions.find((option) => option.value === value)}
onChange={(selectedOption) => {
if (selectedOption) {
onChange(selectedOption.value);
}
}}
placeholder={intl.formatMessage(messages.selectIndexer)}
styles={customStyles}
formatOptionLabel={formatOptionLabel}
/>
</div>
);
};

View File

@@ -197,16 +197,22 @@ const SettingsMetadata = () => {
</h4>
<div className="flex flex-col space-y-3">
<div className="flex items-center">
<span className="mr-2 w-12">TheMovieDB:</span>
<span className={`text-sm ${getStatusClass(providerStatus.tmdb)}`}>
<span className="mr-2 w-24">TheMovieDB:</span>
<span
className={`text-sm ${getStatusClass(providerStatus.tmdb)}`}
data-testid="tmdb-status-container"
>
<Badge badgeType={getBadgeType(providerStatus.tmdb)}>
{getStatusMessage(providerStatus.tmdb)}
</Badge>
</span>
</div>
<div className="flex items-center">
<span className="mr-2 w-12">TheTVDB:</span>
<span className={`text-sm ${getStatusClass(providerStatus.tvdb)}`}>
<span className="mr-2 w-24">TheTVDB:</span>
<span
className={`text-sm ${getStatusClass(providerStatus.tvdb)}`}
data-testid="tvdb-status"
>
<Badge badgeType={getBadgeType(providerStatus.tvdb)}>
{getStatusMessage(providerStatus.tvdb)}
</Badge>
@@ -254,6 +260,7 @@ const SettingsMetadata = () => {
</label>
<div className="form-input-area">
<MetadataSelector
testId="tv-indexer-selector"
value={values.metadata.tv}
onChange={(value) => setFieldValue('metadata.tv', value)}
isDisabled={isSubmitting}
@@ -269,6 +276,7 @@ const SettingsMetadata = () => {
</label>
<div className="form-input-area">
<MetadataSelector
testId="anime-indexer-selector"
value={values.metadata.anime}
onChange={(value) =>
setFieldValue('metadata.anime', value)