mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
test: split oversized specs to meet the file-size guideline
Address review feedback: extract shared drizzle mocks into operations.test-helpers.ts and split the Electron data-source delegation spec into delegation + user-data files. Pure reorganization - test counts and assertions unchanged (29/13/10), all files now under 300 lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d2b95442ed
commit
f8fb03ef1e
5 files changed
+446
-462
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import type { AppDatabase } from '../database.types';
|
||||
|
||||
/**
|
||||
* Shared drizzle-orm query-builder mocks for the operations specs.
|
||||
* Jest keeps a separate module registry per test file, so every spec gets
|
||||
* its own mock instances. `jest.mock()` calls cannot live here (Jest hoists
|
||||
* them per file), so each spec registers the module mock itself:
|
||||
*
|
||||
* jest.mock('drizzle-orm', () => mockDrizzleOrmModule());
|
||||
*
|
||||
* The `mock` prefix on the exports is required so the hoisted factory is
|
||||
* allowed to reference them.
|
||||
*/
|
||||
export const mockDrizzle = {
|
||||
and: jest.fn((...conditions: unknown[]) => ({
|
||||
kind: 'and',
|
||||
conditions,
|
||||
})),
|
||||
desc: jest.fn((value: unknown) => ({ kind: 'desc', value })),
|
||||
eq: jest.fn((left: unknown, right: unknown) => ({
|
||||
kind: 'eq',
|
||||
left,
|
||||
right,
|
||||
})),
|
||||
inArray: jest.fn((left: unknown, values: unknown[]) => ({
|
||||
kind: 'inArray',
|
||||
left,
|
||||
values,
|
||||
})),
|
||||
sql: Object.assign(
|
||||
jest.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({
|
||||
kind: 'sql',
|
||||
strings: Array.from(strings),
|
||||
values,
|
||||
})),
|
||||
{
|
||||
placeholder: jest.fn((name: string) => ({
|
||||
kind: 'placeholder',
|
||||
name,
|
||||
})),
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export function mockDrizzleOrmModule() {
|
||||
return {
|
||||
and: (...conditions: unknown[]) => mockDrizzle.and(...conditions),
|
||||
desc: (value: unknown) => mockDrizzle.desc(value),
|
||||
eq: (left: unknown, right: unknown) => mockDrizzle.eq(left, right),
|
||||
inArray: (left: unknown, values: unknown[]) =>
|
||||
mockDrizzle.inArray(left, values),
|
||||
sql: Object.assign(
|
||||
(strings: TemplateStringsArray, ...values: unknown[]) =>
|
||||
mockDrizzle.sql(strings, ...values),
|
||||
{
|
||||
placeholder: (name: string) =>
|
||||
mockDrizzle.sql.placeholder(name),
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function resetDrizzleMocks() {
|
||||
mockDrizzle.and.mockClear();
|
||||
mockDrizzle.desc.mockClear();
|
||||
mockDrizzle.eq.mockClear();
|
||||
mockDrizzle.inArray.mockClear();
|
||||
mockDrizzle.sql.mockClear();
|
||||
mockDrizzle.sql.placeholder.mockClear();
|
||||
}
|
||||
|
||||
export type QueryMock = {
|
||||
from: jest.Mock;
|
||||
innerJoin: jest.Mock;
|
||||
where: jest.Mock;
|
||||
orderBy: jest.Mock;
|
||||
limit: jest.Mock;
|
||||
then: (
|
||||
resolve: (value: unknown[]) => void,
|
||||
reject: (reason: unknown) => void
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fluent AppDatabase mock. Each `db.select()` call consumes the next entry
|
||||
* of `selectResultsByCall` as its resolved rows; every builder method
|
||||
* returns the same chainable/thenable query object.
|
||||
*/
|
||||
export function createDbMock(selectResultsByCall: unknown[][] = []) {
|
||||
let selectIndex = 0;
|
||||
const queries: QueryMock[] = [];
|
||||
const select = jest.fn(() => {
|
||||
const rows = selectResultsByCall[selectIndex] ?? [];
|
||||
selectIndex += 1;
|
||||
const query: QueryMock = {
|
||||
from: jest.fn(),
|
||||
innerJoin: jest.fn(),
|
||||
where: jest.fn(),
|
||||
orderBy: jest.fn(),
|
||||
limit: jest.fn().mockResolvedValue(rows),
|
||||
then: (resolve, reject) =>
|
||||
Promise.resolve(rows).then(resolve, reject),
|
||||
};
|
||||
query.from.mockReturnValue(query);
|
||||
query.innerJoin.mockReturnValue(query);
|
||||
query.where.mockReturnValue(query);
|
||||
query.orderBy.mockReturnValue(query);
|
||||
queries.push(query);
|
||||
return query;
|
||||
});
|
||||
|
||||
const insertValues = jest.fn().mockResolvedValue(undefined);
|
||||
const insert = jest.fn().mockReturnValue({ values: insertValues });
|
||||
|
||||
const updateWhere = jest.fn().mockResolvedValue(undefined);
|
||||
const updateSet = jest.fn().mockReturnValue({ where: updateWhere });
|
||||
const update = jest.fn().mockReturnValue({ set: updateSet });
|
||||
|
||||
const deleteExecute = jest.fn().mockResolvedValue(undefined);
|
||||
const deletePrepare = jest
|
||||
.fn()
|
||||
.mockReturnValue({ execute: deleteExecute });
|
||||
const deleteResult = {
|
||||
prepare: deletePrepare,
|
||||
then: (
|
||||
resolve: (value: unknown) => void,
|
||||
reject: (reason: unknown) => void
|
||||
) => Promise.resolve(undefined).then(resolve, reject),
|
||||
};
|
||||
const deleteWhere = jest.fn().mockReturnValue(deleteResult);
|
||||
const deleteFn = jest.fn().mockReturnValue({
|
||||
where: deleteWhere,
|
||||
then: (
|
||||
resolve: (value: unknown) => void,
|
||||
reject: (reason: unknown) => void
|
||||
) => Promise.resolve(undefined).then(resolve, reject),
|
||||
});
|
||||
|
||||
const transaction = jest.fn((callback: () => unknown) => {
|
||||
const result = callback();
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
|
||||
return {
|
||||
db: {
|
||||
select,
|
||||
insert,
|
||||
update,
|
||||
delete: deleteFn,
|
||||
transaction,
|
||||
} as unknown as AppDatabase,
|
||||
deleteExecute,
|
||||
deleteFn,
|
||||
deletePrepare,
|
||||
deleteWhere,
|
||||
insert,
|
||||
insertValues,
|
||||
queries,
|
||||
select,
|
||||
transaction,
|
||||
update,
|
||||
updateSet,
|
||||
updateWhere,
|
||||
};
|
||||
}
|
||||
+20
-100
@@ -1,29 +1,13 @@
|
||||
const andMock = jest.fn((...conditions: unknown[]) => ({
|
||||
kind: 'and',
|
||||
conditions,
|
||||
}));
|
||||
const eqMock = jest.fn((left: unknown, right: unknown) => ({
|
||||
kind: 'eq',
|
||||
left,
|
||||
right,
|
||||
}));
|
||||
const descMock = jest.fn((value: unknown) => ({ kind: 'desc', value }));
|
||||
const sqlMock = jest.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({
|
||||
kind: 'sql',
|
||||
strings: Array.from(strings),
|
||||
values,
|
||||
}));
|
||||
import {
|
||||
createDbMock,
|
||||
mockDrizzle,
|
||||
mockDrizzleOrmModule,
|
||||
resetDrizzleMocks,
|
||||
} from './operations.test-helpers';
|
||||
|
||||
jest.mock('drizzle-orm', () => ({
|
||||
and: (...conditions: unknown[]) => andMock(...conditions),
|
||||
desc: (value: unknown) => descMock(value),
|
||||
eq: (left: unknown, right: unknown) => eqMock(left, right),
|
||||
sql: (strings: TemplateStringsArray, ...values: unknown[]) =>
|
||||
sqlMock(strings, ...values),
|
||||
}));
|
||||
jest.mock('drizzle-orm', () => mockDrizzleOrmModule());
|
||||
|
||||
import * as schema from '@iptvnator/shared/database/schema';
|
||||
import type { AppDatabase } from '../database.types';
|
||||
import {
|
||||
clearAllPlaybackPositions,
|
||||
clearPlaybackPosition,
|
||||
@@ -34,73 +18,9 @@ import {
|
||||
savePlaybackPosition,
|
||||
} from './playback-position.operations';
|
||||
|
||||
type QueryMock = {
|
||||
from: jest.Mock;
|
||||
where: jest.Mock;
|
||||
orderBy: jest.Mock;
|
||||
limit: jest.Mock;
|
||||
then: (
|
||||
resolve: (value: unknown[]) => void,
|
||||
reject: (reason: unknown) => void
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
function createDbMock(selectResultsByCall: unknown[][] = []) {
|
||||
let selectIndex = 0;
|
||||
const queries: QueryMock[] = [];
|
||||
const select = jest.fn(() => {
|
||||
const rows = selectResultsByCall[selectIndex] ?? [];
|
||||
selectIndex += 1;
|
||||
const query: QueryMock = {
|
||||
from: jest.fn(),
|
||||
where: jest.fn(),
|
||||
orderBy: jest.fn(),
|
||||
limit: jest.fn().mockResolvedValue(rows),
|
||||
then: (resolve, reject) =>
|
||||
Promise.resolve(rows).then(resolve, reject),
|
||||
};
|
||||
query.from.mockReturnValue(query);
|
||||
query.where.mockReturnValue(query);
|
||||
query.orderBy.mockReturnValue(query);
|
||||
queries.push(query);
|
||||
return query;
|
||||
});
|
||||
|
||||
const insertValues = jest.fn().mockResolvedValue(undefined);
|
||||
const insert = jest.fn().mockReturnValue({ values: insertValues });
|
||||
|
||||
const updateWhere = jest.fn().mockResolvedValue(undefined);
|
||||
const updateSet = jest.fn().mockReturnValue({ where: updateWhere });
|
||||
const update = jest.fn().mockReturnValue({ set: updateSet });
|
||||
|
||||
const deleteWhere = jest.fn().mockResolvedValue(undefined);
|
||||
const deleteFn = jest.fn().mockReturnValue({ where: deleteWhere });
|
||||
|
||||
return {
|
||||
db: {
|
||||
select,
|
||||
insert,
|
||||
update,
|
||||
delete: deleteFn,
|
||||
} as unknown as AppDatabase,
|
||||
deleteFn,
|
||||
deleteWhere,
|
||||
insert,
|
||||
insertValues,
|
||||
queries,
|
||||
select,
|
||||
update,
|
||||
updateSet,
|
||||
updateWhere,
|
||||
};
|
||||
}
|
||||
|
||||
describe('playback-position.operations', () => {
|
||||
beforeEach(() => {
|
||||
andMock.mockClear();
|
||||
descMock.mockClear();
|
||||
eqMock.mockClear();
|
||||
sqlMock.mockClear();
|
||||
resetDrizzleMocks();
|
||||
});
|
||||
|
||||
describe('savePlaybackPosition', () => {
|
||||
@@ -193,7 +113,7 @@ describe('playback-position.operations', () => {
|
||||
positionSeconds: 480,
|
||||
})
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.id,
|
||||
33
|
||||
);
|
||||
@@ -208,7 +128,7 @@ describe('playback-position.operations', () => {
|
||||
positionSeconds: 120,
|
||||
});
|
||||
|
||||
expect(sqlMock).toHaveBeenCalledWith(['CURRENT_TIMESTAMP']);
|
||||
expect(mockDrizzle.sql).toHaveBeenCalledWith(['CURRENT_TIMESTAMP']);
|
||||
expect(insertValues).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
@@ -236,15 +156,15 @@ describe('playback-position.operations', () => {
|
||||
);
|
||||
|
||||
expect(result).toEqual(row);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.contentXtreamId,
|
||||
500
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.contentType,
|
||||
'vod'
|
||||
);
|
||||
@@ -268,11 +188,11 @@ describe('playback-position.operations', () => {
|
||||
getSeriesPlaybackPositions(db, 'playlist-1', 42)
|
||||
).resolves.toEqual(rows);
|
||||
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.seriesXtreamId,
|
||||
42
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.contentType,
|
||||
'episode'
|
||||
);
|
||||
@@ -287,7 +207,7 @@ describe('playback-position.operations', () => {
|
||||
getRecentPlaybackPositions(db, 'playlist-1')
|
||||
).resolves.toEqual(rows);
|
||||
|
||||
expect(descMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.desc).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.updatedAt
|
||||
);
|
||||
expect(queries[0].limit).toHaveBeenCalledWith(20);
|
||||
@@ -309,7 +229,7 @@ describe('playback-position.operations', () => {
|
||||
getAllPlaybackPositions(db, 'playlist-1')
|
||||
).resolves.toEqual(rows);
|
||||
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
@@ -330,7 +250,7 @@ describe('playback-position.operations', () => {
|
||||
expect(deleteWhere).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ kind: 'eq' })
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
@@ -345,11 +265,11 @@ describe('playback-position.operations', () => {
|
||||
|
||||
expect(deleteFn).toHaveBeenCalledWith(schema.playbackPositions);
|
||||
expect(deleteWhere.mock.calls[0][0].conditions).toHaveLength(3);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.contentXtreamId,
|
||||
500
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.playbackPositions.contentType,
|
||||
'vod'
|
||||
);
|
||||
|
||||
+20
-143
@@ -1,46 +1,17 @@
|
||||
const andMock = jest.fn((...conditions: unknown[]) => ({
|
||||
kind: 'and',
|
||||
conditions,
|
||||
}));
|
||||
const eqMock = jest.fn((left: unknown, right: unknown) => ({
|
||||
kind: 'eq',
|
||||
left,
|
||||
right,
|
||||
}));
|
||||
const descMock = jest.fn((value: unknown) => ({ kind: 'desc', value }));
|
||||
const inArrayMock = jest.fn((left: unknown, values: unknown[]) => ({
|
||||
kind: 'inArray',
|
||||
left,
|
||||
values,
|
||||
}));
|
||||
const sqlMock = Object.assign(
|
||||
jest.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({
|
||||
kind: 'sql',
|
||||
strings: Array.from(strings),
|
||||
values,
|
||||
})),
|
||||
{
|
||||
placeholder: jest.fn((name: string) => ({
|
||||
kind: 'placeholder',
|
||||
name,
|
||||
})),
|
||||
}
|
||||
);
|
||||
import {
|
||||
createDbMock,
|
||||
mockDrizzle,
|
||||
mockDrizzleOrmModule,
|
||||
resetDrizzleMocks,
|
||||
} from './operations.test-helpers';
|
||||
|
||||
jest.mock('drizzle-orm', () => ({
|
||||
and: (...conditions: unknown[]) => andMock(...conditions),
|
||||
desc: (value: unknown) => descMock(value),
|
||||
eq: (left: unknown, right: unknown) => eqMock(left, right),
|
||||
inArray: (left: unknown, values: unknown[]) => inArrayMock(left, values),
|
||||
sql: sqlMock,
|
||||
}));
|
||||
jest.mock('drizzle-orm', () => mockDrizzleOrmModule());
|
||||
|
||||
jest.mock('./content-backdrop.operations', () => ({
|
||||
persistContentBackdropIfMissing: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
import * as schema from '@iptvnator/shared/database/schema';
|
||||
import type { AppDatabase } from '../database.types';
|
||||
import { persistContentBackdropIfMissing } from './content-backdrop.operations';
|
||||
import {
|
||||
addRecentItem,
|
||||
@@ -52,103 +23,9 @@ import {
|
||||
removeRecentItemsBatch,
|
||||
} from './recently-viewed.operations';
|
||||
|
||||
type QueryMock = {
|
||||
from: jest.Mock;
|
||||
innerJoin: jest.Mock;
|
||||
where: jest.Mock;
|
||||
orderBy: jest.Mock;
|
||||
limit: jest.Mock;
|
||||
then: (
|
||||
resolve: (value: unknown[]) => void,
|
||||
reject: (reason: unknown) => void
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
function createDbMock(selectResultsByCall: unknown[][] = []) {
|
||||
let selectIndex = 0;
|
||||
const queries: QueryMock[] = [];
|
||||
const select = jest.fn(() => {
|
||||
const rows = selectResultsByCall[selectIndex] ?? [];
|
||||
selectIndex += 1;
|
||||
const query: QueryMock = {
|
||||
from: jest.fn(),
|
||||
innerJoin: jest.fn(),
|
||||
where: jest.fn(),
|
||||
orderBy: jest.fn(),
|
||||
limit: jest.fn().mockResolvedValue(rows),
|
||||
then: (resolve, reject) =>
|
||||
Promise.resolve(rows).then(resolve, reject),
|
||||
};
|
||||
query.from.mockReturnValue(query);
|
||||
query.innerJoin.mockReturnValue(query);
|
||||
query.where.mockReturnValue(query);
|
||||
query.orderBy.mockReturnValue(query);
|
||||
queries.push(query);
|
||||
return query;
|
||||
});
|
||||
|
||||
const insertValues = jest.fn().mockResolvedValue(undefined);
|
||||
const insert = jest.fn().mockReturnValue({ values: insertValues });
|
||||
|
||||
const updateWhere = jest.fn().mockResolvedValue(undefined);
|
||||
const updateSet = jest.fn().mockReturnValue({ where: updateWhere });
|
||||
const update = jest.fn().mockReturnValue({ set: updateSet });
|
||||
|
||||
const deleteExecute = jest.fn().mockResolvedValue(undefined);
|
||||
const deletePrepare = jest
|
||||
.fn()
|
||||
.mockReturnValue({ execute: deleteExecute });
|
||||
const deleteResult = {
|
||||
prepare: deletePrepare,
|
||||
then: (
|
||||
resolve: (value: unknown) => void,
|
||||
reject: (reason: unknown) => void
|
||||
) => Promise.resolve(undefined).then(resolve, reject),
|
||||
};
|
||||
const deleteWhere = jest.fn().mockReturnValue(deleteResult);
|
||||
const deleteFn = jest.fn().mockReturnValue({
|
||||
where: deleteWhere,
|
||||
then: (
|
||||
resolve: (value: unknown) => void,
|
||||
reject: (reason: unknown) => void
|
||||
) => Promise.resolve(undefined).then(resolve, reject),
|
||||
});
|
||||
|
||||
const transaction = jest.fn((callback: () => unknown) => {
|
||||
const result = callback();
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
|
||||
return {
|
||||
db: {
|
||||
select,
|
||||
insert,
|
||||
update,
|
||||
delete: deleteFn,
|
||||
transaction,
|
||||
} as unknown as AppDatabase,
|
||||
deleteExecute,
|
||||
deleteFn,
|
||||
deletePrepare,
|
||||
deleteWhere,
|
||||
insert,
|
||||
insertValues,
|
||||
queries,
|
||||
select,
|
||||
transaction,
|
||||
update,
|
||||
updateSet,
|
||||
};
|
||||
}
|
||||
|
||||
describe('recently-viewed.operations', () => {
|
||||
beforeEach(() => {
|
||||
andMock.mockClear();
|
||||
descMock.mockClear();
|
||||
eqMock.mockClear();
|
||||
inArrayMock.mockClear();
|
||||
sqlMock.mockClear();
|
||||
sqlMock.placeholder.mockClear();
|
||||
resetDrizzleMocks();
|
||||
(persistContentBackdropIfMissing as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
@@ -162,7 +39,7 @@ describe('recently-viewed.operations', () => {
|
||||
|
||||
await expect(getRecentlyViewed(db)).resolves.toEqual(rows);
|
||||
|
||||
expect(descMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.desc).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.viewedAt
|
||||
);
|
||||
expect(queries[0].orderBy).toHaveBeenCalledWith(
|
||||
@@ -180,11 +57,11 @@ describe('recently-viewed.operations', () => {
|
||||
rows
|
||||
);
|
||||
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
expect(descMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.desc).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.viewedAt
|
||||
);
|
||||
expect(queries[0].limit).toHaveBeenCalledWith(100);
|
||||
@@ -228,12 +105,12 @@ describe('recently-viewed.operations', () => {
|
||||
expect(updateSet).toHaveBeenCalledWith({
|
||||
viewedAt: expect.objectContaining({ kind: 'sql' }),
|
||||
});
|
||||
expect(sqlMock).toHaveBeenCalledWith(['CURRENT_TIMESTAMP']);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.sql).toHaveBeenCalledWith(['CURRENT_TIMESTAMP']);
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.contentId,
|
||||
42
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
@@ -266,12 +143,12 @@ describe('recently-viewed.operations', () => {
|
||||
clearPlaylistRecentItems(db, 'playlist-1')
|
||||
).resolves.toEqual({ success: true });
|
||||
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.categories.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
expect(deleteFn).toHaveBeenCalledWith(schema.recentlyViewed);
|
||||
expect(inArrayMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.inArray).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.contentId,
|
||||
[11, 12]
|
||||
);
|
||||
@@ -299,11 +176,11 @@ describe('recently-viewed.operations', () => {
|
||||
|
||||
expect(deleteFn).toHaveBeenCalledWith(schema.recentlyViewed);
|
||||
expect(deleteWhere.mock.calls[0][0].conditions).toHaveLength(2);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.contentId,
|
||||
42
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
expect(mockDrizzle.eq).toHaveBeenCalledWith(
|
||||
schema.recentlyViewed.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
@@ -335,8 +212,8 @@ describe('recently-viewed.operations', () => {
|
||||
).resolves.toEqual({ success: true, count: 2 });
|
||||
|
||||
expect(deletePrepare).toHaveBeenCalledTimes(1);
|
||||
expect(sqlMock.placeholder).toHaveBeenCalledWith('contentId');
|
||||
expect(sqlMock.placeholder).toHaveBeenCalledWith('playlistId');
|
||||
expect(mockDrizzle.sql.placeholder).toHaveBeenCalledWith('contentId');
|
||||
expect(mockDrizzle.sql.placeholder).toHaveBeenCalledWith('playlistId');
|
||||
expect(transaction).toHaveBeenCalledTimes(1);
|
||||
expect(deleteExecute).toHaveBeenNthCalledWith(1, {
|
||||
contentId: 1,
|
||||
|
||||
+5
-219
@@ -1,14 +1,14 @@
|
||||
import { PlaybackPositionData } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
ElectronXtreamDataSourceHarness,
|
||||
setupElectronXtreamDataSource,
|
||||
} from './electron-xtream-data-source.test-helpers';
|
||||
|
||||
/**
|
||||
* Delegation tests for ElectronXtreamDataSource: methods that forward to
|
||||
* DatabaseService / PlaybackPositionService over the IPC boundary.
|
||||
* The DB-first fetch/cache strategy is covered in
|
||||
* electron-xtream-data-source.spec.ts.
|
||||
* Delegation tests for ElectronXtreamDataSource: playlist, category, and
|
||||
* content methods that forward to the DatabaseService over the IPC boundary.
|
||||
* User-data delegation (favorites, recently viewed, playback positions,
|
||||
* cleanup) is covered in electron-xtream-data-source.user-data.spec.ts and
|
||||
* the DB-first fetch/cache strategy in electron-xtream-data-source.spec.ts.
|
||||
*/
|
||||
describe('ElectronXtreamDataSource (delegation)', () => {
|
||||
let harness: ElectronXtreamDataSourceHarness;
|
||||
@@ -174,218 +174,4 @@ describe('ElectronXtreamDataSource (delegation)', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('favorites and recently viewed', () => {
|
||||
it('delegates favorites operations to the DB', async () => {
|
||||
harness.dbService.isFavorite.mockResolvedValue(true);
|
||||
|
||||
await harness.dataSource.addFavorite(
|
||||
202,
|
||||
playlistId,
|
||||
'https://example.com/backdrop.png'
|
||||
);
|
||||
expect(harness.dbService.addToFavorites).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId,
|
||||
'https://example.com/backdrop.png'
|
||||
);
|
||||
|
||||
await harness.dataSource.removeFavorite(202, playlistId);
|
||||
expect(harness.dbService.removeFromFavorites).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId
|
||||
);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.isFavorite(202, playlistId)
|
||||
).resolves.toBe(true);
|
||||
await harness.dataSource.getFavorites(playlistId);
|
||||
expect(harness.dbService.getFavorites).toHaveBeenCalledWith(
|
||||
playlistId
|
||||
);
|
||||
});
|
||||
|
||||
it('delegates recently viewed operations to the DB', async () => {
|
||||
await harness.dataSource.addRecentItem(202, playlistId);
|
||||
expect(harness.dbService.addRecentItem).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId,
|
||||
undefined
|
||||
);
|
||||
|
||||
await harness.dataSource.removeRecentItem(202, playlistId);
|
||||
expect(harness.dbService.removeRecentItem).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId
|
||||
);
|
||||
|
||||
await harness.dataSource.getRecentItems(playlistId);
|
||||
expect(harness.dbService.getRecentItems).toHaveBeenCalledWith(
|
||||
playlistId
|
||||
);
|
||||
|
||||
await harness.dataSource.clearRecentItems(playlistId);
|
||||
expect(
|
||||
harness.dbService.clearPlaylistRecentItems
|
||||
).toHaveBeenCalledWith(playlistId);
|
||||
});
|
||||
|
||||
it('delegates content lookup and backdrop backfill to the DB', async () => {
|
||||
const item = { id: 1, title: 'Movie One', xtream_id: 202 };
|
||||
harness.dbService.getContentByXtreamId.mockResolvedValue(item);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getContentByXtreamId(
|
||||
202,
|
||||
playlistId,
|
||||
'movie'
|
||||
)
|
||||
).resolves.toEqual(item);
|
||||
expect(harness.dbService.getContentByXtreamId).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId,
|
||||
'movie'
|
||||
);
|
||||
|
||||
await harness.dataSource.setContentBackdropIfMissing(
|
||||
1,
|
||||
playlistId,
|
||||
'https://example.com/backdrop.png'
|
||||
);
|
||||
// playlistId is intentionally not forwarded for the Electron DB call
|
||||
expect(
|
||||
harness.dbService.setContentBackdropIfMissing
|
||||
).toHaveBeenCalledWith(1, 'https://example.com/backdrop.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('playback positions', () => {
|
||||
const position = {
|
||||
contentXtreamId: 202,
|
||||
contentType: 'vod',
|
||||
position: 120,
|
||||
duration: 3600,
|
||||
} as unknown as PlaybackPositionData;
|
||||
|
||||
it('delegates playback position operations to the playback service', async () => {
|
||||
harness.playbackService.getPlaybackPosition.mockResolvedValue(
|
||||
position
|
||||
);
|
||||
harness.playbackService.getSeriesPlaybackPositions.mockResolvedValue(
|
||||
[position]
|
||||
);
|
||||
harness.playbackService.getRecentPlaybackPositions.mockResolvedValue(
|
||||
[position]
|
||||
);
|
||||
harness.playbackService.getAllPlaybackPositions.mockResolvedValue([
|
||||
position,
|
||||
]);
|
||||
|
||||
await harness.dataSource.savePlaybackPosition(playlistId, position);
|
||||
expect(
|
||||
harness.playbackService.savePlaybackPosition
|
||||
).toHaveBeenCalledWith(playlistId, position);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getPlaybackPosition(playlistId, 202, 'vod')
|
||||
).resolves.toEqual(position);
|
||||
await expect(
|
||||
harness.dataSource.getSeriesPlaybackPositions(playlistId, 303)
|
||||
).resolves.toEqual([position]);
|
||||
expect(
|
||||
harness.playbackService.getSeriesPlaybackPositions
|
||||
).toHaveBeenCalledWith(playlistId, 303);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getRecentPlaybackPositions(playlistId, 5)
|
||||
).resolves.toEqual([position]);
|
||||
expect(
|
||||
harness.playbackService.getRecentPlaybackPositions
|
||||
).toHaveBeenCalledWith(playlistId, 5);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getAllPlaybackPositions(playlistId)
|
||||
).resolves.toEqual([position]);
|
||||
|
||||
await harness.dataSource.clearPlaybackPosition(
|
||||
playlistId,
|
||||
202,
|
||||
'vod'
|
||||
);
|
||||
expect(
|
||||
harness.playbackService.clearPlaybackPosition
|
||||
).toHaveBeenCalledWith(playlistId, 202, 'vod');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup operations', () => {
|
||||
it('clearSessionCache is a no-op that touches no services', () => {
|
||||
expect(
|
||||
harness.dataSource.clearSessionCache(playlistId)
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
harness.dbService.deleteXtreamPlaylistContent
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('combines DB restore data with playback positions on clearPlaylistContent', async () => {
|
||||
const hidden = [{ categoryType: 'live', xtreamId: 5 }];
|
||||
const favorites = [{ xtreamId: 202, type: 'movie' }];
|
||||
const recentlyViewed = [{ xtreamId: 101, type: 'live' }];
|
||||
const position = { contentXtreamId: 202 } as never;
|
||||
harness.dbService.deleteXtreamPlaylistContent.mockResolvedValue({
|
||||
hiddenCategories: hidden,
|
||||
favorites,
|
||||
recentlyViewed,
|
||||
});
|
||||
harness.playbackService.getAllPlaybackPositions.mockResolvedValue([
|
||||
position,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.clearPlaylistContent(playlistId)
|
||||
).resolves.toEqual({
|
||||
hiddenCategories: hidden,
|
||||
favorites,
|
||||
recentlyViewed,
|
||||
playbackPositions: [position],
|
||||
});
|
||||
});
|
||||
|
||||
it('restores user data, then resets and replays playback positions', async () => {
|
||||
const positionA = { contentXtreamId: 1 } as never;
|
||||
const positionB = { contentXtreamId: 2 } as never;
|
||||
const restoreState = {
|
||||
hiddenCategories: [],
|
||||
favorites: [{ xtreamId: 202, type: 'movie' }],
|
||||
recentlyViewed: [{ xtreamId: 101, type: 'live' }],
|
||||
playbackPositions: [positionA, positionB],
|
||||
} as never;
|
||||
const options = { operationId: 'op-1' };
|
||||
|
||||
await harness.dataSource.restoreUserData(
|
||||
playlistId,
|
||||
restoreState,
|
||||
options
|
||||
);
|
||||
|
||||
expect(
|
||||
harness.dbService.restoreXtreamUserData
|
||||
).toHaveBeenCalledWith(
|
||||
playlistId,
|
||||
[{ xtreamId: 202, type: 'movie' }],
|
||||
[{ xtreamId: 101, type: 'live' }],
|
||||
options
|
||||
);
|
||||
expect(
|
||||
harness.playbackService.clearAllPlaybackPositions
|
||||
).toHaveBeenCalledWith(playlistId);
|
||||
expect(
|
||||
harness.playbackService.savePlaybackPosition.mock.calls
|
||||
).toEqual([
|
||||
[playlistId, positionA],
|
||||
[playlistId, positionB],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
import { PlaybackPositionData } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
ElectronXtreamDataSourceHarness,
|
||||
setupElectronXtreamDataSource,
|
||||
} from './electron-xtream-data-source.test-helpers';
|
||||
|
||||
/**
|
||||
* Delegation tests for ElectronXtreamDataSource user data: favorites,
|
||||
* recently viewed, playback positions, and cleanup/restore flows.
|
||||
* Playlist/category/content delegation is covered in
|
||||
* electron-xtream-data-source.delegation.spec.ts and the DB-first
|
||||
* fetch/cache strategy in electron-xtream-data-source.spec.ts.
|
||||
*/
|
||||
describe('ElectronXtreamDataSource (user data delegation)', () => {
|
||||
let harness: ElectronXtreamDataSourceHarness;
|
||||
|
||||
const playlistId = 'playlist-1';
|
||||
|
||||
beforeEach(() => {
|
||||
harness = setupElectronXtreamDataSource();
|
||||
});
|
||||
|
||||
describe('favorites and recently viewed', () => {
|
||||
it('delegates favorites operations to the DB', async () => {
|
||||
harness.dbService.isFavorite.mockResolvedValue(true);
|
||||
|
||||
await harness.dataSource.addFavorite(
|
||||
202,
|
||||
playlistId,
|
||||
'https://example.com/backdrop.png'
|
||||
);
|
||||
expect(harness.dbService.addToFavorites).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId,
|
||||
'https://example.com/backdrop.png'
|
||||
);
|
||||
|
||||
await harness.dataSource.removeFavorite(202, playlistId);
|
||||
expect(harness.dbService.removeFromFavorites).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId
|
||||
);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.isFavorite(202, playlistId)
|
||||
).resolves.toBe(true);
|
||||
await harness.dataSource.getFavorites(playlistId);
|
||||
expect(harness.dbService.getFavorites).toHaveBeenCalledWith(
|
||||
playlistId
|
||||
);
|
||||
});
|
||||
|
||||
it('delegates recently viewed operations to the DB', async () => {
|
||||
await harness.dataSource.addRecentItem(202, playlistId);
|
||||
expect(harness.dbService.addRecentItem).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId,
|
||||
undefined
|
||||
);
|
||||
|
||||
await harness.dataSource.removeRecentItem(202, playlistId);
|
||||
expect(harness.dbService.removeRecentItem).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId
|
||||
);
|
||||
|
||||
await harness.dataSource.getRecentItems(playlistId);
|
||||
expect(harness.dbService.getRecentItems).toHaveBeenCalledWith(
|
||||
playlistId
|
||||
);
|
||||
|
||||
await harness.dataSource.clearRecentItems(playlistId);
|
||||
expect(
|
||||
harness.dbService.clearPlaylistRecentItems
|
||||
).toHaveBeenCalledWith(playlistId);
|
||||
});
|
||||
|
||||
it('delegates content lookup and backdrop backfill to the DB', async () => {
|
||||
const item = { id: 1, title: 'Movie One', xtream_id: 202 };
|
||||
harness.dbService.getContentByXtreamId.mockResolvedValue(item);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getContentByXtreamId(
|
||||
202,
|
||||
playlistId,
|
||||
'movie'
|
||||
)
|
||||
).resolves.toEqual(item);
|
||||
expect(harness.dbService.getContentByXtreamId).toHaveBeenCalledWith(
|
||||
202,
|
||||
playlistId,
|
||||
'movie'
|
||||
);
|
||||
|
||||
await harness.dataSource.setContentBackdropIfMissing(
|
||||
1,
|
||||
playlistId,
|
||||
'https://example.com/backdrop.png'
|
||||
);
|
||||
// playlistId is intentionally not forwarded for the Electron DB call
|
||||
expect(
|
||||
harness.dbService.setContentBackdropIfMissing
|
||||
).toHaveBeenCalledWith(1, 'https://example.com/backdrop.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('playback positions', () => {
|
||||
const position = {
|
||||
contentXtreamId: 202,
|
||||
contentType: 'vod',
|
||||
position: 120,
|
||||
duration: 3600,
|
||||
} as unknown as PlaybackPositionData;
|
||||
|
||||
it('delegates playback position operations to the playback service', async () => {
|
||||
harness.playbackService.getPlaybackPosition.mockResolvedValue(
|
||||
position
|
||||
);
|
||||
harness.playbackService.getSeriesPlaybackPositions.mockResolvedValue(
|
||||
[position]
|
||||
);
|
||||
harness.playbackService.getRecentPlaybackPositions.mockResolvedValue(
|
||||
[position]
|
||||
);
|
||||
harness.playbackService.getAllPlaybackPositions.mockResolvedValue([
|
||||
position,
|
||||
]);
|
||||
|
||||
await harness.dataSource.savePlaybackPosition(playlistId, position);
|
||||
expect(
|
||||
harness.playbackService.savePlaybackPosition
|
||||
).toHaveBeenCalledWith(playlistId, position);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getPlaybackPosition(playlistId, 202, 'vod')
|
||||
).resolves.toEqual(position);
|
||||
await expect(
|
||||
harness.dataSource.getSeriesPlaybackPositions(playlistId, 303)
|
||||
).resolves.toEqual([position]);
|
||||
expect(
|
||||
harness.playbackService.getSeriesPlaybackPositions
|
||||
).toHaveBeenCalledWith(playlistId, 303);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getRecentPlaybackPositions(playlistId, 5)
|
||||
).resolves.toEqual([position]);
|
||||
expect(
|
||||
harness.playbackService.getRecentPlaybackPositions
|
||||
).toHaveBeenCalledWith(playlistId, 5);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.getAllPlaybackPositions(playlistId)
|
||||
).resolves.toEqual([position]);
|
||||
|
||||
await harness.dataSource.clearPlaybackPosition(
|
||||
playlistId,
|
||||
202,
|
||||
'vod'
|
||||
);
|
||||
expect(
|
||||
harness.playbackService.clearPlaybackPosition
|
||||
).toHaveBeenCalledWith(playlistId, 202, 'vod');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup operations', () => {
|
||||
it('clearSessionCache is a no-op that touches no services', () => {
|
||||
expect(
|
||||
harness.dataSource.clearSessionCache(playlistId)
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
harness.dbService.deleteXtreamPlaylistContent
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('combines DB restore data with playback positions on clearPlaylistContent', async () => {
|
||||
const hidden = [{ categoryType: 'live', xtreamId: 5 }];
|
||||
const favorites = [{ xtreamId: 202, type: 'movie' }];
|
||||
const recentlyViewed = [{ xtreamId: 101, type: 'live' }];
|
||||
const position = { contentXtreamId: 202 } as never;
|
||||
harness.dbService.deleteXtreamPlaylistContent.mockResolvedValue({
|
||||
hiddenCategories: hidden,
|
||||
favorites,
|
||||
recentlyViewed,
|
||||
});
|
||||
harness.playbackService.getAllPlaybackPositions.mockResolvedValue([
|
||||
position,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
harness.dataSource.clearPlaylistContent(playlistId)
|
||||
).resolves.toEqual({
|
||||
hiddenCategories: hidden,
|
||||
favorites,
|
||||
recentlyViewed,
|
||||
playbackPositions: [position],
|
||||
});
|
||||
});
|
||||
|
||||
it('restores user data, then resets and replays playback positions', async () => {
|
||||
const positionA = { contentXtreamId: 1 } as never;
|
||||
const positionB = { contentXtreamId: 2 } as never;
|
||||
const restoreState = {
|
||||
hiddenCategories: [],
|
||||
favorites: [{ xtreamId: 202, type: 'movie' }],
|
||||
recentlyViewed: [{ xtreamId: 101, type: 'live' }],
|
||||
playbackPositions: [positionA, positionB],
|
||||
} as never;
|
||||
const options = { operationId: 'op-1' };
|
||||
|
||||
await harness.dataSource.restoreUserData(
|
||||
playlistId,
|
||||
restoreState,
|
||||
options
|
||||
);
|
||||
|
||||
expect(
|
||||
harness.dbService.restoreXtreamUserData
|
||||
).toHaveBeenCalledWith(
|
||||
playlistId,
|
||||
[{ xtreamId: 202, type: 'movie' }],
|
||||
[{ xtreamId: 101, type: 'live' }],
|
||||
options
|
||||
);
|
||||
expect(
|
||||
harness.playbackService.clearAllPlaybackPositions
|
||||
).toHaveBeenCalledWith(playlistId);
|
||||
expect(
|
||||
harness.playbackService.savePlaybackPosition.mock.calls
|
||||
).toEqual([
|
||||
[playlistId, positionA],
|
||||
[playlistId, positionB],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user