Skip to content

Commit d304dbe

Browse files
authored
Merge pull request #427 from bballdavis/vite-migration
Vite migration
2 parents db334b7 + f8d01a5 commit d304dbe

38 files changed

+7877
-14876
lines changed

.env.dev.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Vite development environment variables
2+
# Backend port for proxy configuration (matches docker-compose.dev.yml mapping)
3+
VITE_BACKEND_PORT=3087

.eslintrc.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,30 @@ module.exports = {
1111
extends: "plugin:react/recommended",
1212
overrides: [
1313
{
14+
// Backend test files
1415
files: ["server/**/__tests__/**/*.js", "server/**/*.test.js"],
1516
env: {
1617
jest: true,
1718
node: true,
1819
},
1920
},
21+
{
22+
// Frontend test files
23+
files: ["client/src/**/__tests__/**/*", "client/src/**/*.test.*"],
24+
extends: ["plugin:testing-library/react"],
25+
env: {
26+
jest: true,
27+
},
28+
},
2029
],
2130
parser: "@typescript-eslint/parser",
2231
parserOptions: {
2332
ecmaVersion: "latest",
2433
},
25-
plugins: ["react", "@typescript-eslint"],
26-
rules: {},
34+
plugins: ["react", "react-hooks", "@typescript-eslint", "testing-library"],
35+
rules: {
36+
"react/react-in-jsx-scope": "off",
37+
"react/no-unescaped-entities": "off",
38+
"react/prop-types": "off",
39+
},
2740
};

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ database
1515
server/images
1616

1717
.env
18+
.env.dev
1819

1920
# Local dev exclusion
2021
ffmpeg.exe

client/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
<meta name="Youtarr" content="Youtube media management for Plex" />
9+
<link rel="apple-touch-icon" href="/logo192.png" />
10+
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
11+
<title>Youtarr</title>
12+
</head>
13+
<body>
14+
<noscript>You need to enable JavaScript to run this app.</noscript>
15+
<div id="root"></div>
16+
<script type="module" src="/src/index.tsx"></script>
17+
</body>
18+
</html>

client/jest.config.cjs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module.exports = {
2+
testEnvironment: 'jsdom',
3+
testEnvironmentOptions: {
4+
url: 'http://localhost/',
5+
},
6+
roots: ['<rootDir>/src'],
7+
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
8+
resetMocks: true,
9+
moduleNameMapper: {
10+
'^src/(.*)$': '<rootDir>/src/$1',
11+
'\\.(css|less|scss|sass)$': 'jest-transform-stub',
12+
'\\.(png|jpg|jpeg|gif|svg|webp|mp4|mp3|wav|woff2?|ttf|eot)$': 'jest-transform-stub',
13+
},
14+
transform: {
15+
'^.+\\.(t|j)sx?$': [
16+
'@swc/jest',
17+
{
18+
jsc: {
19+
parser: {
20+
syntax: 'typescript',
21+
tsx: true,
22+
decorators: true,
23+
},
24+
target: 'es2022',
25+
transform: {
26+
legacyDecorator: true,
27+
decoratorMetadata: true,
28+
react: {
29+
runtime: 'automatic',
30+
},
31+
optimizer: {
32+
globals: {
33+
vars: {
34+
'import.meta.env.DEV': 'true',
35+
'import.meta.env.MODE': '"test"',
36+
},
37+
},
38+
},
39+
},
40+
},
41+
module: {
42+
type: 'commonjs',
43+
},
44+
},
45+
],
46+
},
47+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts', '<rootDir>/src/setupTests.ts'],
48+
transformIgnorePatterns: ['/node_modules/(?!(@mui|@emotion)\\/)/'],
49+
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/', '<rootDir>/storybook-static/'],
50+
};

