Merge pull request #766 from ajnart/tests/add-tests
✅ Add vitest and initial tests
This commit is contained in:
11
.eslintrc.js
11
.eslintrc.js
@@ -2,12 +2,12 @@ module.exports = {
|
|||||||
extends: [
|
extends: [
|
||||||
'mantine',
|
'mantine',
|
||||||
'plugin:@next/next/recommended',
|
'plugin:@next/next/recommended',
|
||||||
'plugin:jest/recommended',
|
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:vitest/recommended',
|
||||||
],
|
],
|
||||||
plugins: ['testing-library', 'jest', 'react-hooks', 'react', 'unused-imports'],
|
plugins: ['testing-library', 'react-hooks', 'react', 'unused-imports', 'vitest'],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
files: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||||
@@ -31,5 +31,12 @@ module.exports = {
|
|||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
'no-continue': 'off',
|
'no-continue': 'off',
|
||||||
'linebreak-style': 0,
|
'linebreak-style': 0,
|
||||||
|
'vitest/max-nested-describe': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'testing-library/no-node-access': ['error', { allowContainerFirstChild: true }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,3 +104,11 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/ajnart
|
|||||||
Homarr uses [GitMoji](https://gitmoji.dev/).
|
Homarr uses [GitMoji](https://gitmoji.dev/).
|
||||||
We would appreciate it if everyone keeps their commit messages withing these rulings.
|
We would appreciate it if everyone keeps their commit messages withing these rulings.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
> Components should be tested using unit tests. A unit is the smallest isolated part of the component. Unit tests must not have any dependencies and must be isolated.
|
||||||
|
|
||||||
|
- Place testfiles directly at the root of the unit
|
||||||
|
- Only test a single unit of work inside a unit test
|
||||||
|
- You may test multiple units inside one test file
|
||||||
|
- Testnames do not begin with ``should`` or the unit name
|
||||||
@@ -28,12 +28,12 @@ module.exports = {
|
|||||||
'sk',
|
'sk',
|
||||||
'no',
|
'no',
|
||||||
],
|
],
|
||||||
localePath: path.resolve('./public/locales'),
|
|
||||||
fallbackLng: 'en',
|
|
||||||
localeDetection: true,
|
localeDetection: true,
|
||||||
returnEmptyString: false,
|
|
||||||
debug: false,
|
|
||||||
appendNamespaceToCIMode: true,
|
|
||||||
reloadOnPrerender: process.env.NODE_ENV === 'development',
|
|
||||||
},
|
},
|
||||||
|
returnEmptyString: false,
|
||||||
|
appendNamespaceToCIMode: true,
|
||||||
|
reloadOnPrerender: process.env.NODE_ENV === 'development',
|
||||||
|
fallbackLng: 'en',
|
||||||
|
localePath: path.resolve('./public/locales'),
|
||||||
};
|
};
|
||||||
|
|||||||
23
package.json
23
package.json
@@ -10,18 +10,18 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"turbo" : "turbo run build",
|
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
"turbo": "turbo run build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"export": "next build && next export",
|
"export": "next build && next export",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"jest": "jest",
|
|
||||||
"jest:watch": "jest --watch",
|
|
||||||
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||||
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
|
"test": "vitest",
|
||||||
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
|
"test:ui": "vitest --ui",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/deluge": "^4.1.0",
|
"@ctrl/deluge": "^4.1.0",
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
"@tabler/icons": "^1.106.0",
|
"@tabler/icons": "^1.106.0",
|
||||||
"@tanstack/react-query": "^4.2.1",
|
"@tanstack/react-query": "^4.2.1",
|
||||||
"@tanstack/react-query-devtools": "^4.24.4",
|
"@tanstack/react-query-devtools": "^4.24.4",
|
||||||
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"consola": "^2.15.3",
|
"consola": "^2.15.3",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
@@ -71,6 +72,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "^12.1.4",
|
"@next/bundle-analyzer": "^12.1.4",
|
||||||
"@next/eslint-plugin-next": "^12.1.4",
|
"@next/eslint-plugin-next": "^12.1.4",
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^14.0.0",
|
||||||
"@types/dockerode": "^3.3.9",
|
"@types/dockerode": "^3.3.9",
|
||||||
"@types/node": "17.0.1",
|
"@types/node": "17.0.1",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
@@ -79,23 +82,27 @@
|
|||||||
"@types/video.js": "^7.3.51",
|
"@types/video.js": "^7.3.51",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||||
"@typescript-eslint/parser": "^5.30.7",
|
"@typescript-eslint/parser": "^5.30.7",
|
||||||
|
"@vitest/coverage-c8": "^0.29.3",
|
||||||
|
"@vitest/ui": "^0.29.3",
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.20.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-config-mantine": "^2.0.0",
|
"eslint-config-mantine": "^2.0.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jest": "^26.6.0",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
"eslint-plugin-react": "^7.30.1",
|
"eslint-plugin-react": "^7.30.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-testing-library": "^5.5.1",
|
"eslint-plugin-testing-library": "^5.5.1",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"jest": "^28.1.3",
|
"eslint-plugin-vitest": "^0.0.54",
|
||||||
|
"happy-dom": "^8.9.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"sass": "^1.56.1",
|
"sass": "^1.56.1",
|
||||||
"turbo": "^1.8.3",
|
"turbo": "^1.8.3",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"video.js": "^8.0.3"
|
"video.js": "^8.0.3",
|
||||||
|
"vitest": "^0.29.3",
|
||||||
|
"vitest-fetch-mock": "^0.2.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "17.0.2",
|
"@types/react": "17.0.2",
|
||||||
|
|||||||
13
src/components/AppAvatar.test.tsx
Normal file
13
src/components/AppAvatar.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { render, screen, cleanup } from '@testing-library/react';
|
||||||
|
import { describe, expect, it, afterEach } from 'vitest';
|
||||||
|
import { AppAvatar } from './AppAvatar';
|
||||||
|
|
||||||
|
describe('AppAvatar', () => {
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
it('display placeholder when no url', () => {
|
||||||
|
render(<AppAvatar iconUrl="" color="blue" />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('app-avatar')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ export const AppAvatar = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
data-testid="app-avatar"
|
||||||
src={iconUrl}
|
src={iconUrl}
|
||||||
bg={colorScheme === 'dark' ? colors.gray[8] : colors.gray[2]}
|
bg={colorScheme === 'dark' ? colors.gray[8] : colors.gray[2]}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useMantineTheme } from '@mantine/core';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
|
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
} from '../../../pages/api/modules/usenet/history';
|
} from '../../../pages/api/modules/usenet/history';
|
||||||
import { UsenetInfoRequestParams, UsenetInfoResponse } from '../../../pages/api/modules/usenet';
|
import { UsenetInfoRequestParams, UsenetInfoResponse } from '../../../pages/api/modules/usenet';
|
||||||
import { UsenetPauseRequestParams } from '../../../pages/api/modules/usenet/pause';
|
import { UsenetPauseRequestParams } from '../../../pages/api/modules/usenet/pause';
|
||||||
import { queryClient } from '../../../tools/queryClient';
|
import { queryClient } from '../../../tools/server/configurations/tanstack/queryClient.tool';
|
||||||
import { UsenetResumeRequestParams } from '../../../pages/api/modules/usenet/resume';
|
import { UsenetResumeRequestParams } from '../../../pages/api/modules/usenet/resume';
|
||||||
|
|
||||||
const POLLING_INTERVAL = 2000;
|
const POLLING_INTERVAL = 2000;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { useState } from 'react';
|
|||||||
import { TFunction } from 'react-i18next';
|
import { TFunction } from 'react-i18next';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { tryMatchService } from '../../tools/addToHomarr';
|
|
||||||
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
|
||||||
|
import { MatchingImages, ServiceType, tryMatchPort } from '../../tools/types';
|
||||||
import { AppType } from '../../types/app';
|
import { AppType } from '../../types/app';
|
||||||
|
|
||||||
let t: TFunction<'modules/docker', undefined>;
|
let t: TFunction<'modules/docker', undefined>;
|
||||||
@@ -206,3 +206,36 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated legacy code
|
||||||
|
*/
|
||||||
|
function tryMatchType(imageName: string): ServiceType {
|
||||||
|
const match = MatchingImages.find(({ image }) => imageName.includes(image));
|
||||||
|
if (match) {
|
||||||
|
return match.type;
|
||||||
|
}
|
||||||
|
// TODO: Remove this legacy code
|
||||||
|
return 'Other';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* @param container the container to match
|
||||||
|
* @returns a new service
|
||||||
|
*/
|
||||||
|
const tryMatchService = (container: Dockerode.ContainerInfo | undefined) => {
|
||||||
|
if (container === undefined) return {};
|
||||||
|
const name = container.Names[0].substring(1);
|
||||||
|
const type = tryMatchType(container.Image);
|
||||||
|
const port = tryMatchPort(type.toLowerCase())?.value ?? container.Ports[0]?.PublicPort;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
id: container.Id,
|
||||||
|
type: tryMatchType(container.Image),
|
||||||
|
url: `localhost${port ? `:${port}` : ''}`,
|
||||||
|
icon: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${name
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.toLowerCase()}.png`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
Table,
|
|
||||||
Checkbox,
|
|
||||||
Group,
|
|
||||||
Badge,
|
Badge,
|
||||||
|
Checkbox,
|
||||||
createStyles,
|
createStyles,
|
||||||
|
Group,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
TextInput,
|
Table,
|
||||||
useMantineTheme,
|
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import { IconSearch } from '@tabler/icons';
|
import { IconSearch } from '@tabler/icons';
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { getServerSideTranslations } from '../tools/server/getServerSideTranslations';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -94,12 +95,11 @@ export default function Custom404() {
|
|||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export async function getStaticProps({ req, res, locale }: GetServerSidePropsContext) {
|
||||||
export async function getStaticProps({ locale }: { locale: string }) {
|
const translations = await getServerSideTranslations(['common'], locale, undefined, undefined);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale, ['common'])),
|
...translations,
|
||||||
// Will be passed to the page component as props
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function getServerSideProps({
|
|||||||
const configPath = path.join(process.cwd(), 'data/configs', `${configName}.json`);
|
const configPath = path.join(process.cwd(), 'data/configs', `${configName}.json`);
|
||||||
const configExists = fs.existsSync(configPath);
|
const configExists = fs.existsSync(configPath);
|
||||||
|
|
||||||
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
|
const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res);
|
||||||
|
|
||||||
if (!configExists) {
|
if (!configExists) {
|
||||||
// Redirect to 404
|
// Redirect to 404
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/Cat
|
|||||||
import { ConfigProvider } from '../config/provider';
|
import { ConfigProvider } from '../config/provider';
|
||||||
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../tools/client/zustands/usePackageAttributesStore';
|
||||||
import { ColorTheme } from '../tools/color';
|
import { ColorTheme } from '../tools/color';
|
||||||
import { queryClient } from '../tools/queryClient';
|
import { queryClient } from '../tools/server/configurations/tanstack/queryClient.tool';
|
||||||
import {
|
import {
|
||||||
getServiceSidePackageAttributes,
|
getServiceSidePackageAttributes,
|
||||||
ServerSidePackageAttributesType,
|
ServerSidePackageAttributesType,
|
||||||
} from '../tools/server/getPackageVersion';
|
} from '../tools/server/getPackageVersion';
|
||||||
import { theme } from '../tools/theme';
|
import { theme } from '../tools/server/theme/theme';
|
||||||
|
|
||||||
import { useEditModeInformationStore } from '../hooks/useEditModeInformation';
|
import { useEditModeInformationStore } from '../hooks/useEditModeInformation';
|
||||||
import '../styles/global.scss';
|
import '../styles/global.scss';
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import Parser from 'rss-parser';
|
import Parser from 'rss-parser';
|
||||||
|
|
||||||
import { getConfig } from '../../../../tools/config/getConfig';
|
import { getConfig } from '../../../../tools/config/getConfig';
|
||||||
import { Stopwatch } from '../../../../tools/shared/stopwatch';
|
|
||||||
import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile';
|
import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile';
|
||||||
|
import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool';
|
||||||
|
|
||||||
type CustomItem = {
|
type CustomItem = {
|
||||||
'media:content': string;
|
'media:content': string;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function getServerSideProps({
|
|||||||
configName = 'default';
|
configName = 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
|
const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res);
|
||||||
const config = getFrontendConfig(configName as string);
|
const config = getFrontendConfig(configName as string);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import Dockerode from 'dockerode';
|
|
||||||
|
|
||||||
import { MatchingImages, ServiceType, tryMatchPort } from './types';
|
|
||||||
|
|
||||||
function tryMatchType(imageName: string): ServiceType {
|
|
||||||
// Try to find imageName inside MatchingImages
|
|
||||||
|
|
||||||
const match = MatchingImages.find(({ image }) => imageName.includes(image));
|
|
||||||
if (match) {
|
|
||||||
return match.type;
|
|
||||||
}
|
|
||||||
return 'Other';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tryMatchService(container: Dockerode.ContainerInfo | undefined) {
|
|
||||||
if (container === undefined) return {};
|
|
||||||
const name = container.Names[0].substring(1);
|
|
||||||
const type = tryMatchType(container.Image);
|
|
||||||
const port = tryMatchPort(type.toLowerCase())?.value ?? container.Ports[0]?.PublicPort;
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
id: container.Id,
|
|
||||||
type: tryMatchType(container.Image),
|
|
||||||
url: `localhost${port ? `:${port}` : ''}`,
|
|
||||||
icon: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${name
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.toLowerCase()}.png`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -3,17 +3,16 @@ import { IncomingMessage, ServerResponse } from 'http';
|
|||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
|
|
||||||
export const getServerSideTranslations = async (
|
export const getServerSideTranslations = async (
|
||||||
req: IncomingMessage,
|
|
||||||
res: ServerResponse,
|
|
||||||
namespaces: string[],
|
namespaces: string[],
|
||||||
requestLocale?: string
|
requestLocale?: string,
|
||||||
|
req?: IncomingMessage,
|
||||||
|
res?: ServerResponse
|
||||||
) => {
|
) => {
|
||||||
|
if (!req || !res) {
|
||||||
|
return serverSideTranslations(requestLocale ?? 'en', namespaces);
|
||||||
|
}
|
||||||
|
|
||||||
const configLocale = getCookie('config-locale', { req, res });
|
const configLocale = getCookie('config-locale', { req, res });
|
||||||
|
|
||||||
const translations = await serverSideTranslations(
|
return serverSideTranslations((configLocale ?? requestLocale ?? 'en') as string, namespaces);
|
||||||
(configLocale ?? requestLocale ?? 'en') as string,
|
|
||||||
namespaces
|
|
||||||
);
|
|
||||||
|
|
||||||
return translations;
|
|
||||||
};
|
};
|
||||||
|
|||||||
77
src/tools/server/sdk/plex/plexClient.test.ts
Normal file
77
src/tools/server/sdk/plex/plexClient.test.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import 'vitest-fetch-mock';
|
||||||
|
|
||||||
|
import { PlexClient } from './plexClient';
|
||||||
|
|
||||||
|
const mockResponse = `<MediaContainer size="1">
|
||||||
|
<Video addedAt="0000000" art="/library/metadata/2/art/00000000" audienceRating="0.0" audienceRatingImage="niceImage" chapterSource="media" contentRating="TV-PG" duration="6262249" guid="plex://movie/0000000000000000" key="/library/metadata/2" lastViewedAt="0000000" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originalTitle="00000000000000" originallyAvailableAt="0000-00-00" rating="0.0" ratingImage="ratingimage" ratingKey="2" sessionKey="1" studio="Example Studio" summary="Lorem Ispum dolor sit amet" tagline="Yep" thumb="/library/metadata/2/thumb/0000000" title="A long title" titleSort="A short title" type="movie" updatedAt="000000" viewOffset="0" year="0000">
|
||||||
|
<Media audioProfile="ma" id="2" videoProfile="high" audioChannels="2" audioCodec="aac" bitrate="20231" container="mp4" duration="6262249" height="1080" optimizedForStreaming="1" protocol="dash" videoCodec="h264" videoFrameRate="24p" videoResolution="1080p" width="1920" selected="1">
|
||||||
|
<Part audioProfile="ma" hasThumbnail="1" id="2" videoProfile="high" bitrate="20231" container="mp4" duration="6262249" height="1080" optimizedForStreaming="1" protocol="dash" width="1920" decision="transcode" selected="1">
|
||||||
|
<Stream bitDepth="8" bitrate="19975" chromaLocation="left" chromaSubsampling="4:2:0" codec="h264" codedHeight="1088" codedWidth="1920" default="1" displayTitle="XXXX" extendedDisplayTitle="Yes" frameRate="23.975999832153320" hasScalingMatrix="0" height="1080" id="4" level="41" profile="high" refFrames="4" scanType="progressive" streamType="1" title="Example" width="1920" decision="copy" location="segments-video"/>
|
||||||
|
<Stream bitrate="256" bitrateMode="cbr" channels="2" codec="aac" default="1" displayTitle="Not Existing" extendedDisplayTitle="Yes, really" id="5" language="Yep" languageCode="jpn" languageTag="ch" selected="1" streamType="2" decision="transcode" location="segments-audio"/>
|
||||||
|
</Part>
|
||||||
|
</Media>
|
||||||
|
<Genre count="13" filter="genre=48" id="48" tag="Drama"/>
|
||||||
|
<Genre count="8" filter="genre=104" id="104" tag="Adventure"/>
|
||||||
|
<User id="1" thumb="https://google.com" title="example_usr"/>
|
||||||
|
<Player address="0.0.0.0" device="Windows" machineIdentifier="72483785378573857385" model="bundled" platform="Chrome" platformVersion="111.0" product="Plex Web" profile="Web" state="paused" title="Chrome" version="0.000.0" local="1" relayed="0" secure="1" userID="1"/>
|
||||||
|
<Session id="2894294r2jf2038fj3098jgf3gt" bandwidth="21560" location="lan"/>
|
||||||
|
<TranscodeSession key="/transcode/sessions/example-session" throttled="0" complete="0" progress="0" size="-22" speed="18.600000381469727" error="0" duration="100" remaining="70" context="streaming" sourceVideoCodec="h264" sourceAudioCodec="dca" videoDecision="copy" audioDecision="transcode" protocol="dash" container="mp4" videoCodec="h264" audioCodec="aac" audioChannels="2" width="1920" height="1080" transcodeHwRequested="0" transcodeHwFullPipeline="0" timeStamp="1679349635.2791338" maxOffsetAvailable="104.27" minOffsetAvailable="84.166999816894531"/>
|
||||||
|
</Video>
|
||||||
|
</MediaContainer>`;
|
||||||
|
|
||||||
|
describe('Plex SDK', () => {
|
||||||
|
it('abc', async () => {
|
||||||
|
// arrange
|
||||||
|
const client = new PlexClient('https://plex.local', 'MY_TOKEN');
|
||||||
|
|
||||||
|
fetchMock.mockResponseOnce(mockResponse);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const response = await client.getSessions();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(fetchMock.requests().length).toBe(1);
|
||||||
|
expect(fetchMock.requests()[0].url).toBe(
|
||||||
|
'https://plex.local/status/sessions?X-Plex-Token=MY_TOKEN'
|
||||||
|
);
|
||||||
|
expect(response).not.toBeNull();
|
||||||
|
expect(response.length).toBe(1);
|
||||||
|
expect(response[0].id).toBe('2894294r2jf2038fj3098jgf3gt');
|
||||||
|
expect(response[0].username).toBe('example_usr');
|
||||||
|
expect(response[0].userProfilePicture).toBe('https://google.com');
|
||||||
|
expect(response[0].sessionName).toBe('Plex Web (Chrome)');
|
||||||
|
expect(response[0].currentlyPlaying).toMatchObject({
|
||||||
|
name: 'A long title',
|
||||||
|
type: 'movie',
|
||||||
|
metadata: {
|
||||||
|
video: {
|
||||||
|
bitrate: '20231',
|
||||||
|
height: '1080',
|
||||||
|
videoCodec: 'h264',
|
||||||
|
videoFrameRate: '24p',
|
||||||
|
width: '1920',
|
||||||
|
},
|
||||||
|
audio: { audioChannels: '2', audioCodec: 'aac' },
|
||||||
|
transcoding: {
|
||||||
|
audioChannels: '2',
|
||||||
|
audioCodec: 'aac',
|
||||||
|
audioDecision: 'transcode',
|
||||||
|
container: 'mp4',
|
||||||
|
context: 'streaming',
|
||||||
|
duration: '100',
|
||||||
|
error: false,
|
||||||
|
height: '1080',
|
||||||
|
sourceAudioCodec: 'dca',
|
||||||
|
sourceVideoCodec: 'h264',
|
||||||
|
timeStamp: '1679349635.2791338',
|
||||||
|
transcodeHwRequested: false,
|
||||||
|
videoCodec: 'h264',
|
||||||
|
videoDecision: 'copy',
|
||||||
|
width: '1920',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
27
src/tools/shared/math/percentage.tool.test.ts
Normal file
27
src/tools/shared/math/percentage.tool.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { percentage } from './percentage.tool';
|
||||||
|
|
||||||
|
describe('percentage', () => {
|
||||||
|
it.concurrent('be fixed value', () => {
|
||||||
|
// arrange
|
||||||
|
const value = 62;
|
||||||
|
|
||||||
|
// act
|
||||||
|
const fixedPercentage = percentage(value, 100);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(fixedPercentage).toBe('62.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.concurrent('be fixed value when decimal places', () => {
|
||||||
|
// arrange
|
||||||
|
const value = 42.69696969;
|
||||||
|
|
||||||
|
// act
|
||||||
|
const fixedPercentage = percentage(value, 100);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(fixedPercentage).toBe('42.7');
|
||||||
|
});
|
||||||
|
});
|
||||||
47
src/tools/shared/time/date.tool.test.ts
Normal file
47
src/tools/shared/time/date.tool.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { isToday } from './date.tool';
|
||||||
|
|
||||||
|
describe('isToday', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.concurrent('should return true if date is today', () => {
|
||||||
|
// arrange
|
||||||
|
const date = new Date(2022, 3, 17);
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const today = isToday(date);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(today).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.concurrent("should return true if date is today and time doesn't match", () => {
|
||||||
|
// arrange
|
||||||
|
vi.setSystemTime(new Date(2022, 3, 17, 16, 25, 11));
|
||||||
|
|
||||||
|
// act
|
||||||
|
const today = isToday(new Date(2022, 3, 17));
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(today).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.concurrent("should be false if date doesn't match", () => {
|
||||||
|
// arrange
|
||||||
|
vi.setSystemTime(new Date(2022, 3, 17, 16));
|
||||||
|
|
||||||
|
// act
|
||||||
|
const today = isToday(new Date(2022, 3, 15));
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(today).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
src/tools/shared/time/stopwatch.tool.test.ts
Normal file
26
src/tools/shared/time/stopwatch.tool.test.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { Stopwatch } from './stopwatch.tool';
|
||||||
|
|
||||||
|
describe('stopwatch', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.concurrent('should be elapsed time between start and current', () => {
|
||||||
|
// arrange
|
||||||
|
vi.setSystemTime(new Date(2023, 2, 26, 0, 0, 0));
|
||||||
|
const stopwatch = new Stopwatch();
|
||||||
|
|
||||||
|
// act
|
||||||
|
vi.setSystemTime(new Date(2023, 2, 26, 0, 0, 2));
|
||||||
|
const milliseconds = stopwatch.getEllapsedMilliseconds();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(milliseconds).toBe(2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MantineTheme } from '@mantine/core';
|
import { MantineTheme } from '@mantine/core';
|
||||||
|
|
||||||
import { OptionValues } from '../modules/ModuleTypes';
|
import { OptionValues } from '../modules/ModuleTypes';
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
@@ -74,6 +75,12 @@ export type ServiceType =
|
|||||||
| 'Sabnzbd'
|
| 'Sabnzbd'
|
||||||
| 'NZBGet';
|
| 'NZBGet';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* @param name the name to match
|
||||||
|
* @param form the form
|
||||||
|
* @returns the port from the map
|
||||||
|
*/
|
||||||
export function tryMatchPort(name: string | undefined, form?: any) {
|
export function tryMatchPort(name: string | undefined, form?: any) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Indicator, IndicatorProps, Popover, useMantineTheme } from '@mantine/core';
|
import { Box, Indicator, IndicatorProps, Popover } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { isToday } from '../../tools/isToday';
|
import { isToday } from '../../tools/shared/time/date.tool';
|
||||||
import { MediaList } from './MediaList';
|
import { MediaList } from './MediaList';
|
||||||
import { MediasType } from './type';
|
import { MediasType } from './type';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { i18n } from 'next-i18next';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { useColorTheme } from '../../tools/color';
|
import { useColorTheme } from '../../tools/color';
|
||||||
import { isToday } from '../../tools/isToday';
|
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { CalendarDay } from './CalendarDay';
|
import { CalendarDay } from './CalendarDay';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import axios from 'axios';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { bytes } from '../../tools/bytesHelper';
|
import { bytes } from '../../tools/bytesHelper';
|
||||||
import { percentage } from '../../tools/percentage';
|
import { percentage } from '../../tools/shared/math/percentage.tool';
|
||||||
import { DashDotInfo } from './DashDotCompactNetwork';
|
import { DashDotInfo } from './DashDotCompactNetwork';
|
||||||
|
|
||||||
interface DashDotCompactStorageProps {
|
interface DashDotCompactStorageProps {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import { IconFileDownload } from '@tabler/icons';
|
import { IconFileDownload } from '@tabler/icons';
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
import {
|
import { Badge, Button, Group, Select, Stack, Tabs, Text, Title } from '@mantine/core';
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Select,
|
|
||||||
Stack,
|
|
||||||
Tabs,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconFileDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons';
|
import { IconFileDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -17,6 +7,7 @@ import dayjs from 'dayjs';
|
|||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
|
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
|
||||||
import {
|
import {
|
||||||
useGetUsenetInfo,
|
useGetUsenetInfo,
|
||||||
usePauseUsenetQueue,
|
usePauseUsenetQueue,
|
||||||
@@ -28,7 +19,6 @@ import { defineWidget } from '../helper';
|
|||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { UsenetHistoryList } from './UsenetHistoryList';
|
import { UsenetHistoryList } from './UsenetHistoryList';
|
||||||
import { UsenetQueueList } from './UsenetQueueList';
|
import { UsenetQueueList } from './UsenetQueueList';
|
||||||
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
|||||||
8
tests/setupVitest.ts
Normal file
8
tests/setupVitest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//setupVitest.js or similar file
|
||||||
|
import createFetchMock from 'vitest-fetch-mock';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
const fetchMocker = createFetchMock(vi);
|
||||||
|
|
||||||
|
// sets globalThis.fetch and globalThis.fetchMock to our mocked version
|
||||||
|
fetchMocker.enableMocks();
|
||||||
18
vitest.config.ts
Normal file
18
vitest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: 'happy-dom',
|
||||||
|
coverage: {
|
||||||
|
provider: 'c8',
|
||||||
|
reporter: ['html'],
|
||||||
|
all: true,
|
||||||
|
exclude: ['.next/', '.yarn/', 'data/'],
|
||||||
|
},
|
||||||
|
setupFiles: ['./tests/setupVitest.ts'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user