client/jest.setup.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import '@testing-library/jest-dom';
2+
3+
// Node < 18 fallback (avoid overriding when native fetch exists)
4+
if (typeof globalThis.fetch === 'undefined') {
5+
globalThis.fetch = jest.fn() as unknown as typeof fetch;
6+
}
7+
8+
// Minimal Request polyfill for tests that do `instanceof Request`
9+
if (typeof globalThis.Request === 'undefined') {
10+
globalThis.Request = class Request {
11+
url: string;
12+
constructor(input: string | { url: string }) {
13+
this.url = typeof input === 'string' ? input : input.url;
14+
}
15+
} as any;
16+
}
17+
18+
// Some libs (and JSDOM) expect these to exist
19+
import { TextDecoder, TextEncoder } from 'util';
20+
import { _testLocationHelpers } from './src/utils/location';
21+
22+
if (typeof globalThis.TextEncoder === 'undefined') {
23+
globalThis.TextEncoder = TextEncoder as any;
24+
}
25+
26+
if (typeof globalThis.TextDecoder === 'undefined') {
27+
globalThis.TextDecoder = TextDecoder as any;
28+
}
29+
30+
type LocationMocks = {
31+
assign: jest.Mock<void, [string]>;
32+
reload: jest.Mock<void, []>;
33+
replace: jest.Mock<void, [string]>;
34+
};
35+
36+
type LocationOverrides = {
37+
href: string;
38+
origin: string;
39+
protocol: string;
40+
host: string;
41+
hostname: string;
42+
port: string;
43+
pathname: string;
44+
search: string;
45+
};
46+
47+
const setMockLocation = (url: string): LocationMocks => {
48+
const parsed = new URL(url, 'http://localhost/');
49+
50+
const overrides: LocationOverrides = {
51+
href: parsed.href,
52+
origin: parsed.origin,
53+
protocol: parsed.protocol,
54+
host: parsed.host,
55+
hostname: parsed.hostname,
56+
port: parsed.port,
57+
pathname: parsed.pathname,
58+
search: parsed.search,
59+
};
60+
61+
_testLocationHelpers.setOverrides(overrides);
62+
63+
// Use existing mocks if they already exist to prevent losing references in tests
64+
const existingMocks = _testLocationHelpers.getMocks();
65+
if (existingMocks) {
66+
return existingMocks as unknown as LocationMocks;
67+
}
68+
69+
const mocks: LocationMocks = {
70+
assign: jest.fn((nextUrl: string) => {
71+
setMockLocation(new URL(nextUrl, _testLocationHelpers.getOverrides()?.href || 'http://localhost/').href);
72+
}),
73+
replace: jest.fn((nextUrl: string) => {
74+
setMockLocation(new URL(nextUrl, _testLocationHelpers.getOverrides()?.href || 'http://localhost/').href);
75+
}),
76+
reload: jest.fn(),
77+
};
78+
79+
_testLocationHelpers.setMocks(mocks as any);
80+
81+
return mocks;
82+
};
83+
84+
declare global {
85+
var setMockLocation: (url: string) => LocationMocks;
86+
}
87+
88+
// @ts-ignore
89+
globalThis.setMockLocation = setMockLocation;
90+
91+
// Note: Direct window.location redefinition is blocked in JSDOM (Jest 30).
92+
// We use the locationUtils abstraction instead (src/utils/location.ts).
93+
94+
beforeEach(() => {
95+
_testLocationHelpers.setMocks(undefined);
96+
_testLocationHelpers.setOverrides(undefined);
97+
setMockLocation('http://localhost/');
98+
});
99+
100+
// Mock import.meta.env for source code that hasn't been transformed by SWC
101+
// We define it on globalThis as well to ensure it's picked up
102+
Object.defineProperty(globalThis, 'importMetaEnv', {
103+
value: { DEV: true, MODE: 'test', VITE_BACKEND_PORT: '3011' },
104+
writable: false,
105+
});
106+
107+
// Polyfill for source code using import.meta.env directly
108+
// @ts-ignore
109+
if (typeof global.importMeta === 'undefined') {
110+
// @ts-ignore
111+
global.importMeta = { env: { DEV: true, MODE: 'test' } };
112+
}

0 commit comments

Comments
 (0)